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}