Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -1017,6 +1017,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
63 changes: 62 additions & 1 deletion Sources/PackageModel/UserToolchain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,8 @@ public final class UserToolchain: Toolchain {
)
}

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

self.configuration = .init(
librarianPath: librarianPath,
swiftCompilerPath: swiftCompilers.manifest,
Expand All @@ -939,7 +941,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 @@ -1071,6 +1075,55 @@ public final class UserToolchain: Toolchain {
return (platform, info)
}

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

let xcrunCmd = ["/usr/bin/xcrun", "--find", "metal"]
guard let output = try? AsyncProcess.checkNonZeroExit(arguments: xcrunCmd, environment: environment).spm_chomp() else {
return nil
}

guard let metalPath = try? AbsolutePath(validating: output) else {
return nil
}

guard let toolchainPath: AbsolutePath = {
var currentPath = metalPath
while currentPath != currentPath.parentDirectory {
if currentPath.basename == "Metal.xctoolchain" {
return currentPath
}
currentPath = currentPath.parentDirectory
}
return nil
}() else {
return nil
}

let toolchainInfoPlist = toolchainPath.appending(component: "ToolchainInfo.plist")

struct MetalToolchainInfo: Decodable {
let Identifier: String
}

let toolchainIdentifier: String
do {
let data: Data = try fileSystem.readFileContents(toolchainInfoPlist)
let info = try PropertyListDecoder().decode(MetalToolchainInfo.self, from: data)
toolchainIdentifier = info.Identifier
} catch {
return nil
}

return (path: toolchainPath.parentDirectory, identifier: toolchainIdentifier)
}

// TODO: We should have some general utility to find tools.
private static func deriveXCTestPath(
swiftSDK: SwiftSDK,
Expand Down Expand Up @@ -1254,6 +1307,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
31 changes: 21 additions & 10 deletions Sources/SwiftBuildSupport/SwiftBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,21 @@ package 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 @@ -546,7 +553,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 @@ -881,16 +888,20 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
if setToolchainSetting {
// If the SwiftPM toolchain corresponds to a toolchain registered with the lower level build system, add it to the toolchain stack.
// Otherwise, apply overrides for each component of the SwiftPM toolchain.
if let toolchainID = try await session.lookupToolchain(at: buildParameters.toolchain.toolchainDir.pathString) {
settings["TOOLCHAINS"] = "\(toolchainID.rawValue) $(inherited)"
} else {
let toolchainID = try await session.lookupToolchain(at: buildParameters.toolchain.toolchainDir.pathString)
if toolchainID == nil {
// FIXME: This list of overrides is incomplete.
// An error with determining the override should not be fatal here.
settings["CC"] = try? buildParameters.toolchain.getClangCompiler().pathString
// Always specify the path of the effective Swift compiler, which was determined in the same way as for the
// native build system.
settings["SWIFT_EXEC"] = buildParameters.toolchain.swiftCompilerPath.pathString
}

let overrideToolchains = [buildParameters.toolchain.metalToolchainId, toolchainID?.rawValue].compactMap { $0 }
if !overrideToolchains.isEmpty {
settings["TOOLCHAINS"] = (overrideToolchains + ["$(inherited)"]).joined(separator: " ")
}
}

for sanitizer in buildParameters.sanitizers.sanitizers {
Expand Down Expand Up @@ -1250,7 +1261,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
package func createLongLivedSession(name: String) async throws -> LongLivedBuildServiceSession {
let service = try await SWBBuildService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint))
do {
let (session, diagnostics) = try await createSession(service: service, name: name, toolchainPath: buildParameters.toolchain.toolchainDir, packageManagerResourcesDirectory: packageManagerResourcesDirectory)
let (session, diagnostics) = try await createSession(service: service, name: name, toolchain: buildParameters.toolchain, packageManagerResourcesDirectory: packageManagerResourcesDirectory)
let teardownHandler = {
try await session.close()
await service.close()
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
71 changes: 71 additions & 0 deletions Tests/BuildMetalTests/BuildMetalTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//===----------------------------------------------------------------------===//
//
// 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
#if os(macOS)
import Metal
#endif

@Suite
struct BuildMetalTests {

#if os(macOS)
@Test(
.disabled("Require downloadable Metal toolchain"),
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): is it possible to detect the location of the metal toolchain and enable the test if it's detected?

.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 = try #require(MTLCreateSystemDefaultDevice())
let library = try device.makeLibrary(URL: URL(fileURLWithPath: metallibPath.pathString))

#expect(library.functionNames.contains("simpleVertexShader"))
}
}
#endif
}
Loading