vergen_lib/emitter.rs
1// Copyright (c) 2022 vergen developers
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9use crate::{AddCustomEntries, AddEntries, CargoRustcEnvMap, DefaultConfig};
10use anyhow::Result;
11use std::{
12 collections::BTreeMap,
13 env,
14 io::{self, Write},
15};
16
17/// The `Emitter` will emit cargo instructions (i.e. cargo:rustc-env=NAME=VALUE)
18/// base on the configuration you enable.
19#[derive(Clone, Debug, PartialEq)]
20pub struct Emitter {
21 idempotent: bool,
22 fail_on_error: bool,
23 quiet: bool,
24 custom_buildrs: Option<&'static str>,
25 #[doc(hidden)]
26 cargo_rustc_env_map: CargoRustcEnvMap,
27 #[doc(hidden)]
28 cargo_rustc_env_map_custom: BTreeMap<String, String>,
29 #[doc(hidden)]
30 cargo_rerun_if_changed: Vec<String>,
31 #[doc(hidden)]
32 cargo_warning: Vec<String>,
33}
34
35impl Default for Emitter {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Emitter {
42 #[doc(hidden)]
43 #[must_use]
44 pub fn cargo_rustc_env_map(&self) -> &CargoRustcEnvMap {
45 &self.cargo_rustc_env_map
46 }
47 #[doc(hidden)]
48 #[must_use]
49 pub fn cargo_rustc_env_map_custom(&self) -> &BTreeMap<String, String> {
50 &self.cargo_rustc_env_map_custom
51 }
52 #[doc(hidden)]
53 #[must_use]
54 pub fn cargo_rerun_if_changed(&self) -> &Vec<String> {
55 &self.cargo_rerun_if_changed
56 }
57 #[doc(hidden)]
58 #[must_use]
59 pub fn cargo_warning(&self) -> &Vec<String> {
60 &self.cargo_warning
61 }
62
63 /// Instantiate the builder to configure the cargo instruction emits
64 #[must_use]
65 pub fn new() -> Self {
66 Self {
67 idempotent: matches!(env::var("VERGEN_IDEMPOTENT"), Ok(_val)),
68 fail_on_error: false,
69 quiet: false,
70 custom_buildrs: None,
71 cargo_rustc_env_map: CargoRustcEnvMap::default(),
72 cargo_rustc_env_map_custom: BTreeMap::default(),
73 cargo_rerun_if_changed: Vec::default(),
74 cargo_warning: Vec::default(),
75 }
76 }
77
78 /// Enable the `idempotent` feature
79 ///
80 /// **NOTE** - This feature can also be enabled via the `VERGEN_IDEMPOTENT`
81 /// environment variable.
82 ///
83 /// When this feature is enabled, certain vergen output (i.e. timestamps, sysinfo)
84 /// will be set to an idempotent default. This will allow systems that
85 /// depend on deterministic builds to override user requested `vergen`
86 /// impurities. This will mainly allow for package maintainers to build
87 /// packages that depend on `vergen` in a deterministic manner.
88 ///
89 /// See [this issue](https://github.com/rustyhorde/vergen/issues/141) for
90 /// more details
91 ///
92 /// | Variable | Sample |
93 /// | ------- | ------ |
94 /// | `VERGEN_BUILD_DATE` | `VERGEN_IDEMPOTENT_OUTPUT` |
95 /// | `VERGEN_BUILD_TIMESTAMP` | `VERGEN_IDEMPOTENT_OUTPUT` |
96 ///
97 /// # Example
98 ///
99 /// ```
100 /// # use anyhow::Result;
101 /// # use vergen_lib::Emitter;
102 /// #
103 /// # fn main() -> Result<()> {
104 /// Emitter::new().idempotent().emit()?;
105 /// // or
106 /// # temp_env::with_var("VERGEN_IDEMPOTENT", Some("true"), || {
107 /// # let result = || -> Result<()> {
108 /// // set::env("VERGEN_IDEMPOTENT", "true");
109 /// Emitter::new().emit()?;
110 /// # Ok(())
111 /// # }();
112 /// # });
113 /// # Ok(())
114 /// # }
115 /// ```
116 ///
117 pub fn idempotent(&mut self) -> &mut Self {
118 self.idempotent = true;
119 self
120 }
121
122 /// Enable the `fail_on_error` feature
123 ///
124 /// By default `vergen` will emit the instructions you requested. If for some
125 /// reason those instructions cannot be generated correctly, placeholder values
126 /// will be used instead. `vergen` will also emit [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning)
127 /// instructions notifying you this has happened.
128 ///
129 /// For example, if you configure `vergen` to emit `VERGEN_GIT_*` instructions and
130 /// you run a build from a source tarball with no `.git` directory, the instructions
131 /// will be populated with placeholder values, rather than information gleaned through git.
132 ///
133 /// You can turn off this behavior by enabling `fail_on_error`.
134 ///
135 /// # Example
136 ///
137 /// ```
138 /// # use anyhow::Result;
139 /// # use vergen_lib::Emitter;
140 /// #
141 /// # fn main() -> Result<()> {
142 /// Emitter::new().fail_on_error().emit()?;
143 /// # Ok(())
144 /// # }
145 /// ```
146 pub fn fail_on_error(&mut self) -> &mut Self {
147 self.fail_on_error = true;
148 self
149 }
150
151 /// Enable the quiet feature
152 ///
153 /// Suppress the emission of the `cargo:warning` instructions.
154 ///
155 /// # Example
156 ///
157 /// ```
158 /// # use anyhow::Result;
159 /// # use vergen_lib::Emitter;
160 /// #
161 /// # fn main() -> Result<()> {
162 /// Emitter::new().quiet().emit()?;
163 /// # Ok(())
164 /// # }
165 /// ```
166 pub fn quiet(&mut self) -> &mut Self {
167 self.quiet = true;
168 self
169 }
170
171 /// Set a custom build.rs path if you are using a non-standard path
172 ///
173 /// By default `vergen` will use `build.rs` as the build path for the
174 /// `cargo:rerun-if-changed` emit. You can specify a custom `build.rs`
175 /// path here if you have changed this default
176 ///
177 /// # Example
178 ///
179 /// ```
180 /// # use anyhow::Result;
181 /// # use vergen_lib::Emitter;
182 /// #
183 /// # fn main() -> Result<()> {
184 /// Emitter::new().custom_build_rs("my/custom/build.rs").emit()?;
185 /// # Ok(())
186 /// # }
187 /// ```
188 pub fn custom_build_rs(&mut self, path: &'static str) -> &mut Self {
189 self.custom_buildrs = Some(path);
190 self
191 }
192
193 /// Add a set of instructions to the emitter output
194 ///
195 /// # Errors
196 ///
197 pub fn add_instructions(&mut self, gen: &dyn AddEntries) -> Result<&mut Self> {
198 gen.add_map_entries(
199 self.idempotent,
200 &mut self.cargo_rustc_env_map,
201 &mut self.cargo_rerun_if_changed,
202 &mut self.cargo_warning,
203 )
204 .or_else(|e| {
205 let default_config = DefaultConfig::new(self.fail_on_error, e);
206 gen.add_default_entries(
207 &default_config,
208 &mut self.cargo_rustc_env_map,
209 &mut self.cargo_rerun_if_changed,
210 &mut self.cargo_warning,
211 )
212 })?;
213 Ok(self)
214 }
215
216 /// Add a set of custom instructions to the emitter output
217 ///
218 /// # Errors
219 ///
220 /// Errors may be generated if `fail_on_error` has been configured.
221 ///
222 pub fn add_custom_instructions<K, V>(
223 &mut self,
224 gen: &impl AddCustomEntries<K, V>,
225 ) -> Result<&mut Self>
226 where
227 K: Into<String> + Ord,
228 V: Into<String>,
229 {
230 let mut map = BTreeMap::default();
231 gen.add_calculated_entries(
232 self.idempotent,
233 &mut map,
234 &mut self.cargo_rerun_if_changed,
235 &mut self.cargo_warning,
236 )
237 .or_else(|e| {
238 let default_config = DefaultConfig::new(self.fail_on_error, e);
239 gen.add_default_entries(
240 &default_config,
241 &mut map,
242 &mut self.cargo_rerun_if_changed,
243 &mut self.cargo_warning,
244 )
245 })?;
246 self.cargo_rustc_env_map_custom.extend(Self::map_into(map));
247 Ok(self)
248 }
249
250 fn map_into<K, V>(map: BTreeMap<K, V>) -> impl Iterator<Item = (String, String)>
251 where
252 K: Into<String> + Ord,
253 V: Into<String>,
254 {
255 map.into_iter().map(|(k, v)| (k.into(), v.into()))
256 }
257
258 fn emit_output<T>(&self, stdout: &mut T) -> Result<()>
259 where
260 T: Write,
261 {
262 self.emit_instructions(stdout)
263 }
264
265 fn emit_instructions<T>(&self, stdout: &mut T) -> Result<()>
266 where
267 T: Write,
268 {
269 // Emit the 'cargo:rustc-env' instructions
270 for (k, v) in &self.cargo_rustc_env_map {
271 let sanitized_value = Self::filter_newlines(v);
272 writeln!(stdout, "cargo:rustc-env={}={sanitized_value}", k.name())?;
273 }
274
275 // Emit the 'cargo:rustc-env' custom instructions
276 for (k, v) in &self.cargo_rustc_env_map_custom {
277 let sanitized_value = Self::filter_newlines(v);
278 writeln!(stdout, "cargo:rustc-env={k}={sanitized_value}")?;
279 }
280
281 // Emit the `cargo:warning` instructions
282 if !self.quiet {
283 for warning in &self.cargo_warning {
284 let sanitized_output = Self::filter_newlines(warning);
285 writeln!(stdout, "cargo:warning={sanitized_output}")?;
286 }
287 }
288
289 // Emit the 'cargo:rerun-if-changed' instructions for the git paths (if added)
290 for path in &self.cargo_rerun_if_changed {
291 let sanitized_output = Self::filter_newlines(path);
292 writeln!(stdout, "cargo:rerun-if-changed={sanitized_output}")?;
293 }
294
295 // Emit the 'cargo:rerun-if-changed' instructions
296 if !self.cargo_rustc_env_map.is_empty() || !self.cargo_warning.is_empty() {
297 let buildrs = self.custom_buildrs.unwrap_or("build.rs");
298 let sanitized_output = Self::filter_newlines(buildrs);
299 writeln!(stdout, "cargo:rerun-if-changed={sanitized_output}")?;
300 writeln!(stdout, "cargo:rerun-if-env-changed=VERGEN_IDEMPOTENT")?;
301 writeln!(stdout, "cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH")?;
302 }
303 Ok(())
304 }
305
306 fn filter_newlines(s: &str) -> String {
307 s.chars().filter(|c| *c != '\n').collect()
308 }
309
310 /// Emit cargo instructions from your build script
311 ///
312 /// - Will emit [`cargo:rustc-env=VAR=VALUE`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-envvarvalue) for each feature you have enabled.
313 /// - Will emit [`cargo:rerun-if-changed=PATH`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed) if the git feature is enabled. This is done to ensure any git variables are regenerated when commits are made.
314 /// - Can emit [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning) outputs if the
315 /// [`fail_on_error`](Self::fail_on_error) feature is not enabled and the requested variable is defaulted through error or
316 /// the [`idempotent`](Self::idempotent) flag.
317 ///
318 /// # Errors
319 /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
320 ///
321 /// # Example
322 ///
323 /// ```
324 /// # use anyhow::Result;
325 /// # use std::env;
326 /// # use vergen_lib::Emitter;
327 /// #
328 /// # fn main() -> Result<()> {
329 /// let emitter = Emitter::default().emit()?;
330 /// # Ok(())
331 /// # }
332 /// ```
333 ///
334 /// # Sample Output
335 ///
336 /// **NOTE** - You won't see this output unless you invoke cargo with the `-vv` flag.
337 /// The instruction output is not displayed by default.
338 ///
339 /// ```text
340 /// cargo:rustc-env=VERGEN_BUILD_DATE=2023-01-04
341 /// cargo:rustc-env=VERGEN_BUILD_TIMESTAMP=2023-01-04T15:38:11.097507114Z
342 /// cargo:rustc-env=VERGEN_CARGO_DEBUG=true
343 /// cargo:rustc-env=VERGEN_CARGO_FEATURES=build,git
344 /// cargo:rustc-env=VERGEN_CARGO_OPT_LEVEL=1
345 /// cargo:rustc-env=VERGEN_CARGO_TARGET_TRIPLE=x86_64-unknown-linux-gnu
346 /// cargo:rustc-env=VERGEN_GIT_BRANCH=feature/version8
347 /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_EMAIL=your@email.com
348 /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_NAME=Yoda
349 /// cargo:rustc-env=VERGEN_GIT_COMMIT_COUNT=476
350 /// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2023-01-03
351 /// cargo:rustc-env=VERGEN_GIT_COMMIT_MESSAGE=The best message
352 /// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2023-01-03T14:08:12.000000000-05:00
353 /// cargo:rustc-env=VERGEN_GIT_DESCRIBE=7.4.4-103-g53ae8a6
354 /// cargo:rustc-env=VERGEN_GIT_SHA=53ae8a69ab7917a2909af40f2e5d015f5b29ae28
355 /// cargo:rustc-env=VERGEN_RUSTC_CHANNEL=nightly
356 /// cargo:rustc-env=VERGEN_RUSTC_COMMIT_DATE=2023-01-03
357 /// cargo:rustc-env=VERGEN_RUSTC_COMMIT_HASH=c7572670a1302f5c7e245d069200e22da9df0316
358 /// cargo:rustc-env=VERGEN_RUSTC_HOST_TRIPLE=x86_64-unknown-linux-gnu
359 /// cargo:rustc-env=VERGEN_RUSTC_LLVM_VERSION=15.0
360 /// cargo:rustc-env=VERGEN_RUSTC_SEMVER=1.68.0-nightly
361 /// cargo:rustc-env=VERGEN_SYSINFO_NAME=Arch Linux
362 /// cargo:rustc-env=VERGEN_SYSINFO_OS_VERSION=Linux Arch Linux
363 /// cargo:rustc-env=VERGEN_SYSINFO_USER=jozias
364 /// cargo:rustc-env=VERGEN_SYSINFO_TOTAL_MEMORY=31 GiB
365 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_VENDOR=AuthenticAMD
366 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_CORE_COUNT=8
367 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_NAME=cpu0,cpu1,cpu2,cpu3,cpu4,cpu5,cpu6,cpu7
368 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_BRAND=AMD Ryzen Threadripper 1900X 8-Core Processor
369 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_FREQUENCY=3792
370 /// cargo:rerun-if-changed=.git/HEAD
371 /// cargo:rerun-if-changed=.git/refs/heads/feature/version8
372 /// cargo:rerun-if-changed=build.rs
373 /// cargo:rerun-if-env-changed=VERGEN_IDEMPOTENT
374 /// cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH
375 /// ```
376 ///
377 pub fn emit(&self) -> Result<()> {
378 self.emit_output(&mut io::stdout())
379 }
380
381 /// Emit cargo instructions from your build script and set environment variables for use in `build.rs`
382 ///
383 /// - Will emit [`cargo:rustc-env=VAR=VALUE`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-envvarvalue) for each feature you have enabled.
384 /// - Will emit [`cargo:rerun-if-changed=PATH`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed) if the git feature is enabled. This is done to ensure any git variables are regenerated when commits are made.
385 /// - Can emit [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning) outputs if the
386 /// [`fail_on_error`](Self::fail_on_error) feature is not enabled and the requested variable is defaulted through error or
387 /// the [`idempotent`](Self::idempotent) flag.
388 ///
389 /// # Errors
390 /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
391 ///
392 /// # Example
393 ///
394 /// ```
395 /// # use anyhow::Result;
396 /// # use std::env;
397 /// # use vergen_lib::Emitter;
398 /// #
399 /// # fn main() -> Result<()> {
400 /// Emitter::new().emit_and_set()?;
401 /// # Ok(())
402 /// # }
403 /// ```
404 ///
405 #[cfg(feature = "emit_and_set")]
406 #[cfg_attr(coverage_nightly, coverage(off))]
407 pub fn emit_and_set(&self) -> Result<()> {
408 self.emit_output(&mut io::stdout()).map(|()| {
409 for (k, v) in &self.cargo_rustc_env_map {
410 if env::var(k.name()).is_err() {
411 env::set_var(k.name(), v);
412 }
413 }
414 })
415 }
416
417 #[doc(hidden)]
418 /// Emit the cargo build script instructions to the given [`Write`](std::io::Write).
419 ///
420 /// **NOTE** - This is generally only used for testing and probably shouldn't be used
421 /// within a `build.rs` file.
422 ///
423 /// # Errors
424 /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
425 ///
426 pub fn emit_to<T>(&self, stdout: &mut T) -> Result<bool>
427 where
428 T: Write,
429 {
430 self.emit_output(stdout).map(|()| false)
431 }
432
433 #[doc(hidden)]
434 #[must_use]
435 pub fn test_emit(&self) -> Emitter {
436 self.clone()
437 }
438}
439
440#[cfg(test)]
441pub(crate) mod test {
442 use super::Emitter;
443 use anyhow::Result;
444 use serial_test::serial;
445 use std::io::Write;
446
447 #[test]
448 #[serial]
449 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
450 fn emitter_clone_works() {
451 let emitter = Emitter::default();
452 let another = emitter.clone();
453 assert_eq!(another, emitter);
454 }
455
456 #[test]
457 #[serial]
458 fn emitter_debug_works() -> Result<()> {
459 let emitter = Emitter::default();
460 let mut buf = vec![];
461 write!(buf, "{emitter:?}")?;
462 assert!(!buf.is_empty());
463 Ok(())
464 }
465
466 #[test]
467 #[serial]
468 fn default_is_no_emit() -> Result<()> {
469 let mut stdout_buf = vec![];
470 _ = Emitter::new().emit_to(&mut stdout_buf)?;
471 assert!(stdout_buf.is_empty());
472 Ok(())
473 }
474
475 #[test]
476 #[serial]
477 fn default_emit_is_ok() {
478 assert!(Emitter::new().emit().is_ok());
479 }
480}