add expr and store support

This commit is contained in:
do butterflies cry? 2026-03-18 16:57:55 +10:00
parent ae6251c3a1
commit 14859de6a6
Signed by: cry
GPG key ID: F68745A836CA0412
16 changed files with 1557 additions and 13 deletions

1
Cargo.lock generated
View file

@ -191,6 +191,7 @@ name = "nixide"
version = "0.1.0"
dependencies = [
"nixide-sys",
"serial_test",
]
[[package]]

View file

@ -7,7 +7,7 @@ license = "GPL-3.0"
repository = "https://codeberg.org/luminary/nixide"
authors = [
"_cry64 <them@dobutterfliescry.net>",
"foxxyora <foxxyora@noreply.codeberg.org>"
"foxxyora <foxxyora@noreply.codeberg.org>",
]
edition = "2024"
@ -25,3 +25,6 @@ gc = []
[dependencies]
nixide-sys = { path = "../nixide-sys", version = "0.1.0" }
[dev-dependencies]
serial_test = "3.4.0"

71
nixide/src/context.rs Normal file
View file

@ -0,0 +1,71 @@
use std::ptr::NonNull;
use crate::error::NixError;
use nixide_sys as sys;
/// Nix context for managing library state.
///
/// This is the root object for all Nix operations. It manages the lifetime
/// of the Nix C API context and provides automatic cleanup.
pub struct Context {
inner: NonNull<sys::nix_c_context>,
}
impl Context {
/// Create a new Nix context.
///
/// This initializes the Nix C API context and the required libraries.
///
/// # Errors
///
/// Returns an error if context creation or library initialization fails.
pub fn new() -> Result<Self, NixError> {
// SAFETY: nix_c_context_create is safe to call
let ctx_ptr = unsafe { sys::nix_c_context_create() };
let ctx = Context {
inner: NonNull::new(ctx_ptr).ok_or(NixError::NullPtr {
location: "nix_c_context_create",
})?,
};
// Initialize required libraries
unsafe {
NixError::from(
sys::nix_libutil_init(ctx.inner.as_ptr()),
"nix_libutil_init",
)?;
NixError::from(
sys::nix_libstore_init(ctx.inner.as_ptr()),
"nix_libstore_init",
)?;
NixError::from(
sys::nix_libexpr_init(ctx.inner.as_ptr()),
"nix_libexpr_init",
)?;
};
Ok(ctx)
}
/// Get the raw context pointer.
///
/// # Safety
///
/// The caller must ensure the pointer is used safely.
pub unsafe fn as_ptr(&self) -> *mut sys::nix_c_context {
self.inner.as_ptr()
}
}
impl Drop for Context {
fn drop(&mut self) {
// SAFETY: We own the context and it's valid until drop
unsafe {
sys::nix_c_context_free(self.inner.as_ptr());
}
}
}
// SAFETY: Context can be shared between threads
unsafe impl Send for Context {}
unsafe impl Sync for Context {}

183
nixide/src/error.rs Normal file
View file

