Skip to content

Commit e1011d1

Browse files
authored
Set TOOLCHAINS environment variable for macOS when proxying (#491)
* Set TOOLCHAINS environment variable for macOS when proxying * Handle additional cases such as already set environment variables and custom swiftly toolchains location * Refine the custom toolchains directory handling for macOS * Add the command-line tools to the end of the PATH in case the open source toolchains do not have the tool
1 parent 5efd761 commit e1011d1

File tree

6 files changed

+109
-22
lines changed

6 files changed

+109
-22
lines changed

.swift-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6.2.1
1+
6.2.3

Sources/LinuxPlatform/Linux.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,5 +649,10 @@ public struct Linux: Platform {
649649
self.swiftlyToolchainsDir(ctx) / "\(toolchain.name)"
650650
}
651651

652+
public func updateEnvironmentWithToolchain(_: SwiftlyCoreContext, _ environment: Environment, _: ToolchainVersion, path _: String) async throws -> Environment {
653+
// No explicit environment customization with the Linux platform
654+
environment
655+
}
656+
652657
public static let currentPlatform: any Platform = Linux()
653658
}

Sources/MacOSPlatform/MacOS.swift

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public struct SwiftPkgInfo: Codable {
1818
public struct MacOS: Platform {
1919
public init() {}
2020

21+
private let SWIFTLY_TOOLCHAINS_DIR = "SWIFTLY_TOOLCHAINS_DIR"
22+
2123
public var defaultSwiftlyHomeDir: FilePath {
2224
fs.home / ".swiftly"
2325
}
@@ -34,7 +36,7 @@ public struct MacOS: Platform {
3436

3537
public func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> FilePath {
3638
ctx.mockedHomeDir.map { $0 / "Toolchains" }
37-
?? ProcessInfo.processInfo.environment["SWIFTLY_TOOLCHAINS_DIR"].map { FilePath($0) }
39+
?? ProcessInfo.processInfo.environment[self.SWIFTLY_TOOLCHAINS_DIR].map { FilePath($0) }
3840
// This is where the installer will put the toolchains, and where Xcode can find them
3941
?? self.defaultToolchainsDirectory
4042
}
@@ -185,6 +187,22 @@ public struct MacOS: Platform {
185187

186188
let toolchainDir = self.swiftlyToolchainsDir(ctx) / "\(toolchain.identifier).xctoolchain"
187189

190+
let bundleID = try await findToolchainBundleID(ctx, toolchain)
191+
192+
try await fs.remove(atPath: toolchainDir)
193+
194+
if let bundleID {
195+
try? await sys.pkgutil(.volume(fs.home)).forget(pkg_id: bundleID).run(quiet: !verbose)
196+
}
197+
}
198+
199+
private func findToolchainBundleID(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> String? {
200+
guard toolchain != .xcode else {
201+
return nil
202+
}
203+
204+
let toolchainDir = self.swiftlyToolchainsDir(ctx) / "\(toolchain.identifier).xctoolchain"
205+
188206
let decoder = PropertyListDecoder()
189207
let infoPlist = toolchainDir / "Info.plist"
190208
let data = try await fs.cat(atPath: infoPlist)
@@ -193,9 +211,7 @@ public struct MacOS: Platform {
193211
throw SwiftlyError(message: "could not decode plist at \(infoPlist)")
194212
}
195213

196-
try await fs.remove(atPath: toolchainDir)
197-
198-
try? await sys.pkgutil(.volume(fs.home)).forget(pkg_id: pkgInfo.CFBundleIdentifier).run(quiet: !verbose)
214+
return pkgInfo.CFBundleIdentifier
199215
}
200216

201217
public func getExecutableName() -> String {
@@ -244,5 +260,83 @@ public struct MacOS: Platform {
244260
return self.swiftlyToolchainsDir(ctx) / "\(toolchain.identifier).xctoolchain"
245261
}
246262

263+
public func updateEnvironmentWithToolchain(_ ctx: SwiftlyCoreContext, _ environment: Environment, _ toolchain: ToolchainVersion, path: String) async throws -> Environment {
264+
var newEnv = environment
265+
266+
// On macOS, we try to set SDKROOT if its empty for tools like clang++ that need it to
267+
// find standard libraries that aren't in the toolchain, like libc++. Here we
268+
// use xcrun to tell us what the default sdk root should be.
269+
if ProcessInfo.processInfo.environment["SDKROOT"] == nil {
270+
newEnv = newEnv.updating([
271+
"SDKROOT": try? await run(
272+
.path(SystemPackage.FilePath("/usr/bin/xcrun")),
273+
arguments: ["--show-sdk-path"],
274+
output: .string(limit: 1024 * 10)
275+
).standardOutput?.replacingOccurrences(of: "\n", with: ""),
276+
])
277+
}
278+
279+
guard let bundleID = try await findToolchainBundleID(ctx, toolchain) else {
280+
return newEnv
281+
}
282+
283+
// If someday the two tools in the toolchain that require xcrun were to remove their dependency on it then
284+
// we can determine the maximum toolchain version where these environment variables are needed and abort early
285+
// here.
286+
287+
let TOOLCHAINS: Environment.Key = "TOOLCHAINS"
288+
let DEVELOPER_DIR: Environment.Key = "DEVELOPER_DIR"
289+
let PATH: Environment.Key = "PATH"
290+
291+
if let existingToolchains = ProcessInfo.processInfo.environment[TOOLCHAINS.rawValue], existingToolchains != bundleID {
292+
throw SwiftlyError(message: "You have already set \(TOOLCHAINS.rawValue) environment variable to \(existingToolchains), but swiftly has picked another toolchain. Please unset it or `swiftly use xcode` to use the Xcode selection mechanism.")
293+
}
294+
newEnv = newEnv.updating([TOOLCHAINS: bundleID])
295+
296+
// Create a compatible DEVELOPER_DIR in case of a custom swiftly toolchain location (not ~/Library/Developer/Toolchains)
297+
if let swiftlyToolchainsDir = ProcessInfo.processInfo.environment[SWIFTLY_TOOLCHAINS_DIR],
298+
case let customToolchainsDir = FilePath(swiftlyToolchainsDir),
299+
customToolchainsDir != defaultToolchainsDirectory
300+
{
301+
// Simulate a custom CommandLineTools within the swiftly home directory that satisfies xcrun and allows it to find
302+
// the selected toolchain on the PATH with the selected toolchain in front. This command-line tools will only have
303+
// the expected libxcrun.dylib in it and no other tools in its usr/bin directory so that none are picked up there by xcrun.
304+
305+
// We need a macOS CLT to be installed for this to work
306+
let realCltDir = FilePath("/Library/Developer/CommandLineTools")
307+
if !(try await fs.exists(atPath: realCltDir)) {
308+
throw SwiftlyError(message: "The macOS command line tools must be installed to support a custom SWIFTLY_TOOLCHAIN_DIR for macOS. You can install it using `xcode-select --install`")
309+
}
310+
311+
let commandLineToolsDir = swiftlyHomeDir(ctx) / "CommandLineTools"
312+
if !(try await fs.exists(atPath: commandLineToolsDir)) {
313+
try await fs.mkdir(atPath: commandLineToolsDir)
314+
}
315+
316+
let usrLibDir = commandLineToolsDir / "usr" / "lib"
317+
if !(try await fs.exists(atPath: usrLibDir)) {
318+
try await fs.mkdir(.parents, atPath: usrLibDir)
319+
}
320+
321+
let xcrunLibLink = usrLibDir / "libxcrun.dylib"
322+
if !(try await fs.exists(atPath: xcrunLibLink)) {
323+
try await fs.symlink(atPath: xcrunLibLink, linkPath: realCltDir / "usr/lib/libxcrun.dylib")
324+
}
325+
326+
let developerDir: FilePath = commandLineToolsDir
327+
328+
if let developerDirEnv = ProcessInfo.processInfo.environment[DEVELOPER_DIR.rawValue],
329+
developerDirEnv != developerDir.string
330+
{
331+
throw SwiftlyError(message: "You have set \(DEVELOPER_DIR.rawValue) environment variable to \(developerDirEnv), but swiftly is trying to use its own location so that a toolchain can be selected. Please unset the environment variable and try again.")
332+
}
333+
334+
newEnv = newEnv.updating([DEVELOPER_DIR: developerDir.string])
335+
newEnv = newEnv.updating([PATH: path + ":" + (realCltDir / "usr/bin").string])
336+
}
337+
338+
return newEnv
339+
}
340+
247341
public static let currentPlatform: any Platform = MacOS()
248342
}

Sources/Swiftly/Run.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ struct Run: SwiftlyCommand {
138138
return
139139
}
140140

141-
let result = try await Subprocess.run(.path(FilePath(command[0])), arguments: Arguments([String](command[1...])), environment: env, input: .standardInput, output: .standardOutput, error: .standardError)
141+
let result = try await Subprocess.run(processConfig, input: .standardInput, output: .standardOutput, error: .standardError)
142142
if !result.terminationStatus.isSuccess {
143143
throw RunProgramError(terminationStatus: result.terminationStatus, config: processConfig)
144144
}

Sources/SwiftlyCore/Platform+Process.swift

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,7 @@ extension Platform {
3939
pathComponents.removeAll(where: { $0 == swiftlyBinDir.string })
4040

4141
environment = environment.updating(["PATH": String(pathComponents.joined(separator: ":"))])
42-
43-
#if os(macOS)
44-
// On macOS, we try to set SDKROOT if its empty for tools like clang++ that need it to
45-
// find standard libraries that aren't in the toolchain, like libc++. Here we
46-
// use xcrun to tell us what the default sdk root should be.
47-
if ProcessInfo.processInfo.environment["SDKROOT"] == nil {
48-
environment = environment.updating([
49-
"SDKROOT": try? await run(
50-
.path(SystemPackage.FilePath("/usr/bin/xcrun")),
51-
arguments: ["--show-sdk-path"],
52-
output: .string(limit: 1024 * 10)
53-
).standardOutput?.replacingOccurrences(of: "\n", with: ""),
54-
]
55-
)
56-
}
57-
#endif
42+
environment = try await self.updateEnvironmentWithToolchain(ctx, environment, toolchain, path: String(pathComponents.joined(separator: ":")))
5843

5944
return environment
6045
}

Sources/SwiftlyCore/Platform.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ public protocol Platform: Sendable {
138138

139139
/// Find the location of the toolchain binaries.
140140
func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> FilePath
141+
142+
/// Update the process environment used when proxying based on the selected toolchain.
143+
func updateEnvironmentWithToolchain(_ ctx: SwiftlyCoreContext, _ environment: Environment, _ toolchain: ToolchainVersion, path: String) async throws -> Environment
141144
}
142145

143146
extension Platform {

0 commit comments

Comments
 (0)