-
Notifications
You must be signed in to change notification settings - Fork 0
Auto-compile Metal shaders via SwiftPM build plugin #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import Foundation | ||
| import PackagePlugin | ||
|
|
||
| /// Compiles Shaders/*.metal into default.metallib at build time, replacing | ||
| /// the checked-in metallib + tools/build_shaders.sh manual step. | ||
| @main | ||
| struct MetalCompilerPlugin: BuildToolPlugin { | ||
| func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { | ||
| guard let target = target as? SourceModuleTarget else { return [] } | ||
| let shaderDir = target.directory.appending("Shaders") | ||
| let fm = FileManager.default | ||
| guard let entries = try? fm.contentsOfDirectory(atPath: shaderDir.string) else { return [] } | ||
| let metalFiles = entries.filter { $0.hasSuffix(".metal") }.sorted() | ||
| .map { shaderDir.appending($0) } | ||
| guard !metalFiles.isEmpty else { return [] } | ||
|
Comment on lines
+12
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail fast when shader discovery fails (don’t silently no-op). Line 12 and Line 15 return Suggested fix+enum MetalCompilerPluginError: Error {
+ case shaderDirectoryUnreadable(String)
+ case noMetalShadersFound(String)
+}
+
`@main`
struct MetalCompilerPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
guard let target = target as? SourceModuleTarget else { return [] }
let shaderDir = target.directory.appending("Shaders")
let fm = FileManager.default
- guard let entries = try? fm.contentsOfDirectory(atPath: shaderDir.string) else { return [] }
+ let entries: [String]
+ do {
+ entries = try fm.contentsOfDirectory(atPath: shaderDir.string)
+ } catch {
+ throw MetalCompilerPluginError.shaderDirectoryUnreadable(shaderDir.string)
+ }
let metalFiles = entries.filter { $0.hasSuffix(".metal") }.sorted()
.map { shaderDir.appending($0) }
- guard !metalFiles.isEmpty else { return [] }
+ guard !metalFiles.isEmpty else {
+ throw MetalCompilerPluginError.noMetalShadersFound(shaderDir.string)
+ }🤖 Prompt for AI Agents |
||
|
|
||
| let out = context.pluginWorkDirectory.appending("default.metallib") | ||
| // the plugin sandbox only permits writes inside pluginWorkDirectory — | ||
| // metal's clang module cache must live there too or cold builds fail | ||
| let moduleCache = context.pluginWorkDirectory.appending("ModuleCache") | ||
| return [.buildCommand( | ||
| displayName: "Compiling \(metalFiles.count) Metal shaders → default.metallib", | ||
| executable: Path("/usr/bin/xcrun"), | ||
| arguments: ["-sdk", "macosx", "metal", | ||
| "-fmodules-cache-path=\(moduleCache.string)"] | ||
| + metalFiles.map(\.string) + ["-o", out.string], | ||
| inputFiles: metalFiles, | ||
| outputFiles: [out] | ||
| )] | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
For// swift-tools-version: 5.10, is this valid inPackage.swift`?.target(name: "SwiftRender", plugins: ["MetalCompilerPlugin"])
Or must plugin usage be declared as:
.target(name: "SwiftRender", plugins: [.plugin(name: "MetalCompilerPlugin")])?`
💡 Result:
Yes, for
// swift-tools-version: 5.10, the following is valid:.target(name: "SwiftRender", plugins: ["MetalCompilerPlugin"])You must only use the explicit structured form when you need to specify the plugin’s package (i.e., the plugin is provided by another package). In that case the syntax is:.target(name: "SwiftRender", plugins: [.plugin(name: "MetalCompilerPlugin")])If the plugin is from a dependency, you’d use the package-qualified form:.target(name: "SwiftRender", plugins: [.plugin(name: "MetalCompilerPlugin", package: "<package-name>")])Evidence: Apple’s plugin product/target API documents the.plugin(name:capability:dependencies:path:exclude:sources:)and shows plugin usage in target declarations using.plugin(name: ...)(i.e., structured entries), and SwiftPM’s manifest/plugin usage is defined asplugins: [PluginUsage]wherePluginUsageprovidesplugin(_ name: String, package: String? = nil). [1][2] Also, the SwiftPM evolution/proposal docs show the modern/typed syntax for target plugin usage asplugins: [ .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator") ], and it notes that when the plugin is within the same package you omit--package(so no package parameter is needed). [3]Citations:
🌐 Web query:
SwiftPM 5.10 Package.swift target plugins: ["PluginName"] syntax valid?💡 Result:
Yes, that specific syntax is valid (for SwiftPM 5.10 and the modern manifest API): in a target declaration you can use the
plugins:parameter with an array ofTarget.PluginUsage, written likeplugins: [.plugin(name: "PluginName")](optionally withpackage:for plugins coming from another package) [1][2]. Example: let package = Package( name: "Example", targets: [.target( name: "MyTarget", plugins: [.plugin(name: "PluginName") ]) ]) Important constraints / common gotchas: 1) Theplugins:parameter applies only to targets like.target(...),.executableTarget(...), and.testTarget(...)that use the manifest API supporting plugin usage [2]. 2) The plugin must be declared as a plugin target in the samePackage.swift(or otherwise present in the dependency closure) and must have a compatible capability (e.g., if it’s used viaplugins:, SwiftPM expects the plugin to support thebuildToolcapability, since plugins are “applied to a target” before the build system creates its dependency graph) [2]. 3) If you declare a plugin capability that doesn’t match what SwiftPM expects, you can get build errors complaining about mismatched capabilities (for instance, a declared command plugin being treated inconsistently) [3].Citations:
🌐 Web query:
SwiftPM 5.10 plugins: ["PluginName"] in Package.swift target declaration valid or must use .plugin(name:)💡 Result:
In SwiftPM 5.10, a target’s plugin usage must be declared with.plugin(name:) (optionally with package:), inside the target’s plugins: array. You cannot just list plugin names like ["PluginName"] in Package.swift’s target declaration. Authoritative syntax: - Define a plugin target in targets with.plugin(name:..., capability:...). [1][2] - Attach a plugin to a regular target using the plugins: parameter on.target /.executableTarget /.testTarget, with plugins: [.plugin(name: "OpenAPIGenerator", package: "...") ]. [3][4] So if your target looks like: - Wrong (not supported):.target(name: "X", plugins: ["PluginName"]) - Right (supported):.target(name: "X", plugins: [.plugin(name: "PluginName")]) (add package: only if the plugin comes from another package). [3][4]
Citations:
Fix SwiftPM target plugin declaration syntax in
Package.swift(line 19).plugins: ["MetalCompilerPlugin"]isn’t valid; target plugin usage must be declared asplugins: [.plugin(name: "MetalCompilerPlugin")](addpackage:only if the plugin comes from another package).🤖 Prompt for AI Agents