Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
19 changes: 19 additions & 0 deletions Fixtures/Metal/SimpleLibrary/Package.swift
Original file line number Diff line number Diff line change
@@ -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")
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import MySharedTypes


let vertex = AAPLVertex(position: .init(250, -250), color: .init(1, 0, 0, 1))
12 changes: 12 additions & 0 deletions Fixtures/Metal/SimpleLibrary/Sources/MyRenderer/Shaders.metal
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// A relative path to SharedTypes.h.
#import "../MySharedTypes/include/SharedTypes.h"

#include <metal_stdlib>
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);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#ifndef SharedTypes_h
#define SharedTypes_h


#import <simd/simd.h>


typedef struct {
vector_float2 position;
vector_float4 color;
} AAPLVertex;


#endif /* SharedTypes_h */
Original file line number Diff line number Diff line change
@@ -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.
}
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,13 @@ let package = Package(
name: "SwiftBuildSupportTests",
dependencies: ["SwiftBuildSupport", "_InternalTestSupport", "_InternalBuildTestSupport"]
),
.testTarget(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should consider putting the new test in SwiftBuildSupportTests instead of creating a whole new test target since this is a fairly specific test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll include a follow-up

name: "BuildMetalTests",
dependencies: [
"_InternalTestSupport",
"Basics"
]
),
// Examples (These are built to ensure they stay up to date with the API.)
.executableTarget(
name: "package-info",
Expand Down
6 changes: 6 additions & 0 deletions Sources/PackageModel/Toolchain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public protocol Toolchain {
/// The manifest and library locations used by this toolchain.
var swiftPMLibrariesLocation: ToolchainConfiguration.SwiftPMLibrariesLocation { get }

/// Path to the Metal toolchain if available.
var metalToolchainPath: AbsolutePath? { get }

// Metal toolchain ID if available.
var metalToolchainId: String? { get }

/// Path of the `clang` compiler.
func getClangCompiler() throws -> AbsolutePath

Expand Down
16 changes: 15 additions & 1 deletion Sources/PackageModel/ToolchainConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public struct ToolchainConfiguration {
/// Currently computed only for Windows.
public var swiftTestingPath: AbsolutePath?

/// Path to the Metal toolchain.
/// This is optional and only available on Darwin platforms.
public var metalToolchainPath: AbsolutePath?

/// Metal toolchain identifier
/// This is optional and only available on Darwin platforms.
public var metalToolchainId: String?

/// Creates the set of manifest resources associated with a `swiftc` executable.
///
/// - Parameters:
Expand All @@ -56,6 +64,8 @@ public struct ToolchainConfiguration {
/// - swiftPMLibrariesRootPath: Custom path for SwiftPM libraries. Computed based on the compiler path by default.
/// - sdkRootPath: Optional path to SDK root.
/// - xctestPath: Optional path to XCTest.
/// - swiftTestingPath: Optional path to swift-testing.
/// - metalToolchainPath: Optional path to Metal toolchain.
public init(
librarianPath: AbsolutePath,
swiftCompilerPath: AbsolutePath,
Expand All @@ -64,7 +74,9 @@ public struct ToolchainConfiguration {
swiftPMLibrariesLocation: SwiftPMLibrariesLocation? = nil,
sdkRootPath: AbsolutePath? = nil,
xctestPath: AbsolutePath? = nil,
swiftTestingPath: AbsolutePath? = nil
swiftTestingPath: AbsolutePath? = nil,
metalToolchainPath: AbsolutePath? = nil,
metalToolchainId: String? = nil
) {
let swiftPMLibrariesLocation = swiftPMLibrariesLocation ?? {
return .init(swiftCompilerPath: swiftCompilerPath)
Expand All @@ -78,6 +90,8 @@ public struct ToolchainConfiguration {
self.sdkRootPath = sdkRootPath
self.xctestPath = xctestPath
self.swiftTestingPath = swiftTestingPath
self.metalToolchainPath = metalToolchainPath
self.metalToolchainId = metalToolchainId
}
}

Expand Down
45 changes: 44 additions & 1 deletion Sources/PackageModel/UserToolchain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,8 @@ public final class UserToolchain: Toolchain {
)
}

let metalToolchain = try? Self.deriveMetalToolchainPath(triple: triple, environment: environment)

self.configuration = .init(
librarianPath: librarianPath,
swiftCompilerPath: swiftCompilers.manifest,
Expand All @@ -909,7 +911,9 @@ public final class UserToolchain: Toolchain {
swiftPMLibrariesLocation: swiftPMLibrariesLocation,
sdkRootPath: self.swiftSDK.pathsConfiguration.sdkRootPath,
xctestPath: xctestPath,
swiftTestingPath: swiftTestingPath
swiftTestingPath: swiftTestingPath,
metalToolchainPath: metalToolchain?.path,
metalToolchainId: metalToolchain?.identifier
)

self.fileSystem = fileSystem
Expand Down Expand Up @@ -1041,6 +1045,37 @@ public final class UserToolchain: Toolchain {
return (platform, info)
}

private static func deriveMetalToolchainPath(
triple: Basics.Triple,
environment: Environment
) throws -> (path: AbsolutePath, identifier: String)? {
guard triple.isDarwin() else {
return nil
}

let xcodebuildArgs = ["/usr/bin/xcrun", "xcodebuild", "-showComponent", "metalToolchain", "-json"]
guard let output = try? AsyncProcess.checkNonZeroExit(arguments: xcodebuildArgs, environment: environment)
.spm_chomp() else {
return nil
}

guard let json = try? JSON(string: output) else {
return nil
}

guard let status = try? json.get("status") as String, status == "installed" else {
return nil
}

guard let toolchainSearchPath = try? json.get("toolchainSearchPath") as String,
let toolchainIdentifier = try? json.get("toolchainIdentifier") as String else {
return nil
}

let path = try AbsolutePath(validating: toolchainSearchPath)
return (path: path, identifier: toolchainIdentifier)
}

// TODO: We should have some general utility to find tools.
private static func deriveXCTestPath(
swiftSDK: SwiftSDK,
Expand Down Expand Up @@ -1224,6 +1259,14 @@ public final class UserToolchain: Toolchain {
configuration.sdkRootPath
}

public var metalToolchainPath: AbsolutePath? {
configuration.metalToolchainPath
}

public var metalToolchainId: String? {
configuration.metalToolchainId
}

public var swiftCompilerEnvironment: Environment {
configuration.swiftCompilerEnvironment
}
Expand Down
27 changes: 21 additions & 6 deletions Sources/SwiftBuildSupport/SwiftBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,21 @@ func withService<T>(
public func createSession(
service: SWBBuildService,
name: String,
toolchainPath: Basics.AbsolutePath,
toolchain: Toolchain,
packageManagerResourcesDirectory: Basics.AbsolutePath?
) async throws-> (SWBBuildServiceSession, [SwiftBuildMessage.DiagnosticInfo]) {

var buildSessionEnv: [String: String]? = nil
if let metalToolchainPath = toolchain.metalToolchainPath {
buildSessionEnv = ["EXTERNAL_TOOLCHAINS_DIR": metalToolchainPath.pathString]
}
let toolchainPath = try toolchain.toolchainDir

// SWIFT_EXEC and SWIFT_EXEC_MANIFEST may need to be overridden in debug scenarios in order to pick up Open Source toolchains
let sessionResult = if toolchainPath.components.contains(where: { $0.hasSuffix(".app") }) {
await service.createSession(name: name, developerPath: nil, resourceSearchPaths: packageManagerResourcesDirectory.map { [$0.pathString] } ?? [], cachePath: nil, inferiorProductsPath: nil, environment: nil)
await service.createSession(name: name, developerPath: nil, resourceSearchPaths: packageManagerResourcesDirectory.map { [$0.pathString] } ?? [], cachePath: nil, inferiorProductsPath: nil, environment: buildSessionEnv)
} else {
await service.createSession(name: name, swiftToolchainPath: toolchainPath.pathString, resourceSearchPaths: packageManagerResourcesDirectory.map { [$0.pathString] } ?? [], cachePath: nil, inferiorProductsPath: nil, environment: nil)
await service.createSession(name: name, swiftToolchainPath: toolchainPath.pathString, resourceSearchPaths: packageManagerResourcesDirectory.map { [$0.pathString] } ?? [], cachePath: nil, inferiorProductsPath: nil, environment: buildSessionEnv)
}
switch sessionResult {
case (.success(let session), let diagnostics):
Expand All @@ -84,14 +91,14 @@ public func createSession(
func withSession(
service: SWBBuildService,
name: String,
toolchainPath: Basics.AbsolutePath,
toolchain: Toolchain,
packageManagerResourcesDirectory: Basics.AbsolutePath?,
body: @escaping (
_ session: SWBBuildServiceSession,
_ diagnostics: [SwiftBuild.SwiftBuildMessage.DiagnosticInfo]
) async throws -> Void
) async throws {
let (session, diagnostics) = try await createSession(service: service, name: name, toolchainPath: toolchainPath, packageManagerResourcesDirectory: packageManagerResourcesDirectory)
let (session, diagnostics) = try await createSession(service: service, name: name, toolchain: toolchain, packageManagerResourcesDirectory: packageManagerResourcesDirectory)
do {
try await body(session, diagnostics)
} catch let bodyError {
Expand Down Expand Up @@ -544,7 +551,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {

var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:]
do {
try await withSession(service: service, name: self.buildParameters.pifManifest.pathString, toolchainPath: self.buildParameters.toolchain.toolchainDir, packageManagerResourcesDirectory: self.packageManagerResourcesDirectory) { session, _ in
try await withSession(service: service, name: self.buildParameters.pifManifest.pathString, toolchain: self.buildParameters.toolchain, packageManagerResourcesDirectory: self.packageManagerResourcesDirectory) { session, _ in
self.outputStream.send("Building for \(self.buildParameters.configuration == .debug ? "debugging" : "production")...\n")

// Load the workspace, and set the system information to the default
Expand Down Expand Up @@ -885,6 +892,14 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
settings["SWIFT_EXEC"] = buildParameters.toolchain.swiftCompilerPath.pathString
}

if let metalToolchainId = buildParameters.toolchain.metalToolchainId {
if let toolChains = settings["TOOLCHAINS"] {
settings["TOOLCHAINS"] = "\(metalToolchainId) " + toolChains
} else {
settings["TOOLCHAINS"] = "\(metalToolchainId)"
}
}

// FIXME: workaround for old Xcode installations such as what is in CI
settings["LM_SKIP_METADATA_EXTRACTION"] = "YES"
if let symbolGraphOptions {
Expand Down
4 changes: 4 additions & 0 deletions Sources/_InternalTestSupport/MockBuildTestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import SPMBuildCore
import TSCUtility

public struct MockToolchain: PackageModel.Toolchain {
public let metalToolchainId: String?
public let metalToolchainPath: Basics.AbsolutePath?
#if os(Windows)
public let librarianPath = AbsolutePath("/fake/path/to/link.exe")
#elseif canImport(Darwin)
Expand Down Expand Up @@ -54,6 +56,8 @@ public struct MockToolchain: PackageModel.Toolchain {

public init(swiftResourcesPath: AbsolutePath? = nil) {
self.swiftResourcesPath = swiftResourcesPath
self.metalToolchainPath = nil
self.metalToolchainId = nil
}
}

Expand Down
70 changes: 70 additions & 0 deletions Tests/BuildMetalTests/BuildMetalTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//===----------------------------------------------------------------------===//
//
// 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 {

#if os(macOS)
@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"))
}
}
#endif
}