anodizer_core/context/runtime.rs
1use super::*;
2
3impl Context {
4 /// Return the current `Version` template variable, or an empty string if
5 /// not yet populated.
6 pub fn version(&self) -> String {
7 self.template_vars
8 .get("Version")
9 .cloned()
10 .unwrap_or_default()
11 }
12
13 /// Reproducible-mtime seed shared by every stage that stamps a build
14 /// timestamp into a produced artifact (release archives, source archives,
15 /// PyPI wheels + sdists).
16 ///
17 /// Resolution ladder, single-sourced here so archives and wheels never
18 /// pick different timestamps in one run:
19 ///
20 /// 1. when ANY build in the crate universe is `reproducible: true`, the
21 /// commit timestamp wins outright — a reproducible build pins its own
22 /// output to the commit, so a stray ambient `SOURCE_DATE_EPOCH` must
23 /// not override it;
24 /// 2. otherwise `SOURCE_DATE_EPOCH` (the standard reproducibility
25 /// contract, set by the determinism harness on every child), falling
26 /// back to the commit timestamp.
27 ///
28 /// Returns `None` when neither a commit timestamp nor `SOURCE_DATE_EPOCH`
29 /// is available (writers then leave the default wall-clock stamp).
30 pub fn resolve_reproducible_mtime(&self) -> Option<u64> {
31 let any_reproducible = self.config.crate_universe().into_iter().any(|c| {
32 c.builds
33 .as_ref()
34 .is_some_and(|builds| builds.iter().any(|b| b.reproducible.unwrap_or(false)))
35 });
36 let commit_ts = self
37 .template_vars()
38 .get("CommitTimestamp")
39 .and_then(|ts| ts.parse::<u64>().ok());
40 if any_reproducible {
41 commit_ts
42 } else {
43 self.env_var("SOURCE_DATE_EPOCH")
44 .and_then(|s| s.parse::<u64>().ok())
45 .or(commit_ts)
46 }
47 }
48
49 /// Derive the verbosity level from context options.
50 pub fn verbosity(&self) -> Verbosity {
51 Verbosity::from_flags(self.options.quiet, self.options.verbose, self.options.debug)
52 }
53
54 /// Resolve the user's `retry:` block into a concrete [`RetryPolicy`],
55 /// applying defaults when `retry:` is unset. Equivalent to
56 /// `ctx.config.retry.unwrap_or_default().to_policy()` but centralizes
57 /// the lookup so a future refactor can hang validation / clamping off
58 /// a single seam.
59 pub fn retry_policy(&self) -> crate::retry::RetryPolicy {
60 self.config.retry.unwrap_or_default().to_policy()
61 }
62
63 /// Resolve the retry wall-clock budget into an absolute deadline anchored at
64 /// the moment of this call. Always `Some`: `retry.max_elapsed` when the user
65 /// sets it, otherwise [`crate::retry::DEFAULT_MAX_ELAPSED`] (15 min) — so a
66 /// publisher that threads this into [`crate::retry::retry_sync_deadline`] /
67 /// [`crate::retry::retry_async_deadline`] is bounded by default and the
68 /// operator can raise or lower the ceiling with one config field. The
69 /// `Option` return lets it feed those engines verbatim (their `None` means
70 /// unbounded, reserved for callers with no context). Computed once at the
71 /// start of a publish sequence so a long transient storm exits cleanly
72 /// (resumable) instead of being SIGKILLed mid-write by the outer job timeout.
73 pub fn retry_deadline(&self) -> Option<std::time::Instant> {
74 let budget = self
75 .config
76 .retry
77 .unwrap_or_default()
78 .max_elapsed_duration()
79 .unwrap_or(crate::retry::DEFAULT_MAX_ELAPSED);
80 Some(std::time::Instant::now() + budget)
81 }
82
83 /// Create a [`StageLogger`] for the given stage name, pre-attached to
84 /// the context's env-pairs list so that subprocess stderr / stdout
85 /// flowing through [`StageLogger::check_output`] is automatically
86 /// redacted. The env list combines the template-engine env
87 /// (process + config + `.env` files) and the current `std::env::vars`
88 /// snapshot, so any secret value reachable to a hook or subprocess is
89 /// available for scrubbing.
90 pub fn logger(&self, stage: &'static str) -> StageLogger {
91 // Snapshot the current redaction env into the shared cell at
92 // construction so a secret injected via `template_vars_mut().set_env`
93 // (which has no mutation hook) is captured, matching the historical
94 // snapshot-at-`logger()` behavior. The cell stays shared afterward, so
95 // a secret minted later through an `env_source` mutation still reaches
96 // this logger via `refresh_secret_env` at that mutation point.
97 self.refresh_secret_env();
98 #[allow(unused_mut)]
99 let mut log =
100 StageLogger::new(stage, self.verbosity()).with_shared_env(Arc::clone(&self.secret_env));
101 #[cfg(feature = "test-helpers")]
102 if let Some(cap) = &self.log_capture {
103 log = log.with_capture_handle(cap.clone());
104 }
105 log
106 }
107}