Skip to main content

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