Build Swift Executable with Metal Library

When you build a Swift Package Xcode will compile all your .swift files to single executable file and process all other resources which includes Assets catalogue, Core Data files, Storyboards and many more including all .metal files. All these resources will be put in package Bundle and you have access to them through Bundle.module if you add them to resources array in package’s target.
But it will work only for library product type. If you want to build a Swift executable product resources won’t be processed or copied to Bundle because there is no actual Bundle there. So Swift executable is limited to only .swift files so you can’t build executable which will run you Metal shaders. Let’s try to fix it!
Metal allows offline library compilation and library initialisation from URL with MTLDevice instance method
func makeLibrary(URL url: URL) throws -> MTLLibraryThis method requires a valid URL to .metallib file so we need a way to build Metal Library from our .metal files, put it somewhere near our executable file and create library on the fly from it.
Let’s do it using shell script! You can do it manually on you Mac or run shell script on your CI assuming that CI is also a Mac server.
You can get the whole script here or go step by step with me.
Create intermediate representation from Metal files
First of all we need to locate all our .metal files and create .air file from each of them
#!/bin/bash
METAL_FILES=`ls /path/to/metal/files/*.metal`
for path in $METAL_FILES
do
FILE_NAME=${path%%.*}
xcrun -sdk macosx metal -c $path -o ${FILE_NAME}.air
doneFILE_NAME here will be the name of Metal file but without .metal extension. xcrun command will build .air file from each .metal file and put in the same folder where all your Metal files exist. We will remove them after we compile the whole library.
If you want to put .air files somewhere else just update -o flag to /path/to/air/files/${FILE_NAME}.air
Also if you have only one Metal file (assume it is named Shaders.metal) you can omit for-loop and run only
xcrun -sdk macosx metal -c Shaders.metal -o Shaders.airArchive AIR files to single MetalAR file
The next step will be to archive all .air files to single .metalar file which will be used to compile the .metallib file later on. You can also omit this step if you have a single .metal file which you already compiled to single .air file. The Metal Library can be created from .air or .metalar file it doesn’t matter.
This step will look pretty like the previous but we will iterate through .air files this time
AIR_FILES=`ls /path/to/air/files/*.air`
for path in $AIR_FILES
do
xcrun metal-ar r default.metalar $path
doneI used default.metalar name here like Xcode do when compiles Metal Library during package or app building. But you can use the name more appropriate for you of course. And you can put it anywhere you want if you replace default.metalar with /path/to/metalar/file/default.metalar.
metal-ar is the name of the tool which will archive .air files and r command will add the second file ($path in our case) to the first one and create it if needed.
So now we have a single .metalar file with all our Metal shaders in intermediate representation. Let’s build a Metal Library file from it.
Build Metal Library from MetalAR archive
This step is pretty straightforward. Again we need to run xcrun command to do the thing
xcrun -sdk macosx metallib default.metalar -o default.metallibSo now we have a .metallib file which is a good candidate to create MTLLibrary from. But the last but not least step is to be good citizens and clean up the shaders folder where we created all our intermediate files.
Clean up
rm default.metalar
for path in $AIR_FILES
do
rm $path
doneCreate MTLLibrary from .metallib file
When you archive you Swift executable Xcode will export it as a folder with your single executable file in Products/usr/local/bin folder inside. You can move it anywhere you want and run it from shell using path to executable file or if you add the executable location to $PATH you can run it from anywhere in your system. But we need a way to tell the executable file where our .metallib is located.
There are two ways to do it. If you create Swift executable you are already familiar with Apple’s Swift Argument Parser framework which is must-have dependency for Swift executables. So you can create an @Option for Metal Library file location which will be required for you script or if you build an executable for internal usage you can assume that .metallib file will be placed near your executable file and create library from it.
There is one thing you need to notice if you use the second approach. If you use
FileManager.default.currentDirectoryPathin your Swift code it will return you not the executable’s location but the current location from where you run your executable. So if you open the Terminal and run
/path/to/your/swift/executableyour current directory will be your user’s home directory and FileManager won’t be able to locate .metallib file which is placed near your executable but not in your home folder.
To get access to executable’s location you need to get process info from inside your run() method in Swift file. So the valid approach to locate .metallib file which is placed near executable will be
func run() throws {
guard let execPath = ProcessInfo.processInfo.arguments.first,
let device = MTLCreateSystemDefaultDevice()
else { return }
let execURL = URL(fileURLWithPath: execPath)
let execRootURL = execURL.deletingLastPathComponent()
let libraryURL = execRootURL.appendingPathComponent("default.metallib")
let library = try device.makeLibrary(URL: libraryURL)
// PROFIT!
}execPath and execURL here will be the full path to executable itself so we need to remove its name from the URL to get the path where it is located and where we put our .metallib file.
Conclusion
Now you’re not limited by Swift only files inside Swift executable and can build an executable which can run Metal Shaders right from the command line.
I personally used this flow to create the Swift Package with two products. The first one is a plain old Swift library which I can add as dependency to iOS apps and the second one is executable which has the first library as dependency and can be used to run the whole library functionality from command line. We use it to simplify library tests and to make the library available to run from Python scripts for example.
Here is the gist with all the previous shell code. I hope it will help you in creating awesome Swift packages with CLI availability.
If you have any questions feel free to shoot me an email to contact@mtldoc.com or connect via Twitter @petertretyakov.