diff --git a/Sources/SWBTestSupport/CoreTestSupport.swift b/Sources/SWBTestSupport/CoreTestSupport.swift index e95da70f..1bf280ea 100644 --- a/Sources/SWBTestSupport/CoreTestSupport.swift +++ b/Sources/SWBTestSupport/CoreTestSupport.swift @@ -70,6 +70,40 @@ extension Core { try POSIX.unsetenv(variable) } + // Handle Xcodes with an unbundled Metal toolchain by querying `xcodebuild` if needed. + // + // If the given environment already contains `EXTERNAL_TOOLCHAINS_DIR` and `TOOLCHAINS`, we're assuming that we do not have to obtain any toolchain information. + var environment = environment + if (try? ProcessInfo.processInfo.hostOperatingSystem()) == .macOS, !(environment.contains("EXTERNAL_TOOLCHAINS_DIR") && environment.contains("TOOLCHAINS")) { + let activeDeveloperPath: Path + if let developerPath { + activeDeveloperPath = developerPath.path + } else { + activeDeveloperPath = try await Xcode.getActiveDeveloperDirectoryPath() + } + let defaultToolchainPath = activeDeveloperPath.join("Toolchains/XcodeDefault.xctoolchain") + + if !localFS.exists(defaultToolchainPath.join("usr/metal/current")) { + struct MetalToolchainInfo: Decodable { + let buildVersion: String + let status: String + let toolchainIdentifier: String + let toolchainSearchPath: String + } + + let result = try await Process.getOutput(url: URL(fileURLWithPath: activeDeveloperPath.join("usr/bin/xcodebuild").str), arguments: ["-showComponent", "metalToolchain", "-json"], environment: ["DEVELOPER_DIR": activeDeveloperPath.str]) + if result.exitStatus != .exit(0) { + throw StubError.error("xcodebuild failed: \(String(data: result.stdout, encoding: .utf8) ?? "")\n\(String(data: result.stderr, encoding: .utf8) ?? "")") + } + + let metalToolchainInfo = try JSONDecoder().decode(MetalToolchainInfo.self, from: result.stdout) + environment.addContents(of: [ + "TOOLCHAINS": "\(metalToolchainInfo.toolchainIdentifier) $(inherited)", + "EXTERNAL_TOOLCHAINS_DIR": metalToolchainInfo.toolchainSearchPath, + ]) + } + } + // When this code is being loaded directly via unit tests *and* we detect the products directory we are running in is for Xcode, then we should run using inferior search paths. let inferiorProductsPath: Path? = self.inferiorProductsPath() diff --git a/Tests/SWBBuildSystemTests/BuildCommandTests.swift b/Tests/SWBBuildSystemTests/BuildCommandTests.swift index 2cd6a109..c2aa61af 100644 --- a/Tests/SWBBuildSystemTests/BuildCommandTests.swift +++ b/Tests/SWBBuildSystemTests/BuildCommandTests.swift @@ -325,6 +325,7 @@ fileprivate struct BuildCommandTests: CoreBasedTests { @Test(.requireSDKs(.macOS), .requireXcode16()) func singleFileCompileMetal() async throws { + let core = try await getCore() try await withTemporaryDirectory { tmpDirPath async throws -> Void in let testWorkspace = try await TestWorkspace( "Test", @@ -337,6 +338,7 @@ fileprivate struct BuildCommandTests: CoreBasedTests { "Debug", buildSettings: ["PRODUCT_NAME": "$(TARGET_NAME)", "SWIFT_ENABLE_EXPLICIT_MODULES": "NO", + "TOOLCHAINS": core.environment["TOOLCHAINS"] ?? "$(inherited)", "SWIFT_VERSION": swiftVersion])], targets: [ TestStandardTarget( @@ -348,7 +350,7 @@ fileprivate struct BuildCommandTests: CoreBasedTests { ) ] ) - let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false) + let tester = try await BuildOperationTester(core, testWorkspace, simulated: false) let metalFile = testWorkspace.sourceRoot.join("aProject/Metal.metal") try await tester.fs.writeFileContents(metalFile) { stream in } diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift index 61624fd0..3d5f3dde 100644 --- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift @@ -5756,6 +5756,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script @Test(.requireSDKs(.macOS)) func incrementalMetalLinkWithCodeSign() async throws { + let core = try await getCore() try await withTemporaryDirectory { tmpDirPath async throws -> Void in let testWorkspace = try await TestWorkspace( "Test", @@ -5773,6 +5774,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script "CODE_SIGN_IDENTITY": "-", "INFOPLIST_FILE": "Info.plist", "CODESIGN": "/usr/bin/true", + "TOOLCHAINS": core.environment["TOOLCHAINS"] ?? "$(inherited)", "SWIFT_VERSION": swiftVersion])], targets: [ TestStandardTarget( @@ -5781,7 +5783,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script buildPhases: [ TestSourcesBuildPhase(["SwiftFile.swift", "Metal.metal"]), ])])]) - let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false, fileSystem: localFS) + let tester = try await BuildOperationTester(core, testWorkspace, simulated: false, fileSystem: localFS) let signableTargets: Set = ["aFramework"] // Create the input files.