spirv_builder/lib.rs
1// FIXME(eddyb) update/review these lints.
2//
3// BEGIN - Embark standard lints v0.4
4// do not change or add/remove here, but one can add exceptions after this section
5// for more info see: <https://github.com/EmbarkStudios/rust-ecosystem/issues/59>
6#![deny(unsafe_code)]
7#![warn(
8 clippy::all,
9 clippy::await_holding_lock,
10 clippy::char_lit_as_u8,
11 clippy::checked_conversions,
12 clippy::dbg_macro,
13 clippy::debug_assert_with_mut_call,
14 clippy::doc_markdown,
15 clippy::empty_enum,
16 clippy::enum_glob_use,
17 clippy::exit,
18 clippy::expl_impl_clone_on_copy,
19 clippy::explicit_deref_methods,
20 clippy::explicit_into_iter_loop,
21 clippy::fallible_impl_from,
22 clippy::filter_map_next,
23 clippy::float_cmp_const,
24 clippy::fn_params_excessive_bools,
25 clippy::if_let_mutex,
26 clippy::implicit_clone,
27 clippy::imprecise_flops,
28 clippy::inefficient_to_string,
29 clippy::invalid_upcast_comparisons,
30 clippy::large_types_passed_by_value,
31 clippy::let_unit_value,
32 clippy::linkedlist,
33 clippy::lossy_float_literal,
34 clippy::macro_use_imports,
35 clippy::manual_ok_or,
36 clippy::map_err_ignore,
37 clippy::map_flatten,
38 clippy::map_unwrap_or,
39 clippy::match_same_arms,
40 clippy::match_wildcard_for_single_variants,
41 clippy::mem_forget,
42 clippy::mut_mut,
43 clippy::mutex_integer,
44 clippy::needless_borrow,
45 clippy::needless_continue,
46 clippy::option_option,
47 clippy::path_buf_push_overwrite,
48 clippy::ptr_as_ptr,
49 clippy::ref_option_ref,
50 clippy::rest_pat_in_fully_bound_structs,
51 clippy::same_functions_in_if_condition,
52 clippy::semicolon_if_nothing_returned,
53 clippy::string_add_assign,
54 clippy::string_add,
55 clippy::string_lit_as_bytes,
56 clippy::string_to_string,
57 clippy::todo,
58 clippy::trait_duplication_in_bounds,
59 clippy::unimplemented,
60 clippy::unnested_or_patterns,
61 clippy::unused_self,
62 clippy::useless_transmute,
63 clippy::verbose_file_reads,
64 clippy::zero_sized_map_values,
65 future_incompatible,
66 nonstandard_style,
67 rust_2018_idioms
68)]
69// END - Embark standard lints v0.4
70// crate-specific exceptions:
71// #![allow()]
72#![doc = include_str!("../README.md")]
73
74pub mod cargo_cmd;
75mod depfile;
76#[cfg(test)]
77mod tests;
78#[cfg(feature = "watch")]
79mod watch;
80
81use raw_string::{RawStr, RawString};
82use semver::Version;
83use serde::Deserialize;
84use std::borrow::Borrow;
85use std::collections::HashMap;
86use std::env;
87use std::fs::File;
88use std::io::BufReader;
89use std::path::{Path, PathBuf};
90use std::process::Stdio;
91use thiserror::Error;
92
93#[cfg(feature = "watch")]
94pub use self::watch::{SpirvWatcher, SpirvWatcherError};
95pub use rustc_codegen_spirv_types::*;
96
97#[derive(Debug, Error)]
98#[non_exhaustive]
99pub enum SpirvBuilderError {
100 #[error("`target` must be set, for example `spirv-unknown-vulkan1.2`")]
101 MissingTarget,
102 #[error("TargetError: {0}")]
103 TargetError(#[from] TargetError),
104 #[error("`path_to_crate` must be set")]
105 MissingCratePath,
106 #[error("crate path '{0}' does not exist")]
107 CratePathDoesntExist(PathBuf),
108 #[error(
109 "Without feature `rustc_codegen_spirv`, you need to set the path of the dylib with `rustc_codegen_spirv_location`"
110 )]
111 MissingRustcCodegenSpirvDylib,
112 #[error("`rustc_codegen_spirv_location` path '{0}' is not a file")]
113 RustcCodegenSpirvDylibDoesNotExist(PathBuf),
114 #[error("build failed")]
115 BuildFailed,
116 #[error("multi-module build cannot be used with print_metadata = MetadataPrintout::Full")]
117 MultiModuleWithPrintMetadata,
118 #[error("multi-module metadata file missing")]
119 MetadataFileMissing(#[from] std::io::Error),
120 #[error("unable to parse multi-module metadata file")]
121 MetadataFileMalformed(#[from] serde_json::Error),
122 #[error(
123 "`{ARTIFACT_SUFFIX}` artifact not found in (supposedly successful) build output.\n--- build output ---\n{stdout}"
124 )]
125 NoArtifactProduced { stdout: String },
126 #[error("cargo metadata error")]
127 CargoMetadata(#[from] cargo_metadata::Error),
128 #[cfg(feature = "watch")]
129 #[error(transparent)]
130 WatchFailed(#[from] SpirvWatcherError),
131}
132
133#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, serde::Deserialize, serde::Serialize)]
134#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
135#[non_exhaustive]
136pub enum MetadataPrintout {
137 /// Print no cargo metadata.
138 #[default]
139 None,
140 /// Print only dependency information (eg for multiple modules).
141 DependencyOnly,
142 /// Print all cargo metadata.
143 ///
144 /// Includes dependency information and spirv environment variable.
145 Full,
146}
147
148#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, serde::Deserialize, serde::Serialize)]
149#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
150#[non_exhaustive]
151pub enum SpirvMetadata {
152 /// Strip all names and other debug information from SPIR-V output.
153 #[default]
154 None,
155 /// Only include `OpName`s for public interface variables (uniforms and the like), to allow
156 /// shader reflection.
157 NameVariables,
158 /// Include all `OpName`s for everything, and `OpLine`s. Significantly increases binary size.
159 Full,
160}
161
162/// Strategy used to handle Rust `panic!`s in shaders compiled to SPIR-V.
163#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, serde::Deserialize, serde::Serialize)]
164#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
165#[non_exhaustive]
166pub enum ShaderPanicStrategy {
167 /// Return from shader entry-point with no side-effects **(default)**.
168 ///
169 /// While similar to the standard SPIR-V `OpTerminateInvocation`, this is
170 /// *not* limited to fragment shaders, and instead supports all shaders
171 /// (as it's handled via control-flow rewriting, instead of SPIR-V features).
172 #[default]
173 SilentExit,
174
175 /// Like `SilentExit`, but also using `debugPrintf` to report the panic in
176 /// a way that can reach the user, before returning from the entry-point.
177 ///
178 /// Quick setup for enabling `debugPrintf` output (to stdout) at runtime:
179 /// - **set these environment variables**:
180 /// - `VK_LOADER_LAYERS_ENABLE=VK_LAYER_KHRONOS_validation`
181 /// - `VK_LAYER_PRINTF_ONLY_PRESET=1`
182 /// - `VK_LAYER_PRINTF_TO_STDOUT=1` (not always needed, but can help)
183 /// - if using `wgpu`, enable `wgpu::Features::SPIRV_SHADER_PASSTHROUGH`,
184 /// and use `create_shader_module_passthrough` instead of `create_shader_module`
185 /// - in case of errors, or no output (from a `panic!()`/`debug_printf!()`),
186 /// keep reading below for additional information and alternatives
187 ///
188 /// ---
189 ///
190 /// **Note**: enabling this automatically adds the `SPV_KHR_non_semantic_info`
191 /// extension, as `debugPrintf` is from a "non-semantic extended instruction set".
192 ///
193 /// **Note**: `debugPrintf` output reaching the user involves:
194 /// - being able to load the shader in the first place:
195 /// - for `wgpu`, use "SPIR-V shader passthrough" (Naga lacks `debugPrintf`):
196 /// - enable `wgpu::Features::SPIRV_SHADER_PASSTHROUGH`
197 /// - replace `create_shader_module` calls with `create_shader_module_passthrough`
198 /// - *in theory*, the `VK_KHR_shader_non_semantic_info` Vulkan *Device* extension
199 /// (or requiring at least Vulkan 1.3, which incorporated it)
200 /// - *however*, Validation Layers don't actually check this anymore,
201 /// since Vulkan SDK version 1.4.313.0 (and drivers shouldn't care either)
202 /// - **general configurability** of [Vulkan SDK](https://vulkan.lunarg.com/sdk/home)
203 /// and/or [Vulkan Loader](https://github.com/KhronosGroup/Vulkan-Loader)
204 /// - *(this list doubles as a legend for shorthands used later below)*
205 /// - **env**: setting environment variables on the fly
206 /// - easiest for quick testing, no code changes/rebuilding needed
207 /// - e.g. `FOO=1 cargo run ...` (in UNIX-style shells)
208 /// - **instance**: programmatic control via `vkCreateInstance()` params
209 /// - best for integration with app-specific debugging functionality
210 /// - limited to direct Vulkan usage (e.g. `ash`, not `wgpu`)
211 /// - `VK_EXT_layer_settings` as a `VK_LAYER_*` environment variables
212 /// analogue, e.g. `VK_LAYER_FOO` controlled by a `VkLayerSettingEXT`
213 /// with `"foo"` as `pSettingName` (and an appropriate `type`/value),
214 /// included in `VkLayerSettingsCreateInfoEXT`'s `pSettings`
215 /// - on-disk configuration and interactive tooling, e.g.:
216 /// - `vk_layer_settings.txt` files, either hand-written, or generated by
217 /// the "Vulkan Configurator" GUI tool (included with the Vulkan SDK)
218 /// - third-party Vulkan debuggers like `RenderDoc`
219 /// - [Vulkan Validation Layers](https://github.com/KhronosGroup/Vulkan-ValidationLayers)
220 /// - (they contain the `debugPrintf` implementation, a SPIR-V -> SPIR-V translation)
221 /// - enabled by one of (as per "**general configurability**" above):
222 /// - **env**: `VK_LOADER_LAYERS_ENABLE=VK_LAYER_KHRONOS_validation`
223 /// - **instance**: `"VK_LAYER_KHRONOS_validation"` in the list of layers
224 /// - via `wgpu`: `wgpu::InstanceFlags::VALIDATION`
225 /// - Validation Layers' `debugPrintf` support
226 /// ([official docs](https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/main/docs/debug_printf.md)):
227 /// - enabled by one of (as per "**general configurability**" above):
228 /// - **env**: `VK_LAYER_PRINTF_ENABLE=1` (validation + `debugPrintf`)
229 /// - **env**: `VK_LAYER_PRINTF_ONLY_PRESET=1` (*only* `debugPrintf`, no validation)
230 /// - **instance**: `"printf_enable"` / `"printf_only_preset"` via `VkLayerSettingEXT`
231 /// (i.e. analogues for the two environment variables)
232 /// - **instance**: `VkValidationFeaturesEXT` with `pEnabledValidationFeatures`
233 /// containing `VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT`
234 /// - outputting the `debugPrintf` messages sent back from the GPU:
235 /// - defaults to common validation logging (itself defaulting to stdout)
236 /// - **env**: `VK_LAYER_PRINTF_TO_STDOUT=1` (and its **instance** analogue)
237 /// forces direct printing to stdout, bypassing `VK_EXT_debug_utils` etc.
238 /// - validation logging can itself be controlled via `VK_EXT_debug_utils`
239 /// - `wgpu` built in debug mode (and/or with debug-assertions enabled):
240 /// - it uses `VK_EXT_debug_utils` internally, exposing it via `log`
241 /// - with e.g. `env_logger`, `RUST_LOG=info` suffices for `debugPrintf`
242 /// messages (as they specifically have the "info" level)
243 /// - other `log`/`tracing` subscribers should be configured similarly
244 #[cfg_attr(feature = "clap", clap(skip))]
245 DebugPrintfThenExit {
246 /// Whether to also print the entry-point inputs (excluding buffers/resources),
247 /// which should uniquely identify the panicking shader invocation.
248 print_inputs: bool,
249
250 /// Whether to also print a "backtrace" (i.e. the chain of function calls
251 /// that led to the `panic!`).
252 ///
253 /// As there is no way to dynamically compute this information, the string
254 /// containing the full backtrace of each `panic!` is statically generated,
255 /// meaning this option could significantly increase binary size.
256 print_backtrace: bool,
257 },
258
259 /// **Warning**: this is _**unsound**_ (i.e. adds Undefined Behavior to *safe* Rust code)
260 ///
261 /// This option only exists for testing (hence the unfriendly name it has),
262 /// and more specifically testing whether conditional panics are responsible
263 /// for performance differences when upgrading from older Rust-GPU versions
264 /// (which used infinite loops for panics, that `spirv-opt`/drivers could've
265 /// sometimes treated as UB, and optimized as if they were impossible to reach).
266 ///
267 /// Unlike those infinite loops, however, this uses `OpUnreachable`, so it
268 /// forces the old worst-case (all `panic!`s become UB and are optimized out).
269 #[allow(non_camel_case_types)]
270 UNSOUND_DO_NOT_USE_UndefinedBehaviorViaUnreachable,
271}
272
273/// Options for specifying the behavior of the validator
274/// Copied from `spirv-tools/src/val.rs` struct `ValidatorOptions`, with some fields disabled.
275#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)]
276#[cfg_attr(feature = "clap", derive(clap::Parser))]
277#[non_exhaustive]
278pub struct ValidatorOptions {
279 /// Record whether or not the validator should relax the rules on types for
280 /// stores to structs. When relaxed, it will allow a type mismatch as long as
281 /// the types are structs with the same layout. Two structs have the same layout
282 /// if
283 ///
284 /// 1) the members of the structs are either the same type or are structs with
285 /// same layout, and
286 ///
287 /// 2) the decorations that affect the memory layout are identical for both
288 /// types. Other decorations are not relevant.
289 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
290 pub relax_struct_store: bool,
291 /// Records whether or not the validator should relax the rules on pointer usage
292 /// in logical addressing mode.
293 ///
294 /// When relaxed, it will allow the following usage cases of pointers:
295 /// 1) `OpVariable` allocating an object whose type is a pointer type
296 /// 2) `OpReturnValue` returning a pointer value
297 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
298 pub relax_logical_pointer: bool,
299 // /// Records whether or not the validator should relax the rules because it is
300 // /// expected that the optimizations will make the code legal.
301 // ///
302 // /// When relaxed, it will allow the following:
303 // /// 1) It will allow relaxed logical pointers. Setting this option will also
304 // /// set that option.
305 // /// 2) Pointers that are pass as parameters to function calls do not have to
306 // /// match the storage class of the formal parameter.
307 // /// 3) Pointers that are actaul parameters on function calls do not have to point
308 // /// to the same type pointed as the formal parameter. The types just need to
309 // /// logically match.
310 // pub before_legalization: bool,
311 /// Records whether the validator should use "relaxed" block layout rules.
312 /// Relaxed layout rules are described by Vulkan extension
313 /// `VK_KHR_relaxed_block_layout`, and they affect uniform blocks, storage blocks,
314 /// and push constants.
315 ///
316 /// This is enabled by default when targeting Vulkan 1.1 or later.
317 /// Relaxed layout is more permissive than the default rules in Vulkan 1.0.
318 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
319 pub relax_block_layout: Option<bool>,
320 /// Records whether the validator should use standard block layout rules for
321 /// uniform blocks.
322 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
323 pub uniform_buffer_standard_layout: bool,
324 /// Records whether the validator should use "scalar" block layout rules.
325 /// Scalar layout rules are more permissive than relaxed block layout.
326 ///
327 /// See Vulkan extnesion `VK_EXT_scalar_block_layout`. The scalar alignment is
328 /// defined as follows:
329 /// - scalar alignment of a scalar is the scalar size
330 /// - scalar alignment of a vector is the scalar alignment of its component
331 /// - scalar alignment of a matrix is the scalar alignment of its component
332 /// - scalar alignment of an array is the scalar alignment of its element
333 /// - scalar alignment of a struct is the max scalar alignment among its
334 /// members
335 ///
336 /// For a struct in Uniform, `StorageClass`, or `PushConstant`:
337 /// - a member Offset must be a multiple of the member's scalar alignment
338 /// - `ArrayStride` or `MatrixStride` must be a multiple of the array or matrix
339 /// scalar alignment
340 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
341 pub scalar_block_layout: bool,
342 /// Records whether or not the validator should skip validating standard
343 /// uniform/storage block layout.
344 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
345 pub skip_block_layout: bool,
346 // /// Applies a maximum to one or more Universal limits
347 // pub max_limits: Vec<(ValidatorLimits, u32)>,
348}
349
350/// Options for specifying the behavior of the optimizer
351/// Copied from `spirv-tools/src/opt.rs` struct `Options`, with some fields disabled.
352#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)]
353#[cfg_attr(feature = "clap", derive(clap::Parser))]
354#[non_exhaustive]
355pub struct OptimizerOptions {
356 // /// Records the validator options that should be passed to the validator,
357 // /// the validator will run with the options before optimizer.
358 // pub validator_options: Option<crate::val::ValidatorOptions>,
359 // /// Records the maximum possible value for the id bound.
360 // pub max_id_bound: Option<u32>,
361 /// Records whether all bindings within the module should be preserved.
362 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
363 pub preserve_bindings: bool,
364 // /// Records whether all specialization constants within the module
365 // /// should be preserved.
366 // pub preserve_spec_constants: bool,
367}
368
369/// Cargo features specification for building the shader crate.
370#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
371#[cfg_attr(feature = "clap", derive(clap::Parser))]
372#[non_exhaustive]
373pub struct ShaderCrateFeatures {
374 /// Set --default-features for the target shader crate.
375 #[cfg_attr(feature = "clap", clap(long = "no-default-features", default_value = "true", action = clap::ArgAction::SetFalse))]
376 pub default_features: bool,
377 /// Set --features for the target shader crate.
378 #[cfg_attr(feature = "clap", clap(long))]
379 pub features: Vec<String>,
380}
381
382impl Default for ShaderCrateFeatures {
383 fn default() -> Self {
384 Self {
385 default_features: true,
386 features: Vec::new(),
387 }
388 }
389}
390
391#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
392#[cfg_attr(feature = "clap", derive(clap::Parser))]
393#[non_exhaustive]
394pub struct SpirvBuilder {
395 /// The path to the shader crate to compile
396 #[cfg_attr(feature = "clap", clap(skip))]
397 pub path_to_crate: Option<PathBuf>,
398 /// The cargo command to run, formatted like `cargo {cargo_cmd} ...`. Defaults to `rustc`.
399 #[cfg_attr(feature = "clap", clap(skip))]
400 pub cargo_cmd: Option<String>,
401 /// Whether the cargo command set in `cargo_cmd` behaves like `cargo rustc` and allows passing args such as
402 /// `--crate-type dylib`. Defaults to true if `cargo_cmd` is `None` or `Some("rustc")`.
403 #[cfg_attr(feature = "clap", clap(skip))]
404 pub cargo_cmd_like_rustc: Option<bool>,
405 /// Whether to print build.rs cargo metadata (e.g. cargo:rustc-env=var=val). Defaults to [`MetadataPrintout::None`].
406 /// Within build scripts, set it to [`MetadataPrintout::DependencyOnly`] or [`MetadataPrintout::Full`] to ensure the build script is rerun on code changes.
407 #[cfg_attr(feature = "clap", clap(skip))]
408 pub print_metadata: MetadataPrintout,
409 /// Build in release. Defaults to true.
410 #[cfg_attr(feature = "clap", clap(long = "debug", default_value = "true", action = clap::ArgAction::SetFalse))]
411 pub release: bool,
412 /// The target triple, eg. `spirv-unknown-vulkan1.2`
413 #[cfg_attr(
414 feature = "clap",
415 clap(long, default_value = "spirv-unknown-vulkan1.2")
416 )]
417 pub target: Option<String>,
418 /// Cargo features specification for building the shader crate.
419 #[cfg_attr(feature = "clap", clap(flatten))]
420 #[serde(flatten)]
421 pub shader_crate_features: ShaderCrateFeatures,
422 /// Deny any warnings, as they may never be printed when building within a build script. Defaults to false.
423 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
424 pub deny_warnings: bool,
425 /// Splits the resulting SPIR-V file into one module per entry point. This is useful in cases
426 /// where ecosystem tooling has bugs around multiple entry points per module - having all entry
427 /// points bundled into a single file is the preferred system.
428 #[cfg_attr(feature = "clap", arg(long, default_value = "false"))]
429 pub multimodule: bool,
430 /// Sets the level of metadata (primarily `OpName` and `OpLine`) included in the SPIR-V binary.
431 /// Including metadata significantly increases binary size.
432 #[cfg_attr(feature = "clap", arg(long, default_value = "none"))]
433 pub spirv_metadata: SpirvMetadata,
434 /// Adds a capability to the SPIR-V module. Checking if a capability is enabled in code can be
435 /// done via `#[cfg(target_feature = "TheCapability")]`.
436 #[cfg_attr(feature = "clap", arg(long, value_parser=Self::parse_spirv_capability))]
437 pub capabilities: Vec<Capability>,
438 /// Adds an extension to the SPIR-V module. Checking if an extension is enabled in code can be
439 /// done via `#[cfg(target_feature = "ext:the_extension")]`.
440 #[cfg_attr(feature = "clap", arg(long))]
441 pub extensions: Vec<String>,
442 /// Set additional "codegen arg". Note: the `RUSTGPU_CODEGEN_ARGS` environment variable
443 /// takes precedence over any set arguments using this function.
444 #[cfg_attr(feature = "clap", clap(skip))]
445 pub extra_args: Vec<String>,
446 // Location of a known `rustc_codegen_spirv` dylib, only required without feature `rustc_codegen_spirv`.
447 #[cfg_attr(feature = "clap", clap(skip))]
448 pub rustc_codegen_spirv_location: Option<PathBuf>,
449 // Overwrite the toolchain like `cargo +nightly`
450 #[cfg_attr(feature = "clap", clap(skip))]
451 pub toolchain_overwrite: Option<String>,
452 // Set the rustc version of the toolchain, used to adjust params to support older toolchains
453 #[cfg_attr(feature = "clap", clap(skip))]
454 pub toolchain_rustc_version: Option<Version>,
455
456 /// Set the target dir path to use for building shaders. Relative paths will be resolved
457 /// relative to the `target` dir of the shader crate, absolute paths are used as is.
458 /// Defaults to `spirv-builder`, resulting in the path `./target/spirv-builder`.
459 #[cfg_attr(feature = "clap", clap(skip))]
460 pub target_dir_path: Option<PathBuf>,
461
462 // `rustc_codegen_spirv::linker` codegen args
463 /// Change the shader `panic!` handling strategy (see [`ShaderPanicStrategy`]).
464 #[cfg_attr(feature = "clap", clap(skip))]
465 pub shader_panic_strategy: ShaderPanicStrategy,
466
467 /// spirv-val flags
468 #[cfg_attr(feature = "clap", clap(flatten))]
469 #[serde(flatten)]
470 pub validator: ValidatorOptions,
471
472 /// spirv-opt flags
473 #[cfg_attr(feature = "clap", clap(flatten))]
474 #[serde(flatten)]
475 pub optimizer: OptimizerOptions,
476}
477
478#[cfg(feature = "clap")]
479impl SpirvBuilder {
480 /// Clap value parser for `Capability`.
481 fn parse_spirv_capability(capability: &str) -> Result<Capability, clap::Error> {
482 use core::str::FromStr;
483 Capability::from_str(capability).map_or_else(
484 |()| Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)),
485 Ok,
486 )
487 }
488}
489
490impl Default for SpirvBuilder {
491 fn default() -> Self {
492 Self {
493 path_to_crate: None,
494 cargo_cmd: None,
495 cargo_cmd_like_rustc: None,
496 print_metadata: MetadataPrintout::default(),
497 release: true,
498 target: None,
499 deny_warnings: false,
500 multimodule: false,
501 spirv_metadata: SpirvMetadata::default(),
502 capabilities: Vec::new(),
503 extensions: Vec::new(),
504 extra_args: Vec::new(),
505 rustc_codegen_spirv_location: None,
506 target_dir_path: None,
507 toolchain_overwrite: None,
508 toolchain_rustc_version: None,
509 shader_panic_strategy: ShaderPanicStrategy::default(),
510 validator: ValidatorOptions::default(),
511 optimizer: OptimizerOptions::default(),
512 shader_crate_features: ShaderCrateFeatures::default(),
513 }
514 }
515}
516
517impl SpirvBuilder {
518 pub fn new(path_to_crate: impl AsRef<Path>, target: impl Into<String>) -> Self {
519 Self {
520 path_to_crate: Some(path_to_crate.as_ref().to_owned()),
521 target: Some(target.into()),
522 ..SpirvBuilder::default()
523 }
524 }
525
526 /// Whether to print build.rs cargo metadata (e.g. cargo:rustc-env=var=val). Defaults to [`MetadataPrintout::Full`].
527 #[must_use]
528 pub fn print_metadata(mut self, v: MetadataPrintout) -> Self {
529 self.print_metadata = v;
530 self
531 }
532
533 #[must_use]
534 pub fn deny_warnings(mut self, v: bool) -> Self {
535 self.deny_warnings = v;
536 self
537 }
538
539 /// Build in release. Defaults to true.
540 #[must_use]
541 pub fn release(mut self, v: bool) -> Self {
542 self.release = v;
543 self
544 }
545
546 /// Splits the resulting SPIR-V file into one module per entry point. This is useful in cases
547 /// where ecosystem tooling has bugs around multiple entry points per module - having all entry
548 /// points bundled into a single file is the preferred system.
549 #[must_use]
550 pub fn multimodule(mut self, v: bool) -> Self {
551 self.multimodule = v;
552 self
553 }
554
555 /// Sets the level of metadata (primarily `OpName` and `OpLine`) included in the SPIR-V binary.
556 /// Including metadata significantly increases binary size.
557 #[must_use]
558 pub fn spirv_metadata(mut self, v: SpirvMetadata) -> Self {
559 self.spirv_metadata = v;
560 self
561 }
562
563 /// Adds a capability to the SPIR-V module. Checking if a capability is enabled in code can be
564 /// done via `#[cfg(target_feature = "TheCapability")]`.
565 #[must_use]
566 pub fn capability(mut self, capability: Capability) -> Self {
567 self.capabilities.push(capability);
568 self
569 }
570
571 /// Adds an extension to the SPIR-V module. Checking if an extension is enabled in code can be
572 /// done via `#[cfg(target_feature = "ext:the_extension")]`.
573 #[must_use]
574 pub fn extension(mut self, extension: impl Into<String>) -> Self {
575 self.extensions.push(extension.into());
576 self
577 }
578
579 /// Change the shader `panic!` handling strategy (see [`ShaderPanicStrategy`]).
580 #[must_use]
581 pub fn shader_panic_strategy(mut self, shader_panic_strategy: ShaderPanicStrategy) -> Self {
582 self.shader_panic_strategy = shader_panic_strategy;
583 self
584 }
585
586 /// Allow store from one struct type to a different type with compatible layout and members.
587 #[must_use]
588 pub fn relax_struct_store(mut self, v: bool) -> Self {
589 self.validator.relax_struct_store = v;
590 self
591 }
592
593 /// Allow allocating an object of a pointer type and returning a pointer value from a function
594 /// in logical addressing mode
595 #[must_use]
596 pub fn relax_logical_pointer(mut self, v: bool) -> Self {
597 self.validator.relax_logical_pointer = v;
598 self
599 }
600
601 /// Enable `VK_KHR_relaxed_block_layout` when checking standard uniform, storage buffer, and
602 /// push constant layouts. This is the default when targeting Vulkan 1.1 or later.
603 #[must_use]
604 pub fn relax_block_layout(mut self, v: bool) -> Self {
605 self.validator.relax_block_layout = Some(v);
606 self
607 }
608
609 /// Enable `VK_KHR_uniform_buffer_standard_layout` when checking standard uniform buffer
610 /// layouts.
611 #[must_use]
612 pub fn uniform_buffer_standard_layout(mut self, v: bool) -> Self {
613 self.validator.uniform_buffer_standard_layout = v;
614 self
615 }
616
617 /// Enable `VK_EXT_scalar_block_layout` when checking standard uniform, storage buffer, and
618 /// push constant layouts. Scalar layout rules are more permissive than relaxed block layout so
619 /// in effect this will override the --relax-block-layout option.
620 #[must_use]
621 pub fn scalar_block_layout(mut self, v: bool) -> Self {
622 self.validator.scalar_block_layout = v;
623 self
624 }
625
626 /// Skip checking standard uniform/storage buffer layout. Overrides any --relax-block-layout or
627 /// --scalar-block-layout option.
628 #[must_use]
629 pub fn skip_block_layout(mut self, v: bool) -> Self {
630 self.validator.skip_block_layout = v;
631 self
632 }
633
634 /// Preserve unused descriptor bindings. Useful for reflection.
635 #[must_use]
636 pub fn preserve_bindings(mut self, v: bool) -> Self {
637 self.optimizer.preserve_bindings = v;
638 self
639 }
640
641 /// Set additional "codegen arg". Note: the `RUSTGPU_CODEGEN_ARGS` environment variable
642 /// takes precedence over any set arguments using this function.
643 #[must_use]
644 pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
645 self.extra_args.push(arg.into());
646 self
647 }
648
649 /// Set --default-features for the target shader crate.
650 #[must_use]
651 pub fn shader_crate_default_features(mut self, default_features: bool) -> Self {
652 self.shader_crate_features.default_features = default_features;
653 self
654 }
655
656 /// Set --features for the target shader crate.
657 #[must_use]
658 pub fn shader_crate_features(mut self, features: impl IntoIterator<Item = String>) -> Self {
659 self.shader_crate_features.features = features.into_iter().collect();
660 self
661 }
662
663 #[must_use]
664 pub fn rustc_codegen_spirv_location(mut self, path_to_dylib: impl AsRef<Path>) -> Self {
665 self.rustc_codegen_spirv_location = Some(path_to_dylib.as_ref().to_path_buf());
666 self
667 }
668
669 /// Set the target dir path to use for building shaders. Relative paths will be resolved
670 /// relative to the `target` dir of the shader crate, absolute paths are used as is.
671 /// Defaults to `spirv-builder`, resulting in the path `./target/spirv-builder`.
672 #[must_use]
673 pub fn target_dir_path(mut self, name: impl Into<PathBuf>) -> Self {
674 self.target_dir_path = Some(name.into());
675 self
676 }
677
678 /// Builds the module. If `print_metadata` is [`MetadataPrintout::Full`], you usually don't have to inspect the path
679 /// in the result, as the environment variable for the path to the module will already be set.
680 pub fn build(&self) -> Result<CompileResult, SpirvBuilderError> {
681 let metadata_file = invoke_rustc(self)?;
682 match self.print_metadata {
683 MetadataPrintout::Full | MetadataPrintout::DependencyOnly => {
684 leaf_deps(&metadata_file, |artifact| {
685 println!("cargo:rerun-if-changed={artifact}");
686 })
687 // Close enough
688 .map_err(SpirvBuilderError::MetadataFileMissing)?;
689 }
690 MetadataPrintout::None => (),
691 }
692 let metadata = self.parse_metadata_file(&metadata_file)?;
693
694 Ok(metadata)
695 }
696
697 pub(crate) fn parse_metadata_file(
698 &self,
699 at: &Path,
700 ) -> Result<CompileResult, SpirvBuilderError> {
701 let metadata_contents = File::open(at).map_err(SpirvBuilderError::MetadataFileMissing)?;
702 // FIXME(eddyb) move this functionality into `rustc_codegen_spirv_types`.
703 let metadata: CompileResult =
704 rustc_codegen_spirv_types::serde_json::from_reader(BufReader::new(metadata_contents))
705 .map_err(SpirvBuilderError::MetadataFileMalformed)?;
706 match &metadata.module {
707 ModuleResult::SingleModule(spirv_module) => {
708 assert!(!self.multimodule);
709 let env_var = format!(
710 "{}.spv",
711 at.file_name()
712 .unwrap()
713 .to_str()
714 .unwrap()
715 .strip_suffix(ARTIFACT_SUFFIX)
716 .unwrap()
717 );
718 if self.print_metadata == MetadataPrintout::Full {
719 println!("cargo:rustc-env={}={}", env_var, spirv_module.display());
720 }
721 }
722 ModuleResult::MultiModule(_) => {
723 assert!(self.multimodule);
724 }
725 }
726 Ok(metadata)
727 }
728}
729
730// https://github.com/rust-lang/cargo/blob/1857880b5124580c4aeb4e8bc5f1198f491d61b1/src/cargo/util/paths.rs#L29-L52
731fn dylib_path_envvar() -> &'static str {
732 if cfg!(windows) {
733 "PATH"
734 } else if cfg!(target_os = "macos") {
735 "DYLD_FALLBACK_LIBRARY_PATH"
736 } else {
737 "LD_LIBRARY_PATH"
738 }
739}
740fn dylib_path() -> Vec<PathBuf> {
741 let mut dylibs = match env::var_os(dylib_path_envvar()) {
742 Some(var) => env::split_paths(&var).collect(),
743 None => Vec::new(),
744 };
745 if let Ok(dir) = env::current_dir() {
746 dylibs.push(dir);
747 }
748 dylibs
749}
750
751fn find_rustc_codegen_spirv() -> Result<PathBuf, SpirvBuilderError> {
752 if cfg!(feature = "rustc_codegen_spirv") {
753 let filename = format!(
754 "{}rustc_codegen_spirv{}",
755 env::consts::DLL_PREFIX,
756 env::consts::DLL_SUFFIX
757 );
758 let dylib_paths = dylib_path();
759 for mut path in dylib_paths {
760 path.push(&filename);
761 if path.is_file() {
762 return Ok(path);
763 }
764 }
765 panic!("Could not find {filename} in library path");
766 } else {
767 Err(SpirvBuilderError::MissingRustcCodegenSpirvDylib)
768 }
769}
770
771/// Joins strings together while ensuring none of the strings contain the separator.
772// NOTE(eddyb) this intentionally consumes the `Vec` to limit accidental misuse.
773fn join_checking_for_separators(strings: Vec<impl Borrow<str>>, sep: &str) -> String {
774 for s in &strings {
775 let s = s.borrow();
776 assert!(!s.contains(sep), "{s:?} may not contain separator {sep:?}");
777 }
778 strings.join(sep)
779}
780
781// Returns path to the metadata json.
782fn invoke_rustc(builder: &SpirvBuilder) -> Result<PathBuf, SpirvBuilderError> {
783 let path_to_crate = builder
784 .path_to_crate
785 .as_ref()
786 .ok_or(SpirvBuilderError::MissingCratePath)?;
787 let target;
788 {
789 let target_str = builder
790 .target
791 .as_ref()
792 .ok_or(SpirvBuilderError::MissingTarget)?;
793 target = SpirvTarget::parse(target_str)?;
794
795 if (builder.print_metadata == MetadataPrintout::Full) && builder.multimodule {
796 return Err(SpirvBuilderError::MultiModuleWithPrintMetadata);
797 }
798 if !path_to_crate.is_dir() {
799 return Err(SpirvBuilderError::CratePathDoesntExist(
800 path_to_crate.clone(),
801 ));
802 }
803 }
804
805 let toolchain_rustc_version =
806 if let Some(toolchain_rustc_version) = &builder.toolchain_rustc_version {
807 toolchain_rustc_version.clone()
808 } else {
809 query_rustc_version(builder.toolchain_overwrite.as_deref())?
810 };
811
812 // Okay, this is a little bonkers: in a normal world, we'd have the user clone
813 // rustc_codegen_spirv and pass in the path to it, and then we'd invoke cargo to build it, grab
814 // the resulting .so, and pass it into -Z codegen-backend. But that's really gross: the user
815 // needs to clone rustc_codegen_spirv and tell us its path! So instead, we *directly reference
816 // rustc_codegen_spirv in spirv-builder's Cargo.toml*, which means that it will get built
817 // alongside build.rs, and cargo will helpfully add it to LD_LIBRARY_PATH for us! However,
818 // rustc expects a full path, instead of a filename looked up via LD_LIBRARY_PATH, so we need
819 // to copy cargo's understanding of library lookup and find the library and its full path.
820 let rustc_codegen_spirv = Ok(builder.rustc_codegen_spirv_location.clone())
821 .transpose()
822 .unwrap_or_else(find_rustc_codegen_spirv)?;
823 if !rustc_codegen_spirv.is_file() {
824 return Err(SpirvBuilderError::RustcCodegenSpirvDylibDoesNotExist(
825 rustc_codegen_spirv,
826 ));
827 }
828
829 let mut rustflags = vec![
830 format!("-Zcodegen-backend={}", rustc_codegen_spirv.display()),
831 // Ensure the codegen backend is emitted in `.d` files to force Cargo
832 // to rebuild crates compiled with it when it changes (this used to be
833 // the default until https://github.com/rust-lang/rust/pull/93969).
834 "-Zbinary-dep-depinfo".to_string(),
835 "-Csymbol-mangling-version=v0".to_string(),
836 "-Zcrate-attr=feature(register_tool)".to_string(),
837 "-Zcrate-attr=register_tool(rust_gpu)".to_string(),
838 // HACK(eddyb) this is the same configuration that we test with, and
839 // ensures no unwanted surprises from e.g. `core` debug assertions.
840 "-Coverflow-checks=off".to_string(),
841 "-Cdebug-assertions=off".to_string(),
842 // HACK(eddyb) we need this for `core::fmt::rt::Argument::new_*` calls
843 // to *never* be inlined, so we can pattern-match the calls themselves.
844 "-Zinline-mir=off".to_string(),
845 // HACK(eddyb) similar to turning MIR inlining off, we also can't allow
846 // optimizations that drastically impact (the quality of) codegen, and
847 // GVN currently can lead to the memcpy-out-of-const-alloc-global-var
848 // pattern, even for `ScalarPair` (e.g. `return None::<u32>;`).
849 "-Zmir-enable-passes=-GVN".to_string(),
850 // HACK(eddyb) avoid ever reusing instantiations from `compiler_builtins`
851 // which is special-cased to turn calls to functions that never return,
852 // into aborts, and this applies to the panics of UB-checking helpers
853 // (https://github.com/rust-lang/rust/pull/122580#issuecomment-3033026194)
854 // but while upstream that only loses the panic message, for us it's even
855 // worse, as we lose the chance to remove otherwise-dead `fmt::Arguments`.
856 "-Zshare-generics=off".to_string(),
857 ];
858
859 // Wrapper for `env::var` that appropriately informs Cargo of the dependency.
860 let tracked_env_var_get = |name| {
861 if let MetadataPrintout::Full | MetadataPrintout::DependencyOnly = builder.print_metadata {
862 println!("cargo:rerun-if-env-changed={name}");
863 }
864 env::var(name)
865 };
866
867 let mut llvm_args = vec![];
868 if builder.multimodule {
869 llvm_args.push("--module-output=multiple".to_string());
870 }
871 match builder.spirv_metadata {
872 SpirvMetadata::None => (),
873 SpirvMetadata::NameVariables => {
874 llvm_args.push("--spirv-metadata=name-variables".to_string());
875 }
876 SpirvMetadata::Full => llvm_args.push("--spirv-metadata=full".to_string()),
877 }
878 if builder.validator.relax_struct_store {
879 llvm_args.push("--relax-struct-store".to_string());
880 }
881 if builder.validator.relax_logical_pointer {
882 llvm_args.push("--relax-logical-pointer".to_string());
883 }
884 if builder.validator.relax_block_layout.unwrap_or(false) {
885 llvm_args.push("--relax-block-layout".to_string());
886 }
887 if builder.validator.uniform_buffer_standard_layout {
888 llvm_args.push("--uniform-buffer-standard-layout".to_string());
889 }
890 if builder.validator.scalar_block_layout {
891 llvm_args.push("--scalar-block-layout".to_string());
892 }
893 if builder.validator.skip_block_layout {
894 llvm_args.push("--skip-block-layout".to_string());
895 }
896 if builder.optimizer.preserve_bindings {
897 llvm_args.push("--preserve-bindings".to_string());
898 }
899 let mut target_features = vec![];
900 let abort_strategy = match builder.shader_panic_strategy {
901 ShaderPanicStrategy::SilentExit => None,
902 ShaderPanicStrategy::DebugPrintfThenExit {
903 print_inputs,
904 print_backtrace,
905 } => {
906 target_features.push("+ext:SPV_KHR_non_semantic_info".into());
907 Some(format!(
908 "debug-printf{}{}",
909 if print_inputs { "+inputs" } else { "" },
910 if print_backtrace { "+backtrace" } else { "" }
911 ))
912 }
913 ShaderPanicStrategy::UNSOUND_DO_NOT_USE_UndefinedBehaviorViaUnreachable => {
914 Some("unreachable".into())
915 }
916 };
917 llvm_args.extend(abort_strategy.map(|strategy| format!("--abort-strategy={strategy}")));
918
919 if let Ok(extra_codegen_args) = tracked_env_var_get("RUSTGPU_CODEGEN_ARGS") {
920 llvm_args.extend(extra_codegen_args.split_whitespace().map(|s| s.to_string()));
921 } else {
922 llvm_args.extend(builder.extra_args.iter().cloned());
923 }
924
925 let llvm_args = join_checking_for_separators(llvm_args, " ");
926 if !llvm_args.is_empty() {
927 rustflags.push(["-Cllvm-args=", &llvm_args].concat());
928 }
929
930 target_features.extend(builder.capabilities.iter().map(|cap| format!("+{cap:?}")));
931 target_features.extend(builder.extensions.iter().map(|ext| format!("+ext:{ext}")));
932 let target_features = join_checking_for_separators(target_features, ",");
933 if !target_features.is_empty() {
934 rustflags.push(["-Ctarget-feature=", &target_features].concat());
935 }
936
937 if builder.deny_warnings {
938 rustflags.push("-Dwarnings".to_string());
939 }
940
941 if let Ok(extra_rustflags) = tracked_env_var_get("RUSTGPU_RUSTFLAGS") {
942 rustflags.extend(extra_rustflags.split_whitespace().map(|s| s.to_string()));
943 }
944
945 let target_dir_path = builder
946 .target_dir_path
947 .clone()
948 .unwrap_or_else(|| PathBuf::from("spirv-builder"));
949 let target_dir = if target_dir_path.is_absolute() {
950 target_dir_path
951 } else {
952 let metadata = cargo_metadata::MetadataCommand::new()
953 .current_dir(path_to_crate)
954 .exec()?;
955 metadata
956 .target_directory
957 .into_std_path_buf()
958 .join(target_dir_path)
959 };
960
961 let mut cargo = cargo_cmd::CargoCmd::new();
962 if let Some(toolchain) = &builder.toolchain_overwrite {
963 cargo.arg(format!("+{toolchain}"));
964 }
965
966 let cargo_cmd = builder.cargo_cmd.as_ref().map_or("rustc", |s| s.as_str());
967 let cargo_cmd_like_rustc = builder.cargo_cmd_like_rustc.unwrap_or(cargo_cmd == "rustc");
968 let profile = if builder.release { "release" } else { "dev" };
969 cargo.args([
970 cargo_cmd,
971 "--lib",
972 "--message-format=json-render-diagnostics",
973 "-Zbuild-std=core",
974 "-Zbuild-std-features=compiler-builtins-mem",
975 "--profile",
976 profile,
977 ]);
978 if cargo_cmd_like_rustc {
979 // About `crate-type`: We use it to determine whether the crate needs to be linked into shaders. For `rlib`,
980 // we're emitting regular rust libraries as is expected. For `dylib` or `cdylib`, we're linking all `rlib`s
981 // together, legalize them in many passes and emit a final `*.spv` file. Quirk: If you depend on a crate
982 // that has crate-type `dylib`, we also link it, and it will fail if it has no shaders, which may not be
983 // desired. (Gathered from reading source code and experimenting, @firestar99)
984 cargo.args(["--crate-type", "dylib"]);
985 }
986
987 if let Ok(extra_cargoflags) = tracked_env_var_get("RUSTGPU_CARGOFLAGS") {
988 cargo.args(extra_cargoflags.split_whitespace());
989 }
990
991 let target_spec_dir = target_dir.join("target-specs");
992 let target = TargetSpecVersion::target_arg(toolchain_rustc_version, &target, &target_spec_dir)?;
993 cargo.arg("--target").arg(target);
994
995 if !builder.shader_crate_features.default_features {
996 cargo.arg("--no-default-features");
997 }
998
999 if !builder.shader_crate_features.features.is_empty() {
1000 cargo
1001 .arg("--features")
1002 .arg(builder.shader_crate_features.features.join(","));
1003 }
1004
1005 cargo.arg("--target-dir").arg(target_dir);
1006
1007 // NOTE(eddyb) this used to be just `RUSTFLAGS` but at some point Cargo
1008 // added a separate environment variable using `\x1f` instead of spaces,
1009 // which allows us to have spaces within individual `rustc` flags.
1010 cargo.env(
1011 "CARGO_ENCODED_RUSTFLAGS",
1012 join_checking_for_separators(rustflags, "\x1f"),
1013 );
1014
1015 // NOTE(eddyb) there's no parallelism to take advantage of multiple CGUs,
1016 // and inter-CGU duplication can be wasteful, so this forces 1 CGU for now.
1017 let profile_in_env_var = profile.replace('-', "_").to_ascii_uppercase();
1018 let num_cgus = 1;
1019 cargo.env(
1020 format!("CARGO_PROFILE_{profile_in_env_var}_CODEGEN_UNITS"),
1021 num_cgus.to_string(),
1022 );
1023
1024 cargo.stderr(Stdio::inherit()).current_dir(path_to_crate);
1025 log::debug!("building shaders with `{cargo:?}`");
1026 let build = cargo.output().expect("failed to execute cargo build");
1027
1028 // `get_last_artifact` has the side-effect of printing invalid lines, so
1029 // we do that even in case of an error, to let through any useful messages
1030 // that ended up on stdout instead of stderr.
1031 let stdout = String::from_utf8(build.stdout).unwrap();
1032 if build.status.success() {
1033 get_sole_artifact(&stdout).ok_or(SpirvBuilderError::NoArtifactProduced { stdout })
1034 } else {
1035 Err(SpirvBuilderError::BuildFailed)
1036 }
1037}
1038
1039#[derive(Deserialize)]
1040struct RustcOutput {
1041 reason: String,
1042 filenames: Option<Vec<String>>,
1043}
1044
1045const ARTIFACT_SUFFIX: &str = ".spv.json";
1046
1047fn get_sole_artifact(out: &str) -> Option<PathBuf> {
1048 let mut last_compiler_artifact = None;
1049 for line in out.lines() {
1050 let Ok(msg) = serde_json::from_str::<RustcOutput>(line) else {
1051 // Pass through invalid lines
1052 println!("{line}");
1053 continue;
1054 };
1055 if msg.reason == "compiler-artifact" {
1056 last_compiler_artifact = Some(msg);
1057 }
1058 }
1059 let last_compiler_artifact =
1060 last_compiler_artifact.expect("Did not find output file in rustc output");
1061
1062 let mut filenames = last_compiler_artifact
1063 .filenames
1064 .unwrap()
1065 .into_iter()
1066 .filter(|v| v.ends_with(ARTIFACT_SUFFIX));
1067 let filename = filenames.next()?;
1068 assert_eq!(
1069 filenames.next(),
1070 None,
1071 "build had multiple `{ARTIFACT_SUFFIX}` artifacts"
1072 );
1073 Some(filename.into())
1074}
1075
1076/// Internally iterate through the leaf dependencies of the artifact at `artifact`
1077fn leaf_deps(artifact: &Path, mut handle: impl FnMut(&RawStr)) -> std::io::Result<()> {
1078 let deps_file = artifact.with_extension("d");
1079 let mut deps_map = HashMap::new();
1080 depfile::read_deps_file(&deps_file, |item, deps| {
1081 deps_map.insert(item, deps);
1082 Ok(())
1083 })?;
1084 fn recurse(
1085 map: &HashMap<RawString, Vec<RawString>>,
1086 artifact: &RawStr,
1087 handle: &mut impl FnMut(&RawStr),
1088 ) {
1089 match map.get(artifact) {
1090 Some(entries) => {
1091 for entry in entries {
1092 recurse(map, entry, handle);
1093 }
1094 }
1095 None => handle(artifact),
1096 }
1097 }
1098 recurse(&deps_map, artifact.to_str().unwrap().into(), &mut handle);
1099 Ok(())
1100}