use std::cell::RefCell;
use std::rc::Rc;
pub type RealizeFn = dyn Fn(&str, &str) -> Result<(), String>;
thread_local! {
static REALIZE_HOOK: RefCell<Option<Rc<RealizeFn>>> = const { RefCell::new(None) };
static IN_FLIGHT: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
#[must_use = "the returned guard uninstalls the hook when dropped"]
pub fn install_realize_hook(hook: Box<RealizeFn>) -> RealizeHookGuard {
let previous = REALIZE_HOOK.with(|h| h.borrow_mut().replace(Rc::from(hook)));
RealizeHookGuard { previous: Some(previous) }
}
pub struct RealizeHookGuard {
previous: Option<Option<Rc<RealizeFn>>>,
}
impl Drop for RealizeHookGuard {
fn drop(&mut self) {
if let Some(prev) = self.previous.take() {
REALIZE_HOOK.with(|h| *h.borrow_mut() = prev);
}
}
}
#[must_use]
pub fn has_realize_hook() -> bool {
REALIZE_HOOK.with(|h| h.borrow().is_some())
}
pub fn realize_output(drv_path: &str, out_path: &str) -> Result<bool, String> {
let hook_present = REALIZE_HOOK.with(|h| h.borrow().is_some());
if !hook_present {
return Ok(false);
}
let already_in_flight = IN_FLIGHT.with(|f| f.borrow().iter().any(|p| p == out_path));
if already_in_flight {
return Err(format!(
"import-from-derivation cycle: realizing '{out_path}' requires evaluating its own realize"
));
}
IN_FLIGHT.with(|f| f.borrow_mut().push(out_path.to_string()));
let hook = REALIZE_HOOK.with(|h| h.borrow().clone());
let result = match hook {
Some(f) => f(drv_path, out_path),
None => Ok(()),
};
IN_FLIGHT.with(|f| {
let mut f = f.borrow_mut();
if let Some(pos) = f.iter().position(|p| p == out_path) {
f.remove(pos);
}
});
result.map(|()| true)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[test]
fn no_hook_returns_false() {
assert!(!has_realize_hook());
assert_eq!(realize_output("/nix/store/x.drv", "/nix/store/x-out").unwrap(), false);
}
#[test]
fn hook_is_invoked_with_drv_and_out() {
let seen: Arc<std::sync::Mutex<Vec<(String, String)>>> =
Arc::new(std::sync::Mutex::new(Vec::new()));
let seen2 = seen.clone();
let _guard = install_realize_hook(Box::new(move |drv, out| {
seen2.lock().unwrap().push((drv.to_string(), out.to_string()));
Ok(())
}));
assert!(has_realize_hook());
assert_eq!(realize_output("/nix/store/a.drv", "/nix/store/a-out").unwrap(), true);
let s = seen.lock().unwrap();
assert_eq!(s.len(), 1);
assert_eq!(s[0].0, "/nix/store/a.drv");
assert_eq!(s[0].1, "/nix/store/a-out");
}
#[test]
fn guard_uninstalls_on_drop() {
assert!(!has_realize_hook());
{
let _guard = install_realize_hook(Box::new(|_, _| Ok(())));
assert!(has_realize_hook());
}
assert!(!has_realize_hook());
}
#[test]
fn hook_error_propagates() {
let _guard = install_realize_hook(Box::new(|_, _| Err("boom".to_string())));
let e = realize_output("/nix/store/b.drv", "/nix/store/b-out").unwrap_err();
assert!(e.contains("boom"));
}
#[test]
fn reentrancy_cycle_is_refused() {
let depth = Arc::new(AtomicUsize::new(0));
let depth2 = depth.clone();
let _guard = install_realize_hook(Box::new(move |drv, out| {
depth2.fetch_add(1, Ordering::SeqCst);
realize_output(drv, out)?;
Ok(())
}));
let e = realize_output("/nix/store/c.drv", "/nix/store/c-out").unwrap_err();
assert!(e.contains("cycle"), "expected a cycle error, got: {e}");
assert_eq!(depth.load(Ordering::SeqCst), 1);
}
#[test]
fn distinct_outputs_do_not_false_cycle() {
let count = Arc::new(AtomicUsize::new(0));
let count2 = count.clone();
let _guard = install_realize_hook(Box::new(move |_drv, out| {
let n = count2.fetch_add(1, Ordering::SeqCst);
if n == 0 && out == "/nix/store/outer" {
realize_output("/nix/store/inner.drv", "/nix/store/inner")?;
}
Ok(())
}));
assert_eq!(realize_output("/nix/store/outer.drv", "/nix/store/outer").unwrap(), true);
assert_eq!(count.load(Ordering::SeqCst), 2);
}
}