@ -0,0 +1,183 @@
use std::ffi::NulError;
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::ptr::NonNull;
use crate::sys;
/// Standard (nix_err) and some additional error codes
/// produced by the libnix C API.
#[derive(Debug)]
pub enum NixError {
/// A generic Nix error occurred.
///
/// # Reason
///
/// This error code is returned when a generic Nix error occurred during the
/// function execution.
NixError { location: &'static str },
/// An overflow error occurred.
///
/// # Reason
///
/// This error code is returned when an overflow error occurred during the
/// function execution.
Overflow { location: &'static str },
/// A key/index access error occurred in C API functions.
///
/// # Reason
///
/// This error code is returned when accessing a key, index, or identifier that
/// does not exist in C API functions. Common scenarios include:
/// - Setting keys that don't exist (nix_setting_get, nix_setting_set)
/// - List indices that are out of bounds (nix_get_list_byidx*)
/// - Attribute names that don't exist (nix_get_attr_byname*)
/// - Attribute indices that are out of bounds (nix_get_attr_byidx*, nix_get_attr_name_byidx)
///
/// This error typically indicates incorrect usage or assumptions about data structure
/// contents, rather than internal Nix evaluation errors.
///
/// # Note
///
/// This error code should ONLY be returned by C API functions themselves,
/// not by underlying Nix evaluation. For example, evaluating `{}.foo` in Nix
/// will throw a normal error (NIX_ERR_NIX_ERROR), not NIX_ERR_KEY.
KeyNotFound {
location: &'static str,
key: Option<String>,
},
/// An unknown error occurred.
///
/// # Reason
///
/// This error code is returned when an unknown error occurred during the
/// function execution.
Unknown {
location: &'static str,
reason: String,
},
/// An undocumented error occurred.
///
/// # Reason
///
/// The libnix C API defines `enum nix_err` as a signed integer value.
/// In the (unexpected) event libnix returns an error code with an
/// invalid enum value, or one I new addition I didn't know existed,
/// then an [NixError::Undocumented] is considered to have occurred.
Undocumented {
location: &'static str,
err_code: sys::nix_err,
},
//////////////////////
// NON-STANDARD ERRORS
//////////////////////
/// NulError
NulError { location: &'static str },
/// Non-standard
NullPtr { location: &'static str },
/// Invalid Argument
InvalidArg {
location: &'static str,
reason: &'static str, // XXX: TODO: make this a String
},
/// Invalid Type
InvalidType {
location: &'static str,
expected: &'static str,
got: String,
},
}
impl NixError {
pub fn from(err_code: sys::nix_err, location: &'static str) -> Result<(), NixError> {
#[allow(nonstandard_style)]
match err_code {
sys::nix_err_NIX_OK => Ok(()),
sys::nix_err_NIX_ERR_OVERFLOW => Err(NixError::Overflow { location }),
sys::nix_err_NIX_ERR_KEY => Err(NixError::KeyNotFound {
location,
key: None,
}),
sys::nix_err_NIX_ERR_NIX_ERROR => Err(NixError::NixError { location }),
sys::nix_err_NIX_ERR_UNKNOWN => Err(NixError::Unknown {
location,
reason: "Unknown error occurred".to_string(),
}),
_ => Err(NixError::Undocumented { location, err_code }),
}
}
pub fn from_nulerror<T>(
result: Result<T, NulError>,
location: &'static str,
) -> Result<T, Self> {
result.or(Err(NixError::NulError { location }))
}
pub fn new_nonnull<T>(ptr: *mut T, location: &'static str) -> Result<NonNull<T>, Self>
where
T: Sized,
{
NonNull::new(ptr).ok_or(NixError::NullPtr { location })
}
}
impl std::error::Error for NixError {}
impl Display for NixError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let msg = match self {
NixError::NixError { location } => {
format!("[libnix] Generic error (at location `{location}`)")
}
NixError::Overflow { location } => {
format!("[libnix] Overflow error (at location `{location}`)")
}
NixError::KeyNotFound { location, key } => format!(
"[libnix] Key not found {} (at location `{location}`)",
match key {
Some(key) => format!("`{key}`"),
None => "".to_owned(),
}
),
NixError::Unknown { location, reason } => {
format!("Unknown error \"{reason}\" (at location `{location}`)")
}
NixError::Undocumented { location, err_code } => {
format!(
"[libnix] An undocumented nix_err was returned with {err_code} (at location `{location}`)"
)
}
NixError::NulError { location } => {
format!("Nul error (at location `{location}`)")
}
NixError::NullPtr { location } => {
format!("[libnix] Null pointer (at location `{location}`)")
}
NixError::InvalidArg { location, reason } => {
format!("Invalid argument \"{reason}\" (at location `{location}`)")
}
NixError::InvalidType {
location,
expected,
got,
} => {
format!("Invalid type, expected \"{expected}\" ${got} (at location `{location}`)")
}
};
write!(f, "{msg}")
}
}

View file

@ -0,0 +1,117 @@
use std::ffi::CString;
use std::ptr::NonNull;
use std::sync::Arc;
use super::Value;
use crate::sys;
use crate::{Context, NixError, Store};
/// Nix evaluation state for evaluating expressions.
///
/// This provides the main interface for evaluating Nix expressions
/// and creating values.
pub struct EvalState {
inner: NonNull<sys::EvalState>,
#[allow(dead_code)]
store: Arc<Store>,
pub(super) context: Arc<Context>,
}
impl EvalState {
/// Construct a new EvalState directly from its attributes
pub(super) fn new(
inner: NonNull<sys::EvalState>,
store: Arc<Store>,
context: Arc<Context>,
) -> Self {
Self {
inner,
store,
context,
}
}
/// Evaluate a Nix expression from a string.
///
/// # Arguments
///
/// * `expr` - The Nix expression to evaluate
/// * `path` - The path to use for error reporting (e.g., "<eval>")
///
/// # Errors
///
/// Returns an error if evaluation fails.
pub fn eval_from_string(&self, expr: &str, path: &str) -> Result<Value<'_>, NixError> {
let expr_c =
NixError::from_nulerror(CString::new(expr), "nixide::EvalState::eval_from_string")?;
let path_c =
NixError::from_nulerror(CString::new(path), "nixide::EvalState::eval_from_string")?;
// Allocate value for result
// SAFETY: context and state are valid
let value_ptr = unsafe { sys::nix_alloc_value(self.context.as_ptr(), self.inner.as_ptr()) };
if value_ptr.is_null() {
return Err(NixError::NullPtr {
location: "nix_alloc_value",
});
}
// Evaluate expression
// SAFETY: all pointers are valid
NixError::from(
unsafe {
sys::nix_expr_eval_from_string(
self.context.as_ptr(),
self.inner.as_ptr(),
expr_c.as_ptr(),
path_c.as_ptr(),
value_ptr,
)
},
"nix_expr_eval_from_string",
)?;
let inner = NonNull::new(value_ptr).ok_or(NixError::NullPtr {
location: "nix_expr_eval_from_string",
})?;
Ok(Value { inner, state: self })
}
/// Allocate a new value.
///
/// # Errors
///
/// Returns an error if value allocation fails.
pub fn alloc_value(&self) -> Result<Value<'_>, NixError> {
// SAFETY: context and state are valid
let value_ptr = unsafe { sys::nix_alloc_value(self.context.as_ptr(), self.inner.as_ptr()) };
let inner = NonNull::new(value_ptr).ok_or(NixError::NullPtr {
location: "nix_alloc_value",
})?;
Ok(Value { inner, state: self })
}
/// Get the raw state pointer.
///
/// # Safety
///
/// The caller must ensure the pointer is used safely.
pub(super) unsafe fn as_ptr(&self) -> *mut sys::EvalState {
self.inner.as_ptr()
}
}
impl Drop for EvalState {
fn drop(&mut self) {
// SAFETY: We own the state and it's valid until drop
unsafe {
sys::nix_state_free(self.inner.as_ptr());
}
}
}
// SAFETY: EvalState can be shared between threads
unsafe impl Send for EvalState {}
unsafe impl Sync for EvalState {}

View file

@ -0,0 +1,82 @@
use std::ptr::NonNull;
use std::sync::Arc;
use super::EvalState;
use crate::sys;
use crate::{Context, NixError, Store};
/// Builder for Nix evaluation state.
///
/// This allows configuring the evaluation environment before creating
/// the evaluation state.
pub struct EvalStateBuilder {
inner: NonNull<sys::nix_eval_state_builder>,
store: Arc<Store>,
context: Arc<Context>,
}
impl EvalStateBuilder {
/// Create a new evaluation state builder.
///
/// # Arguments
///
/// * `store` - The Nix store to use for evaluation
///
/// # Errors
///
/// Returns an error if the builder cannot be created.
pub fn new(store: &Arc<Store>) -> Result<Self, NixError> {
// SAFETY: store context and store are valid
let builder_ptr =
unsafe { sys::nix_eval_state_builder_new(store._context.as_ptr(), store.as_ptr()) };
let inner = NonNull::new(builder_ptr).ok_or(NixError::NullPtr {
location: "nix_eval_state_builder_new",
})?;
Ok(EvalStateBuilder {
inner,
store: Arc::clone(store),
context: Arc::clone(&store._context),
})
}
/// Build the evaluation state.
///
/// # Errors
///
/// Returns an error if the evaluation state cannot be built.
pub fn build(self) -> Result<EvalState, NixError> {
// Load configuration first
// SAFETY: context and builder are valid
NixError::from(
unsafe { sys::nix_eval_state_builder_load(self.context.as_ptr(), self.inner.as_ptr()) },
"nix_eval_state_builder_load",
)?;
// Build the state
// SAFETY: context and builder are valid
let state_ptr =
unsafe { sys::nix_eval_state_build(self.context.as_ptr(), self.inner.as_ptr()) };
let inner = NonNull::new(state_ptr).ok_or(NixError::NullPtr {
location: "nix_eval_state_build",
})?;
// The builder is consumed here - its Drop will clean up
Ok(EvalState::new(
inner,
self.store.clone(),
self.context.clone(),
))
}
}
impl Drop for EvalStateBuilder {
fn drop(&mut self) {
// SAFETY: We own the builder and it's valid until drop
unsafe {
sys::nix_eval_state_builder_free(self.inner.as_ptr());
}
}
}

12
nixide/src/expr/mod.rs Normal file
View file

@ -0,0 +1,12 @@
#[cfg(test)]
mod tests;
mod evalstate;
mod evalstatebuilder;
mod value;
mod valuetype;
pub use evalstate::EvalState;
pub use evalstatebuilder::EvalStateBuilder;
pub use value::Value;
pub use valuetype::ValueType;

153
nixide/src/expr/tests.rs Normal file
View file

@ -0,0 +1,153 @@
use std::sync::Arc;
use serial_test::serial;
use super::{EvalStateBuilder, ValueType};
use crate::{Context, Store};
#[test]
#[serial]
fn test_context_creation() {
let _ctx = Context::new().expect("Failed to create context");
// Context should be dropped automatically
}
#[test]
#[serial]
fn test_eval_state_builder() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let _state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
// State should be dropped automatically
}
#[test]
#[serial]
fn test_simple_evaluation() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let result = state
.eval_from_string("1 + 2", "<eval>")
.expect("Failed to evaluate expression");
assert_eq!(result.value_type(), ValueType::Int);
assert_eq!(result.as_int().expect("Failed to get int value"), 3);
}
#[test]
#[serial]
fn test_value_types() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
// Test integer
let int_val = state
.eval_from_string("42", "<eval>")
.expect("Failed to evaluate int");
assert_eq!(int_val.value_type(), ValueType::Int);
assert_eq!(int_val.as_int().expect("Failed to get int"), 42);
// Test boolean
let bool_val = state
.eval_from_string("true", "<eval>")
.expect("Failed to evaluate bool");
assert_eq!(bool_val.value_type(), ValueType::Bool);
assert!(bool_val.as_bool().expect("Failed to get bool"));
// Test string
let str_val = state
.eval_from_string("\"hello\"", "<eval>")
.expect("Failed to evaluate string");
assert_eq!(str_val.value_type(), ValueType::String);
assert_eq!(str_val.as_string().expect("Failed to get string"), "hello");
}
#[test]
#[serial]
fn test_value_formatting() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
// Test integer formatting
let int_val = state
.eval_from_string("42", "<eval>")
.expect("Failed to evaluate int");
assert_eq!(format!("{int_val}"), "42");
assert_eq!(format!("{int_val:?}"), "Value::Int(42)");
assert_eq!(int_val.to_nix_string().expect("Failed to format"), "42");
// Test boolean formatting
let bool_val = state
.eval_from_string("true", "<eval>")
.expect("Failed to evaluate bool");
assert_eq!(format!("{bool_val}"), "true");
assert_eq!(format!("{bool_val:?}"), "Value::Bool(true)");
assert_eq!(bool_val.to_nix_string().expect("Failed to format"), "true");
let false_val = state
.eval_from_string("false", "<eval>")
.expect("Failed to evaluate bool");
assert_eq!(format!("{false_val}"), "false");
assert_eq!(
false_val.to_nix_string().expect("Failed to format"),
"false"
);
// Test string formatting
let str_val = state
.eval_from_string("\"hello world\"", "<eval>")
.expect("Failed to evaluate string");
assert_eq!(format!("{str_val}"), "hello world");
assert_eq!(format!("{str_val:?}"), "Value::String(\"hello world\")");
assert_eq!(
str_val.to_nix_string().expect("Failed to format"),
"\"hello world\""
);
// Test string with quotes
let quoted_str = state
.eval_from_string("\"say \\\"hello\\\"\"", "<eval>")
.expect("Failed to evaluate quoted string");
assert_eq!(format!("{quoted_str}"), "say \"hello\"");
assert_eq!(
quoted_str.to_nix_string().expect("Failed to format"),
"\"say \\\"hello\\\"\""
);
// Test null formatting
let null_val = state
.eval_from_string("null", "<eval>")
.expect("Failed to evaluate null");
assert_eq!(format!("{null_val}"), "null");
assert_eq!(format!("{null_val:?}"), "Value::Null");
assert_eq!(null_val.to_nix_string().expect("Failed to format"), "null");
// Test collection formatting
let attrs_val = state
.eval_from_string("{ a = 1; }", "<eval>")
.expect("Failed to evaluate attrs");
assert_eq!(format!("{attrs_val}"), "{ <attrs> }");
assert_eq!(format!("{attrs_val:?}"), "Value::Attrs({ <attrs> })");
let list_val = state
.eval_from_string("[ 1 2 3 ]", "<eval>")
.expect("Failed to evaluate list");
assert_eq!(format!("{list_val}"), "[ <list> ]");
assert_eq!(format!("{list_val:?}"), "Value::List([ <list> ])");
}

