Skip to main content

anodizer_core/context/
populate.rs

1use super::*;
2
3impl Context {
4    /// Populate template variables from `self.git_info`.
5    ///
6    /// Must be called after `self.git_info` is set. Sets the following vars:
7    /// - `Tag`, `Version`, `RawVersion` — tag and version strings
8    /// - `Major`, `Minor`, `Patch` — semver components
9    /// - `Prerelease` — prerelease suffix (or empty)
10    /// - `BuildMetadata` — build metadata from semver tag (or empty)
11    /// - `FullCommit`, `Commit` — full commit SHA (`Commit` is alias for `FullCommit`)
12    /// - `ShortCommit` — abbreviated commit SHA
13    /// - `Branch` — current git branch
14    /// - `CommitDate` — ISO 8601 author date of HEAD commit
15    /// - `CommitTimestamp` — unix timestamp of HEAD commit
16    /// - `IsGitDirty` — "true"/"false"
17    /// - `IsGitClean` — "true"/"false" (inverse of `IsGitDirty`)
18    /// - `GitTreeState` — "clean"/"dirty"
19    /// - `GitURL` — git remote URL
20    /// - `Summary` — git describe summary
21    /// - `TagSubject` — annotated tag subject or commit subject
22    /// - `TagContents` — full annotated tag message or commit message
23    /// - `TagBody` — tag message body or commit message body
24    /// - `IsSnapshot` — from context options
25    /// - `IsNightly` — from context options
26    /// - `IsDraft` — "false" (stages may override to "true")
27    /// - `IsSingleTarget` — "true"/"false" based on single_target option
28    /// - `PreviousTag` — previous matching tag, stripped in monorepo mode (or empty)
29    /// - `PrefixedTag` — full tag with monorepo prefix, or tag_prefix-prepended (Pro addition)
30    /// - `PrefixedPreviousTag` — full previous tag with prefix (Pro addition)
31    /// - `PrefixedSummary` — full summary with prefix (Pro addition)
32    /// - `IsRelease` — "true" if not snapshot and not nightly (Pro addition)
33    /// - `IsMerging` — "true" if running with --merge flag (Pro addition)
34    ///
35    /// **Stage-scoped variables** (NOT set here; set per-artifact during stage execution):
36    /// - `Binary` — binary name, set by build stage per binary and archive stage per archive
37    /// - `ArtifactName` — output artifact filename, set by archive stage after creating each archive
38    /// - `ArtifactPath` — absolute path to artifact, set by archive stage after creating each archive
39    /// - `ArtifactExt` — artifact file extension (e.g. `.tar.gz`, `.exe`), set alongside ArtifactName
40    /// - `ArtifactID` — build config `id` field, set by build stage per build config
41    /// - `Os` — target OS, set by archive/nfpm stages per target
42    /// - `Arch` — target architecture, set by archive/nfpm stages per target
43    /// - `Target` — full target triple (e.g. `x86_64-unknown-linux-gnu`), set alongside Os/Arch
44    /// - `Checksums` — combined checksum file contents, set by checksum stage
45    pub fn populate_git_vars(&mut self) {
46        if let Some(ref info) = self.git_info {
47            // The version-derived var block (Tag/Version/RawVersion/Base/Major/
48            // Minor/Patch/Prerelease/BuildMetadata) is factored into
49            // `set_version_vars` so `render_template_for_version` can re-derive
50            // the SAME block for a promotion's target version without drift.
51            // Deriving Version/RawVersion from the parsed `SemVer` struct (not
52            // `tag.strip_prefix('v')`) handles monorepo tags like `core-v0.3.2`.
53            set_version_vars(&mut self.template_vars, &info.semver, &info.tag);
54            self.template_vars.set("FullCommit", &info.commit);
55            self.template_vars.set("Commit", &info.commit);
56            self.template_vars.set("ShortCommit", &info.short_commit);
57            self.template_vars.set("Branch", &info.branch);
58            self.template_vars.set("CommitDate", &info.commit_date);
59            self.template_vars
60                .set("CommitTimestamp", &info.commit_timestamp);
61            self.template_vars.set_bool("IsGitDirty", info.dirty);
62            self.template_vars.set_bool("IsGitClean", !info.dirty);
63            self.template_vars
64                .set("GitTreeState", if info.dirty { "dirty" } else { "clean" });
65            self.template_vars.set("GitURL", &info.remote_url);
66            self.template_vars.set("Summary", &info.summary);
67            self.template_vars.set("TagSubject", &info.tag_subject);
68            self.template_vars.set("TagContents", &info.tag_contents);
69            self.template_vars.set("TagBody", &info.tag_body);
70            self.template_vars
71                .set("PreviousTag", info.previous_tag.as_deref().unwrap_or(""));
72            self.template_vars
73                .set("FirstCommit", info.first_commit.as_deref().unwrap_or(""));
74
75            // Pro additions: PrefixedTag, PrefixedPreviousTag, PrefixedSummary
76            //
77            // When monorepo.tag_prefix is configured, the git tag already
78            // contains the prefix (e.g. "subproject1/v1.2.3"). In this case:
79            //   - Tag = prefix stripped (e.g. "v1.2.3")
80            //   - PrefixedTag = full tag (e.g. "subproject1/v1.2.3")
81            //   - PrefixedPreviousTag = full previous tag
82            //
83            // When monorepo is NOT configured, fall back to the original
84            // behavior: prepend tag.tag_prefix to construct PrefixedTag.
85            let monorepo_prefix = self.config.monorepo_tag_prefix();
86
87            // monorepo.tag_prefix takes precedence over tag.tag_prefix for
88            // PrefixedTag / PrefixedPreviousTag / PrefixedSummary behavior.
89            // When monorepo is configured, info.tag and info.summary already
90            // contain the prefix from git, so it is stripped for the base vars and
91            // use the raw values for the Prefixed variants.
92            if let Some(prefix) = monorepo_prefix {
93                // Monorepo mode: the tag in git_info is the FULL prefixed tag.
94                // PrefixedTag = full tag (already has prefix).
95                self.template_vars.set("PrefixedTag", &info.tag);
96
97                // Tag = prefix stripped. Overrides the Tag set above.
98                let stripped_tag = crate::git::strip_monorepo_prefix(&info.tag, prefix);
99                self.template_vars.set("Tag", stripped_tag);
100
101                // Version: derived from the parsed SemVer struct (same source as
102                // the non-monorepo path and the build stage's per-crate
103                // re-scoping) so all three stay byte-identical. `info.semver`
104                // was parsed from the full prefixed tag, so it already excludes
105                // the monorepo prefix — no separate string-strip needed.
106                //
107                // For a non-semver tag under `--skip=validate`, info.semver is
108                // the skip-validate fallback, so this yields "0.0.0" rather than
109                // the old raw prefix-stripped string.
110                let version = info.semver.version_string();
111                self.template_vars.set("Version", &version);
112
113                // PrefixedPreviousTag = full previous tag (already has prefix).
114                let prev_tag = info.previous_tag.as_deref().unwrap_or("");
115                self.template_vars.set("PrefixedPreviousTag", prev_tag);
116
117                // PreviousTag = prefix stripped, consistent with Tag being stripped.
118                let stripped_prev = crate::git::strip_monorepo_prefix(prev_tag, prefix);
119                self.template_vars.set("PreviousTag", stripped_prev);
120
121                // PrefixedSummary: info.summary from `git describe` already
122                // includes the monorepo prefix (e.g. "subproject1/v1.2.3-0-gabc123d"),
123                // so use it as-is for the prefixed variant.
124                self.template_vars.set("PrefixedSummary", &info.summary);
125                // Summary: strip the monorepo prefix for the base variant.
126                let stripped_summary = crate::git::strip_monorepo_prefix(&info.summary, prefix);
127                self.template_vars.set("Summary", stripped_summary);
128            } else {
129                // Non-monorepo: compose tag.tag_prefix onto the tag git reported. The
130                // composition is idempotent: a `tag_prefix: "v"` beside a `v1.2.3`
131                // tag names `v1.2.3`, not `vv1.2.3`.
132                let tag_prefix = self
133                    .config
134                    .tag
135                    .as_ref()
136                    .and_then(|t| t.tag_prefix.as_deref())
137                    .unwrap_or("");
138                self.template_vars.set(
139                    "PrefixedTag",
140                    &crate::git::compose_prefix(tag_prefix, &info.tag),
141                );
142                let prev_tag = info.previous_tag.as_deref().unwrap_or("");
143                let prefixed_prev = if prev_tag.is_empty() {
144                    String::new()
145                } else {
146                    crate::git::compose_prefix(tag_prefix, prev_tag)
147                };
148                self.template_vars
149                    .set("PrefixedPreviousTag", &prefixed_prev);
150                self.template_vars.set(
151                    "PrefixedSummary",
152                    &crate::git::compose_prefix(tag_prefix, &info.summary),
153                );
154            }
155        }
156
157        // `NightlyBuild`: stateless per-base-version build counter derived
158        // from `git rev-list --count <last-tag>..HEAD`. Resets automatically
159        // when a new version tag arrives (no state anodizer persists). Set
160        // unconditionally (it is just a count), but intended for nightly /
161        // snapshot `version_template`s such as
162        // `"{{ .Base }}-nightly.{{ .NightlyBuild }}+{{ .ShortCommit }}"`.
163        // Defaults to "0" outside a git repo (synthetic snapshot/scratch
164        // builds) and on any git error so templates never fail to render.
165        //
166        // The monorepo prefix constrains the last-tag lookup to the active
167        // crate's tags so per-crate workspace runs count since the right
168        // tag (not the nearest tag from another subproject).
169        let nightly_build = if self.git_info.is_some() {
170            let root = self
171                .options
172                .project_root
173                .clone()
174                .unwrap_or_else(|| PathBuf::from("."));
175            let monorepo_prefix = self.config.monorepo_tag_prefix();
176            crate::git::count_commits_since_last_tag_in(&root, monorepo_prefix).unwrap_or(0)
177        } else {
178            0
179        };
180        self.template_vars
181            .set_structured("NightlyBuild", serde_json::Value::from(nightly_build));
182
183        // Mode flags are injected as real bools (not "true"/"false" strings)
184        // so `not IsSnapshot` / `IsSnapshot == false` / bare `{% if … %}`
185        // forms all evaluate correctly; `{{ IsSnapshot }}` interpolation
186        // still renders "true"/"false".
187        self.template_vars
188            .set_bool("IsSnapshot", self.options.snapshot);
189        self.template_vars
190            .set_bool("IsNightly", self.options.nightly);
191        // Surfaced to user `if_condition:` templates so stages can
192        // selectively run inside the determinism harness even when
193        // `not IsSnapshot` would otherwise skip them.
194        self.template_vars
195            .set_bool("IsHarness", self.in_determinism_harness());
196        // Wire IsDraft from `release.draft`.
197        let is_draft = self
198            .config
199            .release
200            .as_ref()
201            .and_then(|r| r.draft)
202            .unwrap_or(false);
203        self.template_vars.set_bool("IsDraft", is_draft);
204        self.template_vars
205            .set_bool("IsSingleTarget", self.options.single_target.is_some());
206
207        // Pro addition: IsRelease — true if this is a regular release (not snapshot, not nightly).
208        let is_release = !self.options.snapshot && !self.options.nightly;
209        self.template_vars.set_bool("IsRelease", is_release);
210
211        // Pro addition: IsMerging — true if running with --merge flag.
212        self.template_vars.set_bool("IsMerging", self.options.merge);
213    }
214
215    /// Populate time-related template variables.
216    ///
217    /// Sets:
218    /// - `Date` — UTC time as RFC 3339
219    /// - `Timestamp` — unix timestamp as string
220    /// - `Now` — UTC time as RFC 3339
221    /// - `Year` — four-digit year (e.g. "2026")
222    /// - `Month` — zero-padded month (e.g. "03")
223    /// - `Day` — zero-padded day (e.g. "30")
224    /// - `Hour` — zero-padded hour (e.g. "14")
225    /// - `Minute` — zero-padded minute (e.g. "05")
226    ///
227    /// Time source resolution (first match wins):
228    ///
229    /// 1. `SOURCE_DATE_EPOCH` env var — the standard reproducibility contract
230    ///    (set by the determinism harness on every child release subprocess,
231    ///    and the conventional way external CI / packagers signal a fixed
232    ///    epoch). This is required for byte-stability of `metadata.json`
233    ///    (which embeds `Date`) and any user template that consumes `Date` /
234    ///    `Timestamp` / `Now`. Without this branch, two from-clean runs of
235    ///    the same commit emit metadata.json files that differ in the `date`
236    ///    field, defeating release-asset idempotency.
237    /// 2. `chrono::Utc::now()` — wall-clock fallback. The
238    ///    legacy semantics for runs without SDE wired in. Note that the
239    ///    template docs explicitly call `.Now` "not deterministic"
240    ///    — under SDE-aware reproducible builds that claim is deliberately
241    ///    not true.
242    pub fn populate_time_vars(&mut self) {
243        // Resolution order (SDE first, else wall-clock) is centralized in
244        // `crate::sde::resolve_now_with_env` so any caller —
245        // `populate_time_vars`, Tera built-ins, stage-srpm's `%changelog`
246        // date, nightly `date_str` — sees identical "now" semantics.
247        // Routes through the injected `env_source` so tests can inject
248        // SOURCE_DATE_EPOCH via TestContextBuilder::env() without
249        // mutating the process env.
250        let now = crate::sde::resolve_now_with_env(self.env_source());
251        self.template_vars.set("Date", &now.to_rfc3339());
252        self.template_vars
253            .set("Timestamp", &now.timestamp().to_string());
254        self.template_vars.set("Now", &now.to_rfc3339());
255        self.template_vars
256            .set("Year", &now.format("%Y").to_string());
257        self.template_vars
258            .set("Month", &now.format("%m").to_string());
259        self.template_vars.set("Day", &now.format("%d").to_string());
260        self.template_vars
261            .set("Hour", &now.format("%H").to_string());
262        self.template_vars
263            .set("Minute", &now.format("%M").to_string());
264    }
265
266    /// Populate runtime environment variables.
267    ///
268    /// Sets:
269    /// - `RuntimeGoos` — host OS in Go-compatible naming (e.g. "linux", "darwin", "windows")
270    /// - `RuntimeGoarch` — host architecture in Go-compatible naming (e.g. "amd64", "arm64")
271    /// - `Runtime_Goos` / `Runtime_Goarch` — nested aliases
272    /// - `RustcVersion` — host rustc release version (e.g. "1.96.0"), or "" when
273    ///   rustc is unavailable
274    pub fn populate_runtime_vars(&mut self) {
275        let goos = map_os_to_goos(std::env::consts::OS);
276        let goarch = map_arch_to_goarch(std::env::consts::ARCH);
277        self.template_vars.set("RuntimeGoos", goos);
278        self.template_vars.set("RuntimeGoarch", goarch);
279        // Runtime.Goos / Runtime.Goarch — after preprocessing
280        // the dot becomes an underscore-separated flat key. Both forms are exposed.
281        self.template_vars.set("Runtime_Goos", goos);
282        self.template_vars.set("Runtime_Goarch", goarch);
283        // RustcVersion is a host-environment fact like OS/arch, so it is set in
284        // the same call — keeping it a separate populate step risks a call-site
285        // forgetting to invoke the sibling.
286        self.populate_rustc_vars();
287    }
288
289    /// Populate the `RustcVersion` built-in template variable.
290    ///
291    /// Probes `rustc -vV` and extracts the `release:` line (e.g. `"1.96.0"`).
292    /// Sets `RustcVersion` to the extracted string, or to `""` when rustc is
293    /// unavailable or the line is absent — templates that reference
294    /// `{{ .RustcVersion }}` degrade to an empty value rather than erroring.
295    fn populate_rustc_vars(&mut self) {
296        let ver = crate::partial::detect_rustc_version().unwrap_or_default();
297        self.template_vars.set("RustcVersion", &ver);
298    }
299
300    /// Populate the `ReleaseNotes` template variable from stored changelogs.
301    ///
302    /// Should be called after the changelog stage has run and populated
303    /// `self.stage_outputs`. Prefers the single-track AGGREGATE body
304    /// ([`StageOutputs::release_body_changelog`]) when present, so a lockstep
305    /// release's `{{ .ReleaseNotes }}` spans the whole workspace rather than one
306    /// crate's path slice. Falls back to the first crate (by crate universe
307    /// order — top-level `crates:` then every `workspaces[].crates` entry) whose
308    /// per-crate changelog is present, or an empty string. Universe order is
309    /// deterministic, unlike HashMap iteration order.
310    pub fn populate_release_notes_var(&mut self) {
311        // The single-track aggregate spans every crate dir over the whole range;
312        // prefer it so `ReleaseNotes` matches the GitHub release body. Fall back
313        // to per-crate lookup in universe order for determinism — the universe
314        // walk (not `config.crates`) is what lets a pure-`workspaces:` config
315        // resolve a non-empty `ReleaseNotes`, since its crates carry the
316        // changelogs but never appear in the top-level list.
317        let notes = self
318            .stage_outputs
319            .release_body_changelog
320            .clone()
321            .or_else(|| {
322                self.config
323                    .crate_universe()
324                    .into_iter()
325                    .find_map(|c| self.stage_outputs.changelogs.get(&c.name))
326                    .cloned()
327            })
328            .unwrap_or_default();
329        self.template_vars.set("ReleaseNotes", &notes);
330    }
331
332    /// Refresh the `Artifacts` structured template variable from the current
333    /// artifact registry. Should be called before rendering release body and
334    /// announce templates so they can iterate over all artifacts.
335    ///
336    /// Each artifact is serialized as a map with keys: `name`, `path`, `target`,
337    /// `kind`, `crate_name`, and `metadata`.
338    ///
339    /// **Known metadata keys** (populated by individual stages):
340    /// - `format` — archive format (e.g. `"tar.gz"`, `"zip"`), set by archive stage
341    /// - `extra_file` — `"true"` when artifact is an extra file, set by checksum stage
342    /// - `extra_name_template` — name template override for extra files, set by checksum stage
343    /// - `digest` — docker image digest (e.g. `sha256:abc123...`), set by docker stage
344    /// - `id` — artifact ID from config, set by docker and build stages
345    /// - `binary` — binary name, set by build stage
346    pub fn refresh_artifacts_var(&mut self) {
347        // CSV metadata keys exposed as JSON arrays for template iteration.
348        // Storage remains HashMap<String,String> (flat); only the
349        // template-exposed view is expanded. The
350        // ExtraBinaries / ExtraFiles list semantics.
351        const CSV_LIST_KEYS: &[&str] = &["extra_binaries", "extra_files"];
352        // JSON-encoded list metadata keys: stored as a JSON-array string in
353        // `HashMap<String,String>`, exposed as a real array on the template
354        // side so `{% for p in .Artifacts[0].metadata.Platforms %}` works.
355        // `Platforms` is the platform-list slice on
356        // `DockerImageV2` artifacts.
357        const JSON_LIST_KEYS: &[&str] = &["Platforms"];
358
359        let artifacts_value: Vec<serde_json::Value> = self
360            .artifacts
361            .all()
362            .iter()
363            .map(|a| {
364                // Rebuild metadata map converting known CSV keys into arrays.
365                let mut metadata_map = serde_json::Map::with_capacity(a.metadata.len());
366                for (k, v) in &a.metadata {
367                    if CSV_LIST_KEYS.contains(&k.as_str()) {
368                        let items: Vec<serde_json::Value> = if v.is_empty() {
369                            Vec::new()
370                        } else {
371                            v.split(',')
372                                .map(|s| serde_json::Value::String(s.to_string()))
373                                .collect()
374                        };
375                        metadata_map.insert(k.clone(), serde_json::Value::Array(items));
376                    } else if JSON_LIST_KEYS.contains(&k.as_str()) {
377                        // Decode JSON-array string into a real Value::Array;
378                        // a malformed value falls back to the raw string so
379                        // custom publishers can still inspect it.
380                        let parsed = serde_json::from_str::<serde_json::Value>(v)
381                            .unwrap_or_else(|_| serde_json::Value::String(v.clone()));
382                        metadata_map.insert(k.clone(), parsed);
383                    } else {
384                        metadata_map.insert(k.clone(), serde_json::Value::String(v.clone()));
385                    }
386                }
387                serde_json::json!({
388                    "name": a.name,
389                    "path": a.path.to_string_lossy(),
390                    "target": a.target.as_deref().unwrap_or(""),
391                    "kind": a.kind.as_str(),
392                    "crate_name": a.crate_name,
393                    "metadata": serde_json::Value::Object(metadata_map),
394                })
395            })
396            .collect();
397        self.template_vars
398            .set_structured("Artifacts", serde_json::Value::Array(artifacts_value));
399    }
400
401    /// Populate the `Metadata` structured template variable from config.metadata.
402    ///
403    /// Exposes the project metadata block as a nested map with PascalCase keys
404    /// the `.Metadata.*` namespace:
405    /// `Description`, `Homepage`, `Documentation`, `License`, `Repository`,
406    /// `Maintainers`, `ModTimestamp`, `FullDescription` (resolved),
407    /// `CommitAuthor.{Name,Email}`.
408    /// Missing fields default to empty strings / empty arrays.
409    ///
410    /// `full_description` supports `Inline`, `FromFile` (template-rendered
411    /// path, read from disk), and `FromUrl` (template-rendered URL +
412    /// headers, fetched through [`crate::content_source::resolve`] which
413    /// applies retries, body caps, and CR/LF header-injection guards).
414    pub fn populate_metadata_var(&mut self) -> anyhow::Result<()> {
415        // Clone the small scalar fields so no borrow on self.config is held
416        // across the render_template calls below.
417        let (
418            description,
419            homepage,
420            documentation,
421            license,
422            repository,
423            maintainers,
424            mod_timestamp,
425            full_desc_src,
426            commit_author,
427        ) = {
428            let meta = self.config.metadata.as_ref();
429            // Description / homepage / documentation / license resolve through
430            // the project-level fallback: top-level `metadata.*` wins, else the
431            // primary crate's `Cargo.toml`-derived value. This keeps
432            // `{{ Metadata.* }}` single-sourced with the per-publisher
433            // `meta_*_for` resolvers, so dropping a redundant `metadata.license`
434            // (derivable from Cargo.toml) does not silently empty the var.
435            let description = self
436                .config
437                .meta_description_project()
438                .unwrap_or("")
439                .to_string();
440            let homepage = self
441                .config
442                .meta_homepage_project()
443                .unwrap_or("")
444                .to_string();
445            let documentation = self
446                .config
447                .meta_documentation_project()
448                .unwrap_or("")
449                .to_string();
450            let license = self.config.meta_license_project().unwrap_or("").to_string();
451            let repository = self
452                .config
453                .meta_repository_project()
454                .unwrap_or("")
455                .to_string();
456            let maintainers: Vec<String> = meta
457                .and_then(|m| m.maintainers.as_ref())
458                .cloned()
459                .unwrap_or_default();
460            let mod_timestamp = meta
461                .and_then(|m| m.mod_timestamp.as_deref())
462                .unwrap_or("")
463                .to_string();
464            let full_desc_src = meta.and_then(|m| m.full_description.clone());
465            let commit_author = meta.and_then(|m| m.commit_author.clone());
466            (
467                description,
468                homepage,
469                documentation,
470                license,
471                repository,
472                maintainers,
473                mod_timestamp,
474                full_desc_src,
475                commit_author,
476            )
477        };
478
479        // Resolve full_description through the shared ContentSource resolver
480        // so Inline, FromFile (template-rendered path), and FromUrl
481        // (template-rendered URL + headers, retried HTTP fetch with
482        // body cap and CR/LF guard) all behave the same as the release
483        // header/footer fields.
484        let full_description = match full_desc_src {
485            None => String::new(),
486            Some(src) => crate::content_source::resolve(
487                &src,
488                "metadata.full_description",
489                self,
490                &self.logger("metadata"),
491            )?,
492        };
493
494        let commit_author_map = serde_json::json!({
495            "Name": commit_author.as_ref().and_then(|c| c.name.clone()).unwrap_or_default(),
496            "Email": commit_author.as_ref().and_then(|c| c.email.clone()).unwrap_or_default(),
497        });
498
499        let meta_map = serde_json::json!({
500            "Description": description,
501            "Homepage": homepage,
502            "Documentation": documentation,
503            "License": license,
504            "Repository": repository,
505            "Maintainers": maintainers,
506            "ModTimestamp": mod_timestamp,
507            "FullDescription": full_description,
508            "CommitAuthor": commit_author_map,
509        });
510        self.template_vars.set_structured("Metadata", meta_map);
511        Ok(())
512    }
513}
514
515/// Map Rust's `std::env::consts::OS` to Go-compatible GOOS naming.
516/// Templates expect Go runtime names (e.g. "darwin" not "macos").
517pub fn map_os_to_goos(os: &str) -> &str {
518    match os {
519        "macos" => "darwin",
520        other => other, // linux, windows, freebsd, etc. already match
521    }
522}
523
524/// Map Rust's `std::env::consts::ARCH` to Go-compatible GOARCH naming.
525/// Templates expect Go runtime names (e.g. "amd64" not "x86_64").
526///
527/// Delegates to the shared [`crate::target::rust_arch_to_goarch`] table so a
528/// host-derived `{{ .Runtime.Goarch }}` can never disagree with the
529/// triple-derived arch tokens in asset names. `ARCH` doesn't encode
530/// endianness, so the host's own compile-time endianness disambiguates
531/// `powerpc64`/`mips64`. Tokens outside the table (`arm` — GOARCH really is
532/// "arm" — plus exotics) pass through unchanged.
533pub fn map_arch_to_goarch(arch: &str) -> &str {
534    crate::target::rust_arch_to_goarch(arch, cfg!(target_endian = "little")).unwrap_or(arch)
535}