1#![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#![doc = include_str!("../README.md")]
73
74#[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 #[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 None,
158 DependencyOnly,
160 Full,
164}
165
166#[derive(Debug, PartialEq, Eq, Clone, Copy)]
167pub enum SpirvMetadata {
168 None,
170 NameVariables,
173 Full,
175}
176
177#[derive(Debug, PartialEq, Eq, Clone, Copy)]
179pub enum ShaderPanicStrategy {
180 SilentExit,
186
187 DebugPrintfThenExit {
231 print_inputs: bool,
234
235 print_backtrace: bool,
242 },
243
244 #[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 pub shader_panic_strategy: ShaderPanicStrategy,
272
273 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 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 #[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 #[must_use]
327 pub fn release(mut self, v: bool) -> Self {
328 self.release = v;
329 self
330 }
331
332 #[must_use]
336 pub fn multimodule(mut self, v: bool) -> Self {
337 self.multimodule = v;
338 self
339 }
340
341 #[must_use]
344 pub fn spirv_metadata(mut self, v: SpirvMetadata) -> Self {
345 self.spirv_metadata = v;
346 self
347 }
348
349 #[must_use]
352 pub fn capability(mut self, capability: Capability) -> Self {
353 self.capabilities.push(capability);
354 self
355 }
356
357 #[must_use]
360 pub fn extension(mut self, extension: impl Into<String>) -> Self {
361 self.extensions.push(extension.into());
362 self
363 }
364
365 #[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 #[must_use]
374 pub fn relax_struct_store(mut self, v: bool) -> Self {
375 self.relax_struct_store = v;
376 self
377 }
378
379 #[must_use]
382 pub fn relax_logical_pointer(mut self, v: bool) -> Self {
383 self.relax_logical_pointer = v;
384 self
385 }
386
387 #[must_use]
390 pub fn relax_block_layout(mut self, v: bool) -> Self {
391 self.relax_block_layout = v;
392 self
393 }
394
395 #[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 #[must_use]
407 pub fn scalar_block_layout(mut self, v: bool) -> Self {
408 self.scalar_block_layout = v;
409 self
410 }
411
412 #[must_use]
415 pub fn skip_block_layout(mut self, v: bool) -> Self {
416 self.skip_block_layout = v;
417 self
418 }
419
420 #[must_use]
422 pub fn preserve_bindings(mut self, v: bool) -> Self {
423 self.preserve_bindings = v;
424 self
425 }
426
427 #[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 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 .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
498fn 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
530fn 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
540fn invoke_rustc(builder: &SpirvBuilder) -> Result<PathBuf, SpirvBuilderError> {
542 let rustc_codegen_spirv = find_rustc_codegen_spirv();
551
552 let mut rustflags = vec![
553 format!("-Zcodegen-backend={}", rustc_codegen_spirv.display()),
554 "-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 "-Coverflow-checks=off".to_string(),
564 "-Cdebug-assertions=off".to_string(),
565 "-Zinline-mir=off".to_string(),
568 ];
569
570 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 let outer_target_dir = match (env::var("PROFILE"), env::var_os("OUT_DIR")) {
659 (Ok(outer_profile), Some(dir)) => {
660 [&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 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 if let Some(target_dir) = target_dir {
695 cargo.arg("--target-dir").arg(target_dir);
696 }
697
698 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 cargo.env("CARGO_CACHE_RUSTC_INFO", "0");
715
716 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 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 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 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
794fn 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}