diff --git a/crates/evaluator/src/builds.rs b/crates/evaluator/src/builds.rs index c2f89581..6e2a8ca7 100644 --- a/crates/evaluator/src/builds.rs +++ b/crates/evaluator/src/builds.rs @@ -48,6 +48,7 @@ struct DerivationInfo { } fn parse_derivation_infos( + store_dir: &Path, value: &serde_json::Value, ) -> HashMap { let Some(derivations) = value @@ -61,35 +62,11 @@ fn parse_derivation_infos( derivations .iter() .map(|(drv_path, drv_val)| { - let drv_path = if drv_path.starts_with("/nix/store/") { - drv_path.clone() - } else { - format!("/nix/store/{drv_path}") - }; - let system = drv_val - .get("system") - .and_then(serde_json::Value::as_str) - .map(str::to_owned); - let outputs = drv_val - .get("outputs") - .and_then(serde_json::Value::as_object) - .map(|map| { - map - .iter() - .filter_map(|(name, output)| { - output - .get("path") - .or_else(|| output.get("outPath")) - .and_then(serde_json::Value::as_str) - .map(|path| (name.clone(), path.to_string())) - }) - .collect::>() - }) - .filter(|map| !map.is_empty()); - let input_drvs = drv_val.get("inputDrvs").and_then(|v| { - serde_json::from_value::>(v.clone()) - .ok() - }); + let drv_path = circus_nix::derivation::store_path(store_dir, drv_path); + let system = circus_nix::derivation::drv_system(drv_val); + let outputs = circus_nix::derivation::drv_outputs(store_dir, drv_val); + let input_drvs = + circus_nix::derivation::drv_input_drvs(store_dir, drv_val); let required_features = circus_nix::derivation::drv_required_features(drv_val); @@ -110,6 +87,15 @@ async fn show_recursive_derivations( if drv_paths.is_empty() { return HashMap::new(); } + + let store_dir = match circus_nix::derivation::store_dir().await { + Ok(dir) => dir, + Err(error) => { + tracing::warn!(%error, "failed to read the nix store directory"); + return HashMap::new(); + }, + }; + let mut command = Command::new("nix"); command .arg("derivation") @@ -133,7 +119,7 @@ async fn show_recursive_derivations( return HashMap::new(); } serde_json::from_slice::(&output.stdout) - .map(|value| parse_derivation_infos(&value)) + .map(|value| parse_derivation_infos(store_dir, &value)) .unwrap_or_default() } diff --git a/crates/evaluator/src/nix.rs b/crates/evaluator/src/nix.rs index 60e38cf5..194db8be 100644 --- a/crates/evaluator/src/nix.rs +++ b/crates/evaluator/src/nix.rs @@ -560,7 +560,8 @@ async fn resolve_drv( } let json = serde_json::from_slice::(&out.stdout).ok()?; - parse_derivation_show(&json) + let store_dir = circus_nix::derivation::store_dir().await.ok()?; + parse_derivation_show(store_dir, &json) } /// Legacy (non-flake) evaluation: import the nix expression file and evaluate @@ -708,7 +709,10 @@ struct ShownDerivation { input_drvs: Option>, } -fn parse_derivation_show(value: &serde_json::Value) -> Option { +fn parse_derivation_show( + store_dir: &Path, + value: &serde_json::Value, +) -> Option { // Newer nix derivation show wraps output as // {"derivations": {: {...}}, "version": N}; older nix keys the // drv paths directly at the top level. @@ -718,43 +722,11 @@ fn parse_derivation_show(value: &serde_json::Value) -> Option { .or_else(|| value.as_object())?; let (drv_path, drv_val) = derivations.iter().next()?; - let system = drv_val - .get("system") - .and_then(|v| v.as_str()) - .map(std::string::ToString::to_string); - - let outputs = drv_val - .get("outputs") - .and_then(serde_json::Value::as_object) - .map(|map| { - map - .iter() - .filter_map(|(name, output)| { - output - .get("path") - .or_else(|| output.get("outPath")) - .and_then(|v| v.as_str()) - .map(|path| (name.clone(), path.to_string())) - }) - .collect::>() - }) - .filter(|map| !map.is_empty()); - - let input_drvs = drv_val.get("inputDrvs").and_then(|v| { - serde_json::from_value::>(v.clone()).ok() - }); - - let drv_path = if drv_path.starts_with("/nix/store/") { - drv_path.clone() - } else { - format!("/nix/store/{drv_path}") - }; - Some(ShownDerivation { - drv_path, - system, - outputs, - input_drvs, + drv_path: circus_nix::derivation::store_path(store_dir, drv_path), + system: circus_nix::derivation::drv_system(drv_val), + outputs: circus_nix::derivation::drv_outputs(store_dir, drv_val), + input_drvs: circus_nix::derivation::drv_input_drvs(store_dir, drv_val), }) } @@ -869,7 +841,7 @@ mod meta_tests { "version": 3 }); - let shown = parse_derivation_show(&v); + let shown = parse_derivation_show(Path::new("/nix/store"), &v); assert!(shown.is_some()); let Some(shown) = shown else { return; @@ -1001,7 +973,7 @@ mod meta_tests { "version": 3 }); - let shown = parse_derivation_show(&v); + let shown = parse_derivation_show(Path::new("/nix/store"), &v); assert!(shown.is_some()); let Some(shown) = shown else { return; @@ -1044,7 +1016,7 @@ mod meta_tests { } }); - let shown = parse_derivation_show(&v); + let shown = parse_derivation_show(Path::new("/nix/store"), &v); assert!(shown.is_some()); let Some(shown) = shown else { return; diff --git a/crates/nix/src/derivation.rs b/crates/nix/src/derivation.rs index be53b732..1411b727 100644 --- a/crates/nix/src/derivation.rs +++ b/crates/nix/src/derivation.rs @@ -1,8 +1,12 @@ -//! Read `requiredSystemFeatures` out of `nix derivation show` output. +//! Read `requiredSystemFeatures`, outputs, and input derivations out of +//! `nix derivation show` output. -use std::collections::BTreeSet; +use std::{ + collections::{BTreeSet, HashMap}, + path::{Path, PathBuf}, +}; -use tokio::process::Command; +use tokio::{process::Command, sync::OnceCell}; use crate::{Error, Result}; @@ -108,6 +112,107 @@ pub fn drv_required_features(drv: &serde_json::Value) -> Vec { out.into_iter().collect() } +/// Store directory of the local Nix. Derivation show format v3 and later print +/// store paths as basenames, so the directory has to come from Nix itself. +/// +/// # Errors +/// +/// Returns [`Error::Eval`] when nix cannot be run or prints no directory. +pub async fn store_dir() -> Result<&'static Path> { + static STORE_DIR: OnceCell = OnceCell::const_new(); + + STORE_DIR + .get_or_try_init(|| { + async { + let out = Command::new("nix") + .args([ + "--extra-experimental-features", + "nix-command", + "eval", + "--raw", + "--expr", + "builtins.storeDir", + ]) + .kill_on_drop(true) + .output() + .await + .map_err(|e| Error::Eval(format!("failed to run nix eval: {e}")))?; + + let dir = String::from_utf8_lossy(&out.stdout).trim().to_owned(); + if !out.status.success() || !dir.starts_with('/') { + return Err(Error::Eval(format!( + "could not read the nix store directory: {}", + String::from_utf8_lossy(&out.stderr).trim() + ))); + } + + Ok(PathBuf::from(dir)) + } + }) + .await + .map(PathBuf::as_path) +} + +/// Absolute store path for a path printed by `nix derivation show`, which may +/// be a basename. +#[must_use] +pub fn store_path(store_dir: &Path, path: &str) -> String { + if path.starts_with('/') { + path.to_owned() + } else { + store_dir.join(path).to_string_lossy().into_owned() + } +} + +/// System a single derivation value builds for. +#[must_use] +pub fn drv_system(drv: &serde_json::Value) -> Option { + drv + .get("system") + .and_then(serde_json::Value::as_str) + .map(str::to_owned) +} + +/// Output paths of a single derivation value, keyed by output name. Floating +/// content-addressed outputs have no path until they are built, so they are +/// left out. +#[must_use] +pub fn drv_outputs( + store_dir: &Path, + drv: &serde_json::Value, +) -> Option> { + let outputs = drv + .get("outputs")? + .as_object()? + .iter() + .filter_map(|(name, output)| { + output + .get("path") + .or_else(|| output.get("outPath")) + .and_then(serde_json::Value::as_str) + .map(|path| (name.clone(), store_path(store_dir, path))) + }) + .collect::>(); + (!outputs.is_empty()).then_some(outputs) +} + +/// Input derivations of a single derivation value, keyed by store path, read +/// from `inputDrvs` or `inputs.drvs`. +#[must_use] +pub fn drv_input_drvs( + store_dir: &Path, + drv: &serde_json::Value, +) -> Option> { + let inputs = drv + .get("inputDrvs") + .or_else(|| drv.get("inputs")?.get("drvs"))? + .as_object()? + .iter() + .map(|(path, outputs)| (store_path(store_dir, path), outputs.clone())) + .collect::>(); + (!inputs.is_empty()).then_some(inputs) +} + /// Construct the canonical `nix derivation show` command used to inspect /// required system features. #[must_use] @@ -199,4 +304,20 @@ mod tests { "uid-range" ]); } + + #[test] + fn reads_basename_inputs_and_outputs() { + let drv = json!({ + "inputs": { "drvs": { "aaa-leaf.drv": ["out"] }, "srcs": [] }, + "outputs": { "out": { "path": "bbb-hidden" } } + }); + let store_dir = Path::new("/custom/store"); + + let inputs = drv_input_drvs(store_dir, &drv).expect("input drvs"); + assert!(inputs.contains_key("/custom/store/aaa-leaf.drv")); + assert_eq!( + drv_outputs(store_dir, &drv).expect("outputs")["out"], + "/custom/store/bbb-hidden" + ); + } }