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, entries: &dyn AddEntries) -> Result<&mut Self> {
198        entries
199            .add_map_entries(
200                self.idempotent,
201                &mut self.cargo_rustc_env_map,
202                &mut self.cargo_rerun_if_changed,
203                &mut self.cargo_warning,
204            )
205            .or_else(|e| {
206                let default_config = DefaultConfig::new(self.idempotent, self.fail_on_error, e);
207                entries.add_default_entries(
208                    &default_config,
209                    &mut self.cargo_rustc_env_map,
210                    &mut self.cargo_rerun_if_changed,
211                    &mut self.cargo_warning,
212                )
213            })?;
214        Ok(self)
215    }
216
217    /// Add a set of custom instructions to the emitter output
218    ///
219    /// # Errors
220    ///
221    /// Errors may be generated if `fail_on_error` has been configured.
222    ///
223    pub fn add_custom_instructions<K, V>(
224        &mut self,
225        custom_entries: &impl AddCustomEntries<K, V>,
226    ) -> Result<&mut Self>
227    where
228        K: Into<String> + Ord,
229        V: Into<String>,
230    {
231        let mut map = BTreeMap::default();
232        custom_entries
233            .add_calculated_entries(
234                self.idempotent,
235                &mut map,
236                &mut self.cargo_rerun_if_changed,
237                &mut self.cargo_warning,
238            )
239            .or_else(|e| {
240                let default_config = DefaultConfig::new(self.idempotent, self.fail_on_error, e);
241                custom_entries.add_default_entries(
242                    &default_config,
243                    &mut map,
244                    &mut self.cargo_rerun_if_changed,
245                    &mut self.cargo_warning,
246                )
247            })?;
248        self.cargo_rustc_env_map_custom.extend(Self::map_into(map));
249        Ok(self)
250    }
251
252    fn map_into<K, V>(map: BTreeMap<K, V>) -> impl Iterator<Item = (String, String)>
253    where
254        K: Into<String> + Ord,
255        V: Into<String>,
256    {
257        map.into_iter().map(|(k, v)| (k.into(), v.into()))
258    }
259
260    fn emit_output<T>(&self, stdout: &mut T) -> Result<()>
261    where
262        T: Write,
263    {
264        self.emit_instructions(stdout)
265    }
266
267    fn emit_instructions<T>(&self, stdout: &mut T) -> Result<()>
268    where
269        T: Write,
270    {
271        // Emit the 'cargo:rustc-env' instructions
272        for (k, v) in &self.cargo_rustc_env_map {
273            let sanitized_value = Self::filter_newlines(v);
274            writeln!(stdout, "cargo:rustc-env={}={sanitized_value}", k.name())?;
275        }
276
277        // Emit the 'cargo:rustc-env' custom instructions
278        for (k, v) in &self.cargo_rustc_env_map_custom {
279            let sanitized_value = Self::filter_newlines(v);
280            writeln!(stdout, "cargo:rustc-env={k}={sanitized_value}")?;
281        }
282
283        // Emit the `cargo:warning` instructions
284        if !self.quiet {
285            for warning in &self.cargo_warning {
286                let sanitized_output = Self::filter_newlines(warning);
287                writeln!(stdout, "cargo:warning={sanitized_output}")?;
288            }
289        }
290
291        // Emit the 'cargo:rerun-if-changed' instructions for the git paths (if added)
292        for path in &self.cargo_rerun_if_changed {
293            let sanitized_output = Self::filter_newlines(path);
294            writeln!(stdout, "cargo:rerun-if-changed={sanitized_output}")?;
295        }
296
297        // Emit the 'cargo:rerun-if-changed' instructions
298        if !self.cargo_rustc_env_map.is_empty() || !self.cargo_warning.is_empty() {
299            let buildrs = self.custom_buildrs.unwrap_or("build.rs");
300            let sanitized_output = Self::filter_newlines(buildrs);
301            writeln!(stdout, "cargo:rerun-if-changed={sanitized_output}")?;
302            writeln!(stdout, "cargo:rerun-if-env-changed=VERGEN_IDEMPOTENT")?;
303            writeln!(stdout, "cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH")?;
304        }
305        Ok(())
306    }
307
308    fn filter_newlines(s: &str) -> String {
309        s.chars().filter(|c| *c != '\n').collect()
310    }
311
312    /// Emit cargo instructions from your build script
313    ///
314    /// - 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.
315    /// - 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.
316    /// - Can emit [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning) outputs if the
317    ///   [`fail_on_error`](Self::fail_on_error) feature is not enabled and the requested variable is defaulted through error or
318    ///   the [`idempotent`](Self::idempotent) flag.
319    ///
320    /// # Errors
321    /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
322    ///
323    /// # Example
324    ///
325    /// ```
326    /// # use anyhow::Result;
327    /// # use std::env;
328    /// # use vergen_lib::Emitter;
329    /// #
330    /// # fn main() -> Result<()> {
331    /// let emitter = Emitter::default().emit()?;
332    /// #   Ok(())
333    /// # }
334    /// ```
335    ///
336    /// # Sample Output
337    ///
338    /// **NOTE** - You won't see this output unless you invoke cargo with the `-vv` flag.
339    /// The instruction output is not displayed by default.
340    ///
341    /// ```text
342    /// cargo:rustc-env=VERGEN_BUILD_DATE=2023-01-04
343    /// cargo:rustc-env=VERGEN_BUILD_TIMESTAMP=2023-01-04T15:38:11.097507114Z
344    /// cargo:rustc-env=VERGEN_CARGO_DEBUG=true
345    /// cargo:rustc-env=VERGEN_CARGO_FEATURES=build,git
346    /// cargo:rustc-env=VERGEN_CARGO_OPT_LEVEL=1
347    /// cargo:rustc-env=VERGEN_CARGO_TARGET_TRIPLE=x86_64-unknown-linux-gnu
348    /// cargo:rustc-env=VERGEN_GIT_BRANCH=feature/version8
349    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_EMAIL=your@email.com
350    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_NAME=Yoda
351    /// cargo:rustc-env=VERGEN_GIT_COMMIT_COUNT=476
352    /// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2023-01-03
353    /// cargo:rustc-env=VERGEN_GIT_COMMIT_MESSAGE=The best message
354    /// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2023-01-03T14:08:12.000000000-05:00
355    /// cargo:rustc-env=VERGEN_GIT_DESCRIBE=7.4.4-103-g53ae8a6
356    /// cargo:rustc-env=VERGEN_GIT_SHA=53ae8a69ab7917a2909af40f2e5d015f5b29ae28
357    /// cargo:rustc-env=VERGEN_RUSTC_CHANNEL=nightly
358    /// cargo:rustc-env=VERGEN_RUSTC_COMMIT_DATE=2023-01-03
359    /// cargo:rustc-env=VERGEN_RUSTC_COMMIT_HASH=c7572670a1302f5c7e245d069200e22da9df0316
360    /// cargo:rustc-env=VERGEN_RUSTC_HOST_TRIPLE=x86_64-unknown-linux-gnu
361    /// cargo:rustc-env=VERGEN_RUSTC_LLVM_VERSION=15.0
362    /// cargo:rustc-env=VERGEN_RUSTC_SEMVER=1.68.0-nightly
363    /// cargo:rustc-env=VERGEN_SYSINFO_NAME=Arch Linux
364    /// cargo:rustc-env=VERGEN_SYSINFO_OS_VERSION=Linux  Arch Linux
365    /// cargo:rustc-env=VERGEN_SYSINFO_USER=jozias
366    /// cargo:rustc-env=VERGEN_SYSINFO_TOTAL_MEMORY=31 GiB
367    /// cargo:rustc-env=VERGEN_SYSINFO_CPU_VENDOR=AuthenticAMD
368    /// cargo:rustc-env=VERGEN_SYSINFO_CPU_CORE_COUNT=8
369    /// cargo:rustc-env=VERGEN_SYSINFO_CPU_NAME=cpu0,cpu1,cpu2,cpu3,cpu4,cpu5,cpu6,cpu7
370    /// cargo:rustc-env=VERGEN_SYSINFO_CPU_BRAND=AMD Ryzen Threadripper 1900X 8-Core Processor
371    /// cargo:rustc-env=VERGEN_SYSINFO_CPU_FREQUENCY=3792
372    /// cargo:rerun-if-changed=.git/HEAD
373    /// cargo:rerun-if-changed=.git/refs/heads/feature/version8
374    /// cargo:rerun-if-changed=build.rs
375    /// cargo:rerun-if-env-changed=VERGEN_IDEMPOTENT
376    /// cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH
377    /// ```
378    ///
379    pub fn emit(&self) -> Result<()> {
380        self.emit_output(&mut io::stdout())
381    }
382
383    /// Emit cargo instructions from your build script and set environment variables for use in `build.rs`
384    ///
385    /// - 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.
386    /// - 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.
387    /// - Can emit [`cargo:warning`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning) outputs if the
388    ///   [`fail_on_error`](Self::fail_on_error) feature is not enabled and the requested variable is defaulted through error or
389    ///   the [`idempotent`](Self::idempotent) flag.
390    ///
391    /// # Errors
392    /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
393    ///
394    /// # Example
395    ///
396    /// ```
397    /// # use anyhow::Result;
398    /// # use std::env;
399    /// # use vergen_lib::Emitter;
400    /// #
401    /// # fn main() -> Result<()> {
402    /// Emitter::new().emit_and_set()?;
403    /// #   Ok(())
404    /// # }
405    /// ```
406    ///
407    #[cfg(feature = "emit_and_set")]
408    #[cfg_attr(coverage_nightly, coverage(off))]
409    pub fn emit_and_set(&self) -> Result<()> {
410        self.emit_output(&mut io::stdout()).map(|()| {
411            for (k, v) in &self.cargo_rustc_env_map {
412                if env::var(k.name()).is_err() {
413                    unsafe {
414                        env::set_var(k.name(), v);
415                    }
416                }
417            }
418        })
419    }
420
421    #[doc(hidden)]
422    /// Emit the cargo build script instructions to the given [`Write`](std::io::Write).
423    ///
424    /// **NOTE** - This is generally only used for testing and probably shouldn't be used
425    /// within a `build.rs` file.
426    ///
427    /// # Errors
428    /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
429    ///
430    pub fn emit_to<T>(&self, stdout: &mut T) -> Result<bool>
431    where
432        T: Write,
433    {
434        self.emit_output(stdout).map(|()| false)
435    }
436
437    #[doc(hidden)]
438    #[must_use]
439    pub fn test_emit(&self) -> Emitter {
440        self.clone()
441    }
442}
443
444#[cfg(test)]
445pub(crate) mod test {
446    use super::Emitter;
447    use anyhow::Result;
448    use serial_test::serial;
449    use std::io::Write;
450
451    #[test]
452    #[serial]
453    #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
454    fn emitter_clone_works() {
455        let emitter = Emitter::default();
456        let another = emitter.clone();
457        assert_eq!(another, emitter);
458    }
459
460    #[test]
461    #[serial]
462    fn emitter_debug_works() -> Result<()> {
463        let emitter = Emitter::default();
464        let mut buf = vec![];
465        write!(buf, "{emitter:?}")?;
466        assert!(!buf.is_empty());
467        Ok(())
468    }
469
470    #[test]
471    #[serial]
472    fn default_is_no_emit() -> Result<()> {
473        let mut stdout_buf = vec![];
474        _ = Emitter::new().emit_to(&mut stdout_buf)?;
475        assert!(stdout_buf.is_empty());
476        Ok(())
477    }
478
479    #[test]
480    #[serial]
481    fn default_emit_is_ok() {
482        assert!(Emitter::new().emit().is_ok());
483    }
484}