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 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": { 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/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 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); } 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/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 new file mode 100644 index 0000000..ff13c08 --- /dev/null +++ b/rust/nix-bindings-store/src/derivation.rs @@ -0,0 +1,25 @@ +#![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, +} + +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 4650535..5e0885f 100644 --- a/rust/nix-bindings-store/src/store.rs +++ b/rust/nix-bindings-store/src/store.rs @@ -5,11 +5,15 @@ 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; +#[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; /* TODO make Nix itself thread safe */ @@ -65,6 +69,28 @@ 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, + 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); +} + +#[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 +} + 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 +258,158 @@ impl Store { r } + /// 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. + /// + /// # 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. + #[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)?; + 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. + /// + /// **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. + /// + /// # Parameters + /// - `drv`: The derivation to add + /// + /// # 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 { + 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. + /// + /// **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`]). + /// + /// # 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. + #[cfg(nix_at_least = "2.33")] + #[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) + } + + /// 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. + /// + /// # 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. + #[cfg(nix_at_least = "2.33")] + #[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), @@ -251,9 +429,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()); @@ -338,4 +539,445 @@ 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") + } + + #[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 + 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] + #[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(); + let drv = store.derivation_from_json(&drv_json).unwrap(); + // If we got here, parsing succeeded + drop(drv); + drop(store); + drop(temp_dir); + } + + #[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"); + assert!(result.is_err()); + drop(store); + drop(temp_dir); + } + + #[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(); + 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] + #[cfg(nix_at_least = "2.33")] + 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); + } + + #[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) + }); + + 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] + #[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(); + 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] + #[cfg(nix_at_least = "2.33")] + 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] + #[cfg(nix_at_least = "2.33")] + 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] + #[cfg(nix_at_least = "2.33")] + 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); + } + + #[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(); + 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] + #[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(); + 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] + #[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(); + 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] + #[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(); + 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); + } }