Skip to main content

build_info_build/build_script_options/
mod.rs

1use core::sync::atomic::{AtomicBool, Ordering};
2use std::path::{Path, PathBuf};
3
4use build_info_common::{OptimizationLevel, VersionedString};
5use chrono::{DateTime, Utc};
6
7pub use self::crate_info::DependencyDepth;
8use super::BuildInfo;
9
10mod compiler;
11mod crate_info;
12mod target;
13mod timestamp;
14mod version_control;
15
16pub fn cargo_toml() -> &'static Path {
17	static CARGO_TOML: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
18	CARGO_TOML.get_or_init(|| Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("Cargo.toml"))
19}
20
21/// Type to store any (optional) options for the build script.
22pub struct BuildScriptOptions {
23	/// Stores if the build info has already been generated
24	consumed: bool,
25
26	/// Use this as the build timestamp, if set.
27	timestamp: Option<DateTime<Utc>>,
28
29	/// Enable runtime dependency collection
30	collect_runtime_dependencies: DependencyDepth,
31
32	/// Enable build dependency collection
33	collect_build_dependencies: DependencyDepth,
34
35	/// Enable dev dependency collection
36	collect_dev_dependencies: DependencyDepth,
37}
38static BUILD_SCRIPT_RAN: AtomicBool = AtomicBool::new(false);
39
40impl BuildScriptOptions {
41	/// WARNING: Should only be called once!
42	fn drop_to_build_info(&mut self) -> BuildInfo {
43		assert!(!self.consumed);
44		self.consumed = true;
45
46		let profile = std::env::var("PROFILE").unwrap_or_else(|_| "UNKNOWN".to_string());
47		let optimization_level = match std::env::var("OPT_LEVEL")
48			.expect("Expected environment variable `OPT_LEVEL` to be set by cargo")
49			.as_str()
50		{
51			"0" => OptimizationLevel::O0,
52			"1" => OptimizationLevel::O1,
53			"2" => OptimizationLevel::O2,
54			"3" => OptimizationLevel::O3,
55			"s" => OptimizationLevel::Os,
56			"z" => OptimizationLevel::Oz,
57			level => panic!("Unknown optimization level {level:?}"),
58		};
59
60		let compiler = compiler::get_info();
61		let target = target::get_info();
62		let crate_info::Manifest {
63			crate_info,
64			workspace_root,
65		} = crate_info::read_manifest(
66			&target.triple,
67			self.collect_runtime_dependencies,
68			self.collect_build_dependencies,
69			self.collect_dev_dependencies,
70		);
71		let version_control = version_control::get_info();
72
73		let timestamp = self.timestamp.unwrap_or_else(timestamp::get_timestamp);
74		let build_info = BuildInfo {
75			timestamp,
76			profile,
77			optimization_level,
78			crate_info,
79			compiler,
80			target,
81			version_control,
82		};
83
84		let mut bytes = Vec::new();
85		let mut compressed = zstd::Encoder::new(&mut bytes, 22).expect("Could not create ZSTD encoder");
86		ciborium::into_writer(&build_info, &mut compressed).unwrap();
87		compressed.finish().unwrap();
88
89		let string = z85::encode(&bytes);
90		let versioned = VersionedString::build_info_common_versioned(string);
91		let serialized = serde_json::to_string(&versioned).unwrap();
92
93		println!("cargo:rustc-env=BUILD_INFO={serialized}");
94
95		// Whenever any `cargo:rerun-if-changed` key is set, the default set is cleared.
96		// Since we will need to emit such keys to trigger rebuilds when the vcs repository changes state,
97		// we also have to emit the customary triggers again, or we will only be rerun in that exact case.
98		rebuild_if_project_changes(&workspace_root);
99
100		build_info
101	}
102
103	/// Consumes the `BuildScriptOptions` and returns a `BuildInfo` object. Use this function if you wish to inspect the
104	/// generated build information in `build.rs`.
105	pub fn build(mut self) -> BuildInfo {
106		self.drop_to_build_info()
107	}
108}
109
110impl From<BuildScriptOptions> for BuildInfo {
111	fn from(opts: BuildScriptOptions) -> BuildInfo {
112		opts.build()
113	}
114}
115
116impl Default for BuildScriptOptions {
117	fn default() -> Self {
118		let build_script_ran = BUILD_SCRIPT_RAN.swap(true, Ordering::SeqCst);
119		assert!(!build_script_ran, "The build script may only be run once.");
120
121		Self {
122			consumed: false,
123			timestamp: None,
124			collect_runtime_dependencies: DependencyDepth::None,
125			collect_build_dependencies: DependencyDepth::None,
126			collect_dev_dependencies: DependencyDepth::None,
127		}
128	}
129}
130
131impl Drop for BuildScriptOptions {
132	fn drop(&mut self) {
133		if !self.consumed {
134			let _build_info = self.drop_to_build_info();
135		}
136	}
137}
138
139/// Emits a `cargo:rerun-if-changed` line for each file in the target project.
140/// By default, the following files are included:
141/// - `Cargo.toml`
142/// - `$workspace_root/Cargo.lock`
143/// - Any file that ends in `.rs`
144fn rebuild_if_project_changes(workspace_root: &str) {
145	println!("cargo:rerun-if-changed={}", cargo_toml().to_str().unwrap());
146	println!(
147		"cargo:rerun-if-changed={}",
148		Path::new(workspace_root).join("Cargo.lock").to_str().unwrap()
149	);
150
151	for source in glob::glob_with(
152		"**/*.rs",
153		glob::MatchOptions {
154			case_sensitive: false,
155			require_literal_separator: false,
156			require_literal_leading_dot: false,
157		},
158	)
159	.unwrap()
160	.map(|source| source.unwrap())
161	{
162		println!("cargo:rerun-if-changed={}", source.to_str().unwrap());
163	}
164}