From 8f6ec2ec5c3ba8ab33126ec79de7702835592902 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Mon, 5 Jan 2026 20:57:51 -0500 Subject: [PATCH 1/4] Fix path coercion by calling eval_state_builder_load() Without this call, settings from the global Nix configuration are never loaded, leaving readOnlyMode = true (the default). This prevents Nix from adding paths to the store during evaluation, causing errors like: error: path '/some/local/path' does not exist This fix loads global settings before creating the EvalState, enabling path coercion for local file references. --- nix-bindings-expr/src/eval_state.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nix-bindings-expr/src/eval_state.rs b/nix-bindings-expr/src/eval_state.rs index 7d0808c..6fdedab 100644 --- a/nix-bindings-expr/src/eval_state.rs +++ b/nix-bindings-expr/src/eval_state.rs @@ -288,6 +288,15 @@ impl EvalStateBuilder { let mut context = Context::new(); + // Load settings from global configuration (including readOnlyMode = false). + // This is necessary for path coercion to work (adding files to the store). + unsafe { + check_call!(raw::eval_state_builder_load( + &mut context, + self.eval_state_builder + ))?; + } + // Note: these raw C string pointers borrow from self.lookup_path let mut lookup_path: Vec<*const c_char> = self .lookup_path From 22480afeb513ee457ed89de84349798e9f287e80 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 7 Jan 2026 09:20:01 +0100 Subject: [PATCH 2/4] EvalStateBuilder: Specify Nix version constraint --- Cargo.lock | 94 ++++++++++++++++------------- nix-bindings-expr/Cargo.toml | 5 ++ nix-bindings-expr/build.rs | 6 ++ nix-bindings-expr/src/eval_state.rs | 5 ++ 4 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 nix-bindings-expr/build.rs diff --git a/Cargo.lock b/Cargo.lock index e4a8151..821630f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -42,9 +42,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "cexpr" @@ -57,9 +57,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clang-sys" @@ -105,7 +105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.61.1", ] [[package]] @@ -116,14 +116,14 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "getrandom" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", - "wasip2", + "wasi", ] [[package]] @@ -134,11 +134,11 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "home" -version = "0.5.12" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -164,9 +164,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libloading" @@ -192,9 +192,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" -version = "0.4.29" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" @@ -219,6 +219,7 @@ dependencies = [ "nix-bindings-store", "nix-bindings-util", "nix-bindings-util-sys", + "pkg-config", "tempfile", ] @@ -315,18 +316,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -339,9 +340,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "regex" -version = "1.12.2" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -351,9 +352,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -362,9 +363,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rustc-hash" @@ -387,15 +388,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.61.1", ] [[package]] @@ -406,9 +407,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -417,22 +418,31 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", + "rustix 1.1.2", + "windows-sys 0.61.1", ] [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] [[package]] name = "wasip2" @@ -457,9 +467,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-sys" @@ -472,9 +482,9 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.61.2" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ "windows-link", ] diff --git a/nix-bindings-expr/Cargo.toml b/nix-bindings-expr/Cargo.toml index 41b3cc7..e931946 100644 --- a/nix-bindings-expr/Cargo.toml +++ b/nix-bindings-expr/Cargo.toml @@ -2,6 +2,7 @@ name = "nix-bindings-expr" version = "0.1.0" edition = "2021" +build = "build.rs" license = "LGPL-2.1" description = "Rust bindings to Nix expression evaluator" repository = "https://github.com/nixops4/nix-bindings-rust" @@ -19,6 +20,10 @@ ctor = "0.2" tempfile = "3.10" cstr = "0.2" +[build-dependencies] +pkg-config = "0.3" +nix-bindings-util = { path = "../nix-bindings-util" } + [lints.rust] warnings = "deny" dead-code = "allow" diff --git a/nix-bindings-expr/build.rs b/nix-bindings-expr/build.rs new file mode 100644 index 0000000..6a038bd --- /dev/null +++ b/nix-bindings-expr/build.rs @@ -0,0 +1,6 @@ +use nix_bindings_util::nix_version::emit_version_cfg; + +fn main() { + let nix_version = pkg_config::probe_library("nix-expr-c").unwrap().version; + emit_version_cfg(&nix_version, &["2.26"]); +} diff --git a/nix-bindings-expr/src/eval_state.rs b/nix-bindings-expr/src/eval_state.rs index f2e5b29..70d2ae0 100644 --- a/nix-bindings-expr/src/eval_state.rs +++ b/nix-bindings-expr/src/eval_state.rs @@ -224,6 +224,8 @@ impl Drop for EvalStateRef { /// Provides advanced configuration options for evaluation context setup. /// Use [`EvalState::new`] for simple cases or this builder for custom configuration. /// +/// Requires Nix 2.26.0 or later. +/// /// # Examples /// /// ```rust @@ -244,11 +246,13 @@ impl Drop for EvalStateRef { /// # Ok(()) /// # } /// ``` +#[cfg(nix_at_least = "2.26")] pub struct EvalStateBuilder { eval_state_builder: *mut raw::eval_state_builder, lookup_path: Vec, store: Store, } +#[cfg(nix_at_least = "2.26")] impl Drop for EvalStateBuilder { fn drop(&mut self) { unsafe { @@ -256,6 +260,7 @@ impl Drop for EvalStateBuilder { } } } +#[cfg(nix_at_least = "2.26")] impl EvalStateBuilder { /// Creates a new [`EvalStateBuilder`]. pub fn new(store: Store) -> Result { From eff76e99070d1a88aa8855db9d5b5eeb4639825d Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 7 Jan 2026 09:38:28 +0100 Subject: [PATCH 3/4] Test eval_state_builder_load() to prevent regression --- nix-bindings-expr/src/eval_state.rs | 122 ++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/nix-bindings-expr/src/eval_state.rs b/nix-bindings-expr/src/eval_state.rs index 70d2ae0..6ffb1c9 100644 --- a/nix-bindings-expr/src/eval_state.rs +++ b/nix-bindings-expr/src/eval_state.rs @@ -1255,6 +1255,12 @@ mod tests { #[ctor] fn setup() { test_init(); + + // Configure Nix settings for the test suite + // Set max-call-depth to 1000 (lower than default 10000) for the + // eval_state_builder_loads_max_call_depth test case, while + // giving other tests sufficient room for normal evaluation. + std::env::set_var("NIX_CONFIG", "max-call-depth = 1000"); } /// Run a function while making sure that the current thread is registered with the GC. @@ -2657,4 +2663,120 @@ mod tests { }) .unwrap(); } + + /// Test for path coercion fix (commit 8f6ec2e, ). + /// + /// This test verifies that path coercion works correctly with EvalStateBuilder. + /// Path coercion requires readOnlyMode = false, which is loaded from global + /// settings by calling eval_state_builder_load(). + /// + /// # Background + /// + /// Without the eval_state_builder_load() call, settings from global Nix + /// configuration are never loaded, leaving readOnlyMode = true (the default). + /// This prevents Nix from adding paths to the store during evaluation, + /// which could cause errors like: "error: path '/some/local/path' does not exist" + /// + /// # Test Coverage + /// + /// This test exercises store file creation: + /// 1. builtins.toFile successfully creates files in the store + /// 2. Files are actually written to /nix/store + /// 3. Content is written correctly + /// + /// Note: This test may not reliably fail without the fix in all environments. + /// Use eval_state_builder_loads_max_call_depth for a deterministic test. + #[test] + #[cfg(nix_at_least = "2.26" /* real_path, eval_state_builder_load */)] + fn eval_state_builder_path_coercion() { + gc_registering_current_thread(|| { + let mut store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalStateBuilder::new(store.clone()) + .unwrap() + .build() + .unwrap(); + + // Use builtins.toFile to create a file in the store. + // This operation requires readOnlyMode = false to succeed. + let expr = r#"builtins.toFile "test-file.txt" "test content""#; + + // Evaluate the expression + let value = es.eval_from_string(expr, "").unwrap(); + + // Realise the string to get the path and associated store paths + let realised = es.realise_string(&value, false).unwrap(); + + // Verify we got exactly one store path + assert_eq!( + realised.paths.len(), + 1, + "Expected 1 store path, got {}", + realised.paths.len() + ); + + // Get the physical filesystem path for the store path + // In a relocated store, this differs from realised.s + let physical_path = store.real_path(&realised.paths[0]).unwrap(); + + // Verify the store path actually exists on disk + assert!( + std::path::Path::new(&physical_path).exists(), + "Store path should exist: {}", + physical_path + ); + + // Verify the content was written correctly + let store_content = std::fs::read_to_string(&physical_path).unwrap(); + assert_eq!(store_content, "test content"); + }) + .unwrap(); + } + + /// Test that eval_state_builder_load() loads settings. + /// + /// Uses max-call-depth as the test setting. The test suite sets + /// max-call-depth = 1000 via NIX_CONFIG in setup() for the purpose of this test case. + /// This test creates a recursive function that calls itself 1100 times. + /// + /// - WITH the fix: Settings are loaded, max-call-depth=1000 is enforced, + /// recursion fails at depth 1000 + /// - WITHOUT the fix: Settings aren't loaded, default max-call-depth=10000 + /// is used, recursion of 1100 succeeds when it should fail + #[test] + #[cfg(nix_at_least = "2.26")] + fn eval_state_builder_loads_max_call_depth() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalStateBuilder::new(store).unwrap().build().unwrap(); + + // Create a recursive function that calls itself 1100 times + // This should fail because max-call-depth is 1000 (set in setup()) + let expr = r#" + let + recurse = n: if n == 0 then "done" else recurse (n - 1); + in + recurse 1100 + "#; + + let result = es.eval_from_string(expr, ""); + + match result { + Err(e) => { + let err_str = e.to_string(); + assert!( + err_str.contains("max-call-depth"), + "Expected max-call-depth error, got: {}", + err_str + ); + } + Ok(_) => { + panic!( + "Expected recursion to fail with max-call-depth=1000, but it succeeded. \ + This indicates eval_state_builder_load() was not called." + ); + } + } + }) + .unwrap(); + } } From f1d15ff416c83a7e683f13ed57516192f774b06a Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 7 Jan 2026 09:48:18 +0100 Subject: [PATCH 4/4] EvalStateBuilder: Allow opting out of ambient settings The C API provides nix_eval_state_builder_load as a separate function to allow controlling whether settings are loaded from the environment. Add load_ambient_settings() method to expose this control, with the default being to load them (needed in some situations to allow path coercion to work). --- nix-bindings-expr/src/eval_state.rs | 75 +++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/nix-bindings-expr/src/eval_state.rs b/nix-bindings-expr/src/eval_state.rs index 6ffb1c9..5a82fc6 100644 --- a/nix-bindings-expr/src/eval_state.rs +++ b/nix-bindings-expr/src/eval_state.rs @@ -250,6 +250,7 @@ impl Drop for EvalStateRef { pub struct EvalStateBuilder { eval_state_builder: *mut raw::eval_state_builder, lookup_path: Vec, + load_ambient_settings: bool, store: Store, } #[cfg(nix_at_least = "2.26")] @@ -271,6 +272,7 @@ impl EvalStateBuilder { store, eval_state_builder, lookup_path: Vec::new(), + load_ambient_settings: true, }) } /// Sets the [lookup path](https://nix.dev/manual/nix/latest/language/constructs/lookup-path.html) for Nix expression evaluation. @@ -286,6 +288,15 @@ impl EvalStateBuilder { self.lookup_path = lookup_path; Ok(self) } + /// Sets whether to load settings from the ambient environment. + /// + /// When enabled (default), calls `nix_eval_state_builder_load` to load settings + /// from NIX_CONFIG and other environment variables. When disabled, only the + /// explicitly configured settings are used. + pub fn load_ambient_settings(mut self, load: bool) -> Self { + self.load_ambient_settings = load; + self + } /// Builds the configured [`EvalState`]. pub fn build(&self) -> Result { // Make sure the library is initialized @@ -295,11 +306,13 @@ impl EvalStateBuilder { // Load settings from global configuration (including readOnlyMode = false). // This is necessary for path coercion to work (adding files to the store). - unsafe { - check_call!(raw::eval_state_builder_load( - &mut context, - self.eval_state_builder - ))?; + if self.load_ambient_settings { + unsafe { + check_call!(raw::eval_state_builder_load( + &mut context, + self.eval_state_builder + ))?; + } } // Note: these raw C string pointers borrow from self.lookup_path @@ -2742,6 +2755,9 @@ mod tests { /// recursion fails at depth 1000 /// - WITHOUT the fix: Settings aren't loaded, default max-call-depth=10000 /// is used, recursion of 1100 succeeds when it should fail + /// + /// Complementary to eval_state_builder_ignores_ambient_when_disabled which verifies + /// that ambient settings are NOT loaded when disabled. #[test] #[cfg(nix_at_least = "2.26")] fn eval_state_builder_loads_max_call_depth() { @@ -2779,4 +2795,53 @@ mod tests { }) .unwrap(); } + + /// Test that load_ambient_settings(false) ignores the ambient environment. + /// + /// The test suite sets max-call-depth = 1000 via NIX_CONFIG in setup(). + /// When we disable loading ambient settings, this should be ignored and + /// the default max-call-depth = 10000 should be used instead. + /// + /// Complementary to eval_state_builder_loads_max_call_depth which verifies + /// that ambient settings ARE loaded when enabled. + #[test] + #[cfg(nix_at_least = "2.26")] + fn eval_state_builder_ignores_ambient_when_disabled() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalStateBuilder::new(store) + .unwrap() + .load_ambient_settings(false) + .build() + .unwrap(); + + // Create a recursive function that calls itself 1100 times + // With ambient settings disabled, default max-call-depth=10000 is used, + // so this should succeed (unlike eval_state_builder_loads_max_call_depth) + let expr = r#" + let + recurse = n: if n == 0 then "done" else recurse (n - 1); + in + recurse 1100 + "#; + + let result = es.eval_from_string(expr, ""); + + match result { + Ok(value) => { + // Success expected - ambient NIX_CONFIG was ignored + let result_str = es.require_string(&value).unwrap(); + assert_eq!(result_str, "done"); + } + Err(e) => { + panic!( + "Expected recursion to succeed with default max-call-depth=10000, \ + but it failed: {}. This indicates ambient settings were not ignored.", + e + ); + } + } + }) + .unwrap(); + } }