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}