I also do this. (Apple, see FB7744335).
The main option I need is to link them into the same
metallib as the client, since my library contains e.g. math functions that a shader author would call from their shader (and not a set of shader functions that are ready to use, which is a different situation than mine). I also need to add my package to the metal header import paths in the user's library. It would be nice to have a way to specify arbitrary compiler/link flags as well.
For OP, my best-effort solution to this problem is I configure my metal library as a C package (this makes sense for me because I provide a CPU version, and it's also easier to develop/test on CPU anyway). Then for the metal version, I have this build script in the root of my package. At a high level, this concatenates all the
.c files into a single
.metal for them to drag in, and checks some details of their project configuration.
Code Block #!/usr/bin/swift |
import Foundation |
let m = FileManager.default |
let sourcePath = String(#file.dropLast(15)) |
m.changeCurrentDirectoryPath(sourcePath) //makemetal.swift |
var metalAccumulate = "" |
let sourceDir = "Sources/target/" |
for file in try m.contentsOfDirectory(atPath: sourceDir) { |
if file.hasSuffix(".c") { |
metalAccumulate += String(data: m.contents(atPath: sourceDir + file)!, encoding: .utf8)! |
} |
} |
|
let outPath = (ProcessInfo.processInfo.environment["SRCROOT"] ?? ".") + "/target.metal" |
try! metalAccumulate.write(to: URL(fileURLWithPath:outPath), atomically: false, encoding:.utf8) |
func checkPbxProj() { |
guard let srcRoot = ProcessInfo.processInfo.environment["SRCROOT"] else { return } |
guard let projectName = ProcessInfo.processInfo.environment["PROJECT_NAME"] else { return } |
guard let pbxproj = try? String(contentsOfFile: "\(srcRoot)/\(projectName).xcodeproj/project.pbxproj") else { return } |
if !pbxproj.contains("target.metal in Sources") { |
print("warning: Drag \(srcRoot)/target.metal into your xcodeproj") |
} |
} |
|
|
|
func checkHeaderPath() { |
guard let _ = ProcessInfo.processInfo.environment["XCODE_VERSION_MAJOR"] else { return } |
let searchPaths = ProcessInfo.processInfo.environment["MTL_HEADER_SEARCH_PATHS"] ?? "" |
if !searchPaths.contains("target/include") { |
print("warning: Add ${HEADER_SEARCH_PATHS} to the 'Metal Compiler - Build Options - Header Search Paths' setting") |
} |
} |
|
checkPbxProj() |
|
checkHeaderPath() |
Users then have a custom buildphase in their xcode project
Code Block set -e |
for v in ${HEADER_SEARCH_PATHS[@]}; do |
echo $v notfound |
if [[ $v == *"target"* ]]; then |
( |
SDKROOT="" |
$v/../../../makemetal.swift |
) |
fi |
done |
This is the best solution I have come up with, but there's a number of annoyances with it:
Users have to modify their xcode project in numerous ways, such as a custom buildphase, adding a file to their project manually, setting custom metal import paths etc.
I can mitigate this somewhat by checking the situation in my buildscript and warning them about what they did wrong, but I rely a lot on undocumented behavior to inspect xcode, so it's not a future-proof solution
custom buildphase requires input/output files to be set explicitly by the end user, so that xcode understands the dependencies and doesn't intermittently fail. Specifying the inputs, in particular, is a trick because
... I abuse header search paths to search for the swiftpm packages, because there is no dedicated environment variable to figure out where they are
I am hoping the "Build GPU binaries with metal" talk provides some insights into how to solve some of these issues from the metal side at least, but obviously it's not out until tomorrow so I don't know what's in it.