diff --git a/Documentation/SwiftlyDocs.docc/automated-install.md b/Documentation/SwiftlyDocs.docc/automated-install.md index c698a744..ceebc2d7 100644 --- a/Documentation/SwiftlyDocs.docc/automated-install.md +++ b/Documentation/SwiftlyDocs.docc/automated-install.md @@ -4,18 +4,26 @@ Swiftly can be installed automatically in places like build/CI systems. This guide will help you to script to the installation of swiftly and toolchains so that it can be unattended. We assume that you have working understanding of your build system. The examples are based on a typical Unix environment. -First, download a swiftly binary from a trusted source, such as your artifact repository, or a well-known website for the operating system (e.g. Linux) and processor architecture (e.g. arm64, or x86_64). Here's an example using the popular curl command. +First, download the swiftly binary from swift.org for your operating system (e.g. Linux) and processor architecture (e.g. arm64, or x86_64). Here's an example using the popular curl command. ``` -curl -L > swiftly +curl -L > swiftly.tar.gz +tar zxf swiftly.tar.gz +``` + +On macOS you can download the pkg file and extract it like this from the command-line: + +``` +curl -L > swiftly.pkg +installer -pkg swiftly.pkg -target CurrentUserHomeDirectory ``` > Tip: If you are using Linux you will need the "ca-certificates" package for the root certificate authorities that will establish the trust that swiftly needs to make API requests that it needs. This package is frequently pre-installed on end-user environments, but may not be present in more minimal installations. -Once swiftly is downloaded you can run the init subcommand to finish the installation. This command will use the default initialization options and proceed without prompting. +Once swiftly is downloaded you can run the init subcommand to finish the installation. This command will print verbose outputs, assume yes for all prompts, and skip the automatic installation of the latest swift toolchain: ``` -./swiftly init --assume-yes +./swiftly init --verbose --assume-yes --skip-install # the swiftly binary is extracted to ~/local/bin/swiftly on macOS ``` Swiftly is installed, but the current shell may not yet be updated with the new environment variables, such as the PATH. The init command prints instructions on how to update the current shell environment without opening a new shell. This is an example of the output taken from Linux, but the details might be different for other OSes, username, or shell. diff --git a/Documentation/SwiftlyDocs.docc/getting-started.md b/Documentation/SwiftlyDocs.docc/getting-started.md index 422f7852..930d4c6a 100644 --- a/Documentation/SwiftlyDocs.docc/getting-started.md +++ b/Documentation/SwiftlyDocs.docc/getting-started.md @@ -1,31 +1,46 @@ # Getting Started with Swiftly -To download swiftly and install Swift, run the following in your terminal, then follow the on-screen instructions: +Start using swiftly and swift. -``` -curl -L https://swiftlang.github.io/swiftly/swiftly-install.sh | bash -``` +To get started with swiftly you can download it from [swift.org](https://swift.org/download), and extract the package. -Alternatively, you can download the swiftly binary and install itself like this: +@TabNavigator { + @Tab("Linux") { + If you are using Linux then you can verify and extract the archive like this: -``` -swiftly init -``` + ``` + sha256sum swiftly-x.y.z.tar.gz # Check that the hash matches what's reported on swift.org + tar zxf swiftly-x.y.z.tar.gz + ``` -Once swiftly is installed you can use it to install the latest available swift toolchain like this: + Now run swiftly init to finish the installation: -``` -$ swiftly install latest + ``` + ./swiftly init + ``` + } -Fetching the latest stable Swift release... -Installing Swift 5.8.1 -Downloaded 488.5 MiB of 488.5 MiB -Extracting toolchain... -Swift 5.8.1 installed successfully! + @Tab("macOS") { + On macOS you can either run the pkg installer from the command-line like this or just run the package by double-clicking on it (not recommended): + ``` + installer -pkg swift-x.y.z.pkg -target CurrentUserHomeDirectory + ``` + + Now run swiftly init to finish the installation: + + ``` + $HOME/usr/local/bin/swiftly init + ``` + } +} + +Swiftly will install itself and download the latest available Swift toolchain. Follow the prompts for any additional steps. Once everything is done you can begin using swift. + +``` $ swift --version -Swift version 5.8.1 (swift-5.8.1-RELEASE) +Swift version 6.0.1 (swift-6.0.1-RELEASE) Target: x86_64-unknown-linux-gnu $ swift build # Build with the latest (5.8.1) toolchain diff --git a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md index 827a1ecc..4e988fdf 100644 --- a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md +++ b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md @@ -23,7 +23,7 @@ swiftly [--version] [--help] Install a new toolchain. ``` -swiftly install [] [--use] [--verify|no-verify] [--post-install-file=] [--assume-yes] [--version] [--help] +swiftly install [] [--use] [--verify|no-verify] [--post-install-file=] [--assume-yes] [--verbose] [--version] [--help] ``` **version:** @@ -81,6 +81,11 @@ written to this file as commands that can be run after the installation. *Disable confirmation prompts by assuming 'yes'* +**--verbose:** + +*Enable verbose reporting from swiftly* + + **--version:** *Show the version.* @@ -143,7 +148,7 @@ Note that listing available snapshots before the latest release (major and minor Set the in-use toolchain. If no toolchain is provided, print the currently in-use toolchain, if any. ``` -swiftly use [--print-location] [--global-default] [--assume-yes] [] [--version] [--help] +swiftly use [--print-location] [--global-default] [--assume-yes] [--verbose] [] [--version] [--help] ``` **--print-location:** @@ -161,6 +166,11 @@ swiftly use [--print-location] [--global-default] [--assume-yes] [] [ *Disable confirmation prompts by assuming 'yes'* +**--verbose:** + +*Enable verbose reporting from swiftly* + + **toolchain:** *The toolchain to use.* @@ -210,7 +220,7 @@ Likewise, the latest snapshot associated with a given development branch can be Remove an installed toolchain. ``` -swiftly uninstall [--assume-yes] [--version] [--help] +swiftly uninstall [--assume-yes] [--verbose] [--version] [--help] ``` **toolchain:** @@ -249,6 +259,11 @@ Finally, all installed toolchains can be uninstalled by specifying 'all': *Disable confirmation prompts by assuming 'yes'* +**--verbose:** + +*Enable verbose reporting from swiftly* + + **--version:** *Show the version.* @@ -309,7 +324,7 @@ The installed snapshots for a given devlopment branch can be listed by specifyin Update an installed toolchain to a newer version. ``` -swiftly update [] [--assume-yes] [--verify|no-verify] [--post-install-file=] [--version] [--help] +swiftly update [] [--assume-yes] [--verbose] [--verify|no-verify] [--post-install-file=] [--version] [--help] ``` **toolchain:** @@ -355,6 +370,11 @@ A specific snapshot toolchain can be updated by including the date: *Disable confirmation prompts by assuming 'yes'* +**--verbose:** + +*Enable verbose reporting from swiftly* + + **--verify|no-verify:** *Verify the toolchain's PGP signature before proceeding with installation.* @@ -385,7 +405,7 @@ written to this file as commands that can be run after the installation. Perform swiftly initialization into your user account. ``` -swiftly init [--no-modify-profile] [--overwrite] [--platform=] [--assume-yes] [--version] [--help] +swiftly init [--no-modify-profile] [--overwrite] [--platform=] [--skip-install] [--assume-yes] [--verbose] [--version] [--help] ``` **--no-modify-profile:** @@ -400,7 +420,12 @@ swiftly init [--no-modify-profile] [--overwrite] [--platform=] [--assu **--platform=\:** -*Specify the current Linux platform for swiftly.* +*Specify the current Linux platform for swiftly* + + +**--skip-install:** + +*Skip installing the latest toolchain* **--assume-yes:** @@ -408,6 +433,11 @@ swiftly init [--no-modify-profile] [--overwrite] [--platform=] [--assu *Disable confirmation prompts by assuming 'yes'* +**--verbose:** + +*Enable verbose reporting from swiftly* + + **--version:** *Show the version.* @@ -425,9 +455,19 @@ swiftly init [--no-modify-profile] [--overwrite] [--platform=] [--assu Update the version of swiftly itself. ``` -swiftly self-update [--version] [--help] +swiftly self-update [--assume-yes] [--verbose] [--version] [--help] ``` +**--assume-yes:** + +*Disable confirmation prompts by assuming 'yes'* + + +**--verbose:** + +*Enable verbose reporting from swiftly* + + **--version:** *Show the version.* diff --git a/README.md b/README.md index 10f301bb..1c58d06a 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,8 @@ Ongoing maintenance and stewardship of this project is led by the [SSWG](https:/ ### Installation -To download swiftly and install Swift, run the following in your terminal, then follow the on-screen instructions. -``` -curl -L https://swiftlang.github.io/swiftly/swiftly-install.sh | bash -``` +Download the swiftly package from [swift.org](https://swift.org/download) and it can install itself with init: -Alternatively, you can download the swiftly binary and it can install itself: ``` swiftly init ``` diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index 3934ea54..824786f2 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -327,7 +327,7 @@ public struct Linux: Platform { } } - public func install(from tmpFile: URL, version: ToolchainVersion) throws { + public func install(from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws { guard tmpFile.fileExists() else { throw Error(message: "\(tmpFile) doesn't exist") } @@ -348,7 +348,14 @@ public struct Linux: Platform { let relativePath = name.drop { c in c != "/" }.dropFirst() // prepend /path/to/swiftlyHomeDir/toolchains/ to each file name - return toolchainDir.appendingPathComponent(String(relativePath)) + let destination = toolchainDir.appendingPathComponent(String(relativePath)) + + if verbose { + SwiftlyCore.print("\(destination.path)") + } + + // prepend /path/to/swiftlyHomeDir/toolchains/ to each file name + return destination } } @@ -390,7 +397,7 @@ public struct Linux: Platform { FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") } - public func verifySignature(httpClient: SwiftlyHTTPClient, archiveDownloadURL: URL, archive: URL) async throws { + public func verifySignature(httpClient: SwiftlyHTTPClient, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws { SwiftlyCore.print("Downloading toolchain signature...") let sigFile = self.getTempFilePath() let _ = FileManager.default.createFile(atPath: sigFile.path, contents: nil) @@ -405,7 +412,7 @@ public struct Linux: Platform { SwiftlyCore.print("Verifying toolchain signature...") do { - try self.runProgram("gpg", "--verify", sigFile.path, archive.path) + try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose) } catch { throw Error(message: "Signature verification failed: \(error).") } diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index 761e0666..b0ad224f 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -49,7 +49,7 @@ public struct MacOS: Platform { nil } - public func install(from tmpFile: URL, version: ToolchainVersion) throws { + public func install(from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws { guard tmpFile.fileExists() else { throw Error(message: "\(tmpFile) doesn't exist") } @@ -60,23 +60,26 @@ public struct MacOS: Platform { if SwiftlyCore.mockedHomeDir == nil { SwiftlyCore.print("Installing package in user home directory...") - try runProgram("installer", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory") + 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. + SwiftlyCore.print("Expanding pkg...") let tmpDir = self.getTempFilePath() let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(version.identifier).xctoolchain", isDirectory: true) if !toolchainDir.fileExists() { try FileManager.default.createDirectory(at: toolchainDir, withIntermediateDirectories: false) } - try runProgram("pkgutil", "--expand", tmpFile.path, tmpDir.path) + 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. var payload = tmpDir.appendingPathComponent("Payload") if !payload.fileExists() { payload = tmpDir.appendingPathComponent("\(version.identifier)-osx-package.pkg/Payload") } - try runProgram("tar", "-C", toolchainDir.path, "-xf", payload.path) + + SwiftlyCore.print("Untarring pkg Payload...") + try runProgram("tar", "-C", toolchainDir.path, "-xvf", payload.path, quiet: !verbose) } } @@ -146,7 +149,7 @@ public struct MacOS: Platform { FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID()).pkg") } - public func verifySignature(httpClient _: SwiftlyHTTPClient, archiveDownloadURL _: URL, archive _: URL) async throws { + public func verifySignature(httpClient _: SwiftlyHTTPClient, archiveDownloadURL _: URL, archive _: URL, verbose _: Bool) async throws { // No signature verification is required on macOS since the pkg files have their own signing // mechanism and the swift.org downloadables are trusted by stock macOS installations. } diff --git a/Sources/Swiftly/Init.swift b/Sources/Swiftly/Init.swift index 374984eb..8483ad41 100644 --- a/Sources/Swiftly/Init.swift +++ b/Sources/Swiftly/Init.swift @@ -14,39 +14,53 @@ internal struct Init: SwiftlyCommand { installation is found, the swiftly executable will be updated, but the rest of the installation will not be modified. """) var overwrite: Bool = false - @Option(name: .long, help: "Specify the current Linux platform for swiftly.") + @Option(name: .long, help: "Specify the current Linux platform for swiftly") var platform: String? + @Flag(help: "Skip installing the latest toolchain") + var skipInstall: Bool = false @OptionGroup var root: GlobalOptions + private enum CodingKeys: String, CodingKey { + case noModifyProfile, overwrite, platform, skipInstall, root + } + public mutating func validate() throws {} internal mutating func run() async throws { - try await Self.execute(assumeYes: self.root.assumeYes, noModifyProfile: self.noModifyProfile, overwrite: self.overwrite, platform: self.platform) + try await Self.execute(assumeYes: self.root.assumeYes, noModifyProfile: self.noModifyProfile, overwrite: self.overwrite, platform: self.platform, verbose: self.root.verbose, skipInstall: self.skipInstall) } /// Initialize the installation of swiftly. - internal static func execute(assumeYes: Bool, noModifyProfile: Bool, overwrite: Bool, platform: String?) async throws { + internal static func execute(assumeYes: Bool, noModifyProfile: Bool, overwrite: Bool, platform: String?, verbose: Bool, skipInstall: Bool) async throws { try Swiftly.currentPlatform.verifySwiftlySystemPrerequisites() - let config = try? Config.load() + var config = try? Config.load() - if let config = config, !overwrite && config.version != SwiftlyCore.version { + if let config, !overwrite && config.version != SwiftlyCore.version { // We don't support downgrades, and we don't yet support upgrades throw Error(message: "An existing swiftly installation was detected. You can try again with '--overwrite' to overwrite it.") } // Give the user the prompt and the choice to abort at this point. if !assumeYes { +#if os(Linux) + let sigMsg = " In the process of installing the new toolchain swiftly will add swift.org GnuPG keys into your keychain to verify the integrity of the downloads." +#else + let sigMsg = "" +#endif + let installMsg = if !skipInstall { + "\nOnce swiftly is installed it will install the latest available swift toolchain.\(sigMsg)\n" + } else { "" } + SwiftlyCore.print(""" Swiftly will be installed into the following locations: \(Swiftly.currentPlatform.swiftlyHomeDir.path) - Data and configuration files directory including toolchains \(Swiftly.currentPlatform.swiftlyBinDir.path) - Executables installation directory - Note that the locations can be changed with SWIFTLY_HOME and SWIFTLY_BIN environment variables and run - this again. - + These locations can be changed with SWIFTLY_HOME and SWIFTLY_BIN environment variables and run this again. + \(installMsg) """) if SwiftlyCore.readLine(prompt: "Proceed with the installation? [Y/n] ") == "n" { @@ -54,6 +68,7 @@ internal struct Init: SwiftlyCommand { } } + // Ensure swiftly doesn't overwrite any existing executables without getting confirmation first. let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir let swiftlyBinDirContents = (try? FileManager.default.contentsOfDirectory(atPath: swiftlyBinDir.path)) ?? [String]() let willBeOverwritten = Set(["swiftly"]).intersection(swiftlyBinDirContents) @@ -114,32 +129,17 @@ internal struct Init: SwiftlyCommand { // Force the configuration to be present. Generate it if it doesn't already exist or overwrite is set if overwrite || config == nil { let pd = try await Swiftly.currentPlatform.detectPlatform(disableConfirmation: assumeYes, platform: platform) - var config = Config(inUse: nil, installedToolchains: [], platform: pd) + var c = Config(inUse: nil, installedToolchains: [], platform: pd) // Stamp the current version of swiftly on this config - config.version = SwiftlyCore.version - try config.save() + c.version = SwiftlyCore.version + try c.save() + config = c } - let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir.appendingPathComponent("swiftly", isDirectory: false) - - let cmd = URL(fileURLWithPath: CommandLine.arguments[0]) - let systemManagedSwiftlyBin = try Swiftly.currentPlatform.systemManagedBinary(CommandLine.arguments[0]) - - // Don't move the binary if it's already in the right place, this is being invoked inside an xctest, or it is a system managed binary - if cmd != swiftlyBin && !cmd.path.hasSuffix("xctest") && systemManagedSwiftlyBin == nil { - SwiftlyCore.print("Moving swiftly into the installation directory...") - - if swiftlyBin.fileExists() { - try FileManager.default.removeItem(at: swiftlyBin) - } + guard var config else { throw Error(message: "Configuration could not be set") } - do { - try FileManager.default.moveItem(at: cmd, to: swiftlyBin) - } catch { - try FileManager.default.copyItem(at: cmd, to: swiftlyBin) - SwiftlyCore.print("Swiftly has been copied into the installation directory. You can remove '\(cmd.path)'. It is no longer needed.") - } - } + // Move our executable over to the correct place + try Swiftly.currentPlatform.installSwiftlyBin() if overwrite || !FileManager.default.fileExists(atPath: envFile.path) { SwiftlyCore.print("Creating shell environment file for the user...") @@ -208,6 +208,14 @@ 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) @@ -217,6 +225,26 @@ internal struct Init: SwiftlyCommand { """) } + + if pathChanged { + 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 run this command to update your shell to get these items. + + hash -r + + """) + } + + 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/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index 489b1366..7de678fa 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -99,6 +99,7 @@ struct Install: SwiftlyCommand { &config, useInstalledToolchain: self.use, verifySignature: self.verify, + verbose: self.root.verbose, assumeYes: self.root.assumeYes ) @@ -116,7 +117,7 @@ struct Install: SwiftlyCommand { guard let postInstallFile = self.postInstallFile else { throw Error(message: """ - There are some system dependencies that should be installed before using this toolchain. + 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: @@ -133,6 +134,7 @@ struct Install: SwiftlyCommand { _ config: inout Config, useInstalledToolchain: Bool, verifySignature: Bool, + verbose: Bool, assumeYes: Bool ) async throws -> (postInstall: String?, pathChanged: Bool) { guard !config.installedToolchains.contains(version) else { @@ -232,30 +234,23 @@ struct Install: SwiftlyCommand { try await Swiftly.currentPlatform.verifySignature( httpClient: SwiftlyCore.httpClient, archiveDownloadURL: url, - archive: tmpFile + archive: tmpFile, + verbose: verbose ) } - try Swiftly.currentPlatform.install(from: tmpFile, version: version) + try Swiftly.currentPlatform.install(from: tmpFile, version: version, verbose: verbose) var pathChanged = false - // Don't create the proxies in the tests - if CommandLine.arguments.count > 0 && !CommandLine.arguments[0].hasSuffix("xctest") { + // Create proxies if we have a location where we can point them + if let proxyTo = try? Swiftly.currentPlatform.findSwiftlyBin() { // Ensure swiftly doesn't overwrite any existing executables without getting confirmation first. - let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir.appendingPathComponent("swiftly", isDirectory: false) - let systemManagedSwiftlyBin = try Swiftly.currentPlatform.systemManagedBinary(CommandLine.arguments[0]) let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir let swiftlyBinDirContents = (try? FileManager.default.contentsOfDirectory(atPath: swiftlyBinDir.path)) ?? [String]() let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(version) let toolchainBinDirContents = try FileManager.default.contentsOfDirectory(atPath: toolchainBinDir.path) - let proxyTo = if let systemManagedSwiftlyBin = systemManagedSwiftlyBin { - systemManagedSwiftlyBin - } else { - swiftlyBin.path - } - let existingProxies = swiftlyBinDirContents.filter { bin in do { let linkTarget = try FileManager.default.destinationOfSymbolicLink(atPath: swiftlyBinDir.appendingPathComponent(bin).path) diff --git a/Sources/Swiftly/Proxy.swift b/Sources/Swiftly/Proxy.swift index aca9440d..72b642c2 100644 --- a/Sources/Swiftly/Proxy.swift +++ b/Sources/Swiftly/Proxy.swift @@ -11,9 +11,36 @@ public enum Proxy { } guard binName != "swiftly" else { - // Treat this as a swiftly invocation - await Swiftly.main() - return + // Treat this as a swiftly invocation, but first check that we are installed, bootstrapping + // the installation process if we aren't. + let configResult = Result { try Config.load() } + + switch configResult { + case .success: + await Swiftly.main() + return + case let .failure(err): + guard CommandLine.arguments.count > 0 else { fatalError("argv is not set") } + + if CommandLine.arguments.count == 1 { + // User ran swiftly with no extra arguments in an uninstalled environment, so we jump directly into + // an simple init. + try await Init.execute(assumeYes: false, noModifyProfile: false, overwrite: false, platform: nil, verbose: false, skipInstall: false) + return + } else if CommandLine.arguments.count >= 2 && CommandLine.arguments[1] == "init" { + // Let the user run the init command with their arguments, if any. + await Swiftly.main() + return + } else if CommandLine.arguments.count == 2 && (CommandLine.arguments[1] == "--help" || CommandLine.arguments[1] == "--experimental-dump-help") { + // Allow the showing of help information + await Swiftly.main() + return + } else { + // We've been invoked outside the "init" subcommand and we're not yet configured. + // This will throw if the configuration couldn't be loaded and give the user an actionable message. + throw err + } + } } var config = try Config.load() diff --git a/Sources/Swiftly/SelfUpdate.swift b/Sources/Swiftly/SelfUpdate.swift index b8534f48..e41dea60 100644 --- a/Sources/Swiftly/SelfUpdate.swift +++ b/Sources/Swiftly/SelfUpdate.swift @@ -10,7 +10,11 @@ internal struct SelfUpdate: SwiftlyCommand { abstract: "Update the version of swiftly itself." ) - private enum CodingKeys: CodingKey {} + @OptionGroup var root: GlobalOptions + + private enum CodingKeys: String, CodingKey { + case root + } internal mutating func run() async throws { try validateSwiftly() @@ -20,10 +24,10 @@ internal struct SelfUpdate: SwiftlyCommand { throw Error(message: "Self update doesn't work when swiftly has been installed externally. Please keep it updated from the source where you installed it in the first place.") } - let _ = try await Self.execute() + let _ = try await Self.execute(verbose: self.root.verbose) } - public static func execute() async throws -> SwiftlyVersion { + public static func execute(verbose: Bool) async throws -> SwiftlyVersion { SwiftlyCore.print("Checking for swiftly updates...") let swiftlyRelease = try await SwiftlyCore.httpClient.getSwiftlyRelease() @@ -91,7 +95,7 @@ internal struct SelfUpdate: SwiftlyCommand { } animation.complete(success: true) - try await Swiftly.currentPlatform.verifySignature(httpClient: SwiftlyCore.httpClient, archiveDownloadURL: downloadURL, archive: tmpFile) + try await Swiftly.currentPlatform.verifySignature(httpClient: SwiftlyCore.httpClient, archiveDownloadURL: downloadURL, archive: tmpFile, verbose: verbose) try Swiftly.currentPlatform.extractSwiftlyAndInstall(from: tmpFile) SwiftlyCore.print("Successfully updated swiftly to \(version) (was \(SwiftlyCore.version))") diff --git a/Sources/Swiftly/Swiftly.swift b/Sources/Swiftly/Swiftly.swift index 1652c9a2..e0badf85 100644 --- a/Sources/Swiftly/Swiftly.swift +++ b/Sources/Swiftly/Swiftly.swift @@ -11,6 +11,9 @@ public struct GlobalOptions: ParsableArguments { @Flag(name: [.customShort("y"), .long], help: "Disable confirmation prompts by assuming 'yes'") var assumeYes: Bool = false + @Flag(help: "Enable verbose reporting from swiftly") + var verbose: Bool = false + public init() {} } diff --git a/Sources/Swiftly/Update.swift b/Sources/Swiftly/Update.swift index 43e56b55..98540e5e 100644 --- a/Sources/Swiftly/Update.swift +++ b/Sources/Swiftly/Update.swift @@ -113,6 +113,7 @@ struct Update: SwiftlyCommand { &config, useInstalledToolchain: config.inUse == parameters.oldToolchain, verifySignature: self.verify, + verbose: self.root.verbose, assumeYes: self.root.assumeYes ) diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index cad42f9b..a82b4ad2 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -64,7 +64,7 @@ public protocol Platform { /// Installs a toolchain from a file on disk pointed to by the given URL. /// After this completes, a user can “use” the toolchain. - func install(from: URL, version: ToolchainVersion) throws + func install(from: URL, version: ToolchainVersion, verbose: Bool) throws /// Extract swiftly from the provided downloaded archive and install /// ourselves from that. @@ -98,8 +98,7 @@ public protocol Platform { /// Downloads the signature file associated with the archive and verifies it matches the downloaded archive. /// Throws an error if the signature does not match. - /// On Linux, signature verification will be skipped if gpg is not installed. - func verifySignature(httpClient: SwiftlyHTTPClient, archiveDownloadURL: URL, archive: URL) async throws + func verifySignature(httpClient: SwiftlyHTTPClient, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws /// Detect the platform definition for this platform. func detectPlatform(disableConfirmation: Bool, platform: String?) async throws -> PlatformDefinition @@ -268,32 +267,98 @@ extension Platform { } } - public func systemManagedBinary(_ cmd: String) throws -> String? { + // Install ourselves in the final location + public func installSwiftlyBin() throws { + // First, let's find out where we are. + let cmd = CommandLine.arguments[0] + let cmdAbsolute = if cmd.hasPrefix("/") { + cmd + } else { + ([FileManager.default.currentDirectoryPath] + (ProcessInfo.processInfo.environment["PATH"]?.components(separatedBy: ":") ?? [])).map { + $0 + "/" + cmd + }.filter { + FileManager.default.fileExists(atPath: $0) + }.first + } + + // We couldn't find ourselves in the usual places. Assume that no installation is necessary + // since we were most likely invoked at SWIFTLY_BIN_DIR already. + guard let cmdAbsolute else { + return + } + + // Proceed to installation only if we're in the user home directory, or a non-system location. let userHome = FileManager.default.homeDirectoryForCurrentUser - let binLocs = [cmd] + ProcessInfo.processInfo.environment["PATH"]!.components(separatedBy: ":").map { $0 + "/" + cmd } - var bin: String? - for binLoc in binLocs { - if FileManager.default.fileExists(atPath: binLoc) { - bin = binLoc - break - } + guard cmdAbsolute.hasPrefix(userHome.path + "/") || + (!cmdAbsolute.hasPrefix("/usr/") && !cmdAbsolute.hasPrefix("/opt/") && !cmdAbsolute.hasPrefix("/bin/")) + else { + return + } + + // Proceed only if we're not running in the context of a test. + guard !cmdAbsolute.hasSuffix("xctest") else { + return + } + + // We're already running from where we would be installing ourselves. + guard case let swiftlyHomeBin = self.swiftlyBinDir.appendingPathComponent("swiftly", isDirectory: false).path, cmdAbsolute != swiftlyHomeBin else { + return + } + + SwiftlyCore.print("Installing swiftly in \(swiftlyHomeBin)...") + + if FileManager.default.fileExists(atPath: swiftlyHomeBin) { + try FileManager.default.removeItem(atPath: swiftlyHomeBin) } - guard let bin = bin else { - throw Error(message: "Could not locate source of \(cmd) binary in either the PATH, relative, or absolute path") + + do { + try FileManager.default.moveItem(atPath: cmdAbsolute, toPath: swiftlyHomeBin) + } catch { + try FileManager.default.copyItem(atPath: cmdAbsolute, toPath: swiftlyHomeBin) + SwiftlyCore.print("Swiftly has been copied into the installation directory. You can remove '\(cmdAbsolute)'. It is no longer needed.") + } + } + + // Find the location where swiftly should be executed. + public func findSwiftlyBin() throws -> String? { + let swiftlyHomeBin = self.swiftlyBinDir.appendingPathComponent("swiftly", isDirectory: false).path + + // First, let's find out where we are. + let cmd = CommandLine.arguments[0] + let cmdAbsolute = if cmd.hasPrefix("/") { + cmd + } else { + ([FileManager.default.currentDirectoryPath] + (ProcessInfo.processInfo.environment["PATH"]?.components(separatedBy: ":") ?? [])).map { + $0 + "/" + cmd + }.filter { + FileManager.default.fileExists(atPath: $0) + }.first } - // If the binary is in the user's home directory, or is not in system locations ("/usr", "/opt", "/bin") - // then it is expected to be outside of a system package location and we manage the binary ourselves. - if bin.hasPrefix(userHome.path + "/") || (!bin.hasPrefix("/usr") && !bin.hasPrefix("/opt") && !bin.hasPrefix("/bin")) { + // We couldn't find ourselves in the usual places, so if we're not going to be installing + // swiftly then we can assume that we are running from the final location. + if cmdAbsolute == nil && FileManager.default.fileExists(atPath: swiftlyHomeBin) { + return swiftlyHomeBin + } + + // If we are system managed then we know where swiftly should be. + let userHome = FileManager.default.homeDirectoryForCurrentUser + if let cmdAbsolute, !cmdAbsolute.hasPrefix(userHome.path + "/") && (cmdAbsolute.hasPrefix("/usr/") || cmdAbsolute.hasPrefix("/opt/") || cmdAbsolute.hasPrefix("/bin/")) { + return cmdAbsolute + } + + // If we're running inside an xctest then we don't have a location for this swiftly. + guard let cmdAbsolute, !cmdAbsolute.hasSuffix("xctest") else { return nil } - return bin + return FileManager.default.fileExists(atPath: swiftlyHomeBin) ? swiftlyHomeBin : nil } public func findToolchainBinDir(_ toolchain: ToolchainVersion) -> URL { self.findToolchainLocation(toolchain).appendingPathComponent("usr/bin") } + #endif } diff --git a/Tests/SwiftlyTests/InitTests.swift b/Tests/SwiftlyTests/InitTests.swift index b7d8c726..c4169d99 100644 --- a/Tests/SwiftlyTests/InitTests.swift +++ b/Tests/SwiftlyTests/InitTests.swift @@ -27,7 +27,7 @@ final class InitTests: SwiftlyTests { } // WHEN: swiftly is invoked to init the user account and finish swiftly installation - var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes"]) + var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) try await initCmd.run() // THEN: it creates a valid configuration at the correct version @@ -67,7 +67,7 @@ final class InitTests: SwiftlyTests { // GIVEN: a user account with swiftly already installed try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile) - var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes"]) + var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) try await initCmd.run() // Add some customizations to files and directories @@ -79,7 +79,7 @@ final class InitTests: SwiftlyTests { try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir.appendingPathComponent("foo.txt")) // WHEN: swiftly is initialized with overwrite enabled - initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--overwrite"]) + initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install", "--overwrite"]) try await initCmd.run() // THEN: everything is overwritten in initialization @@ -95,7 +95,7 @@ final class InitTests: SwiftlyTests { // GIVEN: a user account with swiftly already installed try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile) - var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes"]) + var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) try await initCmd.run() // Add some customizations to files and directories @@ -107,7 +107,7 @@ final class InitTests: SwiftlyTests { try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir.appendingPathComponent("foo.txt")) // WHEN: swiftly init is invoked a second time - initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes"]) + initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) var threw = false do { try await initCmd.run() diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index 01e5841c..2eff8c25 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -22,7 +22,7 @@ final class PlatformTests: SwiftlyTests { // GIVEN: a toolchain has been downloaded var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.7.1") // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version) + try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchain is extracted in the toolchains directory var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) XCTAssertEqual(1, toolchains.count) @@ -30,7 +30,7 @@ final class PlatformTests: SwiftlyTests { // GIVEN: a second toolchain has been downloaded (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version) + try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchain is added to the toolchains directory toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) XCTAssertEqual(2, toolchains.count) @@ -38,7 +38,7 @@ final class PlatformTests: SwiftlyTests { // GIVEN: an identical toolchain has been downloaded (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version) + try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchains directory remains the same toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) XCTAssertEqual(2, toolchains.count) @@ -49,9 +49,9 @@ final class PlatformTests: SwiftlyTests { try await self.rollbackLocalChanges { // GIVEN: toolchains have been downloaded, and installed var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version) + try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.6.3") - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version) + try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) // WHEN: one of the toolchains is uninstalled try Swiftly.currentPlatform.uninstall(version) // THEN: there is only one remaining toolchain installed diff --git a/Tests/SwiftlyTests/RunTests.swift b/Tests/SwiftlyTests/RunTests.swift index 47337fd0..38a33a39 100644 --- a/Tests/SwiftlyTests/RunTests.swift +++ b/Tests/SwiftlyTests/RunTests.swift @@ -43,7 +43,7 @@ final class RunTests: SwiftlyTests { try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { // The toolchains directory should be the fist entry on the path var run = try self.parseCommand(Run.self, ["run", try await Swiftly.currentPlatform.getShell(), "-c", "echo $PATH"]) - var output = try await run.runWithMockedIO() + let output = try await run.runWithMockedIO() XCTAssert(output.count == 1) XCTAssert(output[0].contains(Swiftly.currentPlatform.swiftlyToolchainsDir.path)) } diff --git a/Tests/SwiftlyTests/SelfUpdateTests.swift b/Tests/SwiftlyTests/SelfUpdateTests.swift index 1adb49a2..628ee9b8 100644 --- a/Tests/SwiftlyTests/SelfUpdateTests.swift +++ b/Tests/SwiftlyTests/SelfUpdateTests.swift @@ -21,7 +21,7 @@ final class SelfUpdateTests: SwiftlyTests { func runSelfUpdateTest(latestVersion: SwiftlyVersion) async throws { try await self.withTestHome { try await self.withMockedSwiftlyVersion(latestSwiftlyVersion: latestVersion) { - let updatedVersion = try await SelfUpdate.execute() + let updatedVersion = try await SelfUpdate.execute(verbose: true) XCTAssertEqual(latestVersion, updatedVersion) } }