Skip to content

Create a special xcode selector that will delegates to xcrun for macOS #317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ Likewise, the latest snapshot associated with a given development branch can be
$ swiftly use 5.7-snapshot
$ swiftly use main-snapshot

macOS ONLY: There is a special selector for swiftly to use your Xcode toolchain. If there are multiple versions of Xcode then swiftly will use the currently selected toolchain from xcode-select.

$ swiftly use xcode


**--version:**

Expand Down
11 changes: 9 additions & 2 deletions Sources/MacOSPlatform/MacOS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,16 @@ public struct MacOS: Platform {
return "/bin/zsh"
}

public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> FilePath
public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> FilePath
{
self.swiftlyToolchainsDir(ctx) / "\(toolchain.identifier).xctoolchain"
if toolchain == .xcodeVersion {
// Print the toolchain location with the help of xcrun
if let xcrunLocation = try? await self.runProgramOutput("/usr/bin/xcrun", "-f", "swift") {
return FilePath(xcrunLocation.replacingOccurrences(of: "\n", with: "")).removingLastComponent().removingLastComponent().removingLastComponent()
}
}

return self.swiftlyToolchainsDir(ctx) / "\(toolchain.identifier).xctoolchain"
}

public static let currentPlatform: any Platform = MacOS()
Expand Down
10 changes: 8 additions & 2 deletions Sources/Swiftly/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,14 @@ public struct Config: Codable, Equatable, Sendable {
}

public func listInstalledToolchains(selector: ToolchainSelector?) -> [ToolchainVersion] {
#if os(macOS)
let systemToolchains: [ToolchainVersion] = [.xcodeVersion]
#else
let systemToolchains: [ToolchainVersion] = []
#endif

guard let selector else {
return Array(self.installedToolchains)
return Array(self.installedToolchains) + systemToolchains
}

if case .latest = selector {
Expand All @@ -64,7 +70,7 @@ public struct Config: Codable, Equatable, Sendable {
return ts
}

return self.installedToolchains.filter { toolchain in
return (self.installedToolchains + systemToolchains).filter { toolchain in
selector.matches(toolchain: toolchain)
}
}
Expand Down
6 changes: 5 additions & 1 deletion Sources/Swiftly/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ struct Install: SwiftlyCommand {
let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(ctx)
let swiftlyBinDirContents =
(try? await fs.ls(atPath: swiftlyBinDir)) ?? [String]()
let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(ctx, version)
let toolchainBinDir = try await Swiftly.currentPlatform.findToolchainBinDir(ctx, version)
let toolchainBinDirContents = try await fs.ls(atPath: toolchainBinDir)

var existingProxies: [String] = []
Expand Down Expand Up @@ -311,6 +311,8 @@ struct Install: SwiftlyCommand {
case .main:
category = "development"
}
case .xcode:
fatalError("unreachable: xcode toolchain cannot be installed with swiftly")
}

let animation: ProgressAnimationProtocol =
Expand Down Expand Up @@ -506,6 +508,8 @@ struct Install: SwiftlyCommand {
}

return .snapshot(firstSnapshot)
case .xcode:
throw SwiftlyError(message: "xcode toolchains are not available from swift.org")
}
}
}
2 changes: 1 addition & 1 deletion Sources/Swiftly/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct List: SwiftlyCommand {
let toolchains = config.listInstalledToolchains(selector: selector).sorted { $0 > $1 }
let (inUse, _) = try await selectToolchain(ctx, config: &config)

let installedToolchainInfos = toolchains.compactMap { toolchain -> InstallToolchainInfo? in
var installedToolchainInfos = toolchains.compactMap { toolchain -> InstallToolchainInfo? in
InstallToolchainInfo(
version: toolchain,
inUse: inUse == toolchain,
Expand Down
16 changes: 16 additions & 0 deletions Sources/Swiftly/OutputSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ struct AvailableToolchainInfo: OutputData {
try versionContainer.encode(major, forKey: .major)
try versionContainer.encode(minor, forKey: .minor)
}
case .xcode:
try versionContainer.encode("system", forKey: .type)
}
}
}
Expand Down Expand Up @@ -233,6 +235,8 @@ struct InstallToolchainInfo: OutputData {
try versionContainer.encode(major, forKey: .major)
try versionContainer.encode(minor, forKey: .minor)
}
case .xcode:
try versionContainer.encode("system", forKey: .type)
}
}

