Skip to main content

ci_engine/
env.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Hermetic check environment construction.
3
4use std::collections::BTreeMap;
5
6/// Host variables needed to locate ordinary POSIX/Rust tooling.
7pub const BASE_ALLOWLIST: &[&str] = &[
8    "PATH",
9    "HOME",
10    "USER",
11    "SHELL",
12    "TERM",
13    "LANG",
14    "LC_ALL",
15    "CARGO_HOME",
16    "RUSTUP_HOME",
17];
18/// Deterministic Git author/committer name.
19pub const GIT_IDENTITY_NAME: &str = "heddle ci";
20/// Deterministic Git author/committer email.
21pub const GIT_IDENTITY_EMAIL: &str = "ci@heddle.invalid";
22
23/// Builder for the exact environment both executed and recorded in `repro`.
24#[derive(Debug, Clone)]
25pub struct HermeticEnv {
26    git_hermetic: bool,
27    host: BTreeMap<String, String>,
28}
29
30impl HermeticEnv {
31    /// Capture the allowed variables from the current process.
32    #[must_use]
33    pub fn new() -> Self {
34        let host = BASE_ALLOWLIST
35            .iter()
36            .filter_map(|name| {
37                std::env::var(name)
38                    .ok()
39                    .map(|value| ((*name).to_string(), value))
40            })
41            .collect();
42        Self {
43            git_hermetic: true,
44            host,
45        }
46    }
47
48    /// Construct from an explicit host map, primarily for tests.
49    #[must_use]
50    pub fn with_host(host: BTreeMap<String, String>) -> Self {
51        Self {
52            git_hermetic: true,
53            host,
54        }
55    }
56
57    /// Enable or disable deterministic Git configuration.
58    #[must_use]
59    pub fn git_hermetic(mut self, enabled: bool) -> Self {
60        self.git_hermetic = enabled;
61        self
62    }
63
64    /// Produce the sorted effective environment.
65    #[must_use]
66    pub fn build(
67        &self,
68        check: &BTreeMap<String, String>,
69        services: &BTreeMap<String, String>,
70        caches: &BTreeMap<String, String>,
71    ) -> BTreeMap<String, String> {
72        let mut output = self.host.clone();
73        if self.git_hermetic {
74            output.insert("GIT_CONFIG_GLOBAL".into(), "/dev/null".into());
75            output.insert("GIT_CONFIG_SYSTEM".into(), "/dev/null".into());
76            output.insert("GIT_AUTHOR_NAME".into(), GIT_IDENTITY_NAME.into());
77            output.insert("GIT_AUTHOR_EMAIL".into(), GIT_IDENTITY_EMAIL.into());
78            output.insert("GIT_COMMITTER_NAME".into(), GIT_IDENTITY_NAME.into());
79            output.insert("GIT_COMMITTER_EMAIL".into(), GIT_IDENTITY_EMAIL.into());
80        }
81        for source in [services, caches, check] {
82            output.extend(
83                source
84                    .iter()
85                    .map(|(key, value)| (key.clone(), value.clone())),
86            );
87        }
88        output
89    }
90}
91
92impl Default for HermeticEnv {
93    fn default() -> Self {
94        Self::new()
95    }
96}