Skip to main content

anodizer_core/template/
vars.rs

1use regex::Regex;
2use std::collections::HashMap;
3use std::sync::LazyLock;
4use tera::Value;
5
6#[derive(Clone)]
7pub struct TemplateVars {
8    pub(super) vars: HashMap<String, String>,
9    pub(super) env: HashMap<String, String>,
10    /// Env vars explicitly configured by the user (config `env:`, `.env` files,
11    /// workspace `env:`).  These are safe to serialize into split contexts and
12    /// inject into subprocess commands.  Process-inherited env vars (HOME, PATH,
13    /// USER, etc.) live only in `env` for template rendering — they must NOT be
14    /// forwarded to subprocesses (which inherit them naturally) or serialized
15    /// across platforms (macOS HOME poisons Linux builds).
16    pub(super) config_env: HashMap<String, String>,
17    /// Custom user-defined variables accessible as {{ .Var.key }}.
18    pub(super) custom_vars: HashMap<String, String>,
19    /// Pipeline outputs map accessible as {{ .Outputs.key }}.
20    /// Stages can populate this and templates can read it.
21    /// Similar to `.Var.*` but for pipeline outputs rather than user config.
22    /// Concrete stage->key mappings will be added as stages are enhanced
23    /// (e.g. build_id, checksum, etc.).
24    pub(super) outputs: HashMap<String, String>,
25    /// Structured values (arrays, objects) inserted into the Tera context as-is.
26    /// Used for complex template variables like `Artifacts` (list of maps) and
27    /// `Metadata` (nested map) that cannot be represented as flat strings.
28    pub(super) structured: HashMap<String, Value>,
29}
30
31impl TemplateVars {
32    pub fn new() -> Self {
33        Self {
34            vars: HashMap::new(),
35            env: HashMap::new(),
36            config_env: HashMap::new(),
37            custom_vars: HashMap::new(),
38            outputs: HashMap::new(),
39            structured: HashMap::new(),
40        }
41    }
42
43    pub fn set(&mut self, key: &str, value: &str) {
44        self.vars.insert(key.to_string(), value.to_string());
45    }
46
47    /// Remove a regular template variable. Returns `true` if the key was
48    /// present. Use when a value is logically *undefined* for downstream
49    /// renders — distinct from `set(key, "")` which keeps the key with an
50    /// empty string. Strict-mode template rendering can distinguish defined-
51    /// empty from undefined; the latter is the correct shape for per-config
52    /// vars (e.g. `BaseImage`) that should not bleed across iterations.
53    pub fn unset(&mut self, key: &str) -> bool {
54        self.vars.remove(key).is_some()
55    }
56
57    /// Remove a structured (non-string) template variable. Mirrors `unset`
58    /// for the structured map. Returns `true` if the key was present.
59    pub fn unset_structured(&mut self, key: &str) -> bool {
60        self.structured.remove(key).is_some()
61    }
62
63    pub fn get(&self, key: &str) -> Option<&String> {
64        self.vars.get(key)
65    }
66
67    pub fn set_env(&mut self, key: &str, value: &str) {
68        self.env.insert(key.to_string(), value.to_string());
69    }
70
71    /// Set an env var that was explicitly configured by the user.
72    /// Also adds it to the general env map for template rendering.
73    pub fn set_config_env(&mut self, key: &str, value: &str) {
74        self.env.insert(key.to_string(), value.to_string());
75        self.config_env.insert(key.to_string(), value.to_string());
76    }
77
78    pub fn set_custom_var(&mut self, key: &str, value: &str) {
79        self.custom_vars.insert(key.to_string(), value.to_string());
80    }
81
82    /// Set a pipeline output value accessible as `{{ .Outputs.key }}`.
83    ///
84    /// Infrastructure: no stage populates Outputs yet. Concrete key mappings
85    /// will be added as individual stages are enhanced (e.g. build -> build_id).
86    pub fn set_output(&mut self, key: &str, value: &str) {
87        self.outputs.insert(key.to_string(), value.to_string());
88    }
89
90    /// Get a pipeline output value by key.
91    pub fn get_output(&self, key: &str) -> Option<&String> {
92        self.outputs.get(key)
93    }
94
95    /// Set a structured (non-string) value accessible directly in Tera context.
96    /// Used for complex types like arrays of maps (`Artifacts`) or nested maps
97    /// (`Metadata`) that cannot be represented as flat key=value strings.
98    pub fn set_structured(&mut self, key: &str, value: Value) {
99        self.structured.insert(key.to_string(), value);
100    }
101
102    /// Return all template variables (excluding env and custom vars).
103    pub fn all(&self) -> &HashMap<String, String> {
104        &self.vars
105    }
106
107    /// Return all environment variables (process + config).
108    /// Used for template rendering ({{ .Env.* }}).
109    pub fn all_env(&self) -> &HashMap<String, String> {
110        &self.env
111    }
112
113    /// Return only explicitly configured env vars (config `env:`, `.env` files).
114    /// Safe to serialize into split contexts and inject into subprocesses.
115    /// Process-inherited vars (HOME, PATH, etc.) are excluded — subprocesses
116    /// inherit them naturally, and serializing them across platforms is poison
117    /// (macOS HOME=/Users/runner breaks Linux docker builds).
118    pub fn all_config_env(&self) -> &HashMap<String, String> {
119        &self.config_env
120    }
121
122    /// Get a structured (non-string) template variable by key.
123    /// Returns `None` if the key does not exist in the structured map.
124    pub fn get_structured(&self, key: &str) -> Option<&tera::Value> {
125        self.structured.get(key)
126    }
127
128    /// Return all structured template variables.
129    pub fn all_structured(&self) -> &HashMap<String, Value> {
130        &self.structured
131    }
132}
133
134impl Default for TemplateVars {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140/// Clear per-target template variables (`Os`, `Arch`, `Target`, `Arm`,
141/// `Arm64`, `Amd64`, `Mips`, `I386`) so they don't leak to downstream
142/// stages after a packaging stage's per-target loop finishes.
143///
144/// Packaging stages (flatpak, snapcraft, nfpm, makeself, etc.) iterate
145/// over (config × target) tuples and set these vars once per iteration so
146/// user templates like `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}`
147/// render correctly. Leaving a stale `Os=linux` value set when a later
148/// stage (announce, publish) renders its own templates causes subtle
149/// cross-stage leaks — the announcement for a multi-platform release gets
150/// tagged with whichever platform finished last.
151pub fn clear_per_target_vars(tv: &mut TemplateVars) {
152    for key in PER_TARGET_VARS {
153        tv.set(key, "");
154    }
155}
156
157/// The template-variable keys that per-target packaging loops populate
158/// and must clear on exit.
159///
160/// Mirrors GoReleaser's `internal/tmpl/tmpl.go` per-artifact key set
161/// (`KeyOS`, `KeyArch`, `KeyAmd64`, `Key386`, `KeyArm`, `KeyArm64`, `KeyMips`,
162/// `KeyPpc64`, `KeyRiscv64` plus `target`). Keeping the set in sync keeps
163/// templates that branch on `{{ .Ppc64 }}` / `{{ .Riscv64 }}` from raising
164/// a Tera "missing key" error in strict-mode rendering.
165pub const PER_TARGET_VARS: &[&str] = &[
166    "Os", "Arch", "Target", "Arm", "Arm64", "Amd64", "Mips", "I386", "Ppc64", "Riscv64",
167];
168
169/// Per-artifact template variable keys (set inside per-artifact loops in
170/// stage-sbom, stage-sign, stage-checksum). Bundled into a constant so the
171/// "set, render, clear" pattern stays in one place — when an additional var
172/// gets added (e.g. `ArtifactPath`), every consumer picks it up.
173pub const PER_ARTIFACT_VARS: &[&str] = &["ArtifactName", "ArtifactExt", "ArtifactID"];
174
175/// Clear both `PER_TARGET_VARS` and `PER_ARTIFACT_VARS` on exit from a
176/// per-artifact loop. Mirrors `clear_per_target_vars` but covers the larger
177/// surface that sbom/sign/checksum loops touch — preventing the "stale
178/// ArtifactName from sbom run leaking into announce" class of bug.
179pub fn clear_per_artifact_vars(tv: &mut TemplateVars) {
180    clear_per_target_vars(tv);
181    for key in PER_ARTIFACT_VARS {
182        tv.set(key, "");
183    }
184}
185
186/// Known numeric template fields that should be inserted as integers into the
187/// Tera context so that numeric comparisons like `{% if Major == 1 %}` work
188/// correctly. Without this, they would be strings and `"1" != 1`.
189pub(super) const NUMERIC_FIELDS: &[&str] =
190    &["Major", "Minor", "Patch", "Timestamp", "CommitTimestamp"];
191
192/// Regex matching `Env.VARNAME` references in a preprocessed template.
193/// Used to discover env var keys referenced by the template so they can be
194/// pre-populated with empty strings (GoReleaser returns "" for missing env vars).
195pub(super) static ENV_REF_RE: LazyLock<Regex> =
196    LazyLock::new(|| crate::util::static_regex(r"Env\.([A-Za-z_][A-Za-z0-9_]*)"));