Skip to content

Commit a6dc97f

Browse files
committed
Auto-install matching Swift toolchain on version mismatch
1 parent a33776f commit a6dc97f

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

Sources/SkiaUICLI/BuildCommand.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ struct BuildCommand: ParsableCommand {
4343
print("Detected toolchain: \(identifier)")
4444
}
4545

46+
// Auto-install matching toolchain if compiler/SDK versions mismatch
47+
if env == nil, let sdkVersion = extractVersion(from: sdk) {
48+
if let compilerVersion = detectSwiftVersion(), sdkVersion != compilerVersion {
49+
try installMatchingToolchain(version: sdkVersion)
50+
51+
if let identifier = findToolchainIdentifier(version: sdkVersion) {
52+
env = ["TOOLCHAINS": identifier]
53+
print("Using installed toolchain: \(identifier)")
54+
} else {
55+
throw DetectionError.toolchainInstallFailed(version: sdkVersion)
56+
}
57+
}
58+
}
59+
4660
print("Building \(productName) for WebAssembly...")
4761
try shellExec(
4862
"/usr/bin/env",

Sources/SkiaUICLI/Helpers.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,16 @@ func detectProductName() throws -> String {
5757
enum DetectionError: Error, CustomStringConvertible {
5858
case noExecutableTarget
5959
case noWasmSDK
60+
case toolchainInstallFailed(version: String)
6061

6162
var description: String {
6263
switch self {
6364
case .noExecutableTarget:
6465
return "No executableTarget found in Package.swift. Use --product to specify one."
6566
case .noWasmSDK:
6667
return "No WASM SDK found. Install one with: swift sdk install <url>"
68+
case .toolchainInstallFailed(let version):
69+
return "Failed to install Swift \(version) toolchain. Please install manually from https://swift.org/install"
6770
}
6871
}
6972
}
@@ -132,11 +135,25 @@ func extractVersion(from sdk: String) -> String? {
132135
return String(match.1)
133136
}
134137

138+
/// Detects the current Swift compiler version (e.g. "6.2" or "6.2.4").
139+
func detectSwiftVersion() -> String? {
140+
guard let output = try? shellOutput("/usr/bin/env", arguments: ["swift", "--version"]) else {
141+
return nil
142+
}
143+
let pattern = #/Swift version (\d+\.\d+(?:\.\d+)?)/#
144+
guard let match = output.firstMatch(of: pattern) else { return nil }
145+
return String(match.1)
146+
}
147+
135148
/// Finds a matching toolchain's CFBundleIdentifier for the given SDK version.
136149
/// Searches both system and user toolchain directories.
137150
func detectToolchainIdentifier(for sdk: String) -> String? {
138151
guard let version = extractVersion(from: sdk) else { return nil }
152+
return findToolchainIdentifier(version: version)
153+
}
139154

155+
/// Searches for an xctoolchain bundle by version and returns its CFBundleIdentifier.
156+
func findToolchainIdentifier(version: String) -> String? {
140157
let toolchainName = "swift-\(version)-RELEASE.xctoolchain"
141158
let searchPaths = [
142159
NSHomeDirectory() + "/Library/Developer/Toolchains/" + toolchainName,
@@ -154,3 +171,47 @@ func detectToolchainIdentifier(for sdk: String) -> String? {
154171
}
155172
return nil
156173
}
174+
175+
/// Downloads and installs a Swift toolchain using the macOS installer.
176+
/// Installs to /Library/Developer/Toolchains/ (requires administrator privileges).
177+
func installMatchingToolchain(version: String) throws {
178+
let fm = FileManager.default
179+
let toolchainName = "swift-\(version)-RELEASE.xctoolchain"
180+
181+
// Skip if already installed (check both user and system paths)
182+
let checkPaths = [
183+
NSHomeDirectory() + "/Library/Developer/Toolchains/" + toolchainName,
184+
"/Library/Developer/Toolchains/" + toolchainName,
185+
]
186+
for path in checkPaths {
187+
if fm.fileExists(atPath: path) { return }
188+
}
189+
190+
let url = "https://download.swift.org/swift-\(version)-release/xcode/swift-\(version)-RELEASE/swift-\(version)-RELEASE-osx.pkg"
191+
let tmpDir = NSTemporaryDirectory() + "skiaui-toolchain-\(version)"
192+
let pkgPath = tmpDir + "/swift.pkg"
193+
194+
defer {
195+
try? fm.removeItem(atPath: tmpDir)
196+
}
197+
198+
try fm.createDirectory(atPath: tmpDir, withIntermediateDirectories: true)
199+
200+
print("")
201+
print("Swift compiler (\(detectSwiftVersion() ?? "unknown")) does not match WASM SDK (\(version)).")
202+
print("Auto-installing Swift \(version) toolchain...")
203+
print("")
204+
205+
try shellExec("/usr/bin/curl", arguments: [
206+
"-f", "-L", "--progress-bar",
207+
"-o", pkgPath, url,
208+
])
209+
210+
print("Installing toolchain (administrator password may be required)...")
211+
try shellExec("/usr/bin/sudo", arguments: [
212+
"/usr/sbin/installer", "-pkg", pkgPath, "-target", "/",
213+
])
214+
215+
print("Installed Swift \(version) toolchain.")
216+
print("")
217+
}

0 commit comments

Comments
 (0)