Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 16 additions & 30 deletions crates/evaluator/src/builds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct DerivationInfo {
}

fn parse_derivation_infos(
store_dir: &Path,
value: &serde_json::Value,
) -> HashMap<String, DerivationInfo> {
let Some(derivations) = value
Expand All @@ -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::<HashMap<_, _>>()
})
.filter(|map| !map.is_empty());
let input_drvs = drv_val.get("inputDrvs").and_then(|v| {
serde_json::from_value::<HashMap<String, serde_json::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);

Expand All @@ -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")
Expand All @@ -133,7 +119,7 @@ async fn show_recursive_derivations(
return HashMap::new();
}
serde_json::from_slice::<serde_json::Value>(&output.stdout)
.map(|value| parse_derivation_infos(&value))
.map(|value| parse_derivation_infos(store_dir, &value))
.unwrap_or_default()
}

Expand Down
54 changes: 13 additions & 41 deletions crates/evaluator/src/nix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,8 @@ async fn resolve_drv(
}

let json = serde_json::from_slice::<serde_json::Value>(&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
Expand Down Expand Up @@ -708,7 +709,10 @@ struct ShownDerivation {
input_drvs: Option<HashMap<String, serde_json::Value>>,
}

fn parse_derivation_show(value: &serde_json::Value) -> Option<ShownDerivation> {
fn parse_derivation_show(
store_dir: &Path,
value: &serde_json::Value,
) -> Option<ShownDerivation> {
// Newer nix derivation show wraps output as
// {"derivations": {<drv>: {...}}, "version": N}; older nix keys the
// drv paths directly at the top level.
Expand All @@ -718,43 +722,11 @@ fn parse_derivation_show(value: &serde_json::Value) -> Option<ShownDerivation> {
.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::<HashMap<_, _>>()
})
.filter(|map| !map.is_empty());

let input_drvs = drv_val.get("inputDrvs").and_then(|v| {
serde_json::from_value::<HashMap<String, serde_json::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),
})
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
127 changes: 124 additions & 3 deletions crates/nix/src/derivation.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -108,6 +112,107 @@ pub fn drv_required_features(drv: &serde_json::Value) -> Vec<String> {
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<PathBuf> = 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<String> {
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<HashMap<String, String>> {
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::<HashMap<_, _>>();
(!outputs.is_empty()).then_some(outputs)
Comment on lines +188 to +196

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only accepts path or outPath but Nix 2.34 seems to also omit these fields for ordinary FODs, not just floating CA derivs. For example, an actual FOD produced:

{
  "derivations": {
    "difmpadv2zdrx2zjx93b7hz4j0bdlb4w-circus-review-fixed.drv": {
      "args": [
        "-c",
        "echo fixed > $out"
      ],
      "builder": "/bin/sh",
      "env": {
        "builder": "/bin/sh",
        "name": "circus-review-fixed",
        "out": "/nix/store/xxsm4d3ygbv8c7f7il5djgz5xfc8w2b3-circus-review-fixed",
        "outputHash": "992a93455c71fedd36ac9bbc439952c041cf61445958472af479269b8d873513",
        "outputHashAlgo": "sha256",
        "outputHashMode": "recursive",
        "system": "x86_64-linux"
      },
      "inputs": {
        "drvs": {},
        "srcs": []
      },
      "name": "circus-review-fixed",
      "outputs": {
        "out": {
          "hash": "sha256-mSqTRVxx/t02rJu8Q5lSwEHPYURZWEcq9Hkmm42HNRM=",
          "method": "nar"
        }
      },
      "system": "x86_64-linux",
      "version": 4
    }
  },
  "version": 4
}

Its output path was already known and present in env.out, but your helper actually returned None.

tl;dr: your previous CA-output note therefore applies more broadly than the new "floating content-addressed outputs" comment suggests. Fixed-output paths should be recovered separately from genuinely unknown floating outputs. I'm not going to treat this as a blocker exactly, but it is a gap. You can address it now, or, we can merge this and you can address it afterwards.5

}

/// 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<HashMap<String, serde_json::Value>> {
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::<HashMap<_, _>>();
(!inputs.is_empty()).then_some(inputs)
}

/// Construct the canonical `nix derivation show` command used to inspect
/// required system features.
#[must_use]
Expand Down Expand Up @@ -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"
);
}
}
Loading