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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#![doc = include_str!("../README.md")]

use std::{env::var, fs::File, io::Write, path::PathBuf};

pub use info::*;
mod info;

/// Gather build-time information for the current crate
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to`] with the most common defaults: it writes to `bosion.rs` a pub(crate) struct named
/// `Bosion`.
pub fn gather() {
	gather_to("bosion.rs", "Bosion", false);
}

/// Gather build-time information for the current crate (public visibility)
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to`]: it writes to `bosion.rs` a pub struct named `Bosion`.
pub fn gather_pub() {
	gather_to("bosion.rs", "Bosion", true);
}

/// Gather build-time information for the current crate (custom output)
///
/// Gathers a limited set of build-time information for the current crate and writes it to a file.
/// The file is always written to the `OUT_DIR` directory, as per Cargo conventions. It contains a
/// zero-size struct with a bunch of associated constants containing the gathered information, and a
/// `long_version_with` function (when the `std` feature is enabled) that takes a slice of extra
/// key-value pairs to append in the same format.
///
/// `public` controls whether the struct is `pub` (true) or `pub(crate)` (false).
///
/// The generated code is entirely documented, and will appear in your documentation (in docs.rs, it
/// only will if visibility is public).
///
/// See [`Info`] for a list of gathered data.
///
/// The constants include all the information from [`Info`], as well as the following:
///
/// - `LONG_VERSION`: A clap-ready long version string, including the crate version, features, build
///   date, and git information when available.
/// - `CRATE_FEATURE_STRING`: A string containing the crate features, in the format `+feat1 +feat2`.
///
/// We also instruct rustc to rerun the build script if the environment changes, as necessary.
pub fn gather_to(filename: &str, structname: &str, public: bool) {
	let path = PathBuf::from(var("OUT_DIR").expect("bosion")).join(filename);
	println!("cargo:rustc-env=BOSION_PATH={}", path.display());

	let info = Info::gather().expect("bosion");
	info.set_reruns();
	let Info {
		crate_version,
		crate_features,
		build_date,
		build_datetime,
		git,
	} = info;

	let crate_feature_string = crate_features
		.iter()
		.filter(|feat| *feat != "default")
		.map(|feat| format!("+{feat}"))
		.collect::<Vec<_>>()
		.join(" ");

	let crate_feature_list = crate_features.join(",");

	let viz = if public { "pub" } else { "pub(crate)" };

	let (git_render, long_version) = if let Some(GitInfo {
		git_hash,
		git_shorthash,
		git_date,
		git_datetime,
		..
	}) = git
	{
		(format!(
		"
			/// The git commit hash
			///
			/// This is the full hash of the commit that was built. Note that if the repository was
			/// dirty, this will be the hash of the last commit, not including the changes.
			pub const GIT_COMMIT_HASH: &'static str = {git_hash:?};

			/// The git commit hash, shortened
			///
			/// This is the shortened hash of the commit that was built. Same caveats as with
			/// `GIT_COMMIT_HASH` apply. The length of the hash is as short as possible while still
			/// being unambiguous, at build time. For large repositories, this may be longer than 7
			/// characters.
			pub const GIT_COMMIT_SHORTHASH: &'static str = {git_shorthash:?};

			/// The git commit date
			///
			/// This is the date (`YYYY-MM-DD`) of the commit that was built. Same caveats as with
			/// `GIT_COMMIT_HASH` apply.
			pub const GIT_COMMIT_DATE: &'static str = {git_date:?};

			/// The git commit date and time
			///
			/// This is the date and time (`YYYY-MM-DD HH:MM:SS`) of the commit that was built. Same
			/// caveats as with `GIT_COMMIT_HASH` apply.
			pub const GIT_COMMIT_DATETIME: &'static str = {git_datetime:?};
		"
	), format!("{crate_version} ({git_shorthash} {git_date}) {crate_feature_string}\ncommit-hash: {git_hash}\ncommit-date: {git_date}\nbuild-date: {build_date}\nrelease: {crate_version}\nfeatures: {crate_feature_list}"))
	} else {
		(String::new(), format!("{crate_version} ({build_date}) {crate_feature_string}\nbuild-date: {build_date}\nrelease: {crate_version}\nfeatures: {crate_feature_list}"))
	};

	#[cfg(feature = "std")]
	let long_version_with_fn = r#"
		/// Returns the long version string with extra information tacked on
		///
		/// This is the same as `LONG_VERSION` but takes a slice of key-value pairs to append to the
		/// end in the same format.
		pub fn long_version_with(extra: &[(&str, &str)]) -> String {
			let mut output = Self::LONG_VERSION.to_string();

			for (k, v) in extra {
				output.push_str(&format!("\n{k}: {v}"));
			}

			output
		}
	"#;
	#[cfg(not(feature = "std"))]
	let long_version_with_fn = "";

	let bosion_version = env!("CARGO_PKG_VERSION");
	let render = format!(
		r#"
		/// Build-time information
		///
		/// This struct is generated by the [bosion](https://docs.rs/bosion) crate at build time.
		///
		/// Bosion version: {bosion_version}
		#[derive(Debug, Clone, Copy)]
		{viz} struct {structname};

		#[allow(dead_code)]
		impl {structname} {{
			/// Clap-compatible long version string
			///
			/// At minimum, this will be the crate version and build date.
			///
			/// It presents as a first "summary" line like `crate_version (build_date) features`,
			/// followed by `key: value` pairs. This is the same format used by `rustc -Vv`.
			///
			/// If git info is available, it also includes the git hash, short hash and commit date,
			/// and swaps the build date for the commit date in the summary line.
			pub const LONG_VERSION: &'static str = {long_version:?};

			/// The crate version, as reported by Cargo
			///
			/// You should probably prefer reading the `CARGO_PKG_VERSION` environment variable.
			pub const CRATE_VERSION: &'static str = {crate_version:?};

			/// The crate features
			///
			/// This is a list of the features that were enabled when this crate was built,
			/// lowercased and with underscores replaced by hyphens.
			pub const CRATE_FEATURES: &'static [&'static str] = &{crate_features:?};

			/// The crate features, as a string
			///
			/// This is in format `+feature +feature2 +feature3`, lowercased with underscores
			/// replaced by hyphens.
			pub const CRATE_FEATURE_STRING: &'static str = {crate_feature_string:?};

			/// The build date
			///
			/// This is the date that the crate was built, in the format `YYYY-MM-DD`. If the
			/// environment variable `SOURCE_DATE_EPOCH` was set, it's used instead of the current
			/// time, for [reproducible builds](https://reproducible-builds.org/).
			pub const BUILD_DATE: &'static str = {build_date:?};

			/// The build datetime
			///
			/// This is the date and time that the crate was built, in the format
			/// `YYYY-MM-DD HH:MM:SS`. If the environment variable `SOURCE_DATE_EPOCH` was set, it's
			/// used instead of the current time, for
			/// [reproducible builds](https://reproducible-builds.org/).
			pub const BUILD_DATETIME: &'static str = {build_datetime:?};

			{git_render}

			{long_version_with_fn}
		}}
		"#
	);

	let mut file = File::create(path).expect("bosion");
	file.write_all(render.as_bytes()).expect("bosion");
}

