# MetalCompilerPlugin A Swift Package Manager plugin to compile Metal files that can be debugged in Xcode Metal Debugger. ## Description Swift Package Manager now[^1] seems to compile all Metal files within a target into a `default.metallib`. Alas, this file cannot be debugged in Xcode Metal Debugger. > Unable to create shader debug session > > Source is unavailable > > Under the target's Build Settings, ensure the Metal Compiler Build Options produces debugging information and includes source code. > > If building with the 'metal' command line tool, include the options '-gline-tables-only' and '-frecord-sources'. ([Screenshot](Documentation/Screenshot%201.png)). This plug-in provides an alternative way to compile Metal files into a `metallib` that can be debugged. This project also shows how to create a ["_Pure-Metal target_"](#pure-metal-targets) that can be used to contain your Metal source code and header files. [^1]: Prior to Swift Package Manager 5.3 it was impossible to process Metal files at all. Version 5.3 added the capability to process resources, including Metal files. Somewhere between versions 5.3 and 5.7 Swift Package Manager gained the ability to transparently compile all Metal files in a package. ## Usage In your `Package.swift` file, add `MetalCompilerPlugin` as a dependency. And add the `MetalCompilerPlugin` to your target's `plugins` array. For example: ```swift dependencies: [ .package(url: "https://github.com/schwa/MetalCompilerPlugin", branch: "main"), ], targets: [ .target(name: "MyExampleShaders", plugins: [ .plugin(name: "MetalCompilerPlugin", package: "MetalCompilerPlugin") ]), ] ``` Note the title of the output metal library file will be `debug.metallib` and will live side-by-side with the `default.metallib` file. See [Limitations](#limitations) below. ## Limitations The output metal library file will be `debug.metallib` and will live side-by-side with the `default.metallib` file. This is because of the `default.metallib` file is created by the Swift Package Manager and cannot be overridden. You will not be able to use `MTLDevice.makeDefaultLibrary()` to load the `debug.metallib` file. Instead, you will need to use `MTLDevice.makeLibrary(url:)` to load the `debug.metallib` file. See the unit tests for an example. ## Pure-Metal Targets A "Pure-Metal" target is a target that contains only Metal source code and header files. This is useful for projects that contain a lot of Metal code and want to keep it separate from the rest of the project. This is also useful so that Metal and Swift can share types defined in common header files. For example, a Vertex or Uniforms struct defined in a header file can be used by both Metal and Swift code. Direct sharing of Metal types with Swift prevents duplication of types and makes sure that your types have a consistent layout and packing across Metal and Swift. Simply defining the same type in both Metal and Swift manually is not enough and can lead to subtle memory alignment-related crashes or data corruption. See the `ExampleShaders` target in the `Package.swift` file. The "Pure-Metal" target must not contain any Swift files. It should contain your Metal source code and header files (contained in an included folder). It should also contain a `Module.map` file that allows Swift to import the header files. ## Configuration The plugin can be configured by placing a `metal-compiler-plugin.json` or `.metal-compiler-plugin.json` file in your target's directory. If no configuration file is found, the plugin will use default settings. ### Configuration Options All configuration options are optional. Without any configuration file, the plugin will use the default settings (add debug flags, use xcrun, use a custom TMPDIR, do not enable logging). ```json { "xcrun": true, "metal": "/path/to/metal", "find-inputs": true, "include-dependencies": false, "dependency-path-suffix": "include", "include-paths": ["Headers", "Metal/Include"], "inputs": ["additional/file.metal"], "output": "debug.metallib", "cache": "/path/to/cache", "flags": ["-gline-tables-only", "-frecord-sources"], "plugin-logging": false, "verbose-logging": false, "metal-enable-logging": false, "logging-prefix": "[Metal]", "env": { "TMPDIR": "/private/tmp" } } ``` #### Option Descriptions - **`xcrun`** (boolean, default: `true`): Whether to use `xcrun` to find the metal compiler. When `true`, uses `/usr/bin/xcrun metal`. When `false`, you must specify the `metal` path. - **`metal`** (string, required when `xcrun` is `false`): Direct path to the metal compiler executable. - **`find-inputs`** (boolean, default: `true`): Whether to automatically scan the target directory for `.metal` files. When `true`, all `.metal` files in the target are included. - **`include-dependencies`** (boolean, default: `false`): Whether to include target dependencies as include paths (`-I`) when compiling. This allows Metal files to import headers from dependency targets. Product dependencies and their nested target dependencies are recursively processed. - **`dependency-path-suffix`** (string, optional): A path suffix to append to each dependency directory when generating include paths. Useful when headers are in a subdirectory like `include/`. Only applies when `include-dependencies` is `true`. - **`include-paths`** (array of strings, optional): Additional include paths relative to the target directory. Each path is prepended with the target directory and added as a `-I` flag. For example, `["Headers", "Metal/Include"]` will add `-I /path/to/target/Headers -I /path/to/target/Metal/Include` to the compiler arguments. - **`inputs`** (array of strings, default: `[]`): Additional input files to compile, in addition to those found by scanning (if enabled). - **`output`** (string, default: `"debug.metallib"`): Name of the output metallib file. - **`cache`** (string, default: plugin work directory): Path to the modules cache directory. - **`flags`** (array of strings, default: `["-gline-tables-only", "-frecord-sources"]`): Compiler flags to pass to the metal compiler. The default flags enable debugging in Xcode Metal Debugger. - **`plugin-logging`** (boolean, default: `false`): Enable logging from the plugin itself for debugging purposes. - **`verbose-logging`** (boolean, default: `false`): Enable more detailed verbose logging. Only takes effect when `plugin-logging` is also `true`. Shows additional details like all environment variables, full command arguments, and input/output file lists. - **`logging-prefix`** (string, optional): Custom prefix to prepend to all log messages from the plugin. Useful for distinguishing plugin output in complex build logs. - **`metal-enable-logging`** (boolean, default: `false`): Enable metal compiler logging by adding the `-fmetal-enable-logging` flag. - **`env`** (object, default: `{}`): Additional environment variables to set when running the metal compiler. ### Example Configuration For basic usage with debugging enabled: ```json { "plugin-logging": true } ``` For verbose debugging with a custom prefix: ```json { "plugin-logging": true, "verbose-logging": true, "logging-prefix": "[MyShaders]" } ``` For custom compiler flags: ```json { "flags": ["-gline-tables-only", "-frecord-sources", "-O2"] } ``` For including headers from dependency targets: ```json { "include-dependencies": true, "dependency-path-suffix": "include" } ``` For adding custom include paths within your target: ```json { "include-paths": ["Headers", "Shaders/Common", "Metal/Include"] } ``` This will automatically prepend your target directory to each path and add them as `-I` flags to the compiler. ## License BSD 3-clause. See [LICENSE.md](LICENSE.md). ## TODO - [ ] File and link to feedback items for the limitations and issues above. - [X] More configuration options. - [ ] Searching for the metallib works in Xcode Unit Tests but fails under `swift test`. Why?