avarok_core/
env_compat.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Backwards compatibility shim for the `ATLAS_*` to `AVAROK_*` env rename.
4//!
5//! The repo-wide rebrand moved every variable the server reads from `ATLAS_*`
6//! to `AVAROK_*`, but the CLI that launches the server lives in a different
7//! repository and still exports the old names (recipes set things like
8//! `ATLAS_MTP_DCUT_RATIO`, `ATLAS_FP8_ROWWISE`, `ATLAS_GDN_FLASHINFER`,
9//! `ATLAS_GDN_LIB` and `ATLAS_TARGET_MODEL`; the agent sets `ATLAS_HOME`).
10//! Without this shim a server built from the renamed tree ignores all of them
11//! silently, which looks exactly like a recipe that does nothing.
12//!
13//! [`mirror_legacy_env`] copies each legacy name onto its new name at process
14//! startup, so the ~440 `std::env::var("AVAROK_...")` reads scattered across
15//! the crates need no per-site fallback.
16//!
17//! REMOVAL POINT: delete this module, its call sites in `main`, and the
18//! `ATLAS_SKIP_BUILD` fallback in `avarok-kernels/build.rs` once the CLI ships
19//! `AVAROK_*` names and no supported release still emits `ATLAS_*`.
20
21use std::ffi::OsString;
22
23/// The name every legacy variable starts with. Matched as an exact ASCII
24/// prefix, so sibling namespaces such as `ATLASCTL_*` are left alone.
25const LEGACY_PREFIX: &str = "ATLAS_";
26
27/// The name the renamed tree reads.
28const CURRENT_PREFIX: &str = "AVAROK_";
29
30/// Mirror every `ATLAS_*` variable onto its `AVAROK_*` name when the new
31/// name is unset. Returns the mirrored keys (new names). Must run before any
32/// thread is spawned; call it first thing in `main`.
33///
34/// An `AVAROK_*` value already present always wins: an operator who sets the
35/// new name explicitly is never overridden by a stale legacy export.
36///
37/// # Example
38///
39/// ```no_run
40/// // First statement of `main`, before any runtime, logging or thread init.
41/// let mirrored = avarok_core::env_compat::mirror_legacy_env();
42/// if !mirrored.is_empty() {
43///     eprintln!("mirrored {} legacy variables", mirrored.len());
44/// }
45/// ```
46pub fn mirror_legacy_env() -> Vec<String> {
47    // Snapshot the environment before touching it. `vars_os` walks the live
48    // environ block, and `set_var` may reallocate that block, so collecting
49    // first keeps the iterator and the mutation strictly separated.
50    //
51    // Keys must be UTF-8 to be rewritten (every variable in play is ASCII);
52    // values stay `OsString` so a non-UTF-8 path is mirrored byte for byte.
53    let candidates: Vec<(String, OsString)> = std::env::vars_os()
54        .filter_map(|(key, value)| {
55            let key = key.into_string().ok()?;
56            let rest = key.strip_prefix(LEGACY_PREFIX)?;
57            Some((format!("{CURRENT_PREFIX}{rest}"), value))
58        })
59        .collect();
60
61    let mut mirrored = Vec::new();
62    for (current_key, value) in candidates {
63        if std::env::var_os(&current_key).is_some() {
64            // The new name is already set. Never clobber it.
65            continue;
66        }
67        // SAFETY: `std::env::set_var` is unsafe in edition 2024 because the
68        // underlying `setenv` is not thread safe: it can reallocate the
69        // environ block while another thread is inside `getenv`, and the C
70        // library gives no lock to share. The contract that makes this call
71        // sound is that it runs as the very first statement of `main`, before
72        // any runtime, logging subscriber, GPU context or `std::thread::spawn`
73        // exists, so this thread is the only one that can observe the
74        // environment. Callers must not call it from anywhere else.
75        unsafe { std::env::set_var(&current_key, &value) };
76        mirrored.push(current_key);
77    }
78
79    mirrored.sort();
80    mirrored
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::sync::Mutex;
87
88    /// These tests mutate the process environment, which is global to the test
89    /// binary, and `mirror_legacy_env` reads all of it. Serialize them so one
90    /// test's half-built key pair is never visible to another's mirror pass.
91    static ENV_LOCK: Mutex<()> = Mutex::new(());
92
93    /// A failed assertion poisons the lock; the data is `()`, so recovering the
94    /// guard keeps one failure from cascading into unrelated test failures.
95    fn env_guard() -> std::sync::MutexGuard<'static, ()> {
96        ENV_LOCK
97            .lock()
98            .unwrap_or_else(|poisoned| poisoned.into_inner())
99    }
100
101    /// Key names are unique per test and carry the pid so a stray variable
102    /// inherited from the surrounding shell cannot collide with them.
103    fn unique(suffix: &str) -> String {
104        format!("ZZ_TEST_{}_{suffix}", std::process::id())
105    }
106
107    /// SAFETY: same contract as [`mirror_legacy_env`]. Every test that calls
108    /// this holds `ENV_LOCK`, and the crate spawns no threads in its tests, so
109    /// no other thread can read the environment concurrently.
110    fn set(key: &str, value: &str) {
111        unsafe { std::env::set_var(key, value) };
112    }
113
114    /// SAFETY: as [`set`] above.
115    fn unset(key: &str) {
116        unsafe { std::env::remove_var(key) };
117    }
118
119    #[test]
120    fn legacy_key_is_mirrored_onto_the_new_name() {
121        let _guard = env_guard();
122        let legacy = format!("ATLAS_{}", unique("MIRRORED"));
123        let current = format!("AVAROK_{}", unique("MIRRORED"));
124        unset(&current);
125        set(&legacy, "recipe-value");
126
127        let mirrored = mirror_legacy_env();
128
129        assert_eq!(
130            std::env::var(&current).ok().as_deref(),
131            Some("recipe-value"),
132            "{legacy} should have been copied onto {current}"
133        );
134        assert!(
135            mirrored.contains(&current),
136            "{current} should be reported as mirrored, got {mirrored:?}"
137        );
138
139        unset(&legacy);
140        unset(&current);
141    }
142
143    #[test]
144    fn existing_new_name_is_not_overwritten() {
145        let _guard = env_guard();
146        let legacy = format!("ATLAS_{}", unique("KEPT"));
147        let current = format!("AVAROK_{}", unique("KEPT"));
148        // Set the winner first so no interleaving can mirror the legacy name
149        // before the new name exists.
150        set(&current, "explicit-new-value");
151        set(&legacy, "stale-legacy-value");
152
153        let mirrored = mirror_legacy_env();
154
155        assert_eq!(
156            std::env::var(&current).ok().as_deref(),
157            Some("explicit-new-value"),
158            "an explicitly set {current} must win over {legacy}"
159        );
160        assert!(
161            !mirrored.contains(&current),
162            "{current} was already set and must not be reported, got {mirrored:?}"
163        );
164
165        unset(&legacy);
166        unset(&current);
167    }
168
169    #[test]
170    fn atlasctl_prefix_is_left_alone() {
171        let _guard = env_guard();
172        // `ATLASCTL_` shares the first five letters but is a different
173        // namespace: the prefix match is on `ATLAS_`, underscore included.
174        let legacy = format!("ATLASCTL_{}", unique("UNTOUCHED"));
175        let would_be = format!("AVAROKCTL_{}", unique("UNTOUCHED"));
176        set(&legacy, "control-plane-value");
177
178        let mirrored = mirror_legacy_env();
179
180        assert!(
181            std::env::var_os(&would_be).is_none(),
182            "{legacy} must not produce {would_be}"
183        );
184        assert!(
185            !mirrored.iter().any(|key| key.contains("UNTOUCHED")),
186            "no ATLASCTL_ key should be mirrored, got {mirrored:?}"
187        );
188
189        unset(&legacy);
190    }
191}