From 0eb313e73b1d3fd026af8f26c3f61e09e573caed Mon Sep 17 00:00:00 2001 From: oxalica Date: Wed, 25 Jan 2023 12:34:25 +0800 Subject: [PATCH] Impl flake lock parser and wrapper of 'nix eval' --- Cargo.lock | 6 + crates/nix-interop/Cargo.toml | 4 + crates/nix-interop/src/eval.rs | 52 ++++++ crates/nix-interop/src/flake_lock.rs | 231 +++++++++++++++++++++++++++ crates/nix-interop/src/lib.rs | 3 + 5 files changed, 296 insertions(+) create mode 100644 crates/nix-interop/src/eval.rs create mode 100644 crates/nix-interop/src/flake_lock.rs diff --git a/Cargo.lock b/Cargo.lock index 62fb4cd..2d5cdf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,12 @@ dependencies = [ [[package]] name = "nix-interop" version = "0.0.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "serde_repr", +] [[package]] name = "num-traits" diff --git a/crates/nix-interop/Cargo.toml b/crates/nix-interop/Cargo.toml index bc21bd6..8d8d6d1 100644 --- a/crates/nix-interop/Cargo.toml +++ b/crates/nix-interop/Cargo.toml @@ -6,3 +6,7 @@ license = "MIT OR Apache-2.0" rust-version = "1.66" [dependencies] +anyhow = "1.0.68" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" +serde_repr = "0.1.10" diff --git a/crates/nix-interop/src/eval.rs b/crates/nix-interop/src/eval.rs new file mode 100644 index 0000000..f661ffd --- /dev/null +++ b/crates/nix-interop/src/eval.rs @@ -0,0 +1,52 @@ +//! Wrapper for `nix eval`. +use std::path::Path; +use std::process::{Command, Stdio}; + +use anyhow::{ensure, Result}; +use serde::de::DeserializeOwned; + +pub fn nix_eval_expr_json(nix_command: &Path, expr: &str) -> Result { + let output = Command::new(nix_command) + .args([ + "eval", + "--experimental-features", + "nix-command", + "--read-only", + "--json", + "--expr", + expr, + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output()?; + + ensure!( + output.status.success(), + "Nix eval failed with {}.\nExpression: {}\nStderr: {}", + output.status, + expr, + String::from_utf8_lossy(&output.stderr), + ); + + let val = serde_json::from_slice(&output.stdout)?; + Ok(val) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore = "requires calling 'nix'"] + fn nix_eval_simple() { + let ret = nix_eval_expr_json::("nix".as_ref(), "1 + 1").unwrap(); + assert_eq!(ret, 2); + } + + #[test] + #[ignore = "requires calling 'nix'"] + fn nix_eval_error() { + nix_eval_expr_json::("nix".as_ref(), "{ }.not-exist").unwrap_err(); + } +} diff --git a/crates/nix-interop/src/flake_lock.rs b/crates/nix-interop/src/flake_lock.rs new file mode 100644 index 0000000..195e276 --- /dev/null +++ b/crates/nix-interop/src/flake_lock.rs @@ -0,0 +1,231 @@ +//! Parser for the flake lock file. +//! +//! We want a custom `nix flake archive` without dumping the current flake +//! which may be very costly for large repositories like nixpkgs. +//! +//! https://github.com/NixOS/nix/blob/2.13.1/src/nix/flake.md#lock-files +use std::collections::HashMap; +use std::path::Path; + +use anyhow::{bail, ensure, Context, Result}; +use serde::Deserialize; +use serde_repr::Deserialize_repr; + +use crate::eval::nix_eval_expr_json; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ResolvedInput { + pub store_path: String, + pub is_flake: bool, +} + +/// Resolve all root inputs from a flake lock. +pub fn resolve_flake_locked_inputs( + nix_command: &Path, + lock_src: &[u8], +) -> Result> { + let lock = + serde_json::from_slice::(lock_src).context("Failed to parse flake lock")?; + let root_node = lock.nodes.get(&lock.root).context("Missing root node")?; + + // Resolve followed inputs. + let inputs = root_node + .inputs + .iter() + .map(|(input_name, input)| { + let name_seq = match input { + FlakeInput::Node(name) => std::slice::from_ref(name), + FlakeInput::Follow(name_seq) => name_seq, + }; + let target = name_seq.iter().try_fold(root_node, |node, input| { + match node + .inputs + .get(input) + .with_context(|| format!("Missing followed input {input:?}"))? + { + FlakeInput::Node(name) => lock + .nodes + .get(name) + .with_context(|| format!("Missing followed node {name:?}")), + FlakeInput::Follow(_) => bail!("Chained 'follows' is not supported"), + } + })?; + + let nar_hash = &target + .locked + .as_ref() + .with_context(|| format!("Flake input {input_name:?} is not locked"))? + .nar_hash; + + // Validate since we'll wrap this in Nix strings below. + ensure!( + nar_hash.bytes().all(|b| b != b'\\' && b != b'"'), + "Invalid nar hash" + ); + + Ok((input_name, target.flake, nar_hash)) + }) + .collect::>>()?; + + // Trivial case to skip calling Nix. + if inputs.is_empty() { + return Ok(HashMap::new()); + } + + let hashes = inputs + .iter() + .flat_map(|(_, _, hash)| ["\"", hash, "\" "]) + .collect::(); + let store_paths = nix_eval_expr_json::>( + nix_command, + &format!( + r#" + builtins.map (hash: (derivation {{ + name = "source"; + system = "dummy"; + builder = "dummy"; + outputHashMode = "recursive"; + outputHashAlgo = null; + outputHash = hash; + }}).outPath) [ {hashes} ] + "# + ), + )?; + + let resolved = std::iter::zip(inputs, store_paths) + .map(|((name, is_flake, _), store_path)| { + ( + name.to_owned(), + ResolvedInput { + is_flake, + store_path, + }, + ) + }) + .collect(); + Ok(resolved) +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +struct FlakeLock { + version: Version, + root: String, + nodes: HashMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize_repr)] +#[repr(u8)] +enum Version { + V7 = 7, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +struct FlakeNode { + #[serde(default)] + inputs: HashMap, + /// For the root node (the current flake), this is `None`. + locked: Option, + #[serde(default = "const_true")] + flake: bool, +} + +fn const_true() -> bool { + true +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(untagged)] +enum FlakeInput { + Node(String), + Follow(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +struct LockedFlakeRef { + nar_hash: String, + // ... +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore = "requires calling 'nix'"] + fn test_resolve_flake_lock_inputs() { + // { + // inputs.nixpkgs.url = "github:NixOS/nixpkgs/5ed481943351e9fd354aeb557679624224de38d5"; + // inputs.flake-utils = { + // url = "github:numtide/flake-utils/5aed5285a952e0b949eb3ba02c12fa4fcfef535f"; + // flake = false; + // }; + // outputs = { ... }: { }; + // } + let lock_src = br#" +{ + "nodes": { + "flake-utils": { + "flake": false, + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1674211260, + "narHash": "sha256-xU6Rv9sgnwaWK7tgCPadV6HhI2Y/fl4lKxJoG2+m9qs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5ed481943351e9fd354aeb557679624224de38d5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5ed481943351e9fd354aeb557679624224de38d5", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} + "#; + let got = resolve_flake_locked_inputs("nix".as_ref(), lock_src).unwrap(); + let expect = HashMap::from_iter([ + ( + "nixpkgs".to_owned(), + ResolvedInput { + store_path: "/nix/store/hap5a6iw5rccl21adfxh5b3lk2c8qnmj-source".to_owned(), + is_flake: true, + }, + ), + ( + "flake-utils".to_owned(), + ResolvedInput { + store_path: "/nix/store/sk4ga2wy0b02k7pnzakwq4r3jdknda4g-source".to_owned(), + is_flake: false, + }, + ), + ]); + assert_eq!(got, expect); + } +} diff --git a/crates/nix-interop/src/lib.rs b/crates/nix-interop/src/lib.rs index 835a4e7..77433b2 100644 --- a/crates/nix-interop/src/lib.rs +++ b/crates/nix-interop/src/lib.rs @@ -1,4 +1,7 @@ //! Nix defined file structures and interoperation with Nix. +pub mod eval; +pub mod flake_lock; + pub const DEFAULT_IMPORT_FILE: &str = "default.nix"; pub const FLAKE_FILE: &str = "flake.nix"; pub const FLAKE_LOCK_FILE: &str = "flake.lock";