Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
nix.url = "github:NixOS/nix";
nix.inputs.nixpkgs.follows = "nixpkgs";
nix = {
# TODO: Change before merging
url = "github:NixOS/nix?ref=pull/15675/merge";
};
# nix.inputs.nixpkgs.follows = "nixpkgs";
nix-cargo-integration.url = "github:90-008/nix-cargo-integration";
nix-cargo-integration.inputs.nixpkgs.follows = "nixpkgs";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
Expand Down
2 changes: 1 addition & 1 deletion nix-bindings-store/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ use nix_bindings_util::nix_version::emit_version_cfg;

fn main() {
let nix_version = pkg_config::probe_library("nix-store-c").unwrap().version;
emit_version_cfg(&nix_version, &["2.26", "2.33.0pre", "2.33"]);
emit_version_cfg(&nix_version, &["2.26", "2.33.0pre", "2.33", "2.35.0pre"]);
}
1 change: 1 addition & 0 deletions nix-bindings-store/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod derivation;
pub mod path;
pub mod path_info;
pub mod store;
151 changes: 151 additions & 0 deletions nix-bindings-store/src/path_info/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#![cfg(nix_at_least = "2.35.0pre")]

use std::os::raw::{c_char, c_uint, c_void};
use std::ptr::NonNull;

use anyhow::Result;
use nix_bindings_store_sys as raw;
use nix_bindings_util::{
check_call,
context::Context,
result_string_init,
string_return::{callback_get_result_string, callback_get_result_string_data},
};

use crate::path::StorePath;

/// Metadata about a store path.
///
/// **Requires Nix 2.35 or later.**
pub struct PathInfo {
pub(crate) inner: NonNull<raw::path_info>,
}

impl PathInfo {
pub(crate) fn new_raw(inner: NonNull<raw::path_info>) -> Self {
PathInfo { inner }
}

/// This is a low level function that you shouldn't have to call unless you are developing the bindings.
///
/// Get a pointer to the underlying API path_info.
///
/// # Safety
///
/// The returned pointer is only valid as long as this `PathInfo` is alive.
pub unsafe fn as_ptr(&self) -> *mut raw::path_info {
self.inner.as_ptr()
}

/// Get the NAR hash of this store path.
///
/// Returns a string with algorithm prefix in base-32 encoding, e.g. `"sha256:1b8m..."`.
pub fn nar_hash(&self) -> Result<String> {
let mut ctx = Context::new();
let mut r = result_string_init!();
unsafe {
check_call!(raw::path_info_get_nar_hash(
&mut ctx,
self.inner.as_ptr(),
Some(callback_get_result_string),
callback_get_result_string_data(&mut r)
))?;
}
r
}

/// Get the NAR size of this store path in bytes.
///
/// Returns 0 if the size is unknown.
pub fn nar_size(&self) -> Result<u64> {
let mut ctx = Context::new();
let size =
unsafe { check_call!(raw::path_info_get_nar_size(&mut ctx, self.inner.as_ptr()))? };
Ok(size)
}

/// Get the references of this store path.
pub fn references(&self) -> Result<Vec<StorePath>> {
let mut ctx = Context::new();
let mut refs: Vec<StorePath> = Vec::new();

unsafe extern "C" fn callback(user_data: *mut c_void, store_path: *const raw::StorePath) {
let refs = &mut *(user_data as *mut Vec<StorePath>);
let cloned = raw::store_path_clone(store_path);
let cloned = NonNull::new(cloned).expect("store_path_clone returned null");
refs.push(StorePath::new_raw(cloned));
}

let user_data = &mut refs as *mut Vec<StorePath> as *mut c_void;
unsafe {
check_call!(raw::path_info_get_references(
&mut ctx,
self.inner.as_ptr(),
user_data,
Some(callback)
))?;
}
Ok(refs)
}

/// Get the deriver of this store path, if known.
///
/// Returns `None` if no deriver is recorded (e.g., paths added directly).
pub fn deriver(&self) -> Result<Option<StorePath>> {
let mut ctx = Context::new();
let ptr =
unsafe { check_call!(raw::path_info_get_deriver(&mut ctx, self.inner.as_ptr()))? };
Ok(NonNull::new(ptr).map(|p| unsafe { StorePath::new_raw(p) }))
}

/// Get the signatures of this store path.
///
/// Returns an empty vector for unsigned paths (e.g., a local store).
/// Each signature has the format `"keyName:base64sig"`.
pub fn sigs(&self) -> Result<Vec<String>> {
let mut ctx = Context::new();
let mut sigs: Vec<String> = Vec::new();

unsafe extern "C" fn callback(user_data: *mut c_void, sig: *const c_char, sig_len: c_uint) {
let sigs = &mut *(user_data as *mut Vec<String>);
let bytes = std::slice::from_raw_parts(sig as *const u8, sig_len as usize);
sigs.push(String::from_utf8_lossy(bytes).into_owned());
}

let user_data = &mut sigs as *mut Vec<String> as *mut c_void;
unsafe {
check_call!(raw::path_info_get_sigs(
&mut ctx,
self.inner.as_ptr(),
user_data,
Some(callback)
))?;
}
Ok(sigs)
}

/// Get the content address of this store path, if it is content-addressed.
///
/// Returns `None` for input-addressed paths.
pub fn ca(&self) -> Result<Option<String>> {
let mut ctx = Context::new();
let mut r = result_string_init!();
unsafe {
check_call!(raw::path_info_get_ca(
&mut ctx,
self.inner.as_ptr(),
Some(callback_get_result_string),
callback_get_result_string_data(&mut r)
))?;
}
Ok(r.ok().filter(|s| !s.is_empty()))
}
}

