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")) + } + +}