Skip to content
Open
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
11 changes: 11 additions & 0 deletions Sources/SWBCore/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,17 @@ public final class Core: Sendable {
FilesSignature(sdkRegistry.inputSignaturePaths)
}

/// The resolved toolchain search paths, in order.
///
/// These include `EXTERNAL_TOOLCHAINS_DIR` entries such as the Metal Toolchain component's cryptex mount point
/// (`…/com.apple.MobileAsset.MetalToolchain-v<version>.<random>`), whose name changes each time it is mounted —
/// for example after a reboot. Tool paths resolved from these directories are baked into build descriptions,
/// so a moved directory must invalidate a cached build description rather than reuse one that points at a
/// toolchain that no longer exists.
public var toolchainSearchPathsSignature: [Path] {
toolchainPaths.map(\.path)
}

/// The toolchain registry.
let _toolchainRegistry = UnsafeDelayedInitializationSendableWrapper<ToolchainRegistry>()
public var toolchainRegistry: ToolchainRegistry {
Expand Down
12 changes: 12 additions & 0 deletions Sources/SWBTaskExecution/BuildDescriptionSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ package struct BuildDescriptionSignatureComponents: Codable, Hashable, Sendable
/// the version — so an updated SDK forces a fresh build description rather than reusing the previous one from disk.
let sdkInputsSignature: FilesSignature

/// The resolved toolchain search paths.
///
/// External toolchains can move between builds without any other input changing: the Metal Toolchain component
/// is mounted under a new random directory name each time (e.g. after a reboot). Tool paths inside the build
/// description point into that directory, so a moved toolchain forces a fresh build description rather than
/// reusing one that references a path that no longer exists.
let toolchainSearchPaths: [Path]

fileprivate init(_ request: BuildPlanRequest) {
workspaceSignature = request.workspaceContext.workspace.signature
buildRequestParameters = request.buildRequest.parameters
Expand Down Expand Up @@ -103,6 +111,10 @@ package struct BuildDescriptionSignatureComponents: Codable, Hashable, Sendable
// Hash the SDK metadata files directly, so in-place SDK edits — including version bumps and SDKs updated
// independently of Xcode — force a fresh build description instead of reusing a stale one from disk.
sdkInputsSignature = request.workspaceContext.core.sdkInputsSignature

// Include where toolchains were found, so a toolchain that moved (e.g. the Metal Toolchain's cryptex mount,
// renamed on every mount) forces a fresh build description instead of reusing one with stale tool paths.
toolchainSearchPaths = request.workspaceContext.core.toolchainSearchPathsSignature
}
}

Expand Down
46 changes: 46 additions & 0 deletions Tests/SWBCoreTests/CoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,52 @@ import SWBServiceCore
}
}

/// The Metal Toolchain component is mounted under a new random directory name each time (e.g. after a reboot).
/// The same toolchain at a new mount point must change the signature, so a cached build description whose tool
/// paths point into the old mount point is not reused. https://github.com/swiftlang/swift-build/issues/1798
@Test(.skipIfEnvironmentVariableSet(key: .externalToolchainsDir))
func toolchainSearchPathsSignatureTracksExternalToolchainMount() async throws {
try await withTemporaryDirectory { tmpDir in
try localFS.createDirectory(tmpDir.join("Toolchains"))
let mountA = tmpDir.join("mnt/com.apple.MobileAsset.MetalToolchain-v1.0.aaaaaa").str
let mountB = tmpDir.join("mnt/com.apple.MobileAsset.MetalToolchain-v1.0.bbbbbb").str

let first = try #require(await makeCore(toolchainPath: tmpDir, environmentOverrides: ["EXTERNAL_TOOLCHAINS_DIR": mountA]))
let same = try #require(await makeCore(toolchainPath: tmpDir, environmentOverrides: ["EXTERNAL_TOOLCHAINS_DIR": mountA]))
let remounted = try #require(await makeCore(toolchainPath: tmpDir, environmentOverrides: ["EXTERNAL_TOOLCHAINS_DIR": mountB]))

#expect(first.toolchainSearchPathsSignature == same.toolchainSearchPathsSignature)
#expect(first.toolchainSearchPathsSignature != remounted.toolchainSearchPathsSignature)
#expect(remounted.toolchainSearchPathsSignature.contains(Path(mountB)))
}
}

private func makeCore(toolchainPath: Path, environmentOverrides: [String: String]) async -> Core? {
let delegate = Delegate()
let pluginManager = await MutablePluginManager(pluginLoadingFilter: { _ in true })
await pluginManager.registerExtensionPoint(DeveloperDirectoryExtensionPoint())
await pluginManager.registerExtensionPoint(SpecificationsExtensionPoint())
await pluginManager.registerExtensionPoint(ToolchainRegistryExtensionPoint())
await pluginManager.register(BuiltinSpecsExtension(), type: SpecificationsExtensionPoint.self)
struct MockDeveloperDirectoryExtensionPoint: DeveloperDirectoryExtension {
let toolchainPath: Path
func fallbackDeveloperDirectory(hostOperatingSystem: OperatingSystem) async throws -> Core.DeveloperPath? {
.swiftToolchain(toolchainPath, xcodeDeveloperPath: nil)
}
}
struct MockToolchainExtension: ToolchainRegistryExtension {
func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
guard context.toolchainRegistry.lookup(ToolchainRegistry.defaultToolchainIdentifier) == nil else {
return []
}
return [Toolchain(identifier: ToolchainRegistry.defaultToolchainIdentifier, displayName: "Mock", version: Version(), aliases: ["default"], path: .root, frameworkPaths: [], libraryPaths: [], defaultSettings: [:], overrideSettings: [:], defaultSettingsWhenPrimary: [:], executableSearchPaths: [], testingLibraryPlatformNames: [], fs: context.fs)]
}
}
await pluginManager.register(MockDeveloperDirectoryExtensionPoint(toolchainPath: toolchainPath), type: DeveloperDirectoryExtensionPoint.self)
await pluginManager.register(MockToolchainExtension(), type: ToolchainRegistryExtensionPoint.self)
return await Core.getInitializedCore(delegate, pluginManager: pluginManager, inferiorProductsPath: Path.root.join("invalid"), environment: environmentOverrides, buildServiceModTime: Date(), connectionMode: .inProcess)
}

func toolchainPathsCount() async throws -> Int {
try await withTemporaryDirectory { tmpDir in
try localFS.createDirectory(tmpDir.join("Toolchains"))
Expand Down