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)]
20#[allow(clippy::struct_excessive_bools)]
21pub struct Emitter {
22 idempotent: bool,
23 default_on_error: bool,
24 fail_on_error: bool,
25 quiet: bool,
26 custom_buildrs: Option<&'static str>,
27 #[doc(hidden)]
28 cargo_rustc_env_map: CargoRustcEnvMap,
29 #[doc(hidden)]
30 cargo_rustc_env_map_custom: BTreeMap<String, String>,
31 #[doc(hidden)]
32 cargo_rerun_if_changed: Vec<String>,
33 #[doc(hidden)]
34 cargo_warning: Vec<String>,
35}
36
37impl Default for Emitter {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl Emitter {
44 #[doc(hidden)]
45 #[must_use]
46 pub fn cargo_rustc_env_map(&self) -> &CargoRustcEnvMap {
47 &self.cargo_rustc_env_map
48 }
49 #[doc(hidden)]
50 #[must_use]
51 pub fn cargo_rustc_env_map_custom(&self) -> &BTreeMap<String, String> {
52 &self.cargo_rustc_env_map_custom
53 }
54 #[doc(hidden)]
55 #[must_use]
56 pub fn cargo_rerun_if_changed(&self) -> &Vec<String> {
57 &self.cargo_rerun_if_changed
58 }
59 #[doc(hidden)]
60 #[must_use]
61 pub fn cargo_warning(&self) -> &Vec<String> {
62 &self.cargo_warning
63 }
64
65 /// Instantiate the builder to configure the cargo instruction emits
66 #[must_use]
67 pub fn new() -> Self {
68 Self {
69 idempotent: matches!(env::var("VERGEN_IDEMPOTENT"), Ok(_val)),
70 default_on_error: matches!(env::var("VERGEN_DEFAULT_ON_ERROR"), Ok(_val)),
71 fail_on_error: false,
72 quiet: false,
73 custom_buildrs: None,
74 cargo_rustc_env_map: CargoRustcEnvMap::default(),
75 cargo_rustc_env_map_custom: BTreeMap::default(),
76 cargo_rerun_if_changed: Vec::default(),
77 cargo_warning: Vec::default(),
78 }
79 }
80
81 /// Enable the `idempotent` feature
82 ///
83 /// **NOTE** - This feature can also be enabled via the `VERGEN_IDEMPOTENT`
84 /// environment variable.
85 ///
86 /// When this feature is enabled, certain vergen output (i.e. timestamps, sysinfo)
87 /// will be set to an idempotent default. This will allow systems that
88 /// depend on deterministic builds to override user requested `vergen`
89 /// impurities. This will mainly allow for package maintainers to build
90 /// packages that depend on `vergen` in a deterministic manner.
91 ///
92 /// See [this issue](https://github.com/rustyhorde/vergen/issues/141) for
93 /// more details
94 ///
95 /// | Variable | Sample |
96 /// | ------- | ------ |
97 /// | `VERGEN_BUILD_DATE` | `VERGEN_IDEMPOTENT_OUTPUT` |
98 /// | `VERGEN_BUILD_TIMESTAMP` | `VERGEN_IDEMPOTENT_OUTPUT` |
99 ///
100 /// **NOTE** - This feature **always** forces idempotent output, even when the
101 /// requested instructions *could* be generated normally (e.g. inside a real git
102 /// worktree). Use it only when you want deterministic builds. If you instead
103 /// want real values when they are available but a default placeholder only when
104 /// generation fails (e.g. building outside a git worktree), use
105 /// [`default_on_error`](Self::default_on_error) instead.
106 ///
107 /// # Example
108 ///
109 /// ```
110 /// # use anyhow::Result;
111 /// # use vergen_lib::Emitter;
112 /// #
113 /// # fn main() -> Result<()> {
114 /// Emitter::new().idempotent().emit()?;
115 /// // or
116 /// # temp_env::with_var("VERGEN_IDEMPOTENT", Some("true"), || {
117 /// # let result = || -> Result<()> {
118 /// // set::env("VERGEN_IDEMPOTENT", "true");
119 /// Emitter::new().emit()?;
120 /// # Ok(())
121 /// # }();
122 /// # });
123 /// # Ok(())
124 /// # }
125 /// ```
126 ///
127 pub fn idempotent(&mut self) -> &mut Self {
128 self.idempotent = true;
129 self
130 }
131
132 /// Enable the `default_on_error` feature
133 ///
134 /// **NOTE** - This feature can also be enabled via the `VERGEN_DEFAULT_ON_ERROR`
135 /// environment variable.
136 ///
137 /// By default, if `vergen` cannot generate a requested instruction (for example
138 /// when you configure `VERGEN_GIT_*` instructions but build from a source tarball
139 /// with no `.git` directory), the variable is left **unset** and only a
140 /// [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning)
141 /// is emitted. This means a downstream `env!("VERGEN_GIT_SHA")` will fail to
142 /// compile.
143 ///
144 /// When this feature is enabled, those un-generatable instructions are instead
145 /// populated with the idempotent default value (`VERGEN_IDEMPOTENT_OUTPUT`), so
146 /// `env!()` keeps compiling. Unlike [`idempotent`](Self::idempotent), this only
147 /// affects the fallback path: when the instructions *can* be generated (e.g. inside
148 /// a real git worktree) you still get the real values.
149 ///
150 /// # Example
151 ///
152 /// ```
153 /// # use anyhow::Result;
154 /// # use vergen_lib::Emitter;
155 /// #
156 /// # fn main() -> Result<()> {
157 /// Emitter::new().default_on_error().emit()?;
158 /// // or
159 /// # temp_env::with_var("VERGEN_DEFAULT_ON_ERROR", Some("true"), || {
160 /// # let result = || -> Result<()> {
161 /// // set::env("VERGEN_DEFAULT_ON_ERROR", "true");
162 /// Emitter::new().emit()?;
163 /// # Ok(())
164 /// # }();
165 /// # });
166 /// # Ok(())
167 /// # }
168 /// ```
169 ///
170 pub fn default_on_error(&mut self) -> &mut Self {
171 self.default_on_error = true;
172 self
173 }
174
175 /// Enable the `fail_on_error` feature
176 ///
177 /// By default `vergen` will emit the instructions you requested. If for some
178 /// reason those instructions cannot be generated correctly, the affected variables
179 /// are left unset (unless [`idempotent`](Self::idempotent) or
180 /// [`default_on_error`](Self::default_on_error) is enabled, in which case they are
181 /// populated with the idempotent default value). `vergen` will also emit
182 /// [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning)
183 /// instructions notifying you this has happened.
184 ///
185 /// For example, if you configure `vergen` to emit `VERGEN_GIT_*` instructions and
186 /// you run a build from a source tarball with no `.git` directory, the instructions
187 /// will be skipped (or defaulted, per above), rather than information gleaned through
188 /// git.
189 ///
190 /// When `fail_on_error` is enabled, that situation produces a hard error instead.
191 ///
192 /// # Example
193 ///
194 /// ```
195 /// # use anyhow::Result;
196 /// # use vergen_lib::Emitter;
197 /// #
198 /// # fn main() -> Result<()> {
199 /// Emitter::new().fail_on_error().emit()?;
200 /// # Ok(())
201 /// # }
202 /// ```
203 pub fn fail_on_error(&mut self) -> &mut Self {
204 self.fail_on_error = true;
205 self
206 }
207
208 /// Enable the quiet feature
209 ///
210 /// Suppress the emission of the `cargo:warning` instructions.
211 ///
212 /// # Example
213 ///
214 /// ```
215 /// # use anyhow::Result;
216 /// # use vergen_lib::Emitter;
217 /// #
218 /// # fn main() -> Result<()> {
219 /// Emitter::new().quiet().emit()?;
220 /// # Ok(())
221 /// # }
222 /// ```
223 pub fn quiet(&mut self) -> &mut Self {
224 self.quiet = true;
225 self
226 }
227
228 /// Set a custom build.rs path if you are using a non-standard path
229 ///
230 /// By default `vergen` will use `build.rs` as the build path for the
231 /// `cargo:rerun-if-changed` emit. You can specify a custom `build.rs`
232 /// path here if you have changed this default
233 ///
234 /// # Example
235 ///
236 /// ```
237 /// # use anyhow::Result;
238 /// # use vergen_lib::Emitter;
239 /// #
240 /// # fn main() -> Result<()> {
241 /// Emitter::new().custom_build_rs("my/custom/build.rs").emit()?;
242 /// # Ok(())
243 /// # }
244 /// ```
245 pub fn custom_build_rs(&mut self, path: &'static str) -> &mut Self {
246 self.custom_buildrs = Some(path);
247 self
248 }
249
250 /// Add a set of instructions to the emitter output
251 ///
252 /// # Errors
253 ///
254 pub fn add_instructions(&mut self, entries: &dyn AddEntries) -> Result<&mut Self> {
255 entries
256 .add_map_entries(
257 self.idempotent,
258 &mut self.cargo_rustc_env_map,
259 &mut self.cargo_rerun_if_changed,
260 &mut self.cargo_warning,
261 )
262 .or_else(|e| {
263 let default_config = DefaultConfig::new(
264 self.idempotent || self.default_on_error,
265 self.fail_on_error,
266 e,
267 );
268 entries.add_default_entries(
269 &default_config,
270 &mut self.cargo_rustc_env_map,
271 &mut self.cargo_rerun_if_changed,
272 &mut self.cargo_warning,
273 )
274 })?;
275 Ok(self)
276 }
277
278 /// Add a set of custom instructions to the emitter output
279 ///
280 /// # Errors
281 ///
282 /// Errors may be generated if `fail_on_error` has been configured.
283 ///
284 pub fn add_custom_instructions<K, V>(
285 &mut self,
286 custom_entries: &impl AddCustomEntries<K, V>,
287 ) -> Result<&mut Self>
288 where
289 K: Into<String> + Ord,
290 V: Into<String>,
291 {
292 let mut map = BTreeMap::default();
293 custom_entries
294 .add_calculated_entries(
295 self.idempotent,
296 &mut map,
297 &mut self.cargo_rerun_if_changed,
298 &mut self.cargo_warning,
299 )
300 .or_else(|e| {
301 let default_config = DefaultConfig::new(
302 self.idempotent || self.default_on_error,
303 self.fail_on_error,
304 e,
305 );
306 custom_entries.add_default_entries(
307 &default_config,
308 &mut map,
309 &mut self.cargo_rerun_if_changed,
310 &mut self.cargo_warning,
311 )
312 })?;
313 self.cargo_rustc_env_map_custom.extend(Self::map_into(map));
314 Ok(self)
315 }
316
317 fn map_into<K, V>(map: BTreeMap<K, V>) -> impl Iterator<Item = (String, String)>
318 where
319 K: Into<String> + Ord,
320 V: Into<String>,
321 {
322 map.into_iter().map(|(k, v)| (k.into(), v.into()))
323 }
324
325 fn emit_output<T>(&self, stdout: &mut T) -> Result<()>
326 where
327 T: Write,
328 {
329 self.emit_instructions(stdout)
330 }
331
332 fn emit_instructions<T>(&self, stdout: &mut T) -> Result<()>
333 where
334 T: Write,
335 {
336 // Emit the 'cargo:rustc-env' instructions
337 for (k, v) in &self.cargo_rustc_env_map {
338 let sanitized_value = Self::filter_newlines(v);
339 writeln!(stdout, "cargo:rustc-env={}={sanitized_value}", k.name())?;
340 }
341
342 // Emit the 'cargo:rustc-env' custom instructions
343 for (k, v) in &self.cargo_rustc_env_map_custom {
344 let sanitized_value = Self::filter_newlines(v);
345 writeln!(stdout, "cargo:rustc-env={k}={sanitized_value}")?;
346 }
347
348 // Emit the `cargo:warning` instructions
349 if !self.quiet {
350 for warning in &self.cargo_warning {
351 let sanitized_output = Self::filter_newlines(warning);
352 writeln!(stdout, "cargo:warning={sanitized_output}")?;
353 }
354 }
355
356 // Emit the 'cargo:rerun-if-changed' instructions for the git paths (if added)
357 for path in &self.cargo_rerun_if_changed {
358 let sanitized_output = Self::filter_newlines(path);
359 writeln!(stdout, "cargo:rerun-if-changed={sanitized_output}")?;
360 }
361
362 // Emit the 'cargo:rerun-if-changed' instructions
363 if !self.cargo_rustc_env_map.is_empty() || !self.cargo_warning.is_empty() {
364 let buildrs = self.custom_buildrs.unwrap_or("build.rs");
365 let sanitized_output = Self::filter_newlines(buildrs);
366 writeln!(stdout, "cargo:rerun-if-changed={sanitized_output}")?;
367 writeln!(stdout, "cargo:rerun-if-env-changed=VERGEN_IDEMPOTENT")?;
368 writeln!(stdout, "cargo:rerun-if-env-changed=VERGEN_DEFAULT_ON_ERROR")?;
369 writeln!(stdout, "cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH")?;
370 }
371 Ok(())
372 }
373
374 fn filter_newlines(s: &str) -> String {
375 s.chars().filter(|c| *c != '\n').collect()
376 }
377
378 /// Emit cargo instructions from your build script
379 ///
380 /// - 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.
381 /// - 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.
382 /// - Can emit [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning) outputs if the
383 /// [`fail_on_error`](Self::fail_on_error) feature is not enabled and the requested variable is defaulted through error or
384 /// the [`idempotent`](Self::idempotent) flag.
385 ///
386 /// # Errors
387 /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
388 ///
389 /// # Example
390 ///
391 /// ```
392 /// # use anyhow::Result;
393 /// # use std::env;
394 /// # use vergen_lib::Emitter;
395 /// #
396 /// # fn main() -> Result<()> {
397 /// let emitter = Emitter::default().emit()?;
398 /// # Ok(())
399 /// # }
400 /// ```
401 ///
402 /// # Sample Output
403 ///
404 /// **NOTE** - You won't see this output unless you invoke cargo with the `-vv` flag.
405 /// The instruction output is not displayed by default.
406 ///
407 /// ```text
408 /// cargo:rustc-env=VERGEN_BUILD_DATE=2023-01-04
409 /// cargo:rustc-env=VERGEN_BUILD_TIMESTAMP=2023-01-04T15:38:11.097507114Z
410 /// cargo:rustc-env=VERGEN_CARGO_DEBUG=true
411 /// cargo:rustc-env=VERGEN_CARGO_FEATURES=build,git
412 /// cargo:rustc-env=VERGEN_CARGO_OPT_LEVEL=1
413 /// cargo:rustc-env=VERGEN_CARGO_TARGET_TRIPLE=x86_64-unknown-linux-gnu
414 /// cargo:rustc-env=VERGEN_GIT_BRANCH=feature/version8
415 /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_EMAIL=your@email.com
416 /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_NAME=Yoda
417 /// cargo:rustc-env=VERGEN_GIT_COMMIT_COUNT=476
418 /// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2023-01-03
419 /// cargo:rustc-env=VERGEN_GIT_COMMIT_MESSAGE=The best message
420 /// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2023-01-03T14:08:12.000000000-05:00
421 /// cargo:rustc-env=VERGEN_GIT_DESCRIBE=7.4.4-103-g53ae8a6
422 /// cargo:rustc-env=VERGEN_GIT_SHA=53ae8a69ab7917a2909af40f2e5d015f5b29ae28
423 /// cargo:rustc-env=VERGEN_RUSTC_CHANNEL=nightly
424 /// cargo:rustc-env=VERGEN_RUSTC_COMMIT_DATE=2023-01-03
425 /// cargo:rustc-env=VERGEN_RUSTC_COMMIT_HASH=c7572670a1302f5c7e245d069200e22da9df0316
426 /// cargo:rustc-env=VERGEN_RUSTC_HOST_TRIPLE=x86_64-unknown-linux-gnu
427 /// cargo:rustc-env=VERGEN_RUSTC_LLVM_VERSION=15.0
428 /// cargo:rustc-env=VERGEN_RUSTC_SEMVER=1.68.0-nightly
429 /// cargo:rustc-env=VERGEN_SYSINFO_NAME=Arch Linux
430 /// cargo:rustc-env=VERGEN_SYSINFO_OS_VERSION=Linux Arch Linux
431 /// cargo:rustc-env=VERGEN_SYSINFO_USER=jozias
432 /// cargo:rustc-env=VERGEN_SYSINFO_TOTAL_MEMORY=31 GiB
433 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_VENDOR=AuthenticAMD
434 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_CORE_COUNT=8
435 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_NAME=cpu0,cpu1,cpu2,cpu3,cpu4,cpu5,cpu6,cpu7
436 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_BRAND=AMD Ryzen Threadripper 1900X 8-Core Processor
437 /// cargo:rustc-env=VERGEN_SYSINFO_CPU_FREQUENCY=3792
438 /// cargo:rerun-if-changed=.git/HEAD
439 /// cargo:rerun-if-changed=.git/refs/heads/feature/version8
440 /// cargo:rerun-if-changed=build.rs
441 /// cargo:rerun-if-env-changed=VERGEN_IDEMPOTENT
442 /// cargo:rerun-if-env-changed=VERGEN_DEFAULT_ON_ERROR
443 /// cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH
444 /// ```
445 ///
446 pub fn emit(&self) -> Result<()> {
447 self.emit_output(&mut io::stdout())
448 }
449
450 /// Emit cargo instructions from your build script and set environment variables for use in `build.rs`
451 ///
452 /// - 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.
453 /// - 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.
454 /// - Can emit [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning) outputs if the
455 /// [`fail_on_error`](Self::fail_on_error) feature is not enabled and the requested variable is defaulted through error or
456 /// the [`idempotent`](Self::idempotent) flag.
457 ///
458 /// # Errors
459 /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
460 ///
461 /// # Example
462 ///
463 /// ```
464 /// # use anyhow::Result;
465 /// # use std::env;
466 /// # use vergen_lib::Emitter;
467 /// #
468 /// # fn main() -> Result<()> {
469 /// Emitter::new().emit_and_set()?;
470 /// # Ok(())
471 /// # }
472 /// ```
473 ///
474 #[cfg(feature = "emit_and_set")]
475 #[cfg_attr(coverage_nightly, coverage(off))]
476 pub fn emit_and_set(&self) -> Result<()> {
477 self.emit_output(&mut io::stdout()).map(|()| {
478 for (k, v) in &self.cargo_rustc_env_map {
479 if env::var(k.name()).is_err() {
480 unsafe {
481 env::set_var(k.name(), v);
482 }
483 }
484 }
485 })
486 }
487
488 #[doc(hidden)]
489 /// Emit the cargo build script instructions to the given [`Write`](std::io::Write).
490 ///
491 /// **NOTE** - This is generally only used for testing and probably shouldn't be used
492 /// within a `build.rs` file.
493 ///
494 /// # Errors
495 /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
496 ///
497 pub fn emit_to<T>(&self, stdout: &mut T) -> Result<bool>
498 where
499 T: Write,
500 {
501 self.emit_output(stdout).map(|()| false)
502 }
503
504 #[doc(hidden)]
505 #[must_use]
506 pub fn test_emit(&self) -> Emitter {
507 self.clone()
508 }
509}
510
511#[cfg(test)]
512pub(crate) mod test {
513 use super::Emitter;
514 use anyhow::Result;
515 use serial_test::serial;
516 use std::io::Write;
517
518 #[test]
519 #[serial]
520 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
521 fn emitter_clone_works() {
522 let emitter = Emitter::default();
523 let another = emitter.clone();
524 assert_eq!(another, emitter);
525 }
526
527 #[test]
528 #[serial]
529 fn emitter_debug_works() -> Result<()> {
530 let emitter = Emitter::default();
531 let mut buf = vec![];
532 write!(buf, "{emitter:?}")?;
533 assert!(!buf.is_empty());
534 Ok(())
535 }
536
537 #[test]
538 #[serial]
539 fn default_is_no_emit() -> Result<()> {
540 let mut stdout_buf = vec![];
541 _ = Emitter::new().emit_to(&mut stdout_buf)?;
542 assert!(stdout_buf.is_empty());
543 Ok(())
544 }
545
546 #[test]
547 #[serial]
548 fn default_emit_is_ok() {
549 assert!(Emitter::new().emit().is_ok());
550 }
551
552 #[test]
553 #[serial]
554 fn default_on_error_emit_is_ok() {
555 assert!(Emitter::new().default_on_error().emit().is_ok());
556 }
557
558 #[test]
559 #[serial]
560 fn default_on_error_populates_fallback() -> Result<()> {
561 use crate::entries::test_gen::CustomInsGen;
562 let custom = CustomInsGen::builder().fail(true).build();
563 let mut stdout_buf = vec![];
564 _ = Emitter::new()
565 .default_on_error()
566 .add_custom_instructions(&custom)?
567 .emit_to(&mut stdout_buf)?;
568 let output = String::from_utf8_lossy(&stdout_buf);
569 assert!(output.contains("VERGEN_IDEMPOTENT_OUTPUT"));
570 Ok(())
571 }
572}