322
nixide/src/expr/value.rs Normal file
View file

@ -0,0 +1,322 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use std::ptr::NonNull;
use super::{EvalState, ValueType};
use crate::sys;
use crate::NixError;
/// A Nix value
///
/// This represents any value in the Nix language, including primitives,
/// collections, and functions.
pub struct Value<'a> {
pub(crate) inner: NonNull<sys::nix_value>,
pub(crate) state: &'a EvalState,
}
impl Value<'_> {
/// Force evaluation of this value.
///
/// If the value is a thunk, this will evaluate it to its final form.
///
/// # Errors
///
/// Returns an error if evaluation fails.
pub fn force(&mut self) -> Result<(), NixError> {
NixError::from(
// SAFETY: context, state, and value are valid
unsafe {
sys::nix_value_force(
self.state.context.as_ptr(),
self.state.as_ptr(),
self.inner.as_ptr(),
)
},
"nix_value_force",
)
}
/// Force deep evaluation of this value.
///
/// This forces evaluation of the value and all its nested components.
///
/// # Errors
///
/// Returns an error if evaluation fails.
pub fn force_deep(&mut self) -> Result<(), NixError> {
NixError::from(
// SAFETY: context, state, and value are valid
unsafe {
sys::nix_value_force_deep(
self.state.context.as_ptr(),
self.state.as_ptr(),
self.inner.as_ptr(),
)
},
"nix_value_force_deep",
)
}
/// Get the type of this value.
#[must_use]
pub fn value_type(&self) -> ValueType {
// SAFETY: context and value are valid
let c_type = unsafe { sys::nix_get_type(self.state.context.as_ptr(), self.inner.as_ptr()) };
ValueType::from_c(c_type)
}
/// Convert this value to an integer.
///
/// # Errors
///
/// Returns an error if the value is not an integer.
pub fn as_int(&self) -> Result<i64, NixError> {
if self.value_type() != ValueType::Int {
return Err(NixError::InvalidType {
location: "nixide::Value::as_int",
expected: "int",
got: self.value_type().to_string(),
});
}
// SAFETY: context and value are valid, type is checked
let result = unsafe { sys::nix_get_int(self.state.context.as_ptr(), self.inner.as_ptr()) };
Ok(result)
}
/// Convert this value to a float.
///
/// # Errors
///
/// Returns an error if the value is not a float.
pub fn as_float(&self) -> Result<f64, NixError> {
if self.value_type() != ValueType::Float {
return Err(NixError::InvalidType {
location: "nixide::Value::as_float",
expected: "float",
got: self.value_type().to_string(),
});
}
// SAFETY: context and value are valid, type is checked
let result =
unsafe { sys::nix_get_float(self.state.context.as_ptr(), self.inner.as_ptr()) };
Ok(result)
}
/// Convert this value to a boolean.
///
/// # Errors
///
/// Returns an error if the value is not a boolean.
pub fn as_bool(&self) -> Result<bool, NixError> {
if self.value_type() != ValueType::Bool {
return Err(NixError::InvalidType {
location: "nixide::Value::as_bool",
expected: "bool",
got: self.value_type().to_string(),
});
}
// SAFETY: context and value are valid, type is checked
let result = unsafe { sys::nix_get_bool(self.state.context.as_ptr(), self.inner.as_ptr()) };
Ok(result)
}
/// Convert this value to a string.
///
/// # Errors
///
/// Returns an error if the value is not a string.
pub fn as_string(&self) -> Result<String, NixError> {
if self.value_type() != ValueType::String {
return Err(NixError::InvalidType {
location: "nixide::Value::as_string",
expected: "string",
got: self.value_type().to_string(),
});
}
// For string values, we need to use realised string API
// SAFETY: context and value are valid, type is checked
let realised_str = unsafe {
sys::nix_string_realise(
self.state.context.as_ptr(),
self.state.as_ptr(),
self.inner.as_ptr(),
false, // don't copy more
)
};
if realised_str.is_null() {
return Err(NixError::NullPtr {
location: "nix_string_realise",
});
}
// SAFETY: realised_str is non-null and points to valid RealizedString
let buffer_start = unsafe { sys::nix_realised_string_get_buffer_start(realised_str) };
let buffer_size = unsafe { sys::nix_realised_string_get_buffer_size(realised_str) };
if buffer_start.is_null() {
// Clean up realised string
unsafe {
sys::nix_realised_string_free(realised_str);
}
return Err(NixError::NullPtr {
location: "nix_realised_string_free",
});
}
// SAFETY: buffer_start is non-null and buffer_size gives us the length
let bytes = unsafe { std::slice::from_raw_parts(buffer_start.cast::<u8>(), buffer_size) };
let string = std::str::from_utf8(bytes)
.map_err(|_| NixError::Unknown {
location: "nixide::Value::as_string",
reason: "Invalid UTF-8 in string".to_string(),
})?
.to_owned();
// Clean up realised string
unsafe {
sys::nix_realised_string_free(realised_str);
}
Ok(string)
}
/// Get the raw value pointer.
///
/// # Safety
///
/// The caller must ensure the pointer is used safely.
#[allow(dead_code)]
unsafe fn as_ptr(&self) -> *mut sys::nix_value {
self.inner.as_ptr()
}
/// Format this value as Nix syntax.
///
/// This provides a string representation that matches Nix's own syntax,
/// making it useful for debugging and displaying values to users.
///
/// # Errors
///
/// Returns an error if the value cannot be converted to a string
/// representation.
pub fn to_nix_string(&self) -> Result<String, NixError> {
match self.value_type() {
ValueType::Int => Ok(self.as_int()?.to_string()),
ValueType::Float => Ok(self.as_float()?.to_string()),
ValueType::Bool => Ok(if self.as_bool()? {
"true".to_string()
} else {
"false".to_string()
}),
ValueType::String => Ok(format!("\"{}\"", self.as_string()?.replace('"', "\\\""))),
ValueType::Null => Ok("null".to_string()),
ValueType::Attrs => Ok("{ <attrs> }".to_string()),
ValueType::List => Ok("[ <list> ]".to_string()),
ValueType::Function => Ok("<function>".to_string()),
ValueType::Path => Ok("<path>".to_string()),
ValueType::Thunk => Ok("<thunk>".to_string()),
ValueType::External => Ok("<external>".to_string()),
}
}
}
impl Drop for Value<'_> {
fn drop(&mut self) {
// SAFETY: We own the value and it's valid until drop
unsafe {
sys::nix_value_decref(self.state.context.as_ptr(), self.inner.as_ptr());
}
}
}
impl Display for Value<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self.value_type() {
ValueType::Int => {
if let Ok(val) = self.as_int() {
write!(f, "{val}")
} else {
write!(f, "<int error>")
}
}
ValueType::Float => {
if let Ok(val) = self.as_float() {
write!(f, "{val}")
} else {
write!(f, "<float error>")
}
}
ValueType::Bool => {
if let Ok(val) = self.as_bool() {
write!(f, "{val}")
} else {
write!(f, "<bool error>")
}
}
ValueType::String => {
if let Ok(val) = self.as_string() {
write!(f, "{val}")
} else {
write!(f, "<string error>")
}
}
ValueType::Null => write!(f, "null"),
ValueType::Attrs => write!(f, "{{ <attrs> }}"),
ValueType::List => write!(f, "[ <list> ]"),
ValueType::Function => write!(f, "<function>"),
ValueType::Path => write!(f, "<path>"),
ValueType::Thunk => write!(f, "<thunk>"),
ValueType::External => write!(f, "<external>"),
}
}
}
impl Debug for Value<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let value_type = self.value_type();
match value_type {
ValueType::Int => {
if let Ok(val) = self.as_int() {
write!(f, "Value::Int({val})")
} else {
write!(f, "Value::Int(<error>)")
}
}
ValueType::Float => {
if let Ok(val) = self.as_float() {
write!(f, "Value::Float({val})")
} else {
write!(f, "Value::Float(<error>)")
}
}
ValueType::Bool => {
if let Ok(val) = self.as_bool() {
write!(f, "Value::Bool({val})")
} else {
write!(f, "Value::Bool(<error>)")
}
}
ValueType::String => {
if let Ok(val) = self.as_string() {
write!(f, "Value::String({val:?})")
} else {
write!(f, "Value::String(<error>)")
}
}
ValueType::Null => write!(f, "Value::Null"),
ValueType::Attrs => write!(f, "Value::Attrs({{ <attrs> }})"),
ValueType::List => write!(f, "Value::List([ <list> ])"),
ValueType::Function => write!(f, "Value::Function(<function>)"),
ValueType::Path => write!(f, "Value::Path(<path>)"),
ValueType::Thunk => write!(f, "Value::Thunk(<thunk>)"),
ValueType::External => write!(f, "Value::External(<external>)"),
}
}
}

