Skip to content

Commit

Permalink
Merge pull request #238 from daltonclaybrook/dalton/xcode-version-select
Browse files Browse the repository at this point in the history
Update select command to use .xcode-version
  • Loading branch information
MattKiazyk authored Nov 11, 2022
2 parents 680a29d + f74075f commit 17f8c95
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 22 deletions.
16 changes: 16 additions & 0 deletions Sources/XcodesKit/Version+Xcode.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import Path
import Version

public extension Version {
Expand Down Expand Up @@ -55,6 +56,21 @@ public extension Version {
self = Version(major: major, minor: minor, patch: patch, prereleaseIdentifiers: prereleaseIdentifiers, buildMetadataIdentifiers: [buildMetadataIdentifier].compactMap { $0 })
}

/// Attempt to instatiate a `Version` using the `.xcode-version` file in the provided directory
static func fromXcodeVersionFile(inDirectory: Path = Path.cwd) -> Version? {
let xcodeVersionFilePath = inDirectory.join(".xcode-version")
guard
Current.files.fileExists(atPath: xcodeVersionFilePath.string),
let contents = Current.files.contents(atPath: xcodeVersionFilePath.string),
let versionString = String(data: contents, encoding: .utf8),
let version = Version(gemVersion: versionString)
else {
return nil
}

return version
}

/// The intent here is to match Apple's marketing version
///
/// Only show the patch number if it's not 0
Expand Down
30 changes: 11 additions & 19 deletions Sources/XcodesKit/XcodeInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,13 @@ public final class XcodeInstaller {
return self.downloadXcode(version: latestPrereleaseXcode.version, dataSource: dataSource, downloader: downloader, willInstall: willInstall)
}
case .path(let versionString, let path):
guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else {
guard let version = Version(xcodeVersion: versionString) ?? Version.fromXcodeVersionFile() else {
throw Error.invalidVersion(versionString)
}
let xcode = Xcode(version: version, url: path.url, filename: String(path.string.suffix(fromLast: "/")), releaseDate: nil)
return Promise.value((xcode, path.url))
case .version(let versionString):
guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else {
guard let version = Version(xcodeVersion: versionString) ?? Version.fromXcodeVersionFile() else {
throw Error.invalidVersion(versionString)
}
if willInstall, let installedXcode = Current.files.installedXcodes(destination).first(where: { $0.version.isEquivalent(to: version) }) {
Expand All @@ -280,14 +280,6 @@ public final class XcodeInstaller {
}
}

private func versionFromXcodeVersionFile() -> Version? {
let xcodeVersionFilePath = Path.cwd.join(".xcode-version")
let version = (try? Data(contentsOf: xcodeVersionFilePath.url))
.flatMap { String(data: $0, encoding: .utf8) }
.flatMap(Version.init(gemVersion:))
return version
}

private func downloadXcode(version: Version, dataSource: DataSource, downloader: Downloader, willInstall: Bool) -> Promise<(Xcode, URL)> {
return firstly { () -> Promise<Void> in
switch dataSource {
Expand Down Expand Up @@ -414,13 +406,13 @@ public final class XcodeInstaller {
.recover { error -> Promise<Void> in

if let error = error as? Client.Error {
switch error {
case .invalidUsernameOrPassword(_):
// remove any keychain password if we fail to log with an invalid username or password so it doesn't try again.
try? Current.keychain.remove(username)
default:
break
}
switch error {
case .invalidUsernameOrPassword(_):
// remove any keychain password if we fail to log with an invalid username or password so it doesn't try again.
try? Current.keychain.remove(username)
default:
break
}
}

return Promise(error: error)
Expand Down Expand Up @@ -529,7 +521,7 @@ public final class XcodeInstaller {

return attemptRetryableTask(maximumRetryCount: 3) {
let (progress, promise) = Current.shell.downloadWithAria2(
aria2Path,
aria2Path,
xcode.url,
destination,
cookies
Expand Down Expand Up @@ -561,7 +553,7 @@ public final class XcodeInstaller {
switch archiveURL.pathExtension {
case "xip":
return unarchiveAndMoveXIP(at: archiveURL, to: destinationURL, experimentalUnxip: experimentalUnxip).map { xcodeURL in
guard
guard
let path = Path(url: xcodeURL),
Current.files.fileExists(atPath: path.string),
let installedXcode = InstalledXcode(path: path)
Expand Down
3 changes: 2 additions & 1 deletion Sources/XcodesKit/XcodeSelect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ public func selectXcode(shouldPrint: Bool, pathOrVersion: String, directory: Pat
}
}

let versionToSelect = pathOrVersion.isEmpty ? Version.fromXcodeVersionFile() : Version(xcodeVersion: pathOrVersion)
let installedXcodes = Current.files.installedXcodes(directory)
if let version = Version(xcodeVersion: pathOrVersion),
if let version = versionToSelect,
let installedXcode = installedXcodes.first(withVersion: version) {
let selectedInstalledXcodeVersion = installedXcodes.first { output.out.hasPrefix($0.path.string) }.map { $0.version }
if installedXcode.version == selectedInstalledXcodeVersion {
Expand Down
4 changes: 2 additions & 2 deletions Sources/xcodes/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ struct Xcodes: AsyncParsableCommand {
}
.then { xcode -> Promise<Void> in
if select {
return selectXcode(shouldPrint: print, pathOrVersion: xcode.path.string, directory: destination, fallbackToInteractive: false)
return selectXcode(shouldPrint: print, pathOrVersion: xcode.path.string, directory: destination, fallbackToInteractive: false)
} else {
return .init()
}
Expand Down Expand Up @@ -378,7 +378,7 @@ struct Xcodes: AsyncParsableCommand {
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.
Select a version of Xcode by specifying a version number or an absolute path. Run without arguments to select the version specified in your .xcode-version file. If no version file is found, you will be prompted to interactively select from a list.
EXAMPLES:
xcodes select
Expand Down
61 changes: 61 additions & 0 deletions Tests/XcodesKitTests/XcodesKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,67 @@ final class XcodesKitTests: XCTestCase {
""")
}

func test_SelectUsingXcodeVersionFile() {
var log = ""
XcodesKit.Current.logging.log = { log.append($0 + "\n") }

// There are installed Xcodes
Current.files.installedXcodes = { _ in
[InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!,
InstalledXcode(path: Path("/Applications/Xcode-2.0.1.app")!)!]
}
Current.files.contentsAtPath = { path in
if path == "/Applications/Xcode-0.0.0.app/Contents/Info.plist" {
let url = Bundle.module.url(forResource: "Stub-0.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path == "/Applications/Xcode-2.0.1.app/Contents/Info.plist" {
let url = Bundle.module.url(forResource: "Stub-2.0.1.Info", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path.contains("version.plist") {
let url = Bundle.module.url(forResource: "Stub.version", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path.hasSuffix(".xcode-version") {
return "2.0.1\n".data(using: .utf8)
}
else {
return nil
}
}
Current.files.fileExistsAtPath = { path in
if path == "" {
return false
}
return true
}
// It prints the expected paths
var xcodeSelectPrintPathCallCount = 0
Current.shell.xcodeSelectPrintPath = {
defer { xcodeSelectPrintPathCallCount += 1 }
if xcodeSelectPrintPathCallCount == 0 {
return Promise.value((status: 0, out: "/Applications/Xcode-0.0.0.app/Contents/Developer", err: ""))
} else if xcodeSelectPrintPathCallCount == 1 {
return Promise.value((status: 0, out: "/Applications/Xcode-2.0.1.app/Contents/Developer", err: ""))
} else {
fatalError("Unexpected third invocation of xcode select")
}
}
// It successfully switches
Current.shell.xcodeSelectSwitch = { _, _ in
Promise.value((status: 0, out: "", err: ""))
}

selectXcode(shouldPrint: false, pathOrVersion: "", directory: Path.root.join("Applications"))
.cauterize()

XCTAssertEqual(log, """
Selected /Applications/Xcode-2.0.1.app/Contents/Developer
""")
}

func test_Installed_InteractiveTerminal() {
var log = ""
Expand Down

0 comments on commit 17f8c95

Please sign in to comment.