diff --git a/CHANGELOG.md b/CHANGELOG.md index da22679..307a96b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `primop::RecoverableError` for primop errors that should not be memoized in the thunk, allowing retry on next force. Required by Nix >= 2.34 ([release note](https://nix.dev/manual/nix/2.34/release-notes/rl-2.34.html#c-api-changes)) for recoverable errors to remain recoverable, as Nix 2.34 memoizes errors by default. + ## [0.2.0] - 2026-01-13 ### Added diff --git a/dev/flake.lock b/dev/flake.lock index 44ec905..1604bb7 100644 --- a/dev/flake.lock +++ b/dev/flake.lock @@ -24,11 +24,11 @@ ] }, "locked": { - "lastModified": 1768135262, - "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", "type": "github" }, "original": { @@ -63,11 +63,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1768476106, - "narHash": "sha256-V0YOJRum50gtKgwavsAfwXc9+XAsJCC7386YZx1sWGQ=", + "lastModified": 1771131391, + "narHash": "sha256-HPBNYf7HiKtBVy7/69vKpLYHX6wTcUxndxmybzDlXP8=", "owner": "hercules-ci", "repo": "hercules-ci-effects", - "rev": "c19e263e6e22ec7379d972f19e6a322f943c73fb", + "rev": "0b152e0f7c5cc265a529cd63374b80e2771b207b", "type": "github" }, "original": { @@ -78,11 +78,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768305791, - "narHash": "sha256-AIdl6WAn9aymeaH/NvBj0H9qM+XuAuYbGMZaP0zcXAQ=", + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1412caf7bf9e660f2f962917c14b1ea1c3bc695e", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", "type": "github" }, "original": { @@ -99,11 +99,11 @@ "nixpkgs": [] }, "locked": { - "lastModified": 1769069492, - "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=", + "lastModified": 1772024342, + "narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", + "rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476", "type": "github" }, "original": { @@ -124,11 +124,11 @@ "nixpkgs": [] }, "locked": { - "lastModified": 1769691507, - "narHash": "sha256-8aAYwyVzSSwIhP2glDhw/G0i5+wOrren3v6WmxkVonM=", + "lastModified": 1770228511, + "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "28b19c5844cc6e2257801d43f2772a4b4c050a1b", + "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7", "type": "github" }, "original": { diff --git a/flake.lock b/flake.lock index 168537c..da94010 100644 --- a/flake.lock +++ b/flake.lock @@ -77,11 +77,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1768135262, - "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", "type": "github" }, "original": { @@ -170,11 +170,11 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1769798267, - "narHash": "sha256-vpI7XEfX5zeCVRANUzhMNsZfrMWuN0rwNenQ3z0rJNo=", + "lastModified": 1772224943, + "narHash": "sha256-jJIlRLPPVYu860MVFx4gsRx3sskmLDSRWXXue5tYncw=", "owner": "NixOS", "repo": "nix", - "rev": "77b6b01b727f0cd1324e431a32a8854768b957ef", + "rev": "0acd0566e85e4597269482824711bcde7b518600", "type": "github" }, "original": { @@ -196,11 +196,11 @@ "treefmt": "treefmt" }, "locked": { - "lastModified": 1769840916, - "narHash": "sha256-bjtDp0NHjLjDOjklQVHCDCVM5q39zDzuwenNri0p4Ys=", + "lastModified": 1772260057, + "narHash": "sha256-NaUqM0i6XIGdgRNxxQ9sfgCAVeE2Ko9rz7e19RsNUKw=", "owner": "90-008", "repo": "nix-cargo-integration", - "rev": "6d583e2098fa3df490c2597df06386e3efcc39b6", + "rev": "c783c5dff02c06f2af6226d4dd4d494542d0a4d2", "type": "github" }, "original": { @@ -211,11 +211,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769461804, - "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", + "lastModified": 1772198003, + "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", + "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", "type": "github" }, "original": { @@ -243,11 +243,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1765674936, - "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", + "lastModified": 1769909678, + "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", + "rev": "72716169fe93074c333e8d0173151350670b824c", "type": "github" }, "original": { @@ -280,11 +280,11 @@ ] }, "locked": { - "lastModified": 1768135262, - "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", "type": "github" }, "original": { @@ -355,11 +355,11 @@ ] }, "locked": { - "lastModified": 1769828398, - "narHash": "sha256-zmnvRUm15QrlKH0V1BZoiT3U+Q+tr+P5Osi8qgtL9fY=", + "lastModified": 1772247314, + "narHash": "sha256-x6IFQ9bL7YYfW2m2z8D3Em2YtAA3HE8kiCFwai2fwrw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "a1d32c90c8a4ea43e9586b7e5894c179d5747425", + "rev": "a1ab5e89ab12e1a37c0b264af6386a7472d68a15", "type": "github" }, "original": { @@ -399,11 +399,11 @@ ] }, "locked": { - "lastModified": 1769691507, - "narHash": "sha256-8aAYwyVzSSwIhP2glDhw/G0i5+wOrren3v6WmxkVonM=", + "lastModified": 1770228511, + "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "28b19c5844cc6e2257801d43f2772a4b4c050a1b", + "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7", "type": "github" }, "original": { diff --git a/nix-bindings-expr/build.rs b/nix-bindings-expr/build.rs index 6a038bd..f56bc3e 100644 --- a/nix-bindings-expr/build.rs +++ b/nix-bindings-expr/build.rs @@ -2,5 +2,5 @@ use nix_bindings_util::nix_version::emit_version_cfg; fn main() { let nix_version = pkg_config::probe_library("nix-expr-c").unwrap().version; - emit_version_cfg(&nix_version, &["2.26"]); + emit_version_cfg(&nix_version, &["2.26", "2.34.0pre"]); } diff --git a/nix-bindings-expr/src/eval_state.rs b/nix-bindings-expr/src/eval_state.rs index b8e27e6..5d8599f 100644 --- a/nix-bindings-expr/src/eval_state.rs +++ b/nix-bindings-expr/src/eval_state.rs @@ -2845,4 +2845,81 @@ mod tests { }) .unwrap(); } + + #[test] + #[cfg(nix_at_least = "2.34.0pre")] + fn eval_state_primop_recoverable_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let call_count = std::cell::Cell::new(0u32); + let v = es + .new_value_thunk( + "recoverable_test", + Box::new(move |es: &mut EvalState| { + let count = call_count.get(); + call_count.set(count + 1); + if count == 0 { + Err(primop::RecoverableError::new("transient failure").into()) + } else { + es.new_value_int(42) + } + }), + ) + .unwrap(); + + // First force should fail with the recoverable error + let r = es.force(&v); + assert!(r.is_err()); + assert!( + r.unwrap_err().to_string().contains("transient failure"), + "Error message should contain 'transient failure'" + ); + + // Second force should succeed because the error was recoverable + es.force(&v).unwrap(); + let i = es.require_int(&v).unwrap(); + assert_eq!(i, 42); + }) + .unwrap(); + } + + #[test] + #[cfg(nix_at_least = "2.34.0pre")] + fn eval_state_primop_recoverable_error_in_chain() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let call_count = std::cell::Cell::new(0u32); + let v = es + .new_value_thunk( + "recoverable_chain_test", + Box::new(move |es: &mut EvalState| { + let count = call_count.get(); + call_count.set(count + 1); + if count == 0 { + // Wrap RecoverableError in .context(), pushing it down the chain + use anyhow::Context; + Err(primop::RecoverableError::new("transient failure")) + .context("wrapper context")? + } else { + es.new_value_int(42) + } + }), + ) + .unwrap(); + + // First force should fail + let r = es.force(&v); + assert!(r.is_err()); + + // Second force should succeed if RecoverableError is found in the chain + es.force(&v).unwrap(); + let i = es.require_int(&v).unwrap(); + assert_eq!(i, 42); + }) + .unwrap(); + } } diff --git a/nix-bindings-expr/src/primop.rs b/nix-bindings-expr/src/primop.rs index 02410fc..1504405 100644 --- a/nix-bindings-expr/src/primop.rs +++ b/nix-bindings-expr/src/primop.rs @@ -8,6 +8,35 @@ use std::ffi::{c_int, c_void, CStr, CString}; use std::mem::ManuallyDrop; use std::ptr::{null, null_mut}; +/// A primop error that is not memoized in the thunk that triggered it, +/// allowing the thunk to be forced again. +/// +/// Since [Nix 2.34](https://nix.dev/manual/nix/2.34/release-notes/rl-2.34.html#c-api-changes), +/// primop errors are memoized by default: once a thunk fails, forcing it +/// again returns the same error. Use `RecoverableError` for errors that +/// are transient, so the caller can retry. +/// +/// On Nix < 2.34, all errors are already recoverable, so this type has +/// no additional effect. +/// +/// Available since nix-bindings-expr 0.2.1. +#[derive(Debug)] +pub struct RecoverableError(String); + +impl RecoverableError { + pub fn new(msg: impl Into) -> Self { + RecoverableError(msg.into()) + } +} + +impl std::fmt::Display for RecoverableError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for RecoverableError {} + /// Metadata for a primop, used with `PrimOp::new`. pub struct PrimOpMeta<'a, const N: usize> { /// Name of the primop. Note that primops do not have to be registered as @@ -36,6 +65,11 @@ impl Drop for PrimOp { } } impl PrimOp { + /// Create a new primop with the given metadata and implementation. + /// + /// When `f` returns an `Err`, the error is propagated to the Nix evaluator. + /// To return a [recoverable error](RecoverableError), include it in the + /// error chain (e.g. `Err(RecoverableError::new("...").into())`). pub fn new( eval_state: &mut EvalState, meta: PrimOpMeta, @@ -108,13 +142,22 @@ unsafe extern "C" fn function_adapter( raw::copy_value(context_out, ret, v.raw_ptr()); }, Err(e) => unsafe { + let err_code = error_code(&e); let cstr = CString::new(e.to_string()).unwrap_or_else(|_e| { CString::new("") .unwrap() }); - raw_util::set_err_msg(context_out, raw_util::err_NIX_ERR_UNKNOWN, cstr.as_ptr()); + raw_util::set_err_msg(context_out, err_code, cstr.as_ptr()); }, } } +fn error_code(e: &anyhow::Error) -> raw_util::err { + #[cfg(nix_at_least = "2.34.0pre")] + if e.downcast_ref::().is_some() { + return raw_util::err_NIX_ERR_RECOVERABLE; + } + raw_util::err_NIX_ERR_UNKNOWN +} + static FUNCTION_ADAPTER: raw::PrimOpFun = Some(function_adapter); diff --git a/nix-bindings-store/src/store.rs b/nix-bindings-store/src/store.rs index 655a088..cdd0977 100644 --- a/nix-bindings-store/src/store.rs +++ b/nix-bindings-store/src/store.rs @@ -788,7 +788,8 @@ mod tests { Err(e) => e.to_string(), }; assert!( - err.contains("required system or feature not available"), + err.contains("required system or feature not available") + || err.contains("platform mismatch"), "Error should mention system not available, got: {}", err );