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}