//! # 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}; //! # use nix_bindings_store::store::Store; //! # use std::collections::HashMap; //! # fn example() -> anyhow::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}; //! # fn example() -> anyhow::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}; //! use nix_bindings_store::store::Store; //! use std::collections::HashMap; //! //! # fn main() -> anyhow::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 anyhow::Context as _; use anyhow::{bail, Result}; use cstr::cstr; use lazy_static::lazy_static; use nix_bindings_store::path::StorePath; use nix_bindings_store::store::{Store, StoreWeak}; 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, result_string_init}; use nix_bindings_util_sys as raw; use std::ffi::{c_char, CString}; use std::iter::FromIterator; use std::os::raw::c_uint; use std::ptr::{null, null_mut, NonNull}; use std::sync::{Arc, Weak}; lazy_static! { static ref INIT: Result<()> = { unsafe { raw::GC_allow_register_threads(); check_call!(raw::libexpr_init(&mut Context::new()))?; Ok(()) } }; } pub fn init() -> Result<()> { let x = INIT.as_ref(); match x { Ok(_) => Ok(()), Err(e) => { // Couldn't just clone the error, so we have to print it here. Err(anyhow::format_err!("nix_bindings_expr::init error: {}", 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, } /// A [Weak] reference to an [EvalState]. pub struct EvalStateWeak { inner: Weak, store: StoreWeak, } impl EvalStateWeak { /// Upgrade the weak reference to a proper [EvalState]. /// /// If no normal reference to the [EvalState] is around anymore elsewhere, this fails by returning `None`. pub fn upgrade(&self) -> Option { self.inner.upgrade().and_then(|eval_state| { self.store.upgrade().map(|store| EvalState { eval_state, store, context: Context::new(), }) }) } } struct EvalStateRef { eval_state: NonNull, } 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}; /// # use nix_bindings_store::store::Store; /// # use std::collections::HashMap; /// # fn example() -> anyhow::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).with_context(|| { format!("EvalStateBuilder::lookup_path: path `{path}` contains null byte") }) }) .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: Arc::new(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(), context, }) } /// 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 } } pub struct EvalState { eval_state: Arc, store: Store, pub(crate) context: Context, } 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 } /// Creates a weak reference to this EvalState. pub fn weak_ref(&self) -> EvalStateWeak { EvalStateWeak { inner: Arc::downgrade(&self.eval_state), store: self.store.weak_ref(), } } /// 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}; /// use nix_bindings_store::store::Store; /// use nix_bindings_expr::value::Value; /// use std::collections::HashMap; /// /// # fn main() -> anyhow::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(&mut self, expr: &str, path: &str) -> Result { let expr_ptr = CString::new(expr).with_context(|| "eval_from_string: expr contains null byte")?; let path_ptr = CString::new(path).with_context(|| "eval_from_string: path contains null byte")?; unsafe { let value = self.new_value_uninitialized()?; check_call!(raw::expr_eval_from_string( &mut self.context, 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. #[doc(alias = "evaluate")] #[doc(alias = "strict")] pub fn force(&mut self, v: &Value) -> Result<()> { unsafe { check_call!(raw::value_force( &mut self.context, 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. #[doc(alias = "type_of")] #[doc(alias = "value_type_lazy")] #[doc(alias = "nix_get_type")] #[doc(alias = "get_type")] #[doc(alias = "nix_value_type")] 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). /// /// 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(&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. /// /// 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}; /// # use nix_bindings_store::store::Store; /// # use std::collections::HashMap; /// # fn example() -> anyhow::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(&mut self, v: &Value) -> Result { let t = self.value_type(v)?; if t != ValueType::Int { bail!("expected an int, but got a {:?}", t); } unsafe { check_call!(raw::get_int(&mut self.context, v.raw_ptr())) } } /// 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(&mut self, v: &Value) -> Result { let t = self.value_type(v)?; if t != ValueType::Bool { bail!("expected a bool, but got a {:?}", t); } unsafe { check_call!(raw::get_bool(&mut self.context, v.raw_ptr())) } } /// 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::value::Value; /// # use std::collections::{VecDeque, LinkedList}; /// # fn example(es: &mut nix_bindings_expr::eval_state::EvalState, list_value: &Value) -> anyhow::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(&mut self, value: &Value) -> Result where C: FromIterator, { let t = self.value_type(value)?; if t != ValueType::List { bail!("expected a list, but got a {:?}", t); } 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 self.context, 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(&mut 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(&mut self, v: &Value) -> Result> { let t = self.value_type(v)?; if t != ValueType::AttrSet { bail!("expected an attrset, but got a {:?}", t); } 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 self.context, 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() .map_err(|e| anyhow::format_err!("Nix attrset key is not valid UTF-8: {}", e))?; 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(&mut self, v: &Value, attr_name: &str) -> Result { let t = self.value_type(v)?; if t != ValueType::AttrSet { bail!("expected an attrset, but got a {:?}", t); } let attr_name = CString::new(attr_name) .with_context(|| "require_attrs_select: attrName contains null byte")?; unsafe { let v2 = check_call!(raw::get_attr_byname( &mut self.context, 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 e.to_string() == "missing attribute" { bail!("attribute `{}` not found", attr_name.to_string_lossy()); } else { 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( &mut self, v: &Value, attr_name: &str, ) -> Result> { let t = self.value_type(v)?; if t != ValueType::AttrSet { bail!("expected an attrset, but got a {:?}", t); } let attr_name = CString::new(attr_name) .with_context(|| "require_attrs_select_opt: attrName contains null byte")?; let v2 = unsafe { check_call_opt_key!(raw::get_attr_byname( &mut self.context, 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(&mut self, v: &Value) -> Result { let t = self.value_type(v)?; if t != ValueType::List { bail!("expected a list, but got a {:?}", t); } let ret = unsafe { check_call!(raw::get_list_size(&mut self.context, 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(&mut self, v: &Value, idx: u32) -> Result> { let t = self.value_type(v)?; if t != ValueType::List { bail!("expected a list, but got a {:?}", 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 size = unsafe { check_call!(raw::get_list_size(&mut self.context, v.raw_ptr())) }?; if idx >= size { return Ok(None); } let v2 = unsafe { check_call_opt_key!(raw::get_list_byidx( &mut self.context, 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(&mut self, s: &str) -> Result { let s = CString::new(s).with_context(|| "new_value_str: contains null byte")?; let v = unsafe { let value = self.new_value_uninitialized()?; check_call!(raw::init_string( &mut self.context, 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(&mut self, i: Int) -> Result { let v = unsafe { let value = self.new_value_uninitialized()?; check_call!(raw::init_int(&mut self.context, 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 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).with_context(|| "new_thunk: name contains null byte")?; 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 = self.new_value_primop(primop)?; 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(&mut self, value: &Value) -> Result { let mut r = result_string_init!(); unsafe { check_call!(raw::get_string( &mut self.context, value.raw_ptr(), Some(callback_get_result_string), callback_get_result_string_data(&mut r) ))?; }; 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(&mut self, value: &Value) -> Result { let t = self.value_type(value)?; if t != ValueType::String { bail!("expected a string, but got a {:?}", 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( &mut self, value: &Value, is_import_from_derivation: bool, ) -> Result { let t = self.value_type(value)?; if t != ValueType::String { bail!("expected a string, but got a {:?}", t); } let rs = unsafe { check_call!(raw::string_realise( &mut self.context, 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 u8; let size = raw::realised_string_get_buffer_size(rs); let slice = std::slice::from_raw_parts(start, size); String::from_utf8(slice.to_vec()) .map_err(|e| anyhow::format_err!("Nix string is not valid UTF-8: {}", e))? }; 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::StorePath).ok_or_else(|| { anyhow::format_err!( "nix_realised_string_get_store_path returned a null pointer" ) })?; 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(&mut self, f: Value, a: Value) -> Result { let value = self.new_value_uninitialized()?; unsafe { check_call!(raw::value_call( &mut self.context, 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}; /// # use nix_bindings_store::store::Store; /// # use std::collections::HashMap; /// # fn example() -> anyhow::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(&mut self, f: &Value, args: &[Value]) -> Result { let value = self.new_value_uninitialized()?; unsafe { let mut args_ptrs = args.iter().map(|a| a.raw_ptr()).collect::>(); check_call!(raw::value_call_multi( &mut self.context, 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(&mut self, f: &Value, a: &Value) -> Result { let value = self.new_value_uninitialized()?; unsafe { check_call!(raw::init_apply( &mut self.context, value.raw_ptr(), f.raw_ptr(), a.raw_ptr() )) }?; Ok(value) } fn new_value_uninitialized(&mut self) -> Result { unsafe { let value = check_call!(raw::alloc_value( &mut self.context, 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`. #[doc(alias = "make_primop")] #[doc(alias = "create_function")] #[doc(alias = "builtin")] pub fn new_value_primop(&mut self, primop: primop::PrimOp) -> Result { let value = self.new_value_uninitialized()?; unsafe { check_call!(raw::init_primop( &mut self.context, value.raw_ptr(), primop.ptr ))?; }; Ok(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}; /// # use nix_bindings_store::store::Store; /// # use std::collections::HashMap; /// # fn example() -> anyhow::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(&mut 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)?; for (name, value) in iter { let name = CString::new(name).with_context(|| "new_value_attrs: name contains null byte")?; unsafe { check_call!(raw::bindings_builder_insert( &mut self.context, bindings_builder.ptr, name.as_ptr(), value.raw_ptr() ))?; } } let value = self.new_value_uninitialized()?; unsafe { check_call!(raw::make_attrs( &mut self.context, 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: &mut EvalState, capacity: usize) -> Result { let ptr = unsafe { check_call!(raw::make_bindings_builder( &mut eval_state.context, 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 { raw::GC_unregister_my_thread(); } } } } fn gc_register_my_thread_do_it() -> Result<()> { unsafe { let mut sb: raw::GC_stack_base = raw::GC_stack_base { mem_base: null_mut(), }; let r = raw::GC_get_stack_base(&mut sb); if r as u32 != raw::GC_SUCCESS { Err(anyhow::format_err!("GC_get_stack_base failed: {}", r))?; } raw::GC_register_my_thread(&sb); Ok(()) } } #[doc(alias = "register_thread")] #[doc(alias = "thread_setup")] #[doc(alias = "gc_register")] pub fn gc_register_my_thread() -> Result { init()?; unsafe { let already_done = raw::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, }) } } impl Clone for EvalState { fn clone(&self) -> Self { EvalState { eval_state: self.eval_state.clone(), store: self.store.clone(), context: Context::new(), } } } /// 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 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 weak_ref() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let es = EvalState::new(store, []).unwrap(); let weak = es.weak_ref(); let _es = weak.upgrade().unwrap(); }) .unwrap(); } #[test] fn weak_ref_gone() { gc_registering_current_thread(|| { let weak = { // Use a slightly different URL which is unique in the test suite, to bypass the global store cache let store = Store::open(Some("auto?foo=bar"), HashMap::new()).unwrap(); let es = EvalState::new(store, []).unwrap(); es.weak_ref() }; assert!(weak.upgrade().is_none()); assert!(weak.store.upgrade().is_none()); assert!(weak.inner.upgrade().is_none()); }) .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 mut es = EvalState::new(Store::open(None, HashMap::new()).unwrap(), []).unwrap(); assert!(es.eval_from_string(import_expression, "").is_err()); let mut 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 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_unforced(&v); assert!(t == Some(ValueType::Int)); let t2 = es.value_type_unforced(&v2); assert!(t2 == Some(ValueType::Int)); gc_now(); }) .unwrap(); } #[test] fn eval_state_value_bool() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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_unforced(&v); assert!(t == Some(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 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(); 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 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(); let t = es.value_type_unforced(&v); assert!(t.is_none()); 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 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(); let t = es.value_type_unforced(&v); assert!(t.is_none()); let i = es.require_bool(&v).unwrap(); assert!(!i); }) .unwrap(); } /// A helper that turns an expression into a thunk. 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 // 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 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(); } #[test] fn eval_state_value_attrs_names_empty() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("{ }", "").unwrap(); es.force(&v).unwrap(); 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); }) .unwrap() } #[test] fn eval_state_require_attrs_names_unsorted_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); 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); }) .unwrap() } #[test] fn eval_state_require_attrs_names_unsorted_bad_type() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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); assert!(r.is_err()); assert_eq!( r.unwrap_err().to_string(), "expected an attrset, but got a Int" ); }) .unwrap() } #[test] fn eval_state_value_attrs_names_example() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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(); 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 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(); 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"); match missing { Ok(_) => panic!("expected an error"), Err(e) => { let s = format!("{e:#}"); if !s.contains("attribute `c` not found") { eprintln!("unexpected error message: {}", s); panic!(); } } } }) .unwrap() } #[test] fn eval_state_require_attrs_select_forces_thunk() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = "aye"; b = "bee"; }"#; 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()); }) .unwrap() } #[test] fn eval_state_require_attrs_select_error() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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"); match r { Ok(_) => panic!("expected an error"), Err(e) => { if !e.to_string().contains("oh no the error") { eprintln!("unexpected error message: {}", e); panic!(); } } } }) .unwrap() } #[test] fn eval_state_require_attrs_select_opt() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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(); 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 mut es = EvalState::new(store, []).unwrap(); let expr = r#"{ a = "aye"; b = "bee"; }"#; 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()); }) .unwrap() } #[test] fn eval_state_require_attrs_select_opt_error() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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"); match r { Ok(_) => panic!("expected an error"), Err(e) => { if !e.to_string().contains("oh no the error") { eprintln!("unexpected error message: {}", e); panic!(); } } } }) .unwrap() } #[test] fn eval_state_value_string() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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_unforced(&v); assert!(t == Some(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 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"); }) .unwrap(); } #[test] fn eval_state_value_string_unexpected_bool() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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); assert!(r.is_err()); // TODO: safe print value (like Nix would) assert_eq!( r.unwrap_err().to_string(), "expected a string, but got a Bool" ); }) .unwrap() } #[test] fn eval_state_value_string_unexpected_path_value() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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); assert!(r.is_err()); assert_eq!( r.unwrap_err().to_string(), "expected a string, but got a Path" ); }) .unwrap() } #[test] fn eval_state_value_string_bad_utf() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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_unforced(&v); assert!(t == Some(ValueType::String)); let r = es.require_string(&v); assert!(r.is_err()); assert!(r .unwrap_err() .to_string() .contains("Nix string is not valid UTF-8")); }) .unwrap(); } #[test] fn eval_state_value_string_unexpected_context() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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_unforced(&v); assert!(t == Some(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 mut es = EvalState::new(store, []).unwrap(); let v = es.new_value_str("hello").unwrap(); es.force(&v).unwrap(); let t = es.value_type_unforced(&v); assert!(t == Some(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 mut es = EvalState::new(store, []).unwrap(); let v = es.new_value_str("").unwrap(); es.force(&v).unwrap(); let t = es.value_type_unforced(&v); assert!(t == Some(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 mut 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 mut es = EvalState::new(store, []).unwrap(); let v = es.new_value_int(42).unwrap(); es.force(&v).unwrap(); let t = es.value_type_unforced(&v); assert!(t == Some(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 mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("{ }", "").unwrap(); es.force(&v).unwrap(); let t = es.value_type_unforced(&v); assert!(t == Some(ValueType::AttrSet)); }) .unwrap(); } #[test] fn eval_state_value_list() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("[ ]", "").unwrap(); es.force(&v).unwrap(); let t = es.value_type_unforced(&v); assert!(t == Some(ValueType::List)); }) .unwrap(); } #[test] fn eval_state_value_list_strict_empty() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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(); 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 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(); 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 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(); 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 mut 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(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.") } }) .unwrap(); } #[test] fn eval_state_value_list_strict_generic_container() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let mut 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(); } #[test] fn eval_state_realise_string() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let mut 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 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_unforced(&v); assert!(t == Some(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 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_unforced(&v); assert!(t == Some(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 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(); assert!(es.value_type_unforced(&v).is_none()); es.force(&v).unwrap(); let t = es.value_type_unforced(&v); assert!(t == Some(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 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); match r { Ok(_) => panic!("expected an error"), Err(e) => { if !e.to_string().contains("cannot coerce") { eprintln!("{}", e); panic!(); } } } }) .unwrap(); } #[test] fn eval_state_call_multi_fail_body() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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("true", "").unwrap(); let r = es.call_multi(&f, &[a, b]); match r { Ok(_) => panic!("expected an error"), Err(e) => { if !e.to_string().contains("expected an integer but found") { eprintln!("{}", e); panic!(); } } } }) .unwrap(); } #[test] fn eval_state_apply_fail_body() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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 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(e) => { if !e.to_string().contains("cannot coerce") { eprintln!("{}", e); panic!(); } } } }) .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 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); match r { Ok(_) => panic!("expected an error"), Err(e) => { if !e.to_string().contains("called without required argument") { eprintln!("{}", e); panic!(); } } } }) .unwrap(); } #[test] fn eval_state_call_multi_fail_args() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).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(); let b = es.eval_from_string("{}", "").unwrap(); let r = es.call_multi(&f, &[a, b]); match r { Ok(_) => panic!("expected an error"), Err(e) => { if !e.to_string().contains("called without required argument") { eprintln!("{}", e); panic!(); } } } }) .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 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 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(e) => { if !e.to_string().contains("called without required argument") { eprintln!("{}", e); panic!(); } } } }) .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 mut 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(); es.new_value_int(a + b + c) }), ) .unwrap(); let f = es.new_value_primop(primop).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)?; bail!("error with arg [{}]", a); }), ) .unwrap(); es.new_value_primop(prim) } .unwrap(); let a = es.new_value_int(2).unwrap(); let r = es.call(f, a); match r { Ok(_) => panic!("expected an error"), Err(e) => { if !e.to_string().contains("error with arg [2]") { eprintln!("unexpected error message: {}", e); panic!(); } } } }) .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| 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 |_| { bail!("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(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!(); } } } }) .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])?; es.new_value_int(a + b) }), ) .unwrap(); let f = es.new_value_primop(primop).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| bail!("The frob unexpectedly fizzled")), ) .unwrap(); let f = es.new_value_primop(primop).unwrap(); let a = es.new_value_int(0).unwrap(); match es.call(f, a) { Ok(_) => panic!("expected an error"), Err(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!(); } } } }) .unwrap(); } #[test] pub fn eval_state_new_value_attrs_from_slice_empty() { gc_registering_current_thread(|| { let store = Store::open(None, []).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); 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 mut 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 mut 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 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(); 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 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(); 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 mut 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 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); }) .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 mut 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 mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("42", "").unwrap(); let r = es.require_list_select_idx_strict(&v, 0); match r { Ok(_) => panic!("expected an error"), Err(e) => { let err_msg = e.to_string(); assert!(err_msg.contains("expected a list, but got a")); } } }) .unwrap(); } #[test] fn eval_state_require_list_size_basic() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let mut 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 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); }) .unwrap(); } #[test] fn eval_state_require_list_size_lazy_elements() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let mut 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 mut es = EvalState::new(store, []).unwrap(); let v = es.eval_from_string("\"not a list\"", "").unwrap(); let r = es.require_list_size(&v); match r { Ok(_) => panic!("expected an error"), Err(e) => { let err_msg = e.to_string(); assert!(err_msg.contains("expected a list, but got a")); } } }) .unwrap(); } /// Test for path coercion fix (commit 8f6ec2e, ). /// /// This test verifies that path coercion works correctly with EvalStateBuilder. /// Path coercion requires readOnlyMode = false, which is loaded from global /// settings by calling eval_state_builder_load(). /// /// # Background /// /// Without the eval_state_builder_load() call, settings from global Nix /// configuration are never loaded, leaving readOnlyMode = true (the default). /// This prevents Nix from adding paths to the store during evaluation, /// which could cause errors like: "error: path '/some/local/path' does not exist" /// /// # Test Coverage /// /// This test exercises store file creation: /// 1. builtins.toFile successfully creates files in the store /// 2. Files are actually written to /nix/store /// 3. Content is written correctly /// /// Note: This test may not reliably fail without the fix in all environments. /// Use eval_state_builder_loads_max_call_depth for a deterministic test. #[test] #[cfg(nix_at_least = "2.26" /* real_path, eval_state_builder_load */)] fn eval_state_builder_path_coercion() { gc_registering_current_thread(|| { let mut store = Store::open(None, HashMap::new()).unwrap(); let mut es = EvalStateBuilder::new(store.clone()) .unwrap() .build() .unwrap(); // Use builtins.toFile to create a file in the store. // This operation requires readOnlyMode = false to succeed. let expr = r#"builtins.toFile "test-file.txt" "test content""#; // Evaluate the expression let value = es.eval_from_string(expr, "").unwrap(); // Realise the string to get the path and associated store paths let realised = es.realise_string(&value, false).unwrap(); // Verify we got exactly one store path assert_eq!( realised.paths.len(), 1, "Expected 1 store path, got {}", realised.paths.len() ); // Get the physical filesystem path for the store path // In a relocated store, this differs from realised.s let physical_path = store.real_path(&realised.paths[0]).unwrap(); // Verify the store path actually exists on disk assert!( std::path::Path::new(&physical_path).exists(), "Store path should exist: {}", physical_path ); // Verify the content was written correctly let store_content = std::fs::read_to_string(&physical_path).unwrap(); assert_eq!(store_content, "test content"); }) .unwrap(); } /// Test that eval_state_builder_load() loads settings. /// /// Uses max-call-depth as the test setting. The test suite sets /// max-call-depth = 1000 via NIX_CONFIG in setup() for the purpose of this test case. /// This test creates a recursive function that calls itself 1100 times. /// /// - WITH the fix: Settings are loaded, max-call-depth=1000 is enforced, /// recursion fails at depth 1000 /// - WITHOUT the fix: Settings aren't loaded, default max-call-depth=10000 /// is used, recursion of 1100 succeeds when it should fail /// /// 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 mut es = EvalStateBuilder::new(store).unwrap().build().unwrap(); // Create a recursive function that calls itself 1100 times // This should fail because max-call-depth is 1000 (set in setup()) let expr = r#" let recurse = n: if n == 0 then "done" else recurse (n - 1); in recurse 1100 "#; let result = es.eval_from_string(expr, ""); match result { Err(e) => { let err_str = e.to_string(); assert!( err_str.contains("max-call-depth"), "Expected max-call-depth error, got: {}", err_str ); } Ok(_) => { panic!( "Expected recursion to fail with max-call-depth=1000, but it succeeded. \ This indicates eval_state_builder_load() was not called." ); } } }) .unwrap(); } /// Test that load_ambient_settings(false) ignores the ambient environment. /// /// The test suite sets max-call-depth = 1000 via NIX_CONFIG in setup(). /// When we disable loading ambient settings, this should be ignored and /// the default max-call-depth = 10000 should be used instead. /// /// Complementary to eval_state_builder_loads_max_call_depth which verifies /// that ambient settings ARE loaded when enabled. #[test] #[cfg(nix_at_least = "2.26")] fn eval_state_builder_ignores_ambient_when_disabled() { gc_registering_current_thread(|| { let store = Store::open(None, HashMap::new()).unwrap(); let mut es = EvalStateBuilder::new(store) .unwrap() .load_ambient_settings(false) .build() .unwrap(); // Create a recursive function that calls itself 1100 times // With ambient settings disabled, default max-call-depth=10000 is used, // so this should succeed (unlike eval_state_builder_loads_max_call_depth) let expr = r#" let recurse = n: if n == 0 then "done" else recurse (n - 1); in recurse 1100 "#; let result = es.eval_from_string(expr, ""); match result { Ok(value) => { // Success expected - ambient NIX_CONFIG was ignored let result_str = es.require_string(&value).unwrap(); assert_eq!(result_str, "done"); } Err(e) => { panic!( "Expected recursion to succeed with default max-call-depth=10000, \ but it failed: {}. This indicates ambient settings were not ignored.", e ); } } }) .unwrap(); } }