Expand Down Expand Up @@ -279,6 +283,9 @@ struct InstallToolchainInfo: OutputData {
branch: branch,
date: date
))
case "system":
// The only system toolchain that exists at the moment is the xcode version
self.version = .xcode
default:
throw DecodingError.dataCorruptedError(
forKey: ToolchainVersionCodingKeys.type,
Expand Down Expand Up @@ -314,6 +321,8 @@ struct InstalledToolchainsListInfo: OutputData {
"main development snapshot"
case let .snapshot(.release(major, minor), nil):
"\(major).\(minor) development snapshot"
case .xcode:
"xcode"
default:
"matching"
}
Expand All @@ -334,6 +343,13 @@ struct InstalledToolchainsListInfo: OutputData {
lines.append("Installed snapshot toolchains")
lines.append("-----------------------------")
lines.append(contentsOf: snapshotToolchains.map(\.description))

#if os(macOS)
lines.append("")
lines.append("Available system toolchains")
lines.append("---------------------------")
lines.append(ToolchainVersion.xcode.description)
#endif
}

return lines.joined(separator: "\n")
Expand Down
10 changes: 8 additions & 2 deletions Sources/Swiftly/Uninstall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct Uninstall: SwiftlyCommand {

let startingConfig = try await Config.load(ctx)

let toolchains: [ToolchainVersion]
var toolchains: [ToolchainVersion]
if self.toolchain == "all" {
// Sort the uninstalled toolchains such that the in-use toolchain will be uninstalled last.
// This avoids printing any unnecessary output from using new toolchains while the uninstall is in progress.
Expand All @@ -72,8 +72,11 @@ struct Uninstall: SwiftlyCommand {
toolchains = installedToolchains
}

// Filter out the xcode toolchain here since it is not uninstallable
toolchains.removeAll(where: { $0 == .xcodeVersion })

guard !toolchains.isEmpty else {
await ctx.message("No toolchains matched \"\(self.toolchain)\"")
await ctx.message("No toolchains can be uninstalled that match \"\(self.toolchain)\"")
return
}

Expand Down Expand Up @@ -105,6 +108,9 @@ struct Uninstall: SwiftlyCommand {
case let .snapshot(s):
// If a snapshot was previously in use, switch to the latest snapshot associated with that branch.
selector = .snapshot(branch: s.branch, date: nil)
case .xcode:
// Xcode will not be in the list of installed toolchains, so this is only here for completeness
selector = .xcode
}

if let toUse = config.listInstalledToolchains(selector: selector)
Expand Down
2 changes: 2 additions & 0 deletions Sources/Swiftly/Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ struct Update: SwiftlyCommand {
default:
fatalError("unreachable")
}
case let .xcode:
throw SwiftlyError(message: "xcode cannot be updated from swiftly")
}
}

Expand Down
10 changes: 8 additions & 2 deletions Sources/Swiftly/Use.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ struct Use: SwiftlyCommand {

$ swiftly use 5.7-snapshot
$ swiftly use main-snapshot

macOS ONLY: There is a special selector for swiftly to use your Xcode toolchain. \
If there are multiple versions of Xcode then swiftly will use the currently selected \
toolchain from xcode-select.

$ swiftly use xcode
"""
))
var toolchain: String?
Expand Down Expand Up @@ -87,7 +93,7 @@ struct Use: SwiftlyCommand {
}

if self.printLocation {
let location = LocationInfo(path: "\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion))")
let location = LocationInfo(path: "\(try await Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion))")
try await ctx.output(location)
return
}
Expand Down Expand Up @@ -251,7 +257,7 @@ public func selectToolchain(_ ctx: SwiftlyCoreContext, config: inout Config, glo

// Check to ensure that the global default in use toolchain matches one of the installed toolchains, and return
// no selected toolchain if it doesn't.
guard let defaultInUse = config.inUse, config.installedToolchains.contains(defaultInUse) else {
guard let defaultInUse = config.inUse, config.listInstalledToolchains(selector: nil).contains(defaultInUse) else {
return (nil, .globalDefault)
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/SwiftlyCore/Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ public protocol Platform: Sendable {
func getShell() async throws -> String

/// Find the location where the toolchain should be installed.
func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> FilePath
func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> FilePath

/// Find the location of the toolchain binaries.
func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> FilePath
func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> FilePath
}

extension Platform {
Expand Down Expand Up @@ -164,7 +164,7 @@ extension Platform {
func proxyEnv(_ ctx: SwiftlyCoreContext, env: [String: String], toolchain: ToolchainVersion) async throws -> [String: String] {
var newEnv = env

let tcPath = self.findToolchainLocation(ctx, toolchain) / "usr/bin"
let tcPath = try await self.findToolchainLocation(ctx, toolchain) / "usr/bin"
guard try await fs.exists(atPath: tcPath) else {
throw SwiftlyError(
message:
Expand Down Expand Up @@ -193,7 +193,7 @@ extension Platform {
/// the exit code and program information.
///
public func proxy(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String], _ env: [String: String] = [:]) async throws {
let tcPath = self.findToolchainLocation(ctx, toolchain) / "usr/bin"
let tcPath = (try await self.findToolchainLocation(ctx, toolchain)) / "usr/bin"

let commandTcPath = tcPath / command
let commandToRun = if try await fs.exists(atPath: commandTcPath) {
Expand Down Expand Up @@ -225,7 +225,7 @@ extension Platform {
/// the exit code and program information.
///
public func proxyOutput(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String]) async throws -> String? {
let tcPath = self.findToolchainLocation(ctx, toolchain) / "usr/bin"
let tcPath = (try await self.findToolchainLocation(ctx, toolchain)) / "usr/bin"

let commandTcPath = tcPath / command
let commandToRun = if try await fs.exists(atPath: commandTcPath) {
Expand Down Expand Up @@ -479,9 +479,9 @@ extension Platform {
return try await fs.exists(atPath: swiftlyHomeBin) ? swiftlyHomeBin : nil
}

public func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> FilePath
public func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> FilePath
{
self.findToolchainLocation(ctx, toolchain) / "usr/bin"
(try await self.findToolchainLocation(ctx, toolchain)) / "usr/bin"
}

#endif
Expand Down
34 changes: 32 additions & 2 deletions Sources/SwiftlyCore/ToolchainVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public enum ToolchainVersion: Sendable {

case stable(StableRelease)
case snapshot(Snapshot)
case xcode

public init(major: Int, minor: Int, patch: Int) {
self = .stable(StableRelease(major: major, minor: minor, patch: patch))
Expand All @@ -99,6 +100,8 @@ public enum ToolchainVersion: Sendable {
self = .snapshot(Snapshot(branch: snapshotBranch, date: date))
}

public static let xcodeVersion: ToolchainVersion = .xcode

static func stableRegex() -> Regex<(Substring, Substring, Substring, Substring)> {
try! Regex("^(?:Swift )?(\\d+)\\.(\\d+)\\.(\\d+)$")
}
Expand Down Expand Up @@ -132,6 +135,8 @@ public enum ToolchainVersion: Sendable {
throw SwiftlyError(message: "invalid release snapshot version: \(string)")
}
self = ToolchainVersion(snapshotBranch: .release(major: major, minor: minor), date: String(match.output.3))
} else if string == "xcode" {
self = ToolchainVersion.xcodeVersion
} else {
throw SwiftlyError(message: "invalid toolchain version: \"\(string)\"")
}
Expand Down Expand Up @@ -176,6 +181,8 @@ public enum ToolchainVersion: Sendable {
case let .release(major, minor):
return "\(major).\(minor)-snapshot-\(release.date)"
}
case .xcode:
return "xcode"
}
}

Expand All @@ -194,6 +201,8 @@ public enum ToolchainVersion: Sendable {
case let .release(major, minor):
return "swift-\(major).\(minor)-DEVELOPMENT-SNAPSHOT-\(release.date)-a"
}
case .xcode:
return "xcode"
}
}
}
Expand All @@ -214,6 +223,8 @@ extension ToolchainVersion: CustomStringConvertible {
return "\(release)"
case let .snapshot(snapshot):
return "\(snapshot)"
case .xcode:
return "xcode"
}
}
}
Expand All @@ -231,6 +242,14 @@ extension ToolchainVersion: Comparable {
return false
case (.stable, .snapshot):
return !(rhs < lhs)
case (.xcode, .xcode):
return false
case (.xcode, _):
return false
case (_, .xcode):
return true
default:
return false
}
}
}
Expand All @@ -254,6 +273,9 @@ public enum ToolchainSelector: Sendable {
/// associated with the given branch.
case snapshot(branch: ToolchainVersion.Snapshot.Branch, date: String?)

/// Selects the Xcode of the current system.
case xcode

public init(major: Int, minor: Int? = nil, patch: Int? = nil) {
self = .stable(major: major, minor: minor, patch: patch)
}
Expand All @@ -267,14 +289,19 @@ public enum ToolchainSelector: Sendable {
return
}

if input == "xcode" {
self = Self.xcode
return
}

throw SwiftlyError(message: "invalid toolchain selector: \"\(input)\"")
}

public func isReleaseSelector() -> Bool {
switch self {
case .latest, .stable:
return true
case .snapshot:
default:
return false
}
}
Expand Down Expand Up @@ -312,7 +339,8 @@ public enum ToolchainSelector: Sendable {
}
}
return true

case (.xcode, .xcode):
return true
default:
return false
}
Expand Down Expand Up @@ -341,6 +369,8 @@ extension ToolchainSelector: CustomStringConvertible {
s += "-\(date)"
}
return s
case .xcode:
return "xcode"
}
}
}
Expand Down
Loading