View file

@ -0,0 +1,68 @@
use std::fmt::{Display, Formatter, Result as FmtResult};
use crate::sys;
/// Nix value types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueType {
/// Thunk (unevaluated expression).
Thunk,
/// Integer value.
Int,
/// Float value.
Float,
/// Boolean value.
Bool,
/// String value.
String,
/// Path value.
Path,
/// Null value.
Null,
/// Attribute set.
Attrs,
/// List.
List,
/// Function.
Function,
/// External value.
External,
}
impl ValueType {
pub(super) fn from_c(value_type: sys::ValueType) -> Self {
match value_type {
sys::ValueType_NIX_TYPE_THUNK => ValueType::Thunk,
sys::ValueType_NIX_TYPE_INT => ValueType::Int,
sys::ValueType_NIX_TYPE_FLOAT => ValueType::Float,
sys::ValueType_NIX_TYPE_BOOL => ValueType::Bool,
sys::ValueType_NIX_TYPE_STRING => ValueType::String,
sys::ValueType_NIX_TYPE_PATH => ValueType::Path,
sys::ValueType_NIX_TYPE_NULL => ValueType::Null,
sys::ValueType_NIX_TYPE_ATTRS => ValueType::Attrs,
sys::ValueType_NIX_TYPE_LIST => ValueType::List,
sys::ValueType_NIX_TYPE_FUNCTION => ValueType::Function,
sys::ValueType_NIX_TYPE_EXTERNAL => ValueType::External,
_ => ValueType::Thunk, // fallback (TODO: is this ok?)
}
}
}
impl Display for ValueType {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let name = match self {
ValueType::Thunk => "thunk",
ValueType::Int => "int",
ValueType::Float => "float",
ValueType::Bool => "bool",
ValueType::String => "string",
ValueType::Path => "path",
ValueType::Null => "null",
ValueType::Attrs => "attrs",
ValueType::List => "list",
ValueType::Function => "function",
ValueType::External => "external",
};
write!(f, "{name}")
}
}

