diff --git a/Sources/XcodesKit/Downloader.swift b/Sources/XcodesKit/Downloader.swift index d12f2f2..9423bec 100644 --- a/Sources/XcodesKit/Downloader.swift +++ b/Sources/XcodesKit/Downloader.swift @@ -7,6 +7,14 @@ public enum Downloader { case urlSession case aria2(Path) + public init(aria2Path: String?) { + guard let aria2Path = aria2Path.flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"), aria2Path.exists else { + self = .urlSession + return + } + self = .aria2(aria2Path) + } + func download(url: URL, to destination: Path, progressChanged: @escaping (Progress) -> Void) -> Promise { switch self { case .urlSession: diff --git a/Sources/XcodesKit/RuntimeInstaller.swift b/Sources/XcodesKit/RuntimeInstaller.swift index dd91799..cf0821c 100644 --- a/Sources/XcodesKit/RuntimeInstaller.swift +++ b/Sources/XcodesKit/RuntimeInstaller.swift @@ -82,7 +82,7 @@ public class RuntimeInstaller { str += " (\(runtime.build))" } if runtime.state == .legacyDownload || runtime.state == .diskImage { - str += " (Downloaded)" + str += " (Installed)" } else if runtime.state == .bundled { str += " (Bundled with selected Xcode)" } @@ -92,11 +92,15 @@ public class RuntimeInstaller { Current.logging.log("\nNote: Bundled runtimes are indicated for the currently selected Xcode, more bundled runtimes may exist in other Xcode(s)") } + public func downloadRuntime(identifier: String, to destinationDirectory: Path, with downloader: Downloader) async throws { + let matchedRuntime = try await getMatchingRuntime(identifier: identifier) + + _ = try await downloadOrUseExistingArchive(runtime: matchedRuntime, to: destinationDirectory, downloader: downloader) + } + + public func downloadAndInstallRuntime(identifier: String, to destinationDirectory: Path, with downloader: Downloader, shouldDelete: Bool) async throws { - let downloadables = try await runtimeList.downloadableRuntimes().downloadables - guard let matchedRuntime = downloadables.first(where: { $0.visibleIdentifier == identifier || $0.simulatorVersion.buildUpdate == identifier }) else { - throw Error.unavailableRuntime(identifier) - } + let matchedRuntime = try await getMatchingRuntime(identifier: identifier) if matchedRuntime.contentType == .package && !Current.shell.isRoot() { throw Error.rootNeeded @@ -115,6 +119,14 @@ public class RuntimeInstaller { } } + private func getMatchingRuntime(identifier: String) async throws -> DownloadableRuntime { + let downloadables = try await runtimeList.downloadableRuntimes().downloadables + guard let runtime = downloadables.first(where: { $0.visibleIdentifier == identifier || $0.simulatorVersion.buildUpdate == identifier }) else { + throw Error.unavailableRuntime(identifier) + } + return runtime + } + private func installFromImage(dmgUrl: URL) async throws { Current.logging.log("Installing Runtime") try await Current.shell.installRuntimeImage(dmgUrl).asVoid().async() @@ -187,7 +199,7 @@ public class RuntimeInstaller { } if Current.files.fileExistsAtPath(destination.string), aria2DownloadIsIncomplete == false { - Current.logging.log("Found existing Runtime that will be used for installation at \(destination).") + Current.logging.log("Found existing Runtime that will be used, at \(destination).") return destination.url } if runtime.authentication == .virtual { diff --git a/Sources/xcodes/App.swift b/Sources/xcodes/App.swift index 69de488..f1adee4 100644 --- a/Sources/xcodes/App.swift +++ b/Sources/xcodes/App.swift @@ -141,12 +141,7 @@ struct Xcodes: AsyncParsableCommand { installation = .version(versionString) } - var downloader = Downloader.urlSession - if let aria2Path = aria2.flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"), - aria2Path.exists, - noAria2 == false { - downloader = .aria2(aria2Path) - } + let downloader = noAria2 ? Downloader.urlSession : Downloader(aria2Path: aria2) let destination = getDirectory(possibleDirectory: directory, default: .environmentDownloads) @@ -241,12 +236,7 @@ struct Xcodes: AsyncParsableCommand { installation = .version(versionString) } - var downloader = Downloader.urlSession - if let aria2Path = aria2.flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"), - aria2Path.exists, - noAria2 == false { - downloader = .aria2(aria2Path) - } + let downloader = noAria2 ? Downloader.urlSession : Downloader(aria2Path: aria2) let destination = getDirectory(possibleDirectory: directory) @@ -368,7 +358,7 @@ struct Xcodes: AsyncParsableCommand { struct Runtimes: AsyncParsableCommand { static var configuration = CommandConfiguration( abstract: "List all simulator runtimes that are available to install", - subcommands: [Install.self] + subcommands: [Download.self, Install.self] ) @Flag(help: "Include beta runtimes available to install") @@ -406,12 +396,7 @@ struct Xcodes: AsyncParsableCommand { func run() async throws { Rainbow.enabled = Rainbow.enabled && globalColor.color - var downloader = Downloader.urlSession - if let aria2Path = aria2.flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"), - aria2Path.exists, - noAria2 == false { - downloader = .aria2(aria2Path) - } + let downloader = noAria2 ? Downloader.urlSession : Downloader(aria2Path: aria2) let destination = getDirectory(possibleDirectory: directory, default: .environmentDownloads) @@ -419,6 +404,41 @@ struct Xcodes: AsyncParsableCommand { Current.logging.log("Finished") } } + + struct Download: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Download a specific simulator runtime" + ) + + @Argument(help: "The runtime to download") + var version: String + + @Option(help: "The path to an aria2 executable. Searches $PATH by default.", + completion: .file()) + var aria2: String? + + @Flag(help: "Don't use aria2 to download the runtime, even if its available.") + var noAria2: Bool = false + + @Option(help: "The directory to download the runtime archive to. Defaults to ~/Downloads.", + completion: .directory) + var directory: String? + + @OptionGroup + var globalColor: GlobalColorOption + + func run() async throws { + Rainbow.enabled = Rainbow.enabled && globalColor.color + + let downloader = noAria2 ? Downloader.urlSession : Downloader(aria2Path: aria2) + + let destination = getDirectory(possibleDirectory: directory, default: .environmentDownloads) + + try await runtimeInstaller.downloadRuntime(identifier: version, to: destination, with: downloader) + Current.logging.log("Finished") + } + } + } struct Select: ParsableCommand { diff --git a/Tests/XcodesKitTests/Fixtures/LogOutput-Runtime_NoBetas.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-Runtime_NoBetas.txt index 102f320..336bf0e 100644 --- a/Tests/XcodesKitTests/Fixtures/LogOutput-Runtime_NoBetas.txt +++ b/Tests/XcodesKitTests/Fixtures/LogOutput-Runtime_NoBetas.txt @@ -1,7 +1,7 @@ -- iOS -- -iOS 12.4 (Downloaded) -iOS 13.0 (Downloaded) -iOS 13.1 (Downloaded) +iOS 12.4 (Installed) +iOS 13.0 (Installed) +iOS 13.1 (Installed) iOS 13.2.2 iOS 13.3 iOS 13.4 @@ -18,7 +18,7 @@ iOS 15.0 iOS 15.2 iOS 15.4 iOS 15.5 (Bundled with selected Xcode) -iOS 15.5 (Downloaded) +iOS 15.5 (Installed) iOS 16.0 -- watchOS -- watchOS 6.0 @@ -31,9 +31,9 @@ watchOS 7.4 watchOS 8.0 watchOS 8.3 watchOS 8.5 (Bundled with selected Xcode) -watchOS 9.0-beta4 (Downloaded) +watchOS 9.0-beta4 (Installed) watchOS 9.0 (20R362) -watchOS 9.0 (UnknownBuildNumber) (Downloaded) +watchOS 9.0 (UnknownBuildNumber) (Installed) -- tvOS -- tvOS 12.4 tvOS 13.0 diff --git a/Tests/XcodesKitTests/Fixtures/LogOutput-Runtimes.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-Runtimes.txt index 785b58b..b577471 100644 --- a/Tests/XcodesKitTests/Fixtures/LogOutput-Runtimes.txt +++ b/Tests/XcodesKitTests/Fixtures/LogOutput-Runtimes.txt @@ -1,7 +1,7 @@ -- iOS -- -iOS 12.4 (Downloaded) -iOS 13.0 (Downloaded) -iOS 13.1 (Downloaded) +iOS 12.4 (Installed) +iOS 13.0 (Installed) +iOS 13.1 (Installed) iOS 13.2.2 iOS 13.3 iOS 13.4 @@ -18,7 +18,7 @@ iOS 15.0 iOS 15.2 iOS 15.4 iOS 15.5 (Bundled with selected Xcode) -iOS 15.5 (Downloaded) +iOS 15.5 (Installed) iOS 16.0 -- watchOS -- watchOS 6.0 @@ -34,10 +34,10 @@ watchOS 8.5 (Bundled with selected Xcode) watchOS 9.0-beta1 watchOS 9.0-beta2 watchOS 9.0-beta3 -watchOS 9.0-beta4 (Downloaded) +watchOS 9.0-beta4 (Installed) watchOS 9.0-beta5 watchOS 9.0 (20R362) -watchOS 9.0 (UnknownBuildNumber) (Downloaded) +watchOS 9.0 (UnknownBuildNumber) (Installed) watchOS 9.1-beta1 -- tvOS -- tvOS 12.4