/// Gather build-time information and write it to the environment
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to_env_with_prefix`] with the most common default prefix of `BOSION_`.
pub fn gather_to_env() {
	gather_to_env_with_prefix("BOSION_");
}

/// Gather build-time information and write it to the environment
///
/// Gathers a limited set of build-time information for the current crate and makes it available to
/// the crate as build environment variables. This is an alternative to [`include!`]ing a file which
/// is generated at build time, like for [`gather`] and variants, which doesn't create any new code
/// and doesn't include any information in the binary that you do not explicitly use.
///
/// The environment variables are prefixed with the given string, which should be generally be
/// uppercase and end with an underscore.
///
/// See [`Info`] for a list of gathered data.
///
/// Unlike [`gather`], there is no Clap-ready `LONG_VERSION` string, but you can of course generate
/// one yourself from the environment variables.
///
/// We also instruct rustc to rerun the build script if the environment changes, as necessary.
pub fn gather_to_env_with_prefix(prefix: &str) {
	let info = Info::gather().expect("bosion");
	info.set_reruns();
	let Info {
		crate_version,
		crate_features,
		build_date,
		build_datetime,
		git,
	} = info;

	println!("cargo:rustc-env={prefix}CRATE_VERSION={crate_version}");
	println!(
		"cargo:rustc-env={prefix}CRATE_FEATURES={}",
		crate_features.join(",")
	);
	println!("cargo:rustc-env={prefix}BUILD_DATE={build_date}");
	println!("cargo:rustc-env={prefix}BUILD_DATETIME={build_datetime}");

	if let Some(GitInfo {
		git_hash,
		git_shorthash,
		git_date,
		git_datetime,
		..
	}) = git
	{
		println!("cargo:rustc-env={prefix}GIT_COMMIT_HASH={git_hash}");
		println!("cargo:rustc-env={prefix}GIT_COMMIT_SHORTHASH={git_shorthash}");
		println!("cargo:rustc-env={prefix}GIT_COMMIT_DATE={git_date}");
		println!("cargo:rustc-env={prefix}GIT_COMMIT_DATETIME={git_datetime}");
	}
}