Skip to content

Commit 41553c1

Browse files
authored
Allow initializing the build system with the containing toolchain as the developer path (#381)
1 parent 086ceee commit 41553c1

40 files changed

+395
-240
lines changed

Sources/SWBBuildService/BuildService.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public struct MsgHandlingError: Swift.Error {
3434

3535
private struct CoreCacheKey: Equatable, Hashable {
3636
/// The path of the developer directory.
37-
let developerPath: Path?
37+
let developerPath: SWBProtocol.DeveloperPath?
3838

3939
/// The inferior build products path, if defined.
4040
let inferiorProducts: Path?
@@ -146,7 +146,7 @@ open class BuildService: Service, @unchecked Sendable {
146146
}
147147

148148
/// Convenience overload which throws if the Core had initialization errors.
149-
func sharedCore(developerPath: Path?, inferiorProducts: Path? = nil, environment: [String: String] = [:]) async throws -> Core {
149+
func sharedCore(developerPath: SWBProtocol.DeveloperPath?, inferiorProducts: Path? = nil, environment: [String: String] = [:]) async throws -> Core {
150150
let (c, diagnostics) = await sharedCore(developerPath: developerPath, inferiorProducts: inferiorProducts, environment: environment)
151151
guard let core = c else {
152152
throw ServiceError.unableToInitializeCore(errors: diagnostics.map { $0.formatLocalizedDescription(.debug) })
@@ -157,7 +157,7 @@ open class BuildService: Service, @unchecked Sendable {
157157
/// Get a shared core instance.
158158
///
159159
/// We use an explicit cache so that we can minimize the number of cores we load while still keeping a flexible public interface that doesn't require all clients to provide all possible required parameters for core initialization (which is useful for testing and debug purposes).
160-
func sharedCore(developerPath: Path?, resourceSearchPaths: [Path] = [], inferiorProducts: Path? = nil, environment: [String: String] = [:]) async -> (Core?, [Diagnostic]) {
160+
func sharedCore(developerPath: SWBProtocol.DeveloperPath?, resourceSearchPaths: [Path] = [], inferiorProducts: Path? = nil, environment: [String: String] = [:]) async -> (Core?, [Diagnostic]) {
161161
let key = CoreCacheKey(developerPath: developerPath, inferiorProducts: inferiorProducts, environment: environment)
162162
return await sharedCoreCacheLock.withLock {
163163
if let existing = sharedCoreCache[key] {
@@ -191,7 +191,17 @@ open class BuildService: Service, @unchecked Sendable {
191191
}
192192
}
193193
let delegate = Delegate()
194-
let (core, diagnostics) = await (Core.getInitializedCore(delegate, pluginManager: pluginManager, developerPath: developerPath, resourceSearchPaths: resourceSearchPaths, inferiorProductsPath: inferiorProducts, environment: environment, buildServiceModTime: buildServiceModTime, connectionMode: connectionMode), delegate.diagnostics)
194+
let coreDeveloperPath: Core.DeveloperPath?
195+
switch developerPath {
196+
case .xcode(let path):
197+
coreDeveloperPath = .xcode(path)
198+
case .swiftToolchain(let path):
199+
let xcodeDeveloperPath = try? await Xcode.getActiveDeveloperDirectoryPath()
200+
coreDeveloperPath = .swiftToolchain(path, xcodeDeveloperPath: xcodeDeveloperPath)
201+
case nil:
202+
coreDeveloperPath = nil
203+
}
204+
let (core, diagnostics) = await (Core.getInitializedCore(delegate, pluginManager: pluginManager, developerPath: coreDeveloperPath, resourceSearchPaths: resourceSearchPaths, inferiorProductsPath: inferiorProducts, environment: environment, buildServiceModTime: buildServiceModTime, connectionMode: connectionMode), delegate.diagnostics)
195205
delegate.freeze()
196206
sharedCoreCache[key] = (core, diagnostics)
197207
return (core, diagnostics)

Sources/SWBBuildService/Messages.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ private struct CreateXCFrameworkHandler: MessageHandler {
7777
guard let buildService = request.service as? BuildService else {
7878
throw StubError.error("service object is not of type BuildService")
7979
}
80-
let (result, output) = try await XCFramework.createXCFramework(commandLine: message.commandLine, currentWorkingDirectory: message.currentWorkingDirectory, infoLookup: buildService.sharedCore(developerPath: message.effectiveDeveloperPath))
80+
let (result, output) = try await XCFramework.createXCFramework(commandLine: message.commandLine, currentWorkingDirectory: message.currentWorkingDirectory, infoLookup: buildService.sharedCore(developerPath: message.effectiveDeveloperPath.map { .xcode($0) }))
8181
if !result {
8282
throw StubError.error(output)
8383
}
@@ -113,7 +113,7 @@ private struct ProductTypeSupportsMacCatalystHandler: MessageHandler {
113113
guard let buildService = request.service as? BuildService else {
114114
throw StubError.error("service object is not of type BuildService")
115115
}
116-
return try await BoolResponse(buildService.sharedCore(developerPath: message.effectiveDeveloperPath).productTypeSupportsMacCatalyst(productTypeIdentifier: message.productTypeIdentifier))
116+
return try await BoolResponse(buildService.sharedCore(developerPath: message.effectiveDeveloperPath.map { .xcode($0) }).productTypeSupportsMacCatalyst(productTypeIdentifier: message.productTypeIdentifier))
117117
}
118118
}
119119

@@ -122,8 +122,16 @@ private struct ProductTypeSupportsMacCatalystHandler: MessageHandler {
122122
private struct CreateSessionHandler: MessageHandler {
123123
func handle(request: Request, message: CreateSessionRequest) async throws -> CreateSessionResponse {
124124
let service = request.buildService
125+
let developerPath: DeveloperPath?
126+
if let devPath = message.developerPath2 {
127+
developerPath = devPath
128+
} else if let devPath = message.effectiveDeveloperPath {
129+
developerPath = .xcode(devPath)
130+
} else {
131+
developerPath = nil
132+
}
125133
let (core, diagnostics) = await service.sharedCore(
126-
developerPath: message.effectiveDeveloperPath,
134+
developerPath: developerPath,
127135
resourceSearchPaths: message.resourceSearchPaths ?? [],
128136
inferiorProducts: message.inferiorProductsPath,
129137
environment: message.environment ?? [:]
@@ -184,7 +192,7 @@ extension SetSessionWorkspaceContainerPathRequest: PIFProvidingRequest {
184192
try fs.createDirectory(dir, recursive: true)
185193
let pifPath = dir.join(Foundation.UUID().description + ".json")
186194
let argument = isProject ? "-project" : "-workspace"
187-
let result = try await Process.getOutput(url: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["xcodebuild", "-dumpPIF", pifPath.str, argument, path.str], currentDirectoryURL: URL(fileURLWithPath: containerPath.dirname.str, isDirectory: true), environment: Environment.current.addingContents(of: [.developerDir: session.core.developerPath.str]))
195+
let result = try await Process.getOutput(url: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["xcodebuild", "-dumpPIF", pifPath.str, argument, path.str], currentDirectoryURL: URL(fileURLWithPath: containerPath.dirname.str, isDirectory: true), environment: Environment.current.addingContents(of: [.developerDir: session.core.developerPath.path.str]))
188196
if !result.exitStatus.isSuccess {
189197
throw StubError.error("Could not dump PIF for '\(path.str)': \(String(decoding: result.stderr, as: UTF8.self))")
190198
}
@@ -1200,7 +1208,7 @@ private struct ClientExchangeResponseMsg<MessageType: ClientExchangeMessage & Re
12001208
private struct DeveloperPathHandler: MessageHandler {
12011209
func handle(request: Request, message: DeveloperPathRequest) throws -> StringResponse {
12021210
let session = try request.session(for: message)
1203-
return StringResponse(session.core.developerPath.str)
1211+
return StringResponse(session.core.developerPath.path.str)
12041212
}
12051213
}
12061214

@@ -1469,7 +1477,7 @@ private struct ExecuteCommandLineToolMsg: MessageHandler {
14691477
request.service.send(message.replyChannel, BoolResponse(false))
14701478
return
14711479
}
1472-
let (core, diagnostics) = await buildService.sharedCore(developerPath: message.developerPath)
1480+
let (core, diagnostics) = await buildService.sharedCore(developerPath: message.developerPath.map { .xcode($0) })
14731481
guard let core else {
14741482
for diagnostic in diagnostics where diagnostic.behavior == .error {
14751483
request.service.send(message.replyChannel, ErrorResponse(diagnostic.formatLocalizedDescription(.messageOnly)))

Sources/SWBCore/Core.swift

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public final class Core: Sendable {
4040
/// Get a configured instance of the core.
4141
///
4242
/// - returns: An initialized Core instance on which all discovery and loading will have been completed. If there are errors during that process, they will be logged to `stderr` and no instance will be returned. Otherwise, the initialized object is returned.
43-
public static func getInitializedCore(_ delegate: any CoreDelegate, pluginManager: PluginManager, developerPath: Path? = nil, resourceSearchPaths: [Path] = [], inferiorProductsPath: Path? = nil, extraPluginRegistration: @PluginExtensionSystemActor (_ pluginPaths: [Path]) -> Void = { _ in }, additionalContentPaths: [Path] = [], environment: [String:String] = [:], buildServiceModTime: Date, connectionMode: ServiceHostConnectionMode) async -> Core? {
43+
public static func getInitializedCore(_ delegate: any CoreDelegate, pluginManager: PluginManager, developerPath: DeveloperPath? = nil, resourceSearchPaths: [Path] = [], inferiorProductsPath: Path? = nil, extraPluginRegistration: @PluginExtensionSystemActor (_ pluginPaths: [Path]) -> Void = { _ in }, additionalContentPaths: [Path] = [], environment: [String:String] = [:], buildServiceModTime: Date, connectionMode: ServiceHostConnectionMode) async -> Core? {
4444
// Enable macro expression interning during loading.
4545
return await MacroNamespace.withExpressionInterningEnabled {
4646
let hostOperatingSystem: OperatingSystem
@@ -59,9 +59,9 @@ public final class Core: Sendable {
5959
await extraPluginRegistration([])
6060
#endif
6161

62-
let resolvedDeveloperPath: String
62+
let resolvedDeveloperPath: DeveloperPath
6363
do {
64-
if let resolved = developerPath?.nilIfEmpty?.str {
64+
if let resolved = developerPath {
6565
resolvedDeveloperPath = resolved
6666
} else {
6767
let values = try await Set(pluginManager.extensions(of: DeveloperDirectoryExtensionPoint.self).asyncMap { try await $0.fallbackDeveloperDirectory(hostOperatingSystem: hostOperatingSystem) }).compactMap { $0 }
@@ -70,7 +70,12 @@ public final class Core: Sendable {
7070
delegate.error("Could not determine path to developer directory because no extensions provided a fallback value")
7171
return nil
7272
case 1:
73-
resolvedDeveloperPath = values[0].str
73+
let path = values[0]
74+
if path.str.hasSuffix(".app/Contents/Developer") {
75+
resolvedDeveloperPath = .xcode(path)
76+
} else {
77+
resolvedDeveloperPath = .fallback(values[0])
78+
}
7479
default:
7580
delegate.error("Could not determine path to developer directory because multiple extensions provided conflicting fallback values: \(values.sorted().map { $0.str }.joined(separator: ", "))")
7681
return nil
@@ -169,8 +174,26 @@ public final class Core: Sendable {
169174

170175
public let pluginManager: PluginManager
171176

177+
public enum DeveloperPath: Sendable, Hashable {
178+
// A path to an Xcode install's "/Contents/Developer" directory
179+
case xcode(Path)
180+
181+
// A path to the root of a Swift toolchain, optionally paired with the developer path of an installed Xcode
182+
case swiftToolchain(Path, xcodeDeveloperPath: Path?)
183+
184+
// A fallback resolved path.
185+
case fallback(Path)
186+
187+
public var path: Path {
188+
switch self {
189+
case .xcode(let path), .swiftToolchain(let path, xcodeDeveloperPath: _), .fallback(let path):
190+
return path
191+
}
192+
}
193+
}
194+
172195
/// The path to the "Developer" directory.
173-
public let developerPath: Path
196+
public let developerPath: DeveloperPath
174197

175198
/// Additional search paths to be used when looking up resource bundles.
176199
public let resourceSearchPaths: [Path]
@@ -205,31 +228,39 @@ public final class Core: Sendable {
205228

206229
public let connectionMode: ServiceHostConnectionMode
207230

208-
@_spi(Testing) public init(delegate: any CoreDelegate, hostOperatingSystem: OperatingSystem, pluginManager: PluginManager, developerPath: String, resourceSearchPaths: [Path], inferiorProductsPath: Path?, additionalContentPaths: [Path], environment: [String:String], buildServiceModTime: Date, connectionMode: ServiceHostConnectionMode) async throws {
231+
@_spi(Testing) public init(delegate: any CoreDelegate, hostOperatingSystem: OperatingSystem, pluginManager: PluginManager, developerPath: DeveloperPath, resourceSearchPaths: [Path], inferiorProductsPath: Path?, additionalContentPaths: [Path], environment: [String:String], buildServiceModTime: Date, connectionMode: ServiceHostConnectionMode) async throws {
209232
self.delegate = delegate
210233
self.hostOperatingSystem = hostOperatingSystem
211234
self.pluginManager = pluginManager
212-
self.developerPath = Path(developerPath)
235+
self.developerPath = developerPath
213236
self.resourceSearchPaths = resourceSearchPaths
214237
self.inferiorProductsPath = inferiorProductsPath
215238
self.additionalContentPaths = additionalContentPaths
216239
self.buildServiceModTime = buildServiceModTime
217240
self.connectionMode = connectionMode
218241
self.environment = environment
219242

220-
let versionPath = self.developerPath.dirname.join("version.plist")
243+
switch developerPath {
244+
case .xcode(let path):
245+
let versionPath = path.dirname.join("version.plist")
221246

222-
// Load the containing app (Xcode or Playgrounds) version information, if available.
223-
//
224-
// We make this optional so tests do not need to provide it.
225-
if let info = try XcodeVersionInfo.versionInfo(versionPath: versionPath) {
226-
self.xcodeVersion = info.shortVersion
247+
// Load the containing app (Xcode or Playgrounds) version information, if available.
248+
//
249+
// We make this optional so tests do not need to provide it.
250+
if let info = try XcodeVersionInfo.versionInfo(versionPath: versionPath) {
251+
self.xcodeVersion = info.shortVersion
227252

228-
// If the ProductBuildVersion key is missing, we use "UNKNOWN" as the value.
229-
self.xcodeProductBuildVersion = info.productBuildVersion ?? ProductBuildVersion(major: 0, train: "A", build: 0, buildSuffix: "")
230-
self.xcodeProductBuildVersionString = info.productBuildVersion?.description ?? "UNKNOWN"
231-
} else {
232-
// Set an arbitrary version for testing purposes.
253+
// If the ProductBuildVersion key is missing, we use "UNKNOWN" as the value.
254+
self.xcodeProductBuildVersion = info.productBuildVersion ?? ProductBuildVersion(major: 0, train: "A", build: 0, buildSuffix: "")
255+
self.xcodeProductBuildVersionString = info.productBuildVersion?.description ?? "UNKNOWN"
256+
} else {
257+
// Set an arbitrary version for testing purposes.
258+
self.xcodeVersion = Version(99, 99, 99)
259+
self.xcodeProductBuildVersion = ProductBuildVersion(major: 99, train: "T", build: 999)
260+
self.xcodeProductBuildVersionString = xcodeProductBuildVersion.description
261+
}
262+
case .swiftToolchain, .fallback:
263+
// FIXME: Eliminate this requirment for Swift toolchains
233264
self.xcodeVersion = Version(99, 99, 99)
234265
self.xcodeProductBuildVersion = ProductBuildVersion(major: 99, train: "T", build: 999)
235266
self.xcodeProductBuildVersionString = xcodeProductBuildVersion.description
@@ -242,7 +273,17 @@ public final class Core: Sendable {
242273
self.toolchainPaths = {
243274
var toolchainPaths = [(Path, strict: Bool)]()
244275

245-
toolchainPaths.append((Path(developerPath).join("Toolchains"), strict: developerPath.hasSuffix(".app/Contents/Developer")))
276+
switch developerPath {
277+
case .xcode(let path):
278+
toolchainPaths.append((path.join("Toolchains"), strict: path.str.hasSuffix(".app/Contents/Developer")))
279+
case .swiftToolchain(let path, xcodeDeveloperPath: let xcodeDeveloperPath):
280+
toolchainPaths.append((path, strict: true))
281+
if let xcodeDeveloperPath {
282+
toolchainPaths.append((xcodeDeveloperPath.join("Toolchains"), strict: xcodeDeveloperPath.str.hasSuffix(".app/Contents/Developer")))
283+
}
284+
case .fallback(let path):
285+
toolchainPaths.append((path.join("Toolchains"), strict: false))
286+
}
246287

247288
// FIXME: We should support building the toolchain locally (for `inferiorProductsPath`).
248289

@@ -372,12 +413,14 @@ public final class Core: Sendable {
372413
public func lookupCASPlugin() -> ToolchainCASPlugin? {
373414
return casPlugin.withLock { casPlugin in
374415
if casPlugin == nil {
375-
if hostOperatingSystem == .macOS {
376-
let pluginPath = developerPath.join("usr/lib/libToolchainCASPlugin.dylib")
416+
switch developerPath {
417+
case .xcode(let path):
418+
let pluginPath = path.join("usr/lib/libToolchainCASPlugin.dylib")
377419
let plugin = try? ToolchainCASPlugin(dylib: pluginPath)
378420
casPlugin = plugin
379-
} else {
421+
case .swiftToolchain, .fallback:
380422
// Unimplemented
423+
break
381424
}
382425
}
383426
return casPlugin
@@ -401,8 +444,19 @@ public final class Core: Sendable {
401444
if let onlySearchAdditionalPlatformPaths = getEnvironmentVariable("XCODE_ONLY_EXTRA_PLATFORM_FOLDERS"), onlySearchAdditionalPlatformPaths.boolValue {
402445
searchPaths = []
403446
} else {
404-
let platformsDir = self.developerPath.join("Platforms")
405-
searchPaths = [platformsDir]
447+
switch developerPath {
448+
case .xcode(let path):
449+
let platformsDir = path.join("Platforms")
450+
searchPaths = [platformsDir]
451+
case .swiftToolchain(_, let xcodeDeveloperDirectoryPath):
452+
if let xcodeDeveloperDirectoryPath {
453+
searchPaths = [xcodeDeveloperDirectoryPath.join("Platforms")]
454+
} else {
455+
searchPaths = []
456+
}
457+
case .fallback:
458+
searchPaths = []
459+
}
406460
}
407461
if let additionalPlatformSearchPaths = getEnvironmentVariable("XCODE_EXTRA_PLATFORM_FOLDERS") {
408462
for searchPath in additionalPlatformSearchPaths.split(separator: Path.pathEnvironmentSeparator) {
@@ -690,7 +744,7 @@ struct CoreRegistryDelegate : PlatformRegistryDelegate, SDKRegistryDelegate, Spe
690744
core.pluginManager
691745
}
692746

693-
var developerPath: Path {
747+
var developerPath: Core.DeveloperPath {
694748
core.developerPath
695749
}
696750
}

Sources/SWBCore/Extensions/PlatformInfoExtension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,6 @@ extension PlatformInfoExtension {
7474

7575
public protocol PlatformInfoExtensionAdditionalPlatformsContext: Sendable {
7676
var hostOperatingSystem: OperatingSystem { get }
77-
var developerPath: Path { get }
77+
var developerPath: Core.DeveloperPath { get }
7878
var fs: any FSProxy { get }
7979
}

0 commit comments

Comments
 (0)