diff --git a/Xcodes/AcknowledgementsGenerator/Package.swift b/Xcodes/AcknowledgementsGenerator/Package.swift index c41837e..98623d2 100644 --- a/Xcodes/AcknowledgementsGenerator/Package.swift +++ b/Xcodes/AcknowledgementsGenerator/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.4 import PackageDescription diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index a60a91c..b339520 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -88,7 +88,7 @@ extension AppState { private func getXcodeArchive(_ installationType: InstallationType, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> { switch installationType { case .version(let availableXcode): - if let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.isEquivalent(to: availableXcode.version) }) { + if let installedXcode = Current.files.installedXcodes(Path.installDirectory).first(where: { $0.version.isEquivalent(to: availableXcode.version) }) { return Fail(error: InstallationError.versionAlreadyInstalled(installedXcode)) .eraseToAnyPublisher() } @@ -178,7 +178,7 @@ extension AppState { public func installArchivedXcode(_ availableXcode: AvailableXcode, at archiveURL: URL) -> AnyPublisher { do { - let destinationURL = Path.root.join("Applications").join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url + let destinationURL = Path.installDirectory.join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url switch archiveURL.pathExtension { case "xip": return unarchiveAndMoveXIP(availableXcode: availableXcode, at: archiveURL, to: destinationURL) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index db190ca..412bd59 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -21,7 +21,7 @@ class AppState: ObservableObject { } updateAllXcodes( availableXcodes: newValue, - installedXcodes: Current.files.installedXcodes(Path.root/"Applications"), + installedXcodes: Current.files.installedXcodes(Path.installDirectory), selectedXcodePath: selectedXcodePath ) } @@ -34,7 +34,7 @@ class AppState: ObservableObject { willSet { updateAllXcodes( availableXcodes: availableXcodes, - installedXcodes: Current.files.installedXcodes(Path.root/"Applications"), + installedXcodes: Current.files.installedXcodes(Path.installDirectory), selectedXcodePath: newValue ) } @@ -495,6 +495,41 @@ class AppState: ObservableObject { NSPasteboard.general.writeObjects([installedXcodePath.url as NSURL]) NSPasteboard.general.setString(installedXcodePath.string, forType: .string) } + + func createSymbolicLink(xcode: Xcode) { + guard let installedXcodePath = xcode.installedPath else { return } + + let destinationPath: Path = Path.installDirectory/"Xcode.app" + + // does an Xcode.app file exist? + if FileManager.default.fileExists(atPath: destinationPath.string) { + do { + // if it's not a symlink, error because we don't want to delete an actual xcode.app file + let attributes: [FileAttributeKey : Any]? = try? FileManager.default.attributesOfItem(atPath: destinationPath.string) + + if attributes?[.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink { + try FileManager.default.removeItem(atPath: destinationPath.string) + Logger.appState.info("Successfully deleted old symlink") + } else { + let message = "Xcode.app exists and is not a symbolic link" + self.presentedAlert = .generic(title: "Unable to create symbolic Link", message: message) + return + } + } catch { + self.presentedAlert = .generic(title: "Unable to create symbolic Link", message: error.localizedDescription) + } + } + + do { + try FileManager.default.createSymbolicLink(atPath: destinationPath.string, withDestinationPath: installedXcodePath.string) + Logger.appState.info("Successfully created symbolic link with Xcode.app") + } catch { + Logger.appState.error("Unable to create symbolic Link") + self.error = error + self.presentedAlert = .generic(title: "Unable to create symbolic Link", message: error.legibleLocalizedDescription) + } + + } func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) { var adjustedAvailableXcodes = availableXcodes diff --git a/Xcodes/Backend/Path+.swift b/Xcodes/Backend/Path+.swift index 1aecb83..76eb29e 100644 --- a/Xcodes/Backend/Path+.swift +++ b/Xcodes/Backend/Path+.swift @@ -16,4 +16,8 @@ extension Path { static var cacheFile: Path { return xcodesApplicationSupport/"available-xcodes.json" } + + static var installDirectory: Path { + return Path.root/"Applications" + } } diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index 2e904ac..37d35f0 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -18,6 +18,7 @@ struct XcodeCommands: Commands { OpenCommand() RevealCommand() CopyPathCommand() + CreateSymbolicLinkCommand() Divider() @@ -153,6 +154,23 @@ struct CopyPathButton: View { } } +struct CreateSymbolicLinkButton: View { + @EnvironmentObject var appState: AppState + let xcode: Xcode? + + var body: some View { + Button(action: createSymbolicLink) { + Text("Create SymLink as Xcode.app") + } + .help("Create SymLink as Xcode.app") + } + + private func createSymbolicLink() { + guard let xcode = xcode else { return } + appState.createSymbolicLink(xcode: xcode) + } +} + // MARK: - Commands struct InstallCommand: View { @@ -225,3 +243,15 @@ struct UninstallCommand: View { .disabled(selectedXcode.unwrapped?.installState.installed != true) } } + +struct CreateSymbolicLinkCommand: View { + @EnvironmentObject var appState: AppState + @FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode? + + var body: some View { + CreateSymbolicLinkButton(xcode: selectedXcode.unwrapped) + .keyboardShortcut("s", modifiers: [.command, .option]) + .disabled(selectedXcode.unwrapped?.installState.installed != true) + } +} + diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift index 5d88a70..65bdc12 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift @@ -53,6 +53,7 @@ struct XcodeListViewRow: View { OpenButton(xcode: xcode) RevealButton(xcode: xcode) CopyPathButton(xcode: xcode) + CreateSymbolicLinkButton(xcode: xcode) Divider() UninstallButton(xcode: xcode)