diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/xcodes.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/xcodes.xcscheme
index cfe6296..3a76266 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/xcodes.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/xcodes.xcscheme
@@ -127,6 +127,10 @@
argument = "install 11 beta 5"
isEnabled = "NO">
+
+
diff --git a/Sources/XcodesKit/Build.swift b/Sources/XcodesKit/Build.swift
new file mode 100644
index 0000000..c447ef7
--- /dev/null
+++ b/Sources/XcodesKit/Build.swift
@@ -0,0 +1,25 @@
+import Foundation
+
+public struct Build: Equatable, CustomStringConvertible {
+
+ let identifier: String
+
+ /**
+ E.g.:
+ 13E500a
+ 12E507
+ 7B85
+ */
+ init?(identifier: String) {
+ let nsrange = NSRange(identifier.startIndex.. Promise<(Xcode, URL)> {
+
+ private func buildFromXcodeVersionFile() -> Build? {
+ let xcodeVersionFilePath = Path.cwd.join(".xcode-version")
+ let version = (try? Data(contentsOf: xcodeVersionFilePath.url))
+ .flatMap { String(data: $0, encoding: .utf8) }
+ .flatMap(Build.init(identifier:))
+ return version
+ }
+
+ private func findXcode(version: Version, dataSource: DataSource) -> Promise {
return firstly { () -> Promise in
if dataSource == .apple {
return loginIfNeeded().map { version }
@@ -308,11 +333,48 @@ public final class XcodeInstaller {
return Promise.value(version)
}
}
- .then { version -> Promise<(Xcode, URL)> in
+ .map { version -> Xcode in
guard let xcode = self.xcodeList.availableXcodes.first(withVersion: version) else {
throw Error.unavailableVersion(version)
}
-
+ return xcode
+ }
+ }
+
+ private func findXcode(build: Build, dataSource: DataSource) -> Promise {
+ return firstly { () -> Promise in
+ if dataSource == .apple {
+ return loginIfNeeded().map { build }
+ } else {
+ guard let xcode = self.xcodeList.availableXcodes.first(where: { xcode in
+ xcode.version.buildMetadataIdentifiers.contains(build.identifier)
+ }) else {
+ throw Error.unavailableVersion(version)
+ }
+
+ return validateADCSession(path: xcode.downloadPath).map { build }
+ }
+ }
+ .then { build -> Promise in
+ if self.xcodeList.shouldUpdate {
+ return self.xcodeList.update(dataSource: dataSource).map { _ in build }
+ }
+ else {
+ return Promise.value(build)
+ }
+ }
+ .map { build -> Xcode in
+ guard let xcode = self.xcodeList.availableXcodes.first(where: { xcode in
+ xcode.version.buildMetadataIdentifiers.contains(build.identifier)
+ }) else {
+ throw Error.unavailableBuild(build)
+ }
+ return xcode
+ }
+ }
+
+ private func downloadXcode(xcode: Xcode, downloader: Downloader, willInstall: Bool) -> Promise<(Xcode, URL)> {
+ return firstly {
if Current.shell.isatty() {
// Move to the next line so that the escape codes below can move up a line and overwrite it with download progress
Current.logging.log("")
@@ -337,6 +399,22 @@ public final class XcodeInstaller {
.map { return (xcode, $0) }
}
}
+
+ private func downloadXcode(version: Version, dataSource: DataSource, downloader: Downloader, willInstall: Bool) -> Promise<(Xcode, URL)> {
+ return firstly {
+ findXcode(version: version, dataSource: dataSource)
+ }.then { xcode in
+ self.downloadXcode(xcode: xcode, downloader: downloader, willInstall: willInstall)
+ }
+ }
+
+ private func downloadXcode(build: Build, dataSource: DataSource, downloader: Downloader, willInstall: Bool) -> Promise<(Xcode, URL)> {
+ return firstly {
+ findXcode(build: build, dataSource: dataSource)
+ }.then { xcode in
+ self.downloadXcode(xcode: xcode, downloader: downloader, willInstall: willInstall)
+ }
+ }
func validateADCSession(path: String) -> Promise {
return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path)).asVoid()
diff --git a/Sources/xcodes/App.swift b/Sources/xcodes/App.swift
index de9ae87..91737ed 100644
--- a/Sources/xcodes/App.swift
+++ b/Sources/xcodes/App.swift
@@ -180,6 +180,9 @@ struct Xcodes: AsyncParsableCommand {
completion: .file(extensions: ["xip"]))
var pathString: String?
+ @Flag(help: "Install a specific build number of Xcode.")
+ var build = false
+
@Flag(help: "Update and then install the latest non-prerelease version available.")
var latest: Bool = false
@@ -224,6 +227,8 @@ struct Xcodes: AsyncParsableCommand {
installation = .latestPrerelease
} else if let pathString = pathString, let path = Path(pathString) {
installation = .path(versionString, path)
+ } else if build {
+ installation = .build(versionString)
} else {
installation = .version(versionString)
}
diff --git a/Tests/XcodesKitTests/Build+XcodeTests.swift b/Tests/XcodesKitTests/Build+XcodeTests.swift
new file mode 100644
index 0000000..5a9ddd0
--- /dev/null
+++ b/Tests/XcodesKitTests/Build+XcodeTests.swift
@@ -0,0 +1,15 @@
+import XCTest
+@testable import XcodesKit
+
+class BuildXcodeTests: XCTestCase {
+
+ func test_InitXcodeVersion() {
+ XCTAssertNotNil(Build(identifier: "13E500a"))
+ XCTAssertNotNil(Build(identifier: "12E507"))
+ XCTAssertNotNil(Build(identifier: "7B85"))
+ XCTAssertNil(Build(identifier: "13.1.0"))
+ XCTAssertNil(Build(identifier: "13"))
+ XCTAssertNil(Build(identifier: "14B"))
+ }
+
+}