spirv_builder/
lib.rs

1// BEGIN - Embark standard lints v0.4
2// do not change or add/remove here, but one can add exceptions after this section
3// for more info see: <https://github.com/EmbarkStudios/rust-ecosystem/issues/59>
4#![deny(unsafe_code)]
5#![warn(
6    clippy::all,
7    clippy::await_holding_lock,
8    clippy::char_lit_as_u8,
9    clippy::checked_conversions,
10    clippy::dbg_macro,
11    clippy::debug_assert_with_mut_call,
12    clippy::doc_markdown,
13    clippy::empty_enum,
14    clippy::enum_glob_use,
15    clippy::exit,
16    clippy::expl_impl_clone_on_copy,
17    clippy::explicit_deref_methods,
18    clippy::explicit_into_iter_loop,
19    clippy::fallible_impl_from,
20    clippy::filter_map_next,
21    clippy::float_cmp_const,
22    clippy::fn_params_excessive_bools,
23    clippy::if_let_mutex,
24    clippy::implicit_clone,
25    clippy::imprecise_flops,
26    clippy::inefficient_to_string,
27    clippy::invalid_upcast_comparisons,
28    clippy::large_types_passed_by_value,
29    clippy::let_unit_value,
30    clippy::linkedlist,
31    clippy::lossy_float_literal,
32    clippy::macro_use_imports,
33    clippy::manual_ok_or,
34    clippy::map_err_ignore,
35    clippy::map_flatten,
36    clippy::map_unwrap_or,
37    clippy::match_on_vec_items,
38    clippy::match_same_arms,
39    clippy::match_wildcard_for_single_variants,
40    clippy::mem_forget,
41    clippy::mismatched_target_os,
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
74// HACK(eddyb) try to catch misuse of Cargo package features very early on
75// (see `spirv-builder/Cargo.toml` for why we go through all of this).
76#[cfg(all(
77    not(any(feature = "use-compiled-tools", feature = "use-installed-tools")),
78    not(doc)
79))]
80compile_error!(
81    "at least one of `use-compiled-tools` or `use-installed-tools` features must be enabled
82(outside of documentation builds, which require disabling both to build on stable)"
83);
84
85#[cfg(doc)]
86fn _ensure_cfg_doc_means_rustdoc() {
87    // HACK(eddyb) this relies on specific `rustdoc` behavior (i.e. it skips
88    // type-checking function bodies, so we trigger a compile-time `panic! from
89    // a type) to check that we're in fact under `rustdoc`, not just `--cfg doc`.
90    #[rustfmt::skip]
91    let _: [(); panic!("
92
93            `--cfg doc` was set outside of `rustdoc`
94            (if you are running `rustdoc` or `cargo doc`, please file an issue)
95
96")];
97}
98
99mod depfile;
100#[cfg(feature = "watch")]
101mod watch;
102
103use raw_string::{RawStr, RawString};
104use serde::Deserialize;
105use std::borrow::Borrow;
106use std::collections::HashMap;
107use std::env;
108use std::error::Error;
109use std::fmt;
110use std::fs::File;
111use std::io::BufReader;
112use std::path::{Path, PathBuf};
113use std::process::{Command, Stdio};
114
115pub use rustc_codegen_spirv_types::Capability;
116pub use rustc_codegen_spirv_types::{CompileResult, ModuleResult};
117
118#[derive(Debug)]
119#[non_exhaustive]
120pub enum SpirvBuilderError {
121    CratePathDoesntExist(PathBuf),
122    BuildFailed,
123    MultiModuleWithPrintMetadata,
124    WatchWithPrintMetadata,
125    MetadataFileMissing(std::io::Error),
126    MetadataFileMalformed(serde_json::Error),
127}
128
129impl fmt::Display for SpirvBuilderError {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            SpirvBuilderError::CratePathDoesntExist(path) => {
133                write!(f, "Crate path {} does not exist", path.display())
134            }
135            SpirvBuilderError::BuildFailed => f.write_str("Build failed"),
136            SpirvBuilderError::MultiModuleWithPrintMetadata => f.write_str(
137                "Multi-module build cannot be used with print_metadata = MetadataPrintout::Full",
138            ),
139            SpirvBuilderError::WatchWithPrintMetadata => {
140                f.write_str("Watching within build scripts will prevent build completion")
141            }
142            SpirvBuilderError::MetadataFileMissing(_) => {
143                f.write_str("Multi-module metadata file missing")
144            }
145            SpirvBuilderError::MetadataFileMalformed(_) => {
146                f.write_str("Unable to parse multi-module metadata file")
147            }
148        }
149    }
150}
151
152impl Error for SpirvBuilderError {}
153
154#[derive(Debug, PartialEq, Eq, Clone, Copy)]
155pub enum MetadataPrintout {
156    /// Print no cargo metadata.
157    None,
158    /// Print only dependency information (eg for multiple modules).
159    DependencyOnly,
160    /// Print all cargo metadata.
161    ///
162    /// Includes dependency information and spirv environment variable.
163    Full,
164}
165
166#[derive(Debug, PartialEq, Eq, Clone, Copy)]
167pub enum SpirvMetadata {
168    /// Strip all names and other debug information from SPIR-V output.
169    None,
170    /// Only include OpNames for public interface variables (uniforms and the like), to allow
171    /// shader reflection.
172    NameVariables,
173    /// Include all OpNames for everything, and OpLines. Significantly increases binary size.
174    Full,
175}
176
177/// Strategy used to handle Rust `panic!`s in shaders compiled to SPIR-V.
178#[derive(Debug, PartialEq, Eq, Clone, Copy)]
179pub enum ShaderPanicStrategy {
180    /// Return from shader entry-point with no side-effects **(default)**.
181    ///
182    /// While similar to the standard SPIR-V `OpTerminateInvocation`, this is
183    /// *not* limited to fragment shaders, and instead supports all shaders
184    /// (as it's handled via control-flow rewriting, instead of SPIR-V features).
185    SilentExit,
186
187    /// Like `SilentExit`, but also using `debugPrintf` to report the panic in
188    /// a way that can reach the user, before returning from the entry-point.
189    ///
190    /// Will automatically require the `SPV_KHR_non_semantic_info` extension,
191    /// as `debugPrintf` uses a "non-semantic extended instruction set".
192    ///
193    /// If you have multiple entry-points, you *may* need to also enable the
194    /// `multimodule` node (see <https://github.com/KhronosGroup/SPIRV-Tools/issues/4892>).
195    ///
196    /// **Note**: actually obtaining the `debugPrintf` output requires:
197    /// * Vulkan Validation Layers (from e.g. the Vulkan SDK)
198    ///   * (they contain the `debugPrintf` implementation, a SPIR-V -> SPIR-V translation)
199    ///   * **set the `VK_LOADER_LAYERS_ENABLE=VK_LAYER_KHRONOS_validation`
200    ///     environment variable** to easily enable them without any code changes
201    ///   * alternatively, `"VK_LAYER_KHRONOS_validation"` can be passed during
202    ///     instance creation, to enable them programmatically
203    /// * Validation Layers' `debugPrintf` support:
204    ///   * **set the `VK_LAYER_ENABLES=VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT`
205    ///     environment variable** to easily enable the `debugPrintf` support
206    ///   * alternatively, `VkValidationFeaturesEXT` during instance creation,
207    ///     or the `khronos_validation.enables` field in `vk_layer_settings.txt`,
208    ///     can be used to enable `VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT`
209    ///     (see also <https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/main/docs/debug_printf.md>)
210    /// * for outputting the `debugPrintf` messages sent back from the GPU:
211    ///   * **set the `DEBUG_PRINTF_TO_STDOUT=1` environment variable** if you don't
212    ///     plan on customizing the reporting (see below for alternatives)
213    /// * for `wgpu`:
214    ///   * **required**: `wgpu::Features::SPIRV_SHADER_PASSTHROUGH` (Naga lacks `debugPrintf`)
215    ///   * *optional*: building in debug mode (and/or with debug-assertions enabled),
216    ///     to enable `wgpu` logging/debug support
217    ///     * (the debug assertions requirement may be lifted in future `wgpu` versions)
218    ///     * this uses `VK_EXT_debug_utils` internally, and is a better-integrated
219    ///       alternative to just setting `DEBUG_PRINTF_TO_STDOUT=1`
220    ///     * `RUST_LOG=wgpu_hal::vulkan=info` (or equivalent) will enable said
221    ///       output (as `debugPrintf` messages have the "info" level)
222    ///     * `RUST_LOG` controls `env_logger`, which isn't itself required,
223    ///       but *some* `log`/`tracing` subscriber is needed to get any output
224    /// * for Vulkan (e.g. via `ash`):
225    ///   * **required**: enabling the `VK_KHR_shader_non_semantic_info` Vulkan *Device* extension
226    ///   * *optional*: as described above, enabling the Validation Layers and
227    ///     their `debugPrintf` support can be done during instance creation
228    ///   * *optional*: integrating [`VK_EXT_debug_utils`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_debug_utils.html)
229    ///     allows more reporting flexibility than `DEBUG_PRINTF_TO_STDOUT=1`)
230    DebugPrintfThenExit {
231        /// Whether to also print the entry-point inputs (excluding buffers/resources),
232        /// which should uniquely identify the panicking shader invocation.
233        print_inputs: bool,
234
235        /// Whether to also print a "backtrace" (i.e. the chain of function calls
236        /// that led to the `panic!).
237        ///
238        /// As there is no way to dynamically compute this information, the string
239        /// containing the full backtrace of each `panic!` is statically generated,
240        /// meaning this option could significantly increase binary size.
241        print_backtrace: bool,
242    },
243
244    /// **Warning**: this is _**unsound**_ (i.e. adds Undefined Behavior to *safe* Rust code)
245    ///
246    /// This option only exists for testing (hence the unfriendly name it has),
247    /// and more specifically testing whether conditional panics are responsible
248    /// for performance differences when upgrading from older Rust-GPU versions
249    /// (which used infinite loops for panics, that `spirv-opt`/drivers could've
250    /// sometimes treated as UB, and optimized as if they were impossible to reach).
251    ///
252    /// Unlike those infinite loops, however, this uses `OpUnreachable`, so it
253    /// forces the old worst-case (all `panic!`s become UB and are optimized out).
254    #[allow(non_camel_case_types)]
255    UNSOUND_DO_NOT_USE_UndefinedBehaviorViaUnreachable,
256}
257
258pub struct SpirvBuilder {
259    path_to_crate: PathBuf,
260    print_metadata: MetadataPrintout,
261    release: bool,
262    target: String,
263    deny_warnings: bool,
264    multimodule: bool,
265    spirv_metadata: SpirvMetadata,
266    capabilities: Vec<Capability>,
267    extensions: Vec<String>,
268    extra_args: Vec<String>,
269
270    // `rustc_codegen_spirv::linker` codegen args
271    pub shader_panic_strategy: ShaderPanicStrategy,
272
273    // spirv-val flags
274    pub relax_struct_store: bool,
275    pub relax_logical_pointer: bool,
276    pub relax_block_layout: bool,
277    pub uniform_buffer_standard_layout: bool,
278    pub scalar_block_layout: bool,
279    pub skip_block_layout: bool,
280
281    // spirv-opt flags
282    pub preserve_bindings: bool,
283}
284
285impl SpirvBuilder {
286    pub fn new(path_to_crate: impl AsRef<Path>, target: impl Into<String>) -> Self {
287        Self {
288            path_to_crate: path_to_crate.as_ref().to_owned(),
289            print_metadata: MetadataPrintout::Full,
290            release: true,
291            target: target.into(),
292            deny_warnings: false,
293            multimodule: false,
294            spirv_metadata: SpirvMetadata::None,
295            capabilities: Vec::new(),
296            extensions: Vec::new(),
297            extra_args: Vec::new(),
298
299            shader_panic_strategy: ShaderPanicStrategy::SilentExit,
300
301            relax_struct_store: false,
302            relax_logical_pointer: false,
303            relax_block_layout: false,
304            uniform_buffer_standard_layout: false,
305            scalar_block_layout: false,
306            skip_block_layout: false,
307
308            preserve_bindings: false,
309        }
310    }
311
312    /// Whether to print build.rs cargo metadata (e.g. cargo:rustc-env=var=val). Defaults to [`MetadataPrintout::Full`].
313    #[must_use]
314    pub fn print_metadata(mut self, v: MetadataPrintout) -> Self {
315        self.print_metadata = v;
316        self
317    }
318
319    #[must_use]
320    pub fn deny_warnings(mut self, v: bool) -> Self {
321        self.deny_warnings = v;
322        self
323    }
324
325    /// Build in release. Defaults to true.
326    #[must_use]
327    pub fn release(mut self, v: bool) -> Self {
328        self.release = v;
329        self
330    }
331
332    /// Splits the resulting SPIR-V file into one module per entry point. This is useful in cases
333    /// where ecosystem tooling has bugs around multiple entry points per module - having all entry
334    /// points bundled into a single file is the preferred system.
335    #[must_use]
336    pub fn multimodule(mut self, v: bool) -> Self {
337        self.multimodule = v;
338        self
339    }
340
341    /// Sets the level of metadata (primarily `OpName` and `OpLine`) included in the SPIR-V binary.
342    /// Including metadata significantly increases binary size.
343    #[must_use]
344    pub fn spirv_metadata(mut self, v: SpirvMetadata) -> Self {
345        self.spirv_metadata = v;
346        self
347    }
348
349    /// Adds a capability to the SPIR-V module. Checking if a capability is enabled in code can be
350    /// done via `#[cfg(target_feature = "TheCapability")]`.
351    #[must_use]
352    pub fn capability(mut self, capability: Capability) -> Self {
353        self.capabilities.push(capability);
354        self
355    }
356
357    /// Adds an extension to the SPIR-V module. Checking if an extension is enabled in code can be
358    /// done via `#[cfg(target_feature = "ext:the_extension")]`.
359    #[must_use]
360    pub fn extension(mut self, extension: impl Into<String>) -> Self {
361        self.extensions.push(extension.into());
362        self
363    }
364
365    /// Change the shader `panic!` handling strategy (see [`ShaderPanicStrategy`]).
366    #[must_use]
367    pub fn shader_panic_strategy(mut self, shader_panic_strategy: ShaderPanicStrategy) -> Self {
368        self.shader_panic_strategy = shader_panic_strategy;
369        self
370    }
371
372    /// Allow store from one struct type to a different type with compatible layout and members.
373    #[must_use]
374    pub fn relax_struct_store(mut self, v: bool) -> Self {
375        self.relax_struct_store = v;
376        self
377    }
378
379    /// Allow allocating an object of a pointer type and returning a pointer value from a function
380    /// in logical addressing mode
381    #[must_use]
382    pub fn relax_logical_pointer(mut self, v: bool) -> Self {
383        self.relax_logical_pointer = v;
384        self
385    }
386
387    /// Enable `VK_KHR_relaxed_block_layout` when checking standard uniform, storage buffer, and
388    /// push constant layouts. This is the default when targeting Vulkan 1.1 or later.
389    #[must_use]
390    pub fn relax_block_layout(mut self, v: bool) -> Self {
391        self.relax_block_layout = v;
392        self
393    }
394
395    /// Enable `VK_KHR_uniform_buffer_standard_layout` when checking standard uniform buffer
396    /// layouts.
397    #[must_use]
398    pub fn uniform_buffer_standard_layout(mut self, v: bool) -> Self {
399        self.uniform_buffer_standard_layout = v;
400        self
401    }
402
403    /// Enable `VK_EXT_scalar_block_layout` when checking standard uniform, storage buffer, and
404    /// push constant layouts. Scalar layout rules are more permissive than relaxed block layout so
405    /// in effect this will override the --relax-block-layout option.
406    #[must_use]
407    pub fn scalar_block_layout(mut self, v: bool) -> Self {
408        self.scalar_block_layout = v;
409        self
410    }
411
412    /// Skip checking standard uniform/storage buffer layout. Overrides any --relax-block-layout or
413    /// --scalar-block-layout option.
414    #[must_use]
415    pub fn skip_block_layout(mut self, v: bool) -> Self {
416        self.skip_block_layout = v;
417        self
418    }
419
420    /// Preserve unused descriptor bindings. Useful for reflection.
421    #[must_use]
422    pub fn preserve_bindings(mut self, v: bool) -> Self {
423        self.preserve_bindings = v;
424        self
425    }
426
427    /// Set additional "codegen arg". Note: the `RUSTGPU_CODEGEN_ARGS` environment variable
428    /// takes precedence over any set arguments using this function.
429    #[must_use]
430    pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
431        self.extra_args.push(arg.into());
432        self
433    }
434
435    /// Builds the module. If `print_metadata` is [`MetadataPrintout::Full`], you usually don't have to inspect the path
436    /// in the result, as the environment variable for the path to the module will already be set.
437    pub fn build(mut self) -> Result<CompileResult, SpirvBuilderError> {
438        self.validate_running_conditions()?;
439        let metadata_file = invoke_rustc(&self)?;
440        match self.print_metadata {
441            MetadataPrintout::Full | MetadataPrintout::DependencyOnly => {
442                leaf_deps(&metadata_file, |artifact| {
443                    println!("cargo:rerun-if-changed={artifact}");
444                })
445                // Close enough
446                .map_err(SpirvBuilderError::MetadataFileMissing)?;
447            }
448            MetadataPrintout::None => (),
449        }
450        let metadata = self.parse_metadata_file(&metadata_file)?;
451
452        Ok(metadata)
453    }
454
455    pub(crate) fn validate_running_conditions(&mut self) -> Result<(), SpirvBuilderError> {
456        if (self.print_metadata == MetadataPrintout::Full) && self.multimodule {
457            return Err(SpirvBuilderError::MultiModuleWithPrintMetadata);
458        }
459        if !self.path_to_crate.is_dir() {
460            return Err(SpirvBuilderError::CratePathDoesntExist(std::mem::take(
461                &mut self.path_to_crate,
462            )));
463        }
464        Ok(())
465    }
466
467    pub(crate) fn parse_metadata_file(
468        &self,
469        at: &Path,
470    ) -> Result<CompileResult, SpirvBuilderError> {
471        let metadata_contents = File::open(at).map_err(SpirvBuilderError::MetadataFileMissing)?;
472        let metadata: CompileResult = serde_json::from_reader(BufReader::new(metadata_contents))
473            .map_err(SpirvBuilderError::MetadataFileMalformed)?;
474        match &metadata.module {
475            ModuleResult::SingleModule(spirv_module) => {
476                assert!(!self.multimodule);
477                let env_var = format!(
478                    "{}.spv",
479                    at.file_name()
480                        .unwrap()
481                        .to_str()
482                        .unwrap()
483                        .strip_suffix(".spv.json")
484                        .unwrap()
485                );
486                if self.print_metadata == MetadataPrintout::Full {
487                    println!("cargo:rustc-env={}={}", env_var, spirv_module.display());
488                }
489            }
490            ModuleResult::MultiModule(_) => {
491                assert!(self.multimodule);
492            }
493        }
494        Ok(metadata)
495    }
496}
497
498// https://github.com/rust-lang/cargo/blob/1857880b5124580c4aeb4e8bc5f1198f491d61b1/src/cargo/util/paths.rs#L29-L52
499fn dylib_path_envvar() -> &'static str {
500    if cfg!(windows) {
501        "PATH"
502    } else if cfg!(target_os = "macos") {
503        "DYLD_FALLBACK_LIBRARY_PATH"
504    } else {
505        "LD_LIBRARY_PATH"
506    }
507}
508fn dylib_path() -> Vec<PathBuf> {
509    match env::var_os(dylib_path_envvar()) {
510        Some(var) => env::split_paths(&var).collect(),
511        None => Vec::new(),
512    }
513}
514
515fn find_rustc_codegen_spirv() -> PathBuf {
516    let filename = format!(
517        "{}rustc_codegen_spirv{}",
518        env::consts::DLL_PREFIX,
519        env::consts::DLL_SUFFIX
520    );
521    for mut path in dylib_path() {
522        path.push(&filename);
523        if path.is_file() {
524            return path;
525        }
526    }
527    panic!("Could not find {filename} in library path");
528}
529
530/// Joins strings together while ensuring none of the strings contain the separator.
531// NOTE(eddyb) this intentionally consumes the `Vec` to limit accidental misuse.
532fn join_checking_for_separators(strings: Vec<impl Borrow<str>>, sep: &str) -> String {
533    for s in &strings {
534        let s = s.borrow();
535        assert!(!s.contains(sep), "{s:?} may not contain separator {sep:?}");
536    }
537    strings.join(sep)
538}
539
540// Returns path to the metadata json.
541fn invoke_rustc(builder: &SpirvBuilder) -> Result<PathBuf, SpirvBuilderError> {
542    // Okay, this is a little bonkers: in a normal world, we'd have the user clone
543    // rustc_codegen_spirv and pass in the path to it, and then we'd invoke cargo to build it, grab
544    // the resulting .so, and pass it into -Z codegen-backend. But that's really gross: the user
545    // needs to clone rustc_codegen_spirv and tell us its path! So instead, we *directly reference
546    // rustc_codegen_spirv in spirv-builder's Cargo.toml*, which means that it will get built
547    // alongside build.rs, and cargo will helpfully add it to LD_LIBRARY_PATH for us! However,
548    // rustc expects a full path, instead of a filename looked up via LD_LIBRARY_PATH, so we need
549    // to copy cargo's understanding of library lookup and find the library and its full path.
550    let rustc_codegen_spirv = find_rustc_codegen_spirv();
551
552    let mut rustflags = vec![
553        format!("-Zcodegen-backend={}", rustc_codegen_spirv.display()),
554        // Ensure the codegen backend is emitted in `.d` files to force Cargo
555        // to rebuild crates compiled with it when it changes (this used to be
556        // the default until https://github.com/rust-lang/rust/pull/93969).
557        "-Zbinary-dep-depinfo".to_string(),
558        "-Csymbol-mangling-version=v0".to_string(),
559        "-Zcrate-attr=feature(register_tool)".to_string(),
560        "-Zcrate-attr=register_tool(rust_gpu)".to_string(),
561        // HACK(eddyb) this is the same configuration that we test with, and
562        // ensures no unwanted surprises from e.g. `core` debug assertions.
563        "-Coverflow-checks=off".to_string(),
564        "-Cdebug-assertions=off".to_string(),
565        // HACK(eddyb) we need this for `core::fmt::rt::Argument::new_*` calls
566        // to *never* be inlined, so we can pattern-match the calls themselves.
567        "-Zinline-mir=off".to_string(),
568    ];
569
570    // Wrapper for `env::var` that appropriately informs Cargo of the dependency.
571    let tracked_env_var_get = |name| {
572        if let MetadataPrintout::Full | MetadataPrintout::DependencyOnly = builder.print_metadata {
573            println!("cargo:rerun-if-env-changed={name}");
574        }
575        env::var(name)
576    };
577
578    let mut llvm_args = vec![];
579    if builder.multimodule {
580        llvm_args.push("--module-output=multiple".to_string());
581    }
582    match builder.spirv_metadata {
583        SpirvMetadata::None => (),
584        SpirvMetadata::NameVariables => {
585            llvm_args.push("--spirv-metadata=name-variables".to_string());
586        }
587        SpirvMetadata::Full => llvm_args.push("--spirv-metadata=full".to_string()),
588    }
589    if builder.relax_struct_store {
590        llvm_args.push("--relax-struct-store".to_string());
591    }
592    if builder.relax_logical_pointer {
593        llvm_args.push("--relax-logical-pointer".to_string());
594    }
595    if builder.relax_block_layout {
596        llvm_args.push("--relax-block-layout".to_string());
597    }
598    if builder.uniform_buffer_standard_layout {
599        llvm_args.push("--uniform-buffer-standard-layout".to_string());
600    }
601    if builder.scalar_block_layout {
602        llvm_args.push("--scalar-block-layout".to_string());
603    }
604    if builder.skip_block_layout {
605        llvm_args.push("--skip-block-layout".to_string());
606    }
607    if builder.preserve_bindings {
608        llvm_args.push("--preserve-bindings".to_string());
609    }
610    let mut target_features = vec![];
611    let abort_strategy = match builder.shader_panic_strategy {
612        ShaderPanicStrategy::SilentExit => None,
613        ShaderPanicStrategy::DebugPrintfThenExit {
614            print_inputs,
615            print_backtrace,
616        } => {
617            target_features.push("+ext:SPV_KHR_non_semantic_info".into());
618            Some(format!(
619                "debug-printf{}{}",
620                if print_inputs { "+inputs" } else { "" },
621                if print_backtrace { "+backtrace" } else { "" }
622            ))
623        }
624        ShaderPanicStrategy::UNSOUND_DO_NOT_USE_UndefinedBehaviorViaUnreachable => {
625            Some("unreachable".into())
626        }
627    };
628    llvm_args.extend(abort_strategy.map(|strategy| format!("--abort-strategy={strategy}")));
629
630    if let Ok(extra_codegen_args) = tracked_env_var_get("RUSTGPU_CODEGEN_ARGS") {
631        llvm_args.extend(extra_codegen_args.split_whitespace().map(|s| s.to_string()));
632    } else {
633        llvm_args.extend(builder.extra_args.iter().cloned());
634    }
635
636    let llvm_args = join_checking_for_separators(llvm_args, " ");
637    if !llvm_args.is_empty() {
638        rustflags.push(["-Cllvm-args=", &llvm_args].concat());
639    }
640
641    target_features.extend(builder.capabilities.iter().map(|cap| format!("+{cap:?}")));
642    target_features.extend(builder.extensions.iter().map(|ext| format!("+ext:{ext}")));
643    let target_features = join_checking_for_separators(target_features, ",");
644    if !target_features.is_empty() {
645        rustflags.push(["-Ctarget-feature=", &target_features].concat());
646    }
647
648    if builder.deny_warnings {
649        rustflags.push("-Dwarnings".to_string());
650    }
651
652    if let Ok(extra_rustflags) = tracked_env_var_get("RUSTGPU_RUSTFLAGS") {
653        rustflags.extend(extra_rustflags.split_whitespace().map(|s| s.to_string()));
654    }
655
656    // If we're nested in `cargo` invocation, use a different `--target-dir`,
657    // to avoid waiting on the same lock (which effectively dead-locks us).
658    let outer_target_dir = match (env::var("PROFILE"), env::var_os("OUT_DIR")) {
659        (Ok(outer_profile), Some(dir)) => {
660            // Strip `$outer_profile/build/*/out`.
661            [&outer_profile, "build", "*", "out"].iter().rev().try_fold(
662                PathBuf::from(dir),
663                |mut dir, &filter| {
664                    if (filter == "*" || dir.ends_with(filter)) && dir.pop() {
665                        Some(dir)
666                    } else {
667                        None
668                    }
669                },
670            )
671        }
672        _ => None,
673    };
674    // FIXME(eddyb) use `crate metadata` to always be able to get the "outer"
675    // (or "default") `--target-dir`, to append `/spirv-builder` to it.
676    let target_dir = outer_target_dir.map(|outer| outer.join("spirv-builder"));
677
678    let profile = if builder.release { "release" } else { "dev" };
679
680    let mut cargo = Command::new("cargo");
681    cargo.args([
682        "build",
683        "--lib",
684        "--message-format=json-render-diagnostics",
685        "-Zbuild-std=core",
686        "-Zbuild-std-features=compiler-builtins-mem",
687        "--profile",
688        profile,
689        "--target",
690        &*builder.target,
691    ]);
692
693    // NOTE(eddyb) see above how this is computed and why it might be missing.
694    if let Some(target_dir) = target_dir {
695        cargo.arg("--target-dir").arg(target_dir);
696    }
697
698    // Clear Cargo environment variables that we don't want to leak into the
699    // inner invocation of Cargo (because e.g. build scripts might read them),
700    // before we set any of our own below.
701    for (key, _) in env::vars_os() {
702        let remove = key.to_str().map_or(false, |s| {
703            s.starts_with("CARGO_FEATURES_") || s.starts_with("CARGO_CFG_")
704        });
705        if remove {
706            cargo.env_remove(key);
707        }
708    }
709
710    // NOTE(eddyb) Cargo caches some information it got from `rustc` in
711    // `.rustc_info.json`, and assumes it only depends on the `rustc` binary,
712    // but in our case, `rustc_codegen_spirv` changes are also relevant,
713    // so we turn off that caching with an env var, just to avoid any issues.
714    cargo.env("CARGO_CACHE_RUSTC_INFO", "0");
715
716    // NOTE(eddyb) this used to be just `RUSTFLAGS` but at some point Cargo
717    // added a separate environment variable using `\x1f` instead of spaces,
718    // which allows us to have spaces within individual `rustc` flags.
719    cargo.env(
720        "CARGO_ENCODED_RUSTFLAGS",
721        join_checking_for_separators(rustflags, "\x1f"),
722    );
723
724    let profile_in_env_var = profile.replace('-', "_").to_ascii_uppercase();
725
726    // NOTE(eddyb) there's no parallelism to take advantage of multiple CGUs,
727    // and inter-CGU duplication can be wasteful, so this forces 1 CGU for now.
728    let num_cgus = 1;
729    cargo.env(
730        format!("CARGO_PROFILE_{profile_in_env_var}_CODEGEN_UNITS"),
731        num_cgus.to_string(),
732    );
733
734    let build = cargo
735        .stderr(Stdio::inherit())
736        .current_dir(&builder.path_to_crate)
737        .output()
738        .expect("failed to execute cargo build");
739
740    // `get_last_artifact` has the side-effect of printing invalid lines, so
741    // we do that even in case of an error, to let through any useful messages
742    // that ended up on stdout instead of stderr.
743    let stdout = String::from_utf8(build.stdout).unwrap();
744    if build.status.success() {
745        get_sole_artifact(&stdout).ok_or_else(|| {
746            eprintln!("--- build output ---\n{stdout}");
747            panic!(
748                "`{ARTIFACT_SUFFIX}` artifact not found in (supposedly successful) build output (see above)"
749            );
750        })
751    } else {
752        Err(SpirvBuilderError::BuildFailed)
753    }
754}
755
756#[derive(Deserialize)]
757struct RustcOutput {
758    reason: String,
759    filenames: Option<Vec<String>>,
760}
761
762const ARTIFACT_SUFFIX: &str = ".spv.json";
763
764fn get_sole_artifact(out: &str) -> Option<PathBuf> {
765    let last = out
766        .lines()
767        .filter_map(|line| {
768            if let Ok(line) = serde_json::from_str::<RustcOutput>(line) {
769                Some(line)
770            } else {
771                // Pass through invalid lines
772                println!("{line}");
773                None
774            }
775        })
776        .filter(|line| line.reason == "compiler-artifact")
777        .last()
778        .expect("Did not find output file in rustc output");
779
780    let mut filenames = last
781        .filenames
782        .unwrap()
783        .into_iter()
784        .filter(|v| v.ends_with(ARTIFACT_SUFFIX));
785    let filename = filenames.next()?;
786    assert_eq!(
787        filenames.next(),
788        None,
789        "build had multiple `{ARTIFACT_SUFFIX}` artifacts"
790    );
791    Some(filename.into())
792}
793
794/// Internally iterate through the leaf dependencies of the artifact at `artifact`
795fn leaf_deps(artifact: &Path, mut handle: impl FnMut(&RawStr)) -> std::io::Result<()> {
796    let deps_file = artifact.with_extension("d");
797    let mut deps_map = HashMap::new();
798    depfile::read_deps_file(&deps_file, |item, deps| {
799        deps_map.insert(item, deps);
800        Ok(())
801    })?;
802    fn recurse(
803        map: &HashMap<RawString, Vec<RawString>>,
804        artifact: &RawStr,
805        handle: &mut impl FnMut(&RawStr),
806    ) {
807        match map.get(artifact) {
808            Some(entries) => {
809                for entry in entries {
810                    recurse(map, entry, handle);
811                }
812            }
813            None => handle(artifact),
814        }
815    }
816    recurse(&deps_map, artifact.to_str().unwrap().into(), &mut handle);
817    Ok(())
818}