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