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_]*)"));