diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index ce1abeb9..f5d86a80 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -37,7 +37,9 @@ public struct Linux: Platform { } public var swiftlyToolchainsDir: URL { - self.swiftlyHomeDir.appendingPathComponent("toolchains", isDirectory: true) + SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("toolchains", isDirectory: true) } + ?? ProcessInfo.processInfo.environment["SWIFTLY_TOOLCHAINS_DIR"].map { URL(fileURLWithPath: $0) } + ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".local/share/swiftly/toolchains") } public var toolchainFileExtension: String { diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index fa4b075d..1e67ac39 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -18,6 +18,11 @@ public struct MacOS: Platform { .appendingPathComponent(".swiftly", isDirectory: true) } + public var defaultToolchainsDirectory: URL { + FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Developer/Toolchains", isDirectory: true) + } + public var swiftlyBinDir: URL { SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) } ?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { URL(fileURLWithPath: $0) } @@ -27,8 +32,9 @@ public struct MacOS: Platform { public var swiftlyToolchainsDir: URL { SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("Toolchains", isDirectory: true) } - // The toolchains are always installed here by the installer. We bypass the installer in the case of test mocks - ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Developer/Toolchains", isDirectory: true) + ?? ProcessInfo.processInfo.environment["SWIFTLY_TOOLCHAINS_DIR"].map { URL(fileURLWithPath: $0) } + // This is where the installer will put the toolchains, and where Xcode can find them + ?? self.defaultToolchainsDirectory } public var toolchainFileExtension: String { @@ -54,22 +60,45 @@ public struct MacOS: Platform { throw SwiftlyError(message: "\(tmpFile) doesn't exist") } - if !self.swiftlyToolchainsDir.fileExists() { - try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir, withIntermediateDirectories: false) + let toolchainsDir = self.swiftlyToolchainsDir + + if !toolchainsDir.fileExists() { + try FileManager.default.createDirectory( + at: toolchainsDir, withIntermediateDirectories: true + ) } - if SwiftlyCore.mockedHomeDir == nil { + if toolchainsDir == self.defaultToolchainsDirectory { + // If the toolchains go into the default user location then we use the installer to install them SwiftlyCore.print("Installing package in user home directory...") - try runProgram("installer", "-verbose", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory", quiet: !verbose) + try runProgram( + "installer", "-verbose", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory", + quiet: !verbose + ) } else { - // In the case of a mock for testing purposes we won't use the installer, perferring a manual process because - // the installer will not install to an arbitrary path, only a volume or user home directory. + // Otherwise, we extract the pkg into the requested toolchains directory. SwiftlyCore.print("Expanding pkg...") let tmpDir = self.getTempFilePath() - let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(version.identifier).xctoolchain", isDirectory: true) + let toolchainDir = toolchainsDir.appendingPathComponent( + "\(version.identifier).xctoolchain", isDirectory: true + ) + if !toolchainDir.fileExists() { try FileManager.default.createDirectory(at: toolchainDir, withIntermediateDirectories: false) } + + SwiftlyCore.print("Checking package signature...") + do { + try runProgram("pkgutil", "--check-signature", tmpFile.path, quiet: !verbose) + } catch { + // If this is not a test that uses mocked toolchains then we must throw this error and abort installation + guard SwiftlyCore.mockedHomeDir != nil else { + throw error + } + + // We permit the signature verification to fail during testing + SwiftlyCore.print("Signature verification failed, which is allowable during testing with mocked toolchains") + } try runProgram("pkgutil", "--verbose", "--expand", tmpFile.path, tmpDir.path, quiet: !verbose) // There's a slight difference in the location of the special Payload file between official swift packages // and the ones that are mocked here in the test framework. diff --git a/Sources/Swiftly/Init.swift b/Sources/Swiftly/Init.swift index 160e3138..e0c063fd 100644 --- a/Sources/Swiftly/Init.swift +++ b/Sources/Swiftly/Init.swift @@ -65,6 +65,8 @@ internal struct Init: SwiftlyCommand { // Give the user the prompt and the choice to abort at this point. if !assumeYes { + let toolchainsDir = Swiftly.currentPlatform.swiftlyToolchainsDir + var msg = """ Welcome to swiftly, the Swift toolchain manager for Linux and macOS! @@ -76,12 +78,20 @@ internal struct Init: SwiftlyCommand { \(Swiftly.currentPlatform.swiftlyHomeDir.path) - Directory for configuration files \(Swiftly.currentPlatform.swiftlyBinDir.path) - Links to the binaries of the active toolchain - \(Swiftly.currentPlatform.swiftlyToolchainsDir.path) - Directory hosting installed toolchains + \(toolchainsDir.path) - Directory hosting installed toolchains These locations can be changed by setting the environment variables - SWIFTLY_HOME_DIR and SWIFTLY_BIN_DIR before running 'swiftly init' again. + SWIFTLY_HOME_DIR, SWIFTLY_BIN_DIR, and SWIFTLY_TOOLCHAINS_DIR before running 'swiftly init' again. """ +#if os(macOS) + if toolchainsDir != FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Developer/Toolchains", isDirectory: true) { + msg += """ + + NOTE: The toolchains are not being installed in a standard macOS location, so Xcode may not be able to find them. + """ + } +#endif if !skipInstall { msg += """ @@ -177,6 +187,7 @@ internal struct Init: SwiftlyCommand { env = """ set -x SWIFTLY_HOME_DIR "\(Swiftly.currentPlatform.swiftlyHomeDir.path)" set -x SWIFTLY_BIN_DIR "\(Swiftly.currentPlatform.swiftlyBinDir.path)" + set -x SWIFTLY_TOOLCHAINS_DIR "\(Swiftly.currentPlatform.swiftlyToolchainsDir.path)" if not contains "$SWIFTLY_BIN_DIR" $PATH set -x PATH "$SWIFTLY_BIN_DIR" $PATH end @@ -186,6 +197,7 @@ internal struct Init: SwiftlyCommand { env = """ export SWIFTLY_HOME_DIR="\(Swiftly.currentPlatform.swiftlyHomeDir.path)" export SWIFTLY_BIN_DIR="\(Swiftly.currentPlatform.swiftlyBinDir.path)" + export SWIFTLY_TOOLCHAINS_DIR="\(Swiftly.currentPlatform.swiftlyToolchainsDir.path)" if [[ ":$PATH:" != *":$SWIFTLY_BIN_DIR:"* ]]; then export PATH="$SWIFTLY_BIN_DIR:$PATH" fi @@ -241,50 +253,50 @@ internal struct Init: SwiftlyCommand { addEnvToProfile = true } - var postInstall: String? - var pathChanged = false - - if !skipInstall { - let latestVersion = try await Install.resolve(config: config, selector: ToolchainSelector.latest) - (postInstall, pathChanged) = try await Install.execute(version: latestVersion, &config, useInstalledToolchain: true, verifySignature: true, verbose: verbose, assumeYes: assumeYes) - } - if addEnvToProfile { try Data(sourceLine.utf8).append(to: profileHome) + } + } - if !quietShellFollowup { - SwiftlyCore.print(""" - To begin using installed swiftly from your current shell, first run the following command: - \(sourceLine) + var postInstall: String? + var pathChanged = false - """) - } - } + if !skipInstall { + let latestVersion = try await Install.resolve(config: config, selector: ToolchainSelector.latest) + (postInstall, pathChanged) = try await Install.execute(version: latestVersion, &config, useInstalledToolchain: true, verifySignature: true, verbose: verbose, assumeYes: assumeYes) + } + + if !quietShellFollowup { + SwiftlyCore.print(""" + To begin using installed swiftly from your current shell, first run the following command: + \(sourceLine) - // Fish doesn't have path caching, so this might only be needed for bash/zsh - if pathChanged && !quietShellFollowup && !shell.hasSuffix("fish") { - SwiftlyCore.print(""" - Your shell caches items on your path for better performance. Swiftly has added - items to your path that may not get picked up right away. You can update your - shell's environment by running + """) + } - hash -r + // Fish doesn't have path caching, so this might only be needed for bash/zsh + if pathChanged && !quietShellFollowup && !shell.hasSuffix("fish") { + SwiftlyCore.print(""" + Your shell caches items on your path for better performance. Swiftly has added + items to your path that may not get picked up right away. You can update your + shell's environment by running - or restarting your shell. + hash -r - """) - } + or restarting your shell. - if let postInstall { - SwiftlyCore.print(""" - There are some dependencies that should be installed before using this toolchain. - You can run the following script as the system administrator (e.g. root) to prepare - your system: + """) + } - \(postInstall) + if let postInstall { + SwiftlyCore.print(""" + There are some dependencies that should be installed before using this toolchain. + You can run the following script as the system administrator (e.g. root) to prepare + your system: - """) - } + \(postInstall) + + """) } } } diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index 2d863241..b608bc68 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -144,7 +144,10 @@ extension Platform { let tcPath = self.findToolchainLocation(toolchain).appendingPathComponent("usr/bin") guard tcPath.fileExists() else { - throw SwiftlyError(message: "Toolchain \(toolchain) could not be located. You can try `swiftly uninstall \(toolchain)` to uninstall it and then `swiftly install \(toolchain)` to install it again.") + throw SwiftlyError( + message: + "Toolchain \(toolchain) could not be located in \(tcPath). You can try `swiftly uninstall \(toolchain)` to uninstall it and then `swiftly install \(toolchain)` to install it again." + ) } var pathComponents = (newEnv["PATH"] ?? "").split(separator: ":").map { String($0) }