Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 93 additions & 17 deletions Plugins/MetalCompilerPlugin/MetalPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ struct MetalCompiler: Decodable {
case scanInputsInDirectory = "find-inputs"
case includeDependencies = "include-dependencies"
case dependencyPathSuffix = "dependency-path-suffix"
case includePaths = "include-paths"
case inputs = "inputs"
case output = "output"
case cache = "cache"
Expand All @@ -51,6 +52,7 @@ struct MetalCompiler: Decodable {
var scanInputsInDirectory: Bool = true
var includeDependencies: Bool = false
var dependencyPathSuffix: String?
var includePaths: [String]?
var inputs: [String]
var output: String = "debug.metallib"
var cache: String?
Expand All @@ -69,6 +71,7 @@ struct MetalCompiler: Decodable {
scanInputsInDirectory = try container.decodeIfPresent(Bool.self, forKey: .scanInputsInDirectory) ?? true
includeDependencies = try container.decodeIfPresent(Bool.self, forKey: .includeDependencies) ?? false
dependencyPathSuffix = try container.decodeIfPresent(String.self, forKey: .dependencyPathSuffix)
includePaths = try container.decodeIfPresent([String].self, forKey: .includePaths)
inputs = try container.decodeIfPresent([String].self, forKey: .inputs) ?? []
output = try container.decodeIfPresent(String.self, forKey: .output) ?? "debug.metallib"
cache = try container.decodeIfPresent(String.self, forKey: .cache)
Expand Down Expand Up @@ -145,23 +148,27 @@ struct MetalCompiler: Decodable {

// Dependencies
if config.includeDependencies {
logger?("Including \(target.dependencies.count) dependency include path(s)")
for dependency: TargetDependency in target.dependencies {
switch dependency {
case .product:
Diagnostics.error("Product dependencies are not supported for include paths in MetalCompilerPlugin.")
case .target(let target):
let includePath: String
if let suffix = config.dependencyPathSuffix {
includePath = target.directory.appending([suffix]).string
} else {
includePath = target.directory.string
}
verbose?(" -I \(includePath)")
arguments += ["-I", includePath]
@unknown default:
Diagnostics.error("Unknown dependency type in MetalCompilerPlugin.")
}
logger?("Including dependency include path(s)")
var visited = Set<String>()
let includePaths = collectIncludePaths(
from: target.dependencies,
suffix: config.dependencyPathSuffix,
visited: &visited,
verbose: verbose
)
logger?("Found \(includePaths.count) dependency include path(s)")
for path in includePaths {
arguments += ["-I", path]
}
}

// User-specified include paths
if let includePaths = config.includePaths {
logger?("Adding \(includePaths.count) custom include path(s)")
for relativePath in includePaths {
let fullPath = target.directory.appending([relativePath]).string
verbose?(" -I \(fullPath)")
arguments += ["-I", fullPath]
}
}

Expand Down Expand Up @@ -204,6 +211,75 @@ struct MetalCompiler: Decodable {
outputFiles: [output]
)
}

private func collectIncludePaths(
from dependencies: [TargetDependency],
suffix: String?,
visited: inout Set<String>,
verbose: ((String) -> Void)?
) -> [String] {
var paths: [String] = []

for dependency in dependencies {
switch dependency {
case .product(let product):
// Process all targets in the product
for target in product.targets {
let targetID = target.id
guard !visited.contains(targetID) else { continue }
visited.insert(targetID)

// Add this target's path
let includePath: String
if let suffix = suffix {
includePath = target.directory.appending([suffix]).string
} else {
includePath = target.directory.string
}
paths.append(includePath)
verbose?(" -I \(includePath)")

// Recursively process this target's dependencies
let nestedPaths = collectIncludePaths(
from: target.dependencies,
suffix: suffix,
visited: &visited,
verbose: verbose
)
paths.append(contentsOf: nestedPaths)
}

case .target(let target):
let targetID = target.id
guard !visited.contains(targetID) else { continue }
visited.insert(targetID)

// Add this target's path
let includePath: String
if let suffix = suffix {
includePath = target.directory.appending([suffix]).string
} else {
includePath = target.directory.string
}
paths.append(includePath)
verbose?(" -I \(includePath)")

// Recursively process this target's dependencies
let nestedPaths = collectIncludePaths(
from: target.dependencies,
suffix: suffix,
visited: &visited,
verbose: verbose
)
paths.append(contentsOf: nestedPaths)

@unknown default:
Diagnostics.error("Unknown dependency type in MetalCompilerPlugin.")
}
}

return paths
}
}

extension PackagePlugin.Target {
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ All configuration options are optional. Without any configuration file, the plug
"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",
Expand All @@ -94,10 +95,12 @@ All configuration options are optional. Without any configuration file, the plug

- **`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.
- **`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.
Expand Down Expand Up @@ -153,6 +156,16 @@ For including headers from dependency targets:
}
```

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).
Expand Down