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