View file

@ -1,14 +1,14 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
// #![warn(missing_docs)]
#[cfg(test)]
mod tests {
use super::*;
mod context;
mod error;
mod expr;
mod store;
mod util;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
pub use context::Context;
pub use error::NixError;
pub use expr::{EvalState, EvalStateBuilder, Value, ValueType};
pub use store::{Store, StorePath};
pub use nixide_sys as sys;

275
nixide/src/store/mod.rs Normal file
View file

@ -0,0 +1,275 @@
// XXX: TODO: should I add support for `nix_libstore_init_no_load_config`
// XXX: TODO: add support for nix_realised_string_* family of functions
// nix_realised_string_get_store_path
// nix_realised_string_get_store_path_count
// # nix_store_real_path
// # nix_store_is_valid_path
// # nix_store_get_version
// # nix_store_get_uri
// # nix_store_get_storedir
// # nix_store_copy_closure
// nix_libstore_init_no_load_config
#[cfg(test)]
mod tests;
mod path;
pub use path::*;
use std::ffi::{CStr, CString, NulError};
use std::os::raw::{c_char, c_void};
use std::path::PathBuf;
use std::ptr::NonNull;
use std::result::Result;
use std::sync::Arc;
use super::{Context, NixError};
use crate::util::bindings::{wrap_libnix_pathbuf_callback, wrap_libnix_string_callback};
use nixide_sys as sys;
/// Nix store for managing packages and derivations.
///
/// The store provides access to Nix packages, derivations, and store paths.
pub struct Store {
pub(crate) inner: NonNull<sys::Store>,
pub(crate) _context: Arc<Context>,
}
impl Store {
/// Open a Nix store.
///
/// # Arguments
///
/// * `context` - The Nix context
/// * `uri` - Optional store URI (None for default store)
///
/// # Errors
///
/// Returns an error if the store cannot be opened.
pub fn open(context: &Arc<Context>, uri: Option<&str>) -> Result<Self, NixError> {
let uri_cstring: CString;
let uri_ptr = if let Some(uri) = uri {
uri_cstring = NixError::from_nulerror(CString::new(uri), "nixide::Store::open")?;
uri_cstring.as_ptr()
} else {
std::ptr::null()
};
// SAFETY: context is valid, uri_ptr is either null or valid CString
let store_ptr =
unsafe { sys::nix_store_open(context.as_ptr(), uri_ptr, std::ptr::null_mut()) };
let inner = NonNull::new(store_ptr).ok_or(NixError::NullPtr {
location: "nix_store_open",
})?;
Ok(Store {
inner,
_context: Arc::clone(context),
})
}
/// Get the raw store pointer.
///
/// # Safety
///
/// The caller must ensure the pointer is used safely.
pub(crate) unsafe fn as_ptr(&self) -> *mut sys::Store {
self.inner.as_ptr()
}
/// Realize a store path.
///
/// This builds/downloads the store path and all its dependencies,
/// making them available in the local store.
///
/// # Arguments
///
/// * `path` - The store path to realize
///
/// # Returns
///
/// A vector of (output_name, store_path) tuples for each realized output.
/// For example, a derivation might produce outputs like ("out", path1), ("dev", path2).
///
/// # Errors
///
/// Returns an error if the path cannot be realized.
pub fn realise<F>(
&self,
path: &StorePath,
callback: fn(&str, &StorePath),
) -> Result<Vec<(String, StorePath)>, NixError> {
// Type alias for our userdata: (outputs vector, context)
type Userdata = (Vec<(String, StorePath)>, Arc<Context>, fn(&str, &StorePath));
// Callback function that will be called for each realized output
unsafe extern "C" fn realise_callback(
userdata: *mut c_void,
out_name_ptr: *const c_char,
out_path_ptr: *const sys::StorePath,
) {
// SAFETY: userdata is a valid pointer to our (Vec, Arc<Context>) tuple
let (outputs, context, callback) = unsafe { &mut *(userdata as *mut Userdata) };
// SAFETY: outname is a valid C string from Nix
let output_name = if !out_name_ptr.is_null() {
unsafe { CStr::from_ptr(out_name_ptr).to_string_lossy().into_owned() }
} else {
String::from("out") // Default output name
};
// SAFETY: out is a valid StorePath pointer from Nix, we need to clone it
// because Nix owns the original and may free it after the callback
if !out_path_ptr.is_null() {
let cloned_path_ptr =
unsafe { sys::nix_store_path_clone(out_path_ptr as *mut sys::StorePath) };
if let Some(inner) = NonNull::new(cloned_path_ptr) {
let store_path = StorePath {
inner,
_context: Arc::clone(context),
};
callback(output_name.as_ref(), &store_path);
outputs.push((output_name, store_path));
}
}
}
// Create userdata with empty outputs vector and context
let mut userdata: Userdata = (Vec::new(), Arc::clone(&self._context), callback);
let userdata_ptr = &mut userdata as *mut Userdata as *mut std::os::raw::c_void;
// SAFETY: All pointers are valid, callback is compatible with the FFI signature
// - self._context is valid for the duration of this call
// - self.inner is valid (checked in Store::open)
// - path.inner is valid (checked in StorePath::parse)
// - userdata_ptr points to valid stack memory
// - realize_callback matches the expected C function signature
let err = unsafe {
sys::nix_store_realise(
self._context.as_ptr(),
self.inner.as_ptr(),
path.as_ptr(),
userdata_ptr,
Some(realise_callback),
)
};
NixError::from(err, "nix_store_realise")?;
// Return the collected outputs
Ok(userdata.0)
}
/// Parse a store path string into a StorePath.
///
/// This is a convenience method that wraps `StorePath::parse()`.
///
/// # Arguments
///
/// * `path` - The store path string (e.g., "/nix/store/...")
///
/// # Errors
///
/// Returns an error if the path cannot be parsed.
///
/// # Example
///
/// ```no_run
/// # use std::sync::Arc;
/// # use nixide::{Context, Store};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let ctx = Arc::new(Context::new()?);
/// let store = Store::open(&ctx, None)?;
/// let path = store.store_path("/nix/store/...")?;
/// # Ok(())
/// # }
/// ```
pub fn store_path(&self, path: &str) -> Result<StorePath, NixError> {
StorePath::parse(&self._context, self, path)
}
/// Get the version of a Nix store
///
/// If the store doesn't have a version (like the dummy store), returns None
pub fn version(&self) -> Result<String, NixError> {
wrap_libnix_string_callback("nix_store_get_version", |callback, user_data| unsafe {
sys::nix_store_get_version(
self._context.as_ptr(),
self.inner.as_ptr(),
Some(callback),
user_data,
)
})
}
/// Get the URI of a Nix store
pub fn uri(&self) -> Result<String, NixError> {
wrap_libnix_string_callback("nix_store_get_uri", |callback, user_data| unsafe {
sys::nix_store_get_uri(
self._context.as_ptr(),
self.inner.as_ptr(),
Some(callback),
user_data,
)
})
}
pub fn store_dir(&self) -> Result<PathBuf, NixError> {
wrap_libnix_pathbuf_callback("nix_store_get_storedir", |callback, user_data| unsafe {
sys::nix_store_get_storedir(
self._context.as_ptr(),
self.inner.as_ptr(),
Some(callback),
user_data,
)
})
}
pub fn copy_closure_to(
&self,
dst_store: &Store,
store_path: &StorePath,
) -> Result<(), NixError> {
let err = unsafe {
sys::nix_store_copy_closure(
self._context.as_ptr(),
self.inner.as_ptr(),
dst_store.inner.as_ptr(),
store_path.inner.as_ptr(),
)
};
NixError::from(err, "nix_store_copy_closure")
}
pub fn copy_closure_from(
&self,
src_store: &Store,
store_path: &StorePath,
) -> Result<(), NixError> {
let err = unsafe {
sys::nix_store_copy_closure(
self._context.as_ptr(),
src_store.inner.as_ptr(),
self.inner.as_ptr(),
store_path.inner.as_ptr(),
)
};
NixError::from(err, "nix_store_copy_closure")
}
}
impl Drop for Store {
fn drop(&mut self) {
// SAFETY: We own the store and it's valid until drop
unsafe {
sys::nix_store_free(self.inner.as_ptr());
}
}
}
// SAFETY: Store can be shared between threads
unsafe impl Send for Store {}
unsafe impl Sync for Store {}

156
nixide/src/store/path.rs Normal file
View file

@ -0,0 +1,156 @@
use std::ffi::CString;
use std::path::PathBuf;
use std::ptr::NonNull;
use std::sync::Arc;
use super::Store;
use crate::util::bindings::{wrap_libnix_pathbuf_callback, wrap_libnix_string_callback};
use crate::{Context, NixError};
use nixide_sys::{self as sys, nix_err_NIX_OK};
/// A path in the Nix store.
///
/// Represents a store path that can be realized, queried, or manipulated.
pub struct StorePath {
pub(crate) inner: NonNull<sys::StorePath>,
pub(crate) _context: Arc<Context>,
}
impl StorePath {
/// Parse a store path string into a StorePath.
///
/// # Arguments
///
/// * `context` - The Nix context
/// * `store` - The store containing the path
/// * `path` - The store path string (e.g., "/nix/store/...")
///
/// # Errors
///
/// Returns an error if the path cannot be parsed.
pub fn parse(context: &Arc<Context>, store: &Store, path: &str) -> Result<Self, NixError> {
let path_cstring = CString::new(path).or(Err(NixError::InvalidArg {
location: "nixide::StorePath::parse",
reason: "`path` contains NUL char",
}))?;
// SAFETY: context, store, and path_cstring are valid
let path_ptr = unsafe {
sys::nix_store_parse_path(context.as_ptr(), store.as_ptr(), path_cstring.as_ptr())
};
let inner = NonNull::new(path_ptr).ok_or(NixError::NullPtr {
location: "nix_store_parse_path",
})?;
Ok(Self {
inner,
_context: Arc::clone(context),
})
}
/// Get the name component of the store path.
///
/// This returns the name part of the store path (everything after the hash).
/// For example, for "/nix/store/abc123...-hello-1.0", this returns "hello-1.0".
///
/// # Errors
///
/// Returns an error if the name cannot be retrieved.
pub fn name(&self) -> Result<String, NixError> {
wrap_libnix_string_callback("nix_store_path_name", |callback, user_data| unsafe {
sys::nix_store_path_name(self.inner.as_ptr(), Some(callback), user_data);
// NOTE: nix_store_path_name doesn't return nix_err, so we force it to return successfully
nix_err_NIX_OK
})
}
/// Get the physical location of a store path
///
/// A store may reside at a different location than its `storeDir` suggests.
/// This situation is called a relocated store.
///
/// Relocated stores are used during NixOS installation, as well as in restricted
/// computing environments that don't offer a writable `/nix/store`.
///
/// Not all types of stores support this operation.
///
/// # Arguments
/// * `context` [in] - Optional, stores error information
/// * `store` [in] - nix store reference
/// * `path` [in] - the path to get the real path from
/// * `callback` [in] - called with the real path
/// * `user_data` [in] - arbitrary data, passed to the callback when it's called.
///
/// # Arguments
///
/// * `store` - The store containing the path
///
pub fn real_path(&self, store: &Store) -> Result<PathBuf, NixError> {
wrap_libnix_pathbuf_callback("nix_store_real_path", |callback, user_data| unsafe {
sys::nix_store_real_path(
self._context.as_ptr(),
store.inner.as_ptr(),
self.inner.as_ptr(),
Some(callback),
user_data,
)
})
}
/// Check if a [StorePath] is valid (i.e. that its corresponding store object
/// and its closure of references exists in the store).
///
/// # Arguments
///
/// * `store` - The store containing the path
///
pub fn is_valid(&self, store: &Store) -> bool {
unsafe {
sys::nix_store_is_valid_path(
self._context.as_ptr(),
store.inner.as_ptr(),
self.inner.as_ptr(),
)
}
}
/// Get the raw store path pointer.
///
/// # Safety
///
/// The caller must ensure the pointer is used safely.
pub(crate) unsafe fn as_ptr(&self) -> *mut sys::StorePath {
self.inner.as_ptr()
}
}
impl Clone for StorePath {
fn clone(&self) -> Self {
// SAFETY: self.inner is valid, nix_store_path_clone creates a new copy
let cloned_ptr = unsafe { sys::nix_store_path_clone(self.inner.as_ptr()) };
// This should never fail as cloning a valid path should always succeed
let inner =
NonNull::new(cloned_ptr).expect("nix_store_path_clone returned null for valid path");
StorePath {
inner,
_context: Arc::clone(&self._context),
}
}
}
impl Drop for StorePath {
fn drop(&mut self) {
// SAFETY: We own the store path and it's valid until drop
unsafe {
sys::nix_store_path_free(self.inner.as_ptr());
}
}
}
// SAFETY: StorePath can be shared between threads
unsafe impl Send for StorePath {}
unsafe impl Sync for StorePath {}

66
nixide/src/store/tests.rs Normal file
View file

@ -0,0 +1,66 @@
use serial_test::serial;
use super::*;
#[test]
#[serial]
fn test_store_opening() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let _store = Store::open(&ctx, None).expect("Failed to open store");
}
#[test]
#[serial]
fn test_store_path_parse() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store = Store::open(&ctx, None).expect("Failed to open store");
// Try parsing a well-formed store path
// Note: This may fail if the path doesn't exist in the store
let result = StorePath::parse(
&ctx,
&store,
"/nix/store/00000000000000000000000000000000-test",
);
// We don't assert success here because the path might not exist
// This test mainly checks that the API works correctly
match result {
Ok(_path) => {
// Successfully parsed the path
}
Err(_) => {
// Path doesn't exist or is invalid, which is expected
}
}
}
#[test]
#[serial]
fn test_store_path_clone() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store = Store::open(&ctx, None).expect("Failed to open store");
// Try to get a valid store path by parsing
// Note: This test is somewhat limited without a guaranteed valid path
if let Ok(path) = StorePath::parse(
&ctx,
&store,
"/nix/store/00000000000000000000000000000000-test",
) {
let cloned = path.clone();
// Assert that the cloned path has the same name as the original
let original_name = path.name().expect("Failed to get original path name");
let cloned_name = cloned.name().expect("Failed to get cloned path name");
assert_eq!(
original_name, cloned_name,
"Cloned path should have the same name as original"
);
}
}
// Note: test_realize is not included because it requires a valid store path
// to realize, which we can't guarantee in a unit test. Integration tests
// would be more appropriate for testing realize() with actual derivations.

