1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
use core::sync::atomic::{AtomicBool, Ordering};
use std::path::{Path, PathBuf};

use base64::write::EncoderWriter as Base64Encoder;
use build_info_common::{OptimizationLevel, VersionedString};
use chrono::{DateTime, Utc};

pub use self::crate_info::DependencyDepth;
use super::BuildInfo;

mod compiler;
mod crate_info;
mod target;
mod timestamp;
mod version_control;

pub fn cargo_toml() -> &'static Path {
	static CARGO_TOML: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
	CARGO_TOML.get_or_init(|| Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("Cargo.toml"))
}

/// Type to store any (optional) options for the build script.
pub struct BuildScriptOptions {
	/// Stores if the build info has already been generated
	consumed: bool,

	/// Use this as the build timestamp, if set.
	timestamp: Option<DateTime<Utc>>,

	/// Enable runtime dependency collection
	collect_runtime_dependencies: DependencyDepth,

	/// Enable build dependency collection
	collect_build_dependencies: DependencyDepth,

	/// Enable dev dependency collection
	collect_dev_dependencies: DependencyDepth,
}
static BUILD_SCRIPT_RAN: AtomicBool = AtomicBool::new(false);

impl BuildScriptOptions {
	/// WARNING: Should only be called once!
	fn drop_to_build_info(&mut self) -> BuildInfo {
		assert!(!self.consumed);
		self.consumed = true;

		let profile = std::env::var("PROFILE").unwrap_or_else(|_| "UNKNOWN".to_string());
		let optimization_level = match std::env::var("OPT_LEVEL")
			.expect("Expected environment variable `OPT_LEVEL` to be set by cargo")
			.as_str()
		{
			"0" => OptimizationLevel::O0,
			"1" => OptimizationLevel::O1,
			"2" => OptimizationLevel::O2,
			"3" => OptimizationLevel::O3,
			"s" => OptimizationLevel::Os,
			"z" => OptimizationLevel::Oz,
			level => panic!("Unknown optimization level {level:?}"),
		};

		let compiler = compiler::get_info();
		let target = target::get_info();
		let crate_info::Manifest {
			crate_info,
			workspace_root,
		} = crate_info::read_manifest(
			&target.triple,
			self.collect_runtime_dependencies,
			self.collect_build_dependencies,
			self.collect_dev_dependencies,
		);
		let version_control = version_control::get_info();

		let timestamp = self.timestamp.unwrap_or_else(timestamp::get_timestamp);
		let build_info = BuildInfo {
			timestamp,
			profile,
			optimization_level,
			crate_info,
			compiler,
			target,
			version_control,
		};

		let mut bytes = Vec::new();
		const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
			&base64::alphabet::STANDARD,
			base64::engine::GeneralPurposeConfig::new()
				.with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
		);
		let string_safe = Base64Encoder::new(&mut bytes, &BASE64_ENGINE);
		let mut compressed = zstd::Encoder::new(string_safe, 22).expect("Could not create ZSTD encoder");
		bincode::serialize_into(&mut compressed, &build_info).unwrap();
		compressed.finish().unwrap().finish().unwrap();

		let string = String::from_utf8(bytes).unwrap();
		let versioned = VersionedString::build_info_common_versioned(string);
		let serialized = serde_json::to_string(&versioned).unwrap();

		println!("cargo:rustc-env=BUILD_INFO={serialized}");

		// Whenever any `cargo:rerun-if-changed` key is set, the default set is cleared.
		// Since we will need to emit such keys to trigger rebuilds when the vcs repository changes state,
		// we also have to emit the customary triggers again, or we will only be rerun in that exact case.
		rebuild_if_project_changes(&workspace_root);

		build_info
	}

	/// Consumes the `BuildScriptOptions` and returns a `BuildInfo` object. Use this function if you wish to inspect the
	/// generated build information in `build.rs`.
	pub fn build(mut self) -> BuildInfo {
		self.drop_to_build_info()
	}
}

impl From<BuildScriptOptions> for BuildInfo {
	fn from(opts: BuildScriptOptions) -> BuildInfo {
		opts.build()
	}
}

impl Default for BuildScriptOptions {
	fn default() -> Self {
		let build_script_ran = BUILD_SCRIPT_RAN.swap(true, Ordering::SeqCst);
		assert!(!build_script_ran, "The build script may only be run once.");

		Self {
			consumed: false,
			timestamp: None,
			collect_runtime_dependencies: DependencyDepth::None,
			collect_build_dependencies: DependencyDepth::None,
			collect_dev_dependencies: DependencyDepth::None,
		}
	}
}

impl Drop for BuildScriptOptions {
	fn drop(&mut self) {
		if !self.consumed {
			let _build_info = self.drop_to_build_info();
		}
	}
}

/// Emits a `cargo:rerun-if-changed` line for each file in the target project.
/// By default, the following files are included:
/// - `Cargo.toml`
/// - `$workspace_root/Cargo.lock`
/// - Any file that ends in `.rs`
fn rebuild_if_project_changes(workspace_root: &str) {
	println!("cargo:rerun-if-changed={}", cargo_toml().to_str().unwrap());
	println!(
		"cargo:rerun-if-changed={}",
		Path::new(workspace_root).join("Cargo.lock").to_str().unwrap()
	);

	for source in glob::glob_with(
		"**/*.rs",
		glob::MatchOptions {
			case_sensitive: false,
			require_literal_separator: false,
			require_literal_leading_dot: false,
		},
	)
	.unwrap()
	.map(|source| source.unwrap())
	{
		println!("cargo:rerun-if-changed={}", source.to_str().unwrap());
	}
}