diff --git a/Fixtures/Metal/SimpleLibrary/Package.swift b/Fixtures/Metal/SimpleLibrary/Package.swift new file mode 100644 index 00000000000..36228ca965d --- /dev/null +++ b/Fixtures/Metal/SimpleLibrary/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. +import PackageDescription + +let package = Package( + name: "MyRenderer", + products: [ + .library( + name: "MyRenderer", + targets: ["MyRenderer"]), + ], + targets: [ + .target( + name: "MyRenderer", + dependencies: ["MySharedTypes"]), + + .target(name: "MySharedTypes") + ] +) diff --git a/Fixtures/Metal/SimpleLibrary/Sources/MyRenderer/Renderer.swift b/Fixtures/Metal/SimpleLibrary/Sources/MyRenderer/Renderer.swift new file mode 100644 index 00000000000..816eb50119b --- /dev/null +++ b/Fixtures/Metal/SimpleLibrary/Sources/MyRenderer/Renderer.swift @@ -0,0 +1,4 @@ +import MySharedTypes + + +let vertex = AAPLVertex(position: .init(250, -250), color: .init(1, 0, 0, 1)) diff --git a/Fixtures/Metal/SimpleLibrary/Sources/MyRenderer/Shaders.metal b/Fixtures/Metal/SimpleLibrary/Sources/MyRenderer/Shaders.metal new file mode 100644 index 00000000000..491edf63048 --- /dev/null +++ b/Fixtures/Metal/SimpleLibrary/Sources/MyRenderer/Shaders.metal @@ -0,0 +1,12 @@ +// A relative path to SharedTypes.h. +#import "../MySharedTypes/include/SharedTypes.h" + +#include +using namespace metal; + +vertex float4 simpleVertexShader(const device AAPLVertex *vertices [[buffer(0)]], + uint vertexID [[vertex_id]]) { + AAPLVertex in = vertices[vertexID]; + return float4(in.position.x, in.position.y, 0.0, 1.0); +} + diff --git a/Fixtures/Metal/SimpleLibrary/Sources/MySharedTypes/include/SharedTypes.h b/Fixtures/Metal/SimpleLibrary/Sources/MySharedTypes/include/SharedTypes.h new file mode 100644 index 00000000000..ea51fd839d4 --- /dev/null +++ b/Fixtures/Metal/SimpleLibrary/Sources/MySharedTypes/include/SharedTypes.h @@ -0,0 +1,14 @@ +#ifndef SharedTypes_h +#define SharedTypes_h + + +#import + + +typedef struct { + vector_float2 position; + vector_float4 color; +} AAPLVertex; + + +#endif /* SharedTypes_h */ diff --git a/Fixtures/Metal/SimpleLibrary/Tests/MyRendererTests/MyRendererTests.swift b/Fixtures/Metal/SimpleLibrary/Tests/MyRendererTests/MyRendererTests.swift new file mode 100644 index 00000000000..fad1d528b79 --- /dev/null +++ b/Fixtures/Metal/SimpleLibrary/Tests/MyRendererTests/MyRendererTests.swift @@ -0,0 +1,6 @@ +import Testing +@testable import MyRenderer + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/Package.swift b/Package.swift index aa32ba1e384..a1b09a03ae8 100644 --- a/Package.swift +++ b/Package.swift @@ -991,6 +991,13 @@ let package = Package( name: "SwiftBuildSupportTests", dependencies: ["SwiftBuildSupport", "_InternalTestSupport", "_InternalBuildTestSupport"] ), + .testTarget( + name: "BuildMetalTests", + dependencies: [ + "_InternalTestSupport", + "Basics" + ] + ), // Examples (These are built to ensure they stay up to date with the API.) .executableTarget( name: "package-info", diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift index 53c7a428be8..48ab9b535c1 100644 --- a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift @@ -19,6 +19,8 @@ import struct Basics.SourceControlURL import struct Basics.Diagnostic import struct Basics.ObservabilityMetadata import class Basics.ObservabilityScope +import class Basics.AsyncProcess +import struct Basics.AsyncProcessResult import class PackageModel.Manifest import class PackageModel.Package @@ -179,6 +181,9 @@ public final class PackagePIFBuilder { /// The file system to read from. let fileSystem: FileSystem + /// Path to the Metal compiler, resolved via `xcrun --find metal` or 'metal' if not found. + let metalCompilerPath: String + /// Whether to suppress warnings from compilers, linkers, and other build tools for package dependencies. private var suppressWarningsForPackageDependencies: Bool { UserDefaults.standard.bool(forKey: "SuppressWarningsForPackageDependencies", defaultValue: true) @@ -215,6 +220,7 @@ public final class PackagePIFBuilder { self.fileSystem = fileSystem self.observabilityScope = observabilityScope self.addLocalRpaths = addLocalRpaths + self.metalCompilerPath = Self.findMetalCompilerPath(observabilityScope: observabilityScope) } public init( @@ -239,6 +245,7 @@ public final class PackagePIFBuilder { self.packageDisplayVersion = packageDisplayVersion self.fileSystem = fileSystem self.observabilityScope = observabilityScope + self.metalCompilerPath = Self.findMetalCompilerPath(observabilityScope: observabilityScope) } /// Build an empty PIF project. @@ -660,6 +667,36 @@ public final class PackagePIFBuilder { self.rule = resource.rule } } + + private static func findMetalCompilerPath(observabilityScope: ObservabilityScope) -> String { + do { + let result = try AsyncProcess.popen( + arguments: ["xcrun", "--find", "metal"] + ) + guard result.exitStatus == .terminated(code: 0) else { + observabilityScope.emit( + debug: "Failed to find Metal compiler using xcrun, exited with \(result.exitStatus). Defaulting to 'metal'" + ) + return "metal" + } + + let output = try result.utf8Output() + + let path = output.trimmingCharacters(in: .whitespacesAndNewlines) + + guard !path.isEmpty else { + observabilityScope.emit(debug: "Metal compiler path is empty, defaulting to 'metal'") + return "metal" + } + + return path + } catch { + observabilityScope.emit( + debug: "Error \(error) finding Metal compiler, defaulting to 'metal'" + ) + return "metal" + } + } } // MARK: - Helpers diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder.swift index 6684a2e5e9e..1e39e7304ae 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder.swift @@ -220,6 +220,8 @@ struct PackagePIFProjectBuilder { settings[.COREML_COMPILER_CONTAINER] = "swift-package" settings[.COREML_CODEGEN_LANGUAGE] = "None" + settings[.MTL_COMPILER_PATH] = self.pifBuilder.metalCompilerPath + self.project[keyPath: resourcesTargetKeyPath].common.addBuildConfig { id in BuildConfig(id: id, name: "Debug", settings: settings) } diff --git a/Tests/BuildMetalTests/BuildMetalTests.swift b/Tests/BuildMetalTests/BuildMetalTests.swift new file mode 100644 index 00000000000..fe48acd6d7b --- /dev/null +++ b/Tests/BuildMetalTests/BuildMetalTests.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import _InternalTestSupport +import Testing +import Basics +import Foundation +import Metal + +@Suite +struct BuildMetalTests { + + @Test( + .tags( + .TestSize.large + ), + .requireHostOS(.macOS), + arguments: getBuildData(for: [.swiftbuild]), + ) + func simpleLibrary(data: BuildData) async throws { + let buildSystem = data.buildSystem + let configuration = data.config + + try await fixture(name: "Metal/SimpleLibrary") { fixturePath in + + // Build the package + let (_, _) = try await executeSwiftBuild( + fixturePath, + configuration: configuration, + buildSystem: buildSystem, + throwIfCommandFails: true + ) + + // Get the bin path + let (binPathOutput, _) = try await executeSwiftBuild( + fixturePath, + configuration: configuration, + extraArgs: ["--show-bin-path"], + buildSystem: buildSystem, + throwIfCommandFails: true + ) + + let binPath = try AbsolutePath(validating: binPathOutput.trimmingCharacters(in: .whitespacesAndNewlines)) + + // Check that default.metallib exists + let metallibPath = binPath.appending(components:["MyRenderer_MyRenderer.bundle", "Contents", "Resources", "default.metallib"]) + #expect( + localFileSystem.exists(metallibPath), + "Expected default.metallib to exist at \(metallibPath)" + ) + + // Verify we can load the metal library + let device = MTLCreateSystemDefaultDevice()! + let library = try device.makeLibrary(URL: URL(fileURLWithPath: metallibPath.pathString)) + + #expect(library.functionNames.contains("simpleVertexShader")) + } + } + +}