Skip to content

Allow initializing the build system with the containing toolchain as the developer path #381

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

Merged
merged 1 commit into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Sources/SWBBuildService/BuildService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public struct MsgHandlingError: Swift.Error {

private struct CoreCacheKey: Equatable, Hashable {
/// The path of the developer directory.
let developerPath: Path?
let developerPath: SWBProtocol.DeveloperPath?

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

/// Convenience overload which throws if the Core had initialization errors.
func sharedCore(developerPath: Path?, inferiorProducts: Path? = nil, environment: [String: String] = [:]) async throws -> Core {
func sharedCore(developerPath: SWBProtocol.DeveloperPath?, inferiorProducts: Path? = nil, environment: [String: String] = [:]) async throws -> Core {
let (c, diagnostics) = await sharedCore(developerPath: developerPath, inferiorProducts: inferiorProducts, environment: environment)
guard let core = c else {
throw ServiceError.unableToInitializeCore(errors: diagnostics.map { $0.formatLocalizedDescription(.debug) })
Expand All @@ -157,7 +157,7 @@ open class BuildService: Service, @unchecked Sendable {
/// Get a shared core instance.
///
/// 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).
func sharedCore(developerPath: Path?, resourceSearchPaths: [Path] = [], inferiorProducts: Path? = nil, environment: [String: String] = [:]) async -> (Core?, [Diagnostic]) {
func sharedCore(developerPath: SWBProtocol.DeveloperPath?, resourceSearchPaths: [Path] = [], inferiorProducts: Path? = nil, environment: [String: String] = [:]) async -> (Core?, [Diagnostic]) {
let key = CoreCacheKey(developerPath: developerPath, inferiorProducts: inferiorProducts, environment: environment)
return await sharedCoreCacheLock.withLock {
if let existing = sharedCoreCache[key] {
Expand Down Expand Up @@ -191,7 +191,17 @@ open class BuildService: Service, @unchecked Sendable {
}
}
let delegate = Delegate()
let (core, diagnostics) = await (Core.getInitializedCore(delegate, pluginManager: pluginManager, developerPath: developerPath, resourceSearchPaths: resourceSearchPaths, inferiorProductsPath: inferiorProducts, environment: environment, buildServiceModTime: buildServiceModTime, connectionMode: connectionMode), delegate.diagnostics)
let coreDeveloperPath: Core.DeveloperPath?
switch developerPath {
case .xcode(let path):
coreDeveloperPath = .xcode(path)
case .swiftToolchain(let path):
let xcodeDeveloperPath = try? await Xcode.getActiveDeveloperDirectoryPath()
coreDeveloperPath = .swiftToolchain(path, xcodeDeveloperPath: xcodeDeveloperPath)
case nil:
coreDeveloperPath = nil
}
let (core, diagnostics) = await (Core.getInitializedCore(delegate, pluginManager: pluginManager, developerPath: coreDeveloperPath, resourceSearchPaths: resourceSearchPaths, inferiorProductsPath: inferiorProducts, environment: environment, buildServiceModTime: buildServiceModTime, connectionMode: connectionMode), delegate.diagnostics)
delegate.freeze()
sharedCoreCache[key] = (core, diagnostics)
return (core, diagnostics)
Expand Down
20 changes: 14 additions & 6 deletions Sources/SWBBuildService/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private struct CreateXCFrameworkHandler: MessageHandler {
guard let buildService = request.service as? BuildService else {
throw StubError.error("service object is not of type BuildService")
}
let (result, output) = try await XCFramework.createXCFramework(commandLine: message.commandLine, currentWorkingDirectory: message.currentWorkingDirectory, infoLookup: buildService.sharedCore(developerPath: message.effectiveDeveloperPath))
let (result, output) = try await XCFramework.createXCFramework(commandLine: message.commandLine, currentWorkingDirectory: message.currentWorkingDirectory, infoLookup: buildService.sharedCore(developerPath: message.effectiveDeveloperPath.map { .xcode($0) }))
if !result {
throw StubError.error(output)
}
Expand Down Expand Up @@ -113,7 +113,7 @@ private struct ProductTypeSupportsMacCatalystHandler: MessageHandler {
guard let buildService = request.service as? BuildService else {
throw StubError.error("service object is not of type BuildService")
}
return try await BoolResponse(buildService.sharedCore(developerPath: message.effectiveDeveloperPath).productTypeSupportsMacCatalyst(productTypeIdentifier: message.productTypeIdentifier))
return try await BoolResponse(buildService.sharedCore(developerPath: message.effectiveDeveloperPath.map { .xcode($0) }).productTypeSupportsMacCatalyst(productTypeIdentifier: message.productTypeIdentifier))
}
}

Expand All @@ -122,8 +122,16 @@ private struct ProductTypeSupportsMacCatalystHandler: MessageHandler {
private struct CreateSessionHandler: MessageHandler {
func handle(request: Request, message: CreateSessionRequest) async throws -> CreateSessionResponse {
let service = request.buildService
let developerPath: DeveloperPath?
if let devPath = message.developerPath2 {
developerPath = devPath
} else if let devPath = message.effectiveDeveloperPath {
developerPath = .xcode(devPath)
} else {
developerPath = nil
}
let (core, diagnostics) = await service.sharedCore(
developerPath: message.effectiveDeveloperPath,
developerPath: developerPath,
resourceSearchPaths: message.resourceSearchPaths ?? [],
inferiorProducts: message.inferiorProductsPath,
environment: message.environment ?? [:]
Expand Down Expand Up @@ -184,7 +192,7 @@ extension SetSessionWorkspaceContainerPathRequest: PIFProvidingRequest {
try fs.createDirectory(dir, recursive: true)
let pifPath = dir.join(Foundation.UUID().description + ".json")
let argument = isProject ? "-project" : "-workspace"
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]))
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]))
if !result.exitStatus.isSuccess {
throw StubError.error("Could not dump PIF for '\(path.str)': \(String(decoding: result.stderr, as: UTF8.self))")
}
Expand Down Expand Up @@ -1200,7 +1208,7 @@ private struct ClientExchangeResponseMsg<MessageType: ClientExchangeMessage & Re
private struct DeveloperPathHandler: MessageHandler {
func handle(request: Request, message: DeveloperPathRequest) throws -> StringResponse {
let session = try request.session(for: message)
return StringResponse(session.core.developerPath.str)
return StringResponse(session.core.developerPath.path.str)
}
}

Expand Down Expand Up @@ -1469,7 +1477,7 @@ private struct ExecuteCommandLineToolMsg: MessageHandler {
request.service.send(message.replyChannel, BoolResponse(false))
return
}
let (core, diagnostics) = await buildService.sharedCore(developerPath: message.developerPath)
let (core, diagnostics) = await buildService.sharedCore(developerPath: message.developerPath.map { .xcode($0) })
guard let core else {
for diagnostic in diagnostics where diagnostic.behavior == .error {
request.service.send(message.replyChannel, ErrorResponse(diagnostic.formatLocalizedDescription(.messageOnly)))
Expand Down
104 changes: 79 additions & 25 deletions Sources/SWBCore/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public final class Core: Sendable {
/// Get a configured instance of the core.
///
/// - 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.
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? {
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? {
// Enable macro expression interning during loading.
return await MacroNamespace.withExpressionInterningEnabled {
let hostOperatingSystem: OperatingSystem
Expand All @@ -59,9 +59,9 @@ public final class Core: Sendable {
await extraPluginRegistration([])
#endif

let resolvedDeveloperPath: String
let resolvedDeveloperPath: DeveloperPath
do {
if let resolved = developerPath?.nilIfEmpty?.str {
if let resolved = developerPath {
resolvedDeveloperPath = resolved
} else {
let values = try await Set(pluginManager.extensions(of: DeveloperDirectoryExtensionPoint.self).asyncMap { try await $0.fallbackDeveloperDirectory(hostOperatingSystem: hostOperatingSystem) }).compactMap { $0 }
Expand All @@ -70,7 +70,12 @@ public final class Core: Sendable {
delegate.error("Could not determine path to developer directory because no extensions provided a fallback value")
return nil
case 1:
resolvedDeveloperPath = values[0].str
let path = values[0]
if path.str.hasSuffix(".app/Contents/Developer") {
resolvedDeveloperPath = .xcode(path)
} else {
resolvedDeveloperPath = .fallback(values[0])
}
default:
delegate.error("Could not determine path to developer directory because multiple extensions provided conflicting fallback values: \(values.sorted().map { $0.str }.joined(separator: ", "))")
return nil
Expand Down Expand Up @@ -169,8 +174,26 @@ public final class Core: Sendable {

public let pluginManager: PluginManager

public enum DeveloperPath: Sendable, Hashable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is great, our previous model definitely didn't well capture the idea that we need to support:

  • macOS with Xcode only
  • macOS with swift.org toolchain + DCLT
  • macOS with swift.org toolchain + Xcode

// A path to an Xcode install's "/Contents/Developer" directory
case xcode(Path)

// A path to the root of a Swift toolchain, optionally paired with the developer path of an installed Xcode
case swiftToolchain(Path, xcodeDeveloperPath: Path?)

// A fallback resolved path.
case fallback(Path)

public var path: Path {
switch self {
case .xcode(let path), .swiftToolchain(let path, xcodeDeveloperPath: _), .fallback(let path):
return path
}
}
}

/// The path to the "Developer" directory.
public let developerPath: Path
public let developerPath: DeveloperPath

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

public let connectionMode: ServiceHostConnectionMode

@_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 {
@_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 {
self.delegate = delegate
self.hostOperatingSystem = hostOperatingSystem
self.pluginManager = pluginManager
self.developerPath = Path(developerPath)
self.developerPath = developerPath
self.resourceSearchPaths = resourceSearchPaths
self.inferiorProductsPath = inferiorProductsPath
self.additionalContentPaths = additionalContentPaths
self.buildServiceModTime = buildServiceModTime
self.connectionMode = connectionMode
self.environment = environment

let versionPath = self.developerPath.dirname.join("version.plist")
switch developerPath {
case .xcode(let path):
let versionPath = path.dirname.join("version.plist")

// Load the containing app (Xcode or Playgrounds) version information, if available.
//
// We make this optional so tests do not need to provide it.
if let info = try XcodeVersionInfo.versionInfo(versionPath: versionPath) {
self.xcodeVersion = info.shortVersion
// Load the containing app (Xcode or Playgrounds) version information, if available.
//
// We make this optional so tests do not need to provide it.
if let info = try XcodeVersionInfo.versionInfo(versionPath: versionPath) {
self.xcodeVersion = info.shortVersion

// If the ProductBuildVersion key is missing, we use "UNKNOWN" as the value.
self.xcodeProductBuildVersion = info.productBuildVersion ?? ProductBuildVersion(major: 0, train: "A", build: 0, buildSuffix: "")
self.xcodeProductBuildVersionString = info.productBuildVersion?.description ?? "UNKNOWN"
} else {
// Set an arbitrary version for testing purposes.
// If the ProductBuildVersion key is missing, we use "UNKNOWN" as the value.
self.xcodeProductBuildVersion = info.productBuildVersion ?? ProductBuildVersion(major: 0, train: "A", build: 0, buildSuffix: "")
self.xcodeProductBuildVersionString = info.productBuildVersion?.description ?? "UNKNOWN"
} else {
// Set an arbitrary version for testing purposes.
self.xcodeVersion = Version(99, 99, 99)
self.xcodeProductBuildVersion = ProductBuildVersion(major: 99, train: "T", build: 999)
self.xcodeProductBuildVersionString = xcodeProductBuildVersion.description
}
case .swiftToolchain, .fallback:
// FIXME: Eliminate this requirment for Swift toolchains
self.xcodeVersion = Version(99, 99, 99)
self.xcodeProductBuildVersion = ProductBuildVersion(major: 99, train: "T", build: 999)
self.xcodeProductBuildVersionString = xcodeProductBuildVersion.description
Expand All @@ -242,7 +273,17 @@ public final class Core: Sendable {
self.toolchainPaths = {
var toolchainPaths = [(Path, strict: Bool)]()

toolchainPaths.append((Path(developerPath).join("Toolchains"), strict: developerPath.hasSuffix(".app/Contents/Developer")))
switch developerPath {
case .xcode(let path):
toolchainPaths.append((path.join("Toolchains"), strict: path.str.hasSuffix(".app/Contents/Developer")))
case .swiftToolchain(let path, xcodeDeveloperPath: let xcodeDeveloperPath):
toolchainPaths.append((path, strict: true))
if let xcodeDeveloperPath {
toolchainPaths.append((xcodeDeveloperPath.join("Toolchains"), strict: xcodeDeveloperPath.str.hasSuffix(".app/Contents/Developer")))
}
case .fallback(let path):
toolchainPaths.append((path.join("Toolchains"), strict: false))
}

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

Expand Down Expand Up @@ -372,12 +413,14 @@ public final class Core: Sendable {
public func lookupCASPlugin() -> ToolchainCASPlugin? {
return casPlugin.withLock { casPlugin in
if casPlugin == nil {
if hostOperatingSystem == .macOS {
let pluginPath = developerPath.join("usr/lib/libToolchainCASPlugin.dylib")
switch developerPath {
case .xcode(let path):
let pluginPath = path.join("usr/lib/libToolchainCASPlugin.dylib")
let plugin = try? ToolchainCASPlugin(dylib: pluginPath)
casPlugin = plugin
} else {
case .swiftToolchain, .fallback:
// Unimplemented
break
}
}
return casPlugin
Expand All @@ -401,8 +444,19 @@ public final class Core: Sendable {
if let onlySearchAdditionalPlatformPaths = getEnvironmentVariable("XCODE_ONLY_EXTRA_PLATFORM_FOLDERS"), onlySearchAdditionalPlatformPaths.boolValue {
searchPaths = []
} else {
let platformsDir = self.developerPath.join("Platforms")
searchPaths = [platformsDir]
switch developerPath {
case .xcode(let path):
let platformsDir = path.join("Platforms")
searchPaths = [platformsDir]
case .swiftToolchain(_, let xcodeDeveloperDirectoryPath):
if let xcodeDeveloperDirectoryPath {
searchPaths = [xcodeDeveloperDirectoryPath.join("Platforms")]
} else {
searchPaths = []
}
case .fallback:
searchPaths = []
}
}
if let additionalPlatformSearchPaths = getEnvironmentVariable("XCODE_EXTRA_PLATFORM_FOLDERS") {
for searchPath in additionalPlatformSearchPaths.split(separator: Path.pathEnvironmentSeparator) {
Expand Down Expand Up @@ -690,7 +744,7 @@ struct CoreRegistryDelegate : PlatformRegistryDelegate, SDKRegistryDelegate, Spe
core.pluginManager
}

var developerPath: Path {
var developerPath: Core.DeveloperPath {
core.developerPath
}
}
2 changes: 1 addition & 1 deletion Sources/SWBCore/Extensions/PlatformInfoExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@ extension PlatformInfoExtension {

public protocol PlatformInfoExtensionAdditionalPlatformsContext: Sendable {
var hostOperatingSystem: OperatingSystem { get }
var developerPath: Path { get }
var developerPath: Core.DeveloperPath { get }
var fs: any FSProxy { get }
}
Loading