Skip to content

Add support for looking up spec resources in a toolchain install #266

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
Mar 10, 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
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func swiftSettings(languageMode: SwiftLanguageMode) -> [SwiftSetting] {

.swiftLanguageMode(.v5),

.define("USE_STATIC_PLUGIN_INITIALIZATION")
.define("USE_STATIC_PLUGIN_INITIALIZATION"),
]
case .v6:
return [
Expand All @@ -55,7 +55,7 @@ func swiftSettings(languageMode: SwiftLanguageMode) -> [SwiftSetting] {

.swiftLanguageMode(.v6),

.define("USE_STATIC_PLUGIN_INITIALIZATION")
.define("USE_STATIC_PLUGIN_INITIALIZATION"),
]
default:
fatalError("unexpected language mode")
Expand Down
4 changes: 2 additions & 2 deletions Sources/SWBAndroidPlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import Foundation
}

struct AndroidPlatformSpecsExtension: SpecificationsExtension {
func specificationFiles() -> Bundle? {
.module
func specificationFiles(resourceSearchPaths: [Path]) -> Bundle? {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBAndroidPlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)
}

func specificationDomains() -> [String : [String]] {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SWBApplePlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ struct ApplePlatformSpecsExtension: SpecificationsExtension {
]
}

func specificationFiles() -> Bundle? {
.module
func specificationFiles(resourceSearchPaths: [Path]) -> Bundle? {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBApplePlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)
}

func specificationDomains() -> [String : [String]] {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SWBBuildService/BuildService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ package 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?, inferiorProducts: Path? = nil, environment: [String: String] = [:]) async -> (Core?, [Diagnostic]) {
func sharedCore(developerPath: Path?, 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,7 @@ package class BuildService: Service, @unchecked Sendable {
}
}
let delegate = Delegate()
let (core, diagnostics) = await (Core.getInitializedCore(delegate, pluginManager: pluginManager, developerPath: developerPath, inferiorProductsPath: inferiorProducts, environment: environment, buildServiceModTime: buildServiceModTime, connectionMode: connectionMode), delegate.diagnostics)
let (core, diagnostics) = await (Core.getInitializedCore(delegate, pluginManager: pluginManager, developerPath: developerPath, resourceSearchPaths: resourceSearchPaths, inferiorProductsPath: inferiorProducts, environment: environment, buildServiceModTime: buildServiceModTime, connectionMode: connectionMode), delegate.diagnostics)
delegate.freeze()
sharedCoreCache[key] = (core, diagnostics)
return (core, diagnostics)
Expand Down
1 change: 1 addition & 0 deletions Sources/SWBBuildService/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ private struct CreateSessionHandler: MessageHandler {
let service = request.buildService
let (core, diagnostics) = await service.sharedCore(
developerPath: message.effectiveDeveloperPath,
resourceSearchPaths: message.resourceSearchPaths ?? [],
inferiorProducts: message.inferiorProductsPath,
environment: message.environment ?? [:]
)
Expand Down
12 changes: 8 additions & 4 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, 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: Path? = 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 Down Expand Up @@ -73,7 +73,7 @@ public final class Core: Sendable {

let core: Core
do {
core = try await Core(delegate: delegate, hostOperatingSystem: hostOperatingSystem, pluginManager: pluginManager, developerPath: resolvedDeveloperPath, inferiorProductsPath: inferiorProductsPath, additionalContentPaths: additionalContentPaths, environment: environment, buildServiceModTime: buildServiceModTime, connectionMode: connectionMode)
core = try await Core(delegate: delegate, hostOperatingSystem: hostOperatingSystem, pluginManager: pluginManager, developerPath: resolvedDeveloperPath, resourceSearchPaths: resourceSearchPaths, inferiorProductsPath: inferiorProductsPath, additionalContentPaths: additionalContentPaths, environment: environment, buildServiceModTime: buildServiceModTime, connectionMode: connectionMode)
} catch {
delegate.error("\(error)")
return nil
Expand Down Expand Up @@ -147,6 +147,9 @@ public final class Core: Sendable {
/// The path to the "Developer" directory.
public let developerPath: Path

/// Additional search paths to be used when looking up resource bundles.
public let resourceSearchPaths: [Path]

/// The path to the inferior Xcode build directory, if used.
public let inferiorProductsPath: Path?

Expand Down Expand Up @@ -177,11 +180,12 @@ public final class Core: Sendable {

public let connectionMode: ServiceHostConnectionMode

@_spi(Testing) public init(delegate: any CoreDelegate, hostOperatingSystem: OperatingSystem, pluginManager: PluginManager, developerPath: String, 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: String, 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.resourceSearchPaths = resourceSearchPaths
self.inferiorProductsPath = inferiorProductsPath
self.additionalContentPaths = additionalContentPaths
self.buildServiceModTime = buildServiceModTime
Expand Down Expand Up @@ -402,7 +406,7 @@ public final class Core: Sendable {

// Find all plugin provided specs.
for ext in await self.pluginManager.extensions(of: SpecificationsExtensionPoint.self) {
if let bundle = ext.specificationFiles() {
if let bundle = ext.specificationFiles(resourceSearchPaths: resourceSearchPaths) {
for url in bundle.urls(forResourcesWithExtension: "xcspec", subdirectory: nil) ?? [] {
do {
try searchPaths.append(((url as URL).filePath, ""))
Expand Down
19 changes: 15 additions & 4 deletions Sources/SWBCore/Extensions/SpecificationsExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,34 @@ public struct SpecificationsExtensionPoint: ExtensionPoint {

public protocol SpecificationsExtension: Sendable {
/// Returns the bundle containing the `.xcspec` files.
func specificationFiles() -> Bundle?
func specificationFiles(resourceSearchPaths: [Path]) -> Bundle?
func specificationDomains() -> [String: [String]]
func specificationTypes() -> [any SpecType.Type]
func specificationClasses() -> [any SpecIdentifierType.Type]
func specificationClassesClassic() -> [any SpecClassType.Type]
func specificationImplementations() -> [any SpecImplementationType.Type]

/// Returns the search paths for two use cases: finding the sole remaining `.xcbuildrules` file, and finding executable scripts next to `.xcspec` files.
func specificationSearchPaths() -> [URL]
func specificationSearchPaths(resourceSearchPaths: [Path]) -> [URL]
}

extension SpecificationsExtension {
public func specificationFiles() -> Bundle? { nil }
public func specificationFiles(resourceSearchPaths: [Path]) -> Bundle? { nil }
public func specificationDomains() -> [String: [String]] { [:] }
public func specificationTypes() -> [any SpecType.Type] { [] }
public func specificationClasses() -> [any SpecIdentifierType.Type] { [] }
public func specificationClassesClassic() -> [any SpecClassType.Type] { [] }
public func specificationImplementations() -> [any SpecImplementationType.Type] { [] }
public func specificationSearchPaths() -> [URL] { [] }
public func specificationSearchPaths(resourceSearchPaths: [Path]) -> [URL] { [] }

public func findResourceBundle(nameWhenInstalledInToolchain: String, resourceSearchPaths: [Path], defaultBundle: @autoclosure () -> Bundle?) -> Bundle? {
for searchPath in resourceSearchPaths {
for bundleBasename in ["\(nameWhenInstalledInToolchain).bundle", "\(nameWhenInstalledInToolchain).resources"] {
if let bundle = Bundle(path: searchPath.join(bundleBasename).str) {
return bundle
}
}
}
return defaultBundle()
}
}
4 changes: 2 additions & 2 deletions Sources/SWBCore/Settings/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ fileprivate struct PreOverridesSettings {

@preconcurrency @PluginExtensionSystemActor func searchPaths() -> [Path] {
core.pluginManager.extensions(of: SpecificationsExtensionPoint.self).flatMap { ext in
ext.specificationSearchPaths().compactMap { try? $0.filePath }
ext.specificationSearchPaths(resourceSearchPaths: core.resourceSearchPaths).compactMap { try? $0.filePath }
}.sorted()
}

Expand Down Expand Up @@ -999,7 +999,7 @@ extension WorkspaceContext {

@preconcurrency @PluginExtensionSystemActor func searchPaths() -> [Path] {
core.pluginManager.extensions(of: SpecificationsExtensionPoint.self).flatMap { ext in
ext.specificationSearchPaths().compactMap { try? $0.filePath }
ext.specificationSearchPaths(resourceSearchPaths: core.resourceSearchPaths).compactMap { try? $0.filePath }
}.sorted()
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public struct BuiltinSpecsExtension: SpecificationsExtension {
]
}

public func specificationFiles() -> Bundle? {
.module
public func specificationFiles(resourceSearchPaths: [SWBUtil.Path]) -> Bundle? {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBCore", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)
}
}
4 changes: 2 additions & 2 deletions Sources/SWBGenericUnixPlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import Foundation
}

struct GenericUnixPlatformSpecsExtension: SpecificationsExtension {
func specificationFiles() -> Bundle? {
.module
func specificationFiles(resourceSearchPaths: [Path]) -> Bundle? {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBGenericUnixPlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)
}

func specificationDomains() -> [String: [String]] {
Expand Down
9 changes: 8 additions & 1 deletion Sources/SWBProtocol/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ public struct CreateSessionRequest: RequestMessage, Equatable, SerializableCodab

public let name: String
public let developerPath: Path?
public let resourceSearchPaths: [Path]?
public let appPath: Path?
public let cachePath: Path?
public let inferiorProductsPath: Path?
Expand All @@ -432,9 +433,14 @@ public struct CreateSessionRequest: RequestMessage, Equatable, SerializableCodab
self.init(name: name, developerPath: developerPath, cachePath: cachePath, inferiorProductsPath: inferiorProductsPath, environment: nil)
}

public init(name: String, developerPath: Path?, cachePath: Path?, inferiorProductsPath: Path?, environment: [String:String]?) {
public init(name: String, developerPath: Path?, cachePath: Path?, inferiorProductsPath: Path?, environment: [String:String]?) { // ABI Compatibility
self.init(name: name, developerPath: developerPath, resourceSearchPaths: [], cachePath: cachePath, inferiorProductsPath: inferiorProductsPath, environment: environment)
}

public init(name: String, developerPath: Path?, resourceSearchPaths: [Path], cachePath: Path?, inferiorProductsPath: Path?, environment: [String:String]?) {
self.name = name
self.developerPath = developerPath
self.resourceSearchPaths = resourceSearchPaths
self.appPath = developerPath?.dirname.dirname
self.cachePath = cachePath
self.inferiorProductsPath = inferiorProductsPath
Expand All @@ -446,6 +452,7 @@ public struct CreateSessionRequest: RequestMessage, Equatable, SerializableCodab
self.name = try deserializer.deserialize()
self.appPath = try deserializer.deserialize()
self.developerPath = count >= 5 ? try deserializer.deserialize() : appPath?.join("Contents").join("Developer")
self.resourceSearchPaths = []
self.cachePath = try deserializer.deserialize()
self.inferiorProductsPath = try deserializer.deserialize()
self.environment = nil
Expand Down
4 changes: 2 additions & 2 deletions Sources/SWBQNXPlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import Foundation
}

struct QNXPlatformSpecsExtension: SpecificationsExtension {
func specificationFiles() -> Bundle? {
.module
func specificationFiles(resourceSearchPaths: [Path]) -> Bundle? {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBQNXPlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/SWBTestSupport/CoreTestSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ extension Core {
developerPath = "/"
}
let delegate = TestingCoreDelegate()
return await (try Core(delegate: delegate, hostOperatingSystem: hostOperatingSystem, pluginManager: PluginManager(skipLoadingPluginIdentifiers: []), developerPath: developerPath, inferiorProductsPath: nil, additionalContentPaths: [], environment: [:], buildServiceModTime: Date(), connectionMode: .inProcess), delegate.diagnostics)
return await (try Core(delegate: delegate, hostOperatingSystem: hostOperatingSystem, pluginManager: PluginManager(skipLoadingPluginIdentifiers: []), developerPath: developerPath, resourceSearchPaths: [], inferiorProductsPath: nil, additionalContentPaths: [], environment: [:], buildServiceModTime: Date(), connectionMode: .inProcess), delegate.diagnostics)
}

/// Get an initialized Core suitable for testing.
Expand Down
8 changes: 4 additions & 4 deletions Sources/SWBUniversalPlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ struct UniversalPlatformSpecsExtension: SpecificationsExtension {
]
}

func specificationFiles() -> Bundle? {
.module
func specificationFiles(resourceSearchPaths: [Path]) -> Bundle? {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBUniversalPlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)
}

// Allow locating the sole remaining `.xcbuildrules` file.
func specificationSearchPaths() -> [URL] {
Bundle.module.resourceURL.map { [$0] } ?? []
func specificationSearchPaths(resourceSearchPaths: [Path]) -> [URL] {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBUniversalPlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)?.resourceURL.map { [$0] } ?? []
}
}
4 changes: 2 additions & 2 deletions Sources/SWBWebAssemblyPlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import Foundation
}

struct WebAssemblyPlatformSpecsExtension: SpecificationsExtension {
func specificationFiles() -> Bundle? {
.module
func specificationFiles(resourceSearchPaths: [Path]) -> Bundle? {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBWebAssemblyPlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/SWBWindowsPlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import Foundation
}

struct WindowsPlatformSpecsExtension: SpecificationsExtension {
func specificationFiles() -> Bundle? {
.module
func specificationFiles(resourceSearchPaths: [Path]) -> Bundle? {
findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBWindowsPlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)
}
}

Expand Down
9 changes: 7 additions & 2 deletions Sources/SwiftBuild/SWBBuildService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ public final class SWBBuildService: Sendable {
await createSession(name: name, developerPath: developerPath, cachePath: cachePath, inferiorProductsPath: inferiorProductsPath, environment: nil)
}

// ABI compatibility
public func createSession(name: String, developerPath: String? = nil, cachePath: String?, inferiorProductsPath: String?, environment: [String:String]?) async -> (Result<SWBBuildServiceSession, any Error>, [SwiftBuildMessage.DiagnosticInfo]) {
await createSession(name: name, resourceSearchPaths: [], cachePath: cachePath, inferiorProductsPath: inferiorProductsPath, environment: environment)
}

/// Create a new service session.
///
/// - Parameters:
Expand All @@ -202,9 +207,9 @@ public final class SWBBuildService: Sendable {
/// - inferiorProductsPath: If provided, the path to where inferior Xcode build data is located.
/// - environment: If provided, a set of environment variables that are relevant to the build session's context
/// - returns: The new session.
public func createSession(name: String, developerPath: String? = nil, cachePath: String?, inferiorProductsPath: String?, environment: [String:String]?) async -> (Result<SWBBuildServiceSession, any Error>, [SwiftBuildMessage.DiagnosticInfo]) {
public func createSession(name: String, developerPath: String? = nil, resourceSearchPaths: [String], cachePath: String?, inferiorProductsPath: String?, environment: [String:String]?) async -> (Result<SWBBuildServiceSession, any Error>, [SwiftBuildMessage.DiagnosticInfo]) {
do {
let response = try await send(request: CreateSessionRequest(name: name, developerPath: developerPath.map(Path.init), cachePath: cachePath.map(Path.init), inferiorProductsPath: inferiorProductsPath.map(Path.init), environment: environment))
let response = try await send(request: CreateSessionRequest(name: name, developerPath: developerPath.map(Path.init), resourceSearchPaths: resourceSearchPaths.map(Path.init), cachePath: cachePath.map(Path.init), inferiorProductsPath: inferiorProductsPath.map(Path.init), environment: environment))
let diagnostics = response.diagnostics.map { SwiftBuildMessage.DiagnosticInfo(.init($0, .global)) }
if let sessionID = response.sessionID {
return (.success(SWBBuildServiceSession(name: name, uid: sessionID, service: self)), diagnostics)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2302,7 +2302,7 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {

@PluginExtensionSystemActor func pluginSearchPaths() -> [Path] {
core.pluginManager.extensions(of: SpecificationsExtensionPoint.self).flatMap { ext in
ext.specificationSearchPaths().compactMap { try? $0.filePath }
ext.specificationSearchPaths(resourceSearchPaths: core.resourceSearchPaths).compactMap { try? $0.filePath }
}.sorted()
}

Expand Down