build_info_build/build_script_options/
mod.rs

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