Skip to main content

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}