Skip to main content

sui_eval/
realize.rs

1//! Import-from-derivation (IFD): realize a derivation's output mid-eval.
2//!
3//! When eval coerces a **derivation** to a path for a filesystem read
4//! (`import`, `readFile`, `readDir`, `pathExists`, `builtins.path`) and the
5//! derivation's `outPath` is **not yet materialized on disk**, cppnix realizes
6//! it — building or substituting the derivation during evaluation — so the read
7//! can proceed. This is *import-from-derivation*; sui must do the same to
8//! compute drvPaths whose graph reads a built output (e.g. the darwin toplevel
9//! importing `ishou.stylix-fonts`, itself a `runCommand` derivation).
10//!
11//! ## Why a hook, not an inline builder
12//!
13//! `sui-eval` is a **pure, synchronous** library crate: it owns no store
14//! handle, no tokio runtime, and no build sandbox. The realize pipeline
15//! (`Substitutor` + `LocalBuilder` over an `open_rw` store) lives in the `sui`
16//! binary, is async, and needs privileged store writes. Wiring that pipeline
17//! *into* the evaluator would invert the dependency graph
18//! (`sui-eval → sui-build → sandbox`) and force a tokio runtime + privileged
19//! store into every pure eval.
20//!
21//! Instead the binary installs a **realize hook** — a thread-local callback the
22//! evaluator invokes with `(drv_path, out_path)` at the exact moment a
23//! derivation output is demanded on disk. The binary's hook opens the store,
24//! substitutes-then-builds the closure, and returns once the output exists.
25//! sui-eval stays pure; orchestration stays in the binary. This mirrors the
26//! `INPUT_SOURCE_MAP` thread-local in `path.rs` (fetched-input redirect) — same
27//! separation-of-concerns pattern, one layer up (a build, not a read-redirect).
28//!
29//! ## Byte-parity invariant
30//!
31//! The realize hook changes **no value** the evaluator observes: the `drvPath`
32//! and `outPath` are computed by the module fixpoint *before* realize runs, and
33//! are already byte-correct against nix (the marquee darwin roots proved this).
34//! Realize only makes the bytes at that already-correct `outPath` *present on
35//! disk*. Because the drvPath is byte-identical to nix, the realized output is
36//! byte-identical to nix (same drv ⇒ same output path ⇒ same content). If no
37//! hook is installed, IFD degrades to the pre-existing ENOENT — never a wrong
38//! answer.
39
40use std::cell::RefCell;
41use std::rc::Rc;
42
43/// The realize callback: given a derivation's `.drv` path and its expected
44/// output store path, materialize that output on disk (substitute or build the
45/// closure) and return `Ok(())` once the output path exists. On failure returns
46/// a human-readable error string (surfaced as an eval `IoError`).
47pub type RealizeFn = dyn Fn(&str, &str) -> Result<(), String>;
48
49thread_local! {
50    /// The installed realize hook for this eval thread, if any. `None` means
51    /// IFD is unsupported on this thread — a demanded-but-absent derivation
52    /// output falls through to the normal ENOENT read error (no wrong answer,
53    /// no silent success). Held behind an `Rc` so a call can clone the handle
54    /// out and invoke it while a **nested** realize on the same thread still
55    /// observes the hook as installed (required for re-entrant dependency
56    /// realizes and for cycle detection).
57    static REALIZE_HOOK: RefCell<Option<Rc<RealizeFn>>> = const { RefCell::new(None) };
58
59    /// Re-entrancy guard: a set of `out_path`s currently being realized on this
60    /// thread. A derivation whose realize itself triggers eval that demands the
61    /// SAME output must not recurse infinitely — it is a genuine cycle nix also
62    /// rejects. Bounded: an entry is removed as soon as its realize returns.
63    static IN_FLIGHT: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
64}
65
66/// Install a realize hook for the current thread. Returns a guard that removes
67/// the hook (restoring any previous one) when dropped — so a scoped install
68/// (e.g. one CLI eval) does not leak into unrelated thread reuse.
69///
70/// The binary calls this once around the eval whose drvPath graph may read a
71/// built output.
72#[must_use = "the returned guard uninstalls the hook when dropped"]
73pub fn install_realize_hook(hook: Box<RealizeFn>) -> RealizeHookGuard {
74    let previous = REALIZE_HOOK.with(|h| h.borrow_mut().replace(Rc::from(hook)));
75    RealizeHookGuard { previous: Some(previous) }
76}
77
78/// RAII guard restoring the prior realize hook when dropped.
79pub struct RealizeHookGuard {
80    previous: Option<Option<Rc<RealizeFn>>>,
81}
82
83impl Drop for RealizeHookGuard {
84    fn drop(&mut self) {
85        if let Some(prev) = self.previous.take() {
86            REALIZE_HOOK.with(|h| *h.borrow_mut() = prev);
87        }
88    }
89}
90
91/// Whether a realize hook is installed on this thread.
92#[must_use]
93pub fn has_realize_hook() -> bool {
94    REALIZE_HOOK.with(|h| h.borrow().is_some())
95}
96
97/// Realize a derivation output mid-eval so a subsequent filesystem read of
98/// `out_path` succeeds.
99///
100/// - If no hook is installed, returns `Ok(false)` (caller falls through to the
101///   normal read, which will ENOENT — never a wrong value).
102/// - If the same `out_path` is already being realized on this thread (an
103///   IFD-realizes-itself cycle — a real nix error too), returns an `Err` rather
104///   than recursing.
105/// - Otherwise invokes the hook and returns `Ok(true)` once it succeeds.
106///
107/// # Errors
108///
109/// Returns `Err(message)` when the hook itself fails, or when a realize cycle
110/// is detected.
111pub fn realize_output(drv_path: &str, out_path: &str) -> Result<bool, String> {
112    let hook_present = REALIZE_HOOK.with(|h| h.borrow().is_some());
113    if !hook_present {
114        return Ok(false);
115    }
116
117    // Bounded re-entrancy guard: refuse to realize an output whose realize is
118    // already in flight on this thread.
119    let already_in_flight = IN_FLIGHT.with(|f| f.borrow().iter().any(|p| p == out_path));
120    if already_in_flight {
121        return Err(format!(
122            "import-from-derivation cycle: realizing '{out_path}' requires evaluating its own realize"
123        ));
124    }
125
126    IN_FLIGHT.with(|f| f.borrow_mut().push(out_path.to_string()));
127    // Clone the `Rc` handle OUT of the RefCell so the borrow is released before
128    // the (possibly re-entrant) hook runs — the hook's own eval may query the
129    // hook again (nested dependency realize), which must observe it still
130    // installed. Dropping the clone after the call is cheap.
131    let hook = REALIZE_HOOK.with(|h| h.borrow().clone());
132    let result = match hook {
133        Some(f) => f(drv_path, out_path),
134        None => Ok(()),
135    };
136    IN_FLIGHT.with(|f| {
137        let mut f = f.borrow_mut();
138        if let Some(pos) = f.iter().position(|p| p == out_path) {
139            f.remove(pos);
140        }
141    });
142
143    result.map(|()| true)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::sync::atomic::{AtomicUsize, Ordering};
150    use std::sync::Arc;
151
152    #[test]
153    fn no_hook_returns_false() {
154        // A fresh thread has no hook: realize is a no-op that reports "not
155        // realized" so the caller falls through to the normal read.
156        assert!(!has_realize_hook());
157        assert_eq!(realize_output("/nix/store/x.drv", "/nix/store/x-out").unwrap(), false);
158    }
159
160    #[test]
161    fn hook_is_invoked_with_drv_and_out() {
162        let seen: Arc<std::sync::Mutex<Vec<(String, String)>>> =
163            Arc::new(std::sync::Mutex::new(Vec::new()));
164        let seen2 = seen.clone();
165        let _guard = install_realize_hook(Box::new(move |drv, out| {
166            seen2.lock().unwrap().push((drv.to_string(), out.to_string()));
167            Ok(())
168        }));
169        assert!(has_realize_hook());
170        assert_eq!(realize_output("/nix/store/a.drv", "/nix/store/a-out").unwrap(), true);
171        let s = seen.lock().unwrap();
172        assert_eq!(s.len(), 1);
173        assert_eq!(s[0].0, "/nix/store/a.drv");
174        assert_eq!(s[0].1, "/nix/store/a-out");
175    }
176
177    #[test]
178    fn guard_uninstalls_on_drop() {
179        assert!(!has_realize_hook());
180        {
181            let _guard = install_realize_hook(Box::new(|_, _| Ok(())));
182            assert!(has_realize_hook());
183        }
184        assert!(!has_realize_hook());
185    }
186
187    #[test]
188    fn hook_error_propagates() {
189        let _guard = install_realize_hook(Box::new(|_, _| Err("boom".to_string())));
190        let e = realize_output("/nix/store/b.drv", "/nix/store/b-out").unwrap_err();
191        assert!(e.contains("boom"));
192    }
193
194    #[test]
195    fn reentrancy_cycle_is_refused() {
196        // A hook that tries to realize the SAME out_path it is already realizing
197        // must be refused, not recurse forever.
198        let depth = Arc::new(AtomicUsize::new(0));
199        let depth2 = depth.clone();
200        let _guard = install_realize_hook(Box::new(move |drv, out| {
201            depth2.fetch_add(1, Ordering::SeqCst);
202            // Re-enter with the same out_path — must return an Err (cycle),
203            // NOT recurse into the hook again.
204            realize_output(drv, out)?;
205            Ok(())
206        }));
207        let e = realize_output("/nix/store/c.drv", "/nix/store/c-out").unwrap_err();
208        assert!(e.contains("cycle"), "expected a cycle error, got: {e}");
209        // The outer hook ran exactly once; the inner re-entry was refused before
210        // invoking the hook a second time.
211        assert_eq!(depth.load(Ordering::SeqCst), 1);
212    }
213
214    #[test]
215    fn distinct_outputs_do_not_false_cycle() {
216        // Realizing one output whose hook realizes a DIFFERENT output is fine
217        // (a real dependency chain), not a cycle.
218        let count = Arc::new(AtomicUsize::new(0));
219        let count2 = count.clone();
220        let _guard = install_realize_hook(Box::new(move |_drv, out| {
221            let n = count2.fetch_add(1, Ordering::SeqCst);
222            if n == 0 && out == "/nix/store/outer" {
223                // Nested realize of a distinct output succeeds.
224                realize_output("/nix/store/inner.drv", "/nix/store/inner")?;
225            }
226            Ok(())
227        }));
228        assert_eq!(realize_output("/nix/store/outer.drv", "/nix/store/outer").unwrap(), true);
229        assert_eq!(count.load(Ordering::SeqCst), 2);
230    }
231}