diff --git a/Package.swift b/Package.swift index 5415c7fa..acb0a783 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription diff --git a/Plugins/GenerateDocsReference/GenerateDocsReferenceError.swift b/Plugins/GenerateDocsReference/GenerateDocsReferenceError.swift index fb0bd5d0..5e4b44fa 100644 --- a/Plugins/GenerateDocsReference/GenerateDocsReferenceError.swift +++ b/Plugins/GenerateDocsReference/GenerateDocsReferenceError.swift @@ -1,6 +1,7 @@ import Foundation -import PackagePlugin +@preconcurrency import PackagePlugin +@preconcurrency enum GenerateDocsReferencePluginError: Error { case unknownBuildConfiguration(String) case buildFailed(String) diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index b2796ff3..682bc23b 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -334,7 +334,7 @@ public struct Linux: Platform { public func install( _ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool - ) throws { + ) async throws { guard tmpFile.fileExists() else { throw SwiftlyError(message: "\(tmpFile) doesn't exist") } @@ -345,7 +345,7 @@ public struct Linux: Platform { ) } - ctx.print("Extracting toolchain...") + await ctx.print("Extracting toolchain...") let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(version.name) if toolchainDir.fileExists() { @@ -360,7 +360,10 @@ public struct Linux: Platform { let destination = toolchainDir.appendingPathComponent(String(relativePath)) if verbose { - ctx.print("\(destination.path)") + // To avoid having to make extractArchive async this is a regular print + // to stdout. Note that it is unlikely that the test mocking will require + // capturing this output. + print("\(destination.path)") } // prepend /path/to/swiftlyHomeDir/toolchains/ to each file name @@ -368,7 +371,7 @@ public struct Linux: Platform { } } - public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) throws { + public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) async throws { guard archive.fileExists() else { throw SwiftlyError(message: "\(archive) doesn't exist") } @@ -379,7 +382,7 @@ public struct Linux: Platform { } try FileManager.default.createDirectory(atPath: tmpDir.path, withIntermediateDirectories: true) - ctx.print("Extracting new swiftly...") + await ctx.print("Extracting new swiftly...") try extractArchive(atPath: archive) { name in // Extract to the temporary directory tmpDir.appendingPathComponent(String(name)) @@ -409,7 +412,7 @@ public struct Linux: Platform { _ ctx: SwiftlyCoreContext, toolchainFile: ToolchainFile, archive: URL, verbose: Bool ) async throws { if verbose { - ctx.print("Downloading toolchain signature...") + await ctx.print("Downloading toolchain signature...") } let sigFile = self.getTempFilePath() @@ -420,7 +423,7 @@ public struct Linux: Platform { try await ctx.httpClient.getSwiftToolchainFileSignature(toolchainFile).download(to: sigFile) - ctx.print("Verifying toolchain signature...") + await ctx.print("Verifying toolchain signature...") do { if let mockedHomeDir = ctx.mockedHomeDir { try self.runProgram( @@ -439,7 +442,7 @@ public struct Linux: Platform { _ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: URL, verbose: Bool ) async throws { if verbose { - ctx.print("Downloading swiftly signature...") + await ctx.print("Downloading swiftly signature...") } let sigFile = self.getTempFilePath() @@ -452,7 +455,7 @@ public struct Linux: Platform { url: archiveDownloadURL.appendingPathExtension("sig") ).download(to: sigFile) - ctx.print("Verifying swiftly signature...") + await ctx.print("Verifying swiftly signature...") do { if let mockedHomeDir = ctx.mockedHomeDir { try self.runProgram( @@ -492,7 +495,7 @@ public struct Linux: Platform { """) let choice = - ctx.readLine(prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") + await ctx.readLine(prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") ?? "0" guard let choiceNum = Int(choice) else { diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index 02407df4..d6b73825 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -51,7 +51,7 @@ public struct MacOS: Platform { public func install( _ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool - ) throws { + ) async throws { guard tmpFile.fileExists() else { throw SwiftlyError(message: "\(tmpFile) doesn't exist") } @@ -63,7 +63,7 @@ public struct MacOS: Platform { } if ctx.mockedHomeDir == nil { - ctx.print("Installing package in user home directory...") + await ctx.print("Installing package in user home directory...") try runProgram( "installer", "-verbose", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory", quiet: !verbose @@ -71,7 +71,7 @@ public struct MacOS: Platform { } 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. - ctx.print("Expanding pkg...") + await ctx.print("Expanding pkg...") let tmpDir = self.getTempFilePath() let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent( "\(version.identifier).xctoolchain", isDirectory: true @@ -89,12 +89,12 @@ public struct MacOS: Platform { payload = tmpDir.appendingPathComponent("\(version.identifier)-osx-package.pkg/Payload") } - ctx.print("Untarring pkg Payload...") + await ctx.print("Untarring pkg Payload...") try runProgram("tar", "-C", toolchainDir.path, "-xvf", payload.path, quiet: !verbose) } } - public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) throws { + public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) async throws { guard archive.fileExists() else { throw SwiftlyError(message: "\(archive) doesn't exist") } @@ -104,7 +104,7 @@ public struct MacOS: Platform { if ctx.mockedHomeDir == nil { homeDir = FileManager.default.homeDirectoryForCurrentUser - ctx.print("Extracting the swiftly package...") + await ctx.print("Extracting the swiftly package...") try runProgram("installer", "-pkg", archive.path, "-target", "CurrentUserHomeDirectory") try? runProgram("pkgutil", "--volume", homeDir.path, "--forget", "org.swift.swiftly") } else { @@ -127,7 +127,7 @@ public struct MacOS: Platform { throw SwiftlyError(message: "Payload file could not be found at \(tmpDir).") } - ctx.print("Extracting the swiftly package into \(installDir.path)...") + await ctx.print("Extracting the swiftly package into \(installDir.path)...") try runProgram("tar", "-C", installDir.path, "-xvf", payload.path, quiet: false) } @@ -135,9 +135,9 @@ public struct MacOS: Platform { } public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose: Bool) - throws + async throws { - ctx.print("Uninstalling package in user home directory...") + await ctx.print("Uninstalling package in user home directory...") let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent( "\(toolchain.identifier).xctoolchain", isDirectory: true diff --git a/Sources/Swiftly/Init.swift b/Sources/Swiftly/Init.swift index dc18113d..6afc047b 100644 --- a/Sources/Swiftly/Init.swift +++ b/Sources/Swiftly/Init.swift @@ -3,7 +3,7 @@ import Foundation import SwiftlyCore struct Init: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Perform swiftly initialization into your user account." ) @@ -53,7 +53,7 @@ struct Init: SwiftlyCommand { // This is a simple upgrade from the 0.4.0 pre-releases, or 1.x // Move our executable over to the correct place - try Swiftly.currentPlatform.installSwiftlyBin(ctx) + try await Swiftly.currentPlatform.installSwiftlyBin(ctx) // Update and save the version config.version = SwiftlyCore.version @@ -113,9 +113,9 @@ struct Init: SwiftlyCommand { """ } - ctx.print(msg) + await ctx.print(msg) - guard ctx.promptForConfirmation(defaultBehavior: true) else { + guard await ctx.promptForConfirmation(defaultBehavior: true) else { throw SwiftlyError(message: "swiftly installation has been cancelled") } } @@ -177,10 +177,10 @@ struct Init: SwiftlyCommand { guard var config else { throw SwiftlyError(message: "Configuration could not be set") } // Move our executable over to the correct place - try Swiftly.currentPlatform.installSwiftlyBin(ctx) + try await Swiftly.currentPlatform.installSwiftlyBin(ctx) if overwrite || !FileManager.default.fileExists(atPath: envFile.path) { - ctx.print("Creating shell environment file for the user...") + await ctx.print("Creating shell environment file for the user...") var env = "" if shell.hasSuffix("fish") { env = """ @@ -206,7 +206,7 @@ struct Init: SwiftlyCommand { } if !noModifyProfile { - ctx.print("Updating profile...") + await ctx.print("Updating profile...") let userHome = ctx.mockedHomeDir ?? FileManager.default.homeDirectoryForCurrentUser @@ -262,7 +262,7 @@ struct Init: SwiftlyCommand { try Data(sourceLine.utf8).append(to: profileHome) if !quietShellFollowup { - ctx.print(""" + await ctx.print(""" To begin using installed swiftly from your current shell, first run the following command: \(sourceLine) @@ -272,7 +272,7 @@ struct Init: SwiftlyCommand { // Fish doesn't have path caching, so this might only be needed for bash/zsh if pathChanged && !quietShellFollowup && !shell.hasSuffix("fish") { - ctx.print(""" + await ctx.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 @@ -285,7 +285,7 @@ struct Init: SwiftlyCommand { } if let postInstall { - ctx.print(""" + await ctx.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: diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index 98fee2c2..ac056362 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -2,11 +2,11 @@ import _StringProcessing import ArgumentParser import Foundation import SwiftlyCore -import TSCBasic +@preconcurrency import TSCBasic import TSCUtility struct Install: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Install a new toolchain." ) @@ -129,7 +129,7 @@ struct Install: SwiftlyCommand { // Fish doesn't cache its path, so this instruction is not necessary. if pathChanged && !shell.hasSuffix("fish") { - ctx.print( + await ctx.print( """ NOTE: Swiftly has updated some elements in your path and your shell may not yet be aware of the changes. You can update your shell's environment by running @@ -170,7 +170,7 @@ struct Install: SwiftlyCommand { assumeYes: Bool ) async throws -> (postInstall: String?, pathChanged: Bool) { guard !config.installedToolchains.contains(version) else { - ctx.print("\(version) is already installed.") + await ctx.print("\(version) is already installed.") return (nil, false) } @@ -182,7 +182,7 @@ struct Install: SwiftlyCommand { requireSignatureValidation: verifySignature ) - ctx.print("Installing \(version)") + await ctx.print("Installing \(version)") let tmpFile = Swiftly.currentPlatform.getTempFilePath() FileManager.default.createFile(atPath: tmpFile.path, contents: nil) @@ -272,7 +272,7 @@ struct Install: SwiftlyCommand { ) } - try Swiftly.currentPlatform.install(ctx, from: tmpFile, version: version, verbose: verbose) + try await Swiftly.currentPlatform.install(ctx, from: tmpFile, version: version, verbose: verbose) var pathChanged = false @@ -297,19 +297,19 @@ struct Install: SwiftlyCommand { let overwrite = Set(toolchainBinDirContents).subtracting(existingProxies).intersection( swiftlyBinDirContents) if !overwrite.isEmpty && !assumeYes { - ctx.print("The following existing executables will be overwritten:") + await ctx.print("The following existing executables will be overwritten:") for executable in overwrite { - ctx.print(" \(swiftlyBinDir.appendingPathComponent(executable).path)") + await ctx.print(" \(swiftlyBinDir.appendingPathComponent(executable).path)") } - guard ctx.promptForConfirmation(defaultBehavior: false) else { + guard await ctx.promptForConfirmation(defaultBehavior: false) else { throw SwiftlyError(message: "Toolchain installation has been cancelled") } } if verbose { - ctx.print("Setting up toolchain proxies...") + await ctx.print("Setting up toolchain proxies...") } let proxiesToCreate = Set(toolchainBinDirContents).subtracting(swiftlyBinDirContents).union( @@ -346,10 +346,10 @@ struct Install: SwiftlyCommand { if config.inUse == nil { config.inUse = version try config.save(ctx) - ctx.print("The global default toolchain has been set to `\(version)`") + await ctx.print("The global default toolchain has been set to `\(version)`") } - ctx.print("\(version) installed successfully!") + await ctx.print("\(version) installed successfully!") return (postInstallScript, pathChanged) } @@ -359,7 +359,7 @@ struct Install: SwiftlyCommand { { switch selector { case .latest: - ctx.print("Fetching the latest stable Swift release...") + await ctx.print("Fetching the latest stable Swift release...") guard let release = try await ctx.httpClient.getReleaseToolchains( @@ -382,7 +382,7 @@ struct Install: SwiftlyCommand { return .stable(ToolchainVersion.StableRelease(major: major, minor: minor, patch: patch)) } - ctx.print("Fetching the latest stable Swift \(major).\(minor) release...") + await ctx.print("Fetching the latest stable Swift \(major).\(minor) release...") // If a patch was not provided, perform a lookup to get the latest patch release // of the provided major/minor version pair. let toolchain = try await ctx.httpClient.getReleaseToolchains( @@ -402,7 +402,7 @@ struct Install: SwiftlyCommand { return ToolchainVersion(snapshotBranch: branch, date: date) } - ctx.print("Fetching the latest \(branch) branch snapshot...") + await ctx.print("Fetching the latest \(branch) branch snapshot...") // If a date was not provided, perform a lookup to find the most recent snapshot // for the given branch. diff --git a/Sources/Swiftly/List.swift b/Sources/Swiftly/List.swift index 718f35ad..a3fd54ed 100644 --- a/Sources/Swiftly/List.swift +++ b/Sources/Swiftly/List.swift @@ -2,7 +2,7 @@ import ArgumentParser import SwiftlyCore struct List: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "List installed toolchains." ) @@ -56,7 +56,7 @@ struct List: SwiftlyCommand { if toolchain == config.inUse { message += " (default)" } - ctx.print(message) + await ctx.print(message) } if let selector { @@ -76,26 +76,26 @@ struct List: SwiftlyCommand { } let message = "Installed \(modifier) toolchains" - ctx.print(message) - ctx.print(String(repeating: "-", count: message.count)) + await ctx.print(message) + await ctx.print(String(repeating: "-", count: message.count)) for toolchain in toolchains { - printToolchain(toolchain) + await printToolchain(toolchain) } } else { - ctx.print("Installed release toolchains") - ctx.print("----------------------------") + await ctx.print("Installed release toolchains") + await ctx.print("----------------------------") for toolchain in toolchains { guard toolchain.isStableRelease() else { continue } - printToolchain(toolchain) + await printToolchain(toolchain) } - ctx.print("") - ctx.print("Installed snapshot toolchains") - ctx.print("-----------------------------") + await ctx.print("") + await ctx.print("Installed snapshot toolchains") + await ctx.print("-----------------------------") for toolchain in toolchains where toolchain.isSnapshot() { - printToolchain(toolchain) + await printToolchain(toolchain) } } } diff --git a/Sources/Swiftly/ListAvailable.swift b/Sources/Swiftly/ListAvailable.swift index 653ddc26..e8ba1f2f 100644 --- a/Sources/Swiftly/ListAvailable.swift +++ b/Sources/Swiftly/ListAvailable.swift @@ -2,7 +2,7 @@ import ArgumentParser import SwiftlyCore struct ListAvailable: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "List toolchains available for install." ) @@ -80,7 +80,7 @@ struct ListAvailable: SwiftlyCommand { } else if installedToolchains.contains(toolchain) { message += " (installed)" } - ctx.print(message) + await ctx.print(message) } if let selector { @@ -100,16 +100,16 @@ struct ListAvailable: SwiftlyCommand { } let message = "Available \(modifier) toolchains" - ctx.print(message) - ctx.print(String(repeating: "-", count: message.count)) + await ctx.print(message) + await ctx.print(String(repeating: "-", count: message.count)) for toolchain in toolchains { - printToolchain(toolchain) + await printToolchain(toolchain) } } else { print("Available release toolchains") print("----------------------------") for toolchain in toolchains where toolchain.isStableRelease() { - printToolchain(toolchain) + await printToolchain(toolchain) } } } diff --git a/Sources/Swiftly/Proxy.swift b/Sources/Swiftly/Proxy.swift index bc9ce3ed..37b710c6 100644 --- a/Sources/Swiftly/Proxy.swift +++ b/Sources/Swiftly/Proxy.swift @@ -68,10 +68,10 @@ public enum Proxy { } catch let terminated as RunProgramError { exit(terminated.exitCode) } catch let error as SwiftlyError { - ctx.print(error.message) + await ctx.print(error.message) exit(1) } catch { - ctx.print("\(error)") + await ctx.print("\(error)") exit(1) } } diff --git a/Sources/Swiftly/Run.swift b/Sources/Swiftly/Run.swift index 056617d9..3bbc1012 100644 --- a/Sources/Swiftly/Run.swift +++ b/Sources/Swiftly/Run.swift @@ -3,7 +3,7 @@ import Foundation import SwiftlyCore struct Run: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Run a command while proxying to the selected toolchain commands." ) @@ -97,7 +97,7 @@ struct Run: SwiftlyCommand { if let outputHandler = ctx.outputHandler { if let output = try await Swiftly.currentPlatform.proxyOutput(ctx, toolchain, command[0], [String](command[1...])) { for line in output.split(separator: "\n") { - outputHandler.handleOutputLine(String(line)) + await outputHandler.handleOutputLine(String(line)) } } return diff --git a/Sources/Swiftly/SelfUpdate.swift b/Sources/Swiftly/SelfUpdate.swift index e0a4f8c3..e0d8d7c2 100644 --- a/Sources/Swiftly/SelfUpdate.swift +++ b/Sources/Swiftly/SelfUpdate.swift @@ -1,11 +1,11 @@ import ArgumentParser import Foundation import SwiftlyCore -import TSCBasic +@preconcurrency import TSCBasic import TSCUtility struct SelfUpdate: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Update the version of swiftly itself." ) @@ -36,12 +36,12 @@ struct SelfUpdate: SwiftlyCommand { public static func execute(_ ctx: SwiftlyCoreContext, verbose: Bool) async throws -> SwiftlyVersion { - ctx.print("Checking for swiftly updates...") + await ctx.print("Checking for swiftly updates...") let swiftlyRelease = try await ctx.httpClient.getCurrentSwiftlyRelease() guard try swiftlyRelease.swiftlyVersion > SwiftlyCore.version else { - ctx.print("Already up to date.") + await ctx.print("Already up to date.") return SwiftlyCore.version } @@ -73,7 +73,7 @@ struct SelfUpdate: SwiftlyCommand { let version = try swiftlyRelease.swiftlyVersion - ctx.print("A new version is available: \(version)") + await ctx.print("A new version is available: \(version)") let tmpFile = Swiftly.currentPlatform.getTempFilePath() FileManager.default.createFile(atPath: tmpFile.path, contents: nil) @@ -109,9 +109,9 @@ struct SelfUpdate: SwiftlyCommand { try await Swiftly.currentPlatform.verifySwiftlySignature( ctx, archiveDownloadURL: downloadURL, archive: tmpFile, verbose: verbose ) - try Swiftly.currentPlatform.extractSwiftlyAndInstall(ctx, from: tmpFile) + try await Swiftly.currentPlatform.extractSwiftlyAndInstall(ctx, from: tmpFile) - ctx.print("Successfully updated swiftly to \(version) (was \(SwiftlyCore.version))") + await ctx.print("Successfully updated swiftly to \(version) (was \(SwiftlyCore.version))") return version } } diff --git a/Sources/Swiftly/Swiftly.swift b/Sources/Swiftly/Swiftly.swift index abdaacb6..dc4ecb82 100644 --- a/Sources/Swiftly/Swiftly.swift +++ b/Sources/Swiftly/Swiftly.swift @@ -18,7 +18,7 @@ public struct GlobalOptions: ParsableArguments { } public struct Swiftly: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "A utility for installing and managing Swift toolchains.", version: String(describing: SwiftlyCore.version), diff --git a/Sources/Swiftly/Uninstall.swift b/Sources/Swiftly/Uninstall.swift index 472eb568..e73dc213 100644 --- a/Sources/Swiftly/Uninstall.swift +++ b/Sources/Swiftly/Uninstall.swift @@ -2,7 +2,7 @@ import ArgumentParser import SwiftlyCore struct Uninstall: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Remove an installed toolchain." ) @@ -69,24 +69,24 @@ struct Uninstall: SwiftlyCommand { } guard !toolchains.isEmpty else { - ctx.print("No toolchains matched \"\(self.toolchain)\"") + await ctx.print("No toolchains matched \"\(self.toolchain)\"") return } if !self.root.assumeYes { - ctx.print("The following toolchains will be uninstalled:") + await ctx.print("The following toolchains will be uninstalled:") for toolchain in toolchains { - ctx.print(" \(toolchain)") + await ctx.print(" \(toolchain)") } - guard ctx.promptForConfirmation(defaultBehavior: true) else { - ctx.print("Aborting uninstall") + guard await ctx.promptForConfirmation(defaultBehavior: true) else { + await ctx.print("Aborting uninstall") return } } - ctx.print() + await ctx.print() for toolchain in toolchains { var config = try Config.load(ctx) @@ -120,12 +120,12 @@ struct Uninstall: SwiftlyCommand { try await Self.execute(ctx, toolchain, &config, verbose: self.root.verbose) } - ctx.print() - ctx.print("\(toolchains.count) toolchain(s) successfully uninstalled") + await ctx.print() + await ctx.print("\(toolchains.count) toolchain(s) successfully uninstalled") } static func execute(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ config: inout Config, verbose: Bool) async throws { - ctx.print("Uninstalling \(toolchain)...", terminator: "") + await ctx.print("Uninstalling \(toolchain)...", terminator: "") config.installedToolchains.remove(toolchain) // This is here to prevent the inUse from referencing a toolchain that is not installed if config.inUse == toolchain { @@ -133,7 +133,7 @@ struct Uninstall: SwiftlyCommand { } try config.save(ctx) - try Swiftly.currentPlatform.uninstall(ctx, toolchain, verbose: verbose) - ctx.print("done") + try await Swiftly.currentPlatform.uninstall(ctx, toolchain, verbose: verbose) + await ctx.print("done") } } diff --git a/Sources/Swiftly/Update.swift b/Sources/Swiftly/Update.swift index cb73efb3..60247555 100644 --- a/Sources/Swiftly/Update.swift +++ b/Sources/Swiftly/Update.swift @@ -3,7 +3,7 @@ import Foundation import SwiftlyCore struct Update: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Update an installed toolchain to a newer version." ) @@ -87,27 +87,27 @@ struct Update: SwiftlyCommand { guard let parameters = try await self.resolveUpdateParameters(ctx, &config) else { if let toolchain = self.toolchain { - ctx.print("No installed toolchain matched \"\(toolchain)\"") + await ctx.print("No installed toolchain matched \"\(toolchain)\"") } else { - ctx.print("No toolchains are currently installed") + await ctx.print("No toolchains are currently installed") } return } guard let newToolchain = try await self.lookupNewToolchain(ctx, config, parameters) else { - ctx.print("\(parameters.oldToolchain) is already up to date") + await ctx.print("\(parameters.oldToolchain) is already up to date") return } guard !config.installedToolchains.contains(newToolchain) else { - ctx.print("The newest version of \(parameters.oldToolchain) (\(newToolchain)) is already installed") + await ctx.print("The newest version of \(parameters.oldToolchain) (\(newToolchain)) is already installed") return } if !self.root.assumeYes { - ctx.print("Update \(parameters.oldToolchain) -> \(newToolchain)?") - guard ctx.promptForConfirmation(defaultBehavior: true) else { - ctx.print("Aborting") + await ctx.print("Update \(parameters.oldToolchain) -> \(newToolchain)?") + guard await ctx.promptForConfirmation(defaultBehavior: true) else { + await ctx.print("Aborting") return } } @@ -123,7 +123,7 @@ struct Update: SwiftlyCommand { ) try await Uninstall.execute(ctx, parameters.oldToolchain, &config, verbose: self.root.verbose) - ctx.print("Successfully updated \(parameters.oldToolchain) ⟶ \(newToolchain)") + await ctx.print("Successfully updated \(parameters.oldToolchain) ⟶ \(newToolchain)") if let postInstallScript { guard let postInstallFile = self.postInstallFile else { @@ -141,7 +141,7 @@ struct Update: SwiftlyCommand { } if pathChanged { - ctx.print(""" + await ctx.print(""" NOTE: Swiftly has updated some elements in your path and your shell may not yet be aware of the changes. You can update your shell's environment by running diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index 37bf6fd7..c5f146dc 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -3,7 +3,7 @@ import Foundation import SwiftlyCore struct Use: SwiftlyCommand { - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Set the in-use or default toolchain. If no toolchain is provided, print the currently in-use toolchain, if any." ) @@ -78,7 +78,7 @@ struct Use: SwiftlyCommand { if self.printLocation { // Print the toolchain location and exit - ctx.print("\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion).path)") + await ctx.print("\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion).path)") return } @@ -91,7 +91,7 @@ struct Use: SwiftlyCommand { message += " (default)" } - ctx.print(message) + await ctx.print(message) return } @@ -103,7 +103,7 @@ struct Use: SwiftlyCommand { let selector = try ToolchainSelector(parsing: toolchain) guard let toolchain = config.listInstalledToolchains(selector: selector).max() else { - ctx.print("No installed toolchains match \"\(toolchain)\"") + await ctx.print("No installed toolchains match \"\(toolchain)\"") return } @@ -123,10 +123,10 @@ struct Use: SwiftlyCommand { message = "The file `\(versionFile.path)` has been set to `\(toolchain)`" } else if let newVersionFile = findNewVersionFile(ctx), !globalDefault { if !assumeYes { - ctx.print("A new file `\(newVersionFile)` will be created to set the new in-use toolchain for this project. Alternatively, you can set your default globally with the `--global-default` flag. Proceed with creating this file?") + await ctx.print("A new file `\(newVersionFile)` will be created to set the new in-use toolchain for this project. Alternatively, you can set your default globally with the `--global-default` flag. Proceed with creating this file?") - guard ctx.promptForConfirmation(defaultBehavior: true) else { - ctx.print("Aborting setting in-use toolchain") + guard await ctx.promptForConfirmation(defaultBehavior: true) else { + await ctx.print("Aborting setting in-use toolchain") return } } @@ -144,7 +144,7 @@ struct Use: SwiftlyCommand { message += " (was \(selectedVersion.name))" } - ctx.print(message) + await ctx.print(message) } static func findNewVersionFile(_ ctx: SwiftlyCoreContext) -> URL? { @@ -168,7 +168,7 @@ struct Use: SwiftlyCommand { } } -public enum ToolchainSelectionResult { +public enum ToolchainSelectionResult: Sendable { case globalDefault case swiftVersionFile(URL, ToolchainSelector?, Error?) } diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 71ff5036..308e1700 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -72,7 +72,7 @@ public struct ToolchainFile: Sendable { } } -public protocol HTTPRequestExecutor { +public protocol HTTPRequestExecutor: Sendable { func getCurrentSwiftlyRelease() async throws -> SwiftlyWebsiteAPI.Components.Schemas.SwiftlyRelease func getReleaseToolchains() async throws -> [SwiftlyWebsiteAPI.Components.Schemas.Release] func getSnapshotToolchains( @@ -102,7 +102,7 @@ struct SwiftlyUserAgentMiddleware: ClientMiddleware { } /// An `HTTPRequestExecutor` backed by a shared `HTTPClient`. This makes actual network requests. -public class HTTPRequestExecutorImpl: HTTPRequestExecutor { +public final class HTTPRequestExecutorImpl: HTTPRequestExecutor { let httpClient: HTTPClient public init() { @@ -365,11 +365,12 @@ extension SwiftlyWebsiteAPI.Components.Schemas.Platform { } extension SwiftlyWebsiteAPI.Components.Schemas.DevToolchainForArch { - private static let snapshotRegex: Regex<(Substring, Substring?, Substring?, Substring)> = + private static func snapshotRegex() -> Regex<(Substring, Substring?, Substring?, Substring)> { try! Regex("swift(?:-(\\d+)\\.(\\d+))?-DEVELOPMENT-SNAPSHOT-(\\d{4}-\\d{2}-\\d{2})") + } func parseSnapshot() throws -> ToolchainVersion.Snapshot? { - guard let match = try? Self.snapshotRegex.firstMatch(in: self.dir) else { + guard let match = try? Self.snapshotRegex().firstMatch(in: self.dir) else { return nil } @@ -402,7 +403,7 @@ public struct DownloadNotFoundError: LocalizedError { } /// HTTPClient wrapper used for interfacing with various REST APIs and downloading things. -public struct SwiftlyHTTPClient { +public struct SwiftlyHTTPClient: Sendable { public let httpRequestExecutor: HTTPRequestExecutor public init(httpRequestExecutor: HTTPRequestExecutor) { diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index 0d28f3fe..2d06ee01 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -1,6 +1,6 @@ import Foundation -public struct PlatformDefinition: Codable, Equatable { +public struct PlatformDefinition: Codable, Equatable, Sendable { /// The name of the platform as it is used in the Swift download URLs. /// For instance, for Ubuntu 16.04 this would return “ubuntu1604”. /// For macOS / Xcode, this would return “xcode”. @@ -53,7 +53,7 @@ public struct RunProgramError: Swift.Error { public let arguments: [String] } -public protocol Platform { +public protocol Platform: Sendable { /// The platform-specific default location on disk for swiftly's home /// directory. var defaultSwiftlyHomeDirectory: URL { get } @@ -76,15 +76,15 @@ 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(_ ctx: SwiftlyCoreContext, from: URL, version: ToolchainVersion, verbose: Bool) - throws + async throws /// Extract swiftly from the provided downloaded archive and install /// ourselves from that. - func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) throws + func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) async throws /// Uninstalls a toolchain associated with the given version. /// If this version is in use, the next latest version will be used afterwards. - func uninstall(_ ctx: SwiftlyCoreContext, _ version: ToolchainVersion, verbose: Bool) throws + func uninstall(_ ctx: SwiftlyCoreContext, _ version: ToolchainVersion, verbose: Bool) async throws /// Get the name of the swiftly release binary. func getExecutableName() -> String @@ -327,7 +327,7 @@ extension Platform { } // Install ourselves in the final location - public func installSwiftlyBin(_ ctx: SwiftlyCoreContext) throws { + public func installSwiftlyBin(_ ctx: SwiftlyCoreContext) async throws { // First, let's find out where we are. let cmd = CommandLine.arguments[0] let cmdAbsolute = @@ -374,7 +374,7 @@ extension Platform { return } - ctx.print("Installing swiftly in \(swiftlyHomeBin)...") + await ctx.print("Installing swiftly in \(swiftlyHomeBin)...") if FileManager.default.fileExists(atPath: swiftlyHomeBin) { try FileManager.default.removeItem(atPath: swiftlyHomeBin) @@ -384,7 +384,7 @@ extension Platform { try FileManager.default.moveItem(atPath: cmdAbsolute, toPath: swiftlyHomeBin) } catch { try FileManager.default.copyItem(atPath: cmdAbsolute, toPath: swiftlyHomeBin) - ctx.print( + await ctx.print( "Swiftly has been copied into the installation directory. You can remove '\(cmdAbsolute)'. It is no longer needed." ) } diff --git a/Sources/SwiftlyCore/SwiftlyCore.swift b/Sources/SwiftlyCore/SwiftlyCore.swift index ebfd6668..4cdd9f04 100644 --- a/Sources/SwiftlyCore/SwiftlyCore.swift +++ b/Sources/SwiftlyCore/SwiftlyCore.swift @@ -5,17 +5,17 @@ public let version = SwiftlyVersion(major: 1, minor: 1, patch: 0, suffix: "dev") /// Protocol defining a handler for information swiftly intends to print to stdout. /// This is currently only used to intercept print statements for testing. -public protocol OutputHandler { - func handleOutputLine(_ string: String) +public protocol OutputHandler: Actor { + func handleOutputLine(_ string: String) async } /// Protocol defining a provider for information swiftly intends to read from stdin. -public protocol InputProvider { - func readLine() -> String? +public protocol InputProvider: Actor { + func readLine() async -> String? } /// This struct provides a actor-specific and mockable context for swiftly. -public struct SwiftlyCoreContext { +public struct SwiftlyCoreContext: Sendable { /// A separate home directory to use for testing purposes. This overrides swiftly's default /// home directory location logic. public var mockedHomeDir: URL? @@ -46,7 +46,7 @@ public struct SwiftlyCoreContext { /// Pass the provided string to the set output handler if any. /// If no output handler has been set, just print to stdout. - public func print(_ string: String = "", terminator: String? = nil) { + public func print(_ string: String = "", terminator: String? = nil) async { guard let handler = self.outputHandler else { if let terminator { Swift.print(string, terminator: terminator) @@ -55,18 +55,18 @@ public struct SwiftlyCoreContext { } return } - handler.handleOutputLine(string + (terminator ?? "")) + await handler.handleOutputLine(string + (terminator ?? "")) } - public func readLine(prompt: String) -> String? { - self.print(prompt, terminator: ": \n") + public func readLine(prompt: String) async -> String? { + await self.print(prompt, terminator: ": \n") guard let provider = self.inputProvider else { return Swift.readLine(strippingNewline: true) } - return provider.readLine() + return await provider.readLine() } - public func promptForConfirmation(defaultBehavior: Bool) -> Bool { + public func promptForConfirmation(defaultBehavior: Bool) async -> Bool { let options: String if defaultBehavior { options = "(Y/n)" @@ -75,10 +75,10 @@ public struct SwiftlyCoreContext { } while true { - let answer = (self.readLine(prompt: "Proceed? \(options)") ?? (defaultBehavior ? "y" : "n")).lowercased() + let answer = (await self.readLine(prompt: "Proceed? \(options)") ?? (defaultBehavior ? "y" : "n")).lowercased() guard ["y", "n", ""].contains(answer) else { - self.print("Please input either \"y\" or \"n\", or press ENTER to use the default.") + await self.print("Please input either \"y\" or \"n\", or press ENTER to use the default.") continue } diff --git a/Sources/SwiftlyCore/SwiftlyVersion.swift b/Sources/SwiftlyCore/SwiftlyVersion.swift index 57f787e4..d9532666 100644 --- a/Sources/SwiftlyCore/SwiftlyVersion.swift +++ b/Sources/SwiftlyCore/SwiftlyVersion.swift @@ -2,10 +2,11 @@ import _StringProcessing import Foundation /// Struct modeling a version of swiftly itself. -public struct SwiftlyVersion: Equatable, Comparable, CustomStringConvertible { +public struct SwiftlyVersion: Equatable, Comparable, CustomStringConvertible, Sendable { /// Regex matching versions like "a.b.c", "a.b.c-alpha", and "a.b.c-alpha2". - static let regex: Regex<(Substring, Substring, Substring, Substring, Substring?)> = + public static func regex() -> Regex<(Substring, Substring, Substring, Substring, Substring?)> { try! Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([a-zA-Z0-9]+))?$") + } public let major: Int public let minor: Int @@ -20,7 +21,7 @@ public struct SwiftlyVersion: Equatable, Comparable, CustomStringConvertible { } public init(parsing tag: String) throws { - guard let match = try Self.regex.wholeMatch(in: tag) else { + guard let match = try Self.regex().wholeMatch(in: tag) else { throw SwiftlyError(message: "unable to parse release tag: \"\(tag)\"") } diff --git a/Sources/SwiftlyCore/ToolchainVersion.swift b/Sources/SwiftlyCore/ToolchainVersion.swift index 5c197dfa..6286adc9 100644 --- a/Sources/SwiftlyCore/ToolchainVersion.swift +++ b/Sources/SwiftlyCore/ToolchainVersion.swift @@ -1,9 +1,9 @@ import _StringProcessing /// Enum representing a fully resolved toolchain version (e.g. 5.6.7 or 5.7-snapshot-2022-07-05). -public enum ToolchainVersion { - public struct Snapshot: Equatable, Hashable, CustomStringConvertible, Comparable { - public enum Branch: Equatable, Hashable, CustomStringConvertible { +public enum ToolchainVersion: Sendable { + public struct Snapshot: Equatable, Hashable, CustomStringConvertible, Comparable, Sendable { + public enum Branch: Equatable, Hashable, CustomStringConvertible, Sendable { case main case release(major: Int, minor: Int) @@ -62,7 +62,7 @@ public enum ToolchainVersion { } } - public struct StableRelease: Equatable, Comparable, Hashable, CustomStringConvertible { + public struct StableRelease: Equatable, Comparable, Hashable, CustomStringConvertible, Sendable { public let major: Int public let minor: Int public let patch: Int @@ -99,18 +99,21 @@ public enum ToolchainVersion { self = .snapshot(Snapshot(branch: snapshotBranch, date: date)) } - static let stableRegex: Regex<(Substring, Substring, Substring, Substring)> = + static func stableRegex() -> Regex<(Substring, Substring, Substring, Substring)> { try! Regex("^(?:Swift )?(\\d+)\\.(\\d+)\\.(\\d+)$") + } - static let mainSnapshotRegex: Regex<(Substring, Substring)> = + static func mainSnapshotRegex() -> Regex<(Substring, Substring)> { try! Regex("^main-snapshot-(\\d{4}-\\d{2}-\\d{2})$") + } - static let releaseSnapshotRegex: Regex<(Substring, Substring, Substring, Substring)> = + static func releaseSnapshotRegex() -> Regex<(Substring, Substring, Substring, Substring)> { try! Regex("^(\\d+)\\.(\\d+)-snapshot-(\\d{4}-\\d{2}-\\d{2})$") + } /// Parse a toolchain version from the provided string. public init(parsing string: String) throws { - if let match = try Self.stableRegex.wholeMatch(in: string) { + if let match = try Self.stableRegex().wholeMatch(in: string) { guard let major = Int(match.output.1), let minor = Int(match.output.2), @@ -119,9 +122,9 @@ public enum ToolchainVersion { throw SwiftlyError(message: "invalid stable version: \(string)") } self = ToolchainVersion(major: major, minor: minor, patch: patch) - } else if let match = try Self.mainSnapshotRegex.wholeMatch(in: string) { + } else if let match = try Self.mainSnapshotRegex().wholeMatch(in: string) { self = ToolchainVersion(snapshotBranch: .main, date: String(match.output.1)) - } else if let match = try Self.releaseSnapshotRegex.wholeMatch(in: string) { + } else if let match = try Self.releaseSnapshotRegex().wholeMatch(in: string) { guard let major = Int(match.output.1), let minor = Int(match.output.2) @@ -235,7 +238,7 @@ extension ToolchainVersion: Comparable { extension ToolchainVersion: Hashable {} /// Enum modeling a partially or fully supplied selector of a toolchain version. -public enum ToolchainSelector { +public enum ToolchainSelector: Sendable { /// Select the latest stable toolchain. case latest @@ -347,7 +350,7 @@ extension ToolchainSelector: Equatable {} extension ToolchainSelector: Hashable {} /// Protocol used to facilitate parsing `ToolchainSelector`s from strings. -protocol ToolchainSelectorParser { +protocol ToolchainSelectorParser: Sendable { func parse(_ string: String) throws -> ToolchainSelector? } @@ -363,15 +366,16 @@ private let parsers: [any ToolchainSelectorParser] = [ /// - a.b.c /// - a.b struct StableReleaseParser: ToolchainSelectorParser { - static let regex: Regex<(Substring, Substring, Substring?, Substring?)> = + static func regex() -> Regex<(Substring, Substring, Substring?, Substring?)> { try! Regex("^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?$") + } func parse(_ input: String) throws -> ToolchainSelector? { if input == "latest" { return .latest } - guard let match = try Self.regex.wholeMatch(in: input) else { + guard let match = try Self.regex().wholeMatch(in: input) else { return nil } @@ -399,11 +403,12 @@ struct StableReleaseParser: ToolchainSelectorParser { /// - swift-a.b-DEVELOPMENT-SNAPSHOT-YYYY-mm-dd /// - swift-a.b-DEVELOPMENT-SNAPSHOT struct ReleaseSnapshotParser: ToolchainSelectorParser { - static let regex: Regex<(Substring, Substring, Substring, Substring?)> = + static func regex() -> Regex<(Substring, Substring, Substring, Substring?)> { try! Regex("^(?:swift-)?([0-9]+)\\.([0-9]+)-(?:snapshot|DEVELOPMENT-SNAPSHOT|SNAPSHOT)(?:-([0-9]{4}-[0-9]{2}-[0-9]{2}))?(?:-a)?$") + } func parse(_ input: String) throws -> ToolchainSelector? { - guard let match = try Self.regex.wholeMatch(in: input) else { + guard let match = try Self.regex().wholeMatch(in: input) else { return nil } @@ -434,11 +439,12 @@ struct ReleaseSnapshotParser: ToolchainSelectorParser { /// - swift-DEVELOPMENT-SNAPSHOT-YYYY-mm-dd /// - swift-DEVELOPMENT-SNAPSHOT struct MainSnapshotParser: ToolchainSelectorParser { - static let regex: Regex<(Substring, Substring?)> = + static func regex() -> Regex<(Substring, Substring?)> { try! Regex("^(?:swift-)?(?:main-snapshot|DEVELOPMENT-SNAPSHOT|main-SNAPSHOT)(?:-([0-9]{4}-[0-9]{2}-[0-9]{2}))?(?:-a)?$") + } func parse(_ input: String) throws -> ToolchainSelector? { - guard let match = try Self.regex.wholeMatch(in: input) else { + guard let match = try Self.regex().wholeMatch(in: input) else { return nil } return .snapshot(branch: .main, date: match.output.1.map(String.init)) diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index 6a548010..d0e14400 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -11,7 +11,7 @@ import Testing let tmpDir = Swiftly.currentPlatform.getTempFilePath() try! FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true) let mockedToolchainFile = tmpDir.appendingPathComponent("swift-\(version).\(ext)") - let mockedToolchain = try mockDownloader.makeMockedToolchain(toolchain: version, name: tmpDir.path) + let mockedToolchain = try await mockDownloader.makeMockedToolchain(toolchain: version, name: tmpDir.path) try mockedToolchain.write(to: mockedToolchainFile) return (mockedToolchainFile, version, tmpDir) @@ -28,7 +28,7 @@ import Testing } // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + try await Swiftly.currentPlatform.install(SwiftlyTests.ctx, 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(SwiftlyTests.ctx), includingPropertiesForKeys: nil) #expect(1 == toolchains.count) @@ -37,7 +37,7 @@ import Testing (mockedToolchainFile, version, tmpDir) = try await self.mockToolchainDownload(version: "5.8.0") cleanup += [tmpDir] // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + try await Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchain is added to the toolchains directory toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) #expect(2 == toolchains.count) @@ -46,7 +46,7 @@ import Testing (mockedToolchainFile, version, tmpDir) = try await self.mockToolchainDownload(version: "5.8.0") cleanup += [tmpDir] // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + try await Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchains directory remains the same toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) #expect(2 == toolchains.count) @@ -61,26 +61,26 @@ import Testing try? FileManager.default.removeItem(at: dir) } } - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + try await Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) (mockedToolchainFile, version, tmpDir) = try await self.mockToolchainDownload(version: "5.6.3") cleanup += [tmpDir] - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + try await Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // WHEN: one of the toolchains is uninstalled - try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, version, verbose: true) + try await Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, version, verbose: true) // THEN: there is only one remaining toolchain installed var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) #expect(1 == toolchains.count) // GIVEN; there is only one toolchain installed // WHEN: a non-existent toolchain is uninstalled - try? Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.9.1"), verbose: true) + try? await Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.9.1"), verbose: true) // THEN: there is the one remaining toolchain that is still installed toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) #expect(1 == toolchains.count) // GIVEN: there is only one toolchain installed // WHEN: the last toolchain is uninstalled - try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.8.0"), verbose: true) + try await Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.8.0"), verbose: true) // THEN: there are no toolchains installed toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) #expect(0 == toolchains.count) diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index c26d6c2c..da92e593 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -20,13 +20,13 @@ struct SwiftlyTestError: LocalizedError { let unmockedMsg = "All swiftly test case logic must be mocked in order to prevent mutation of the system running the test. This test must either run swiftly components inside a SwiftlyTests.with... closure, or it must have one of the @Test traits, such as @Test(.testHome), or @Test(.mock...)" -struct OutputHandlerFail: OutputHandler { +actor OutputHandlerFail: OutputHandler { func handleOutputLine(_: String) { fatalError(unmockedMsg) } } -struct InputProviderFail: InputProvider { +actor InputProviderFail: InputProvider { func readLine() -> String? { fatalError(unmockedMsg) } @@ -216,7 +216,7 @@ public enum SwiftlyTests { try await cmd.run(ctx) - return handler.lines + return await handler.lines } static func getTestHomePath(name: String) -> URL { @@ -423,8 +423,8 @@ public enum SwiftlyTests { } } -public class TestOutputHandler: SwiftlyCore.OutputHandler { - public var lines: [String] +public actor TestOutputHandler: SwiftlyCore.OutputHandler { + private(set) var lines: [String] private let quiet: Bool public init(quiet: Bool) { @@ -441,7 +441,7 @@ public class TestOutputHandler: SwiftlyCore.OutputHandler { } } -public class TestInputProvider: SwiftlyCore.InputProvider { +public actor TestInputProvider: SwiftlyCore.InputProvider { private var lines: [String] public init(lines: [String]) { @@ -457,11 +457,9 @@ public class TestInputProvider: SwiftlyCore.InputProvider { public struct SwiftExecutable { public let path: URL - private static let stableRegex: Regex<(Substring, Substring)> = + private static func stableRegex() -> Regex<(Substring, Substring)> { try! Regex("swift-([^-]+)-RELEASE") - - private static let snapshotRegex: Regex<(Substring, Substring)> = - try! Regex("\\(LLVM [a-z0-9]+, Swift ([a-z0-9]+)\\)") + } public func exists() -> Bool { self.path.fileExists() @@ -489,7 +487,7 @@ public struct SwiftExecutable { let outputString = String(decoding: outputData, as: UTF8.self).trimmingCharacters(in: .newlines) - if let match = try Self.stableRegex.firstMatch(in: outputString) { + if let match = try Self.stableRegex().firstMatch(in: outputString) { let versions = match.output.1.split(separator: ".") let major = Int(versions[0])! @@ -514,11 +512,14 @@ public struct SwiftExecutable { /// An `HTTPRequestExecutor` which will return a mocked response to any toolchain download requests. /// All other requests are performed using an actual HTTP client. -public class MockToolchainDownloader: HTTPRequestExecutor { - private static let releaseURLRegex: Regex<(Substring, Substring, Substring, Substring?)> = +public final actor MockToolchainDownloader: HTTPRequestExecutor { + private static func releaseURLRegex() -> Regex<(Substring, Substring, Substring, Substring?)> { try! Regex("swift-(\\d+)\\.(\\d+)(?:\\.(\\d+))?-RELEASE") - private static let snapshotURLRegex: Regex = + } + + private static func snapshotURLRegex() -> Regex { try! Regex("swift(?:-[0-9]+\\.[0-9]+)?-DEVELOPMENT-SNAPSHOT-[0-9]{4}-[0-9]{2}-[0-9]{2}") + } private let executables: [String] #if os(Linux) @@ -659,7 +660,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { private func makeToolchainDownloadResponse(from url: URL) throws -> OpenAPIRuntime.HTTPBody { let toolchain: ToolchainVersion - if let match = try Self.releaseURLRegex.firstMatch(in: url.path) { + if let match = try Self.releaseURLRegex().firstMatch(in: url.path) { var version = "\(match.output.1).\(match.output.2)." if let patch = match.output.3 { version += patch @@ -667,7 +668,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { version += "0" } toolchain = try ToolchainVersion(parsing: version) - } else if let match = try Self.snapshotURLRegex.firstMatch(in: url.path) { + } else if let match = try Self.snapshotURLRegex().firstMatch(in: url.path) { let selector = try ToolchainSelector(parsing: String(match.output)) guard case let .snapshot(branch, date) = selector else { throw SwiftlyTestError(message: "unexpected selector: \(selector)")