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", ¬es);
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}