Skip to main content

anodizer_core/log/
depth.rs

1//! Process-global section nesting depth and the RAII guards that move it.
2
3use super::render::PENDING;
4use std::sync::OnceLock;
5use std::sync::atomic::{AtomicUsize, Ordering};
6
7/// Process-global section nesting depth. Drives the 2-space-per-level
8/// indentation applied to every stderr log line so output produced
9/// inside a [`StageLogger::group`](crate::log::StageLogger::group) sits visually beneath its header.
10///
11/// A single atomic (rather than per-logger state) is correct because the
12/// release pipeline drives one stderr stream and no `group()` is ever
13/// opened from a worker thread — sections bracket whole stages on the
14/// main thread, while a stage's interior parallelism (e.g. `build`
15/// spawning per-target threads) emits *inside* an already-open section.
16/// The depth is therefore a property of "where the main thread is in the
17/// run", not of any individual logger clone or worker.
18pub(super) static SECTION_DEPTH: AtomicUsize = AtomicUsize::new(0);
19
20/// Env var carrying a parent `anodizer` process's visual nesting depth.
21///
22/// The determinism harness spawns child `anodizer release` subprocesses
23/// whose stderr is inherited, so the child's lines interleave directly
24/// into the parent's stream. Without an inherited base depth the child's
25/// section headers would render flush-left, visually escaping the
26/// parent's open section. The parent exports its depth here; the child
27/// reads it once (see `base_depth`) and offsets every indent by it.
28pub const LOG_DEPTH_ENV: &str = "ANODIZER_LOG_DEPTH";
29
30/// Base nesting depth inherited from a parent process via
31/// [`LOG_DEPTH_ENV`], parsed once on first use. Zero when the var is
32/// absent or unparseable (a standalone process indents from column 0).
33pub(super) static BASE_DEPTH: OnceLock<usize> = OnceLock::new();
34
35/// Parse the inherited base depth from a raw [`LOG_DEPTH_ENV`] value.
36/// Lenient by design: a missing or malformed value degrades to 0 (the
37/// standalone-process default) rather than failing — indentation is
38/// presentation, never worth aborting a release over.
39pub(super) fn parse_base_depth(raw: Option<&str>) -> usize {
40    raw.and_then(|v| v.trim().parse().ok()).unwrap_or(0)
41}
42
43/// The process's inherited base depth (see [`LOG_DEPTH_ENV`]).
44pub(super) fn base_depth() -> usize {
45    *BASE_DEPTH.get_or_init(|| parse_base_depth(std::env::var(LOG_DEPTH_ENV).ok().as_deref()))
46}
47
48/// Current absolute nesting depth: the inherited base plus every open
49/// section. This is the value [`indent`](crate::log::indent) renders and the value a parent
50/// exports (offset for the child's nesting) when spawning a subprocess
51/// whose stderr joins this process's stream.
52pub fn current_depth() -> usize {
53    base_depth() + SECTION_DEPTH.load(Ordering::Relaxed)
54}
55
56/// RAII guard returned by [`indent_one_level`]. Removes the extra indent
57/// level when dropped.
58#[must_use = "dropping the guard immediately removes the extra indent"]
59pub struct IndentGuard {
60    _private: (),
61}
62
63impl Drop for IndentGuard {
64    fn drop(&mut self) {
65        SECTION_DEPTH.fetch_sub(1, Ordering::Relaxed);
66    }
67}
68
69/// Deepen the body indent by one level WITHOUT opening a section header.
70///
71/// For rows that must align with the body bullets of sibling sections
72/// while no section is open — e.g. the pipeline's consolidated
73/// `skipped  a, b, c` row, which prints between stage sections (the
74/// previous stage's guard has already dropped) but should sit at the
75/// same column as those sections' own `•` lines instead of two columns
76/// to their left. Unlike [`StageLogger::group`](crate::log::StageLogger::group) this pushes no pending
77/// header, so nothing extra ever prints.
78pub fn indent_one_level() -> IndentGuard {
79    SECTION_DEPTH.fetch_add(1, Ordering::Relaxed);
80    IndentGuard { _private: () }
81}
82
83/// RAII guard returned by [`StageLogger::group`](crate::log::StageLogger::group). Closes the section
84/// (decrements the indent depth) when dropped, so a stage's body
85/// indentation is always balanced even if the stage bails early with `?`.
86#[must_use = "dropping the guard immediately ends the section"]
87pub struct SectionGuard {
88    pub(super) _private: (),
89}
90
91impl Drop for SectionGuard {
92    fn drop(&mut self) {
93        // Take the PENDING lock BEFORE decrementing the depth: a
94        // flush_pending observer on another thread serializes on this
95        // lock, so it sees the depth decrement and the pop as one
96        // transition instead of a window where the depth is already
97        // lowered but the section's pending header is still queued.
98        let mut pending = PENDING.lock().unwrap_or_else(|e| e.into_inner());
99        SECTION_DEPTH.fetch_sub(1, Ordering::Relaxed);
100        // Remove this section's pending entry (LIFO matches nesting). An
101        // unflushed entry means the section emitted no body line — a no-op
102        // stage — so dropping it without printing is exactly the desired
103        // "no-op stages print nothing" behavior.
104        pending.pop();
105    }
106}