A Swift Package plugin to compile and link Core Image Metal Shaders to a single Metal Library that can be use in code.
This repo significantly changed some implementations of this original repo.
More details and insights about creating a Swift Package plugin can be read here.
Note: For Xcode 26 Beta, there will be some compiling issues when using Swift Package Plugin. More investigations are to come.
Why do you need this Swift Package Plugin?
- Compiling and linking Core Image Metal Shaders (not pure Metal Shaders) requires specifying certain flags (
-fcikernel
and-cikernel
) in the Xcode Build Settings. - Swift Package Manager doesn't support setting user-defined flags for compiling and linking Metal Shaders.
- Therefore, you have to put your Core Image Metal Shaders inside a Framework to prebuild, or inside a target to build.
- If you have Swift code that resides in a Swift Package and reads the Metal library, the code and the shader code can't be in the same package.
- Putting shader code inside targets to build increases the app bundle size, as the built artifacts will exist in every target that uses the shader code.
Note: Swift Package can build pure Metal Shader out of the box. Separating your Core Image Metal Shader and pure Metal Shader should be a good start.
Add this package as a dependency to your Swift Package:
dependencies: [
.package(url: "https://github.com/JuniperPhoton/CIMetalCompilerPlugin", from: "0.11.0")
]
Specify the plugin to use for your target.
targets: [
.target(
name: "MyPackage",
exclude: [
"Shaders/"
],
plugins: [
.plugin(name: "CIMetalCompilerPlugin", package: "CIMetalCompilerPlugin")
]
)
]
Note:
- You should put all your Core Image Metal Shaders in a folder like
Shaders
, as shown above, including any C headers that are included in the shaders. - You also need to explicitly exclude the entire
Shaders
folder, as the Swift Package build system will still try to build the shaders and will fail.
After the Swift Packgage being built, the default.metallib
will reside in the Package's Bundle. Get this file using Bundle.module
in your Swift Package's code:
let url = Bundle.module.url(forResource: "default", withExtension: "metallib")
It internally iterates over the directory to find all files that end with "metal".
Then it uses the equivalent command to create the intermediate files:
xcrun metal -c -fcikernel MyKernel.metal -o MyKernel.air "-fmodules=none"
Clang enables Modules for Metal by default. Enabling Modules will cause the build to fail when running on Xcode Cloud, as it may not have permissions to write the files to the system default cache dir. Thus, disabling Modules by specifying
-fmodules=none
or specifying the dir in the plugin sandbox dir-fmodules-cache-path=xxxx
will do the trick.
For each intermediate file:
xcrun metallib --cikernel MyKernel.air -o MyKernel.metallib
Those metallib
can be already used directly in code:
let resource = "your_metallib_name"
guard let url = Bundle.module.url(forResource: resource, withExtension: "metallib") else {
return nil
}
guard let data = try? Data(contentsOf: url) else {
return nil
}
let kernel = try? CIKernel(functionName: functionName, fromMetalLibraryData: data)
They can be then merged into one default.metallib
:
xcrun metal -fcikernel -o default.metallib MyKernel1.metallib MyKernel2.metallib ...
The original repo:
https://github.com/schwa/MetalCompilerPlugin
Building a Shader Library by Precompiling Source Files:
Xcrun:
https://keith.github.io/xcode-man-pages/xcrun.1.html
Clang Modules:
https://clang.llvm.org/docs/Modules.html#problems-with-the-current-model
BSD 3-clause. See LICENSE.md.