impl Drop for PathInfo {
fn drop(&mut self) {
unsafe {
raw::path_info_free(self.inner.as_ptr());
}
}
}
72 changes: 72 additions & 0 deletions nix-bindings-store/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use std::sync::{Arc, LazyLock, Mutex, Weak};
#[cfg(nix_at_least = "2.33.0pre")]
use crate::derivation::Derivation;
use crate::path::StorePath;
#[cfg(nix_at_least = "2.35.0pre")]
use crate::path_info::PathInfo;

/* TODO make Nix itself thread safe */
static INIT: LazyLock<Result<()>> = LazyLock::new(|| unsafe {
Expand Down Expand Up @@ -410,6 +412,26 @@ impl Store {
Ok(r)
}

/// Query path info for a store path.
///
/// **Requires Nix 2.35 or later.**
///
/// Returns the [`PathInfo`] for a path that exists in the store.
#[cfg(nix_at_least = "2.35.0pre")]
#[doc(alias = "nix_store_query_path_info")]
pub fn query_path_info(&mut self, path: &StorePath) -> Result<PathInfo> {
unsafe {
let ptr = check_call!(raw::store_query_path_info(
&mut self.context,
self.inner.ptr(),
path.as_ptr()
))?;
let inner = NonNull::new(ptr)
.ok_or_else(|| Error::msg("store_query_path_info returned null"))?;
Ok(PathInfo::new_raw(inner))
}
}

pub fn weak_ref(&self) -> StoreWeak {
StoreWeak {
inner: Arc::downgrade(&self.inner),
Expand Down Expand Up @@ -991,6 +1013,56 @@ mod tests {
drop(temp_dir);
}

#[test]
#[cfg(nix_at_least = "2.35.0pre")]
fn query_path_info() {
let (mut store, temp_dir) = create_temp_store();
let drv_json = create_test_derivation_json();
let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
let drv_path = store.add_derivation(&drv).unwrap();

let outputs = store.realise(&drv_path).unwrap();
let out_path = &outputs["out"];

let info = store.query_path_info(out_path).unwrap();

let nar_hash = info.nar_hash().unwrap();
assert!(
nar_hash.starts_with("sha256:"),
"Expected sha256 nar hash, got: {nar_hash}"
);

let nar_size = info.nar_size().unwrap();
assert!(nar_size > 0, "Expected non-zero nar_size, got {nar_size}");

let refs = info.references().unwrap();
assert!(refs.is_empty(), "Expected no references for simple path");

let deriver = info.deriver().unwrap();
assert!(deriver.is_some(), "Expected a deriver");
assert!(deriver.unwrap().name().unwrap().ends_with(".drv"));

let sigs = info.sigs().unwrap();
assert!(
sigs.is_empty(),
"Expected no sigs on unsigned local store path"
);

let ca = info.ca().unwrap();
let ca_str = ca.expect("Expected CA for content-addressed derivation output");
assert!(
!ca_str.is_empty(),
"Expected non-empty CA string, got empty string"
);
assert!(
ca_str.starts_with("fixed:"),
"Expected CA to start with 'fixed:', got: {ca_str}"
);

drop(store);
drop(temp_dir);
}

#[test]
#[cfg(nix_at_least = "2.33")]
fn get_fs_closure_include_derivers() {
Expand Down