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
This 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
FILE_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
Archive 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
I 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
So 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
Create 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
in 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
your 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
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.