diff --git a/Package.resolved b/Package.resolved
index 9e562b2..a8ee643 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -6,17 +6,8 @@
"repositoryURL": "https://github.com/PromiseKit/Foundation.git",
"state": {
"branch": null,
- "revision": "0a59b0d89f469979aecacdf9046df14f99d1cc6b",
- "version": "3.3.2"
- }
- },
- {
- "package": "Guaka",
- "repositoryURL": "https://github.com/nsomar/Guaka.git",
- "state": {
- "branch": null,
- "revision": "6fb29b2378166a30d72120980e1c099c664598de",
- "version": "0.4.1"
+ "revision": "1a276e598dac59489ed904887e0740fa75e571e0",
+ "version": "3.3.4"
}
},
{
@@ -24,8 +15,8 @@
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state": {
"branch": null,
- "revision": "0046db4ab8fd7be31a59e57912dbb96639ad8ce9",
- "version": "3.2.0"
+ "revision": "8d33ffd6f74b3bcfc99af759d4204c6395a3f918",
+ "version": "3.2.1"
}
},
{
@@ -33,8 +24,8 @@
"repositoryURL": "https://github.com/mxcl/LegibleError.git",
"state": {
"branch": null,
- "revision": "c615c01e461e8a3495ba4ea75f5d671c76820105",
- "version": "1.0.1"
+ "revision": "909e9bab3ded97350b28a5ab41dd745dd8aa9710",
+ "version": "1.0.4"
}
},
{
@@ -42,8 +33,8 @@
"repositoryURL": "https://github.com/mxcl/Path.swift.git",
"state": {
"branch": null,
- "revision": "f324b4a562ca421fd2905414a10fb0fc91d58b67",
- "version": "0.16.2"
+ "revision": "dac007e907a4f4c565cfdc55a9ce148a761a11d5",
+ "version": "0.16.3"
}
},
{
@@ -51,17 +42,17 @@
"repositoryURL": "https://github.com/mxcl/PromiseKit.git",
"state": {
"branch": null,
- "revision": "fe1e9c5b62465227cceb7b0e6e79489ba7b824af",
- "version": "6.8.4"
+ "revision": "1c296a8637838901d2b01e4c46875ee749506133",
+ "version": "6.8.5"
}
},
{
- "package": "StringScanner",
- "repositoryURL": "https://github.com/getGuaka/StringScanner.git",
+ "package": "swift-argument-parser",
+ "repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
- "revision": "de1685ad202cb586d626ed52d6de904dd34189f3",
- "version": "0.4.1"
+ "revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
+ "version": "0.3.1"
}
},
{
diff --git a/Package.swift b/Package.swift
index c4f7bc1..d7b446c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -12,7 +12,7 @@ let package = Package(
.library(name: "AppleAPI", targets: ["AppleAPI"]),
],
dependencies: [
- .package(url: "https://github.com/nsomar/Guaka.git", .upToNextMinor(from: "0.4.0")),
+ .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.0")),
.package(url: "https://github.com/mxcl/Path.swift.git", .upToNextMinor(from: "0.16.0")),
.package(url: "https://github.com/mxcl/Version.git", .upToNextMinor(from: "1.0.3")),
.package(url: "https://github.com/mxcl/PromiseKit.git", .upToNextMinor(from: "6.8.3")),
@@ -25,7 +25,7 @@ let package = Package(
.target(
name: "xcodes",
dependencies: [
- "Guaka", "XcodesKit"
+ .product(name: "ArgumentParser", package: "swift-argument-parser"), "XcodesKit"
]),
.testTarget(
name: "xcodesTests",
diff --git a/README.md b/README.md
index a602d69..312534d 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,21 @@ Xcode will be installed to /Applications by default, but you can provide the pat
- `update`: Update the list of available versions of Xcode
- `version`: Print the version number of xcodes itself
+### Shell Completion Scripts
+
+xcodes can generate completion scripts which allow you to press the tab key on your keyboard to autocomplete commands and arguments when typing an xcodes command. The steps to install a completion script depend on the shell that you use. More information about installation instructions for different shells and the underlying implementation is available in the [swift-argument-parser repo](https://github.com/apple/swift-argument-parser/blob/master/Documentation/07%20Completion%20Scripts.md).
+
+
+Zsh, with oh-my-zsh:
+
+Run the following commands:
+
+```sh
+mkdir ~/.oh-my-zsh/completions
+xcodes --generate-completion-script > ~/.oh-my-zsh/completions/_xcodes
+```
+
+
## Development
You'll need Xcode 12 in order to build and run xcodes.
diff --git a/Sources/xcodes/ParsableArguments+LegibleError.swift b/Sources/xcodes/ParsableArguments+LegibleError.swift
new file mode 100644
index 0000000..e4a85d2
--- /dev/null
+++ b/Sources/xcodes/ParsableArguments+LegibleError.swift
@@ -0,0 +1,10 @@
+import ArgumentParser
+import LegibleError
+import XcodesKit
+
+extension ParsableArguments {
+ static func exit(withLegibleError error: Error) -> Never {
+ Current.logging.log(error.legibleLocalizedDescription)
+ Self.exit(withError: ExitCode.failure)
+ }
+}
diff --git a/Sources/xcodes/main.swift b/Sources/xcodes/main.swift
index 6d3b48e..bc5fcd0 100644
--- a/Sources/xcodes/main.swift
+++ b/Sources/xcodes/main.swift
@@ -1,28 +1,15 @@
import Foundation
-import Guaka
+import ArgumentParser
import Version
import PromiseKit
import XcodesKit
import LegibleError
import Path
-var configuration = Configuration()
-try? configuration.load()
-let xcodeList = XcodeList()
-let installer = XcodeInstaller(configuration: configuration, xcodeList: xcodeList)
-
-migrateApplicationSupportFiles()
-
-let globalDirectoryFlag = Flag(
- longName: "directory",
- type: String.self,
- description: "The directory where your Xcodes are installed. Defaults to /Applications.",
- inheritable: true
-)
-func getDirectory(flags: Flags) -> Path {
- let directory = flags.getString(name: "directory").flatMap(Path.init) ??
- ProcessInfo.processInfo.environment["XCODES_DIRECTORY"].flatMap(Path.init) ??
- Path.root.join("Applications")
+func getDirectory(possibleDirectory: String?, default: Path = Path.root.join("Applications")) -> Path {
+ let directory = possibleDirectory.flatMap(Path.init) ??
+ ProcessInfo.processInfo.environment["XCODES_DIRECTORY"].flatMap(Path.init) ??
+ `default`
guard directory.isDirectory else {
Current.logging.log("Directory argument must be a directory, but was provided \(directory.string).")
exit(1)
@@ -30,235 +17,329 @@ func getDirectory(flags: Flags) -> Path {
return directory
}
-// This is awkward, but Guaka wants a root command in order to add subcommands,
-// but then seems to want it to behave like a normal command even though it'll only ever print the help.
-// But it doesn't even print the help without the user providing the --help flag,
-// so we need to tell it to do this explicitly
-var app: Command!
-app = Command(usage: "xcodes", flags: [globalDirectoryFlag]) { _, _ in print(GuakaConfig.helpGenerator.init(command: app).helpMessage) }
-
-let installed = Command(usage: "installed",
- shortMessage: "List the versions of Xcode that are installed") { flags, _ in
- let directory = getDirectory(flags: flags)
- installer.printInstalledXcodes(directory: directory)
- .done {
- exit(0)
- }
- .catch { error in
- print(error.legibleLocalizedDescription)
- exit(1)
- }
-
- RunLoop.current.run()
+struct GlobalDirectoryOption: ParsableArguments {
+ @Option(help: "The directory where your Xcodes are installed. Defaults to /Applications.",
+ completion: .directory)
+ var directory: String?
}
-app.add(subCommand: installed)
-
-let printFlag = Flag(shortName: "p", longName: "print-path", value: false, description: "Print the path of the selected Xcode")
-let select = Command(usage: "select ",
- shortMessage: "Change the selected Xcode",
- longMessage: "Change the selected Xcode. Run without any arguments to interactively select from a list, or provide an absolute path.",
- flags: [printFlag],
- example: """
- xcodes select
- xcodes select 11.4.0
- xcodes select /Applications/Xcode-11.4.0.app
- xcodes select -p
- """) { flags, args in
- let directory = getDirectory(flags: flags)
-
- selectXcode(shouldPrint: flags.getBool(name: "print-path") ?? false, pathOrVersion: args.joined(separator: " "), directory: directory)
- .catch { error in
- print(error.legibleLocalizedDescription)
- exit(1)
- }
- RunLoop.current.run()
-}
-app.add(subCommand: select)
-
-let list = Command(usage: "list",
- shortMessage: "List all versions of Xcode that are available to install") { flags, _ in
- let directory = getDirectory(flags: flags)
+struct Xcodes: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "Manage the Xcodes installed on your Mac",
+ shouldDisplay: true,
+ subcommands: [Download.self, Install.self, Installed.self, List.self, Select.self, Uninstall.self, Update.self, Version.self]
+ )
- firstly { () -> Promise in
- if xcodeList.shouldUpdate {
- return installer.updateAndPrint(directory: directory)
- }
- else {
- return installer.printAvailableXcodes(xcodeList.availableXcodes, installed: Current.files.installedXcodes(directory))
- }
+ static var xcodesConfiguration = Configuration()
+ static let xcodeList = XcodeList()
+ static var installer: XcodeInstaller!
+
+ static func main() {
+ try? xcodesConfiguration.load()
+ installer = XcodeInstaller(configuration: xcodesConfiguration, xcodeList: xcodeList)
+ migrateApplicationSupportFiles()
+
+ self.main(nil)
}
- .done {
- exit(0)
+
+ struct Download: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "Download a specific version of Xcode",
+ discussion: """
+ By default, xcodes will use a URLSession to download the specified version. If aria2 (https://aria2.github.io, available in Homebrew) is installed, either somewhere in PATH or at the path specified by the --aria2 flag, then it will be used instead. aria2 will use up to 16 connections to download Xcode 3-5x faster. If you have aria2 installed and would prefer to not use it, you can use the --no-aria2 flag.
+
+ EXAMPLES:
+ xcodes download 10.2.1
+ xcodes download 11 Beta 7
+ xcodes download 11.2 GM seed
+ xcodes download 9.0 --directory ~/Archive
+ xcodes download --latest-prerelease
+ """
+ )
+
+ @Argument(help: "The version to download",
+ completion: .custom { args in xcodeList.availableXcodes.sorted { $0.version < $1.version }.map { $0.version.xcodeDescription } })
+ var version: [String] = []
+
+ @Flag(help: "Update and then download the latest non-prerelease version available.")
+ var latest: Bool = false
+
+ @Flag(help: "Update and then download the latest prerelease version available, including GM seeds and GMs.")
+ var latestPrerelease = false
+
+ @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 Xcode, even if its available.")
+ var noAria2: Bool = false
+
+ @Option(help: "The directory to download Xcode to. Defaults to ~/Downloads.",
+ completion: .directory)
+ var directory: String?
+
+ func run() {
+ let versionString = version.joined(separator: " ")
+
+ let installation: XcodeInstaller.InstallationType
+ // Deliberately not using InstallationType.path here as it doesn't make sense to download an Xcode from a .xip that's already on disk
+ if latest {
+ installation = .latest
+ } else if latestPrerelease {
+ installation = .latestPrerelease
+ } else {
+ installation = .version(versionString)
+ }
+
+ var downloader = XcodeInstaller.Downloader.urlSession
+ if let aria2Path = aria2.flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"),
+ aria2Path.exists,
+ noAria2 == false {
+ downloader = .aria2(aria2Path)
+ }
+
+ let destination = getDirectory(possibleDirectory: directory, default: Path.home.join("Downloads"))
+
+ installer.download(installation, downloader: downloader, destinationDirectory: destination)
+ .catch { error in
+ switch error {
+ case Process.PMKError.execution(let process, let standardOutput, let standardError):
+ Current.logging.log("""
+ Failed executing: `\(process)` (\(process.terminationStatus))
+ \([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n"))
+ """)
+ default:
+ Current.logging.log(error.legibleLocalizedDescription)
+ }
+
+ Install.exit(withError: ExitCode.failure)
+ }
+
+ RunLoop.current.run()
+ }
}
- .catch { error in
- print(error.legibleLocalizedDescription)
- exit(1)
+
+ struct Install: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "Download and install a specific version of Xcode",
+ discussion: """
+ By default, xcodes will use a URLSession to download the specified version. If aria2 (https://aria2.github.io, available in Homebrew) is installed, either somewhere in PATH or at the path specified by the --aria2 flag, then it will be used instead. aria2 will use up to 16 connections to download Xcode 3-5x faster. If you have aria2 installed and would prefer to not use it, you can use the --no-aria2 flag.
+
+ EXAMPLES:
+ xcodes install 10.2.1
+ xcodes install 11 Beta 7
+ xcodes install 11.2 GM seed
+ xcodes install 9.0 --path ~/Archive/Xcode_9.xip
+ xcodes install --latest-prerelease
+ xcodes install --latest --directory "/Volumes/Bag Of Holding/"
+ """
+ )
+
+ @Argument(help: "The version to install",
+ completion: .custom { args in xcodeList.availableXcodes.sorted { $0.version < $1.version }.map { $0.version.xcodeDescription } })
+ var version: [String] = []
+
+ @Option(name: .customLong("path"),
+ help: "Local path to Xcode .xip",
+ completion: .file(extensions: ["xip"]))
+ var pathString: String?
+
+ @Flag(help: "Update and then install the latest non-prerelease version available.")
+ var latest: Bool = false
+
+ @Flag(help: "Update and then install the latest prerelease version available, including GM seeds and GMs.")
+ var latestPrerelease = false
+
+ @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 Xcode, even if its available.")
+ var noAria2: Bool = false
+
+ @Option(help: "The directory to install Xcode into. Defaults to /Applications.",
+ completion: .directory)
+ var directory: String?
+
+ func run() {
+ let versionString = version.joined(separator: " ")
+
+ let installation: XcodeInstaller.InstallationType
+ if latest {
+ installation = .latest
+ } else if latestPrerelease {
+ installation = .latestPrerelease
+ } else if let pathString = pathString, let path = Path(pathString) {
+ installation = .path(versionString, path)
+ } else {
+ installation = .version(versionString)
+ }
+
+ var downloader = XcodeInstaller.Downloader.urlSession
+ if let aria2Path = aria2.flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"),
+ aria2Path.exists,
+ noAria2 == false {
+ downloader = .aria2(aria2Path)
+ }
+
+ let destination = getDirectory(possibleDirectory: directory)
+
+ installer.install(installation, downloader: downloader, destination: destination)
+ .done { Install.exit() }
+ .catch { error in
+ switch error {
+ case Process.PMKError.execution(let process, let standardOutput, let standardError):
+ Current.logging.log("""
+ Failed executing: `\(process)` (\(process.terminationStatus))
+ \([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n"))
+ """)
+ default:
+ Current.logging.log(error.legibleLocalizedDescription)
+ }
+
+ Install.exit(withError: ExitCode.failure)
+ }
+
+ RunLoop.current.run()
+ }
}
-
- RunLoop.current.run()
-}
-app.add(subCommand: list)
-
-let update = Command(usage: "update",
- shortMessage: "Update the list of available versions of Xcode") { flags, _ in
- let directory = getDirectory(flags: flags)
-
- firstly {
- installer.updateAndPrint(directory: directory)
+
+ struct Installed: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "List the versions of Xcode that are installed"
+ )
+
+ @OptionGroup
+ var globalDirectory: GlobalDirectoryOption
+
+ func run() {
+ let directory = getDirectory(possibleDirectory: globalDirectory.directory)
+
+ installer.printInstalledXcodes(directory: directory)
+ .done { Installed.exit() }
+ .catch { error in Installed.exit(withLegibleError: error) }
+
+ RunLoop.current.run()
+ }
}
- .catch { error in
- print(error.legibleLocalizedDescription)
- exit(1)
+
+ struct List: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "List all versions of Xcode that are available to install"
+ )
+
+ @OptionGroup
+ var globalDirectory: GlobalDirectoryOption
+
+ func run() {
+ let directory = getDirectory(possibleDirectory: globalDirectory.directory)
+
+ firstly { () -> Promise in
+ if xcodeList.shouldUpdate {
+ return installer.updateAndPrint(directory: directory)
+ }
+ else {
+ return installer.printAvailableXcodes(xcodeList.availableXcodes, installed: Current.files.installedXcodes(directory))
+ }
+ }
+ .done { List.exit() }
+ .catch { error in List.exit(withLegibleError: ExitCode.failure) }
+
+ RunLoop.current.run()
+ }
}
-
- RunLoop.current.run()
-}
-app.add(subCommand: update)
-
-let installPathFlag = Flag(longName: "path", type: String.self, description: "Local path to Xcode .xip")
-let installLatestFlag = Flag(longName: "latest", value: false, description: "Update and then install the latest non-prerelease version available.")
-let installLatestPrereleaseFlag = Flag(longName: "latest-prerelease", value: false, description: "Update and then install the latest prerelease version available, including GM seeds and GMs.")
-let directory = Flag(longName: "directory", type: String.self, description: "The directory to install Xcode into. Defaults to /Applications.")
-let aria2 = Flag(longName: "aria2", type: String.self, description: "The path to an aria2 executable. Defaults to /usr/local/bin/aria2c.")
-let noAria2 = Flag(longName: "no-aria2", value: false, description: "Don't use aria2 to download Xcode, even if its available.")
-
-let install = Command(usage: "install ",
- shortMessage: "Download and install a specific version of Xcode",
- longMessage: """
- Download and install a specific version of Xcode
-
- By default, xcodes will use a URLSession to download the specified version. If aria2 (https://aria2.github.io, available in Homebrew) is installed, either somewhere in PATH or at the path specified by the --aria2 flag, then it will be used instead. aria2 will use up to 16 connections to download Xcode 3-5x faster. If you have aria2 installed and would prefer to not use it, you can use the --no-aria2 flag.
- """,
- flags: [installPathFlag, installLatestFlag, installLatestPrereleaseFlag, directory, aria2, noAria2],
- example: """
- xcodes install 10.2.1
- xcodes install 11 Beta 7
- xcodes install 11.2 GM seed
- xcodes install 9.0 --path ~/Archive/Xcode_9.xip
- xcodes install --latest-prerelease
- xcodes install --latest --directory "/Volumes/Bag Of Holding/"
- """) { flags, args in
- let versionString = args.joined(separator: " ")
-
- let installation: XcodeInstaller.InstallationType
- if flags.getBool(name: "latest") == true {
- installation = .latest
- } else if flags.getBool(name: "latest-prerelease") == true {
- installation = .latestPrerelease
- } else if let pathString = flags.getString(name: "path"), let path = Path(pathString) {
- installation = .path(versionString, path)
- } else {
- installation = .version(versionString)
+
+ struct Select: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "Change the selected Xcode",
+ discussion: """
+ Run without any arguments to interactively select from a list, or provide an absolute path.
+
+ EXAMPLES:
+ xcodes select
+ xcodes select 11.4.0
+ xcodes select /Applications/Xcode-11.4.0.app
+ xcodes select -p
+ """
+ )
+
+ @ArgumentParser.Flag(name: [.customShort("p"), .customLong("print-path")], help: "Print the path of the selected Xcode")
+ var print: Bool = false
+
+ @Argument(help: "Version or path",
+ completion: .custom { _ in Current.files.installedXcodes(getDirectory(possibleDirectory: nil)).sorted { $0.version < $1.version }.map { $0.version.xcodeDescription } })
+ var versionOrPath: [String] = []
+
+ @OptionGroup
+ var globalDirectory: GlobalDirectoryOption
+
+ func run() {
+ let directory = getDirectory(possibleDirectory: globalDirectory.directory)
+
+ selectXcode(shouldPrint: print, pathOrVersion: versionOrPath.joined(separator: " "), directory: directory)
+ .done { Select.exit() }
+ .catch { error in Select.exit(withLegibleError: error) }
+
+ RunLoop.current.run()
+ }
}
- var downloader = XcodeInstaller.Downloader.urlSession
- if let aria2Path = flags.getString(name: "aria2").flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"),
- aria2Path.exists,
- flags.getBool(name: "no-aria2") != true {
- downloader = .aria2(aria2Path)
+ struct Uninstall: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "Uninstall a specific version of Xcode",
+ discussion: """
+ EXAMPLES:
+ xcodes uninstall 10.2.1
+ """
+ )
+
+ @Argument(help: "The version to uninstall",
+ completion: .custom { _ in Current.files.installedXcodes(getDirectory(possibleDirectory: nil)).sorted { $0.version < $1.version }.map { $0.version.xcodeDescription } })
+ var version: [String] = []
+
+ @OptionGroup
+ var globalDirectory: GlobalDirectoryOption
+
+ func run() {
+ let directory = getDirectory(possibleDirectory: globalDirectory.directory)
+
+ installer.uninstallXcode(version.joined(separator: " "), directory: directory)
+ .done { Uninstall.exit() }
+ .catch { error in Uninstall.exit(withLegibleError: ExitCode.failure) }
+
+ RunLoop.current.run()
+ }
}
-
- let destination = getDirectory(flags: flags)
-
- installer.install(installation, downloader: downloader, destination: destination)
- .catch { error in
- switch error {
- case Process.PMKError.execution(let process, let standardOutput, let standardError):
- Current.logging.log("""
- Failed executing: `\(process)` (\(process.terminationStatus))
- \([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n"))
- """)
- default:
- Current.logging.log(error.legibleLocalizedDescription)
- }
-
- exit(1)
+
+ struct Update: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "Update the list of available versions of Xcode"
+ )
+
+ @OptionGroup
+ var globalDirectory: GlobalDirectoryOption
+
+ func run() {
+ let directory = getDirectory(possibleDirectory: globalDirectory.directory)
+
+ installer.updateAndPrint(directory: directory)
+ .done { Update.exit() }
+ .catch { error in Update.exit(withLegibleError: error) }
+
+ RunLoop.current.run()
}
-
- RunLoop.current.run()
-}
-app.add(subCommand: install)
-
-let downloadDirectoryFlag = Flag(longName: "directory", type: String.self, description: "Directory to download .xip to. Defaults to ~/Downloads.")
-let downloadLatestFlag = Flag(longName: "latest", value: false, description: "Update and then download the latest non-prerelease version available.")
-let downloadLatestPrereleaseFlag = Flag(longName: "latest-prerelease", value: false, description: "Update and then download the latest prerelease version available, including GM seeds and GMs.")
-let download = Command(usage: "download ",
- shortMessage: "Download a specific version of Xcode",
- longMessage: """
- Download a specific version of Xcode
-
- By default, xcodes will use a URLSession to download the specified version. If aria2 (https://aria2.github.io, available in Homebrew) is installed, either somewhere in PATH or at the path specified by the --aria2 flag, then it will be used instead. aria2 will use up to 16 connections to download Xcode 3-5x faster. If you have aria2 installed and would prefer to not use it, you can use the --no-aria2 flag.
- """,
- flags: [downloadDirectoryFlag, downloadLatestFlag, downloadLatestPrereleaseFlag, aria2, noAria2],
- example: """
- xcodes download 10.2.1
- xcodes download 11 Beta 7
- xcodes download 11.2 GM seed
- xcodes download 9.0 --directory ~/Archive
- xcodes download --latest-prerelease
- """) { flags, args in
- let versionString = args.joined(separator: " ")
-
- let installation: XcodeInstaller.InstallationType
- // Deliberately not using InstallationType.path here as it doesn't make sense to download an Xcode from a .xip that's already on disk
- if flags.getBool(name: "latest") == true {
- installation = .latest
- } else if flags.getBool(name: "latest-prerelease") == true {
- installation = .latestPrerelease
- } else {
- installation = .version(versionString)
- }
-
- var downloader = XcodeInstaller.Downloader.urlSession
- if let aria2Path = flags.getString(name: "aria2").flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"),
- aria2Path.exists,
- flags.getBool(name: "no-aria2") != true {
- downloader = .aria2(aria2Path)
- }
-
- let directory = flags.getString(name: "directory").flatMap(Path.init)
- installer.download(installation, downloader: downloader, destinationDirectory: directory ?? Path.home.join("Downloads"))
- .catch { error in
- switch error {
- case Process.PMKError.execution(let process, let standardOutput, let standardError):
- Current.logging.log("""
- Failed executing: `\(process)` (\(process.terminationStatus))
- \([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n"))
- """)
- default:
- Current.logging.log(error.legibleLocalizedDescription)
- }
-
- exit(1)
- }
-
- RunLoop.current.run()
-}
-app.add(subCommand: download)
-
-let uninstall = Command(usage: "uninstall ",
- shortMessage: "Uninstall a specific version of Xcode",
- example: "xcodes uninstall 10.2.1") { flags, args in
- let versionString = args.joined(separator: " ")
-
- let directory = getDirectory(flags: flags)
-
- installer.uninstallXcode(versionString, directory: directory)
- .catch { error in
- print(error.legibleLocalizedDescription)
- exit(1)
+ }
+
+ struct Version: ParsableCommand {
+ static var configuration = CommandConfiguration(
+ abstract: "Print the version number of xcodes itself"
+ )
+
+ func run() {
+ Current.logging.log(XcodesKit.version.description)
}
-
- RunLoop.current.run()
-}
-app.add(subCommand: uninstall)
-
-let version = Command(usage: "version",
- shortMessage: "Print the version number of xcodes itself") { _, _ in
- print(XcodesKit.version)
- exit(0)
+ }
}
-app.add(subCommand: version)
-app.execute()
+// @main doesn't work yet because of https://bugs.swift.org/browse/SR-12683
+Xcodes.main()