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