Swift Package with Metal

Hi, I've got a Swift Framework with a bunch of Metal files. Currently users have to manually include a Metal Lib in their bundle provided separately, to use the Swift Package.

First question; Is there a way to make a Metal Lib target in a Swift Package, and just include the .metal files? (without a binary asset)

Second question; If not, Swift 5.3 has resource support, how would you recommend to bundle a Metal Lib in a Swift Package?
  • annca answer worked and it should be the accepted answer, it has no hacks or workarounds.

Add a Comment

Accepted Reply

I was successfully able to copy my metallib resources to my swift package target with swift-tools-version:5.3

I added a folder with different metallib files in my target like this: resources: [.copy("Metal/")]

Then I could access my metallib url via this code:
Code Block swift
let metalLibURL: URL = Bundle.module.url(forResource: "MyMetalLib", withExtension: "metallib", subdirectory: "Metal")!

Note that Bundle.module is only available once you have specified your resources in the package manifest.


Then I could access my shader without any problem:
Code Block swift
guard let metalDevice: MTLDevice = MTLCreateSystemDefaultDevice() else { return }
guard let metalLib: MTLLibrary = try? metalDevice.makeLibrary(filepath: metalLibURL.path) else { return }
guard let metalShader: MTLFunction = metalLib.makeFunction(name: "myMetalFunc") else { return }


Regarding a future Swift & Metal Package with only .swift & .metal files, and no .metallib resources, there would be no special settings I would need, just access to the MTLFunction shaders.


Anton ⬢ hexagons.net

Replies

Hello,

There isn't currently a way to include .metal source files in a Swift Package and have them compiled. So with Swift Package Manager 5.3, the client app would need to manually include the metal library, as with earlier versions.

It sounds as if this would be a useful future enhancement; in your case, would there also need to be any options or settings that the package target would need to set that affects how the .metal library is built?
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.

I was successfully able to copy my metallib resources to my swift package target with swift-tools-version:5.3

I added a folder with different metallib files in my target like this: resources: [.copy("Metal/")]

Then I could access my metallib url via this code:
Code Block swift
let metalLibURL: URL = Bundle.module.url(forResource: "MyMetalLib", withExtension: "metallib", subdirectory: "Metal")!

Note that Bundle.module is only available once you have specified your resources in the package manifest.


Then I could access my shader without any problem:
Code Block swift
guard let metalDevice: MTLDevice = MTLCreateSystemDefaultDevice() else { return }
guard let metalLib: MTLLibrary = try? metalDevice.makeLibrary(filepath: metalLibURL.path) else { return }
guard let metalShader: MTLFunction = metalLib.makeFunction(name: "myMetalFunc") else { return }


Regarding a future Swift & Metal Package with only .swift & .metal files, and no .metallib resources, there would be no special settings I would need, just access to the MTLFunction shaders.


Anton ⬢ hexagons.net
I did it like this with Xcode 12.0.1 and SPM 5.3:
  1. Put my metal shader file inside directory: ProjectRootDirectory/Sources/TargetName/Metal/Shaders.metal

  2. In Package.swift

Code Block
.target(
      name: "TargetName",
      dependencies: [],
      resources: [.process("Metal/Shaders.metal")]
 )

3. Then I was able to load the library inside my package like this:

Code Block
guard let library = try? device.makeDefaultLibrary(bundle: Bundle.module)
      else { fatalError("Unable to create default library") }


  • It worked, thanks, this should be the accepted answer, it is way easier.

  • I have tried but it didn't work.

Add a Comment
@Developer Tools Engineer
It would be a very nice feature to the Swift Package Manager. I don't think any special settings would be needed in my case. Thanks.
Swift Package Manager in tools 5.3 does support Metal compilation like others said. However this is for pure non-CoreImage metal files. If you need to add Metal Compiler Flags -fcikernel and Metal Linker Flags -cikernel, it seems you need to compile your metal files manually. And note that currently you can't have both non-CoreImage metal code and CoreImage metal code in the same library... it seems the core image flags ****-up the non-CoreImage code...

Guys how u compile it for ios simulator? My bundle won't compiled for ios simulato on M1 macbook with error: bundle format unrecognized, invalid, or unsuitable

Any improvements on Metal compilation workflows in Xcode 15 beta?