diff --git a/Plugins/MetalCompilerPlugin/MetalPlugin.swift b/Plugins/MetalCompilerPlugin/MetalPlugin.swift index 4df047d..2448e26 100644 --- a/Plugins/MetalCompilerPlugin/MetalPlugin.swift +++ b/Plugins/MetalCompilerPlugin/MetalPlugin.swift @@ -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" @@ -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? @@ -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) @@ -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() + 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] } } @@ -204,6 +211,75 @@ struct MetalCompiler: Decodable { outputFiles: [output] ) } + + private func collectIncludePaths( + from dependencies: [TargetDependency], + suffix: String?, + visited: inout Set, + 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 { diff --git a/README.md b/README.md index 3397f1f..4a91460 100644 --- a/README.md +++ b/README.md @@ -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", @@ -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. @@ -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).