View file

@ -0,0 +1,34 @@
use std::os::raw::{c_char, c_uint, c_void};
use std::path::PathBuf;
use crate::NixError;
pub fn wrap_libnix_string_callback<F>(name: &'static str, callback: F) -> Result<String, NixError>
where
F: FnOnce(unsafe extern "C" fn(*const c_char, c_uint, *mut c_void), *mut c_void) -> i32,
{
// Callback to receive the string
unsafe extern "C" fn wrapper_callback(start: *const c_char, n: c_uint, user_data: *mut c_void) {
let result = unsafe { &mut *(user_data as *mut Option<String>) };
if !start.is_null() && n > 0 {
let bytes = unsafe { std::slice::from_raw_parts(start.cast::<u8>(), n as usize) };
if let Ok(s) = std::str::from_utf8(bytes) {
*result = Some(s.to_string());
}
}
}
let mut result: Option<String> = None;
let user_data = &mut result as *mut _ as *mut c_void;
NixError::from(callback(wrapper_callback, user_data), name)?;
result.ok_or(NixError::NullPtr { location: name })
}
pub fn wrap_libnix_pathbuf_callback<F>(name: &'static str, callback: F) -> Result<PathBuf, NixError>
where
F: FnOnce(unsafe extern "C" fn(*const c_char, c_uint, *mut c_void), *mut c_void) -> i32,
{
wrap_libnix_string_callback(name, callback).map(PathBuf::from)
}

1
nixide/src/util/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod bindings;