From c245ed49743e58196da7fd28efff888fc1fbb9a4 Mon Sep 17 00:00:00 2001 From: Naxdy Date: Tue, 19 May 2026 13:26:05 +0200 Subject: [PATCH 1/2] Revert "[noupstream] Refactor `EvalState` & other types to account for DetNix' thread-safety" This reverts commit 15b159b6f1c4a3ee76cbbeb4075df2b6e85be631. --- nix-bindings-expr/src/eval_state.rs | 398 +++++++++++++--------------- nix-bindings-expr/src/primop.rs | 7 +- nix-bindings-expr/src/value.rs | 4 - nix-bindings-flake/src/lib.rs | 13 +- nix-bindings-store/src/path/mod.rs | 3 - nix-bindings-store/src/store.rs | 48 ++-- nix-bindings-util/src/context.rs | 2 - 7 files changed, 214 insertions(+), 261 deletions(-) diff --git a/nix-bindings-expr/src/eval_state.rs b/nix-bindings-expr/src/eval_state.rs index 9ade017a..b5ff7044 100644 --- a/nix-bindings-expr/src/eval_state.rs +++ b/nix-bindings-expr/src/eval_state.rs @@ -208,9 +208,6 @@ pub struct RealisedString { struct EvalStateRef { eval_state: NonNull, } - -unsafe impl Send for EvalStateRef {} - impl EvalStateRef { /// Returns a raw pointer to the underlying EvalState. /// @@ -221,7 +218,6 @@ impl EvalStateRef { self.eval_state.as_ptr() } } - impl Drop for EvalStateRef { fn drop(&mut self) { unsafe { @@ -351,6 +347,7 @@ impl EvalStateBuilder { }), }, store: self.store.clone(), + context, }) } /// Returns a raw pointer to the underlying eval state builder. @@ -368,19 +365,12 @@ impl EvalStateBuilder { /// /// When an `EvalState` is constructed, it will allocate a number of threads to be used for /// evaluating expressions. These threads will remain allocated until the `EvalState` is dropped. -/// -/// Note: Determinate Nix' EvalState is thread-safe due to parallel eval, which is why it -/// implements [`Send`] and [`Sync`], and takes in `&self` as opposed to `&mut self` for most of -/// its methods. Upstream Nix is not (yet) thread-safe. #[clippy::has_significant_drop] pub struct EvalState { eval_state: EvalStateRef, store: Store, + pub(crate) context: Context, } - -unsafe impl Send for EvalState {} -unsafe impl Sync for EvalState {} - impl EvalState { /// Creates a new EvalState with basic configuration. /// @@ -431,7 +421,7 @@ impl EvalState { #[doc(alias = "parse")] #[doc(alias = "eval")] #[doc(alias = "evaluate")] - pub fn eval_from_string(&self, expr: &str, path: &str) -> Result { + pub fn eval_from_string(&mut self, expr: &str, path: &str) -> Result { let expr_ptr = CString::new(expr).map_err(|e| EvalStateError::NulError { arg: "expr", err: e, @@ -440,11 +430,10 @@ impl EvalState { arg: "path", err: e, })?; - let mut ctx = Context::new(); unsafe { let value = self.new_value_uninitialized()?; check_call!(raw::expr_eval_from_string( - &mut ctx, + &mut self.context, self.eval_state.as_ptr(), expr_ptr.as_ptr(), path_ptr.as_ptr(), @@ -463,11 +452,10 @@ impl EvalState { /// See also: [Shared Evaluation State](Value#shared-evaluation-state) #[doc(alias = "evaluate")] #[doc(alias = "strict")] - pub fn force(&self, v: &Value) -> Result<()> { - let mut ctx = Context::new(); + pub fn force(&mut self, v: &Value) -> Result<()> { unsafe { check_call!(raw::value_force( - &mut ctx, + &mut self.context, self.eval_state.as_ptr(), v.raw_ptr() )) @@ -487,11 +475,11 @@ impl EvalState { #[doc(alias = "nix_get_type")] #[doc(alias = "get_type")] #[doc(alias = "nix_value_type")] - #[deprecated(note = "Values will now always be forced; use `EvalState::value_type` instead.")] - pub fn value_type_unforced(&self, value: &Value) -> Option { - self.value_type(value).ok() + pub fn value_type_unforced(&mut self, value: &Value) -> Option { + let r = unsafe { check_call!(raw::get_type(&mut self.context, value.raw_ptr())) }; + // .unwrap(): no reason for this to fail, as it does not evaluate + ValueType::from_raw(r.unwrap()) } - /// Returns the [type][`ValueType`] of a value, [forcing][`EvalState::force`] [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) if necessary. /// /// Forces evaluation if the value is an unevaluated [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness). @@ -504,14 +492,19 @@ impl EvalState { #[doc(alias = "nix_get_type")] #[doc(alias = "get_type")] #[doc(alias = "nix_value_type_strict")] - pub fn value_type(&self, value: &Value) -> Result { - self.force(value)?; - - let mut ctx = Context::new(); - let r = unsafe { check_call!(raw::get_type(&mut ctx, value.raw_ptr())) }; - - Ok(ValueType::from_raw(r.unwrap()) - .expect("ValueType must not be a thunk after being forced")) + pub fn value_type(&mut self, value: &Value) -> Result { + match self.value_type_unforced(value) { + Some(a) => Ok(a), + None => { + self.force(value)?; + match self.value_type_unforced(value) { + Some(a) => Ok(a), + None => { + panic!("Nix value must not be thunk after being forced.") + } + } + } + } } /// Extracts the value from an [integer][`ValueType::Int`] Nix value. /// @@ -542,7 +535,7 @@ impl EvalState { #[doc(alias = "number")] #[doc(alias = "nix_get_int")] #[doc(alias = "get_int")] - pub fn require_int(&self, v: &Value) -> Result { + pub fn require_int(&mut self, v: &Value) -> Result { let t = self.value_type(v)?; if t != ValueType::Int { Err(EvalStateError::UnexpectedValueType { @@ -550,8 +543,7 @@ impl EvalState { received: t, })? } - let mut ctx = Context::new(); - unsafe { check_call!(raw::get_int(&mut ctx, v.raw_ptr())) }.map_err(Into::into) + unsafe { check_call!(raw::get_int(&mut self.context, v.raw_ptr())) }.map_err(Into::into) } /// Extracts the value from a [boolean][`ValueType::Bool`] Nix value. @@ -562,7 +554,7 @@ impl EvalState { #[doc(alias = "boolean")] #[doc(alias = "nix_get_bool")] #[doc(alias = "get_bool")] - pub fn require_bool(&self, v: &Value) -> Result { + pub fn require_bool(&mut self, v: &Value) -> Result { let t = self.value_type(v)?; if t != ValueType::Bool { Err(EvalStateError::UnexpectedValueType { @@ -570,8 +562,7 @@ impl EvalState { received: t, })? } - let mut ctx = Context::new(); - unsafe { check_call!(raw::get_bool(&mut ctx, v.raw_ptr())) }.map_err(Into::into) + unsafe { check_call!(raw::get_bool(&mut self.context, v.raw_ptr())) }.map_err(Into::into) } /// Extracts all elements from a [list][`ValueType::List`] Nix value. @@ -600,7 +591,7 @@ impl EvalState { #[doc(alias = "all")] #[doc(alias = "nix_get_list_size")] #[doc(alias = "nix_get_list_byidx")] - pub fn require_list_strict(&self, value: &Value) -> Result + pub fn require_list_strict(&mut self, value: &Value) -> Result where C: FromIterator, { @@ -611,14 +602,13 @@ impl EvalState { received: t, })? } - let mut ctx = Context::new(); - let size = unsafe { check_call!(raw::get_list_size(&mut ctx, value.raw_ptr())) }?; + let size = unsafe { check_call!(raw::get_list_size(&mut self.context, value.raw_ptr())) }?; (0..size) .map(|i| { let element_ptr = unsafe { check_call!(raw::get_list_byidx( - &mut ctx, + &mut self.context, value.raw_ptr(), self.eval_state.as_ptr(), i @@ -637,7 +627,7 @@ impl EvalState { #[doc(alias = "keys")] #[doc(alias = "attributes")] #[doc(alias = "fields")] - pub fn require_attrs_names(&self, v: &Value) -> Result> { + pub fn require_attrs_names(&mut self, v: &Value) -> Result> { self.require_attrs_names_unsorted(v).map(|mut v| { v.sort(); v @@ -649,7 +639,7 @@ impl EvalState { /// Only use when it's ok that the keys are returned in an arbitrary order. #[doc(alias = "keys_unsorted")] #[doc(alias = "attributes_unsorted")] - pub fn require_attrs_names_unsorted(&self, v: &Value) -> Result> { + pub fn require_attrs_names_unsorted(&mut self, v: &Value) -> Result> { let t = self.value_type(v)?; if t != ValueType::AttrSet { Err(EvalStateError::UnexpectedValueType { @@ -657,13 +647,12 @@ impl EvalState { received: t, })? } - let mut ctx = Context::new(); - let n = unsafe { check_call!(raw::get_attrs_size(&mut ctx, v.raw_ptr())) }?; + let n = unsafe { check_call!(raw::get_attrs_size(&mut self.context, v.raw_ptr())) }?; let mut attrs = Vec::with_capacity(n as usize); for i in 0..n { let cstr_ptr: *const c_char = unsafe { check_call!(raw::get_attr_name_byidx( - &mut ctx, + &mut self.context, v.raw_ptr(), self.eval_state.as_ptr(), i as c_uint @@ -684,7 +673,7 @@ impl EvalState { #[doc(alias = "get_attr")] #[doc(alias = "attribute")] #[doc(alias = "field")] - pub fn require_attrs_select(&self, v: &Value, attr_name: &str) -> Result { + pub fn require_attrs_select(&mut self, v: &Value, attr_name: &str) -> Result { let t = self.value_type(v)?; if t != ValueType::AttrSet { Err(EvalStateError::UnexpectedValueType { @@ -697,10 +686,9 @@ impl EvalState { arg: "attrName", err: e, })?; - let mut ctx = Context::new(); unsafe { let v2 = check_call!(raw::get_attr_byname( - &mut ctx, + &mut self.context, v.raw_ptr(), self.eval_state.as_ptr(), attr_name.as_ptr() @@ -740,7 +728,11 @@ impl EvalState { #[doc(alias = "get_attr_opt")] #[doc(alias = "try_get")] #[doc(alias = "maybe_get")] - pub fn require_attrs_select_opt(&self, v: &Value, attr_name: &str) -> Result> { + pub fn require_attrs_select_opt( + &mut self, + v: &Value, + attr_name: &str, + ) -> Result> { let t = self.value_type(v)?; if t != ValueType::AttrSet { Err(EvalStateError::UnexpectedValueType { @@ -752,10 +744,9 @@ impl EvalState { arg: "attrName", err: e, })?; - let mut ctx = Context::new(); let v2 = unsafe { check_call_opt_key!(raw::get_attr_byname( - &mut ctx, + &mut self.context, v.raw_ptr(), self.eval_state.as_ptr(), attr_name.as_ptr() @@ -774,7 +765,7 @@ impl EvalState { #[doc(alias = "len")] #[doc(alias = "nix_get_list_size")] #[doc(alias = "get_list_size")] - pub fn require_list_size(&self, v: &Value) -> Result { + pub fn require_list_size(&mut self, v: &Value) -> Result { let t = self.value_type(v)?; if t != ValueType::List { Err(EvalStateError::UnexpectedValueType { @@ -782,8 +773,7 @@ impl EvalState { received: t, })? } - let mut ctx = Context::new(); - let ret = unsafe { check_call!(raw::get_list_size(&mut ctx, v.raw_ptr())) }?; + let ret = unsafe { check_call!(raw::get_list_size(&mut self.context, v.raw_ptr())) }?; Ok(ret) } @@ -802,7 +792,7 @@ impl EvalState { #[doc(alias = "at")] #[doc(alias = "nix_get_list_byidx")] #[doc(alias = "get_list_byidx")] - pub fn require_list_select_idx_strict(&self, v: &Value, idx: u32) -> Result> { + pub fn require_list_select_idx_strict(&mut self, v: &Value, idx: u32) -> Result> { let t = self.value_type(v)?; if t != ValueType::List { Err(EvalStateError::UnexpectedValueType { @@ -814,8 +804,7 @@ impl EvalState { // TODO: Remove this bounds checking once https://github.com/NixOS/nix/pull/14030 // is merged, which will add proper bounds checking to the underlying C API. // Currently we perform bounds checking in Rust to avoid undefined behavior. - let mut ctx = Context::new(); - let size = unsafe { check_call!(raw::get_list_size(&mut ctx, v.raw_ptr())) }?; + let size = unsafe { check_call!(raw::get_list_size(&mut self.context, v.raw_ptr())) }?; if idx >= size { return Ok(None); @@ -823,7 +812,7 @@ impl EvalState { let v2 = unsafe { check_call_opt_key!(raw::get_list_byidx( - &mut ctx, + &mut self.context, v.raw_ptr(), self.eval_state.as_ptr(), idx @@ -838,12 +827,15 @@ impl EvalState { #[doc(alias = "make_string")] #[doc(alias = "create_string")] #[doc(alias = "string_value")] - pub fn new_value_str(&self, s: &str) -> Result { + pub fn new_value_str(&mut self, s: &str) -> Result { let s = CString::new(s).map_err(|e| EvalStateError::NulError { arg: "s", err: e })?; let v = unsafe { let value = self.new_value_uninitialized()?; - let mut ctx = Context::new(); - check_call!(raw::init_string(&mut ctx, value.raw_ptr(), s.as_ptr()))?; + check_call!(raw::init_string( + &mut self.context, + value.raw_ptr(), + s.as_ptr() + ))?; value }; Ok(v) @@ -854,11 +846,10 @@ impl EvalState { #[doc(alias = "create_int")] #[doc(alias = "int_value")] #[doc(alias = "integer_value")] - pub fn new_value_int(&self, i: Int) -> Result { + pub fn new_value_int(&mut self, i: Int) -> Result { let v = unsafe { let value = self.new_value_uninitialized()?; - let mut ctx = Context::new(); - check_call!(raw::init_int(&mut ctx, value.raw_ptr(), i))?; + check_call!(raw::init_int(&mut self.context, value.raw_ptr(), i))?; value }; Ok(v) @@ -902,12 +893,11 @@ impl EvalState { } /// Not exposed, because the caller must always explicitly handle the context or not accept one at all. - fn get_string(&self, value: &Value) -> Result { + fn get_string(&mut self, value: &Value) -> Result { let mut r = Err(NixBindingsError::StringInit); - let mut ctx = Context::new(); unsafe { check_call!(raw::get_string( - &mut ctx, + &mut self.context, value.raw_ptr(), Some(callback_get_result_string), callback_get_result_string_data(&mut r) @@ -925,7 +915,7 @@ impl EvalState { #[doc(alias = "text")] #[doc(alias = "nix_get_string")] #[doc(alias = "get_string")] - pub fn require_string(&self, value: &Value) -> Result { + pub fn require_string(&mut self, value: &Value) -> Result { let t = self.value_type(value)?; if t != ValueType::String { Err(EvalStateError::UnexpectedValueType { @@ -943,7 +933,7 @@ impl EvalState { #[doc(alias = "string_with_context")] #[doc(alias = "build_string")] pub fn realise_string( - &self, + &mut self, value: &Value, is_import_from_derivation: bool, ) -> Result { @@ -955,10 +945,9 @@ impl EvalState { })? } - let mut ctx = Context::new(); let rs = unsafe { check_call!(raw::string_realise( - &mut ctx, + &mut self.context, self.eval_state.as_ptr(), value.raw_ptr(), is_import_from_derivation @@ -1002,12 +991,11 @@ impl EvalState { #[doc(alias = "apply")] #[doc(alias = "invoke")] #[doc(alias = "execute")] - pub fn call(&self, f: Value, a: Value) -> Result { + pub fn call(&mut self, f: Value, a: Value) -> Result { let value = self.new_value_uninitialized()?; - let mut ctx = Context::new(); unsafe { check_call!(raw::value_call( - &mut ctx, + &mut self.context, self.eval_state.as_ptr(), f.raw_ptr(), a.raw_ptr(), @@ -1060,13 +1048,12 @@ impl EvalState { #[doc(alias = "apply_multi")] #[doc(alias = "curry")] #[doc(alias = "call_with_args")] - pub fn call_multi(&self, f: &Value, args: &[Value]) -> Result { + pub fn call_multi(&mut self, f: &Value, args: &[Value]) -> Result { let value = self.new_value_uninitialized()?; - let mut ctx = Context::new(); unsafe { let mut args_ptrs = args.iter().map(|a| a.raw_ptr()).collect::>(); check_call!(raw::value_call_multi( - &mut ctx, + &mut self.context, self.eval_state.as_ptr(), f.raw_ptr(), args_ptrs.len(), @@ -1084,12 +1071,11 @@ impl EvalState { #[doc(alias = "lazy_apply")] #[doc(alias = "thunk_apply")] #[doc(alias = "defer_call")] - pub fn new_value_apply(&self, f: &Value, a: &Value) -> Result { + pub fn new_value_apply(&mut self, f: &Value, a: &Value) -> Result { let value = self.new_value_uninitialized()?; - let mut ctx = Context::new(); unsafe { check_call!(raw::init_apply( - &mut ctx, + &mut self.context, value.raw_ptr(), f.raw_ptr(), a.raw_ptr() @@ -1098,10 +1084,12 @@ impl EvalState { Ok(value) } - pub(crate) fn new_value_uninitialized(&self) -> Result { - let mut ctx = Context::new(); + pub(crate) fn new_value_uninitialized(&mut self) -> Result { unsafe { - let value = check_call!(raw::alloc_value(&mut ctx, self.eval_state.as_ptr()))?; + let value = check_call!(raw::alloc_value( + &mut self.context, + self.eval_state.as_ptr() + ))?; Ok(Value::new(value)) } } @@ -1164,7 +1152,7 @@ impl EvalState { #[doc(alias = "create_attrset")] #[doc(alias = "object")] #[doc(alias = "record")] - pub fn new_value_attrs(&self, attrs: I) -> Result + pub fn new_value_attrs(&mut self, attrs: I) -> Result where I: IntoIterator, I::IntoIter: ExactSizeIterator, @@ -1172,7 +1160,6 @@ impl EvalState { let iter = attrs.into_iter(); let size = iter.len(); let bindings_builder = BindingsBuilder::new(self, size)?; - let mut ctx = Context::new(); for (name, value) in iter { let name = CString::new(name).map_err(|e| EvalStateError::NulError { arg: "name", @@ -1180,7 +1167,7 @@ impl EvalState { })?; unsafe { check_call!(raw::bindings_builder_insert( - &mut ctx, + &mut self.context, bindings_builder.ptr, name.as_ptr(), value.raw_ptr() @@ -1190,7 +1177,7 @@ impl EvalState { let value = self.new_value_uninitialized()?; unsafe { check_call!(raw::make_attrs( - &mut ctx, + &mut self.context, value.raw_ptr(), bindings_builder.ptr ))?; @@ -1211,11 +1198,10 @@ impl Drop for BindingsBuilder { } } impl BindingsBuilder { - fn new(eval_state: &EvalState, capacity: usize) -> Result { - let mut ctx = Context::new(); + fn new(eval_state: &mut EvalState, capacity: usize) -> Result { let ptr = unsafe { check_call!(raw::make_bindings_builder( - &mut ctx, + &mut eval_state.context, eval_state.eval_state.as_ptr(), capacity )) @@ -1367,10 +1353,10 @@ mod tests { writeln!(test_file0, "{integer0}").unwrap(); writeln!(test_file1, "{integer1}").unwrap(); gc_registering_current_thread(|| { - let es = EvalState::new(Store::open(None, HashMap::new()).unwrap(), []).unwrap(); + let mut es = EvalState::new(Store::open(None, HashMap::new()).unwrap(), []).unwrap(); assert!(es.eval_from_string(import_expression, "").is_err()); - let es = EvalState::new( + let mut es = EvalState::new( Store::open(None, HashMap::new()).unwrap(), [ format!("test_file0={}", test_file0.path().to_str().unwrap()).as_str(), @@ -1391,14 +1377,14 @@ mod tests { fn eval_state_eval_from_string() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("1", "").unwrap(); let v2 = v.clone(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::Int))); - let t2 = es.value_type(&v2); - assert!(matches!(t2, Ok(ValueType::Int))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); + let t2 = es.value_type_unforced(&v2); + assert!(t2 == Some(ValueType::Int)); gc_now(); }) .unwrap(); @@ -1408,11 +1394,11 @@ mod tests { fn eval_state_value_bool() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("true", "").unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::Bool))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Bool)); let b = es.require_bool(&v).unwrap(); assert!(b); @@ -1428,7 +1414,7 @@ mod tests { fn eval_state_value_int() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("1", "").unwrap(); es.force(&v).unwrap(); let t = es.value_type(&v).unwrap(); @@ -1443,13 +1429,12 @@ mod tests { fn eval_state_require_int_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let f = es.eval_from_string("x: x + 1", "").unwrap(); let a = es.eval_from_string("2", "").unwrap(); let v = es.new_value_apply(&f, &a).unwrap(); - // Note: value_type_unforced now forces evaluation - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::Int))); + let t = es.value_type_unforced(&v); + assert!(t.is_none()); let i = es.require_int(&v).unwrap(); assert!(i == 3); }) @@ -1460,13 +1445,12 @@ mod tests { fn eval_state_require_bool_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let f = es.eval_from_string("x: !x", "").unwrap(); let a = es.eval_from_string("true", "").unwrap(); let v = es.new_value_apply(&f, &a).unwrap(); - // Note: value_type_unforced now forces evaluation - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::Bool))); + let t = es.value_type_unforced(&v); + assert!(t.is_none()); let i = es.require_bool(&v).unwrap(); assert!(!i); }) @@ -1474,7 +1458,7 @@ mod tests { } /// A helper that turns an expression into a thunk. - fn make_thunk(es: &EvalState, expr: &str) -> Value { + fn make_thunk(es: &mut EvalState, expr: &str) -> Value { // This would be silly in real code, but it works for the current Nix implementation. // A Nix implementation that applies the identity function eagerly would be a valid // Nix implementation, but annoying because we'll have to change this helper to do @@ -1488,11 +1472,10 @@ mod tests { fn make_thunk_helper_works() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); - let v = make_thunk(&es, "1"); - // Note: value_type_unforced now forces evaluation, so the value is always forced. - let t = es.value_type(&v); - assert!(t.is_ok()); + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "1"); + let t = es.value_type_unforced(&v); + assert!(t.is_none()); }) .unwrap(); } @@ -1501,11 +1484,11 @@ mod tests { fn eval_state_value_attrs_names_empty() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("{ }", "").unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::AttrSet))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::AttrSet)); let attrs = es.require_attrs_names_unsorted(&v).unwrap(); assert_eq!(attrs.len(), 0); }) @@ -1516,11 +1499,10 @@ mod tests { fn eval_state_require_attrs_names_unsorted_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); - let v = make_thunk(&es, "{ a = 1; b = 2; }"); - // Note: value_type_unforced now forces evaluation - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::AttrSet))); + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "{ a = 1; b = 2; }"); + let t = es.value_type_unforced(&v); + assert!(t.is_none()); let attrs = es.require_attrs_names_unsorted(&v).unwrap(); assert_eq!(attrs.len(), 2); }) @@ -1531,7 +1513,7 @@ mod tests { fn eval_state_require_attrs_names_unsorted_bad_type() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("1", "").unwrap(); es.force(&v).unwrap(); let r = es.require_attrs_names_unsorted(&v); @@ -1550,7 +1532,7 @@ mod tests { fn eval_state_value_attrs_names_example() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = throw "nope a"; b = throw "nope b"; }"#; let v = es.eval_from_string(expr, "").unwrap(); let attrs = es.require_attrs_names(&v).unwrap(); @@ -1565,7 +1547,7 @@ mod tests { fn eval_state_require_attrs_select() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = "aye"; b = "bee"; }"#; let v = es.eval_from_string(expr, "").unwrap(); let a = es.require_attrs_select(&v, "a").unwrap(); @@ -1582,10 +1564,10 @@ mod tests { fn eval_state_require_attrs_select_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = "aye"; b = "bee"; }"#; - let v = make_thunk(&es, expr); - // Note: value_type_unforced now forces evaluation, so the value is always forced. + let v = make_thunk(&mut es, expr); + assert!(es.value_type_unforced(&v).is_none()); let r = es.require_attrs_select(&v, "a"); assert!(r.is_ok()); }) @@ -1596,7 +1578,7 @@ mod tests { fn eval_state_require_attrs_select_error() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = throw "oh no the error"; }"#; let v = es.eval_from_string(expr, "").unwrap(); let r = es.require_attrs_select(&v, "a"); @@ -1620,7 +1602,7 @@ mod tests { fn eval_state_require_attrs_select_opt() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = "aye"; b = "bee"; }"#; let v = es.eval_from_string(expr, "").unwrap(); let a = es.require_attrs_select_opt(&v, "a").unwrap().unwrap(); @@ -1637,10 +1619,10 @@ mod tests { fn eval_state_require_attrs_select_opt_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = "aye"; b = "bee"; }"#; - let v = make_thunk(&es, expr); - // Note: value_type_unforced now forces evaluation, so the value is always forced. + let v = make_thunk(&mut es, expr); + assert!(es.value_type_unforced(&v).is_none()); let r = es.require_attrs_select_opt(&v, "a"); assert!(r.is_ok()); }) @@ -1651,7 +1633,7 @@ mod tests { fn eval_state_require_attrs_select_opt_error() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = throw "oh no the error"; }"#; let v = es.eval_from_string(expr, "").unwrap(); let r = es.require_attrs_select_opt(&v, "a"); @@ -1674,11 +1656,11 @@ mod tests { fn eval_state_value_string() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("\"hello\"", "").unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::String))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); let s = es.require_string(&v).unwrap(); assert!(s == "hello"); }) @@ -1689,9 +1671,9 @@ mod tests { fn eval_state_value_string_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); - let v = make_thunk(&es, "\"hello\""); - // Note: value_type_unforced now forces evaluation, so the value is always forced. + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "\"hello\""); + assert!(es.value_type_unforced(&v).is_none()); let s = es.require_string(&v).unwrap(); assert!(s == "hello"); }) @@ -1702,7 +1684,7 @@ mod tests { fn eval_state_value_string_unexpected_bool() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("true", "").unwrap(); es.force(&v).unwrap(); let r = es.require_string(&v); @@ -1720,7 +1702,7 @@ mod tests { fn eval_state_value_string_unexpected_path_value() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("/foo", "").unwrap(); es.force(&v).unwrap(); let r = es.require_string(&v); @@ -1737,13 +1719,13 @@ mod tests { fn eval_state_value_string_bad_utf() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es .eval_from_string("builtins.substring 0 1 \"ΓΌ\"", "") .unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::String))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); let r = es.require_string(&v); assert!(r.is_err()); assert!(matches!( @@ -1760,13 +1742,13 @@ mod tests { fn eval_state_value_string_unexpected_context() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es .eval_from_string("(derivation { name = \"hello\"; system = \"dummy\"; builder = \"cmd.exe\"; }).outPath", "") .unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::String))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); // TODO // let r = es.require_string_without_context(&v); // assert!(r.is_err()); @@ -1779,11 +1761,11 @@ mod tests { fn eval_state_new_string() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.new_value_str("hello").unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::String))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); let s = es.require_string(&v).unwrap(); assert!(s == "hello"); }) @@ -1794,11 +1776,11 @@ mod tests { fn eval_state_new_string_empty() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.new_value_str("").unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::String))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); let s = es.require_string(&v).unwrap(); assert!(s.is_empty()); }) @@ -1809,7 +1791,7 @@ mod tests { fn eval_state_new_string_invalid() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let r = es.new_value_str("hell\0no"); match r { Ok(_) => panic!("expected an error"), @@ -1828,11 +1810,11 @@ mod tests { fn eval_state_new_int() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.new_value_int(42).unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::Int))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); let i = es.require_int(&v).unwrap(); assert!(i == 42); }) @@ -1843,11 +1825,11 @@ mod tests { fn eval_state_value_attrset() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("{ }", "").unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::AttrSet))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::AttrSet)); }) .unwrap(); } @@ -1856,11 +1838,11 @@ mod tests { fn eval_state_value_list() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[ ]", "").unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::List))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::List)); }) .unwrap(); } @@ -1869,7 +1851,7 @@ mod tests { fn eval_state_value_list_strict_empty() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[]", "").unwrap(); es.force(&v).unwrap(); let list: Vec = es.require_list_strict(&v).unwrap(); @@ -1882,7 +1864,7 @@ mod tests { fn eval_state_value_list_strict_int() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[42]", "").unwrap(); es.force(&v).unwrap(); let list: Vec = es.require_list_strict(&v).unwrap(); @@ -1896,7 +1878,7 @@ mod tests { fn eval_state_value_list_strict_int_bool() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[42 true]", "").unwrap(); es.force(&v).unwrap(); let list: Vec = es.require_list_strict(&v).unwrap(); @@ -1911,7 +1893,7 @@ mod tests { fn eval_state_value_list_strict_error() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es .eval_from_string(r#"[(throw "_evaluated_item_")]"#, "") .unwrap(); @@ -1940,7 +1922,7 @@ mod tests { fn eval_state_value_list_strict_generic_container() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[1 2 3]", "").unwrap(); // Test with Vec @@ -1963,7 +1945,7 @@ mod tests { fn eval_state_realise_string() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let expr = r#" '' a derivation output: ${ @@ -2010,13 +1992,13 @@ mod tests { fn eval_state_call() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let f = es.eval_from_string("x: x + 1", "").unwrap(); let a = es.eval_from_string("2", "").unwrap(); let v = es.call(f, a).unwrap(); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::Int))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); let i = es.require_int(&v).unwrap(); assert!(i == 3); }) @@ -2027,14 +2009,14 @@ mod tests { fn eval_state_call_multi() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); // This is a function that takes two arguments. let f = es.eval_from_string("x: y: x - y", "").unwrap(); let a = es.eval_from_string("2", "").unwrap(); let b = es.eval_from_string("3", "").unwrap(); let v = es.call_multi(&f, &[a, b]).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::Int))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); let i = es.require_int(&v).unwrap(); assert!(i == -1); }) @@ -2045,15 +2027,15 @@ mod tests { fn eval_state_apply() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); // This is a function that takes two arguments. let f = es.eval_from_string("x: x + 1", "").unwrap(); let a = es.eval_from_string("2", "").unwrap(); let v = es.new_value_apply(&f, &a).unwrap(); - // Note: value_type_unforced now forces evaluation, so the value is always forced. + assert!(es.value_type_unforced(&v).is_none()); es.force(&v).unwrap(); - let t = es.value_type(&v); - assert!(matches!(t, Ok(ValueType::Int))); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); let i = es.require_int(&v).unwrap(); assert!(i == 3); }) @@ -2064,7 +2046,7 @@ mod tests { fn eval_state_call_fail_body() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let f = es.eval_from_string("x: x + 1", "").unwrap(); let a = es.eval_from_string("true", "").unwrap(); let r = es.call(f, a); @@ -2087,7 +2069,7 @@ mod tests { fn eval_state_call_multi_fail_body() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); // This is a function that takes two arguments. let f = es.eval_from_string("x: y: x - y", "").unwrap(); let a = es.eval_from_string("2", "").unwrap(); @@ -2112,7 +2094,7 @@ mod tests { fn eval_state_apply_fail_body() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let f = es.eval_from_string("x: x + 1", "").unwrap(); let a = es.eval_from_string("true", "").unwrap(); // Lazy => no error @@ -2139,7 +2121,7 @@ mod tests { fn eval_state_call_fail_args() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let f = es.eval_from_string("{x}: x + 1", "").unwrap(); let a = es.eval_from_string("{}", "").unwrap(); let r = es.call(f, a); @@ -2162,7 +2144,7 @@ mod tests { fn eval_state_call_multi_fail_args() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); // This is a function that takes two arguments. let f = es.eval_from_string("{x}: {y}: x - y", "").unwrap(); let a = es.eval_from_string("{x = 2;}", "").unwrap(); @@ -2188,7 +2170,7 @@ mod tests { fn eval_state_apply_fail_args_lazy() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let f = es.eval_from_string("{x}: x + 1", "").unwrap(); let a = es.eval_from_string("{}", "").unwrap(); // Lazy => no error @@ -2220,7 +2202,7 @@ mod tests { let log = tempfile::tempdir().unwrap(); let log_path = log.path().to_str().unwrap(); - let es = EvalState::new( + let mut es = EvalState::new( Store::open( Some("local"), HashMap::from([ @@ -2506,7 +2488,7 @@ mod tests { pub fn eval_state_new_value_attrs_from_slice_empty() { gc_registering_current_thread(|| { let store = Store::open(None, []).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let attrs = es.new_value_attrs([]).unwrap(); let t = es.value_type(&attrs).unwrap(); assert!(t == ValueType::AttrSet); @@ -2520,7 +2502,7 @@ mod tests { pub fn eval_state_new_value_attrs_from_vec() { gc_registering_current_thread(|| { let store = Store::open(None, []).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let attrs = { let a = es.new_value_int(1).unwrap(); let b = es.new_value_int(2).unwrap(); @@ -2547,7 +2529,7 @@ mod tests { pub fn eval_state_new_value_attrs_from_hashmap() { gc_registering_current_thread(|| { let store = Store::open(None, []).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let attrs = { let a = es.new_value_int(1).unwrap(); let b = es.new_value_int(2).unwrap(); @@ -2574,7 +2556,7 @@ mod tests { fn eval_state_require_list_select_idx_strict_basic() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[ 10 20 30 ]", "").unwrap(); let elem0 = es.require_list_select_idx_strict(&v, 0).unwrap().unwrap(); @@ -2592,7 +2574,7 @@ mod tests { fn eval_state_require_list_select_idx_strict_out_of_bounds() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[ 1 2 3 ]", "").unwrap(); let out_of_bounds = es.require_list_select_idx_strict(&v, 3).unwrap(); @@ -2609,7 +2591,7 @@ mod tests { fn eval_state_require_list_select_idx_strict_empty_list() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[ ]", "").unwrap(); // Test that the safe version properly handles empty list access @@ -2627,9 +2609,9 @@ mod tests { fn eval_state_require_list_select_idx_strict_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); - let v = make_thunk(&es, "[ 42 ]"); - // Note: value_type_unforced now forces evaluation, so the value is always forced. + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "[ 42 ]"); + assert!(es.value_type_unforced(&v).is_none()); let elem = es.require_list_select_idx_strict(&v, 0).unwrap().unwrap(); assert_eq!(es.require_int(&elem).unwrap(), 42); @@ -2641,7 +2623,7 @@ mod tests { fn eval_state_require_list_select_idx_strict_error_element() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es .eval_from_string("[ (1 + 1) (throw \"error\") (3 + 3) ]", "") @@ -2668,7 +2650,7 @@ mod tests { fn eval_state_require_list_select_idx_strict_wrong_type() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("42", "").unwrap(); let r = es.require_list_select_idx_strict(&v, 0); @@ -2687,7 +2669,7 @@ mod tests { fn eval_state_require_list_size_basic() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let empty = es.eval_from_string("[ ]", "").unwrap(); assert_eq!(es.require_list_size(&empty).unwrap(), 0); @@ -2702,9 +2684,9 @@ mod tests { fn eval_state_require_list_size_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); - let v = make_thunk(&es, "[ 1 2 3 4 5 ]"); - // Note: value_type_unforced now forces evaluation, so the value is always forced. + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "[ 1 2 3 4 5 ]"); + assert!(es.value_type_unforced(&v).is_none()); let size = es.require_list_size(&v).unwrap(); assert_eq!(size, 5); @@ -2716,7 +2698,7 @@ mod tests { fn eval_state_require_list_size_lazy_elements() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es .eval_from_string( @@ -2735,7 +2717,7 @@ mod tests { fn eval_state_require_list_size_wrong_type() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalState::new(store, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("\"not a list\"", "").unwrap(); let r = es.require_list_size(&v); @@ -2777,7 +2759,7 @@ mod tests { fn eval_state_builder_path_coercion() { gc_registering_current_thread(|| { let mut store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalStateBuilder::new(store.clone()) + let mut es = EvalStateBuilder::new(store.clone()) .unwrap() .build() .unwrap(); @@ -2836,7 +2818,7 @@ mod tests { fn eval_state_builder_loads_max_call_depth() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalStateBuilder::new(store).unwrap().build().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()) @@ -2885,7 +2867,7 @@ mod tests { fn eval_state_builder_ignores_ambient_when_disabled() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); - let es = EvalStateBuilder::new(store) + let mut es = EvalStateBuilder::new(store) .unwrap() .load_ambient_settings(false) .build() diff --git a/nix-bindings-expr/src/primop.rs b/nix-bindings-expr/src/primop.rs index 7b56f11b..19b89c4b 100644 --- a/nix-bindings-expr/src/primop.rs +++ b/nix-bindings-expr/src/primop.rs @@ -2,7 +2,6 @@ use crate::eval_state::{EvalState, EvalStateError}; use crate::value::Value; use nix_bindings_expr_sys as raw; use nix_bindings_util::check_call; -use nix_bindings_util::context::Context; use nix_bindings_util_sys as raw_util; use std::error::Error; use std::ffi::{c_int, c_void, CStr, CString}; @@ -72,10 +71,9 @@ impl<'a> PrimOp<'a> { })); user_data as *const PrimOpContext as *mut c_void }; - let mut ctx = Context::new(); let op = unsafe { check_call!(raw::alloc_primop( - &mut ctx, + &mut eval_state.context, FUNCTION_ADAPTER, N as c_int, meta.name.as_ptr(), @@ -102,9 +100,8 @@ impl<'a> PrimOp<'a> { pub fn new_value(mut self) -> Result { self.with_state_and_ptr(|ptr, this| { let value = this.new_value_uninitialized()?; - let mut ctx = Context::new(); unsafe { - check_call!(raw::init_primop(&mut ctx, value.raw_ptr(), ptr))?; + check_call!(raw::init_primop(&mut this.context, value.raw_ptr(), ptr))?; }; Ok(value) }) diff --git a/nix-bindings-expr/src/value.rs b/nix-bindings-expr/src/value.rs index f9b59ca9..68bb4442 100644 --- a/nix-bindings-expr/src/value.rs +++ b/nix-bindings-expr/src/value.rs @@ -83,10 +83,6 @@ impl ValueType { pub struct Value { inner: NonNull, } - -unsafe impl Send for Value {} -unsafe impl Sync for Value {} - impl Value { /// Take ownership of a new [`Value`]. /// diff --git a/nix-bindings-flake/src/lib.rs b/nix-bindings-flake/src/lib.rs index 3e1b4f72..75994822 100644 --- a/nix-bindings-flake/src/lib.rs +++ b/nix-bindings-flake/src/lib.rs @@ -13,10 +13,6 @@ use nix_bindings_util::{Error, Result}; pub struct FlakeSettings { pub(crate) ptr: *mut raw::flake_settings, } - -unsafe impl Send for FlakeSettings {} -unsafe impl Sync for FlakeSettings {} - impl Drop for FlakeSettings { fn drop(&mut self) { unsafe { @@ -24,7 +20,6 @@ impl Drop for FlakeSettings { } } } - impl FlakeSettings { pub fn new() -> Result { let mut ctx = Context::new(); @@ -54,7 +49,6 @@ pub trait EvalStateBuilderExt { settings: &FlakeSettings, ) -> Result; } - impl EvalStateBuilderExt for nix_bindings_expr::eval_state::EvalStateBuilder { /// Configures the eval state to provide flakes features such as `builtins.getFlake`. fn flakes( @@ -212,10 +206,6 @@ impl FlakeLockFlags { pub struct LockedFlake { pub(crate) ptr: NonNull, } - -unsafe impl Send for LockedFlake {} -unsafe impl Sync for LockedFlake {} - impl Drop for LockedFlake { fn drop(&mut self) { unsafe { @@ -223,7 +213,6 @@ impl Drop for LockedFlake { } } } - impl LockedFlake { pub fn lock( fetch_settings: &FetchersSettings, @@ -306,7 +295,7 @@ mod tests { init(); let gc_registration = gc_register_my_thread(); let store = Store::open(None, []).unwrap(); - let eval_state = EvalStateBuilder::new(store) + let mut eval_state = EvalStateBuilder::new(store) .unwrap() .flakes(&FlakeSettings::new().unwrap()) .unwrap() diff --git a/nix-bindings-store/src/path/mod.rs b/nix-bindings-store/src/path/mod.rs index 63cc969e..a65cf928 100644 --- a/nix-bindings-store/src/path/mod.rs +++ b/nix-bindings-store/src/path/mod.rs @@ -20,9 +20,6 @@ pub struct StorePath { raw: NonNull, } -unsafe impl Send for StorePath {} -unsafe impl Sync for StorePath {} - impl StorePath { /// Get the name of the store path. /// diff --git a/nix-bindings-store/src/store.rs b/nix-bindings-store/src/store.rs index 8bf07fb1..755f2e9f 100644 --- a/nix-bindings-store/src/store.rs +++ b/nix-bindings-store/src/store.rs @@ -56,7 +56,10 @@ impl StoreWeak { /// /// If no normal reference to the [Store] is around anymore elsewhere, this fails by returning `None`. pub fn upgrade(&self) -> Option { - self.inner.upgrade().map(|inner| Store { inner }) + self.inner.upgrade().map(|inner| Store { + inner, + context: Context::new(), + }) } } @@ -141,6 +144,8 @@ fn callback_make_drv_outputs_data(vec: &mut HashMap) -> *mut std #[clippy::has_significant_drop] pub struct Store { inner: Arc, + /* An error context to reuse. This way we don't have to allocate them for each store operation. */ + context: Context, } impl Store { /// Open a store. @@ -231,6 +236,7 @@ impl Store { inner: Arc::new(StoreRef { inner: NonNull::new(store).unwrap(), }), + context, }; Ok(store) } @@ -245,10 +251,9 @@ impl Store { #[doc(alias = "nix_store_get_uri")] pub fn get_uri(&mut self) -> Result { let mut r = Err(Error::StringInit); - let mut ctx = Context::new(); unsafe { check_call!(raw::store_get_uri( - &mut ctx, + &mut self.context, self.inner.ptr(), Some(callback_get_result_string), callback_get_result_string_data(&mut r) @@ -261,10 +266,9 @@ impl Store { #[doc(alias = "nix_store_get_storedir")] pub fn get_storedir(&mut self) -> Result { let mut r = Err(Error::StringInit); - let mut ctx = Context::new(); unsafe { check_call!(raw::store_get_storedir( - &mut ctx, + &mut self.context, self.inner.ptr(), Some(callback_get_result_string), callback_get_result_string_data(&mut r) @@ -276,10 +280,9 @@ impl Store { #[doc(alias = "nix_store_get_version")] pub fn get_version(&mut self) -> Result { let mut r = Err(Error::StringInit); - let mut ctx = Context::new(); unsafe { check_call!(raw::store_get_version( - &mut ctx, + &mut self.context, self.inner.ptr(), Some(callback_get_result_string), callback_get_result_string_data(&mut r) @@ -291,10 +294,9 @@ impl Store { #[doc(alias = "nix_store_parse_path")] pub fn parse_store_path(&mut self, path: &str) -> Result { let path = CString::new(path)?; - let mut ctx = Context::new(); unsafe { let store_path = check_call!(raw::store_parse_path( - &mut ctx, + &mut self.context, self.inner.ptr(), path.as_ptr() ))?; @@ -307,10 +309,9 @@ impl Store { #[doc(alias = "nix_store_real_path")] pub fn real_path(&mut self, path: &StorePath) -> Result { let mut r = Err(Error::StringInit); - let mut ctx = Context::new(); unsafe { check_call!(raw::store_real_path( - &mut ctx, + &mut self.context, self.inner.ptr(), path.as_ptr(), Some(callback_get_result_string), @@ -338,10 +339,9 @@ impl Store { #[doc(alias = "nix_derivation_from_json")] pub fn derivation_from_json(&mut self, json: &str) -> Result { let json_cstr = CString::new(json)?; - let mut ctx = Context::new(); unsafe { let drv = check_call!(raw::derivation_from_json( - &mut ctx, + &mut self.context, self.inner.ptr(), json_cstr.as_ptr() ))?; @@ -365,10 +365,9 @@ impl Store { #[cfg(nix_at_least = "2.31")] #[doc(alias = "nix_add_derivation")] pub fn add_derivation(&mut self, drv: &Derivation) -> Result { - let mut ctx = Context::new(); unsafe { let path = check_call!(raw::add_derivation( - &mut ctx, + &mut self.context, self.inner.ptr(), drv.inner.as_ptr() ))?; @@ -381,10 +380,9 @@ impl Store { pub fn make_drv_outputs(&mut self, json: &str) -> Result> { let json = CString::new(json)?; let mut r = HashMap::new(); - let mut ctx = Context::new(); unsafe { check_call!(raw::derivation_make_outputs( - &mut ctx, + &mut self.context, self.inner.ptr(), json.as_ptr(), Some(callback_make_drv_outputs), @@ -427,10 +425,9 @@ impl Store { .map(|p| unsafe { p.as_ptr() as *const raw::StorePath }) .collect(); - let mut ctx = Context::new(); unsafe { check_call!(raw::store_build_paths( - &mut ctx, + &mut self.context, self.inner.ptr(), paths.as_mut_ptr(), paths.len() as std::os::raw::c_uint, @@ -482,10 +479,9 @@ impl Store { outputs.insert(name, path); } - let mut ctx = Context::new(); unsafe { check_call!(raw::store_realise( - &mut ctx, + &mut self.context, self.inner.ptr(), path.as_ptr(), userdata, @@ -524,10 +520,9 @@ impl Store { include_derivers: bool, ) -> Result> { let mut r = Vec::new(); - let mut ctx = Context::new(); unsafe { check_call!(raw::store_get_fs_closure( - &mut ctx, + &mut self.context, self.inner.ptr(), store_path.as_ptr(), flip_direction, @@ -543,10 +538,9 @@ impl Store { #[doc(alias = "nix_store_drv_from_path")] pub fn drv_from_path(&mut self, path: &StorePath) -> Result { let mut r = Err(Error::DerivationInit); - let mut ctx = Context::new(); unsafe { check_call!(raw::store_drv_from_path( - &mut ctx, + &mut self.context, self.inner.ptr(), path.as_ptr(), Some(callback_get_result_derivation), @@ -559,10 +553,9 @@ impl Store { #[doc(alias = "nix_store_query_path_info")] pub fn query_path_info(&mut self, path: &StorePath) -> Result { let mut r = Err(Error::StringInit); - let mut ctx = Context::new(); unsafe { check_call!(raw::store_query_path_info( - &mut ctx, + &mut self.context, self.inner.ptr(), path.as_ptr(), callback_get_result_string_data(&mut r), @@ -583,6 +576,7 @@ impl Clone for Store { fn clone(&self) -> Self { Store { inner: self.inner.clone(), + context: Context::new(), } } } diff --git a/nix-bindings-util/src/context.rs b/nix-bindings-util/src/context.rs index 2f74da98..f4371e8f 100644 --- a/nix-bindings-util/src/context.rs +++ b/nix-bindings-util/src/context.rs @@ -12,8 +12,6 @@ pub struct Context { inner: NonNull, } -unsafe impl Send for Context {} - impl Default for Context { fn default() -> Self { Self::new() From 4ef4ea8208f215b1a11d8c345123d5f071ca9198 Mon Sep 17 00:00:00 2001 From: Naxdy Date: Tue, 19 May 2026 13:59:33 +0200 Subject: [PATCH 2/2] [noupstream] Add `detnix` feature to individual crates --- nix-bindings-expr/Cargo.toml | 4 + nix-bindings-expr/src/eval_state_detnix.rs | 3004 ++++++++++++++++++++ nix-bindings-expr/src/lib.rs | 14 + nix-bindings-expr/src/primop_detnix.rs | 172 ++ nix-bindings-expr/src/value.rs | 7 + nix-bindings-flake/Cargo.toml | 4 + nix-bindings-flake/src/lib.rs | 22 + nix-bindings-store/Cargo.toml | 2 + nix-bindings-store/src/path/mod.rs | 5 + nix-bindings-store/src/store.rs | 162 +- nix-bindings-util/Cargo.toml | 4 + nix-bindings-util/src/context.rs | 3 + 12 files changed, 3386 insertions(+), 17 deletions(-) create mode 100644 nix-bindings-expr/src/eval_state_detnix.rs create mode 100644 nix-bindings-expr/src/primop_detnix.rs diff --git a/nix-bindings-expr/Cargo.toml b/nix-bindings-expr/Cargo.toml index 9bc0bbc4..15a1c943 100644 --- a/nix-bindings-expr/Cargo.toml +++ b/nix-bindings-expr/Cargo.toml @@ -36,3 +36,7 @@ dead-code = "allow" type-complexity = "allow" # We're still trying to make Nix more thread-safe, want forward-compat arc-with-non-send-sync = "allow" + +[features] +default = [] +detnix = ["nix-bindings-store/detnix", "nix-bindings-util/detnix"] diff --git a/nix-bindings-expr/src/eval_state_detnix.rs b/nix-bindings-expr/src/eval_state_detnix.rs new file mode 100644 index 00000000..9ade017a --- /dev/null +++ b/nix-bindings-expr/src/eval_state_detnix.rs @@ -0,0 +1,3004 @@ +//! # Nix Expression Evaluation +//! +//! This module provides the core [`EvalState`] type for evaluating Nix expressions +//! and extracting typed values from the results. +//! +//! ## Overview +//! +//! The [`EvalState`] manages the evaluation context for Nix expressions, including: +//! - Expression parsing and evaluation with [`eval_from_string`](EvalState::eval_from_string) +//! - Type-safe value extraction with [`require_*`](EvalState#implementations) methods +//! - Memory management and garbage collection integration +//! - Store integration for derivations and store paths +//! - Custom function creation with [`new_value_primop`](EvalState::new_value_primop) and [`new_value_thunk`](EvalState::new_value_thunk) +//! +//! ### Construction +//! +//! Create an [`EvalState`] using [`EvalState::new`] or [`EvalStateBuilder`] for advanced configuration: +//! +//! ```rust +//! # use nix_bindings_expr::eval_state::{EvalState, EvalStateBuilder, test_init, gc_register_my_thread, Result}; +//! # use nix_bindings_store::store::Store; +//! # use std::collections::HashMap; +//! # fn example() -> Result<()> { +//! # test_init(); let guard = gc_register_my_thread()?; +//! let store = Store::open(None, HashMap::new())?; +//! +//! // Simple creation +//! let mut es = EvalState::new(store.clone(), [])?; +//! +//! // With custom lookup paths +//! let mut es = EvalStateBuilder::new(store)? +//! .lookup_path(["nixpkgs=/path/to/nixpkgs"])? +//! .build()?; +//! # drop(guard); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Value Extraction +//! +//! All `require_*` methods perform these steps: +//! 1. **Evaluation**: Force evaluation of thunks as needed +//! 2. **Type checking**: Verify the value matches the expected type +//! 3. **Extraction**: Return the typed Rust value or an error +//! +//! Methods with `_strict` in their name also evaluate their return values before returning them. +//! +//! ### Evaluation Strictness +//! +//! - **Lazy methods** (e.g., [`require_list_size`](EvalState::require_list_size)): +//! Evaluate only the structure needed +//! - **Strict methods** (e.g., [`require_list_strict`](EvalState::require_list_strict)): +//! Force full evaluation of all contained values +//! - **Selective methods** (e.g., [`require_list_select_idx_strict`](EvalState::require_list_select_idx_strict)): +//! Evaluate only the accessed elements +//! +//! ## Laziness and Strictness +//! +//! The terms "lazy" and "strict" in this API refer to Nix's [Weak Head Normal Form (WHNF)](https://nix.dev/manual/nix/latest/language/evaluation.html#values) +//! evaluation model, not the kind of deep strictness that is exercised by functions such as `builtins.toJSON` or `builtins.deepSeq`. +//! +//! - **WHNF evaluation**: Values are evaluated just enough to determine their type and basic structure +//! - **Deep evaluation**: All nested values are recursively forced (like `builtins.deepSeq`) +//! +//! For example, a list in WHNF has its length determined but individual elements may remain unevaluated thunks. +//! Methods marked as "strict" in this API force WHNF evaluation of their results, but do not perform deep evaluation +//! of arbitrarily nested structures unless explicitly documented otherwise. +//! +//! ### Thread Safety and Memory Management +//! +//! Before using [`EvalState`] in a thread, register it with the (process memory) garbage collector: +//! +//! ```rust,no_run +//! # use nix_bindings_expr::eval_state::{init, gc_register_my_thread, test_init, Result}; +//! # fn example() -> Result<()> { +//! # test_init(); // Use test_init() in tests +//! init()?; // Initialize Nix library +//! let guard = gc_register_my_thread()?; // Register thread with GC +//! // Now safe to use EvalState in this thread +//! drop(guard); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Error Handling +//! +//! Evaluation methods return [`Result`] types. Common error scenarios include: +//! - **Type mismatches**: Expected type doesn't match actual value type +//! - **Evaluation errors**: Nix expressions that throw or have undefined behavior +//! - **Bounds errors**: Out-of-range access for indexed operations +//! +//! ## Examples +//! +//! ```rust +//! use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread, Result}; +//! use nix_bindings_store::store::Store; +//! use std::collections::HashMap; +//! +//! # fn main() -> Result<()> { +//! test_init(); // init() in non-test code +//! let guard = gc_register_my_thread()?; +//! +//! let store = Store::open(None, HashMap::new())?; +//! let mut es = EvalState::new(store, [])?; +//! +//! // Evaluate a list expression +//! let list_value = es.eval_from_string("[1 2 3]", "")?; +//! +//! // Check the size (lazy - doesn't evaluate elements) +//! let size = es.require_list_size(&list_value)?; +//! println!("List has {} elements", size); +//! +//! // Access specific elements (evaluates only accessed elements) +//! if let Some(first) = es.require_list_select_idx_strict(&list_value, 0)? { +//! let value = es.require_int(&first)?; +//! println!("First element: {}", value); +//! } +//! +//! // Process all elements (evaluates all elements) +//! let all_elements: Vec<_> = es.require_list_strict(&list_value)?; +//! for element in all_elements { +//! let value = es.require_int(&element)?; +//! println!("Element: {}", value); +//! } +//! +//! drop(guard); +//! # Ok(()) +//! # } +//! ``` + +use crate::primop; +use crate::value::{Int, Value, ValueType}; +use cstr::cstr; +use nix_bindings_bdwgc_sys as gc; +use nix_bindings_expr_sys as raw; +use nix_bindings_store::path::StorePath; +use nix_bindings_store::store::Store; +use nix_bindings_store_sys as raw_store; +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, check_call_opt_key, Error as NixBindingsError, GcError, NixError, +}; +use std::ffi::{c_char, CString, NulError}; +use std::iter::FromIterator; +use std::os::raw::c_uint; +use std::ptr::{null, null_mut, NonNull}; +use std::str::Utf8Error; +use std::string::FromUtf8Error; +use std::sync::LazyLock; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum EvalStateError { + #[error("attribute {0} not found")] + MissingAttribute(String), + #[error("expected a value of type {expected:?}, but got {received:?} instead")] + UnexpectedValueType { + expected: ValueType, + received: ValueType, + }, + #[error("{arg} contains null byte")] + NulError { + arg: &'static str, + #[source] + err: NulError, + }, + #[error("{0} returned a null pointer")] + NullPointer(&'static str), + + #[error(transparent)] + NixBindings(#[from] NixBindingsError), + #[error(transparent)] + StrUtf8Error(#[from] Utf8Error), + #[error(transparent)] + StringUtf8Error(#[from] FromUtf8Error), +} + +static INIT: LazyLock> = LazyLock::new(|| unsafe { + gc::GC_allow_register_threads(); + check_call!(raw::libexpr_init(&mut Context::new()))?; + Ok(()) +}); + +pub fn init() -> nix_bindings_util::Result<()> { + let x = INIT.as_ref(); + match x { + Ok(_) => Ok(()), + Err(e) => Err(NixBindingsError::NixBindingsExprInit(e)), + } +} + +/// A string value with its associated [store paths](https://nix.dev/manual/nix/stable/store/store-path.html). +/// +/// Represents a Nix string with references to store paths. +pub struct RealisedString { + /// The string content. + pub s: String, + /// Store paths referenced by the string. + pub paths: Vec, +} + +#[clippy::has_significant_drop] +struct EvalStateRef { + eval_state: NonNull, +} + +unsafe impl Send for EvalStateRef {} + +impl EvalStateRef { + /// Returns a raw pointer to the underlying EvalState. + /// + /// # Safety + /// + /// The caller must ensure that the pointer is not used beyond the lifetime of the underlying [raw::EvalState]. + unsafe fn as_ptr(&self) -> *mut raw::EvalState { + self.eval_state.as_ptr() + } +} + +impl Drop for EvalStateRef { + fn drop(&mut self) { + unsafe { + raw::state_free(self.eval_state.as_ptr()); + } + } +} +/// Builder for configuring and creating an [`EvalState`]. +/// +/// 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 +/// # use nix_bindings_expr::eval_state::{EvalState, EvalStateBuilder, test_init, gc_register_my_thread, Result}; +/// # use nix_bindings_store::store::Store; +/// # use std::collections::HashMap; +/// # fn example() -> Result<()> { +/// # test_init(); +/// # let guard = gc_register_my_thread()?; +/// let store = Store::open(None, HashMap::new())?; +/// +/// let mut es: EvalState = EvalStateBuilder::new(store)? +/// .lookup_path(["nixpkgs=/path/to/nixpkgs", "home-manager=/path/to/hm"])? +/// .build()?; +/// +/// let value = es.eval_from_string("", /* path display: */ "in-memory")?; +/// # drop(guard); +/// # Ok(()) +/// # } +/// ``` +#[cfg(nix_at_least = "2.26")] +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")] +impl Drop for EvalStateBuilder { + fn drop(&mut self) { + unsafe { + raw::eval_state_builder_free(self.eval_state_builder); + } + } +} +#[cfg(nix_at_least = "2.26")] +impl EvalStateBuilder { + /// Creates a new [`EvalStateBuilder`]. + pub fn new(store: Store) -> Result { + let mut context = Context::new(); + let eval_state_builder = + unsafe { check_call!(raw::eval_state_builder_new(&mut context, store.raw_ptr())) }?; + Ok(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. + pub fn lookup_path<'a>(mut self, path: impl IntoIterator) -> Result { + let lookup_path: Vec = path + .into_iter() + .map(|path| { + CString::new(path).map_err(|e| EvalStateError::NulError { + arg: "path", + err: e, + }) + }) + .collect::>()?; + 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 + init()?; + + 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). + 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 + let mut lookup_path: Vec<*const c_char> = self + .lookup_path + .iter() + .map(|s| s.as_ptr()) + .chain(std::iter::once(null())) // signal the end of the array + .collect(); + + unsafe { + check_call!(raw::eval_state_builder_set_lookup_path( + &mut context, + self.eval_state_builder, + lookup_path.as_mut_ptr() + ))?; + } + + let eval_state = + unsafe { check_call!(raw::eval_state_build(&mut context, self.eval_state_builder)) }?; + Ok(EvalState { + eval_state: EvalStateRef { + eval_state: NonNull::new(eval_state).unwrap_or_else(|| { + panic!("nix_state_create returned a null pointer without an error") + }), + }, + store: self.store.clone(), + }) + } + /// Returns a raw pointer to the underlying eval state builder. + /// + /// # Safety + /// + /// The caller must ensure that the pointer is not used beyond the lifetime of this builder. + // TODO: This function should be marked `unsafe`. + pub fn raw_ptr(&self) -> *mut raw::eval_state_builder { + self.eval_state_builder + } +} + +/// An abstraction over the underlying eval state. +/// +/// When an `EvalState` is constructed, it will allocate a number of threads to be used for +/// evaluating expressions. These threads will remain allocated until the `EvalState` is dropped. +/// +/// Note: Determinate Nix' EvalState is thread-safe due to parallel eval, which is why it +/// implements [`Send`] and [`Sync`], and takes in `&self` as opposed to `&mut self` for most of +/// its methods. Upstream Nix is not (yet) thread-safe. +#[clippy::has_significant_drop] +pub struct EvalState { + eval_state: EvalStateRef, + store: Store, +} + +unsafe impl Send for EvalState {} +unsafe impl Sync for EvalState {} + +impl EvalState { + /// Creates a new EvalState with basic configuration. + /// + /// For more options, use [EvalStateBuilder]. + pub fn new<'a>(store: Store, lookup_path: impl IntoIterator) -> Result { + EvalStateBuilder::new(store)? + .lookup_path(lookup_path)? + .build() + } + + /// Returns a raw pointer to the raw Nix C API EvalState. + /// + /// # Safety + /// + /// The caller must ensure that the pointer is not used beyond the lifetime of this `EvalState`. + pub unsafe fn raw_ptr(&self) -> *mut raw::EvalState { + self.eval_state.as_ptr() + } + + /// Returns a reference to the Store that's used for instantiation, import from derivation, etc. + pub fn store(&self) -> &Store { + &self.store + } + + /// Parses and evaluates a Nix expression `expr`. + /// + /// Expressions can contain relative paths such as `./.` that are resolved relative to the given `path`. + /// + /// # Examples + /// + /// ``` + /// # use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread, Result}; + /// use nix_bindings_store::store::Store; + /// use nix_bindings_expr::value::Value; + /// use std::collections::HashMap; + /// + /// # fn main() -> Result<()> { + /// # test_init(); + /// # let guard = gc_register_my_thread()?; + /// # let mut es = EvalState::new(Store::open(None, HashMap::new())?, [])?; + /// let v: Value = es.eval_from_string("42", ".")?; + /// assert_eq!(es.require_int(&v)?, 42); + /// # drop(guard); + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "nix_expr_eval_from_string")] + #[doc(alias = "parse")] + #[doc(alias = "eval")] + #[doc(alias = "evaluate")] + pub fn eval_from_string(&self, expr: &str, path: &str) -> Result { + let expr_ptr = CString::new(expr).map_err(|e| EvalStateError::NulError { + arg: "expr", + err: e, + })?; + let path_ptr = CString::new(path).map_err(|e| EvalStateError::NulError { + arg: "path", + err: e, + })?; + let mut ctx = Context::new(); + unsafe { + let value = self.new_value_uninitialized()?; + check_call!(raw::expr_eval_from_string( + &mut ctx, + self.eval_state.as_ptr(), + expr_ptr.as_ptr(), + path_ptr.as_ptr(), + value.raw_ptr() + ))?; + Ok(value) + } + } + + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) of a value to [weak head normal form](https://nix.dev/manual/nix/latest/language/evaluation.html?highlight=WHNF#values). + /// + /// Converts [thunks](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) to their evaluated form. Does not modify already-evaluated values. + /// + /// Does not perform deep evaluation of nested structures. + /// + /// See also: [Shared Evaluation State](Value#shared-evaluation-state) + #[doc(alias = "evaluate")] + #[doc(alias = "strict")] + pub fn force(&self, v: &Value) -> Result<()> { + let mut ctx = Context::new(); + unsafe { + check_call!(raw::value_force( + &mut ctx, + self.eval_state.as_ptr(), + v.raw_ptr() + )) + }?; + Ok(()) + } + + /// Returns the type of a value without forcing [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html). + /// + /// Returns [`None`] if the value is an unevaluated [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness). + /// + /// Returns [`Some`] if the value is already evaluated. + /// + /// See also: [Shared Evaluation State](Value#shared-evaluation-state) + #[doc(alias = "type_of")] + #[doc(alias = "value_type_lazy")] + #[doc(alias = "nix_get_type")] + #[doc(alias = "get_type")] + #[doc(alias = "nix_value_type")] + #[deprecated(note = "Values will now always be forced; use `EvalState::value_type` instead.")] + pub fn value_type_unforced(&self, value: &Value) -> Option { + self.value_type(value).ok() + } + + /// Returns the [type][`ValueType`] of a value, [forcing][`EvalState::force`] [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) if necessary. + /// + /// Forces evaluation if the value is an unevaluated [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness). + /// + /// Evaluation may fail, producing an [`Err`]. + /// + /// Guarantees a definitive result if [`Ok`], thanks to the language being [pure](https://nix.dev/manual/nix/latest/language/index.html?highlight=pure#nix-language) and [lazy](https://nix.dev/manual/nix/latest/language/index.html?highlight=lazy#nix-language). + #[doc(alias = "type_of")] + #[doc(alias = "value_type_strict")] + #[doc(alias = "nix_get_type")] + #[doc(alias = "get_type")] + #[doc(alias = "nix_value_type_strict")] + pub fn value_type(&self, value: &Value) -> Result { + self.force(value)?; + + let mut ctx = Context::new(); + let r = unsafe { check_call!(raw::get_type(&mut ctx, value.raw_ptr())) }; + + Ok(ValueType::from_raw(r.unwrap()) + .expect("ValueType must not be a thunk after being forced")) + } + /// Extracts the value from an [integer][`ValueType::Int`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is an integer. + /// + /// Returns the integer value if successful, or an [`Err`] if evaluation failed or the value is not an integer. + /// + /// # Examples + /// + /// ```rust + /// # use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread, Result}; + /// # use nix_bindings_store::store::Store; + /// # use std::collections::HashMap; + /// # fn example() -> Result<()> { + /// # test_init(); + /// # let guard = gc_register_my_thread()?; + /// let store = Store::open(None, HashMap::new())?; + /// let mut es = EvalState::new(store, [])?; + /// + /// let value = es.eval_from_string("42", "")?; + /// let int_val = es.require_int(&value)?; + /// assert_eq!(int_val, 42); + /// # drop(guard); + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "integer")] + #[doc(alias = "number")] + #[doc(alias = "nix_get_int")] + #[doc(alias = "get_int")] + pub fn require_int(&self, v: &Value) -> Result { + let t = self.value_type(v)?; + if t != ValueType::Int { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::Int, + received: t, + })? + } + let mut ctx = Context::new(); + unsafe { check_call!(raw::get_int(&mut ctx, v.raw_ptr())) }.map_err(Into::into) + } + + /// Extracts the value from a [boolean][`ValueType::Bool`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is a boolean. + /// + /// Returns the boolean value if successful, or an [`Err`] if evaluation failed or the value is not a boolean. + #[doc(alias = "boolean")] + #[doc(alias = "nix_get_bool")] + #[doc(alias = "get_bool")] + pub fn require_bool(&self, v: &Value) -> Result { + let t = self.value_type(v)?; + if t != ValueType::Bool { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::Bool, + received: t, + })? + } + let mut ctx = Context::new(); + unsafe { check_call!(raw::get_bool(&mut ctx, v.raw_ptr())) }.map_err(Into::into) + } + + /// Extracts all elements from a [list][`ValueType::List`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is a list. + /// + /// Returns the contained values in the specified container type (e.g., [`Vec`], [`VecDeque`][`std::collections::VecDeque`], etc.). + /// + /// This is [strict](https://nix.dev/manual/nix/latest/language/evaluation.html#strictness) - all list elements will be evaluated. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use nix_bindings_expr::eval_state::Result; + /// # use nix_bindings_expr::value::Value; + /// # use std::collections::{VecDeque, LinkedList}; + /// # fn example(es: &mut nix_bindings_expr::eval_state::EvalState, list_value: &Value) -> Result<()> { + /// let vec: Vec = es.require_list_strict(&list_value)?; + /// let deque: VecDeque = es.require_list_strict(&list_value)?; + /// let linked_list = es.require_list_strict::>(&list_value)?; + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "collect")] + #[doc(alias = "to_vec")] + #[doc(alias = "all")] + #[doc(alias = "nix_get_list_size")] + #[doc(alias = "nix_get_list_byidx")] + pub fn require_list_strict(&self, value: &Value) -> Result + where + C: FromIterator, + { + let t = self.value_type(value)?; + if t != ValueType::List { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::List, + received: t, + })? + } + let mut ctx = Context::new(); + let size = unsafe { check_call!(raw::get_list_size(&mut ctx, value.raw_ptr())) }?; + + (0..size) + .map(|i| { + let element_ptr = unsafe { + check_call!(raw::get_list_byidx( + &mut ctx, + value.raw_ptr(), + self.eval_state.as_ptr(), + i + )) + }?; + Ok(unsafe { Value::new(element_ptr) }) + }) + .collect() + } + + /// Evaluate, and require that the [`Value`] is a Nix [`ValueType::AttrSet`]. + /// + /// Returns a list of the keys in the attrset. + /// + /// NOTE: this currently implements its own sorting, which probably matches Nix's implementation, but is not guaranteed. + #[doc(alias = "keys")] + #[doc(alias = "attributes")] + #[doc(alias = "fields")] + pub fn require_attrs_names(&self, v: &Value) -> Result> { + self.require_attrs_names_unsorted(v).map(|mut v| { + v.sort(); + v + }) + } + + /// For when [`EvalState::require_attrs_names`] isn't fast enough. + /// + /// Only use when it's ok that the keys are returned in an arbitrary order. + #[doc(alias = "keys_unsorted")] + #[doc(alias = "attributes_unsorted")] + pub fn require_attrs_names_unsorted(&self, v: &Value) -> Result> { + let t = self.value_type(v)?; + if t != ValueType::AttrSet { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::AttrSet, + received: t, + })? + } + let mut ctx = Context::new(); + let n = unsafe { check_call!(raw::get_attrs_size(&mut ctx, v.raw_ptr())) }?; + let mut attrs = Vec::with_capacity(n as usize); + for i in 0..n { + let cstr_ptr: *const c_char = unsafe { + check_call!(raw::get_attr_name_byidx( + &mut ctx, + v.raw_ptr(), + self.eval_state.as_ptr(), + i as c_uint + )) + }?; + let cstr = unsafe { std::ffi::CStr::from_ptr(cstr_ptr) }; + let s = cstr.to_str()?; + attrs.insert(i as usize, s.to_owned()); + } + Ok(attrs) + } + + /// Extracts an attribute value from an [attribute set][`ValueType::AttrSet`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is an attribute set. + /// + /// Returns the attribute value if found, or an [`Err`] if evaluation failed, the attribute doesn't exist, or the value is not an attribute set. + #[doc(alias = "get_attr")] + #[doc(alias = "attribute")] + #[doc(alias = "field")] + pub fn require_attrs_select(&self, v: &Value, attr_name: &str) -> Result { + let t = self.value_type(v)?; + if t != ValueType::AttrSet { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::AttrSet, + received: t, + })? + } + + let attr_name = CString::new(attr_name).map_err(|e| EvalStateError::NulError { + arg: "attrName", + err: e, + })?; + let mut ctx = Context::new(); + unsafe { + let v2 = check_call!(raw::get_attr_byname( + &mut ctx, + v.raw_ptr(), + self.eval_state.as_ptr(), + attr_name.as_ptr() + )); + match v2 { + Ok(v2) => Ok(Value::new(v2)), + Err(e) => { + // As of Nix 2.26, the error message is not helpful when it + // is simply missing, so we provide a better one. (Note that + // missing attributes requested by Nix expressions OTOH is a + // different error message which works fine.) + if let NixBindingsError::Nix(NixError::Key(e)) = &e { + if e.eq("missing attribute") { + Err(EvalStateError::MissingAttribute( + attr_name.to_string_lossy().to_string(), + ))? + } + } + + Err(e)? + } + } + } + } + + /// Extracts an optional attribute value from an [attribute set][`ValueType::AttrSet`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is an attribute set. + /// + /// Returns [`Err`] if evaluation failed or the value is not an attribute set. + /// + /// Returns `Ok(None)` if the attribute is not present. + /// + /// Returns `Ok(Some(value))` if the attribute is present. + #[doc(alias = "nix_get_attr_byname")] + #[doc(alias = "get_attr_byname")] + #[doc(alias = "get_attr_opt")] + #[doc(alias = "try_get")] + #[doc(alias = "maybe_get")] + pub fn require_attrs_select_opt(&self, v: &Value, attr_name: &str) -> Result> { + let t = self.value_type(v)?; + if t != ValueType::AttrSet { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::AttrSet, + received: t, + })? + } + let attr_name = CString::new(attr_name).map_err(|e| EvalStateError::NulError { + arg: "attrName", + err: e, + })?; + let mut ctx = Context::new(); + let v2 = unsafe { + check_call_opt_key!(raw::get_attr_byname( + &mut ctx, + v.raw_ptr(), + self.eval_state.as_ptr(), + attr_name.as_ptr() + )) + }?; + Ok(v2.map(|x| unsafe { Value::new(x) })) + } + + /// Returns the number of elements in a [list][`ValueType::List`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) of the list structure and verifies the value is a list. + /// + /// Individual elements remain as lazy [thunks](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) and are not evaluated. + #[doc(alias = "length")] + #[doc(alias = "count")] + #[doc(alias = "len")] + #[doc(alias = "nix_get_list_size")] + #[doc(alias = "get_list_size")] + pub fn require_list_size(&self, v: &Value) -> Result { + let t = self.value_type(v)?; + if t != ValueType::List { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::List, + received: t, + })? + } + let mut ctx = Context::new(); + let ret = unsafe { check_call!(raw::get_list_size(&mut ctx, v.raw_ptr())) }?; + Ok(ret) + } + + /// Extracts an element from a [list][`ValueType::List`] Nix value by index. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is a list. + /// Forces evaluation of the selected element, similar to [`Self::require_attrs_select`]. + /// + /// Returns `Ok(Some(value))` if the element is found. + /// + /// Returns `Ok(None)` if the index is out of bounds. + /// + /// Returns [`Err`] if evaluation failed, the element contains an error (e.g., `throw`), or the value is not a list. + #[doc(alias = "get")] + #[doc(alias = "index")] + #[doc(alias = "at")] + #[doc(alias = "nix_get_list_byidx")] + #[doc(alias = "get_list_byidx")] + pub fn require_list_select_idx_strict(&self, v: &Value, idx: u32) -> Result> { + let t = self.value_type(v)?; + if t != ValueType::List { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::List, + received: t, + })? + } + + // TODO: Remove this bounds checking once https://github.com/NixOS/nix/pull/14030 + // is merged, which will add proper bounds checking to the underlying C API. + // Currently we perform bounds checking in Rust to avoid undefined behavior. + let mut ctx = Context::new(); + let size = unsafe { check_call!(raw::get_list_size(&mut ctx, v.raw_ptr())) }?; + + if idx >= size { + return Ok(None); + } + + let v2 = unsafe { + check_call_opt_key!(raw::get_list_byidx( + &mut ctx, + v.raw_ptr(), + self.eval_state.as_ptr(), + idx + )) + }?; + Ok(v2.map(|x| unsafe { Value::new(x) })) + } + + /// Creates a new [string][`ValueType::String`] Nix value. + /// + /// Returns a string value without any [string context](https://nix.dev/manual/nix/latest/language/string-context.html). + #[doc(alias = "make_string")] + #[doc(alias = "create_string")] + #[doc(alias = "string_value")] + pub fn new_value_str(&self, s: &str) -> Result { + let s = CString::new(s).map_err(|e| EvalStateError::NulError { arg: "s", err: e })?; + let v = unsafe { + let value = self.new_value_uninitialized()?; + let mut ctx = Context::new(); + check_call!(raw::init_string(&mut ctx, value.raw_ptr(), s.as_ptr()))?; + value + }; + Ok(v) + } + + /// Creates a new [integer][`ValueType::Int`] Nix value. + #[doc(alias = "make_int")] + #[doc(alias = "create_int")] + #[doc(alias = "int_value")] + #[doc(alias = "integer_value")] + pub fn new_value_int(&self, i: Int) -> Result { + let v = unsafe { + let value = self.new_value_uninitialized()?; + let mut ctx = Context::new(); + check_call!(raw::init_int(&mut ctx, value.raw_ptr(), i))?; + value + }; + Ok(v) + } + + /// Creates a new [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) Nix value. + /// + /// The [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) will lazily evaluate to the result of the given Rust function when forced. + /// The Rust function will be called with the current [`EvalState`] and must not return a thunk. + /// + /// The name is shown in stack traces. + #[doc(alias = "make_thunk")] + #[doc(alias = "create_thunk")] + #[doc(alias = "lazy_value")] + pub fn new_value_thunk( + &mut self, + name: &str, + f: Box std::result::Result>>, + ) -> Result { + // Nix doesn't have a function for creating a thunk, so we have to + // create a function and pass it a dummy argument. + let name = CString::new(name).map_err(|e| EvalStateError::NulError { + arg: "name", + err: e, + })?; + let primop = primop::PrimOp::new( + self, + primop::PrimOpMeta { + // name is observable in stack traces, ie if the thunk returns Err + name: name.as_c_str(), + // doc is unlikely to be observable, so we provide a constant one for simplicity. + doc: cstr!("Performs an on demand computation, implemented outside the Nix language in native code."), + // like doc, unlikely to be observed + args: [CString::new("internal_unused").unwrap().as_c_str()], + }, + Box::new(move |eval_state, _dummy: &[Value; 1]| f(eval_state)), + )?; + + let p = primop.new_value()?; + self.new_value_apply(&p, &p) + } + + /// Not exposed, because the caller must always explicitly handle the context or not accept one at all. + fn get_string(&self, value: &Value) -> Result { + let mut r = Err(NixBindingsError::StringInit); + let mut ctx = Context::new(); + unsafe { + check_call!(raw::get_string( + &mut ctx, + value.raw_ptr(), + Some(callback_get_result_string), + callback_get_result_string_data(&mut r) + ))?; + }; + Ok(r?) + } + /// Extracts a string value from a [string][`ValueType::String`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is a string. + /// Returns the string value if successful, or an [`Err`] if evaluation failed or the value is not a string. + /// + /// NOTE: this will be replaced by two methods, one that also returns the context, and one that checks that the context is empty. + #[doc(alias = "str")] + #[doc(alias = "text")] + #[doc(alias = "nix_get_string")] + #[doc(alias = "get_string")] + pub fn require_string(&self, value: &Value) -> Result { + let t = self.value_type(value)?; + if t != ValueType::String { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::String, + received: t, + })? + } + self.get_string(value) + } + /// Realises a [string][`ValueType::String`] Nix value with context information. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html), verifies the value is a string, and builds any derivations + /// referenced in the [string context](https://nix.dev/manual/nix/latest/language/string-context.html) if required. + #[doc(alias = "realize_string")] + #[doc(alias = "string_with_context")] + #[doc(alias = "build_string")] + pub fn realise_string( + &self, + value: &Value, + is_import_from_derivation: bool, + ) -> Result { + let t = self.value_type(value)?; + if t != ValueType::String { + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::String, + received: t, + })? + } + + let mut ctx = Context::new(); + let rs = unsafe { + check_call!(raw::string_realise( + &mut ctx, + self.eval_state.as_ptr(), + value.raw_ptr(), + is_import_from_derivation + )) + }?; + + let s = unsafe { + let start = raw::realised_string_get_buffer_start(rs) as *const std::os::raw::c_char; + let size = raw::realised_string_get_buffer_size(rs); + let slice = std::slice::from_raw_parts(start.cast::(), size); + String::from_utf8(slice.to_vec())? + }; + + let paths = unsafe { + let n = raw::realised_string_get_store_path_count(rs); + let mut paths = Vec::with_capacity(n as usize); + for i in 0..n { + let path = raw::realised_string_get_store_path(rs, i); + let path = NonNull::new(path as *mut raw_store::StorePath).ok_or( + EvalStateError::NullPointer("nix_realised_string_get_store_path"), + )?; + paths.push(StorePath::new_raw_clone(path)); + } + paths + }; + + // We've converted the nix_realised_string to a native struct containing copies, so we can free it now. + unsafe { + raw::realised_string_free(rs); + } + + Ok(RealisedString { s, paths }) + } + + /// Applies a function to an argument and returns the result. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) of the function application. + /// For a lazy version, see [`Self::new_value_apply`]. + #[doc(alias = "nix_value_call")] + #[doc(alias = "value_call")] + #[doc(alias = "apply")] + #[doc(alias = "invoke")] + #[doc(alias = "execute")] + pub fn call(&self, f: Value, a: Value) -> Result { + let value = self.new_value_uninitialized()?; + let mut ctx = Context::new(); + unsafe { + check_call!(raw::value_call( + &mut ctx, + self.eval_state.as_ptr(), + f.raw_ptr(), + a.raw_ptr(), + value.raw_ptr() + )) + }?; + Ok(value) + } + + /// Apply a sequence of [function applications](https://nix.dev/manual/nix/latest/language/operators.html#function-application). + /// + /// When argument `f` is a curried function, this applies each argument in sequence. + /// Equivalent to the Nix expression `f arg1 arg2 arg3`. + /// + /// Returns a [`Value`] in at least weak head normal form if successful. + /// + /// Returns an [`Err`] + /// - if `f` did not evaluate to a function + /// - if `f arg1` had any problems + /// - if `f arg1` did not evaluate to a function (for `(f arg1) arg2`) + /// - etc + /// + /// # Examples + /// + /// ```rust + /// # use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread, Result}; + /// # use nix_bindings_store::store::Store; + /// # use std::collections::HashMap; + /// # fn example() -> Result<()> { + /// # test_init(); + /// # let guard = gc_register_my_thread()?; + /// let store = Store::open(None, HashMap::new())?; + /// let mut es = EvalState::new(store, [])?; + /// + /// // Create a curried function: x: y: x + y + /// let f = es.eval_from_string("x: y: x + y", "")?; + /// let arg1 = es.eval_from_string("5", "")?; + /// let arg2 = es.eval_from_string("3", "")?; + /// + /// // Equivalent to: (x: y: x + y) 5 3 + /// let result = es.call_multi(&f, &[arg1, arg2])?; + /// let value = es.require_int(&result)?; + /// assert_eq!(value, 8); + /// # drop(guard); + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "nix_value_call_multi")] + #[doc(alias = "value_call_multi")] + #[doc(alias = "apply_multi")] + #[doc(alias = "curry")] + #[doc(alias = "call_with_args")] + pub fn call_multi(&self, f: &Value, args: &[Value]) -> Result { + let value = self.new_value_uninitialized()?; + let mut ctx = Context::new(); + unsafe { + let mut args_ptrs = args.iter().map(|a| a.raw_ptr()).collect::>(); + check_call!(raw::value_call_multi( + &mut ctx, + self.eval_state.as_ptr(), + f.raw_ptr(), + args_ptrs.len(), + args_ptrs.as_mut_ptr(), + value.raw_ptr() + )) + }?; + Ok(value) + } + + /// Applies a function to an argument lazily, creating a [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness). + /// + /// Does not force [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) of the function application. + /// For an eager version, see [`Self::call`]. + #[doc(alias = "lazy_apply")] + #[doc(alias = "thunk_apply")] + #[doc(alias = "defer_call")] + pub fn new_value_apply(&self, f: &Value, a: &Value) -> Result { + let value = self.new_value_uninitialized()?; + let mut ctx = Context::new(); + unsafe { + check_call!(raw::init_apply( + &mut ctx, + value.raw_ptr(), + f.raw_ptr(), + a.raw_ptr() + )) + }?; + Ok(value) + } + + pub(crate) fn new_value_uninitialized(&self) -> Result { + let mut ctx = Context::new(); + unsafe { + let value = check_call!(raw::alloc_value(&mut ctx, self.eval_state.as_ptr()))?; + Ok(Value::new(value)) + } + } + + /// Creates a new [function][`ValueType::Function`] Nix value implemented by a Rust function. + /// + /// This is also known as a "primop" in Nix, short for primitive operation. + /// Most of the `builtins.*` values are examples of primops, but this function + /// does not affect `builtins`. + /// + /// # Deprecated + /// + /// This function is deprecated and has been replaced by + /// [`PrimOp::new_value`](crate::primop::PrimOp::new_value). + #[doc(alias = "make_primop")] + #[doc(alias = "create_function")] + #[doc(alias = "builtin")] + #[deprecated = "use `PrimOp::new_value` instead"] + pub fn new_value_primop(primop: primop::PrimOp) -> Result { + primop.new_value() + } + + /// Creates a new [attribute set][`ValueType::AttrSet`] Nix value from an iterator of name-value pairs. + /// + /// Accepts any iterator that yields `(String, Value)` pairs and has an exact size. + /// Common usage includes [`Vec`], [`std::collections::HashMap`], and array literals. + /// + /// # Examples + /// + /// ```rust + /// # use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread, Result}; + /// # use nix_bindings_store::store::Store; + /// # use std::collections::HashMap; + /// # fn example() -> Result<()> { + /// # test_init(); + /// # let guard = gc_register_my_thread()?; + /// let store = Store::open(None, HashMap::new())?; + /// let mut es = EvalState::new(store, [])?; + /// let a = es.new_value_int(1)?; + /// let b = es.new_value_int(2)?; + /// let c = es.new_value_int(3)?; + /// let d = es.new_value_int(4)?; + /// + /// // From array + /// let attrs1 = es.new_value_attrs([ + /// ("x".to_string(), a), + /// ("y".to_string(), b) + /// ])?; + /// + /// // From HashMap + /// let mut map = HashMap::new(); + /// map.insert("foo".to_string(), c); + /// map.insert("bar".to_string(), d); + /// let attrs2 = es.new_value_attrs(map)?; + /// # drop(guard); + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "make_attrs")] + #[doc(alias = "create_attrset")] + #[doc(alias = "object")] + #[doc(alias = "record")] + pub fn new_value_attrs(&self, attrs: I) -> Result + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + let iter = attrs.into_iter(); + let size = iter.len(); + let bindings_builder = BindingsBuilder::new(self, size)?; + let mut ctx = Context::new(); + for (name, value) in iter { + let name = CString::new(name).map_err(|e| EvalStateError::NulError { + arg: "name", + err: e, + })?; + unsafe { + check_call!(raw::bindings_builder_insert( + &mut ctx, + bindings_builder.ptr, + name.as_ptr(), + value.raw_ptr() + ))?; + } + } + let value = self.new_value_uninitialized()?; + unsafe { + check_call!(raw::make_attrs( + &mut ctx, + value.raw_ptr(), + bindings_builder.ptr + ))?; + } + Ok(value) + } +} + +// Internal RAII helper; could be refactored and made pub +struct BindingsBuilder { + ptr: *mut raw::BindingsBuilder, +} +impl Drop for BindingsBuilder { + fn drop(&mut self) { + unsafe { + raw::bindings_builder_free(self.ptr); + } + } +} +impl BindingsBuilder { + fn new(eval_state: &EvalState, capacity: usize) -> Result { + let mut ctx = Context::new(); + let ptr = unsafe { + check_call!(raw::make_bindings_builder( + &mut ctx, + eval_state.eval_state.as_ptr(), + capacity + )) + }?; + Ok(BindingsBuilder { ptr }) + } +} + +/// Triggers garbage collection immediately. +#[doc(alias = "garbage_collect")] +#[doc(alias = "collect")] +#[doc(alias = "gc")] +pub fn gc_now() { + unsafe { + raw::gc_now(); + } +} + +/// RAII guard for thread registration with the garbage collector. +/// +/// Automatically unregisters the thread when dropped. +pub struct ThreadRegistrationGuard { + must_unregister: bool, +} +impl Drop for ThreadRegistrationGuard { + fn drop(&mut self) { + if self.must_unregister { + unsafe { + gc::GC_unregister_my_thread(); + } + } + } +} + +fn gc_register_my_thread_do_it() -> nix_bindings_util::Result<()> { + unsafe { + let mut sb: gc::GC_stack_base = gc::GC_stack_base { + mem_base: null_mut(), + }; + let r = gc::GC_get_stack_base(&mut sb); + if r as u32 != gc::GC_SUCCESS { + Err(GcError::from(r))?; + } + gc::GC_register_my_thread(&sb); + Ok(()) + } +} + +/// Register the calling thread with the Nix garbage collector. +/// +/// # Errors +/// +/// The returned `Err` will be of type [`Error::GcStackBase`](nix_bindings_util::Error::GcStackBase). +#[doc(alias = "register_thread")] +#[doc(alias = "thread_setup")] +#[doc(alias = "gc_register")] +pub fn gc_register_my_thread() -> Result { + init()?; + unsafe { + let already_done = gc::GC_thread_is_registered(); + if already_done != 0 { + return Ok(ThreadRegistrationGuard { + must_unregister: false, + }); + } + gc_register_my_thread_do_it()?; + Ok(ThreadRegistrationGuard { + must_unregister: true, + }) + } +} + +/// Initialize the Nix library for testing. This includes some modifications to the Nix settings, that must not be used in production. +/// Use at your own peril, in rust test suites. +#[doc(alias = "test_initialize")] +#[doc(alias = "test_setup")] +pub fn test_init() { + init().unwrap(); + + // During development, we encountered a problem where the build hook + // would cause the test suite to reinvokes itself, causing an infinite loop. + // While _NIX_TEST_NO_SANDBOX=1 should prevent this, we may also set the + // build hook to "" to prevent this. + nix_bindings_util::settings::set("build-hook", "").unwrap(); + + // When testing in the sandbox, the default build dir would be a parent of the storeDir, + // which causes an error. So we set a custom build dir here. + // Only available on linux + if cfg!(target_os = "linux") { + nix_bindings_util::settings::set("sandbox-build-dir", "/custom-build-dir-for-test") + .unwrap(); + } + std::env::set_var("_NIX_TEST_NO_SANDBOX", "1"); + + // The tests run offline + nix_bindings_util::settings::set("substituters", "").unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + use cstr::cstr; + use ctor::ctor; + use nix_bindings_util::NixError; + use std::collections::HashMap; + use std::fs::read_dir; + use std::io::Write as _; + use std::sync::{Arc, Mutex}; + + #[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. + pub fn gc_registering_current_thread(f: F) -> Result + where + F: FnOnce() -> R, + { + let guard = gc_register_my_thread()?; + let r = f(); + drop(guard); + Ok(r) + } + + #[test] + fn eval_state_new_and_drop() { + gc_registering_current_thread(|| { + // very basic test: make sure initialization doesn't crash + let store = Store::open(None, HashMap::new()).unwrap(); + let _e = EvalState::new(store, []).unwrap(); + }) + .unwrap(); + } + + #[test] + fn eval_state_lookup_path() { + let import_expression = "import + import "; + let integer0 = 83; + let integer1 = 103; + let mut test_file0 = tempfile::NamedTempFile::new().unwrap(); + let mut test_file1 = tempfile::NamedTempFile::new().unwrap(); + writeln!(test_file0, "{integer0}").unwrap(); + writeln!(test_file1, "{integer1}").unwrap(); + gc_registering_current_thread(|| { + let es = EvalState::new(Store::open(None, HashMap::new()).unwrap(), []).unwrap(); + assert!(es.eval_from_string(import_expression, "").is_err()); + + let es = EvalState::new( + Store::open(None, HashMap::new()).unwrap(), + [ + format!("test_file0={}", test_file0.path().to_str().unwrap()).as_str(), + format!("test_file1={}", test_file1.path().to_str().unwrap()).as_str(), + ], + ) + .unwrap(); + let ie = &es.eval_from_string(import_expression, "").unwrap(); + let v = es.require_int(ie).unwrap(); + assert_eq!(v, integer0 + integer1); + }) + .unwrap(); + test_file0.close().unwrap(); + test_file1.close().unwrap(); + } + + #[test] + fn eval_state_eval_from_string() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("1", "").unwrap(); + let v2 = v.clone(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::Int))); + let t2 = es.value_type(&v2); + assert!(matches!(t2, Ok(ValueType::Int))); + gc_now(); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_bool() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("true", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::Bool))); + let b = es.require_bool(&v).unwrap(); + assert!(b); + + let v = es.eval_from_string("false", "").unwrap(); + es.require_bool(&v).unwrap(); + let b = es.require_bool(&v).unwrap(); + assert!(!b); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_int() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("1", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v).unwrap(); + assert!(t == ValueType::Int); + let i = es.require_int(&v).unwrap(); + assert!(i == 1); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_int_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let v = es.new_value_apply(&f, &a).unwrap(); + // Note: value_type_unforced now forces evaluation + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::Int))); + let i = es.require_int(&v).unwrap(); + assert!(i == 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_bool_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: !x", "").unwrap(); + let a = es.eval_from_string("true", "").unwrap(); + let v = es.new_value_apply(&f, &a).unwrap(); + // Note: value_type_unforced now forces evaluation + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::Bool))); + let i = es.require_bool(&v).unwrap(); + assert!(!i); + }) + .unwrap(); + } + + /// A helper that turns an expression into a thunk. + fn make_thunk(es: &EvalState, expr: &str) -> Value { + // This would be silly in real code, but it works for the current Nix implementation. + // A Nix implementation that applies the identity function eagerly would be a valid + // Nix implementation, but annoying because we'll have to change this helper to do + // something more complicated that isn't optimized away. + let f = es.eval_from_string("x: x", "").unwrap(); + let v = es.eval_from_string(expr, "").unwrap(); + es.new_value_apply(&f, &v).unwrap() + } + + #[test] + fn make_thunk_helper_works() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&es, "1"); + // Note: value_type_unforced now forces evaluation, so the value is always forced. + let t = es.value_type(&v); + assert!(t.is_ok()); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_attrs_names_empty() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("{ }", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::AttrSet))); + let attrs = es.require_attrs_names_unsorted(&v).unwrap(); + assert_eq!(attrs.len(), 0); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_names_unsorted_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&es, "{ a = 1; b = 2; }"); + // Note: value_type_unforced now forces evaluation + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::AttrSet))); + let attrs = es.require_attrs_names_unsorted(&v).unwrap(); + assert_eq!(attrs.len(), 2); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_names_unsorted_bad_type() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("1", "").unwrap(); + es.force(&v).unwrap(); + let r = es.require_attrs_names_unsorted(&v); + assert!(matches!( + r, + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::AttrSet, + received: ValueType::Int + }) + )); + }) + .unwrap() + } + + #[test] + fn eval_state_value_attrs_names_example() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = throw "nope a"; b = throw "nope b"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let attrs = es.require_attrs_names(&v).unwrap(); + assert_eq!(attrs.len(), 2); + assert_eq!(attrs[0], "a"); + assert_eq!(attrs[1], "b"); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_attrs_select() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = "aye"; b = "bee"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let a = es.require_attrs_select(&v, "a").unwrap(); + let b = es.require_attrs_select(&v, "b").unwrap(); + assert_eq!(es.require_string(&a).unwrap(), "aye"); + assert_eq!(es.require_string(&b).unwrap(), "bee"); + let missing = es.require_attrs_select(&v, "c"); + assert!(matches!(missing, Err(EvalStateError::MissingAttribute(_)))); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = "aye"; b = "bee"; }"#; + let v = make_thunk(&es, expr); + // Note: value_type_unforced now forces evaluation, so the value is always forced. + let r = es.require_attrs_select(&v, "a"); + assert!(r.is_ok()); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = throw "oh no the error"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let r = es.require_attrs_select(&v, "a"); + match r { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("oh no the error") { + eprintln!("{e:?}"); + panic!("unexpected error message: {}", e); + } + } + Err(e) => panic!( + "expected an err of type EvalStateError::NixBindings, but received {e:?}" + ), + } + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_opt() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = "aye"; b = "bee"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let a = es.require_attrs_select_opt(&v, "a").unwrap().unwrap(); + let b = es.require_attrs_select_opt(&v, "b").unwrap().unwrap(); + assert_eq!(es.require_string(&a).unwrap(), "aye"); + assert_eq!(es.require_string(&b).unwrap(), "bee"); + let c = es.require_attrs_select_opt(&v, "c").unwrap(); + assert!(c.is_none()); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_opt_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = "aye"; b = "bee"; }"#; + let v = make_thunk(&es, expr); + // Note: value_type_unforced now forces evaluation, so the value is always forced. + let r = es.require_attrs_select_opt(&v, "a"); + assert!(r.is_ok()); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_opt_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = throw "oh no the error"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let r = es.require_attrs_select_opt(&v, "a"); + match r { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(nix_bindings_util::Error::Nix(NixError::Nix( + e, + )))) => { + if !e.to_string().contains("oh no the error") { + panic!("unexpected error message: {}", e); + } + } + Err(e) => panic!("expected an underlying Nix error, but got {e:?}"), + } + }) + .unwrap() + } + + #[test] + fn eval_state_value_string() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("\"hello\"", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::String))); + let s = es.require_string(&v).unwrap(); + assert!(s == "hello"); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_string_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&es, "\"hello\""); + // Note: value_type_unforced now forces evaluation, so the value is always forced. + let s = es.require_string(&v).unwrap(); + assert!(s == "hello"); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_string_unexpected_bool() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("true", "").unwrap(); + es.force(&v).unwrap(); + let r = es.require_string(&v); + assert!(r.is_err()); + // TODO: safe print value (like Nix would) + assert_eq!( + r.unwrap_err().to_string(), + "expected a value of type String, but got Bool instead" + ); + }) + .unwrap() + } + + #[test] + fn eval_state_value_string_unexpected_path_value() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("/foo", "").unwrap(); + es.force(&v).unwrap(); + let r = es.require_string(&v); + assert!(r.is_err()); + assert_eq!( + r.unwrap_err().to_string(), + "expected a value of type String, but got Path instead" + ); + }) + .unwrap() + } + + #[test] + fn eval_state_value_string_bad_utf() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es + .eval_from_string("builtins.substring 0 1 \"ΓΌ\"", "") + .unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::String))); + let r = es.require_string(&v); + assert!(r.is_err()); + assert!(matches!( + r, + Err(EvalStateError::NixBindings( + nix_bindings_util::Error::StringUtf8Error(_) + )) + )) + }) + .unwrap(); + } + + #[test] + fn eval_state_value_string_unexpected_context() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es + .eval_from_string("(derivation { name = \"hello\"; system = \"dummy\"; builder = \"cmd.exe\"; }).outPath", "") + .unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::String))); + // TODO + // let r = es.require_string_without_context(&v); + // assert!(r.is_err()); + // assert!(r.unwrap_err().to_string().contains("unexpected context")); + }) + .unwrap(); + } + + #[test] + fn eval_state_new_string() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.new_value_str("hello").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::String))); + let s = es.require_string(&v).unwrap(); + assert!(s == "hello"); + }) + .unwrap(); + } + + #[test] + fn eval_state_new_string_empty() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.new_value_str("").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::String))); + let s = es.require_string(&v).unwrap(); + assert!(s.is_empty()); + }) + .unwrap(); + } + + #[test] + fn eval_state_new_string_invalid() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let r = es.new_value_str("hell\0no"); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("contains null byte") { + eprintln!("{}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_new_int() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.new_value_int(42).unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::Int))); + let i = es.require_int(&v).unwrap(); + assert!(i == 42); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_attrset() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("{ }", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::AttrSet))); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[ ]", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::List))); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_empty() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[]", "").unwrap(); + es.force(&v).unwrap(); + let list: Vec = es.require_list_strict(&v).unwrap(); + assert_eq!(list.len(), 0); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_int() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[42]", "").unwrap(); + es.force(&v).unwrap(); + let list: Vec = es.require_list_strict(&v).unwrap(); + assert_eq!(list.len(), 1); + assert_eq!(es.require_int(&list[0]).unwrap(), 42); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_int_bool() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[42 true]", "").unwrap(); + es.force(&v).unwrap(); + let list: Vec = es.require_list_strict(&v).unwrap(); + assert_eq!(list.len(), 2); + assert_eq!(es.require_int(&list[0]).unwrap(), 42); + assert!(es.require_bool(&list[1]).unwrap()); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es + .eval_from_string(r#"[(throw "_evaluated_item_")]"#, "") + .unwrap(); + es.force(&v).unwrap(); + // This should fail because require_list_strict evaluates all elements + let result: Result> = es.require_list_strict(&v); + assert!(result.is_err()); + match result { + Err(EvalStateError::NixBindings(nix_bindings_util::Error::Nix(NixError::Nix( + error_msg, + )))) => { + let error_str = error_msg.to_string(); + assert!(error_str.contains("_evaluated_item_")); + } + Ok(_) => panic!( + "unexpected success. The item should have \ + been evaluated and its error propagated." + ), + Err(e) => panic!("expected an underlying Nix error, but received {e:?}"), + } + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_generic_container() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[1 2 3]", "").unwrap(); + + // Test with Vec + let vec: Vec = es.require_list_strict(&v).unwrap(); + assert_eq!(vec.len(), 3); + + // Test with VecDeque + let deque: std::collections::VecDeque = es.require_list_strict(&v).unwrap(); + assert_eq!(deque.len(), 3); + + // Verify contents are the same + assert_eq!(es.require_int(&vec[0]).unwrap(), 1); + assert_eq!(es.require_int(&deque[0]).unwrap(), 1); + }) + .unwrap(); + } + + #[ignore = "Derivation fails to build in CI"] + #[test] + fn eval_state_realise_string() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let expr = r#" + '' + a derivation output: ${ + derivation { name = "letsbuild"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + }} + a path: ${builtins.toFile "just-a-file" "ooh file good"} + a derivation path by itself: ${ + builtins.unsafeDiscardOutputDependency + (derivation { + name = "not-actually-built-yet"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + }).drvPath} + '' + "#; + let v = es.eval_from_string(expr, "").unwrap(); + es.force(&v).unwrap(); + let rs = es.realise_string(&v, false).unwrap(); + + assert!(rs.s.starts_with("a derivation output:")); + assert!(rs.s.contains("-letsbuild\n")); + assert!(!rs.s.contains("-letsbuild.drv")); + assert!(rs.s.contains("a path:")); + assert!(rs.s.contains("-just-a-file")); + assert!(!rs.s.contains("-just-a-file.drv")); + assert!(!rs.s.contains("ooh file good")); + assert!(rs.s.ends_with("-not-actually-built-yet.drv\n")); + + assert_eq!(rs.paths.len(), 3); + let mut names: Vec = rs.paths.iter().map(|p| p.name().unwrap()).collect(); + names.sort(); + assert_eq!(names[0], "just-a-file"); + assert_eq!(names[1], "letsbuild"); + assert_eq!(names[2], "not-actually-built-yet.drv"); + }) + .unwrap(); + } + + #[test] + fn eval_state_call() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let v = es.call(f, a).unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::Int))); + let i = es.require_int(&v).unwrap(); + assert!(i == 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_call_multi() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + // This is a function that takes two arguments. + let f = es.eval_from_string("x: y: x - y", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let b = es.eval_from_string("3", "").unwrap(); + let v = es.call_multi(&f, &[a, b]).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::Int))); + let i = es.require_int(&v).unwrap(); + assert!(i == -1); + }) + .unwrap(); + } + + #[test] + fn eval_state_apply() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + // This is a function that takes two arguments. + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let v = es.new_value_apply(&f, &a).unwrap(); + // Note: value_type_unforced now forces evaluation, so the value is always forced. + es.force(&v).unwrap(); + let t = es.value_type(&v); + assert!(matches!(t, Ok(ValueType::Int))); + let i = es.require_int(&v).unwrap(); + assert!(i == 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_call_fail_body() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("true", "").unwrap(); + let r = es.call(f, a); + match r { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("cannot coerce") { + panic!("error message didn't contain expected string: {}", e); + } + } + Err(e) => panic!( + "expected an error of type EvalStateError::NixBindings, but received {e:?}" + ), + } + }) + .unwrap(); + } + + #[test] + fn eval_state_call_multi_fail_body() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + // This is a function that takes two arguments. + let f = es.eval_from_string("x: y: x - y", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let b = es.eval_from_string("true", "").unwrap(); + let r = es.call_multi(&f, &[a, b]); + match r { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("expected an integer but found") { + panic!("error message doesn't contain expected string: {}", e); + } + } + Err(e) => panic!( + "expected an error of type EvalStateError::NixBindings, but received {e:?}" + ), + } + }) + .unwrap(); + } + + #[test] + fn eval_state_apply_fail_body() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("true", "").unwrap(); + // Lazy => no error + let r = es.new_value_apply(&f, &a).unwrap(); + // Force it => error + let res = es.force(&r); + match res { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("cannot coerce") { + panic!("error message didn't contain expected string: {}", e) + } + } + Err(e) => panic!( + "expected an error of type EvalStateError::NixBindings, but received {e:?}" + ), + } + }) + .unwrap(); + } + + /// This tests the behavior of `call`, which is strict, unlike `new_value_apply`. + #[test] + fn eval_state_call_fail_args() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("{x}: x + 1", "").unwrap(); + let a = es.eval_from_string("{}", "").unwrap(); + let r = es.call(f, a); + match r { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("called without required argument") { + panic!("error message didn't contain expected string: {}", e); + } + } + Err(e) => panic!( + "expected an error of type EvalStateError::NixBindings, but received {e:?}" + ), + } + }) + .unwrap(); + } + + #[test] + fn eval_state_call_multi_fail_args() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + // This is a function that takes two arguments. + let f = es.eval_from_string("{x}: {y}: x - y", "").unwrap(); + let a = es.eval_from_string("{x = 2;}", "").unwrap(); + let b = es.eval_from_string("{}", "").unwrap(); + let r = es.call_multi(&f, &[a, b]); + match r { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("called without required argument") { + panic!("error message didn't contain expected string: {}", e); + } + } + Err(e) => panic!( + "expected an error of type EvalStateError::NixBindings, but received {e:?}" + ), + } + }) + .unwrap(); + } + + /// This tests the behavior of `new_value_apply`, which is lazy, unlike `call`. + #[test] + fn eval_state_apply_fail_args_lazy() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("{x}: x + 1", "").unwrap(); + let a = es.eval_from_string("{}", "").unwrap(); + // Lazy => no error + let r = es.new_value_apply(&f, &a).unwrap(); + // Force it => error + let res = es.force(&r); + match res { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("called without required argument") { + panic!("error message didn't contain expected string: {}", e); + } + } + Err(e) => panic!( + "expected an error of type EvalStateError::NixBindings, but received {e:?}" + ), + } + }) + .unwrap(); + } + + #[test] + fn store_open_params() { + gc_registering_current_thread(|| { + let store = tempfile::tempdir().unwrap(); + let store_path = store.path().to_str().unwrap(); + let state = tempfile::tempdir().unwrap(); + let state_path = state.path().to_str().unwrap(); + let log = tempfile::tempdir().unwrap(); + let log_path = log.path().to_str().unwrap(); + + let es = EvalState::new( + Store::open( + Some("local"), + HashMap::from([ + ("store", store_path), + ("state", state_path), + ("log", log_path), + ]) + .iter() + .map(|(a, b)| (*a, *b)), + ) + .unwrap(), + [], + ) + .unwrap(); + + let expr = r#" + '' + a derivation output: ${ + derivation { name = "letsbuild"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + }} + a path: ${builtins.toFile "just-a-file" "ooh file good"} + a derivation path by itself: ${ + builtins.unsafeDiscardOutputDependency + (derivation { + name = "not-actually-built-yet"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + }).drvPath} + '' + "#; + let derivations: [&[u8]; 3] = [ + b"letsbuild.drv", + b"just-a-file", + b"not-actually-built-yet.drv", + ]; + let _ = es.eval_from_string(expr, "").unwrap(); + + // assert that all three `derivations` are inside the store and the `state` directory is not empty either. + let store_contents: Vec<_> = read_dir(store.path()) + .unwrap() + .map(|dir_entry| dir_entry.unwrap().file_name()) + .collect(); + for derivation in derivations { + assert!(store_contents + .iter() + .any(|f| f.as_encoded_bytes().ends_with(derivation))); + } + assert!(!empty(read_dir(state.path()).unwrap())); + + store.close().unwrap(); + state.close().unwrap(); + log.close().unwrap(); + }) + .unwrap(); + } + + fn empty(foldable: impl IntoIterator) -> bool { + foldable.into_iter().all(|_| false) + } + + #[test] + fn eval_state_primop_anon_call() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let bias: Arc> = Arc::new(Mutex::new(0)); + let bias_control = bias.clone(); + + let primop = primop::PrimOp::new( + &mut es, + primop::PrimOpMeta { + name: cstr!("testFunction"), + args: [cstr!("a"), cstr!("b")], + doc: cstr!("anonymous test function"), + }, + Box::new(move |es, [a, b]| { + let a = es.require_int(a)?; + let b = es.require_int(b)?; + let c = *bias.lock().unwrap(); + Ok(es.new_value_int(a + b + c)?) + }), + ) + .unwrap(); + + let f = primop.new_value().unwrap(); + + { + *bias_control.lock().unwrap() = 10; + } + let a = es.new_value_int(2).unwrap(); + let b = es.new_value_int(3).unwrap(); + let fa = es.call(f, a).unwrap(); + let v = es.call(fa, b).unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v).unwrap(); + assert!(t == ValueType::Int); + let i = es.require_int(&v).unwrap(); + assert!(i == 15); + }) + .unwrap(); + } + + #[test] + fn eval_state_primop_anon_call_throw() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = { + let es: &mut EvalState = &mut es; + let prim = primop::PrimOp::new( + es, + primop::PrimOpMeta { + name: cstr!("throwingTestFunction"), + args: [cstr!("arg")], + doc: cstr!("anonymous test function"), + }, + Box::new(move |es, [a]| { + let a = es.require_int(a)?; + Err(EvalStateError::MissingAttribute(format!( + "error with arg [{a}]" + )))? + }), + ) + .unwrap(); + + prim.new_value() + } + .unwrap(); + let a = es.new_value_int(2).unwrap(); + let r = es.call(f, a); + match r { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("error with arg [2]") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + } + Err(e) => panic!("expected an EvalStateError::NixBindings, but got {e:?}"), + } + }) + .unwrap(); + } + + #[test] + fn eval_state_primop_anon_call_no_args() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es + .new_value_thunk( + "test_thunk", + Box::new(move |es: &mut EvalState| Ok(es.new_value_int(42)?)), + ) + .unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v).unwrap(); + eprintln!("{:?}", t); + assert!(t == ValueType::Int); + let i = es.require_int(&v).unwrap(); + assert!(i == 42); + }) + .unwrap(); + } + + #[test] + fn eval_state_primop_anon_call_no_args_lazy() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es + .new_value_thunk( + "test_thunk", + Box::new(move |_| { + Err(EvalStateError::MissingAttribute(String::from( + "error message in test case eval_state_primop_anon_call_no_args_lazy", + )))? + }), + ) + .unwrap(); + let r = es.force(&v); + match r { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains( + "error message in test case eval_state_primop_anon_call_no_args_lazy", + ) { + eprintln!("unexpected error message: {}", e); + panic!(); + } + if !e.to_string().contains("test_thunk") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + } + Err(e) => { + panic!("expected an EvalStateError::NixBindings, but got {e:?} instead") + } + } + }) + .unwrap(); + } + + #[test] + pub fn eval_state_primop_custom() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let primop = primop::PrimOp::new( + &mut es, + primop::PrimOpMeta { + name: cstr!("frobnicate"), + doc: cstr!("Frobnicates widgets"), + args: [cstr!("x"), cstr!("y")], + }, + Box::new(|es, args| { + let a = es.require_int(&args[0])?; + let b = es.require_int(&args[1])?; + Ok(es.new_value_int(a + b)?) + }), + ) + .unwrap(); + let f = primop.new_value().unwrap(); + let a = es.new_value_int(2).unwrap(); + let b = es.new_value_int(3).unwrap(); + let fa = es.call(f, a).unwrap(); + let fb = es.call(fa, b).unwrap(); + es.force(&fb).unwrap(); + let t = es.value_type(&fb).unwrap(); + assert!(t == ValueType::Int); + let i = es.require_int(&fb).unwrap(); + assert!(i == 5); + }) + .unwrap(); + } + + #[test] + pub fn eval_state_primop_custom_throw() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let primop = primop::PrimOp::new( + &mut es, + primop::PrimOpMeta { + name: cstr!("frobnicate"), + doc: cstr!("Frobnicates widgets"), + args: [cstr!("x")], + }, + Box::new(|_es, _args| { + Err(EvalStateError::MissingAttribute(String::from( + "The frob unexpectedly fizzled", + )))? + }), + ) + .unwrap(); + let f = primop.new_value().unwrap(); + let a = es.new_value_int(0).unwrap(); + match es.call(f, a) { + Ok(_) => panic!("expected an error"), + Err(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(e)))) => { + if !e.to_string().contains("The frob unexpectedly fizzled") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + if !e.to_string().contains("frobnicate") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + } + Err(e) => { + panic!("expected an EvalStateError::NixBindings, but got {e:?} instead") + } + } + }) + .unwrap(); + } + + #[test] + pub fn eval_state_new_value_attrs_from_slice_empty() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let attrs = es.new_value_attrs([]).unwrap(); + let t = es.value_type(&attrs).unwrap(); + assert!(t == ValueType::AttrSet); + let names = es.require_attrs_names(&attrs).unwrap(); + assert!(names.is_empty()); + }) + .unwrap(); + } + + #[test] + pub fn eval_state_new_value_attrs_from_vec() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let attrs = { + let a = es.new_value_int(1).unwrap(); + let b = es.new_value_int(2).unwrap(); + es.new_value_attrs(vec![("a".to_string(), a), ("b".to_string(), b)]) + .unwrap() + }; + let t = es.value_type(&attrs).unwrap(); + assert!(t == ValueType::AttrSet); + let names = es.require_attrs_names(&attrs).unwrap(); + assert_eq!(names.len(), 2); + assert_eq!(names[0], "a"); + assert_eq!(names[1], "b"); + let a = es.require_attrs_select(&attrs, "a").unwrap(); + let b = es.require_attrs_select(&attrs, "b").unwrap(); + let i = es.require_int(&a).unwrap(); + assert_eq!(i, 1); + let i = es.require_int(&b).unwrap(); + assert_eq!(i, 2); + }) + .unwrap(); + } + + #[test] + pub fn eval_state_new_value_attrs_from_hashmap() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let attrs = { + let a = es.new_value_int(1).unwrap(); + let b = es.new_value_int(2).unwrap(); + es.new_value_attrs(HashMap::from([("a".to_string(), a), ("b".to_string(), b)])) + .unwrap() + }; + let t = es.value_type(&attrs).unwrap(); + assert!(t == ValueType::AttrSet); + let names = es.require_attrs_names(&attrs).unwrap(); + assert_eq!(names.len(), 2); + assert_eq!(names[0], "a"); + assert_eq!(names[1], "b"); + let a = es.require_attrs_select(&attrs, "a").unwrap(); + let b = es.require_attrs_select(&attrs, "b").unwrap(); + let i = es.require_int(&a).unwrap(); + assert_eq!(i, 1); + let i = es.require_int(&b).unwrap(); + assert_eq!(i, 2); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_basic() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[ 10 20 30 ]", "").unwrap(); + + let elem0 = es.require_list_select_idx_strict(&v, 0).unwrap().unwrap(); + let elem1 = es.require_list_select_idx_strict(&v, 1).unwrap().unwrap(); + let elem2 = es.require_list_select_idx_strict(&v, 2).unwrap().unwrap(); + + assert_eq!(es.require_int(&elem0).unwrap(), 10); + assert_eq!(es.require_int(&elem1).unwrap(), 20); + assert_eq!(es.require_int(&elem2).unwrap(), 30); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_out_of_bounds() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[ 1 2 3 ]", "").unwrap(); + + let out_of_bounds = es.require_list_select_idx_strict(&v, 3).unwrap(); + assert!(out_of_bounds.is_none()); + + // Test boundary case - the last valid index + let last_elem = es.require_list_select_idx_strict(&v, 2).unwrap().unwrap(); + assert_eq!(es.require_int(&last_elem).unwrap(), 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_empty_list() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[ ]", "").unwrap(); + + // Test that the safe version properly handles empty list access + let elem = es.require_list_select_idx_strict(&v, 0).unwrap(); + assert!(elem.is_none()); + + // Verify we can get the size of an empty list + let size = es.require_list_size(&v).unwrap(); + assert_eq!(size, 0); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&es, "[ 42 ]"); + // Note: value_type_unforced now forces evaluation, so the value is always forced. + + let elem = es.require_list_select_idx_strict(&v, 0).unwrap().unwrap(); + assert_eq!(es.require_int(&elem).unwrap(), 42); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_error_element() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + + let v = es + .eval_from_string("[ (1 + 1) (throw \"error\") (3 + 3) ]", "") + .unwrap(); + + let elem0 = es.require_list_select_idx_strict(&v, 0).unwrap().unwrap(); + assert_eq!(es.require_int(&elem0).unwrap(), 2); + + let elem2 = es.require_list_select_idx_strict(&v, 2).unwrap().unwrap(); + assert_eq!(es.require_int(&elem2).unwrap(), 6); + + let elem1_result = es.require_list_select_idx_strict(&v, 1); + match elem1_result { + Ok(_) => panic!("expected an error from throw during selection"), + Err(e) => { + assert!(e.to_string().contains("error")); + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_wrong_type() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("42", "").unwrap(); + + let r = es.require_list_select_idx_strict(&v, 0); + assert!(matches!( + r, + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::List, + received: ValueType::Int + }) + )); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_size_basic() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + + let empty = es.eval_from_string("[ ]", "").unwrap(); + assert_eq!(es.require_list_size(&empty).unwrap(), 0); + + let three_elem = es.eval_from_string("[ 1 2 3 ]", "").unwrap(); + assert_eq!(es.require_list_size(&three_elem).unwrap(), 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_size_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&es, "[ 1 2 3 4 5 ]"); + // Note: value_type_unforced now forces evaluation, so the value is always forced. + + let size = es.require_list_size(&v).unwrap(); + assert_eq!(size, 5); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_size_lazy_elements() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + + let v = es + .eval_from_string( + "[ (throw \"error1\") (throw \"error2\") (throw \"error3\") ]", + "", + ) + .unwrap(); + + let size = es.require_list_size(&v).unwrap(); + assert_eq!(size, 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_size_wrong_type() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("\"not a list\"", "").unwrap(); + + let r = es.require_list_size(&v); + assert!(matches!( + r, + Err(EvalStateError::UnexpectedValueType { + expected: ValueType::List, + received: ValueType::String + }) + )) + }) + .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 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 + /// + /// 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() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let 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(EvalStateError::NixBindings(NixBindingsError::Nix(NixError::Nix(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." + ); + } + Err(e) => panic!( + "expected an error of type EvalStateError::NixBindings, but received {e:?}" + ), + } + }) + .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 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(); + } + + #[test] + #[cfg(nix_at_least = "2.34.0pre")] + #[ignore = "recoverable primop errors not retried"] + fn eval_state_primop_recoverable_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let call_count = std::cell::Cell::new(0u32); + let v = es + .new_value_thunk( + "recoverable_test", + Box::new(move |es: &mut EvalState| { + let count = call_count.get(); + call_count.set(count + 1); + if count == 0 { + Err(Box::new(NixBindingsError::RecoverableError(String::from( + "transient failure", + )))) + } else { + Ok(es.new_value_int(42)?) + } + }), + ) + .unwrap(); + + // First force should fail with the recoverable error + let r = es.force(&v); + assert!(r.is_err()); + assert!( + r.unwrap_err().to_string().contains("transient failure"), + "Error message should contain 'transient failure'" + ); + + // Second force should succeed because the error was recoverable + es.force(&v).unwrap(); + let i = es.require_int(&v).unwrap(); + assert_eq!(i, 42); + }) + .unwrap(); + } + + #[test] + #[cfg(nix_at_least = "2.34.0pre")] + #[ignore = "recoverable primop errors not retried"] + fn eval_state_primop_recoverable_error_in_chain() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let call_count = std::cell::Cell::new(0u32); + let v = es + .new_value_thunk( + "recoverable_chain_test", + Box::new(move |es: &mut EvalState| { + let count = call_count.get(); + call_count.set(count + 1); + if count == 0 { + // Wrap RecoverableError in .context(), pushing it down the chain + Err(Box::new(NixBindingsError::RecoverableError(String::from( + "transient failure", + )))) + } else { + Ok(es.new_value_int(42)?) + } + }), + ) + .unwrap(); + + // First force should fail + let r = es.force(&v); + assert!(r.is_err()); + + // Second force should succeed if RecoverableError is found in the chain + es.force(&v).unwrap(); + let i = es.require_int(&v).unwrap(); + assert_eq!(i, 42); + }) + .unwrap(); + } +} diff --git a/nix-bindings-expr/src/lib.rs b/nix-bindings-expr/src/lib.rs index 41c4b39b..e58fa73f 100644 --- a/nix-bindings-expr/src/lib.rs +++ b/nix-bindings-expr/src/lib.rs @@ -1,3 +1,17 @@ +#[cfg(not(feature = "detnix"))] pub mod eval_state; +#[cfg(feature = "detnix")] +#[doc(hidden)] +pub mod eval_state_detnix; +#[cfg(feature = "detnix")] +pub use eval_state_detnix as eval_state; + +#[cfg(not(feature = "detnix"))] pub mod primop; +#[cfg(feature = "detnix")] +#[doc(hidden)] +pub mod primop_detnix; +#[cfg(feature = "detnix")] +pub use primop_detnix as primop; + pub mod value; diff --git a/nix-bindings-expr/src/primop_detnix.rs b/nix-bindings-expr/src/primop_detnix.rs new file mode 100644 index 00000000..7b56f11b --- /dev/null +++ b/nix-bindings-expr/src/primop_detnix.rs @@ -0,0 +1,172 @@ +use crate::eval_state::{EvalState, EvalStateError}; +use crate::value::Value; +use nix_bindings_expr_sys as raw; +use nix_bindings_util::check_call; +use nix_bindings_util::context::Context; +use nix_bindings_util_sys as raw_util; +use std::error::Error; +use std::ffi::{c_int, c_void, CStr, CString}; +use std::ptr::{null, null_mut}; + +#[cfg(nix_at_least = "2.34.0pre")] +use nix_bindings_util::Error as NixError; + +/// Metadata for a primop, used with `PrimOp::new`. +pub struct PrimOpMeta<'a, const N: usize> { + /// Name of the primop. Note that primops do not have to be registered as + /// builtins. Nonetheless, a name is required for documentation purposes, e.g. + /// :doc in the repl. + pub name: &'a CStr, + + /// Documentation for the primop. This is displayed in the repl when using + /// :doc. The format is markdown. + pub doc: &'a CStr, + + /// The number of arguments the function takes, as well as names for the + /// arguments, to be presented in the documentation (if applicable, e.g. + /// :doc in the repl). + pub args: [&'a CStr; N], +} + +pub struct PrimOp<'a> { + ptr: *mut raw::PrimOp, + eval_state: &'a mut EvalState, +} + +impl Drop for PrimOp<'_> { + fn drop(&mut self) { + unsafe { + raw::gc_decref(null_mut(), self.ptr as *mut c_void); + } + } +} + +impl<'a> PrimOp<'a> { + /// Create a new primop with the given metadata and implementation. + /// + /// When `f` returns an `Err`, the error is propagated to the Nix evaluator. + /// To return a [recoverable error](RecoverableError), include it in the + /// error chain (e.g. `Err(RecoverableError::new("...").into())`). + pub fn new( + eval_state: &'a mut EvalState, + meta: PrimOpMeta, + f: Box Result>>, + ) -> Result, EvalStateError> { + assert!(N != 0); + + let mut args = Vec::new(); + for arg in meta.args { + args.push(arg.as_ptr()); + } + args.push(null()); + + // Primops weren't meant to be dynamically created, as of writing. + // This leaks, and so do the primop fields in Nix internally. + let user_data = { + // We'll be leaking this Box. + // TODO: Use the GC with finalizer, if possible. + let user_data = Box::leak(Box::new(PrimOpContext { + arity: N, + function: Box::new(move |eval_state, args| f(eval_state, args.try_into().unwrap())), + eval_state, + })); + user_data as *const PrimOpContext as *mut c_void + }; + let mut ctx = Context::new(); + let op = unsafe { + check_call!(raw::alloc_primop( + &mut ctx, + FUNCTION_ADAPTER, + N as c_int, + meta.name.as_ptr(), + args.as_mut_ptr(), /* TODO add an extra const to bindings to avoid mut here. */ + meta.doc.as_ptr(), + user_data + ))? + }; + + Ok(PrimOp { + ptr: op, + eval_state, + }) + } + + /// Creates a new [`function`](crate::value::ValueType::Function) Nix value implemented by a Rust function. + /// + /// This is also known as a "primop" in Nix, short for primitive operation. + /// Most of the `builtins.*` values are examples of primops, but this function + /// does not affect `builtins`. + #[doc(alias = "make_primop")] + #[doc(alias = "create_function")] + #[doc(alias = "builtin")] + pub fn new_value(mut self) -> Result { + self.with_state_and_ptr(|ptr, this| { + let value = this.new_value_uninitialized()?; + let mut ctx = Context::new(); + unsafe { + check_call!(raw::init_primop(&mut ctx, value.raw_ptr(), ptr))?; + }; + Ok(value) + }) + } + + pub(crate) fn with_state_and_ptr(&mut self, f: F) -> T + where + F: Fn(*mut raw::PrimOp, &mut EvalState) -> T, + { + f(self.ptr, self.eval_state) + } +} + +/// The user_data for our Nix primops +struct PrimOpContext<'a> { + arity: usize, + function: Box Result>>, + eval_state: &'a mut EvalState, +} + +unsafe extern "C" fn function_adapter( + user_data: *mut ::std::os::raw::c_void, + context_out: *mut raw_util::c_context, + _state: *mut raw::EvalState, + args: *mut *mut raw::Value, + ret: *mut raw::Value, +) { + let primop_info = (user_data as *mut PrimOpContext).as_mut().unwrap(); + let args_raw_slice = unsafe { std::slice::from_raw_parts(args, primop_info.arity) }; + let args_vec: Vec = args_raw_slice + .iter() + .map(|v| Value::new_borrowed(*v)) + .collect(); + let args_slice = args_vec.as_slice(); + + let r = primop_info.function.as_ref()(primop_info.eval_state, args_slice); + + match r { + Ok(v) => unsafe { + raw::copy_value(context_out, ret, v.raw_ptr()); + }, + Err(e) => unsafe { + let err_str = e.to_string(); + let err_code = error_code(e); + let cstr = CString::new(err_str).unwrap_or_else(|_e| { + CString::new("") + .unwrap() + }); + raw_util::set_err_msg(context_out, err_code, cstr.as_ptr()); + }, + } +} + +#[cfg_attr(not(nix_at_least = "2.34.0pre"), allow(unused))] +fn error_code(e: Box) -> raw_util::err { + #[cfg(nix_at_least = "2.34.0pre")] + if e.downcast_ref::() + .is_some_and(|e| matches!(e, NixError::RecoverableError(_))) + { + return raw_util::err_NIX_ERR_RECOVERABLE; + } + raw_util::err_NIX_ERR_UNKNOWN +} + +static FUNCTION_ADAPTER: raw::PrimOpFun = Some(function_adapter); diff --git a/nix-bindings-expr/src/value.rs b/nix-bindings-expr/src/value.rs index 68bb4442..8a296908 100644 --- a/nix-bindings-expr/src/value.rs +++ b/nix-bindings-expr/src/value.rs @@ -83,6 +83,13 @@ impl ValueType { pub struct Value { inner: NonNull, } + +#[cfg(feature = "detnix")] +unsafe impl Send for Value {} + +#[cfg(feature = "detnix")] +unsafe impl Sync for Value {} + impl Value { /// Take ownership of a new [`Value`]. /// diff --git a/nix-bindings-flake/Cargo.toml b/nix-bindings-flake/Cargo.toml index 2d7b96f0..5ea28af3 100644 --- a/nix-bindings-flake/Cargo.toml +++ b/nix-bindings-flake/Cargo.toml @@ -30,3 +30,7 @@ dead-code = "allow" type-complexity = "allow" # We're still trying to make Nix more thread-safe, want forward-compat arc-with-non-send-sync = "allow" + +[features] +default = [] +detnix = ["nix-bindings-expr/detnix", "nix-bindings-util/detnix", "nix-bindings-store/detnix"] diff --git a/nix-bindings-flake/src/lib.rs b/nix-bindings-flake/src/lib.rs index 75994822..64b61c01 100644 --- a/nix-bindings-flake/src/lib.rs +++ b/nix-bindings-flake/src/lib.rs @@ -13,6 +13,12 @@ use nix_bindings_util::{Error, Result}; pub struct FlakeSettings { pub(crate) ptr: *mut raw::flake_settings, } + +#[cfg(feature = "detnix")] +unsafe impl Send for FlakeSettings {} +#[cfg(feature = "detnix")] +unsafe impl Sync for FlakeSettings {} + impl Drop for FlakeSettings { fn drop(&mut self) { unsafe { @@ -206,6 +212,12 @@ impl FlakeLockFlags { pub struct LockedFlake { pub(crate) ptr: NonNull, } + +#[cfg(feature = "detnix")] +unsafe impl Send for LockedFlake {} +#[cfg(feature = "detnix")] +unsafe impl Sync for LockedFlake {} + impl Drop for LockedFlake { fn drop(&mut self) { unsafe { @@ -295,6 +307,16 @@ mod tests { init(); let gc_registration = gc_register_my_thread(); let store = Store::open(None, []).unwrap(); + + #[cfg(feature = "detnix")] + let eval_state = EvalStateBuilder::new(store) + .unwrap() + .flakes(&FlakeSettings::new().unwrap()) + .unwrap() + .build() + .unwrap(); + + #[cfg(not(feature = "detnix"))] let mut eval_state = EvalStateBuilder::new(store) .unwrap() .flakes(&FlakeSettings::new().unwrap()) diff --git a/nix-bindings-store/Cargo.toml b/nix-bindings-store/Cargo.toml index 31c507a2..dbde95e7 100644 --- a/nix-bindings-store/Cargo.toml +++ b/nix-bindings-store/Cargo.toml @@ -33,7 +33,9 @@ pkg-config = "0.3" nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" } [features] +default = [] harmonia = [ "dep:harmonia-store-core", "dep:serde_json" ] +detnix = ["nix-bindings-util/detnix"] [lints.rust] warnings = "deny" diff --git a/nix-bindings-store/src/path/mod.rs b/nix-bindings-store/src/path/mod.rs index a65cf928..938aefc3 100644 --- a/nix-bindings-store/src/path/mod.rs +++ b/nix-bindings-store/src/path/mod.rs @@ -20,6 +20,11 @@ pub struct StorePath { raw: NonNull, } +#[cfg(feature = "detnix")] +unsafe impl Send for StorePath {} +#[cfg(feature = "detnix")] +unsafe impl Sync for StorePath {} + impl StorePath { /// Get the name of the store path. /// diff --git a/nix-bindings-store/src/store.rs b/nix-bindings-store/src/store.rs index 755f2e9f..f7f26f63 100644 --- a/nix-bindings-store/src/store.rs +++ b/nix-bindings-store/src/store.rs @@ -58,6 +58,7 @@ impl StoreWeak { pub fn upgrade(&self) -> Option { self.inner.upgrade().map(|inner| Store { inner, + #[cfg(not(feature = "detnix"))] context: Context::new(), }) } @@ -145,6 +146,7 @@ fn callback_make_drv_outputs_data(vec: &mut HashMap) -> *mut std pub struct Store { inner: Arc, /* An error context to reuse. This way we don't have to allocate them for each store operation. */ + #[cfg(not(feature = "detnix"))] context: Context, } impl Store { @@ -236,6 +238,7 @@ impl Store { inner: Arc::new(StoreRef { inner: NonNull::new(store).unwrap(), }), + #[cfg(not(feature = "detnix"))] context, }; Ok(store) @@ -251,9 +254,19 @@ impl Store { #[doc(alias = "nix_store_get_uri")] pub fn get_uri(&mut self) -> Result { let mut r = Err(Error::StringInit); + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_get_uri( - &mut self.context, + ctx, self.inner.ptr(), Some(callback_get_result_string), callback_get_result_string_data(&mut r) @@ -266,9 +279,19 @@ impl Store { #[doc(alias = "nix_store_get_storedir")] pub fn get_storedir(&mut self) -> Result { let mut r = Err(Error::StringInit); + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_get_storedir( - &mut self.context, + ctx, self.inner.ptr(), Some(callback_get_result_string), callback_get_result_string_data(&mut r) @@ -280,9 +303,19 @@ impl Store { #[doc(alias = "nix_store_get_version")] pub fn get_version(&mut self) -> Result { let mut r = Err(Error::StringInit); + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_get_version( - &mut self.context, + ctx, self.inner.ptr(), Some(callback_get_result_string), callback_get_result_string_data(&mut r) @@ -294,12 +327,19 @@ impl Store { #[doc(alias = "nix_store_parse_path")] pub fn parse_store_path(&mut self, path: &str) -> Result { let path = CString::new(path)?; + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { - let store_path = check_call!(raw::store_parse_path( - &mut self.context, - self.inner.ptr(), - path.as_ptr() - ))?; + let store_path = + check_call!(raw::store_parse_path(ctx, self.inner.ptr(), path.as_ptr()))?; let store_path = NonNull::new(store_path).expect("nix_store_parse_path returned a null pointer"); Ok(StorePath::new_raw(store_path)) @@ -309,9 +349,19 @@ impl Store { #[doc(alias = "nix_store_real_path")] pub fn real_path(&mut self, path: &StorePath) -> Result { let mut r = Err(Error::StringInit); + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_real_path( - &mut self.context, + ctx, self.inner.ptr(), path.as_ptr(), Some(callback_get_result_string), @@ -339,9 +389,19 @@ impl Store { #[doc(alias = "nix_derivation_from_json")] pub fn derivation_from_json(&mut self, json: &str) -> Result { let json_cstr = CString::new(json)?; + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { let drv = check_call!(raw::derivation_from_json( - &mut self.context, + ctx, self.inner.ptr(), json_cstr.as_ptr() ))?; @@ -365,9 +425,18 @@ impl Store { #[cfg(nix_at_least = "2.31")] #[doc(alias = "nix_add_derivation")] pub fn add_derivation(&mut self, drv: &Derivation) -> Result { + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { let path = check_call!(raw::add_derivation( - &mut self.context, + ctx, self.inner.ptr(), drv.inner.as_ptr() ))?; @@ -380,9 +449,19 @@ impl Store { pub fn make_drv_outputs(&mut self, json: &str) -> Result> { let json = CString::new(json)?; let mut r = HashMap::new(); + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::derivation_make_outputs( - &mut self.context, + ctx, self.inner.ptr(), json.as_ptr(), Some(callback_make_drv_outputs), @@ -425,9 +504,18 @@ impl Store { .map(|p| unsafe { p.as_ptr() as *const raw::StorePath }) .collect(); + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_build_paths( - &mut self.context, + ctx, self.inner.ptr(), paths.as_mut_ptr(), paths.len() as std::os::raw::c_uint, @@ -479,9 +567,18 @@ impl Store { outputs.insert(name, path); } + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_realise( - &mut self.context, + ctx, self.inner.ptr(), path.as_ptr(), userdata, @@ -520,9 +617,19 @@ impl Store { include_derivers: bool, ) -> Result> { let mut r = Vec::new(); + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_get_fs_closure( - &mut self.context, + ctx, self.inner.ptr(), store_path.as_ptr(), flip_direction, @@ -538,9 +645,19 @@ impl Store { #[doc(alias = "nix_store_drv_from_path")] pub fn drv_from_path(&mut self, path: &StorePath) -> Result { let mut r = Err(Error::DerivationInit); + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_drv_from_path( - &mut self.context, + ctx, self.inner.ptr(), path.as_ptr(), Some(callback_get_result_derivation), @@ -553,9 +670,19 @@ impl Store { #[doc(alias = "nix_store_query_path_info")] pub fn query_path_info(&mut self, path: &StorePath) -> Result { let mut r = Err(Error::StringInit); + + #[cfg(feature = "detnix")] + let mut context = Context::new(); + + #[cfg(feature = "detnix")] + let ctx = &mut context; + + #[cfg(not(feature = "detnix"))] + let ctx = &mut self.context; + unsafe { check_call!(raw::store_query_path_info( - &mut self.context, + ctx, self.inner.ptr(), path.as_ptr(), callback_get_result_string_data(&mut r), @@ -576,6 +703,7 @@ impl Clone for Store { fn clone(&self) -> Self { Store { inner: self.inner.clone(), + #[cfg(not(feature = "detnix"))] context: Context::new(), } } diff --git a/nix-bindings-util/Cargo.toml b/nix-bindings-util/Cargo.toml index fee2037a..478e1e9f 100644 --- a/nix-bindings-util/Cargo.toml +++ b/nix-bindings-util/Cargo.toml @@ -27,3 +27,7 @@ dead-code = "allow" type-complexity = "allow" # We're still trying to make Nix more thread-safe, want forward-compat arc-with-non-send-sync = "allow" + +[features] +default = [] +detnix = [] diff --git a/nix-bindings-util/src/context.rs b/nix-bindings-util/src/context.rs index f4371e8f..f0f8e750 100644 --- a/nix-bindings-util/src/context.rs +++ b/nix-bindings-util/src/context.rs @@ -12,6 +12,9 @@ pub struct Context { inner: NonNull, } +#[cfg(feature = "detnix")] +unsafe impl Send for Context {} + impl Default for Context { fn default() -> Self { Self::new()