From 40303c27796a557e50fc7fe5fb4fbb1329851fe3 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Wed, 15 Oct 2025 18:18:20 +0200 Subject: [PATCH 1/8] feat: nix_store::store add get_fs_closure function --- rust/nix-bindings-store/src/store.rs | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/rust/nix-bindings-store/src/store.rs b/rust/nix-bindings-store/src/store.rs index 4650535..0da9ebc 100644 --- a/rust/nix-bindings-store/src/store.rs +++ b/rust/nix-bindings-store/src/store.rs @@ -65,6 +65,25 @@ lazy_static! { static ref STORE_CACHE: Arc> = Arc::new(Mutex::new(HashMap::new())); } +unsafe extern "C" fn callback_get_result_store_path_set( + user_data: *mut std::os::raw::c_void, + store_path: *const raw::StorePath, +) { + let ret = user_data as *mut Vec; + let ret: &mut Vec = &mut *ret; + + let store_path = raw::store_path_clone(store_path); + + let store_path = + NonNull::new(store_path).expect("nix_store_parse_path returned a null pointer"); + let store_path = StorePath::new_raw(store_path); + ret.push(store_path); +} + +fn callback_get_result_store_path_set_data(vec: &mut Vec) -> *mut std::os::raw::c_void { + vec as *mut Vec as *mut std::os::raw::c_void +} + pub struct Store { inner: Arc, /* An error context to reuse. This way we don't have to allocate them for each store operation. */ @@ -232,6 +251,30 @@ impl Store { r } + #[doc(alias = "nix_store_get_fs_closure")] + pub fn get_fs_closure( + &mut self, + store_path: &StorePath, + flip_direction: bool, + include_outputs: bool, + include_derivers: bool, + ) -> Result> { + let mut r = Vec::new(); + unsafe { + check_call!(raw::store_get_fs_closure( + &mut self.context, + self.inner.ptr(), + store_path.as_ptr(), + flip_direction, + include_outputs, + include_derivers, + callback_get_result_store_path_set_data(&mut r), + Some(callback_get_result_store_path_set) + )) + }?; + Ok(r) + } + pub fn weak_ref(&self) -> StoreWeak { StoreWeak { inner: Arc::downgrade(&self.inner), From 2d210260f92da9391d8b21a1b57d169b5fe6fe8c Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 15 Oct 2025 18:45:55 +0200 Subject: [PATCH 2/8] maint: Remove unintentional addition --- .claude/settings.local.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 1b694a3..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/claude-code-settings.json", - "permissions": { - "allow": [ - "Bash(nix flake check:*)", - "Bash(git grep:*)", - "Bash(git cherry-pick:*)", - "Bash(sed:*)" - ], - "deny": [] - } -} \ No newline at end of file From da869e998cedb34b45cea1c986871bbae3779a54 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 16 Oct 2025 00:40:52 +0200 Subject: [PATCH 3/8] feat: Store::realise, Store::add_derivation, Store::derivation_from_json --- rust/Cargo.lock | 2 + rust/nix-bindings-store/Cargo.toml | 4 + rust/nix-bindings-store/src/derivation.rs | 21 + rust/nix-bindings-store/src/lib.rs | 1 + rust/nix-bindings-store/src/store.rs | 459 +++++++++++++++++++++- 5 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 rust/nix-bindings-store/src/derivation.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 54f6191..416f121 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -264,10 +264,12 @@ name = "nix-bindings-store" version = "0.1.0" dependencies = [ "anyhow", + "ctor", "lazy_static", "nix-bindings-bindgen-raw", "nix-bindings-util", "pkg-config", + "tempfile", ] [[package]] diff --git a/rust/nix-bindings-store/Cargo.toml b/rust/nix-bindings-store/Cargo.toml index cc92326..c1300b7 100644 --- a/rust/nix-bindings-store/Cargo.toml +++ b/rust/nix-bindings-store/Cargo.toml @@ -14,5 +14,9 @@ nix-bindings-util = { path = "../nix-bindings-util" } nix-bindings-bindgen-raw = { path = "../nix-bindings-bindgen-raw" } lazy_static = "1.4" +[dev-dependencies] +ctor = "0.2" +tempfile = "3.10" + [build-dependencies] pkg-config = "0.3" diff --git a/rust/nix-bindings-store/src/derivation.rs b/rust/nix-bindings-store/src/derivation.rs new file mode 100644 index 0000000..6d05549 --- /dev/null +++ b/rust/nix-bindings-store/src/derivation.rs @@ -0,0 +1,21 @@ +use nix_bindings_bindgen_raw as raw; +use std::ptr::NonNull; + +/// A Nix derivation +pub struct Derivation { + pub(crate) inner: NonNull, +} + +impl Derivation { + pub(crate) fn new_raw(inner: NonNull) -> Self { + Derivation { inner } + } +} + +impl Drop for Derivation { + fn drop(&mut self) { + unsafe { + raw::derivation_free(self.inner.as_ptr()); + } + } +} diff --git a/rust/nix-bindings-store/src/lib.rs b/rust/nix-bindings-store/src/lib.rs index 5c57e2c..6010f2e 100644 --- a/rust/nix-bindings-store/src/lib.rs +++ b/rust/nix-bindings-store/src/lib.rs @@ -1,2 +1,3 @@ +pub mod derivation; pub mod path; pub mod store; diff --git a/rust/nix-bindings-store/src/store.rs b/rust/nix-bindings-store/src/store.rs index 0da9ebc..b8cfb06 100644 --- a/rust/nix-bindings-store/src/store.rs +++ b/rust/nix-bindings-store/src/store.rs @@ -4,12 +4,13 @@ use nix_bindings_bindgen_raw as raw; use nix_bindings_util::context::Context; use nix_bindings_util::string_return::{callback_get_result_string, callback_get_result_string_data}; use nix_bindings_util::{check_call, result_string_init}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::ffi::{c_char, CString}; use std::ptr::null_mut; use std::ptr::NonNull; use std::sync::{Arc, Mutex, Weak}; +use crate::derivation::Derivation; use crate::path::StorePath; /* TODO make Nix itself thread safe */ @@ -66,6 +67,7 @@ lazy_static! { } unsafe extern "C" fn callback_get_result_store_path_set( + _context: *mut raw::c_context, user_data: *mut std::os::raw::c_void, store_path: *const raw::StorePath, ) { @@ -251,6 +253,106 @@ impl Store { r } + /// Parse a derivation from JSON. + /// + /// The JSON format follows the [Nix derivation JSON schema](https://nix.dev/manual/nix/latest/protocols/json/derivation.html). + /// Note that this format is experimental as of writing. + /// The derivation is not added to the store; use [`Store::add_derivation`] for that. + /// + /// # Parameters + /// - `json`: A JSON string representing the derivation + /// + /// # Returns + /// A [`Derivation`] object if parsing succeeds, or an error if the JSON is invalid + /// or malformed. + #[doc(alias = "nix_derivation_from_json")] + pub fn derivation_from_json(&mut self, json: &str) -> Result { + let json_cstr = CString::new(json)?; + unsafe { + let drv = check_call!(raw::derivation_from_json( + &mut self.context, + self.inner.ptr(), + json_cstr.as_ptr() + ))?; + let inner = NonNull::new(drv) + .ok_or_else(|| Error::msg("derivation_from_json returned null"))?; + Ok(Derivation::new_raw(inner)) + } + } + + /// Add a derivation to the store. + /// + /// This computes the store path for the derivation and registers it in the store. + /// The derivation itself is written to the store as a `.drv` file. + /// + /// # Parameters + /// - `drv`: The derivation to add + /// + /// # Returns + /// The store path of the derivation (ending in `.drv`). + #[doc(alias = "nix_add_derivation")] + pub fn add_derivation(&mut self, drv: &Derivation) -> Result { + unsafe { + let path = check_call!(raw::add_derivation( + &mut self.context, + self.inner.ptr(), + drv.inner.as_ptr() + ))?; + let path = NonNull::new(path) + .ok_or_else(|| Error::msg("add_derivation returned null"))?; + Ok(StorePath::new_raw(path)) + } + } + + /// Build a derivation and return its outputs. + /// + /// This builds the derivation at the given store path and returns a map of output + /// names to their realized store paths. The derivation must already exist in the store + /// (see [`Store::add_derivation`]). + /// + /// # Parameters + /// - `path`: The store path of the derivation to build (typically ending in `.drv`) + /// + /// # Returns + /// A [`BTreeMap`] mapping output names (e.g., "out", "dev", "doc") to their store paths. + /// The map is ordered alphabetically by output name for deterministic iteration. + #[doc(alias = "nix_store_realise")] + pub fn realise(&mut self, path: &StorePath) -> Result> { + let mut outputs = BTreeMap::new(); + let userdata = &mut outputs as *mut BTreeMap as *mut std::os::raw::c_void; + + unsafe extern "C" fn callback( + userdata: *mut std::os::raw::c_void, + outname: *const c_char, + out_path: *const raw::StorePath, + ) { + let outputs = userdata as *mut BTreeMap; + let outputs = &mut *outputs; + + let name = std::ffi::CStr::from_ptr(outname) + .to_string_lossy() + .into_owned(); + + let path = raw::store_path_clone(out_path); + let path = NonNull::new(path).expect("store_path_clone returned null"); + let path = StorePath::new_raw(path); + + outputs.insert(name, path); + } + + unsafe { + check_call!(raw::store_realise( + &mut self.context, + self.inner.ptr(), + path.as_ptr(), + userdata, + Some(callback) + ))?; + } + + Ok(outputs) + } + #[doc(alias = "nix_store_get_fs_closure")] pub fn get_fs_closure( &mut self, @@ -294,9 +396,32 @@ impl Clone for Store { #[cfg(test)] mod tests { use std::collections::HashMap; + use ctor::ctor; use super::*; + #[ctor] + fn test_setup() { + // Initialize settings for tests + let _ = INIT.as_ref(); + + // Enable ca-derivations for all tests + nix_bindings_util::settings::set("experimental-features", "ca-derivations").ok(); + + // Disable build hooks to prevent test recursion + nix_bindings_util::settings::set("build-hook", "").ok(); + + // Set custom build dir for sandbox + if cfg!(target_os = "linux") { + nix_bindings_util::settings::set("sandbox-build-dir", "/custom-build-dir-for-test").ok(); + } + + std::env::set_var("_NIX_TEST_NO_SANDBOX", "1"); + + // Tests run offline + nix_bindings_util::settings::set("substituters", "").ok(); + } + #[test] fn none_works() { let res = Store::open(None, HashMap::new()); @@ -381,4 +506,336 @@ mod tests { assert!(weak.upgrade().is_none()); assert!(weak.inner.upgrade().is_none()); } + + fn create_temp_store() -> (Store, tempfile::TempDir) { + let temp_dir = tempfile::tempdir().unwrap(); + + let store_dir = temp_dir.path().join("store"); + let state_dir = temp_dir.path().join("state"); + let log_dir = temp_dir.path().join("log"); + + let store_dir_str = store_dir.to_str().unwrap(); + let state_dir_str = state_dir.to_str().unwrap(); + let log_dir_str = log_dir.to_str().unwrap(); + + let params = vec![ + ("store", store_dir_str), + ("state", state_dir_str), + ("log", log_dir_str), + ]; + + let store = Store::open(Some("local"), params).unwrap(); + (store, temp_dir) + } + + fn current_system() -> Result { + nix_bindings_util::settings::get("system") + } + + fn create_test_derivation_json() -> String { + let system = current_system().unwrap_or_else(|_| { + // Fallback to Rust's platform detection + format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS) + }); + format!( + r#"{{ + "args": ["-c", "echo $name foo > $out"], + "builder": "/bin/sh", + "env": {{ + "builder": "/bin/sh", + "name": "myname", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "system": "{}" + }}, + "inputDrvs": {{}}, + "inputSrcs": [], + "name": "myname", + "outputs": {{ + "out": {{ + "hashAlgo": "sha256", + "method": "nar" + }} + }}, + "system": "{}", + "version": 3 + }}"#, + system, system + ) + } + + #[test] + fn derivation_from_json() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + // If we got here, parsing succeeded + drop(drv); + drop(store); + drop(temp_dir); + } + + #[test] + fn derivation_from_invalid_json() { + let (mut store, temp_dir) = create_temp_store(); + let result = store.derivation_from_json("not valid json"); + assert!(result.is_err()); + drop(store); + drop(temp_dir); + } + + #[test] + fn add_derivation() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Verify we got a .drv path + let name = drv_path.name().unwrap(); + assert!(name.ends_with(".drv")); + + drop(store); + drop(temp_dir); + } + + #[test] + fn realise() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation + let outputs = store.realise(&drv_path).unwrap(); + + // Verify we got the expected output + assert!(outputs.contains_key("out")); + let out_path = &outputs["out"]; + let out_name = out_path.name().unwrap(); + assert_eq!(out_name, "myname"); + + drop(store); + drop(temp_dir); + } + + fn create_multi_output_derivation_json() -> String { + let system = current_system().unwrap_or_else(|_| { + format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS) + }); + + format!( + r#"{{ + "version": 3, + "name": "multi-output-test", + "system": "{}", + "builder": "/bin/sh", + "args": ["-c", "echo a > $outa; echo b > $outb; echo c > $outc; echo d > $outd; echo e > $oute; echo f > $outf; echo g > $outg; echo h > $outh; echo i > $outi; echo j > $outj"], + "env": {{ + "builder": "/bin/sh", + "name": "multi-output-test", + "system": "{}", + "outf": "/1vkfzqpwk313b51x0xjyh5s7w1lx141mr8da3dr9wqz5aqjyr2fh", + "outd": "/1ypxifgmbzp5sd0pzsp2f19aq68x5215260z3lcrmy5fch567lpm", + "outi": "/1wmasjnqi12j1mkjbxazdd0qd0ky6dh1qry12fk8qyp5kdamhbdx", + "oute": "/1f9r2k1s168js509qlw8a9di1qd14g5lqdj5fcz8z7wbqg11qp1f", + "outh": "/1rkx1hmszslk5nq9g04iyvh1h7bg8p92zw0hi4155hkjm8bpdn95", + "outc": "/1rj4nsf9pjjqq9jsq58a2qkwa7wgvgr09kgmk7mdyli6h1plas4w", + "outb": "/1p7i1dxifh86xq97m5kgb44d7566gj7rfjbw7fk9iij6ca4akx61", + "outg": "/14f8qi0r804vd6a6v40ckylkk1i6yl6fm243qp6asywy0km535lc", + "outj": "/0gkw1366qklqfqb2lw1pikgdqh3cmi3nw6f1z04an44ia863nxaz", + "outa": "/039akv9zfpihrkrv4pl54f3x231x362bll9afblsgfqgvx96h198" + }}, + "inputDrvs": {{}}, + "inputSrcs": [], + "outputs": {{ + "outd": {{ "hashAlgo": "sha256", "method": "nar" }}, + "outf": {{ "hashAlgo": "sha256", "method": "nar" }}, + "outg": {{ "hashAlgo": "sha256", "method": "nar" }}, + "outb": {{ "hashAlgo": "sha256", "method": "nar" }}, + "outc": {{ "hashAlgo": "sha256", "method": "nar" }}, + "outi": {{ "hashAlgo": "sha256", "method": "nar" }}, + "outj": {{ "hashAlgo": "sha256", "method": "nar" }}, + "outh": {{ "hashAlgo": "sha256", "method": "nar" }}, + "outa": {{ "hashAlgo": "sha256", "method": "nar" }}, + "oute": {{ "hashAlgo": "sha256", "method": "nar" }} + }} + }}"#, + system, system + ) + } + + #[test] + fn realise_multi_output_ordering() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_multi_output_derivation_json(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation + let outputs = store.realise(&drv_path).unwrap(); + + // Verify outputs are complete (BTreeMap guarantees ordering) + let output_names: Vec<&String> = outputs.keys().collect(); + let expected_order = vec!["outa", "outb", "outc", "outd", "oute", "outf", "outg", "outh", "outi", "outj"]; + assert_eq!(output_names, expected_order); + + drop(store); + drop(temp_dir); + } + + #[test] + fn realise_invalid_system() { + let (mut store, temp_dir) = create_temp_store(); + + // Create a derivation with an invalid system + let system = "bogus65-bogusos"; + let drv_json = format!( + r#"{{ + "args": ["-c", "echo $name foo > $out"], + "builder": "/bin/sh", + "env": {{ + "builder": "/bin/sh", + "name": "myname", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "system": "{}" + }}, + "inputDrvs": {{}}, + "inputSrcs": [], + "name": "myname", + "outputs": {{ + "out": {{ + "hashAlgo": "sha256", + "method": "nar" + }} + }}, + "system": "{}", + "version": 3 + }}"#, + system, system + ); + + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Try to build - should fail + let result = store.realise(&drv_path); + let err = match result { + Ok(_) => panic!("Build should fail with invalid system"), + Err(e) => e.to_string(), + }; + assert!( + err.contains("required system or feature not available"), + "Error should mention system not available, got: {}", + err + ); + + drop(store); + drop(temp_dir); + } + + #[test] + fn realise_builder_fails() { + let (mut store, temp_dir) = create_temp_store(); + + let system = current_system().unwrap_or_else(|_| { + format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS) + }); + + // Create a derivation where the builder exits with error + let drv_json = format!( + r#"{{ + "args": ["-c", "exit 1"], + "builder": "/bin/sh", + "env": {{ + "builder": "/bin/sh", + "name": "failing", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "system": "{}" + }}, + "inputDrvs": {{}}, + "inputSrcs": [], + "name": "failing", + "outputs": {{ + "out": {{ + "hashAlgo": "sha256", + "method": "nar" + }} + }}, + "system": "{}", + "version": 3 + }}"#, + system, system + ); + + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Try to build - should fail + let result = store.realise(&drv_path); + let err = match result { + Ok(_) => panic!("Build should fail when builder exits with error"), + Err(e) => e.to_string(), + }; + assert!( + err.contains("builder failed with exit code 1"), + "Error should mention builder failed with exit code, got: {}", + err + ); + + drop(store); + drop(temp_dir); + } + + #[test] + fn realise_builder_no_output() { + let (mut store, temp_dir) = create_temp_store(); + + let system = current_system().unwrap_or_else(|_| { + format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS) + }); + + // Create a derivation where the builder succeeds but produces no output + let drv_json = format!( + r#"{{ + "args": ["-c", "true"], + "builder": "/bin/sh", + "env": {{ + "builder": "/bin/sh", + "name": "no-output", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "system": "{}" + }}, + "inputDrvs": {{}}, + "inputSrcs": [], + "name": "no-output", + "outputs": {{ + "out": {{ + "hashAlgo": "sha256", + "method": "nar" + }} + }}, + "system": "{}", + "version": 3 + }}"#, + system, system + ); + + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Try to build - should fail + let result = store.realise(&drv_path); + let err = match result { + Ok(_) => panic!("Build should fail when builder produces no output"), + Err(e) => e.to_string(), + }; + assert!( + err.contains("failed to produce output path"), + "Error should mention failed to produce output, got: {}", + err + ); + + drop(store); + drop(temp_dir); + } } From 03c0dac5b324853c86cb2d2cdf23342e23f1b32f Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 16 Oct 2025 01:30:14 +0200 Subject: [PATCH 4/8] test: Test and document Store::get_fs_closure --- rust/nix-bindings-store/src/store.rs | 111 +++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/rust/nix-bindings-store/src/store.rs b/rust/nix-bindings-store/src/store.rs index b8cfb06..64864e5 100644 --- a/rust/nix-bindings-store/src/store.rs +++ b/rust/nix-bindings-store/src/store.rs @@ -353,6 +353,22 @@ impl Store { Ok(outputs) } + /// Get the closure of a specific store path. + /// + /// Computes the filesystem closure (dependency graph) of a store path, with options + /// to control the direction and which related paths to include. + /// + /// # Parameters + /// - `store_path`: The path to compute the closure from + /// - `flip_direction`: If false, compute the forward closure (paths referenced by this path). + /// If true, compute the backward closure (paths that reference this path). + /// - `include_outputs`: When `flip_direction` is false: for any derivation in the closure, include its outputs. + /// When `flip_direction` is true: for any output in the closure, include derivations that produce it. + /// - `include_derivers`: When `flip_direction` is false: for any output in the closure, include the derivation that produced it. + /// When `flip_direction` is true: for any derivation in the closure, include its outputs. + /// + /// # Returns + /// A vector of store paths in the closure, in no particular order. #[doc(alias = "nix_store_get_fs_closure")] pub fn get_fs_closure( &mut self, @@ -838,4 +854,99 @@ mod tests { drop(store); drop(temp_dir); } + + #[test] + fn get_fs_closure_with_outputs() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation to get the output path + let outputs = store.realise(&drv_path).unwrap(); + let out_path = &outputs["out"]; + let out_path_name = out_path.name().unwrap(); + + // Get closure with include_outputs=true + let closure = store.get_fs_closure(&drv_path, false, true, false).unwrap(); + + // The closure should contain at least the derivation and its output + assert!(closure.len() >= 2, "Closure should contain at least drv and output"); + + // Verify the output path is in the closure + let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name); + assert!(out_in_closure, "Output path should be in closure when include_outputs=true"); + + drop(store); + drop(temp_dir); + } + + #[test] + fn get_fs_closure_without_outputs() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation to get the output path + let outputs = store.realise(&drv_path).unwrap(); + let out_path = &outputs["out"]; + let out_path_name = out_path.name().unwrap(); + + // Get closure with include_outputs=false + let closure = store.get_fs_closure(&drv_path, false, false, false).unwrap(); + + // Verify the output path is NOT in the closure + let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name); + assert!(!out_in_closure, "Output path should not be in closure when include_outputs=false"); + + drop(store); + drop(temp_dir); + } + + #[test] + fn get_fs_closure_flip_direction() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation to get the output path + let outputs = store.realise(&drv_path).unwrap(); + let out_path = &outputs["out"]; + let out_path_name = out_path.name().unwrap(); + + // Get closure with flip_direction=true (reverse dependencies) + let closure = store.get_fs_closure(&drv_path, true, true, false).unwrap(); + + // Verify the output path is NOT in the closure when direction is flipped + let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name); + assert!(!out_in_closure, "Output path should not be in closure when flip_direction=true"); + + drop(store); + drop(temp_dir); + } + + #[test] + fn get_fs_closure_include_derivers() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + let drv_path_name = drv_path.name().unwrap(); + + // Build the derivation to get the output path + let outputs = store.realise(&drv_path).unwrap(); + let out_path = &outputs["out"]; + + // Get closure of the output path with include_derivers=true + let closure = store.get_fs_closure(out_path, false, false, true).unwrap(); + + // Verify the derivation path is in the closure + let drv_in_closure = closure.iter().any(|p| p.name().unwrap() == drv_path_name); + assert!(drv_in_closure, "Derivation should be in closure when include_derivers=true"); + + drop(store); + drop(temp_dir); + } } From 01443c7f69fa3afe9b1bca4bb068da93375cb732 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 21 Oct 2025 23:18:33 +0200 Subject: [PATCH 5/8] maint: Add version bound to new additions --- rust/nix-bindings-store/build.rs | 2 +- rust/nix-bindings-store/src/derivation.rs | 4 +++ rust/nix-bindings-store/src/store.rs | 33 ++++++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/rust/nix-bindings-store/build.rs b/rust/nix-bindings-store/build.rs index f573fe8..e97be70 100644 --- a/rust/nix-bindings-store/build.rs +++ b/rust/nix-bindings-store/build.rs @@ -6,7 +6,7 @@ fn main() { // Unfortunately, Rust doesn't give us a "greater than" operator in conditional // compilation, so we pre-evaluate the version comparisons here, making use // of the multi-valued nature of Rust cfgs. - let relevant_versions = vec!["2.26"]; + let relevant_versions = vec!["2.26", "2.33"]; let versions = relevant_versions .iter() .map(|v| format!("\"{}\"", v)) diff --git a/rust/nix-bindings-store/src/derivation.rs b/rust/nix-bindings-store/src/derivation.rs index 6d05549..ff13c08 100644 --- a/rust/nix-bindings-store/src/derivation.rs +++ b/rust/nix-bindings-store/src/derivation.rs @@ -1,7 +1,11 @@ +#![cfg(nix_at_least = "2.33")] + use nix_bindings_bindgen_raw as raw; use std::ptr::NonNull; /// A Nix derivation +/// +/// **Requires Nix 2.33 or later.** pub struct Derivation { pub(crate) inner: NonNull, } diff --git a/rust/nix-bindings-store/src/store.rs b/rust/nix-bindings-store/src/store.rs index 64864e5..5e0885f 100644 --- a/rust/nix-bindings-store/src/store.rs +++ b/rust/nix-bindings-store/src/store.rs @@ -4,12 +4,15 @@ use nix_bindings_bindgen_raw as raw; use nix_bindings_util::context::Context; use nix_bindings_util::string_return::{callback_get_result_string, callback_get_result_string_data}; use nix_bindings_util::{check_call, result_string_init}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; +#[cfg(nix_at_least = "2.33")] +use std::collections::BTreeMap; use std::ffi::{c_char, CString}; use std::ptr::null_mut; use std::ptr::NonNull; use std::sync::{Arc, Mutex, Weak}; +#[cfg(nix_at_least = "2.33")] use crate::derivation::Derivation; use crate::path::StorePath; @@ -66,6 +69,7 @@ lazy_static! { static ref STORE_CACHE: Arc> = Arc::new(Mutex::new(HashMap::new())); } +#[cfg(nix_at_least = "2.33")] unsafe extern "C" fn callback_get_result_store_path_set( _context: *mut raw::c_context, user_data: *mut std::os::raw::c_void, @@ -82,6 +86,7 @@ unsafe extern "C" fn callback_get_result_store_path_set( ret.push(store_path); } +#[cfg(nix_at_least = "2.33")] fn callback_get_result_store_path_set_data(vec: &mut Vec) -> *mut std::os::raw::c_void { vec as *mut Vec as *mut std::os::raw::c_void } @@ -255,6 +260,8 @@ impl Store { /// Parse a derivation from JSON. /// + /// **Requires Nix 2.33 or later.** + /// /// The JSON format follows the [Nix derivation JSON schema](https://nix.dev/manual/nix/latest/protocols/json/derivation.html). /// Note that this format is experimental as of writing. /// The derivation is not added to the store; use [`Store::add_derivation`] for that. @@ -265,6 +272,7 @@ impl Store { /// # Returns /// A [`Derivation`] object if parsing succeeds, or an error if the JSON is invalid /// or malformed. + #[cfg(nix_at_least = "2.33")] #[doc(alias = "nix_derivation_from_json")] pub fn derivation_from_json(&mut self, json: &str) -> Result { let json_cstr = CString::new(json)?; @@ -282,6 +290,8 @@ impl Store { /// Add a derivation to the store. /// + /// **Requires Nix 2.33 or later.** + /// /// This computes the store path for the derivation and registers it in the store. /// The derivation itself is written to the store as a `.drv` file. /// @@ -290,6 +300,7 @@ impl Store { /// /// # Returns /// The store path of the derivation (ending in `.drv`). + #[cfg(nix_at_least = "2.33")] #[doc(alias = "nix_add_derivation")] pub fn add_derivation(&mut self, drv: &Derivation) -> Result { unsafe { @@ -306,6 +317,8 @@ impl Store { /// Build a derivation and return its outputs. /// + /// **Requires Nix 2.33 or later.** + /// /// This builds the derivation at the given store path and returns a map of output /// names to their realized store paths. The derivation must already exist in the store /// (see [`Store::add_derivation`]). @@ -316,6 +329,7 @@ impl Store { /// # Returns /// A [`BTreeMap`] mapping output names (e.g., "out", "dev", "doc") to their store paths. /// The map is ordered alphabetically by output name for deterministic iteration. + #[cfg(nix_at_least = "2.33")] #[doc(alias = "nix_store_realise")] pub fn realise(&mut self, path: &StorePath) -> Result> { let mut outputs = BTreeMap::new(); @@ -355,6 +369,8 @@ impl Store { /// Get the closure of a specific store path. /// + /// **Requires Nix 2.33 or later.** + /// /// Computes the filesystem closure (dependency graph) of a store path, with options /// to control the direction and which related paths to include. /// @@ -369,6 +385,7 @@ impl Store { /// /// # Returns /// A vector of store paths in the closure, in no particular order. + #[cfg(nix_at_least = "2.33")] #[doc(alias = "nix_store_get_fs_closure")] pub fn get_fs_closure( &mut self, @@ -548,6 +565,7 @@ mod tests { nix_bindings_util::settings::get("system") } + #[cfg(nix_at_least = "2.33")] fn create_test_derivation_json() -> String { let system = current_system().unwrap_or_else(|_| { // Fallback to Rust's platform detection @@ -580,6 +598,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn derivation_from_json() { let (mut store, temp_dir) = create_temp_store(); let drv_json = create_test_derivation_json(); @@ -591,6 +610,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn derivation_from_invalid_json() { let (mut store, temp_dir) = create_temp_store(); let result = store.derivation_from_json("not valid json"); @@ -600,6 +620,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn add_derivation() { let (mut store, temp_dir) = create_temp_store(); let drv_json = create_test_derivation_json(); @@ -615,6 +636,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn realise() { let (mut store, temp_dir) = create_temp_store(); let drv_json = create_test_derivation_json(); @@ -634,6 +656,7 @@ mod tests { drop(temp_dir); } + #[cfg(nix_at_least = "2.33")] fn create_multi_output_derivation_json() -> String { let system = current_system().unwrap_or_else(|_| { format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS) @@ -681,6 +704,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn realise_multi_output_ordering() { let (mut store, temp_dir) = create_temp_store(); let drv_json = create_multi_output_derivation_json(); @@ -700,6 +724,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn realise_invalid_system() { let (mut store, temp_dir) = create_temp_store(); @@ -750,6 +775,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn realise_builder_fails() { let (mut store, temp_dir) = create_temp_store(); @@ -803,6 +829,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn realise_builder_no_output() { let (mut store, temp_dir) = create_temp_store(); @@ -856,6 +883,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn get_fs_closure_with_outputs() { let (mut store, temp_dir) = create_temp_store(); let drv_json = create_test_derivation_json(); @@ -882,6 +910,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn get_fs_closure_without_outputs() { let (mut store, temp_dir) = create_temp_store(); let drv_json = create_test_derivation_json(); @@ -905,6 +934,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn get_fs_closure_flip_direction() { let (mut store, temp_dir) = create_temp_store(); let drv_json = create_test_derivation_json(); @@ -928,6 +958,7 @@ mod tests { } #[test] + #[cfg(nix_at_least = "2.33")] fn get_fs_closure_include_derivers() { let (mut store, temp_dir) = create_temp_store(); let drv_json = create_test_derivation_json(); From 220ff29bccb15964673a3efd61011abd1bfe9917 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 21 Oct 2025 23:21:25 +0200 Subject: [PATCH 6/8] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nix': 'github:NixOS/nix/ab095c029c7deef98b99c5249c09fe9a8a095800?narHash=sha256-JpSWFjOgPWeWb5bb%2BHMMGR%2BQ0dOH7nl2t4WgyBVaWx8%3D' (2025-09-01) → 'github:NixOS/nix/7e8db2eb59d8798047e8cc025a3eb18613a8918c?narHash=sha256-R6uBB3fef75wVM1OjiM0uYLLf2P5eTCWHPCQAbCaGzA%3D' (2025-10-21) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index dce5dbc..602976c 100644 --- a/flake.lock +++ b/flake.lock @@ -170,11 +170,11 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1756762578, - "narHash": "sha256-JpSWFjOgPWeWb5bb+HMMGR+Q0dOH7nl2t4WgyBVaWx8=", + "lastModified": 1761069056, + "narHash": "sha256-R6uBB3fef75wVM1OjiM0uYLLf2P5eTCWHPCQAbCaGzA=", "owner": "NixOS", "repo": "nix", - "rev": "ab095c029c7deef98b99c5249c09fe9a8a095800", + "rev": "7e8db2eb59d8798047e8cc025a3eb18613a8918c", "type": "github" }, "original": { From 18da552952b1ecb832bc914e7500a4ed18874995 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 23 Oct 2025 19:42:05 +0200 Subject: [PATCH 7/8] fix: Pre-enable ca-derivations in tests This way we don't run into any race conditions when individual tests enable the feature. Specifically this affects the initialization of our default store for testing. The test suite *should* not be all that sensitive to the environment, but that's some future work to make sure. --- rust/nci.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/nci.nix b/rust/nci.nix index fabe122..1509336 100644 --- a/rust/nci.nix +++ b/rust/nci.nix @@ -56,6 +56,10 @@ echo "Configuring relocated store at $NIX_REMOTE..." + # Create nix.conf with experimental features enabled + mkdir -p "$NIX_CONF_DIR" + echo "experimental-features = ca-derivations flakes" > "$NIX_CONF_DIR/nix.conf" + # Init ahead of time, because concurrent initialization is flaky ${ # Not using nativeBuildInputs because this should (hopefully) be From 510ba4abe231c8371c1717f588998d08fe7ebbbf Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 26 Oct 2025 22:49:14 +0100 Subject: [PATCH 8/8] fix: Uncrash the tests by keeping fetchers_settings around This is arguably a partial fix. This should either be modeled with lifetimes, or be addressed in Nix itself. --- rust/nix-bindings-flake/src/lib.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/rust/nix-bindings-flake/src/lib.rs b/rust/nix-bindings-flake/src/lib.rs index ef8a963..96d5852 100644 --- a/rust/nix-bindings-flake/src/lib.rs +++ b/rust/nix-bindings-flake/src/lib.rs @@ -267,6 +267,7 @@ mod tests { // Only set experimental-features once to minimize the window where // concurrent Nix operations might read the setting while it's being modified INIT.call_once(|| { + nix_bindings_expr::eval_state::init().unwrap(); nix_bindings_util::settings::set("experimental-features", "flakes").unwrap(); }); } @@ -299,6 +300,7 @@ mod tests { init(); let gc_registration = gc_register_my_thread(); let store = Store::open(None, []).unwrap(); + let fetchers_settings = FetchersSettings::new().unwrap(); let flake_settings = FlakeSettings::new().unwrap(); let mut eval_state = EvalStateBuilder::new(store) .unwrap() @@ -326,7 +328,7 @@ mod tests { let flake_lock_flags = FlakeLockFlags::new(&flake_settings).unwrap(); let (flake_ref, fragment) = FlakeReference::parse_with_fragment( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &FlakeReferenceParseFlags::new(&flake_settings).unwrap(), &format!("path:{}#subthing", tmp_dir.path().display()), @@ -336,7 +338,7 @@ mod tests { assert_eq!(fragment, "subthing"); let locked_flake = LockedFlake::lock( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &eval_state, &flake_lock_flags, @@ -353,6 +355,7 @@ mod tests { assert_eq!(hello, "potato"); + drop(fetchers_settings); drop(tmp_dir); drop(gc_registration); } @@ -362,6 +365,7 @@ mod tests { init(); let gc_registration = gc_register_my_thread(); let store = Store::open(None, []).unwrap(); + let fetchers_settings = FetchersSettings::new().unwrap(); let flake_settings = FlakeSettings::new().unwrap(); let mut eval_state = EvalStateBuilder::new(store) .unwrap() @@ -382,6 +386,8 @@ mod tests { let flake_dir_a_str = flake_dir_a.to_str().unwrap(); let flake_dir_c_str = flake_dir_c.to_str().unwrap(); + assert!(!flake_dir_a_str.is_empty()); + assert!(!flake_dir_c_str.is_empty()); // a std::fs::write( @@ -434,7 +440,7 @@ mod tests { .unwrap(); let (flake_ref_a, fragment) = FlakeReference::parse_with_fragment( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &flake_reference_parse_flags, &format!("path:{}", &flake_dir_a_str), @@ -448,7 +454,7 @@ mod tests { flake_lock_flags.set_mode_check().unwrap(); let locked_flake = LockedFlake::lock( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &eval_state, &flake_lock_flags, @@ -465,7 +471,7 @@ mod tests { flake_lock_flags.set_mode_virtual().unwrap(); let locked_flake = LockedFlake::lock( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &eval_state, &flake_lock_flags, @@ -487,7 +493,7 @@ mod tests { flake_lock_flags.set_mode_check().unwrap(); let locked_flake = LockedFlake::lock( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &eval_state, &flake_lock_flags, @@ -507,7 +513,7 @@ mod tests { flake_lock_flags.set_mode_write_as_needed().unwrap(); let locked_flake = LockedFlake::lock( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &eval_state, &flake_lock_flags, @@ -527,7 +533,7 @@ mod tests { flake_lock_flags.set_mode_check().unwrap(); let locked_flake = LockedFlake::lock( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &eval_state, &flake_lock_flags, @@ -548,7 +554,7 @@ mod tests { flake_lock_flags.set_mode_write_as_needed().unwrap(); let (flake_ref_c, fragment) = FlakeReference::parse_with_fragment( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &flake_reference_parse_flags, &format!("path:{}", &flake_dir_c_str), @@ -561,7 +567,7 @@ mod tests { .unwrap(); let locked_flake = LockedFlake::lock( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &eval_state, &flake_lock_flags, @@ -584,7 +590,7 @@ mod tests { flake_lock_flags.set_mode_check().unwrap(); let locked_flake = LockedFlake::lock( - &FetchersSettings::new().unwrap(), + &fetchers_settings, &flake_settings, &eval_state, &flake_lock_flags, @@ -599,6 +605,7 @@ mod tests { let hello = eval_state.require_string(&hello).unwrap(); assert_eq!(hello, "BOB"); + drop(fetchers_settings); drop(tmp_dir); drop(gc_registration); }