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