+
+
+
+ nix-bindings-rust
+
+
+
+
+
+
+ nix-bindings-rust
+ Rust bindings for the Nix C API
+ Crates
+
+
+ Low-level bindings
+
+ These -sys crates provide raw FFI bindings generated by
+ bindgen .
+ They expose the C API directly without safety wrappers.
+ Most users should prefer the high-level crates above.
+
+
+
+
+
+ EOF
+ '';
+ };
+ }
+ ];
+ }).config.public;
+
+ devShells.default = pkgs.mkShell {
+ name = "nix-bindings-devshell";
+ strictDeps = true;
+ inputsFrom = [ config.nci.outputs.nix-bindings.devShell ];
+ inherit (config.nci.outputs.nix-bindings.devShell.env)
+ LIBCLANG_PATH
+ NIX_CC_UNWRAPPED
+ ;
+ NIX_DEBUG_INFO_DIRS =
+ let
+ # TODO: add to Nixpkgs lib
+ getDebug =
+ pkg:
+ if pkg ? debug then
+ pkg.debug
+ else if pkg ? lib then
+ pkg.lib
+ else
+ pkg;
+ in
+ "${getDebug config.packages.nix}/lib/debug";
+ buildInputs = [
+ config.packages.nix
+ ];
+ nativeBuildInputs = [
+ config.treefmt.build.wrapper
+
+ pkgs.rust-analyzer
+ pkgs.nixfmt
+ pkgs.rustfmt
+ pkgs.pkg-config
+ pkgs.clang-tools # clangd
+ pkgs.valgrind
+ pkgs.gdb
+ pkgs.hci
+ # TODO: set up cargo-valgrind in shell and build
+ # currently both this and `cargo install cargo-valgrind`
+ # produce a binary that says ENOENT.
+ # pkgs.cargo-valgrind
+ ];
+ shellHook = ''
+ ${config.pre-commit.shellHook}
+ echo 1>&2 "Welcome to the development shell!"
+ '';
+ # rust-analyzer needs a NIX_PATH for some reason
+ NIX_PATH = "nixpkgs=${inputs.nixpkgs}";
+ };
+ };
+ herculesCI =
+ hci@{ lib, ... }:
+ {
+ ciSystems = [ "x86_64-linux" ];
+ onPush.default.outputs = {
+ effects.pushDocs = lib.optionalAttrs (hci.config.repo.branch == "main") (
+ withSystem "x86_64-linux" (
+ { config, hci-effects, ... }:
+ hci-effects.gitWriteBranch {
+ git.checkout.remote.url = hci.config.repo.remoteHttpUrl;
+ git.checkout.forgeType = "github";
+ git.checkout.user = "x-access-token";
+ git.update.branch = "gh-pages";
+ contents = config.packages.docs;
+ destination = "development"; # directory
+ }
+ )
+ );
+ };
+ };
+ hercules-ci.flake-update = {
+ enable = true;
+ baseMerge.enable = true;
+ autoMergeMethod = "merge";
+ when = {
+ dayOfMonth = 1;
+ };
+ flakes = {
+ "." = { };
+ "dev" = { };
+ };
+ };
+ hercules-ci.cargo-publish = {
+ enable = true;
+ secretName = "crates-io";
+ assertVersions = true;
+ };
+ flake = { };
+}
diff --git a/dev/flake.lock b/dev/flake.lock
new file mode 100644
index 0000000..1604bb7
--- /dev/null
+++ b/dev/flake.lock
@@ -0,0 +1,143 @@
+{
+ "nodes": {
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1767039857,
+ "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
+ "owner": "NixOS",
+ "repo": "flake-compat",
+ "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-parts": {
+ "inputs": {
+ "nixpkgs-lib": [
+ "hercules-ci-effects",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1769996383,
+ "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
+ "type": "github"
+ },
+ "original": {
+ "id": "flake-parts",
+ "type": "indirect"
+ }
+ },
+ "gitignore": {
+ "inputs": {
+ "nixpkgs": [
+ "pre-commit-hooks-nix",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1709087332,
+ "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "type": "github"
+ }
+ },
+ "hercules-ci-effects": {
+ "inputs": {
+ "flake-parts": "flake-parts",
+ "nixpkgs": "nixpkgs"
+ },
+ "locked": {
+ "lastModified": 1771131391,
+ "narHash": "sha256-HPBNYf7HiKtBVy7/69vKpLYHX6wTcUxndxmybzDlXP8=",
+ "owner": "hercules-ci",
+ "repo": "hercules-ci-effects",
+ "rev": "0b152e0f7c5cc265a529cd63374b80e2771b207b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "hercules-ci-effects",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1771008912,
+ "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "a82ccc39b39b621151d6732718e3e250109076fa",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "pre-commit-hooks-nix": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "gitignore": "gitignore",
+ "nixpkgs": []
+ },
+ "locked": {
+ "lastModified": 1772024342,
+ "narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=",
+ "owner": "cachix",
+ "repo": "pre-commit-hooks.nix",
+ "rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "pre-commit-hooks.nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "hercules-ci-effects": "hercules-ci-effects",
+ "pre-commit-hooks-nix": "pre-commit-hooks-nix",
+ "treefmt-nix": "treefmt-nix"
+ }
+ },
+ "treefmt-nix": {
+ "inputs": {
+ "nixpkgs": []
+ },
+ "locked": {
+ "lastModified": 1770228511,
+ "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/dev/flake.nix b/dev/flake.nix
new file mode 100644
index 0000000..09a06a1
--- /dev/null
+++ b/dev/flake.nix
@@ -0,0 +1,11 @@
+{
+ description = "dependencies only";
+ inputs = {
+ pre-commit-hooks-nix.url = "github:cachix/pre-commit-hooks.nix";
+ pre-commit-hooks-nix.inputs.nixpkgs.follows = "";
+ hercules-ci-effects.url = "github:hercules-ci/hercules-ci-effects";
+ treefmt-nix.url = "github:numtide/treefmt-nix";
+ treefmt-nix.inputs.nixpkgs.follows = "";
+ };
+ outputs = { ... }: { };
+}
diff --git a/doc/hacking/test-ffi.md b/doc/hacking/test-ffi.md
new file mode 100644
index 0000000..c19c485
--- /dev/null
+++ b/doc/hacking/test-ffi.md
@@ -0,0 +1,25 @@
+
+# Testing FFI code
+
+If `cargo-valgrind` is broken, you may run `valgrind` manually.
+
+1. `cargo test -v`
+2. find the relevant test suite executable in the log
+ - example: `/home/user/src/nix-bindings-rust/target/debug/deps/nix_util-036ec381a9e3fd6d`
+3. `valgrind --leak-check=full `
+4. check that
+ - `definitely lost: 0 bytes in 0 blocks`
+
+## Paranoid check
+
+Although normal valgrind tends to catch things, you may choose to enable `--show-leak-kinds=all`.
+This will print a few false positive.
+
+Acceptable leaks are those involving (and this may be Linux-specific)
+- `call_init`: static initializers
+ - `nix::GlobalConfig::Register::Register`
+ - `_GLOBAL__sub_I_logging.cc`
+ - ...
+- `new`: a leak in the rust test framework
+
+When in doubt, compare the log to a run with your new test case commented out.
diff --git a/doc/maintainers/release.md b/doc/maintainers/release.md
new file mode 100644
index 0000000..2d04d96
--- /dev/null
+++ b/doc/maintainers/release.md
@@ -0,0 +1,23 @@
+# Release process
+
+This project uses simple tags, that trigger a release of all crates using Hercules CI.
+Based on the [HCI Effects cargo publish workflow].
+
+## Steps
+
+1. Create a `release` branch
+2. Decide the version bump (patch for fixes, minor for features, major for breaking changes)
+3. Update `CHANGELOG.md`: make sure the Unreleased section is up to date, then change it to the new version and release date
+4. Open a draft release PR and wait for CI to pass
+5. Create and push a tag matching the version
+6. Add a new Unreleased section to `CHANGELOG.md`
+7. Bump version in all `Cargo.toml` files to the next patch version (e.g., `0.2.0` → `0.2.1`)
+ and run `cargo update --workspace` to update `Cargo.lock`,
+ so that `cargo publish --dry-run` passes on subsequent commits
+8. Merge the release PR
+
+---
+
+Dissatisfied with the coarse grained release process? Complain to @roberth and he'll get it done for you.
+
+[HCI Effects cargo publish workflow]: https://docs.hercules-ci.com/hercules-ci-effects/reference/flake-parts/cargo-publish/#_releasing_a_version
diff --git a/docs/ref.md b/docs/ref.md
deleted file mode 100644
index db53d00..0000000
--- a/docs/ref.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Nix Bindgen-rs References
-- https://github.com/NotAShelf/nix-bindings
-- https://github.com/nixops4/nix-bindings-rust
diff --git a/flake.lock b/flake.lock
index b306085..da94010 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,39 +1,414 @@
{
"nodes": {
- "nixpkgs": {
+ "crane": {
+ "flake": false,
"locked": {
- "lastModified": 1773222311,
- "narHash": "sha256-BHoB/XpbqoZkVYZCfXJXfkR+GXFqwb/4zbWnOr2cRcU=",
+ "lastModified": 1758758545,
+ "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
+ "owner": "ipetkov",
+ "repo": "crane",
+ "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ipetkov",
+ "ref": "v0.21.1",
+ "repo": "crane",
+ "type": "github"
+ }
+ },
+ "dream2nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nix-cargo-integration",
+ "nixpkgs"
+ ],
+ "purescript-overlay": "purescript-overlay",
+ "pyproject-nix": "pyproject-nix"
+ },
+ "locked": {
+ "lastModified": 1765953015,
+ "narHash": "sha256-5FBZbbWR1Csp3Y2icfRkxMJw/a/5FGg8hCXej2//bbI=",
+ "owner": "nix-community",
+ "repo": "dream2nix",
+ "rev": "69eb01fa0995e1e90add49d8ca5bcba213b0416f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "dream2nix",
+ "type": "github"
+ }
+ },
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1767039857,
+ "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
- "repo": "nixpkgs",
- "rev": "0590cd39f728e129122770c029970378a79d076a",
+ "repo": "flake-compat",
+ "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-25.11",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-compat_2": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1696426674,
+ "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-parts": {
+ "inputs": {
+ "nixpkgs-lib": "nixpkgs-lib"
+ },
+ "locked": {
+ "lastModified": 1769996383,
+ "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "type": "github"
+ }
+ },
+ "flake-parts_2": {
+ "inputs": {
+ "nixpkgs-lib": [
+ "nix",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1733312601,
+ "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "type": "github"
+ }
+ },
+ "git-hooks-nix": {
+ "inputs": {
+ "flake-compat": [
+ "nix"
+ ],
+ "gitignore": [
+ "nix"
+ ],
+ "nixpkgs": [
+ "nix",
+ "nixpkgs"
+ ],
+ "nixpkgs-stable": [
+ "nix",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1734279981,
+ "narHash": "sha256-NdaCraHPp8iYMWzdXAt5Nv6sA3MUzlCiGiR586TCwo0=",
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "rev": "aa9f40c906904ebd83da78e7f328cd8aeaeae785",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "type": "github"
+ }
+ },
+ "mk-naked-shell": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1681286841,
+ "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=",
+ "owner": "90-008",
+ "repo": "mk-naked-shell",
+ "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd",
+ "type": "github"
+ },
+ "original": {
+ "owner": "90-008",
+ "repo": "mk-naked-shell",
+ "type": "github"
+ }
+ },
+ "nix": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "flake-parts": "flake-parts_2",
+ "git-hooks-nix": "git-hooks-nix",
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "nixpkgs-23-11": "nixpkgs-23-11",
+ "nixpkgs-regression": "nixpkgs-regression"
+ },
+ "locked": {
+ "lastModified": 1772224943,
+ "narHash": "sha256-jJIlRLPPVYu860MVFx4gsRx3sskmLDSRWXXue5tYncw=",
+ "owner": "NixOS",
+ "repo": "nix",
+ "rev": "0acd0566e85e4597269482824711bcde7b518600",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "nix",
+ "type": "github"
+ }
+ },
+ "nix-cargo-integration": {
+ "inputs": {
+ "crane": "crane",
+ "dream2nix": "dream2nix",
+ "mk-naked-shell": "mk-naked-shell",
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "parts": "parts",
+ "rust-overlay": "rust-overlay",
+ "treefmt": "treefmt"
+ },
+ "locked": {
+ "lastModified": 1772260057,
+ "narHash": "sha256-NaUqM0i6XIGdgRNxxQ9sfgCAVeE2Ko9rz7e19RsNUKw=",
+ "owner": "90-008",
+ "repo": "nix-cargo-integration",
+ "rev": "c783c5dff02c06f2af6226d4dd4d494542d0a4d2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "90-008",
+ "repo": "nix-cargo-integration",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1772198003,
+ "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=",
+ "owner": "NixOS",
"repo": "nixpkgs",
+ "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs-23-11": {
+ "locked": {
+ "lastModified": 1717159533,
+ "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
+ "type": "github"
+ }
+ },
+ "nixpkgs-lib": {
+ "locked": {
+ "lastModified": 1769909678,
+ "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "rev": "72716169fe93074c333e8d0173151350670b824c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "type": "github"
+ }
+ },
+ "nixpkgs-regression": {
+ "locked": {
+ "lastModified": 1643052045,
+ "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+ "type": "github"
+ }
+ },
+ "parts": {
+ "inputs": {
+ "nixpkgs-lib": [
+ "nix-cargo-integration",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1769996383,
+ "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "type": "github"
+ }
+ },
+ "purescript-overlay": {
+ "inputs": {
+ "flake-compat": "flake-compat_2",
+ "nixpkgs": [
+ "nix-cargo-integration",
+ "dream2nix",
+ "nixpkgs"
+ ],
+ "slimlock": "slimlock"
+ },
+ "locked": {
+ "lastModified": 1728546539,
+ "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=",
+ "owner": "thomashoneyman",
+ "repo": "purescript-overlay",
+ "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4",
+ "type": "github"
+ },
+ "original": {
+ "owner": "thomashoneyman",
+ "repo": "purescript-overlay",
+ "type": "github"
+ }
+ },
+ "pyproject-nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nix-cargo-integration",
+ "dream2nix",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1763017646,
+ "narHash": "sha256-Z+R2lveIp6Skn1VPH3taQIuMhABg1IizJd8oVdmdHsQ=",
+ "owner": "pyproject-nix",
+ "repo": "pyproject.nix",
+ "rev": "47bd6f296502842643078d66128f7b5e5370790c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "pyproject.nix",
"type": "github"
}
},
"root": {
"inputs": {
- "nixpkgs": "nixpkgs",
- "systems": "systems"
+ "flake-parts": "flake-parts",
+ "nix": "nix",
+ "nix-cargo-integration": "nix-cargo-integration",
+ "nixpkgs": "nixpkgs"
}
},
- "systems": {
+ "rust-overlay": {
+ "inputs": {
+ "nixpkgs": [
+ "nix-cargo-integration",
+ "nixpkgs"
+ ]
+ },
"locked": {
- "lastModified": 1681028828,
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
- "owner": "nix-systems",
- "repo": "default",
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "lastModified": 1772247314,
+ "narHash": "sha256-x6IFQ9bL7YYfW2m2z8D3Em2YtAA3HE8kiCFwai2fwrw=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "a1ab5e89ab12e1a37c0b264af6386a7472d68a15",
"type": "github"
},
"original": {
- "owner": "nix-systems",
- "repo": "default",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ },
+ "slimlock": {
+ "inputs": {
+ "nixpkgs": [
+ "nix-cargo-integration",
+ "dream2nix",
+ "purescript-overlay",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1688756706,
+ "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=",
+ "owner": "thomashoneyman",
+ "repo": "slimlock",
+ "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "thomashoneyman",
+ "repo": "slimlock",
+ "type": "github"
+ }
+ },
+ "treefmt": {
+ "inputs": {
+ "nixpkgs": [
+ "nix-cargo-integration",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1770228511,
+ "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "treefmt-nix",
"type": "github"
}
}
diff --git a/flake.nix b/flake.nix
index f52bd4c..a8295dd 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,128 +1,227 @@
{
- description = "rust wrapper for libnix";
+ description = "Rust bindings for the Nix C API";
inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
- systems.url = "github:nix-systems/default";
+ flake-parts.url = "github:hercules-ci/flake-parts";
+ nix.url = "github:NixOS/nix";
+ 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";
};
- outputs = {
- self,
- nixpkgs,
- ...
- } @ inputs: let
- systems = import inputs.systems;
-
- mkPkgs = system: repo:
- import repo {
- inherit system;
- allowUnfree = false;
- allowBroken = false;
- overlays = builtins.attrValues self.overlays or {};
- };
-
- forAllSystems = f:
- nixpkgs.lib.genAttrs systems (system:
- f rec {
- inherit system;
- inherit (pkgs) lib;
- pkgs = mkPkgs system nixpkgs;
- });
- in {
- overlays.default = self: super: {
- libclang = super.llvmPackages_21.libclang;
- };
-
- devShells = forAllSystems (
- {
- pkgs,
+ outputs =
+ inputs@{ flake-parts, ... }:
+ flake-parts.lib.mkFlake { inherit inputs; } (
+ toplevel@{
lib,
...
- }: {
- default = let
- nixForBindings = pkgs.nixVersions.nix_2_32;
- inherit (pkgs.rustc) llvmPackages;
- in
- pkgs.mkShell rec {
- name = "nixide";
- shell = "${pkgs.bash}/bin/bash";
- strictDeps = true;
+ }:
+ let
+ /**
+ Makes perSystem.nix-bindings-rust available.
+ */
+ flake-parts-modules.basic =
+ {
+ flake-parts-lib,
+ ...
+ }:
+ {
+ _file = ./flake.nix;
+ imports = [ ./input-propagation-workaround.nix ];
+ options.perSystem = flake-parts-lib.mkPerSystemOption (
+ { pkgs, ... }:
+ {
+ options.nix-bindings-rust = {
+ nixPackage = lib.mkOption {
+ type = lib.types.package;
+ default = pkgs.nix;
+ defaultText = lib.literalMD "pkgs.nix";
+ description = ''
+ The Nix package to use when building the `nix-bindings-...` crates.
+ '';
+ };
+ nciBuildConfig = lib.mkOption {
+ type = lib.types.deferredModule;
+ description = ''
+ A module to load into your nix-cargo-integration
+ [`perSystem.nci.projects..depsDrvConfig`](https://flake.parts/options/nix-cargo-integration.html#opt-perSystem.nci.projects._name_.depsDrvConfig) or similar such options.
- # packages we need at runtime
- packages = with pkgs; [
- rustc
- llvmPackages.lld
- lldb
+ This provides common build configuration (pkg-config, libclang, etc.) and
+ automatically adds Nix C library build inputs based on which nix-bindings
+ crates are *direct* dependencies of your crate.
- cargo
- cargo-c
- cargo-llvm-cov
- cargo-nextest
+ To disable automatic build input detection:
+ ```nix
+ nix-bindings-rust.inputPropagationWorkaround.enable = false;
+ ```
- rust-analyzer-unwrapped
- (rustfmt.override {asNightly = true;})
- clippy
- taplo
- ];
+ Example:
+ ```nix
+ perSystem = perSystem@{ config, ... }: {
+ nci.projects."my_project".drvConfig = {
+ imports = [ perSystem.config.nix-bindings-rust.nciBuildConfig ];
+ };
+ }
+ ```
+ '';
+ };
+ };
+ config.nix-bindings-rust = {
+ nciBuildConfig = {
+ mkDerivation = rec {
+ buildInputs = [
+ # stdbool.h
+ pkgs.stdenv.cc
+ ];
+ nativeBuildInputs = [
+ pkgs.pkg-config
+ ];
+ # bindgen uses clang to generate bindings, but it doesn't know where to
+ # find our stdenv cc's headers, so when it's gcc, we need to tell it.
+ postConfigure = lib.optionalString pkgs.stdenv.cc.isGNU ''
+ source ${./bindgen-gcc.sh}
+ '';
+ shellHook = postConfigure;
+ };
+ # NOTE: duplicated in flake.nix devShell
+ env = {
+ LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.buildPackages.llvmPackages.clang-unwrapped ];
+ BINDGEN_EXTRA_CLANG_ARGS =
+ # Work around missing [[deprecated]] in clang
+ "-x c++ -std=c++2a";
+ }
+ // lib.optionalAttrs pkgs.stdenv.cc.isGNU {
+ # Avoid cc wrapper, because we only need to add the compiler/"system" dirs
+ NIX_CC_UNWRAPPED = "${pkgs.stdenv.cc.cc}/bin/gcc";
+ };
+ };
+ };
+ }
+ );
+ };
- # packages we need at build time
- nativeBuildInputs = with pkgs; [
- pkg-config
- glibc.dev
- nixForBindings.dev
+ /**
+ A flake-parts module for dependents to import. Also dogfooded locally
+ (extra, not required for normal CI).
- rustPlatform.bindgenHook
- ];
+ Adds flake checks that test the nix-bindings crates with the
+ dependent's nix package.
- # packages we link against
- buildInputs = with pkgs; [
- stdenv.cc
+ See https://github.com/nixops4/nix-bindings-rust?tab=readme-ov-file#integration-with-nix-projects
+ */
+ flake-parts-modules.tested =
+ # Consumer toplevel
+ { ... }:
+ {
+ _file = ./flake.nix;
+ imports = [ flake-parts-modules.basic ];
+ config.perSystem =
+ # Consumer perSystem
+ {
+ lib,
+ config,
+ system,
+ pkgs,
+ ...
+ }:
+ let
+ # nix-bindings-rust's perSystem, but with the consumer's `pkgs`
+ nix-bindings-rust-perSystemConfig =
+ # Extending our own perSystem, not the consumer's perSystem!
+ toplevel.config.partitions.testing-support.module.nix-bindings-rust.internalWithSystem system
+ ({ extendModules, ... }: extendModules)
+ {
+ modules = [
+ {
+ config = {
+ # Overriding our `perSystem` to use the consumer's `pkgs`
+ _module.args.pkgs = lib.mkForce pkgs;
+ # ... and `nixPackage`
+ nix-bindings-rust.nixPackage = lib.mkForce config.nix-bindings-rust.nixPackage;
+ };
+ }
+ ];
+ };
+ in
+ {
+ key = "nix-bindings-rust-add-checks";
+ # Exclude clippy checks; those are part of this repo's local CI.
+ # This module is for dependents (and local dogfooding), which
+ # don't need to run clippy on nix-bindings-rust.
+ config.checks = lib.concatMapAttrs (
+ k: v:
+ lib.optionalAttrs (lib.strings.hasPrefix "nix-bindings-" k && !lib.strings.hasSuffix "-clippy" k) {
+ "dependency-${k}" = v;
+ }
+ ) nix-bindings-rust-perSystemConfig.config.checks;
+ };
+ };
- nixForBindings
- ];
+ flake-parts-modules.default = flake-parts-modules.tested;
- # bindgen uses clang to generate bindings, but it doesn't know where to
- # find our stdenv cc's headers, so when it's gcc, we need to tell it.
- postConfigure = lib.optionalString pkgs.stdenv.cc.isGNU ''
- #!/usr/bin/env bash
- # REF: https://github.com/nixops4/nix-bindings-rust/blob/main/bindgen-gcc.sh
- # Rust bindgen uses Clang to generate bindings, but that means that it can't
- # find the "system" or compiler headers when the stdenv compiler is GCC.
- # This script tells it where to find them.
+ in
+ {
+ imports = [
+ inputs.nix-cargo-integration.flakeModule
+ inputs.flake-parts.flakeModules.partitions
+ inputs.flake-parts.flakeModules.modules
+ # dogfood
+ flake-parts-modules.tested
+ ./nci.nix
+ ];
+ systems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ "x86_64-darwin"
+ "aarch64-darwin"
+ ];
+ perSystem =
+ {
+ inputs',
+ ...
+ }:
+ {
+ packages.nix = inputs'.nix.packages.nix;
+ };
- echo "Extending BINDGEN_EXTRA_CLANG_ARGS with system include paths..." 2>&1
- BINDGEN_EXTRA_CLANG_ARGS="$${BINDGEN_EXTRA_CLANG_ARGS:-}"
- export BINDGEN_EXTRA_CLANG_ARGS
- include_paths=$(
- echo | $NIX_CC_UNWRAPPED -v -E -x c - 2>&1 \
- | awk '/#include <...> search starts here:/{flag=1;next} \
- /End of search list./{flag=0} \
- flag==1 {print $1}'
- )
- for path in $include_paths; do
- echo " - $path" 2>&1
- BINDGEN_EXTRA_CLANG_ARGS="$BINDGEN_EXTRA_CLANG_ARGS -I$path"
- done
- '';
+ partitionedAttrs.devShells = "dev";
+ partitionedAttrs.checks = "dev";
+ partitionedAttrs.herculesCI = "dev";
+ # Packages are basically just checks in this project; a library by
+ # itself is not useful. That's just not how the Rust integration works.
+ # By taking `packages` from `dev` we benefit from this dev-only definition:
+ # nix-bindings-rust.nixPackage = inputs'.nix.packages.default;
+ partitionedAttrs.packages = "dev";
- shellHook = postConfigure;
+ partitions.dev.extraInputsFlake = ./dev;
+ partitions.dev.module = {
+ imports = [ ./dev/flake-module.nix ];
+ };
- env = let
- inherit (llvmPackages) llvm libclang;
- in {
- LD_LIBRARY_PATH = builtins.toString (lib.makeLibraryPath buildInputs);
- LIBCLANG_PATH = "${libclang.lib}/lib";
-
- RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}";
- BINDGEN_EXTRA_CLANG_ARGS = "--sysroot=${pkgs.glibc.dev}";
-
- # `cargo-llvm-cov` reads these environment variables to find these binaries,
- # which are needed to run the tests
- LLVM_COV = "${llvm}/bin/llvm-cov";
- LLVM_PROFDATA = "${llvm}/bin/llvm-profdata";
+ # A partition that doesn't dogfood the flake-parts-modules.tested module
+ # so that we can actually retrieve `checks` without infinite recursions
+ # from trying to include the dogfooded attrs.
+ partitions.testing-support.module =
+ { withSystem, ... }:
+ {
+ # Make a clean withSystem available for consumers
+ options.nix-bindings-rust.internalWithSystem = lib.mkOption { internal = true; };
+ config = {
+ nix-bindings-rust.internalWithSystem = withSystem;
+ perSystem = {
+ # Remove dogfooded checks. This configuration's checks are
+ # *consumed* by nix-bindings-rust-add-checks, so they should
+ # *NOT* also be *produced* by it.
+ disabledModules = [ { key = "nix-bindings-rust-add-checks"; } ];
+ };
};
};
+
+ # flake output attributes
+ flake = {
+ modules.flake = flake-parts-modules;
+ };
}
);
- };
}
diff --git a/input-propagation-workaround.nix b/input-propagation-workaround.nix
new file mode 100644
index 0000000..c3ac92f
--- /dev/null
+++ b/input-propagation-workaround.nix
@@ -0,0 +1,143 @@
+# Workaround for missing native input propagation in nix-cargo-integration
+#
+# Automatically adds Nix C library build inputs based on which nix-bindings
+# crates are direct dependencies of the crate being built. The mapping is
+# recursive, so depending on nix-bindings-flake will also bring in the
+# transitive C library dependencies (nix-fetchers-c, nix-expr-c, etc.).
+#
+# Note: For multi-crate workspaces, if your crate A depends on your crate B
+# which depends on nix-bindings, you'll need to add an A -> B mapping to
+# `crateInputMapping` so that A also gets B's nix-bindings inputs.
+{
+ perSystem =
+ {
+ lib,
+ config,
+ pkgs,
+ ...
+ }:
+ let
+ cfg = config.nix-bindings-rust.inputPropagationWorkaround;
+ nixPackage = config.nix-bindings-rust.nixPackage;
+
+ nixLibs =
+ if nixPackage ? libs then
+ nixPackage.libs
+ else
+ # Fallback for older Nix versions without split libs
+ {
+ nix-util-c = nixPackage;
+ nix-store-c = nixPackage;
+ nix-expr-c = nixPackage;
+ nix-fetchers-c = nixPackage;
+ nix-flake-c = nixPackage;
+ };
+
+ # A module for nciBuildConfig that sets buildInputs based on nix-bindings dependencies.
+ # Uses options inspection to detect drvConfig vs depsDrvConfig context.
+ workaroundModule =
+ {
+ lib,
+ config,
+ options,
+ ...
+ }:
+ let
+ # rust-cargo-lock exists in drvConfig but not depsDrvConfig
+ isDrvConfig = options ? rust-cargo-lock;
+
+ dreamLock = config.rust-cargo-lock.dreamLock;
+ depsList = dreamLock.dependencies.${config.name}.${config.version} or [ ];
+
+ # Convert list of deps to attrset keyed by name for efficient lookup
+ deps = builtins.listToAttrs (
+ map (dep: {
+ name = dep.name;
+ value = dep;
+ }) depsList
+ );
+
+ # Inputs for the crate itself if it's in the mapping
+ selfInputs = cfg.crateInputMapping.${config.name} or [ ];
+
+ # Inputs for direct dependencies that have mappings
+ depInputs = lib.concatLists (lib.attrValues (lib.intersectAttrs deps cfg.crateInputMapping));
+
+ allInputs = selfInputs ++ depInputs;
+ in
+ {
+ config = lib.optionalAttrs isDrvConfig {
+ mkDerivation.buildInputs = allInputs;
+ rust-crane.depsDrv.mkDerivation.buildInputs = allInputs;
+ };
+ };
+ in
+ {
+ options.nix-bindings-rust.inputPropagationWorkaround = {
+ enable = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = ''
+ Whether to automatically add Nix C library build inputs based on
+ which nix-bindings crates are direct dependencies.
+
+ Set to `false` to disable automatic detection and specify buildInputs manually.
+ '';
+ };
+
+ crateInputMapping = lib.mkOption {
+ type = lib.types.lazyAttrsOf (lib.types.listOf lib.types.package);
+ description = ''
+ Mapping from crate names to build inputs. Entries can reference
+ other entries for transitive dependencies.
+
+ The input propagation workaround can see direct dependencies, so
+ if you have `my-crate -> nix-bindings`, that works out of the box.
+ If you have `my-other-crate -> my-crate -> nix-bindings`, then you
+ need to specify `my-other-crate -> my-crate` as follows:
+
+ ```nix
+ nix-bindings-rust.inputPropagationWorkaround.crateInputMapping."my-other-crate" =
+ config.nix-bindings-rust.inputPropagationWorkaround.crateInputMapping."my-crate";
+ ```
+ '';
+ default = { };
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ nix-bindings-rust.inputPropagationWorkaround.crateInputMapping = {
+ # -sys crates with their transitive dependencies
+ "nix-bindings-bdwgc-sys" = [ pkgs.boehmgc ];
+ "nix-bindings-util-sys" = [ nixLibs.nix-util-c.dev ];
+ "nix-bindings-store-sys" = [
+ nixLibs.nix-store-c.dev
+ ]
+ ++ cfg.crateInputMapping."nix-bindings-util-sys";
+ "nix-bindings-expr-sys" = [
+ nixLibs.nix-expr-c.dev
+ ]
+ ++ cfg.crateInputMapping."nix-bindings-store-sys"
+ ++ cfg.crateInputMapping."nix-bindings-bdwgc-sys";
+ "nix-bindings-fetchers-sys" = [
+ nixLibs.nix-fetchers-c.dev
+ ]
+ ++ cfg.crateInputMapping."nix-bindings-expr-sys";
+ "nix-bindings-flake-sys" = [
+ nixLibs.nix-flake-c.dev
+ ]
+ ++ cfg.crateInputMapping."nix-bindings-fetchers-sys"
+ ++ cfg.crateInputMapping."nix-bindings-bdwgc-sys";
+ # High-level crates reference their -sys counterparts
+ "nix-bindings-bdwgc" = cfg.crateInputMapping."nix-bindings-bdwgc-sys";
+ "nix-bindings-util" = cfg.crateInputMapping."nix-bindings-util-sys";
+ "nix-bindings-store" = cfg.crateInputMapping."nix-bindings-store-sys";
+ "nix-bindings-expr" = cfg.crateInputMapping."nix-bindings-expr-sys";
+ "nix-bindings-fetchers" = cfg.crateInputMapping."nix-bindings-fetchers-sys";
+ "nix-bindings-flake" = cfg.crateInputMapping."nix-bindings-flake-sys";
+ };
+
+ nix-bindings-rust.nciBuildConfig.imports = [ workaroundModule ];
+ };
+ };
+}
diff --git a/nci.nix b/nci.nix
new file mode 100644
index 0000000..351ac77
--- /dev/null
+++ b/nci.nix
@@ -0,0 +1,72 @@
+{
+ perSystem =
+ { config, ... }:
+ let
+ cfg = config.nix-bindings-rust;
+ in
+ {
+ # https://flake.parts/options/nix-cargo-integration
+ nci.projects.nix-bindings = {
+ path = ./.;
+ profiles = {
+ dev.drvConfig.env.RUSTFLAGS = "-D warnings";
+ release.runTests = true;
+ };
+ drvConfig = {
+ imports = [
+ # Downstream projects import this into depsDrvConfig instead
+ cfg.nciBuildConfig
+ ];
+ # Extra settings for running the tests
+ mkDerivation = {
+ # Prepare the environment for Nix to work.
+ # Nix does not provide a suitable environment for running itself in
+ # the sandbox - not by default. We configure it to use a relocated store.
+ preCheck = ''
+ # nix needs a home directory
+ export HOME="$(mktemp -d $TMPDIR/home.XXXXXX)"
+
+ # configure a relocated store
+ store_data=$(mktemp -d $TMPDIR/store-data.XXXXXX)
+ export NIX_REMOTE="$store_data"
+ export NIX_BUILD_HOOK=
+ export NIX_CONF_DIR=$store_data/etc
+ export NIX_LOCALSTATE_DIR=$store_data/nix/var
+ export NIX_LOG_DIR=$store_data/nix/var/log/nix
+ export NIX_STATE_DIR=$store_data/nix/var/nix
+
+ echo "Configuring relocated store at $NIX_REMOTE..."
+
+ # Create nix.conf with experimental features enabled
+ mkdir -p "$NIX_CONF_DIR"
+ echo "experimental-features = ca-derivations flakes" > "$NIX_CONF_DIR/nix.conf"
+
+ # Init ahead of time, because concurrent initialization is flaky
+ ${cfg.nixPackage}/bin/nix-store --init
+
+ echo "Store initialized."
+ '';
+ };
+ };
+ };
+ nci.crates.nix-bindings-store =
+ let
+ addHarmoniaProfile = ''
+ cat >> Cargo.toml <<'EOF'
+
+ [profile.harmonia]
+ inherits = "release"
+ EOF
+ '';
+ in
+ {
+ profiles.harmonia = {
+ features = [ "harmonia" ];
+ runTests = true;
+ # Add harmonia profile to Cargo.toml for both deps and main builds
+ depsDrvConfig.mkDerivation.postPatch = addHarmoniaProfile;
+ drvConfig.mkDerivation.postPatch = addHarmoniaProfile;
+ };
+ };
+ };
+}
diff --git a/nix-bindings-bdwgc-sys/Cargo.toml b/nix-bindings-bdwgc-sys/Cargo.toml
new file mode 100644
index 0000000..488f41e
--- /dev/null
+++ b/nix-bindings-bdwgc-sys/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "nix-bindings-bdwgc-sys"
+version = "0.2.1"
+edition = "2021"
+build = "build.rs"
+license = "LGPL-2.1"
+description = "Low-level FFI bindings to the Boehm-Demers-Weiser garbage collector"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_bdwgc_sys/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+
+[build-dependencies]
+bindgen = "0.69"
+pkg-config = "0.3"
diff --git a/nix-bindings-bdwgc-sys/README.md b/nix-bindings-bdwgc-sys/README.md
new file mode 100644
index 0000000..b49b5b4
--- /dev/null
+++ b/nix-bindings-bdwgc-sys/README.md
@@ -0,0 +1,10 @@
+# nix-bindings-bdwgc-sys
+
+This crate contains generated bindings for the Boehm-Demers-Weiser garbage collector (`bdw-gc`).
+**You should not have to use this crate directly,** and so you should probably not add it to your dependencies.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_bdwgc_sys/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-bdwgc-sys/build.rs b/nix-bindings-bdwgc-sys/build.rs
new file mode 100644
index 0000000..aed2a8c
--- /dev/null
+++ b/nix-bindings-bdwgc-sys/build.rs
@@ -0,0 +1,27 @@
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+ println!("cargo:rerun-if-changed=include/bdwgc.h");
+
+ let mut args = Vec::new();
+ for path in pkg_config::probe_library("bdw-gc")
+ .unwrap()
+ .include_paths
+ .iter()
+ {
+ args.push(format!("-I{}", path.to_str().unwrap()));
+ }
+
+ let bindings = bindgen::Builder::default()
+ .header("include/bdwgc.h")
+ .clang_args(args)
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+ .generate()
+ .expect("Unable to generate bindings");
+
+ let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+ bindings
+ .write_to_file(out_path.join("bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
diff --git a/nix-bindings-bdwgc-sys/include/bdwgc.h b/nix-bindings-bdwgc-sys/include/bdwgc.h
new file mode 100644
index 0000000..2a70434
--- /dev/null
+++ b/nix-bindings-bdwgc-sys/include/bdwgc.h
@@ -0,0 +1,2 @@
+#define GC_THREADS
+#include
diff --git a/nix-bindings-bdwgc-sys/src/lib.rs b/nix-bindings-bdwgc-sys/src/lib.rs
new file mode 100644
index 0000000..e722c20
--- /dev/null
+++ b/nix-bindings-bdwgc-sys/src/lib.rs
@@ -0,0 +1,30 @@
+//! Raw bindings to Nix C API
+//!
+//! This crate contains automatically generated bindings from the Nix C headers.
+//! The bindings are generated by bindgen and include C-style naming conventions
+//! and documentation comments that don't always conform to Rust standards.
+//!
+//! Normally you don't have to use this crate directly.
+
+// This file must only contain generated code, so that the module-level
+// #![allow(...)] attributes don't suppress warnings in hand-written code.
+// If you need to add hand-written code, use a submodule to isolate the
+// generated code. See:
+// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9
+
+// Standard bindgen suppressions for C naming conventions
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// Clippy suppressions for generated C bindings
+// bindgen doesn't generate safety docs
+#![allow(clippy::missing_safety_doc)]
+// Rustdoc suppressions for generated C documentation
+// The C headers contain Doxygen-style documentation that doesn't translate
+// well to Rust's rustdoc format, causing various warnings:
+#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve
+#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs
+#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like
+#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/nix-bindings-expr-sys/Cargo.toml b/nix-bindings-expr-sys/Cargo.toml
new file mode 100644
index 0000000..0cf3e12
--- /dev/null
+++ b/nix-bindings-expr-sys/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "nix-bindings-expr-sys"
+version = "0.2.1"
+edition = "2021"
+build = "build.rs"
+license = "LGPL-2.1"
+description = "Low-level FFI bindings to the Nix expression evaluator"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_expr_sys/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" }
+nix-bindings-store-sys = { path = "../nix-bindings-store-sys", version = "0.2.1" }
+
+[build-dependencies]
+bindgen = "0.69"
+pkg-config = "0.3"
diff --git a/nix-bindings-expr-sys/README.md b/nix-bindings-expr-sys/README.md
new file mode 100644
index 0000000..9fdb3a0
--- /dev/null
+++ b/nix-bindings-expr-sys/README.md
@@ -0,0 +1,11 @@
+# nix-bindings-expr-sys
+
+This crate contains generated bindings for the Nix C API (`nix-expr-c`).
+**You should not have to use this crate directly,** and so you should probably not add it to your dependencies.
+Instead, use the `nix-bindings-expr` crate, which _should_ be sufficient.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_expr_sys/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-expr-sys/build.rs b/nix-bindings-expr-sys/build.rs
new file mode 100644
index 0000000..6e0eed2
--- /dev/null
+++ b/nix-bindings-expr-sys/build.rs
@@ -0,0 +1,42 @@
+use std::path::PathBuf;
+
+#[derive(Debug)]
+struct StripNixPrefix;
+
+impl bindgen::callbacks::ParseCallbacks for StripNixPrefix {
+ fn item_name(&self, name: &str) -> Option {
+ name.strip_prefix("nix_").map(String::from)
+ }
+}
+
+fn main() {
+ println!("cargo:rerun-if-changed=include/nix-c-expr.h");
+ println!("cargo:rustc-link-lib=nixexprc");
+
+ let mut args = Vec::new();
+ for path in pkg_config::probe_library("nix-expr-c")
+ .unwrap()
+ .include_paths
+ .iter()
+ {
+ args.push(format!("-I{}", path.to_str().unwrap()));
+ }
+
+ let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());
+
+ let bindings = bindgen::Builder::default()
+ .header("include/nix-c-expr.h")
+ .clang_args(args)
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+ .parse_callbacks(Box::new(StripNixPrefix))
+ // Blocklist symbols from nix-bindings-util-sys
+ .blocklist_file(".*nix_api_util\\.h")
+ // Blocklist symbols from nix-bindings-store-sys
+ .blocklist_file(".*nix_api_store\\.h")
+ .generate()
+ .expect("Unable to generate bindings");
+
+ bindings
+ .write_to_file(out_path.join("bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
diff --git a/nix-bindings-expr-sys/include/nix-c-expr.h b/nix-bindings-expr-sys/include/nix-c-expr.h
new file mode 100644
index 0000000..c2649a4
--- /dev/null
+++ b/nix-bindings-expr-sys/include/nix-c-expr.h
@@ -0,0 +1,2 @@
+#include
+#include
diff --git a/nix-bindings-expr-sys/src/lib.rs b/nix-bindings-expr-sys/src/lib.rs
new file mode 100644
index 0000000..739f706
--- /dev/null
+++ b/nix-bindings-expr-sys/src/lib.rs
@@ -0,0 +1,34 @@
+//! Raw bindings to Nix C API
+//!
+//! This crate contains automatically generated bindings from the Nix C headers.
+//! The bindings are generated by bindgen and include C-style naming conventions
+//! and documentation comments that don't always conform to Rust standards.
+//!
+//! Normally you don't have to use this crate directly.
+//! Instead use `nix-expr`.
+
+// This file must only contain generated code, so that the module-level
+// #![allow(...)] attributes don't suppress warnings in hand-written code.
+// If you need to add hand-written code, use a submodule to isolate the
+// generated code. See:
+// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9
+
+// Standard bindgen suppressions for C naming conventions
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// Clippy suppressions for generated C bindings
+// bindgen doesn't generate safety docs
+#![allow(clippy::missing_safety_doc)]
+// Rustdoc suppressions for generated C documentation
+// The C headers contain Doxygen-style documentation that doesn't translate
+// well to Rust's rustdoc format, causing various warnings:
+#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve
+#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs
+#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like
+#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes
+
+use nix_bindings_store_sys::*;
+use nix_bindings_util_sys::*;
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/nix-bindings-expr/Cargo.toml b/nix-bindings-expr/Cargo.toml
new file mode 100644
index 0000000..ef9a373
--- /dev/null
+++ b/nix-bindings-expr/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "nix-bindings-expr"
+version = "0.2.1"
+edition = "2021"
+build = "build.rs"
+license = "LGPL-2.1"
+description = "Rust bindings to Nix expression evaluator"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_expr/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+anyhow = "1.0"
+nix-bindings-store = { path = "../nix-bindings-store", version = "0.2.1" }
+nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" }
+nix-bindings-bdwgc-sys = { path = "../nix-bindings-bdwgc-sys", version = "0.2.1" }
+nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" }
+nix-bindings-store-sys = { path = "../nix-bindings-store-sys", version = "0.2.1" }
+nix-bindings-expr-sys = { path = "../nix-bindings-expr-sys", version = "0.2.1" }
+ctor = "0.2"
+tempfile = "3.10"
+cstr = "0.2"
+
+[build-dependencies]
+pkg-config = "0.3"
+nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" }
+
+[lints.rust]
+warnings = "deny"
+dead-code = "allow"
+
+[lints.clippy]
+type-complexity = "allow"
+# We're still trying to make Nix more thread-safe, want forward-compat
+arc-with-non-send-sync = "allow"
diff --git a/nix-bindings-expr/README.md b/nix-bindings-expr/README.md
new file mode 100644
index 0000000..cb92996
--- /dev/null
+++ b/nix-bindings-expr/README.md
@@ -0,0 +1,9 @@
+# nix-bindings-expr
+
+Rust bindings to the Nix expression evaluator.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_expr/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-expr/build.rs b/nix-bindings-expr/build.rs
new file mode 100644
index 0000000..f56bc3e
--- /dev/null
+++ b/nix-bindings-expr/build.rs
@@ -0,0 +1,6 @@
+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", "2.34.0pre"]);
+}
diff --git a/nix-bindings-expr/src/eval_state.rs b/nix-bindings-expr/src/eval_state.rs
new file mode 100644
index 0000000..5d8599f
--- /dev/null
+++ b/nix-bindings-expr/src/eval_state.rs
@@ -0,0 +1,2925 @@
+//! # 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 nix_bindings_bdwgc_sys as gc;
+use nix_bindings_expr_sys as raw;
+use nix_bindings_store::path::StorePath;
+use nix_bindings_store::store::{Store, StoreWeak};
+use nix_bindings_store_sys as raw_store;
+use nix_bindings_util::context::Context;
+use nix_bindings_util::string_return::{
+ callback_get_result_string, callback_get_result_string_data,
+};
+use nix_bindings_util::{check_call, check_call_opt_key, result_string_init};
+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, LazyLock, Weak};
+
+static INIT: LazyLock> = LazyLock::new(|| unsafe {
+ gc::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.
+ ///
+ /// See also: [Shared Evaluation State](Value#shared-evaluation-state)
+ #[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.
+ ///
+ /// See also: [Shared Evaluation State](Value#shared-evaluation-state)
+ #[doc(alias = "type_of")]
+ #[doc(alias = "value_type_lazy")]
+ #[doc(alias = "nix_get_type")]
+ #[doc(alias = "get_type")]
+ #[doc(alias = "nix_value_type")]
+ 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_store::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 {
+ gc::GC_unregister_my_thread();
+ }
+ }
+ }
+}
+
+fn gc_register_my_thread_do_it() -> Result<()> {
+ unsafe {
+ let mut sb: gc::GC_stack_base = gc::GC_stack_base {
+ mem_base: null_mut(),
+ };
+ let r = gc::GC_get_stack_base(&mut sb);
+ if r as u32 != gc::GC_SUCCESS {
+ Err(anyhow::format_err!("GC_get_stack_base failed: {}", r))?;
+ }
+ gc::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 = gc::GC_thread_is_registered();
+ if already_done != 0 {
+ return Ok(ThreadRegistrationGuard {
+ must_unregister: false,
+ });
+ }
+ gc_register_my_thread_do_it()?;
+ Ok(ThreadRegistrationGuard {
+ must_unregister: true,
+ })
+ }
+}
+
+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();
+ }
+
+ #[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/lib.rs b/nix-bindings-expr/src/lib.rs
new file mode 100644
index 0000000..41c4b39
--- /dev/null
+++ b/nix-bindings-expr/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod eval_state;
+pub mod primop;
+pub mod value;
diff --git a/nix-bindings-expr/src/primop.rs b/nix-bindings-expr/src/primop.rs
new file mode 100644
index 0000000..1504405
--- /dev/null
+++ b/nix-bindings-expr/src/primop.rs
@@ -0,0 +1,163 @@
+use crate::eval_state::{EvalState, EvalStateWeak};
+use crate::value::Value;
+use anyhow::Result;
+use nix_bindings_expr_sys as raw;
+use nix_bindings_util::check_call;
+use nix_bindings_util_sys as raw_util;
+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
+ /// builtins. Nonetheless, a name is required for documentation purposes, e.g.
+ /// :doc in the repl.
+ pub name: &'a CStr,
+
+ /// Documentation for the primop. This is displayed in the repl when using
+ /// :doc. The format is markdown.
+ pub doc: &'a CStr,
+
+ /// The number of arguments the function takes, as well as names for the
+ /// arguments, to be presented in the documentation (if applicable, e.g.
+ /// :doc in the repl).
+ pub args: [&'a CStr; N],
+}
+
+pub struct PrimOp {
+ pub(crate) ptr: *mut raw::PrimOp,
+}
+impl Drop for PrimOp {
+ fn drop(&mut self) {
+ unsafe {
+ raw::gc_decref(null_mut(), self.ptr as *mut c_void);
+ }
+ }
+}
+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,
+ f: Box Result>,
+ ) -> Result {
+ assert!(N != 0);
+
+ let mut args = Vec::new();
+ for arg in meta.args {
+ args.push(arg.as_ptr());
+ }
+ args.push(null());
+
+ // Primops weren't meant to be dynamically created, as of writing.
+ // This leaks, and so do the primop fields in Nix internally.
+ let user_data = {
+ // We'll be leaking this Box.
+ // TODO: Use the GC with finalizer, if possible.
+ let user_data = ManuallyDrop::new(Box::new(PrimOpContext {
+ arity: N,
+ function: Box::new(move |eval_state, args| f(eval_state, args.try_into().unwrap())),
+ eval_state: eval_state.weak_ref(),
+ }));
+ user_data.as_ref() as *const PrimOpContext as *mut c_void
+ };
+ let op = unsafe {
+ check_call!(raw::alloc_primop(
+ &mut eval_state.context,
+ FUNCTION_ADAPTER,
+ N as c_int,
+ meta.name.as_ptr(),
+ args.as_mut_ptr(), /* TODO add an extra const to bindings to avoid mut here. */
+ meta.doc.as_ptr(),
+ user_data
+ ))?
+ };
+ Ok(PrimOp { ptr: op })
+ }
+}
+
+/// The user_data for our Nix primops
+struct PrimOpContext {
+ arity: usize,
+ function: Box Result>,
+ eval_state: EvalStateWeak,
+}
+
+unsafe extern "C" fn function_adapter(
+ user_data: *mut ::std::os::raw::c_void,
+ context_out: *mut raw_util::c_context,
+ _state: *mut raw::EvalState,
+ args: *mut *mut raw::Value,
+ ret: *mut raw::Value,
+) {
+ let primop_info = (user_data as *const PrimOpContext).as_ref().unwrap();
+ let mut eval_state = primop_info.eval_state.upgrade().unwrap_or_else(|| {
+ panic!("Nix primop called after EvalState was dropped");
+ });
+ let args_raw_slice = unsafe { std::slice::from_raw_parts(args, primop_info.arity) };
+ let args_vec: Vec = args_raw_slice
+ .iter()
+ .map(|v| Value::new_borrowed(*v))
+ .collect();
+ let args_slice = args_vec.as_slice();
+
+ let r = primop_info.function.as_ref()(&mut eval_state, args_slice);
+
+ match r {
+ Ok(v) => unsafe {
+ 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, 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-expr/src/value.rs b/nix-bindings-expr/src/value.rs
new file mode 100644
index 0000000..68bb444
--- /dev/null
+++ b/nix-bindings-expr/src/value.rs
@@ -0,0 +1,141 @@
+pub mod __private;
+
+use nix_bindings_expr_sys as raw;
+use nix_bindings_util::{check_call, context::Context};
+use std::ptr::{null_mut, NonNull};
+
+// TODO: test: cloning a thunk does not duplicate the evaluation.
+
+pub type Int = i64;
+
+/// The type discriminator of a [`Value`] that has successfully evaluated to at least [weak head normal form](https://nix.dev/manual/nix/latest/language/evaluation.html?highlight=WHNF#values).
+///
+/// Typically acquired with [`EvalState::value_type`][`crate::eval_state::EvalState::value_type`]
+#[derive(Eq, PartialEq, Debug)]
+pub enum ValueType {
+ /// A Nix [attribute set](https://nix.dev/manual/nix/stable/language/types.html#type-attrs)
+ AttrSet,
+ /// A Nix [boolean](https://nix.dev/manual/nix/stable/language/types.html#type-bool)
+ Bool,
+ /// A Nix external value (mostly-opaque value for plugins, linked applications)
+ External,
+ /// A Nix [float](https://nix.dev/manual/nix/stable/language/types.html#type-float)
+ Float,
+ /// A Nix [function](https://nix.dev/manual/nix/stable/language/types.html#type-function)
+ Function,
+ /// A Nix [integer](https://nix.dev/manual/nix/stable/language/types.html#type-int)
+ Int,
+ /// A Nix [list](https://nix.dev/manual/nix/stable/language/types.html#type-list)
+ List,
+ /// A Nix [`null`](https://nix.dev/manual/nix/stable/language/types.html#type-null)
+ Null,
+ /// A Nix [path value](https://nix.dev/manual/nix/stable/language/types.html#type-path)
+ Path,
+ /// A Nix [string](https://nix.dev/manual/nix/stable/language/types.html#type-string)
+ String,
+ /// An unknown value, presumably from a new, partially unsupported version of Nix
+ Unknown,
+}
+
+impl ValueType {
+ /// Convert a raw value type to a [`ValueType`].
+ ///
+ /// Return [`None`] if the Value is still a thunk (i.e. not yet evaluated).
+ ///
+ /// Return `Some(`[`ValueType::Unknown`]`)` if the value type is not recognized.
+ pub(crate) fn from_raw(raw: raw::ValueType) -> Option {
+ match raw {
+ raw::ValueType_NIX_TYPE_ATTRS => Some(ValueType::AttrSet),
+ raw::ValueType_NIX_TYPE_BOOL => Some(ValueType::Bool),
+ raw::ValueType_NIX_TYPE_EXTERNAL => Some(ValueType::External),
+ raw::ValueType_NIX_TYPE_FLOAT => Some(ValueType::Float),
+ raw::ValueType_NIX_TYPE_FUNCTION => Some(ValueType::Function),
+ raw::ValueType_NIX_TYPE_INT => Some(ValueType::Int),
+ raw::ValueType_NIX_TYPE_LIST => Some(ValueType::List),
+ raw::ValueType_NIX_TYPE_NULL => Some(ValueType::Null),
+ raw::ValueType_NIX_TYPE_PATH => Some(ValueType::Path),
+ raw::ValueType_NIX_TYPE_STRING => Some(ValueType::String),
+
+ raw::ValueType_NIX_TYPE_THUNK => None,
+
+ // This would happen if a new type of value is added in Nix.
+ _ => Some(ValueType::Unknown),
+ }
+ }
+}
+
+/// A pointer to a [value](https://nix.dev/manual/nix/latest/language/types.html) or [thunk](https://nix.dev/manual/nix/2.31/language/evaluation.html?highlight=thunk#laziness), to be used with [`EvalState`][`crate::eval_state::EvalState`] methods.
+///
+/// # Shared Evaluation State
+///
+/// Multiple `Value` instances can reference the same underlying Nix value.
+/// This occurs when a `Value` is [cloned](Clone), or when multiple Nix
+/// expressions reference the same binding.
+///
+/// When any reference to a thunk is evaluated—whether through
+/// [`force`](crate::eval_state::EvalState::force), other `EvalState` methods,
+/// or indirectly as a consequence of evaluating something else—all references
+/// observe the evaluated result. This means
+/// [`value_type_unforced`](crate::eval_state::EvalState::value_type_unforced)
+/// can return `None` (thunk) initially but a specific type later, even without
+/// directly operating on that `Value`. The state will not regress back to a
+/// less determined state.
+pub struct Value {
+ inner: NonNull,
+}
+impl Value {
+ /// Take ownership of a new [`Value`].
+ ///
+ /// This does not call [`nix_bindings_util_sys::gc_incref`], but does call [`nix_bindings_util_sys::gc_decref`] when [dropped][`Drop`].
+ ///
+ /// # Safety
+ ///
+ /// The caller must ensure that the provided `inner` has a positive reference count, and that `inner` is not used after the returned `Value` is dropped.
+ pub(crate) unsafe fn new(inner: *mut raw::Value) -> Self {
+ Value {
+ inner: NonNull::new(inner).unwrap(),
+ }
+ }
+
+ /// Borrow a reference to a [`Value`].
+ ///
+ /// This calls [`nix_bindings_util_sys::value_incref`], and the returned Value will call [`nix_bindings_util_sys::value_decref`] when dropped.
+ ///
+ /// # Safety
+ ///
+ /// The caller must ensure that the provided `inner` has a positive reference count.
+ pub(crate) unsafe fn new_borrowed(inner: *mut raw::Value) -> Self {
+ let v = Value::new(inner);
+ unsafe { raw::value_incref(null_mut(), inner) };
+ v
+ }
+
+ /// # Safety
+ ///
+ /// The caller must ensure that the returned pointer is not used after the `Value` is dropped.
+ pub(crate) unsafe fn raw_ptr(&self) -> *mut raw::Value {
+ self.inner.as_ptr()
+ }
+}
+impl Drop for Value {
+ fn drop(&mut self) {
+ unsafe {
+ // ignoring error because the only failure mode is leaking memory
+ raw::value_decref(null_mut(), self.inner.as_ptr());
+ }
+ }
+}
+impl Clone for Value {
+ fn clone(&self) -> Self {
+ // TODO: Is it worth allocating a new Context here? Ideally cloning is cheap.
+ // this is very unlikely to error, and it is not recoverable
+ // Maybe try without, and try again with context to report details?
+ unsafe {
+ check_call!(raw::value_incref(&mut Context::new(), self.inner.as_ptr())).unwrap();
+ }
+ // can't return an error here, but we don't want to ignore the error either as it means we could use-after-free
+ Value { inner: self.inner }
+ }
+}
+
+// Tested in eval_state.rs
diff --git a/nix-bindings-expr/src/value/__private.rs b/nix-bindings-expr/src/value/__private.rs
new file mode 100644
index 0000000..770a217
--- /dev/null
+++ b/nix-bindings-expr/src/value/__private.rs
@@ -0,0 +1,26 @@
+//! Functions that are relevant for other bindings modules, but normally not end users.
+use super::Value;
+use nix_bindings_expr_sys as raw;
+
+/// Take ownership of a new [`Value`].
+///
+/// This does not call `nix_gc_incref`, but does call `nix_gc_decref` when dropped.
+///
+/// # Safety
+///
+/// The caller must ensure that the provided `ptr` has a positive reference count,
+/// and that `ptr` is not used after the returned `Value` is dropped.
+pub unsafe fn raw_value_new(ptr: *mut raw::Value) -> Value {
+ Value::new(ptr)
+}
+
+/// Borrow a reference to a [`Value`].
+///
+/// This calls `value_incref`, and the returned Value will call `value_decref` when dropped.
+///
+/// # Safety
+///
+/// The caller must ensure that the provided `ptr` has a positive reference count.
+pub unsafe fn raw_value_new_borrowed(ptr: *mut raw::Value) -> Value {
+ Value::new_borrowed(ptr)
+}
diff --git a/nix-bindings-fetchers-sys/Cargo.toml b/nix-bindings-fetchers-sys/Cargo.toml
new file mode 100644
index 0000000..2dfaa3b
--- /dev/null
+++ b/nix-bindings-fetchers-sys/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "nix-bindings-fetchers-sys"
+version = "0.2.1"
+edition = "2021"
+build = "build.rs"
+license = "LGPL-2.1"
+description = "Low-level FFI bindings to the nix-fetchers library"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_fetchers_sys/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" }
+
+[build-dependencies]
+bindgen = "0.69"
+pkg-config = "0.3"
diff --git a/nix-bindings-fetchers-sys/README.md b/nix-bindings-fetchers-sys/README.md
new file mode 100644
index 0000000..8230b81
--- /dev/null
+++ b/nix-bindings-fetchers-sys/README.md
@@ -0,0 +1,11 @@
+# nix-bindings-fetchers-sys
+
+This crate contains generated bindings for the Nix C API (`nix-fetchers-c`).
+**You should not have to use this crate directly,** and so you should probably not add it to your dependencies.
+Instead, use the `nix-bindings-fetchers` crate, which _should_ be sufficient.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_fetchers_sys/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-fetchers-sys/build.rs b/nix-bindings-fetchers-sys/build.rs
new file mode 100644
index 0000000..34f6640
--- /dev/null
+++ b/nix-bindings-fetchers-sys/build.rs
@@ -0,0 +1,40 @@
+use std::path::PathBuf;
+
+#[derive(Debug)]
+struct StripNixPrefix;
+
+impl bindgen::callbacks::ParseCallbacks for StripNixPrefix {
+ fn item_name(&self, name: &str) -> Option {
+ name.strip_prefix("nix_").map(String::from)
+ }
+}
+
+fn main() {
+ println!("cargo:rerun-if-changed=include/nix-c-fetchers.h");
+ println!("cargo:rustc-link-lib=nixfetchersc");
+
+ let mut args = Vec::new();
+ for path in pkg_config::probe_library("nix-fetchers-c")
+ .unwrap()
+ .include_paths
+ .iter()
+ {
+ args.push(format!("-I{}", path.to_str().unwrap()));
+ }
+
+ let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());
+
+ let bindings = bindgen::Builder::default()
+ .header("include/nix-c-fetchers.h")
+ .clang_args(args)
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+ .parse_callbacks(Box::new(StripNixPrefix))
+ // Blocklist symbols from nix-bindings-util-sys
+ .blocklist_file(".*nix_api_util\\.h")
+ .generate()
+ .expect("Unable to generate bindings");
+
+ bindings
+ .write_to_file(out_path.join("bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
diff --git a/nix-bindings-fetchers-sys/include/nix-c-fetchers.h b/nix-bindings-fetchers-sys/include/nix-c-fetchers.h
new file mode 100644
index 0000000..2f4c542
--- /dev/null
+++ b/nix-bindings-fetchers-sys/include/nix-c-fetchers.h
@@ -0,0 +1 @@
+#include
diff --git a/nix-bindings-fetchers-sys/src/lib.rs b/nix-bindings-fetchers-sys/src/lib.rs
new file mode 100644
index 0000000..97a30ed
--- /dev/null
+++ b/nix-bindings-fetchers-sys/src/lib.rs
@@ -0,0 +1,33 @@
+//! Raw bindings to Nix C API
+//!
+//! This crate contains automatically generated bindings from the Nix C headers.
+//! The bindings are generated by bindgen and include C-style naming conventions
+//! and documentation comments that don't always conform to Rust standards.
+//!
+//! Normally you don't have to use this crate directly.
+//! Instead use `nix-fetchers`.
+
+// This file must only contain generated code, so that the module-level
+// #![allow(...)] attributes don't suppress warnings in hand-written code.
+// If you need to add hand-written code, use a submodule to isolate the
+// generated code. See:
+// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9
+
+// Standard bindgen suppressions for C naming conventions
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// Clippy suppressions for generated C bindings
+// bindgen doesn't generate safety docs
+#![allow(clippy::missing_safety_doc)]
+// Rustdoc suppressions for generated C documentation
+// The C headers contain Doxygen-style documentation that doesn't translate
+// well to Rust's rustdoc format, causing various warnings:
+#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve
+#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs
+#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like
+#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes
+
+use nix_bindings_util_sys::*;
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/nix-bindings-fetchers/Cargo.toml b/nix-bindings-fetchers/Cargo.toml
new file mode 100644
index 0000000..4ae993d
--- /dev/null
+++ b/nix-bindings-fetchers/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "nix-bindings-fetchers"
+version = "0.2.1"
+edition = "2021"
+license = "LGPL-2.1"
+description = "Rust bindings to Nix fetchers"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_fetchers/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+anyhow = "1.0"
+nix-bindings-store = { path = "../nix-bindings-store", version = "0.2.1" }
+nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" }
+nix-bindings-fetchers-sys = { path = "../nix-bindings-fetchers-sys", version = "0.2.1" }
+ctor = "0.2"
+tempfile = "3.10"
+cstr = "0.2"
+
+[lints.rust]
+warnings = "deny"
+dead-code = "allow"
+
+[lints.clippy]
+type-complexity = "allow"
+# We're still trying to make Nix more thread-safe, want forward-compat
+arc-with-non-send-sync = "allow"
diff --git a/nix-bindings-fetchers/README.md b/nix-bindings-fetchers/README.md
new file mode 100644
index 0000000..b8f3941
--- /dev/null
+++ b/nix-bindings-fetchers/README.md
@@ -0,0 +1,9 @@
+# nix-bindings-fetchers
+
+Rust bindings to the nix-fetchers library.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_fetchers/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-fetchers/src/lib.rs b/nix-bindings-fetchers/src/lib.rs
new file mode 100644
index 0000000..0b7b6cc
--- /dev/null
+++ b/nix-bindings-fetchers/src/lib.rs
@@ -0,0 +1,38 @@
+use anyhow::{Context as _, Result};
+use nix_bindings_fetchers_sys as raw;
+use nix_bindings_util::context::{self, Context};
+use std::ptr::NonNull;
+
+pub struct FetchersSettings {
+ pub(crate) ptr: NonNull,
+}
+impl Drop for FetchersSettings {
+ fn drop(&mut self) {
+ unsafe {
+ raw::fetchers_settings_free(self.ptr.as_ptr());
+ }
+ }
+}
+impl FetchersSettings {
+ pub fn new() -> Result {
+ let mut ctx = Context::new();
+ let ptr = unsafe { context::check_call!(raw::fetchers_settings_new(&mut ctx))? };
+ Ok(FetchersSettings {
+ ptr: NonNull::new(ptr).context("fetchers_settings_new unexpectedly returned null")?,
+ })
+ }
+
+ pub fn raw_ptr(&self) -> *mut raw::fetchers_settings {
+ self.ptr.as_ptr()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn fetchers_settings_new() {
+ let _ = FetchersSettings::new().unwrap();
+ }
+}
diff --git a/nix-bindings-flake-sys/Cargo.toml b/nix-bindings-flake-sys/Cargo.toml
new file mode 100644
index 0000000..987cd6d
--- /dev/null
+++ b/nix-bindings-flake-sys/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "nix-bindings-flake-sys"
+version = "0.2.1"
+edition = "2021"
+build = "build.rs"
+license = "LGPL-2.1"
+description = "Low-level FFI bindings to Nix flakes"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_flake_sys/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" }
+nix-bindings-store-sys = { path = "../nix-bindings-store-sys", version = "0.2.1" }
+nix-bindings-expr-sys = { path = "../nix-bindings-expr-sys", version = "0.2.1" }
+nix-bindings-fetchers-sys = { path = "../nix-bindings-fetchers-sys", version = "0.2.1" }
+nix-bindings-bdwgc-sys = { path = "../nix-bindings-bdwgc-sys", version = "0.2.1" }
+
+[build-dependencies]
+bindgen = "0.69"
+pkg-config = "0.3"
diff --git a/nix-bindings-flake-sys/README.md b/nix-bindings-flake-sys/README.md
new file mode 100644
index 0000000..1d26c8d
--- /dev/null
+++ b/nix-bindings-flake-sys/README.md
@@ -0,0 +1,11 @@
+# nix-bindings-flake-sys
+
+This crate contains generated bindings for the Nix C API (`nix-flake-c`).
+**You should not have to use this crate directly,** and so you should probably not add it to your dependencies.
+Instead, use the `nix-bindings-flake` crate, which _should_ be sufficient.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_flake_sys/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-flake-sys/build.rs b/nix-bindings-flake-sys/build.rs
new file mode 100644
index 0000000..1fb3233
--- /dev/null
+++ b/nix-bindings-flake-sys/build.rs
@@ -0,0 +1,56 @@
+use std::path::PathBuf;
+
+#[derive(Debug)]
+struct StripNixPrefix;
+
+impl bindgen::callbacks::ParseCallbacks for StripNixPrefix {
+ fn item_name(&self, name: &str) -> Option {
+ name.strip_prefix("nix_").map(String::from)
+ }
+}
+
+fn main() {
+ println!("cargo:rerun-if-changed=include/nix-c-flake.h");
+ println!("cargo:rustc-link-lib=nixflakec");
+
+ let mut args = Vec::new();
+ for path in pkg_config::probe_library("nix-flake-c")
+ .unwrap()
+ .include_paths
+ .iter()
+ {
+ args.push(format!("-I{}", path.to_str().unwrap()));
+ }
+ for path in pkg_config::probe_library("bdw-gc")
+ .unwrap()
+ .include_paths
+ .iter()
+ {
+ args.push(format!("-I{}", path.to_str().unwrap()));
+ }
+
+ let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());
+
+ let bindings = bindgen::Builder::default()
+ .header("include/nix-c-flake.h")
+ .clang_args(args)
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+ .parse_callbacks(Box::new(StripNixPrefix))
+ // Blocklist symbols from nix-bindings-util-sys
+ .blocklist_file(".*nix_api_util\\.h")
+ // Blocklist symbols from nix-bindings-store-sys
+ .blocklist_file(".*nix_api_store\\.h")
+ // Blocklist symbols from nix-bindings-expr-sys
+ .blocklist_file(".*nix_api_expr\\.h")
+ .blocklist_file(".*nix_api_value\\.h")
+ // Blocklist symbols from nix-bindings-fetchers-sys
+ .blocklist_file(".*nix_api_fetchers\\.h")
+ // Blocklist symbols from nix-bindings-bdwgc-sys
+ .blocklist_file(".*/gc\\.h")
+ .generate()
+ .expect("Unable to generate bindings");
+
+ bindings
+ .write_to_file(out_path.join("bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
diff --git a/nix-bindings-flake-sys/include/nix-c-flake.h b/nix-bindings-flake-sys/include/nix-c-flake.h
new file mode 100644
index 0000000..89a45f1
--- /dev/null
+++ b/nix-bindings-flake-sys/include/nix-c-flake.h
@@ -0,0 +1,3 @@
+#define GC_THREADS
+#include
+#include
diff --git a/nix-bindings-flake-sys/src/lib.rs b/nix-bindings-flake-sys/src/lib.rs
new file mode 100644
index 0000000..2e9209b
--- /dev/null
+++ b/nix-bindings-flake-sys/src/lib.rs
@@ -0,0 +1,35 @@
+//! Raw bindings to Nix C API
+//!
+//! This crate contains automatically generated bindings from the Nix C headers.
+//! The bindings are generated by bindgen and include C-style naming conventions
+//! and documentation comments that don't always conform to Rust standards.
+//!
+//! Normally you don't have to use this crate directly.
+//! Instead use `nix-flake`.
+
+// This file must only contain generated code, so that the module-level
+// #![allow(...)] attributes don't suppress warnings in hand-written code.
+// If you need to add hand-written code, use a submodule to isolate the
+// generated code. See:
+// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9
+
+// Standard bindgen suppressions for C naming conventions
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// Clippy suppressions for generated C bindings
+// bindgen doesn't generate safety docs
+#![allow(clippy::missing_safety_doc)]
+// Rustdoc suppressions for generated C documentation
+// The C headers contain Doxygen-style documentation that doesn't translate
+// well to Rust's rustdoc format, causing various warnings:
+#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve
+#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs
+#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like
+#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes
+
+use nix_bindings_expr_sys::*;
+use nix_bindings_fetchers_sys::*;
+use nix_bindings_util_sys::*;
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/nix-bindings-flake/Cargo.toml b/nix-bindings-flake/Cargo.toml
new file mode 100644
index 0000000..5fbb42a
--- /dev/null
+++ b/nix-bindings-flake/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "nix-bindings-flake"
+version = "0.2.1"
+edition = "2021"
+license = "LGPL-2.1"
+description = "Rust bindings to Nix flakes"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_flake/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+anyhow = "1.0"
+nix-bindings-expr = { path = "../nix-bindings-expr", version = "0.2.1" }
+nix-bindings-fetchers = { path = "../nix-bindings-fetchers", version = "0.2.1" }
+nix-bindings-store = { path = "../nix-bindings-store", version = "0.2.1" }
+nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" }
+nix-bindings-flake-sys = { path = "../nix-bindings-flake-sys", version = "0.2.1" }
+ctor = "0.2"
+tempfile = "3.10"
+cstr = "0.2"
+
+[lints.rust]
+warnings = "deny"
+dead-code = "allow"
+
+[lints.clippy]
+type-complexity = "allow"
+# We're still trying to make Nix more thread-safe, want forward-compat
+arc-with-non-send-sync = "allow"
diff --git a/nix-bindings-flake/README.md b/nix-bindings-flake/README.md
new file mode 100644
index 0000000..43dd11b
--- /dev/null
+++ b/nix-bindings-flake/README.md
@@ -0,0 +1,9 @@
+# nix-bindings-flake
+
+Rust bindings to Nix flakes.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_flake/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nixide/src/flake/locked_flake.rs b/nix-bindings-flake/src/lib.rs
similarity index 55%
rename from nixide/src/flake/locked_flake.rs
rename to nix-bindings-flake/src/lib.rs
index f11732c..beb6b31 100644
--- a/nixide/src/flake/locked_flake.rs
+++ b/nix-bindings-flake/src/lib.rs
@@ -1,18 +1,217 @@
-use std::ptr::NonNull;
+use std::{ffi::CString, os::raw::c_char, ptr::NonNull};
-use super::{FetchersSettings, FlakeLockFlags, FlakeReference, FlakeSettings};
-use crate::errors::{new_nixide_error, ErrorContext};
-use crate::sys;
-use crate::util::wrappers::AsInnerPtr;
-use crate::{EvalState, NixideError, Value};
+use anyhow::{Context as _, Result};
+use nix_bindings_expr::eval_state::EvalState;
+use nix_bindings_fetchers::FetchersSettings;
+use nix_bindings_flake_sys as raw;
+use nix_bindings_util::{
+ context::{self, Context},
+ result_string_init,
+ string_return::{callback_get_result_string, callback_get_result_string_data},
+};
+
+/// Store settings for the flakes feature.
+pub struct FlakeSettings {
+ pub(crate) ptr: *mut raw::flake_settings,
+}
+impl Drop for FlakeSettings {
+ fn drop(&mut self) {
+ unsafe {
+ raw::flake_settings_free(self.ptr);
+ }
+ }
+}
+impl FlakeSettings {
+ pub fn new() -> Result {
+ let mut ctx = Context::new();
+ let s = unsafe { context::check_call!(raw::flake_settings_new(&mut ctx)) }?;
+ Ok(FlakeSettings { ptr: s })
+ }
+ fn add_to_eval_state_builder(
+ &self,
+ builder: &mut nix_bindings_expr::eval_state::EvalStateBuilder,
+ ) -> Result<()> {
+ let mut ctx = Context::new();
+ unsafe {
+ context::check_call!(raw::flake_settings_add_to_eval_state_builder(
+ &mut ctx,
+ self.ptr,
+ builder.raw_ptr()
+ ))
+ }?;
+ Ok(())
+ }
+}
+
+pub trait EvalStateBuilderExt {
+ /// Configures the eval state to provide flakes features such as `builtins.getFlake`.
+ fn flakes(
+ self,
+ settings: &FlakeSettings,
+ ) -> Result;
+}
+impl EvalStateBuilderExt for nix_bindings_expr::eval_state::EvalStateBuilder {
+ /// Configures the eval state to provide flakes features such as `builtins.getFlake`.
+ fn flakes(
+ mut self,
+ settings: &FlakeSettings,
+ ) -> Result {
+ settings.add_to_eval_state_builder(&mut self)?;
+ Ok(self)
+ }
+}
+
+/// Parameters for parsing a flake reference.
+pub struct FlakeReferenceParseFlags {
+ pub(crate) ptr: NonNull,
+}
+impl Drop for FlakeReferenceParseFlags {
+ fn drop(&mut self) {
+ unsafe {
+ raw::flake_reference_parse_flags_free(self.ptr.as_ptr());
+ }
+ }
+}
+impl FlakeReferenceParseFlags {
+ pub fn new(settings: &FlakeSettings) -> Result {
+ let mut ctx = Context::new();
+ let ptr = unsafe {
+ context::check_call!(raw::flake_reference_parse_flags_new(&mut ctx, settings.ptr))
+ }?;
+ let ptr = NonNull::new(ptr)
+ .context("flake_reference_parse_flags_new unexpectedly returned null")?;
+ Ok(FlakeReferenceParseFlags { ptr })
+ }
+ /// Sets the [base directory](https://nix.dev/manual/nix/latest/glossary#gloss-base-directory)
+ /// for resolving local flake references.
+ pub fn set_base_directory(&mut self, base_directory: &str) -> Result<()> {
+ let mut ctx = Context::new();
+ unsafe {
+ context::check_call!(raw::flake_reference_parse_flags_set_base_directory(
+ &mut ctx,
+ self.ptr.as_ptr(),
+ base_directory.as_ptr() as *const c_char,
+ base_directory.len()
+ ))
+ }?;
+ Ok(())
+ }
+}
+
+#[derive(Clone)]
+pub struct FlakeReference {
+ pub(crate) ptr: NonNull,
+}
+impl Drop for FlakeReference {
+ fn drop(&mut self) {
+ unsafe {
+ raw::flake_reference_free(self.ptr.as_ptr());
+ }
+ }
+}
+impl FlakeReference {
+ /// Parse a flake reference from a string.
+ /// The string must be a valid flake reference, such as `github:owner/repo`.
+ /// It may also be suffixed with a `#` and a fragment, such as `github:owner/repo#something`,
+ /// in which case, the returned string will contain the fragment.
+ pub fn parse_with_fragment(
+ fetch_settings: &FetchersSettings,
+ flake_settings: &FlakeSettings,
+ flags: &FlakeReferenceParseFlags,
+ reference: &str,
+ ) -> Result<(FlakeReference, String)> {
+ let mut ctx = Context::new();
+ let mut r = result_string_init!();
+ let mut ptr: *mut raw::flake_reference = std::ptr::null_mut();
+ unsafe {
+ context::check_call!(raw::flake_reference_and_fragment_from_string(
+ &mut ctx,
+ fetch_settings.raw_ptr(),
+ flake_settings.ptr,
+ flags.ptr.as_ptr(),
+ reference.as_ptr() as *const c_char,
+ reference.len(),
+ // pointer to ptr
+ &mut ptr,
+ Some(callback_get_result_string),
+ callback_get_result_string_data(&mut r)
+ ))
+ }?;
+ let ptr = NonNull::new(ptr)
+ .context("flake_reference_and_fragment_from_string unexpectedly returned null")?;
+ Ok((FlakeReference { ptr }, r?))
+ }
+}
+
+/// Parameters that affect the locking of a flake.
+pub struct FlakeLockFlags {
+ pub(crate) ptr: *mut raw::flake_lock_flags,
+}
+impl Drop for FlakeLockFlags {
+ fn drop(&mut self) {
+ unsafe {
+ raw::flake_lock_flags_free(self.ptr);
+ }
+ }
+}
+impl FlakeLockFlags {
+ pub fn new(settings: &FlakeSettings) -> Result {
+ let mut ctx = Context::new();
+ let s = unsafe { context::check_call!(raw::flake_lock_flags_new(&mut ctx, settings.ptr)) }?;
+ Ok(FlakeLockFlags { ptr: s })
+ }
+ /// Configures [LockedFlake::lock] to make incremental changes to the lock file as needed. Changes are written to file.
+ pub fn set_mode_write_as_needed(&mut self) -> Result<()> {
+ let mut ctx = Context::new();
+ unsafe {
+ context::check_call!(raw::flake_lock_flags_set_mode_write_as_needed(
+ &mut ctx, self.ptr
+ ))
+ }?;
+ Ok(())
+ }
+ /// Make [LockedFlake::lock] check if the lock file is up to date. If not, an error is returned.
+ pub fn set_mode_check(&mut self) -> Result<()> {
+ let mut ctx = Context::new();
+ unsafe { context::check_call!(raw::flake_lock_flags_set_mode_check(&mut ctx, self.ptr)) }?;
+ Ok(())
+ }
+ /// Like `set_mode_write_as_needed`, but does not write to the lock file.
+ pub fn set_mode_virtual(&mut self) -> Result<()> {
+ let mut ctx = Context::new();
+ unsafe {
+ context::check_call!(raw::flake_lock_flags_set_mode_virtual(&mut ctx, self.ptr))
+ }?;
+ Ok(())
+ }
+ /// Adds an input override to the lock file that will be produced. The [LockedFlake::lock] operation will not write to the lock file.
+ pub fn add_input_override(
+ &mut self,
+ override_path: &str,
+ override_ref: &FlakeReference,
+ ) -> Result<()> {
+ let mut ctx = Context::new();
+ unsafe {
+ context::check_call!(raw::flake_lock_flags_add_input_override(
+ &mut ctx,
+ self.ptr,
+ CString::new(override_path)
+ .context("Failed to create CString for override_path")?
+ .as_ptr(),
+ override_ref.ptr.as_ptr()
+ ))
+ }?;
+ Ok(())
+ }
+}
pub struct LockedFlake {
- pub(crate) ptr: NonNull,
+ pub(crate) ptr: NonNull,
}
impl Drop for LockedFlake {
fn drop(&mut self) {
unsafe {
- sys::nix_locked_flake_free(self.ptr.as_ptr());
+ raw::locked_flake_free(self.ptr.as_ptr());
}
}
}
@@ -23,24 +222,20 @@ impl LockedFlake {
eval_state: &EvalState,
flags: &FlakeLockFlags,
flake_ref: &FlakeReference,
- ) -> Result {
- let ctx = ErrorContext::new();
-
- let ptr = NonNull::new(unsafe {
- sys::nix_flake_lock(
- ctx.as_ptr(),
- fetch_settings.as_ptr(),
- flake_settings.as_ptr(),
- eval_state.as_ptr(),
- flags.as_ptr(),
- flake_ref.as_ptr(),
- )
- });
-
- match ptr {
- Some(ptr) => Ok(LockedFlake { ptr }),
- None => Err(new_nixide_error!(NullPtr)),
- }
+ ) -> Result {
+ let mut ctx = Context::new();
+ let ptr = unsafe {
+ context::check_call!(raw::flake_lock(
+ &mut ctx,
+ fetch_settings.raw_ptr(),
+ flake_settings.ptr,
+ eval_state.raw_ptr(),
+ flags.ptr,
+ flake_ref.ptr.as_ptr()
+ ))
+ }?;
+ let ptr = NonNull::new(ptr).context("flake_lock unexpectedly returned null")?;
+ Ok(LockedFlake { ptr })
}
/// Returns the outputs of the flake - the result of calling the `outputs` attribute.
@@ -48,27 +243,24 @@ impl LockedFlake {
&self,
flake_settings: &FlakeSettings,
eval_state: &mut EvalState,
- ) -> Result {
- let ctx = ErrorContext::new();
-
- let r = unsafe {
- sys::nix_locked_flake_get_output_attrs(
- ctx.as_ptr(),
- flake_settings.as_ptr(),
- eval_state.as_ptr(),
- self.ptr.as_ptr(),
- )
- };
- Ok(nix_bindings_expr::value::__private::raw_value_new(r))
+ ) -> Result {
+ let mut ctx = Context::new();
+ unsafe {
+ let r = context::check_call!(raw::locked_flake_get_output_attrs(
+ &mut ctx,
+ flake_settings.ptr,
+ eval_state.raw_ptr(),
+ self.ptr.as_ptr()
+ ))?;
+ Ok(nix_bindings_expr::value::__private::raw_value_new(r))
+ }
}
}
#[cfg(test)]
mod tests {
- // use nix_bindings_expr::eval_state::{gc_register_my_thread, EvalStateBuilder};
-
- use crate::flake::{FlakeLockMode, FlakeReferenceParseFlags};
- use crate::{EvalStateBuilder, Store};
+ use nix_bindings_expr::eval_state::{gc_register_my_thread, EvalStateBuilder};
+ use nix_bindings_store::store::Store;
use super::*;
use std::sync::Once;
@@ -263,9 +455,7 @@ mod tests {
// Step 1: Do not update (check), fails
- flake_lock_flags
- .set_lock_mode(&FlakeLockMode::Check)
- .unwrap();
+ flake_lock_flags.set_mode_check().unwrap();
let locked_flake = LockedFlake::lock(
&fetchers_settings,
@@ -282,9 +472,7 @@ mod tests {
};
// Step 2: Update but do not write, succeeds
- flake_lock_flags
- .set_lock_mode(&FlakeLockMode::Virtual)
- .unwrap();
+ flake_lock_flags.set_mode_virtual().unwrap();
let locked_flake = LockedFlake::lock(
&fetchers_settings,
@@ -306,9 +494,7 @@ mod tests {
// Step 3: The lock was not written, so Step 1 would fail again
- flake_lock_flags
- .set_lock_mode(&FlakeLockMode::Check)
- .unwrap();
+ flake_lock_flags.set_mode_check().unwrap();
let locked_flake = LockedFlake::lock(
&fetchers_settings,
@@ -328,9 +514,7 @@ mod tests {
// Step 4: Update and write, succeeds
- flake_lock_flags
- .set_lock_mode(&FlakeLockMode::WriteAsNeeded)
- .unwrap();
+ flake_lock_flags.set_mode_write_as_needed().unwrap();
let locked_flake = LockedFlake::lock(
&fetchers_settings,
@@ -350,9 +534,7 @@ mod tests {
// Step 5: Lock was written, so Step 1 succeeds
- flake_lock_flags
- .set_lock_mode(&FlakeLockMode::Check)
- .unwrap();
+ flake_lock_flags.set_mode_check().unwrap();
let locked_flake = LockedFlake::lock(
&fetchers_settings,
@@ -373,9 +555,7 @@ mod tests {
// Step 6: Lock with override, do not write
// This shouldn't matter; write_as_needed will be overridden
- flake_lock_flags
- .set_lock_mode(&FlakeLockMode::WriteAsNeeded)
- .unwrap();
+ flake_lock_flags.set_mode_write_as_needed().unwrap();
let (flake_ref_c, fragment) = FlakeReference::parse_with_fragment(
&fetchers_settings,
@@ -386,7 +566,9 @@ mod tests {
.unwrap();
assert_eq!(fragment, "");
- flake_lock_flags.override_input("b", &flake_ref_c).unwrap();
+ flake_lock_flags
+ .add_input_override("b", &flake_ref_c)
+ .unwrap();
let locked_flake = LockedFlake::lock(
&fetchers_settings,
@@ -409,9 +591,7 @@ mod tests {
// Step 7: Override was not written; lock still points to b
- flake_lock_flags
- .set_lock_mode(&FlakeLockMode::Check)
- .unwrap();
+ flake_lock_flags.set_mode_check().unwrap();
let locked_flake = LockedFlake::lock(
&fetchers_settings,
diff --git a/nix-bindings-store-sys/Cargo.toml b/nix-bindings-store-sys/Cargo.toml
new file mode 100644
index 0000000..9e8c374
--- /dev/null
+++ b/nix-bindings-store-sys/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "nix-bindings-store-sys"
+version = "0.2.1"
+edition = "2021"
+build = "build.rs"
+license = "LGPL-2.1"
+description = "Low-level FFI bindings to the Nix store library"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_store_sys/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" }
+zerocopy = { version = "0.8", features = ["derive"] }
+
+[build-dependencies]
+bindgen = "0.69"
+pkg-config = "0.3"
diff --git a/nix-bindings-store-sys/README.md b/nix-bindings-store-sys/README.md
new file mode 100644
index 0000000..936505d
--- /dev/null
+++ b/nix-bindings-store-sys/README.md
@@ -0,0 +1,11 @@
+# nix-bindings-store-sys
+
+This crate contains generated bindings for the Nix C API (`nix-store-c`).
+**You should not have to use this crate directly,** and so you should probably not add it to your dependencies.
+Instead, use the `nix-bindings-store` crate, which _should_ be sufficient.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_store_sys/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-store-sys/build.rs b/nix-bindings-store-sys/build.rs
new file mode 100644
index 0000000..8702756
--- /dev/null
+++ b/nix-bindings-store-sys/build.rs
@@ -0,0 +1,57 @@
+use std::path::PathBuf;
+
+#[derive(Debug)]
+struct StripNixPrefix;
+
+impl bindgen::callbacks::ParseCallbacks for StripNixPrefix {
+ fn item_name(&self, name: &str) -> Option {
+ name.strip_prefix("nix_").map(String::from)
+ }
+}
+
+#[derive(Debug)]
+struct AddZerocopyDerives {}
+impl bindgen::callbacks::ParseCallbacks for AddZerocopyDerives {
+ fn add_derives(&self, info: &bindgen::callbacks::DeriveInfo<'_>) -> Vec {
+ if info.name == "store_path_hash_part" {
+ vec![
+ "zerocopy::FromBytes".to_string(),
+ "zerocopy::IntoBytes".to_string(),
+ "zerocopy::Immutable".to_string(),
+ ]
+ } else {
+ vec![]
+ }
+ }
+}
+
+fn main() {
+ println!("cargo:rerun-if-changed=include/nix-c-store.h");
+ println!("cargo:rustc-link-lib=nixstorec");
+
+ let mut args = Vec::new();
+ for path in pkg_config::probe_library("nix-store-c")
+ .unwrap()
+ .include_paths
+ .iter()
+ {
+ args.push(format!("-I{}", path.to_str().unwrap()));
+ }
+
+ let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());
+
+ let bindings = bindgen::Builder::default()
+ .header("include/nix-c-store.h")
+ .clang_args(args)
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+ .parse_callbacks(Box::new(StripNixPrefix))
+ .parse_callbacks(Box::new(AddZerocopyDerives {}))
+ // Blocklist symbols from nix-bindings-util-sys
+ .blocklist_file(".*nix_api_util\\.h")
+ .generate()
+ .expect("Unable to generate bindings");
+
+ bindings
+ .write_to_file(out_path.join("bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
diff --git a/nix-bindings-store-sys/include/nix-c-store.h b/nix-bindings-store-sys/include/nix-c-store.h
new file mode 100644
index 0000000..2c04d1d
--- /dev/null
+++ b/nix-bindings-store-sys/include/nix-c-store.h
@@ -0,0 +1 @@
+#include
diff --git a/nix-bindings-store-sys/src/lib.rs b/nix-bindings-store-sys/src/lib.rs
new file mode 100644
index 0000000..eb41931
--- /dev/null
+++ b/nix-bindings-store-sys/src/lib.rs
@@ -0,0 +1,33 @@
+//! Raw bindings to Nix C API
+//!
+//! This crate contains automatically generated bindings from the Nix C headers.
+//! The bindings are generated by bindgen and include C-style naming conventions
+//! and documentation comments that don't always conform to Rust standards.
+//!
+//! Normally you don't have to use this crate directly.
+//! Instead use `nix-store`.
+
+// This file must only contain generated code, so that the module-level
+// #![allow(...)] attributes don't suppress warnings in hand-written code.
+// If you need to add hand-written code, use a submodule to isolate the
+// generated code. See:
+// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9
+
+// Standard bindgen suppressions for C naming conventions
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// Clippy suppressions for generated C bindings
+// bindgen doesn't generate safety docs
+#![allow(clippy::missing_safety_doc)]
+// Rustdoc suppressions for generated C documentation
+// The C headers contain Doxygen-style documentation that doesn't translate
+// well to Rust's rustdoc format, causing various warnings:
+#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve
+#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs
+#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like
+#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes
+
+use nix_bindings_util_sys::*;
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/nix-bindings-store/Cargo.toml b/nix-bindings-store/Cargo.toml
new file mode 100644
index 0000000..24336db
--- /dev/null
+++ b/nix-bindings-store/Cargo.toml
@@ -0,0 +1,45 @@
+[package]
+name = "nix-bindings-store"
+version = "0.2.1"
+edition = "2021"
+build = "build.rs"
+license = "LGPL-2.1"
+description = "Rust bindings to Nix store library"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_store/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+anyhow = "1.0"
+nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" }
+nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" }
+nix-bindings-store-sys = { path = "../nix-bindings-store-sys", version = "0.2.1" }
+zerocopy = "0.8"
+harmonia-store-core = { version = "0.0.0-alpha.0", optional = true }
+serde_json = { version = "1.0", optional = true }
+
+[dev-dependencies]
+ctor = "0.2"
+hex-literal = "0.4"
+tempfile = "3.10"
+serde_json = "1.0"
+
+[build-dependencies]
+pkg-config = "0.3"
+# Needed for version parsing in build.rs
+nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" }
+
+[features]
+harmonia = [ "dep:harmonia-store-core", "dep:serde_json" ]
+
+[lints.rust]
+warnings = "deny"
+dead-code = "allow"
+
+[lints.clippy]
+type-complexity = "allow"
+# We're still trying to make Nix more thread-safe, want forward-compat
+arc-with-non-send-sync = "allow"
diff --git a/nix-bindings-store/README.md b/nix-bindings-store/README.md
new file mode 100644
index 0000000..8002364
--- /dev/null
+++ b/nix-bindings-store/README.md
@@ -0,0 +1,9 @@
+# nix-bindings-store
+
+Rust bindings to the Nix store library.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_store/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-store/build.rs b/nix-bindings-store/build.rs
new file mode 100644
index 0000000..85a20d6
--- /dev/null
+++ b/nix-bindings-store/build.rs
@@ -0,0 +1,6 @@
+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"]);
+}
diff --git a/nix-bindings-store/src/derivation/harmonia.rs b/nix-bindings-store/src/derivation/harmonia.rs
new file mode 100644
index 0000000..44b34a2
--- /dev/null
+++ b/nix-bindings-store/src/derivation/harmonia.rs
@@ -0,0 +1,100 @@
+use anyhow::Context as _;
+
+use super::Derivation;
+
+impl Derivation {
+ /// Convert harmonia Derivation to nix-bindings Derivation.
+ ///
+ /// This requires a Store instance because the Nix C API needs it for internal validation.
+ pub fn from_harmonia(
+ store: &mut crate::store::Store,
+ harmonia_drv: &harmonia_store_core::derivation::Derivation,
+ ) -> anyhow::Result {
+ let json = serde_json::to_string(harmonia_drv)
+ .context("Failed to serialize harmonia Derivation to JSON")?;
+
+ store.derivation_from_json(&json)
+ }
+}
+
+impl TryFrom<&Derivation> for harmonia_store_core::derivation::Derivation {
+ type Error = anyhow::Error;
+
+ fn try_from(nix_drv: &Derivation) -> anyhow::Result {
+ let json = nix_drv
+ .to_json_string()
+ .context("Failed to convert nix Derivation to JSON")?;
+
+ serde_json::from_str(&json).context("Failed to parse JSON as harmonia Derivation")
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn create_harmonia_derivation() -> harmonia_store_core::derivation::Derivation {
+ use harmonia_store_core::derivation::{Derivation, DerivationOutput};
+ use harmonia_store_core::derived_path::OutputName;
+ use harmonia_store_core::store_path::StorePath;
+ use std::collections::{BTreeMap, BTreeSet};
+ use std::str::FromStr;
+
+ let system = format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS);
+ let out_path = "8bs8sd27bzzy6w94fznjd2j8ldmdg7x6-myname";
+
+ let env = BTreeMap::from([
+ ("builder".into(), "/bin/sh".into()),
+ ("name".into(), "myname".into()),
+ ("out".into(), format!("/{out_path}").into()),
+ ("system".into(), system.clone().into()),
+ ]);
+ let mut outputs = BTreeMap::new();
+ outputs.insert(
+ OutputName::from_str("out").unwrap(),
+ DerivationOutput::InputAddressed(StorePath::from_base_path(out_path).unwrap()),
+ );
+
+ Derivation {
+ args: vec!["-c".into(), "echo $name foo > $out".into()],
+ builder: "/bin/sh".into(),
+ env,
+ inputs: BTreeSet::new(),
+ name: b"myname".as_slice().try_into().unwrap(),
+ outputs,
+ platform: system.into(),
+ structured_attrs: None,
+ }
+ }
+
+ #[test]
+ fn derivation_round_trip_harmonia() {
+ let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap();
+ let harmonia_drv = create_harmonia_derivation();
+
+ // Convert to nix-bindings Derivation
+ let nix_drv = Derivation::from_harmonia(&mut store, &harmonia_drv).unwrap();
+
+ // Convert back to harmonia Derivation
+ let harmonia_round_trip: harmonia_store_core::derivation::Derivation =
+ (&nix_drv).try_into().unwrap();
+
+ assert_eq!(harmonia_drv, harmonia_round_trip);
+ }
+
+ #[test]
+ fn derivation_clone() {
+ let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap();
+ let harmonia_drv = create_harmonia_derivation();
+
+ let derivation = Derivation::from_harmonia(&mut store, &harmonia_drv).unwrap();
+ let cloned_derivation = derivation.clone();
+
+ let original_harmonia: harmonia_store_core::derivation::Derivation =
+ (&derivation).try_into().unwrap();
+ let cloned_harmonia: harmonia_store_core::derivation::Derivation =
+ (&cloned_derivation).try_into().unwrap();
+
+ assert_eq!(original_harmonia, cloned_harmonia);
+ }
+}
diff --git a/nix-bindings-store/src/derivation/mod.rs b/nix-bindings-store/src/derivation/mod.rs
new file mode 100644
index 0000000..557994e
--- /dev/null
+++ b/nix-bindings-store/src/derivation/mod.rs
@@ -0,0 +1,92 @@
+#![cfg(nix_at_least = "2.33.0pre")]
+
+use nix_bindings_store_sys as raw;
+#[cfg(nix_at_least = "2.33")]
+use nix_bindings_util::{
+ check_call,
+ context::Context,
+ result_string_init,
+ string_return::{callback_get_result_string, callback_get_result_string_data},
+};
+use std::ptr::NonNull;
+
+/// A Nix derivation
+///
+/// **Requires Nix 2.33 or later.**
+pub struct Derivation {
+ pub(crate) inner: NonNull,
+}
+
+impl Derivation {
+ pub(crate) fn new_raw(inner: NonNull) -> Self {
+ Derivation { inner }
+ }
+
+ /// Convert the derivation to JSON (which is encoded to a string).
+ ///
+ /// **Requires Nix 2.33 or later.**
+ ///
+ /// The JSON format follows the [Nix derivation JSON schema](https://nix.dev/manual/nix/latest/protocols/json/derivation.html).
+ /// Note that this format is experimental as of writing.
+ #[cfg(nix_at_least = "2.33")]
+ pub fn to_json_string(&self) -> anyhow::Result {
+ let mut ctx = Context::new();
+
+ unsafe {
+ let mut r = result_string_init!();
+ check_call!(raw::derivation_to_json(
+ &mut ctx,
+ self.inner.as_ptr(),
+ Some(callback_get_result_string),
+ callback_get_result_string_data(&mut r)
+ ))?;
+ r
+ }
+ }
+
+ /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings.
+ ///
+ /// Construct a new `Derivation` by first cloning the C derivation.
+ ///
+ /// # Safety
+ ///
+ /// This does not take ownership of the C derivation, so it should be a borrowed pointer, or you should free it.
+ pub unsafe fn new_raw_clone(inner: NonNull) -> Self {
+ Self::new_raw(
+ NonNull::new(raw::derivation_clone(inner.as_ptr()))
+ .or_else(|| panic!("nix_derivation_clone returned a null pointer"))
+ .unwrap(),
+ )
+ }
+
+ /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings.
+ ///
+ /// Get a pointer to the underlying Nix C API derivation.
+ ///
+ /// # Safety
+ ///
+ /// This function is unsafe because it returns a raw pointer. The caller must ensure that the pointer is not used beyond the lifetime of this `Derivation`.
+ pub unsafe fn as_ptr(&self) -> *mut raw::derivation {
+ self.inner.as_ptr()
+ }
+}
+
+impl Clone for Derivation {
+ fn clone(&self) -> Self {
+ unsafe { Self::new_raw_clone(self.inner) }
+ }
+}
+
+impl Drop for Derivation {
+ fn drop(&mut self) {
+ unsafe {
+ raw::derivation_free(self.inner.as_ptr());
+ }
+ }
+}
+
+#[cfg(feature = "harmonia")]
+mod harmonia;
+
+#[cfg(test)]
+mod tests {}
diff --git a/nix-bindings-store/src/lib.rs b/nix-bindings-store/src/lib.rs
new file mode 100644
index 0000000..6010f2e
--- /dev/null
+++ b/nix-bindings-store/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod derivation;
+pub mod path;
+pub mod store;
diff --git a/nix-bindings-store/src/path/harmonia.rs b/nix-bindings-store/src/path/harmonia.rs
new file mode 100644
index 0000000..660fa1f
--- /dev/null
+++ b/nix-bindings-store/src/path/harmonia.rs
@@ -0,0 +1,65 @@
+use anyhow::{Context as _, Result};
+
+use super::{StorePath, STORE_PATH_HASH_SIZE};
+
+impl TryFrom<&harmonia_store_core::store_path::StorePath> for StorePath {
+ type Error = anyhow::Error;
+
+ fn try_from(harmonia_path: &harmonia_store_core::store_path::StorePath) -> Result {
+ let hash: &[u8; STORE_PATH_HASH_SIZE] = harmonia_path.hash().as_ref();
+ StorePath::from_parts(hash, harmonia_path.name().as_ref())
+ }
+}
+
+impl TryFrom<&StorePath> for harmonia_store_core::store_path::StorePath {
+ type Error = anyhow::Error;
+
+ fn try_from(nix_path: &StorePath) -> Result {
+ let hash = nix_path
+ .hash()
+ .context("Failed to get hash from nix StorePath")?;
+ let harmonia_hash = harmonia_store_core::store_path::StorePathHash::new(hash);
+
+ let name = nix_path
+ .name()
+ .context("Failed to get name from nix StorePath")?;
+
+ let harmonia_name: harmonia_store_core::store_path::StorePathName = name
+ .parse()
+ .context("Failed to parse name as StorePathName")?;
+
+ Ok(harmonia_store_core::store_path::StorePath::from((
+ harmonia_hash,
+ harmonia_name,
+ )))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ #[test]
+ fn store_path_round_trip_harmonia() {
+ let harmonia_path: harmonia_store_core::store_path::StorePath =
+ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo.drv".parse().unwrap();
+
+ let nix_path: crate::path::StorePath = (&harmonia_path).try_into().unwrap();
+
+ let harmonia_round_trip: harmonia_store_core::store_path::StorePath =
+ (&nix_path).try_into().unwrap();
+
+ assert_eq!(harmonia_path, harmonia_round_trip);
+ }
+
+ #[test]
+ fn store_path_harmonia_clone() {
+ let harmonia_path: harmonia_store_core::store_path::StorePath =
+ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo.drv".parse().unwrap();
+
+ let nix_path: crate::path::StorePath = (&harmonia_path).try_into().unwrap();
+ let cloned_path = nix_path.clone();
+
+ assert_eq!(nix_path.name().unwrap(), cloned_path.name().unwrap());
+ assert_eq!(nix_path.hash().unwrap(), cloned_path.hash().unwrap());
+ }
+}
diff --git a/nix-bindings-store/src/path/mod.rs b/nix-bindings-store/src/path/mod.rs
new file mode 100644
index 0000000..c3478fa
--- /dev/null
+++ b/nix-bindings-store/src/path/mod.rs
@@ -0,0 +1,160 @@
+use std::ptr::NonNull;
+
+use anyhow::{Context as _, Result};
+use nix_bindings_store_sys as raw;
+#[cfg(nix_at_least = "2.33")]
+use nix_bindings_util::{check_call, context::Context};
+use nix_bindings_util::{
+ result_string_init,
+ string_return::{callback_get_result_string, callback_get_result_string_data},
+};
+
+/// The size of a store path hash in bytes (20 bytes, decoded from nix32).
+pub const STORE_PATH_HASH_SIZE: usize = 20;
+
+#[cfg(nix_at_least = "2.33")]
+const _: () = assert!(std::mem::size_of::() == STORE_PATH_HASH_SIZE);
+
+pub struct StorePath {
+ raw: NonNull,
+}
+
+impl StorePath {
+ /// Get the name of the store path.
+ ///
+ /// For a store path like `/nix/store/abc1234...-foo-1.2`, this function will return `foo-1.2`.
+ pub fn name(&self) -> Result {
+ unsafe {
+ let mut r = result_string_init!();
+ raw::store_path_name(
+ self.as_ptr(),
+ Some(callback_get_result_string),
+ callback_get_result_string_data(&mut r),
+ );
+ r
+ }
+ }
+
+ /// Get the hash part of the store path.
+ ///
+ /// This returns the decoded hash (not the nix32-encoded string).
+ #[cfg(nix_at_least = "2.33")]
+ pub fn hash(&self) -> Result<[u8; STORE_PATH_HASH_SIZE]> {
+ let mut result = [0u8; STORE_PATH_HASH_SIZE];
+ let hash_part: &mut raw::store_path_hash_part = zerocopy::transmute_mut!(&mut result);
+
+ let mut ctx = Context::new();
+
+ unsafe {
+ check_call!(raw::store_path_hash(&mut ctx, self.as_ptr(), hash_part))?;
+ }
+ Ok(result)
+ }
+
+ /// Create a StorePath from hash and name components.
+ #[cfg(nix_at_least = "2.33")]
+ pub fn from_parts(hash: &[u8; STORE_PATH_HASH_SIZE], name: &str) -> Result {
+ let hash_part: &raw::store_path_hash_part = zerocopy::transmute_ref!(hash);
+
+ let mut ctx = Context::new();
+
+ let out_path = unsafe {
+ check_call!(raw::store_create_from_parts(
+ &mut ctx,
+ hash_part,
+ name.as_ptr() as *const i8,
+ name.len()
+ ))?
+ };
+
+ NonNull::new(out_path)
+ .map(|ptr| unsafe { Self::new_raw(ptr) })
+ .context("store_create_from_parts returned null")
+ }
+
+ /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings.
+ ///
+ /// Construct a new `StorePath` by first cloning the C store path.
+ ///
+ /// # Safety
+ ///
+ /// This does not take ownership of the C store path, so it should be a borrowed pointer, or you should free it.
+ pub unsafe fn new_raw_clone(raw: NonNull) -> Self {
+ Self::new_raw(
+ NonNull::new(raw::store_path_clone(raw.as_ptr()))
+ .or_else(|| panic!("nix_store_path_clone returned a null pointer"))
+ .unwrap(),
+ )
+ }
+
+ /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings.
+ ///
+ /// Takes ownership of a C `nix_store_path`. It will be freed when the `StorePath` is dropped.
+ ///
+ /// # Safety
+ ///
+ /// The caller must ensure that the provided `NonNull` is valid and that the ownership
+ /// semantics are correctly followed. The `raw` pointer must not be used after being passed to this function.
+ pub unsafe fn new_raw(raw: NonNull) -> Self {
+ StorePath { raw }
+ }
+
+ /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings.
+ ///
+ /// Get a pointer to the underlying Nix C API store path.
+ ///
+ /// # Safety
+ ///
+ /// This function is unsafe because it returns a raw pointer. The caller must ensure that the pointer is not used beyond the lifetime of this `StorePath`.
+ pub unsafe fn as_ptr(&self) -> *mut raw::StorePath {
+ self.raw.as_ptr()
+ }
+}
+
+impl Clone for StorePath {
+ fn clone(&self) -> Self {
+ unsafe { Self::new_raw_clone(self.raw) }
+ }
+}
+
+impl Drop for StorePath {
+ fn drop(&mut self) {
+ unsafe {
+ raw::store_path_free(self.as_ptr());
+ }
+ }
+}
+
+#[cfg(all(feature = "harmonia", nix_at_least = "2.33"))]
+mod harmonia;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use hex_literal::hex;
+
+ #[test]
+ #[cfg(nix_at_least = "2.26" /* get_storedir */)]
+ fn store_path_name() {
+ let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap();
+ let store_dir = store.get_storedir().unwrap();
+ let store_path_string =
+ format!("{store_dir}/rdd4pnr4x9rqc9wgbibhngv217w2xvxl-bash-interactive-5.2p26");
+ let store_path = store.parse_store_path(store_path_string.as_str()).unwrap();
+ assert_eq!(store_path.name().unwrap(), "bash-interactive-5.2p26");
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn store_path_round_trip() {
+ let original_hash: [u8; STORE_PATH_HASH_SIZE] =
+ hex!("0123456789abcdef0011223344556677deadbeef");
+ let original_name = "foo.drv";
+
+ let store_path = StorePath::from_parts(&original_hash, original_name).unwrap();
+
+ // Round trip gets back what we started with
+ assert_eq!(store_path.hash().unwrap(), original_hash);
+ assert_eq!(store_path.name().unwrap(), original_name);
+ }
+}
diff --git a/nix-bindings-store/src/store.rs b/nix-bindings-store/src/store.rs
new file mode 100644
index 0000000..cdd0977
--- /dev/null
+++ b/nix-bindings-store/src/store.rs
@@ -0,0 +1,1020 @@
+use anyhow::{bail, Error, Result};
+use nix_bindings_store_sys as raw;
+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, result_string_init};
+use nix_bindings_util_sys as raw_util;
+#[cfg(nix_at_least = "2.33.0pre")]
+use std::collections::BTreeMap;
+use std::collections::HashMap;
+use std::ffi::{c_char, CString};
+use std::ptr::null_mut;
+use std::ptr::NonNull;
+use std::sync::{Arc, LazyLock, Mutex, Weak};
+
+#[cfg(nix_at_least = "2.33.0pre")]
+use crate::derivation::Derivation;
+use crate::path::StorePath;
+
+/* TODO make Nix itself thread safe */
+static INIT: LazyLock> = LazyLock::new(|| unsafe {
+ check_call!(raw::libstore_init(&mut Context::new()))?;
+ Ok(())
+});
+
+struct StoreRef {
+ inner: NonNull,
+}
+impl StoreRef {
+ /// # Safety
+ ///
+ /// The returned pointer is only valid as long as the `StoreRef` is alive.
+ pub unsafe fn ptr(&self) -> *mut raw::Store {
+ self.inner.as_ptr()
+ }
+}
+impl Drop for StoreRef {
+ fn drop(&mut self) {
+ unsafe {
+ raw::store_free(self.inner.as_ptr());
+ }
+ }
+}
+unsafe impl Send for StoreRef {}
+/// Unlike pointers in general, operations on raw::Store are thread safe and it is therefore safe to share them between threads.
+unsafe impl Sync for StoreRef {}
+
+/// A [Weak] reference to a store.
+pub struct StoreWeak {
+ inner: Weak,
+}
+impl StoreWeak {
+ /// Upgrade the weak reference to a proper [Store].
+ ///
+ /// If no normal reference to the [Store] is around anymore elsewhere, this fails by returning `None`.
+ pub fn upgrade(&self) -> Option {
+ self.inner.upgrade().map(|inner| Store {
+ inner,
+ context: Context::new(),
+ })
+ }
+}
+
+/// Protects against https://github.com/NixOS/nix/issues/11979 (unless different parameters are passed, in which case it's up to luck, but you do get your own parameters as you asked for).
+type StoreCacheMap = HashMap<(Option, Vec<(String, String)>), StoreWeak>;
+
+static STORE_CACHE: LazyLock>> =
+ LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
+
+#[cfg(nix_at_least = "2.33.0pre")]
+unsafe extern "C" fn callback_get_result_store_path_set(
+ _context: *mut raw_util::c_context,
+ user_data: *mut std::os::raw::c_void,
+ store_path: *const raw::StorePath,
+) {
+ let ret = user_data as *mut Vec;
+ let ret: &mut Vec = &mut *ret;
+
+ let store_path = raw::store_path_clone(store_path);
+
+ let store_path =
+ NonNull::new(store_path).expect("nix_store_parse_path returned a null pointer");
+ let store_path = StorePath::new_raw(store_path);
+ ret.push(store_path);
+}
+
+#[cfg(nix_at_least = "2.33.0pre")]
+fn callback_get_result_store_path_set_data(vec: &mut Vec) -> *mut std::os::raw::c_void {
+ vec as *mut Vec as *mut std::os::raw::c_void
+}
+
+pub struct Store {
+ inner: Arc,
+ /* An error context to reuse. This way we don't have to allocate them for each store operation. */
+ context: Context,
+}
+impl Store {
+ /// Open a store.
+ ///
+ /// See [`nix_bindings_store_sys::store_open`] for more information.
+ #[doc(alias = "nix_store_open")]
+ pub fn open<'a, 'b>(
+ url: Option<&str>,
+ params: impl IntoIterator- ,
+ ) -> Result
{
+ let params = params
+ .into_iter()
+ .map(|(k, v)| (k.to_owned(), v.to_owned()))
+ .collect::>();
+ let params2 = params.clone();
+ let mut store_cache = STORE_CACHE
+ .lock()
+ .map_err(|_| Error::msg("Failed to lock store cache. This should never happen."))?;
+ match store_cache.entry((url.map(Into::into), params)) {
+ std::collections::hash_map::Entry::Occupied(mut e) => {
+ if let Some(store) = e.get().upgrade() {
+ Ok(store)
+ } else {
+ let store = Self::open_uncached(
+ url,
+ params2.iter().map(|(k, v)| (k.as_str(), v.as_str())),
+ )?;
+ e.insert(store.weak_ref());
+ Ok(store)
+ }
+ }
+ std::collections::hash_map::Entry::Vacant(e) => {
+ let store = Self::open_uncached(
+ url,
+ params2.iter().map(|(k, v)| (k.as_str(), v.as_str())),
+ )?;
+ e.insert(store.weak_ref());
+ Ok(store)
+ }
+ }
+ }
+ fn open_uncached<'a, 'b>(
+ url: Option<&str>,
+ params: impl IntoIterator- ,
+ ) -> Result
{
+ let x = INIT.as_ref();
+ match x {
+ Ok(_) => {}
+ Err(e) => {
+ // Couldn't just clone the error, so we have to print it here.
+ bail!("nix_libstore_init error: {}", e);
+ }
+ }
+
+ let mut context: Context = Context::new();
+
+ let uri_cstring = match url {
+ Some(url) => Some(CString::new(url)?),
+ None => None,
+ };
+ let uri_ptr = uri_cstring
+ .as_ref()
+ .map(|s| s.as_ptr())
+ .unwrap_or(null_mut());
+
+ // this intermediate value must be here and must not be moved
+ // because it owns the data the `*const c_char` pointers point to.
+ let params: Vec<(CString, CString)> = params
+ .into_iter()
+ .map(|(k, v)| Ok((CString::new(k)?, CString::new(v)?))) // to do. context
+ .collect::>()?;
+ // this intermediate value owns the data the `*mut *const c_char` pointer points to.
+ let mut params: Vec<_> = params
+ .iter()
+ .map(|(k, v)| [k.as_ptr(), v.as_ptr()])
+ .collect();
+ // this intermediate value owns the data the `*mut *mut *const c_char` pointer points to.
+ let mut params: Vec<*mut *const c_char> = params
+ .iter_mut()
+ .map(|t| t.as_mut_ptr())
+ .chain(std::iter::once(null_mut())) // signal the end of the array
+ .collect();
+
+ let store =
+ unsafe { check_call!(raw::store_open(&mut context, uri_ptr, params.as_mut_ptr())) }?;
+ if store.is_null() {
+ panic!("nix_c_store_open returned a null pointer without an error");
+ }
+ let store = Store {
+ inner: Arc::new(StoreRef {
+ inner: NonNull::new(store).unwrap(),
+ }),
+ context,
+ };
+ Ok(store)
+ }
+
+ /// # Safety
+ ///
+ /// The returned pointer is only valid as long as the `Store` is alive.
+ pub unsafe fn raw_ptr(&self) -> *mut raw::Store {
+ self.inner.ptr()
+ }
+
+ #[doc(alias = "nix_store_get_uri")]
+ pub fn get_uri(&mut self) -> Result {
+ let mut r = result_string_init!();
+ unsafe {
+ check_call!(raw::store_get_uri(
+ &mut self.context,
+ self.inner.ptr(),
+ Some(callback_get_result_string),
+ callback_get_result_string_data(&mut r)
+ ))
+ }?;
+ r
+ }
+
+ #[cfg(nix_at_least = "2.26")]
+ #[doc(alias = "nix_store_get_storedir")]
+ pub fn get_storedir(&mut self) -> Result {
+ let mut r = result_string_init!();
+ unsafe {
+ check_call!(raw::store_get_storedir(
+ &mut self.context,
+ self.inner.ptr(),
+ Some(callback_get_result_string),
+ callback_get_result_string_data(&mut r)
+ ))
+ }?;
+ r
+ }
+
+ #[doc(alias = "nix_store_parse_path")]
+ pub fn parse_store_path(&mut self, path: &str) -> Result {
+ let path = CString::new(path)?;
+ unsafe {
+ let store_path = check_call!(raw::store_parse_path(
+ &mut self.context,
+ self.inner.ptr(),
+ path.as_ptr()
+ ))?;
+ let store_path =
+ NonNull::new(store_path).expect("nix_store_parse_path returned a null pointer");
+ Ok(StorePath::new_raw(store_path))
+ }
+ }
+
+ #[doc(alias = "nix_store_real_path")]
+ pub fn real_path(&mut self, path: &StorePath) -> Result {
+ let mut r = result_string_init!();
+ unsafe {
+ check_call!(raw::store_real_path(
+ &mut self.context,
+ self.inner.ptr(),
+ path.as_ptr(),
+ Some(callback_get_result_string),
+ callback_get_result_string_data(&mut r)
+ ))
+ }?;
+ r
+ }
+
+ /// Parse a derivation from JSON.
+ ///
+ /// **Requires Nix 2.33 or later.**
+ ///
+ /// The JSON format follows the [Nix derivation JSON schema](https://nix.dev/manual/nix/latest/protocols/json/derivation.html).
+ /// Note that this format is experimental as of writing.
+ /// The derivation is not added to the store; use [`Store::add_derivation`] for that.
+ ///
+ /// # Parameters
+ /// - `json`: A JSON string representing the derivation
+ ///
+ /// # Returns
+ /// A [`Derivation`] object if parsing succeeds, or an error if the JSON is invalid
+ /// or malformed.
+ #[cfg(nix_at_least = "2.33.0pre")]
+ #[doc(alias = "nix_derivation_from_json")]
+ pub fn derivation_from_json(&mut self, json: &str) -> Result {
+ let json_cstr = CString::new(json)?;
+ unsafe {
+ let drv = check_call!(raw::derivation_from_json(
+ &mut self.context,
+ self.inner.ptr(),
+ json_cstr.as_ptr()
+ ))?;
+ let inner = NonNull::new(drv)
+ .ok_or_else(|| Error::msg("derivation_from_json returned null"))?;
+ Ok(Derivation::new_raw(inner))
+ }
+ }
+
+ /// Add a derivation to the store.
+ ///
+ /// **Requires Nix 2.33 or later.**
+ ///
+ /// This computes the store path for the derivation and registers it in the store.
+ /// The derivation itself is written to the store as a `.drv` file.
+ ///
+ /// # Parameters
+ /// - `drv`: The derivation to add
+ ///
+ /// # Returns
+ /// The store path of the derivation (ending in `.drv`).
+ #[cfg(nix_at_least = "2.33.0pre")]
+ #[doc(alias = "nix_add_derivation")]
+ pub fn add_derivation(&mut self, drv: &Derivation) -> Result {
+ unsafe {
+ let path = check_call!(raw::add_derivation(
+ &mut self.context,
+ self.inner.ptr(),
+ drv.inner.as_ptr()
+ ))?;
+ let path =
+ NonNull::new(path).ok_or_else(|| Error::msg("add_derivation returned null"))?;
+ Ok(StorePath::new_raw(path))
+ }
+ }
+
+ /// Build a derivation and return its outputs.
+ ///
+ /// **Requires Nix 2.33 or later.**
+ ///
+ /// This builds the derivation at the given store path and returns a map of output
+ /// names to their realized store paths. The derivation must already exist in the store
+ /// (see [`Store::add_derivation`]).
+ ///
+ /// # Parameters
+ /// - `path`: The store path of the derivation to build (typically ending in `.drv`)
+ ///
+ /// # Returns
+ /// A [`BTreeMap`] mapping output names (e.g., "out", "dev", "doc") to their store paths.
+ /// The map is ordered alphabetically by output name for deterministic iteration.
+ #[cfg(nix_at_least = "2.33.0pre")]
+ #[doc(alias = "nix_store_realise")]
+ pub fn realise(&mut self, path: &StorePath) -> Result> {
+ let mut outputs = BTreeMap::new();
+ let userdata =
+ &mut outputs as *mut BTreeMap as *mut std::os::raw::c_void;
+
+ unsafe extern "C" fn callback(
+ userdata: *mut std::os::raw::c_void,
+ outname: *const c_char,
+ out_path: *const raw::StorePath,
+ ) {
+ let outputs = userdata as *mut BTreeMap;
+ let outputs = &mut *outputs;
+
+ let name = std::ffi::CStr::from_ptr(outname)
+ .to_string_lossy()
+ .into_owned();
+
+ let path = raw::store_path_clone(out_path);
+ let path = NonNull::new(path).expect("store_path_clone returned null");
+ let path = StorePath::new_raw(path);
+
+ outputs.insert(name, path);
+ }
+
+ unsafe {
+ check_call!(raw::store_realise(
+ &mut self.context,
+ self.inner.ptr(),
+ path.as_ptr(),
+ userdata,
+ Some(callback)
+ ))?;
+ }
+
+ Ok(outputs)
+ }
+
+ /// Get the closure of a specific store path.
+ ///
+ /// **Requires Nix 2.33 or later.**
+ ///
+ /// Computes the filesystem closure (dependency graph) of a store path, with options
+ /// to control the direction and which related paths to include.
+ ///
+ /// # Parameters
+ /// - `store_path`: The path to compute the closure from
+ /// - `flip_direction`: If false, compute the forward closure (paths referenced by this path).
+ /// If true, compute the backward closure (paths that reference this path).
+ /// - `include_outputs`: When `flip_direction` is false: for any derivation in the closure, include its outputs.
+ /// When `flip_direction` is true: for any output in the closure, include derivations that produce it.
+ /// - `include_derivers`: When `flip_direction` is false: for any output in the closure, include the derivation that produced it.
+ /// When `flip_direction` is true: for any derivation in the closure, include its outputs.
+ ///
+ /// # Returns
+ /// A vector of store paths in the closure, in no particular order.
+ #[cfg(nix_at_least = "2.33.0pre")]
+ #[doc(alias = "nix_store_get_fs_closure")]
+ pub fn get_fs_closure(
+ &mut self,
+ store_path: &StorePath,
+ flip_direction: bool,
+ include_outputs: bool,
+ include_derivers: bool,
+ ) -> Result> {
+ let mut r = Vec::new();
+ unsafe {
+ check_call!(raw::store_get_fs_closure(
+ &mut self.context,
+ self.inner.ptr(),
+ store_path.as_ptr(),
+ flip_direction,
+ include_outputs,
+ include_derivers,
+ callback_get_result_store_path_set_data(&mut r),
+ Some(callback_get_result_store_path_set)
+ ))
+ }?;
+ Ok(r)
+ }
+
+ pub fn weak_ref(&self) -> StoreWeak {
+ StoreWeak {
+ inner: Arc::downgrade(&self.inner),
+ }
+ }
+}
+
+impl Clone for Store {
+ fn clone(&self) -> Self {
+ Store {
+ inner: self.inner.clone(),
+ context: Context::new(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use ctor::ctor;
+ use std::collections::HashMap;
+
+ use super::*;
+
+ #[ctor]
+ fn test_setup() {
+ // Initialize settings for tests
+ let _ = INIT.as_ref();
+
+ // Enable ca-derivations for all tests
+ nix_bindings_util::settings::set("experimental-features", "ca-derivations").ok();
+
+ // Disable build hooks to prevent test recursion
+ nix_bindings_util::settings::set("build-hook", "").ok();
+
+ // Set custom build dir for sandbox
+ if cfg!(target_os = "linux") {
+ nix_bindings_util::settings::set("sandbox-build-dir", "/custom-build-dir-for-test")
+ .ok();
+ }
+
+ std::env::set_var("_NIX_TEST_NO_SANDBOX", "1");
+
+ // Tests run offline
+ nix_bindings_util::settings::set("substituters", "").ok();
+ }
+
+ #[test]
+ fn none_works() {
+ let res = Store::open(None, HashMap::new());
+ res.unwrap();
+ }
+
+ #[test]
+ fn auto_works() {
+ // This is not actually a given.
+ // Maybe whatever is in NIX_REMOTE or nix.conf is really important.
+ let res = Store::open(Some("auto"), HashMap::new());
+ res.unwrap();
+ }
+
+ #[test]
+ fn invalid_uri_fails() {
+ let res = Store::open(Some("invalid://uri"), HashMap::new());
+ assert!(res.is_err());
+ }
+
+ #[test]
+ fn get_uri() {
+ let mut store = Store::open(None, HashMap::new()).unwrap();
+ let uri = store.get_uri().unwrap();
+ assert!(!uri.is_empty());
+ // must be ascii
+ assert!(uri.is_ascii());
+ // usually something like "daemon", but that's not something we can check here.
+ println!("uri: {}", uri);
+ }
+
+ #[test]
+ #[ignore] // Needs network access
+ fn get_uri_nixos_cache() {
+ let mut store = Store::open(Some("https://cache.nixos.org/"), HashMap::new()).unwrap();
+ let uri = store.get_uri().unwrap();
+ assert_eq!(uri, "https://cache.nixos.org");
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.26" /* get_storedir */)]
+ fn parse_store_path_ok() {
+ let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap();
+ let store_dir = store.get_storedir().unwrap();
+ let store_path_string =
+ format!("{store_dir}/rdd4pnr4x9rqc9wgbibhngv217w2xvxl-bash-interactive-5.2p26");
+ let store_path = store.parse_store_path(store_path_string.as_str()).unwrap();
+ let real_store_path = store.real_path(&store_path).unwrap();
+ assert_eq!(store_path.name().unwrap(), "bash-interactive-5.2p26");
+ assert_eq!(real_store_path, store_path_string);
+ }
+
+ #[test]
+ fn parse_store_path_fail() {
+ let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap();
+ let store_path_string = "bash-interactive-5.2p26".to_string();
+ let r = store.parse_store_path(store_path_string.as_str());
+ match r {
+ Err(e) => {
+ assert!(e.to_string().contains("bash-interactive-5.2p26"));
+ }
+ _ => panic!("Expected error"),
+ }
+ }
+
+ #[test]
+ fn weak_ref() {
+ let mut store = Store::open(None, HashMap::new()).unwrap();
+ let uri = store.get_uri().unwrap();
+ let weak = store.weak_ref();
+ let mut store2 = weak.upgrade().unwrap();
+ assert_eq!(store2.get_uri().unwrap(), uri);
+ }
+ #[test]
+ fn weak_ref_gone() {
+ let weak = {
+ // Concurrent tests calling Store::open will keep the weak reference to auto alive,
+ // so for this test we need to bypass the global cache.
+ let store = Store::open_uncached(None, HashMap::new()).unwrap();
+ store.weak_ref()
+ };
+ assert!(weak.upgrade().is_none());
+ assert!(weak.inner.upgrade().is_none());
+ }
+
+ #[cfg(nix_at_least = "2.33.0pre")]
+ fn create_temp_store() -> (Store, tempfile::TempDir) {
+ let temp_dir = tempfile::tempdir().unwrap();
+
+ let store_dir = temp_dir.path().join("store");
+ let state_dir = temp_dir.path().join("state");
+ let log_dir = temp_dir.path().join("log");
+
+ let store_dir_str = store_dir.to_str().unwrap();
+ let state_dir_str = state_dir.to_str().unwrap();
+ let log_dir_str = log_dir.to_str().unwrap();
+
+ let params = vec![
+ ("store", store_dir_str),
+ ("state", state_dir_str),
+ ("log", log_dir_str),
+ ];
+
+ let store = Store::open(Some("local"), params).unwrap();
+ (store, temp_dir)
+ }
+
+ fn current_system() -> Result {
+ nix_bindings_util::settings::get("system")
+ }
+
+ #[cfg(nix_at_least = "2.33")]
+ fn create_test_derivation_json() -> serde_json::Value {
+ let system = current_system().unwrap_or_else(|_| {
+ // Fallback to Rust's platform detection
+ format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)
+ });
+ serde_json::json!({
+ "args": ["-c", "echo $name foo > $out"],
+ "builder": "/bin/sh",
+ "env": {
+ "builder": "/bin/sh",
+ "name": "myname",
+ "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9",
+ "system": system
+ },
+ "inputs": {
+ "drvs": {},
+ "srcs": []
+ },
+ "name": "myname",
+ "outputs": {
+ "out": {
+ "hashAlgo": "sha256",
+ "method": "nar"
+ }
+ },
+ "system": system,
+ "version": 4
+ })
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn derivation_from_json() {
+ 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();
+ // If we got here, parsing succeeded
+ drop(drv);
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33.0pre")]
+ fn derivation_from_invalid_json() {
+ let (mut store, temp_dir) = create_temp_store();
+ let result = store.derivation_from_json("not valid json");
+ assert!(result.is_err());
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn derivation_to_json_round_trip() {
+ let (mut store, _temp_dir) = create_temp_store();
+ let original_value = create_test_derivation_json();
+
+ // Parse JSON to Derivation
+ let drv = store
+ .derivation_from_json(&original_value.to_string())
+ .unwrap();
+
+ // Convert back to JSON
+ let round_trip_json = drv.to_json_string().unwrap();
+ let round_trip_value: serde_json::Value = serde_json::from_str(&round_trip_json).unwrap();
+
+ // Verify the round-trip JSON matches the original
+ assert_eq!(
+ original_value, round_trip_value,
+ "Round-trip JSON should match original.\nOriginal: {}\nRound-trip: {}",
+ original_value, round_trip_value
+ );
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn add_derivation() {
+ 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();
+
+ // Verify we got a .drv path
+ let name = drv_path.name().unwrap();
+ assert!(name.ends_with(".drv"));
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn realise() {
+ 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();
+
+ // Build the derivation
+ let outputs = store.realise(&drv_path).unwrap();
+
+ // Verify we got the expected output
+ assert!(outputs.contains_key("out"));
+ let out_path = &outputs["out"];
+ let out_name = out_path.name().unwrap();
+ assert_eq!(out_name, "myname");
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[cfg(nix_at_least = "2.33")]
+ fn create_multi_output_derivation_json() -> serde_json::Value {
+ let system = current_system()
+ .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS));
+
+ serde_json::json!({
+ "version": 4,
+ "name": "multi-output-test",
+ "system": system,
+ "builder": "/bin/sh",
+ "args": ["-c", "echo a > $outa; echo b > $outb; echo c > $outc; echo d > $outd; echo e > $oute; echo f > $outf; echo g > $outg; echo h > $outh; echo i > $outi; echo j > $outj"],
+ "env": {
+ "builder": "/bin/sh",
+ "name": "multi-output-test",
+ "system": system,
+ "outf": "/1vkfzqpwk313b51x0xjyh5s7w1lx141mr8da3dr9wqz5aqjyr2fh",
+ "outd": "/1ypxifgmbzp5sd0pzsp2f19aq68x5215260z3lcrmy5fch567lpm",
+ "outi": "/1wmasjnqi12j1mkjbxazdd0qd0ky6dh1qry12fk8qyp5kdamhbdx",
+ "oute": "/1f9r2k1s168js509qlw8a9di1qd14g5lqdj5fcz8z7wbqg11qp1f",
+ "outh": "/1rkx1hmszslk5nq9g04iyvh1h7bg8p92zw0hi4155hkjm8bpdn95",
+ "outc": "/1rj4nsf9pjjqq9jsq58a2qkwa7wgvgr09kgmk7mdyli6h1plas4w",
+ "outb": "/1p7i1dxifh86xq97m5kgb44d7566gj7rfjbw7fk9iij6ca4akx61",
+ "outg": "/14f8qi0r804vd6a6v40ckylkk1i6yl6fm243qp6asywy0km535lc",
+ "outj": "/0gkw1366qklqfqb2lw1pikgdqh3cmi3nw6f1z04an44ia863nxaz",
+ "outa": "/039akv9zfpihrkrv4pl54f3x231x362bll9afblsgfqgvx96h198"
+ },
+ "inputs": {
+ "drvs": {},
+ "srcs": []
+ },
+ "outputs": {
+ "outd": { "hashAlgo": "sha256", "method": "nar" },
+ "outf": { "hashAlgo": "sha256", "method": "nar" },
+ "outg": { "hashAlgo": "sha256", "method": "nar" },
+ "outb": { "hashAlgo": "sha256", "method": "nar" },
+ "outc": { "hashAlgo": "sha256", "method": "nar" },
+ "outi": { "hashAlgo": "sha256", "method": "nar" },
+ "outj": { "hashAlgo": "sha256", "method": "nar" },
+ "outh": { "hashAlgo": "sha256", "method": "nar" },
+ "outa": { "hashAlgo": "sha256", "method": "nar" },
+ "oute": { "hashAlgo": "sha256", "method": "nar" }
+ }
+ })
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn realise_multi_output_ordering() {
+ let (mut store, temp_dir) = create_temp_store();
+ let drv_json = create_multi_output_derivation_json();
+ let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
+ let drv_path = store.add_derivation(&drv).unwrap();
+
+ // Build the derivation
+ let outputs = store.realise(&drv_path).unwrap();
+
+ // Verify outputs are complete (BTreeMap guarantees ordering)
+ let output_names: Vec<&String> = outputs.keys().collect();
+ let expected_order = vec![
+ "outa", "outb", "outc", "outd", "oute", "outf", "outg", "outh", "outi", "outj",
+ ];
+ assert_eq!(output_names, expected_order);
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn realise_invalid_system() {
+ let (mut store, temp_dir) = create_temp_store();
+
+ // Create a derivation with an invalid system
+ let system = "bogus65-bogusos";
+ let drv_json = serde_json::json!({
+ "args": ["-c", "echo $name foo > $out"],
+ "builder": "/bin/sh",
+ "env": {
+ "builder": "/bin/sh",
+ "name": "myname",
+ "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9",
+ "system": system
+ },
+ "inputs": {
+ "drvs": {},
+ "srcs": []
+ },
+ "name": "myname",
+ "outputs": {
+ "out": {
+ "hashAlgo": "sha256",
+ "method": "nar"
+ }
+ },
+ "system": system,
+ "version": 4
+ });
+
+ let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
+ let drv_path = store.add_derivation(&drv).unwrap();
+
+ // Try to build - should fail
+ let result = store.realise(&drv_path);
+ let err = match result {
+ Ok(_) => panic!("Build should fail with invalid system"),
+ Err(e) => e.to_string(),
+ };
+ assert!(
+ err.contains("required system or feature not available")
+ || err.contains("platform mismatch"),
+ "Error should mention system not available, got: {}",
+ err
+ );
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn realise_builder_fails() {
+ let (mut store, temp_dir) = create_temp_store();
+
+ let system = current_system()
+ .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS));
+
+ // Create a derivation where the builder exits with error
+ let drv_json = serde_json::json!({
+ "args": ["-c", "exit 1"],
+ "builder": "/bin/sh",
+ "env": {
+ "builder": "/bin/sh",
+ "name": "failing",
+ "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9",
+ "system": system
+ },
+ "inputs": {
+ "drvs": {},
+ "srcs": []
+ },
+ "name": "failing",
+ "outputs": {
+ "out": {
+ "hashAlgo": "sha256",
+ "method": "nar"
+ }
+ },
+ "system": system,
+ "version": 4
+ });
+
+ let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
+ let drv_path = store.add_derivation(&drv).unwrap();
+
+ // Try to build - should fail
+ let result = store.realise(&drv_path);
+ let err = match result {
+ Ok(_) => panic!("Build should fail when builder exits with error"),
+ Err(e) => e.to_string(),
+ };
+ assert!(
+ err.contains("builder failed with exit code 1"),
+ "Error should mention builder failed with exit code, got: {}",
+ err
+ );
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn realise_builder_no_output() {
+ let (mut store, temp_dir) = create_temp_store();
+
+ let system = current_system()
+ .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS));
+
+ // Create a derivation where the builder succeeds but produces no output
+ let drv_json = serde_json::json!({
+ "args": ["-c", "true"],
+ "builder": "/bin/sh",
+ "env": {
+ "builder": "/bin/sh",
+ "name": "no-output",
+ "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9",
+ "system": system
+ },
+ "inputs": {
+ "drvs": {},
+ "srcs": []
+ },
+ "name": "no-output",
+ "outputs": {
+ "out": {
+ "hashAlgo": "sha256",
+ "method": "nar"
+ }
+ },
+ "system": system,
+ "version": 4
+ });
+
+ let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
+ let drv_path = store.add_derivation(&drv).unwrap();
+
+ // Try to build - should fail
+ let result = store.realise(&drv_path);
+ let err = match result {
+ Ok(_) => panic!("Build should fail when builder produces no output"),
+ Err(e) => e.to_string(),
+ };
+ assert!(
+ err.contains("failed to produce output path"),
+ "Error should mention failed to produce output, got: {}",
+ err
+ );
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn get_fs_closure_with_outputs() {
+ 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();
+
+ // Build the derivation to get the output path
+ let outputs = store.realise(&drv_path).unwrap();
+ let out_path = &outputs["out"];
+ let out_path_name = out_path.name().unwrap();
+
+ // Get closure with include_outputs=true
+ let closure = store.get_fs_closure(&drv_path, false, true, false).unwrap();
+
+ // The closure should contain at least the derivation and its output
+ assert!(
+ closure.len() >= 2,
+ "Closure should contain at least drv and output"
+ );
+
+ // Verify the output path is in the closure
+ let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name);
+ assert!(
+ out_in_closure,
+ "Output path should be in closure when include_outputs=true"
+ );
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn get_fs_closure_without_outputs() {
+ 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();
+
+ // Build the derivation to get the output path
+ let outputs = store.realise(&drv_path).unwrap();
+ let out_path = &outputs["out"];
+ let out_path_name = out_path.name().unwrap();
+
+ // Get closure with include_outputs=false
+ let closure = store
+ .get_fs_closure(&drv_path, false, false, false)
+ .unwrap();
+
+ // Verify the output path is NOT in the closure
+ let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name);
+ assert!(
+ !out_in_closure,
+ "Output path should not be in closure when include_outputs=false"
+ );
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn get_fs_closure_flip_direction() {
+ 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();
+
+ // Build the derivation to get the output path
+ let outputs = store.realise(&drv_path).unwrap();
+ let out_path = &outputs["out"];
+ let out_path_name = out_path.name().unwrap();
+
+ // Get closure with flip_direction=true (reverse dependencies)
+ let closure = store.get_fs_closure(&drv_path, true, true, false).unwrap();
+
+ // Verify the output path is NOT in the closure when direction is flipped
+ let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name);
+ assert!(
+ !out_in_closure,
+ "Output path should not be in closure when flip_direction=true"
+ );
+
+ drop(store);
+ drop(temp_dir);
+ }
+
+ #[test]
+ #[cfg(nix_at_least = "2.33")]
+ fn get_fs_closure_include_derivers() {
+ 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 drv_path_name = drv_path.name().unwrap();
+
+ // Build the derivation to get the output path
+ let outputs = store.realise(&drv_path).unwrap();
+ let out_path = &outputs["out"];
+
+ // Get closure of the output path with include_derivers=true
+ let closure = store.get_fs_closure(out_path, false, false, true).unwrap();
+
+ // Verify the derivation path is in the closure
+ let drv_in_closure = closure.iter().any(|p| p.name().unwrap() == drv_path_name);
+ assert!(
+ drv_in_closure,
+ "Derivation should be in closure when include_derivers=true"
+ );
+
+ drop(store);
+ drop(temp_dir);
+ }
+}
diff --git a/nix-bindings-util-sys/Cargo.toml b/nix-bindings-util-sys/Cargo.toml
new file mode 100644
index 0000000..2441074
--- /dev/null
+++ b/nix-bindings-util-sys/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "nix-bindings-util-sys"
+version = "0.2.1"
+edition = "2021"
+build = "build.rs"
+license = "LGPL-2.1"
+description = "Low-level FFI bindings to Nix utility library"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_util_sys/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+
+[build-dependencies]
+bindgen = "0.69"
+pkg-config = "0.3"
diff --git a/nix-bindings-util-sys/README.md b/nix-bindings-util-sys/README.md
new file mode 100644
index 0000000..b504cb8
--- /dev/null
+++ b/nix-bindings-util-sys/README.md
@@ -0,0 +1,11 @@
+# nix-bindings-util-sys
+
+This crate contains generated bindings for the Nix C API (`nix-util-c`).
+**You should not have to use this crate directly,** and so you should probably not add it to your dependencies.
+Instead, use the `nix-bindings-util` crate, which _should_ be sufficient.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_util_sys/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-util-sys/build.rs b/nix-bindings-util-sys/build.rs
new file mode 100644
index 0000000..2851ee2
--- /dev/null
+++ b/nix-bindings-util-sys/build.rs
@@ -0,0 +1,52 @@
+use std::env;
+use std::path::PathBuf;
+
+#[derive(Debug)]
+struct StripNixPrefix {}
+impl bindgen::callbacks::ParseCallbacks for StripNixPrefix {
+ fn item_name(&self, name: &str) -> Option {
+ name.strip_prefix("nix_").map(String::from)
+ }
+}
+
+fn main() {
+ // Tell cargo to invalidate the built crate whenever the wrapper changes
+ println!("cargo:rerun-if-changed=include/nix-c-util.h");
+ println!("cargo:rustc-link-lib=nixutil");
+
+ // https://rust-lang.github.io/rust-bindgen/library-usage.html
+ let bindings = bindgen::Builder::default()
+ .header("include/nix-c-util.h")
+ // Find the includes
+ .clang_args(c_headers())
+ // Tell cargo to invalidate the built crate whenever any of the
+ // included header files changed.
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+ .parse_callbacks(Box::new(StripNixPrefix {}))
+ // Finish the builder and generate the bindings.
+ .generate()
+ // Unwrap the Result and panic on failure.
+ .expect("Unable to generate bindings");
+
+ // Write the bindings to the $OUT_DIR/bindings.rs file.
+ let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+ bindings
+ .write_to_file(out_path.join("bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
+
+fn c_headers() -> Vec {
+ let mut args = Vec::new();
+ // args.push("-isystem".to_string());
+ for path in pkg_config::probe_library("nix-util-c")
+ .unwrap()
+ .include_paths
+ .iter()
+ {
+ args.push(format!("-I{}", path.to_str().unwrap()));
+ }
+
+ // write to stderr for debugging
+ eprintln!("c_headers: {:?}", args);
+ args
+}
diff --git a/nix-bindings-util-sys/include/nix-c-util.h b/nix-bindings-util-sys/include/nix-c-util.h
new file mode 100644
index 0000000..7fd0bc6
--- /dev/null
+++ b/nix-bindings-util-sys/include/nix-c-util.h
@@ -0,0 +1 @@
+#include
diff --git a/nix-bindings-util-sys/src/lib.rs b/nix-bindings-util-sys/src/lib.rs
new file mode 100644
index 0000000..b7c06dc
--- /dev/null
+++ b/nix-bindings-util-sys/src/lib.rs
@@ -0,0 +1,31 @@
+//! Raw bindings to Nix C API
+//!
+//! This crate contains automatically generated bindings from the Nix C headers.
+//! The bindings are generated by bindgen and include C-style naming conventions
+//! and documentation comments that don't always conform to Rust standards.
+//!
+//! Normally you don't have to use this crate directly.
+//! Instead use `nix-util`.
+
+// This file must only contain generated code, so that the module-level
+// #![allow(...)] attributes don't suppress warnings in hand-written code.
+// If you need to add hand-written code, use a submodule to isolate the
+// generated code. See:
+// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9
+
+// Standard bindgen suppressions for C naming conventions
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// Clippy suppressions for generated C bindings
+// bindgen doesn't generate safety docs
+#![allow(clippy::missing_safety_doc)]
+// Rustdoc suppressions for generated C documentation
+// The C headers contain Doxygen-style documentation that doesn't translate
+// well to Rust's rustdoc format, causing various warnings:
+#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve
+#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs
+#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like
+#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/nix-bindings-util/Cargo.toml b/nix-bindings-util/Cargo.toml
new file mode 100644
index 0000000..78e8353
--- /dev/null
+++ b/nix-bindings-util/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "nix-bindings-util"
+version = "0.2.1"
+edition = "2021"
+license = "LGPL-2.1"
+description = "Rust bindings to Nix utility library"
+repository = "https://github.com/nixops4/nix-bindings-rust"
+documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_util/"
+readme = "README.md"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+anyhow = "1.0"
+nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" }
+
+[dev-dependencies]
+ctor = "0.2"
+
+[lints.rust]
+warnings = "deny"
+dead-code = "allow"
+
+[lints.clippy]
+type-complexity = "allow"
+# We're still trying to make Nix more thread-safe, want forward-compat
+arc-with-non-send-sync = "allow"
diff --git a/nix-bindings-util/README.md b/nix-bindings-util/README.md
new file mode 100644
index 0000000..0a29f29
--- /dev/null
+++ b/nix-bindings-util/README.md
@@ -0,0 +1,9 @@
+# nix-bindings-util
+
+Rust bindings to the Nix utility library.
+
+[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_util/)
+
+## Changelog
+
+See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md).
diff --git a/nix-bindings-util/src/context.rs b/nix-bindings-util/src/context.rs
new file mode 100644
index 0000000..dbb333e
--- /dev/null
+++ b/nix-bindings-util/src/context.rs
@@ -0,0 +1,159 @@
+use anyhow::{bail, Result};
+use nix_bindings_util_sys as raw;
+use std::ptr::null_mut;
+use std::ptr::NonNull;
+
+/// A context for error handling, when interacting directly with the generated bindings for the C API in [nix_bindings_util_sys].
+///
+/// The `nix-store` and `nix-expr` libraries that consume this type internally store a private context in their `EvalState` and `Store` structs to avoid allocating a new context for each operation. The state of a context is irrelevant when used correctly (e.g. with [check_call!]), so it's safe to reuse, and safe to allocate more contexts in methods such as [Clone::clone].
+pub struct Context {
+ inner: NonNull,
+}
+
+impl Default for Context {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Context {
+ pub fn new() -> Self {
+ let ctx = unsafe { raw::c_context_create() };
+ if ctx.is_null() {
+ // We've failed to allocate a (relatively small) Context struct.
+ // We're almost certainly going to crash anyways.
+ panic!("nix_c_context_create returned a null pointer");
+ }
+ Context {
+ inner: NonNull::new(ctx).unwrap(),
+ }
+ }
+
+ /// Access the C context pointer.
+ ///
+ /// We recommend to use `check_call!` if possible.
+ pub fn ptr(&mut self) -> *mut raw::c_context {
+ self.inner.as_ptr()
+ }
+
+ /// Check the error code and return an error if it's not `NIX_OK`.
+ ///
+ /// We recommend to use `check_call!` if possible.
+ pub fn check_err(&self) -> Result<()> {
+ let err = unsafe { raw::err_code(self.inner.as_ptr()) };
+ if err != raw::err_NIX_OK {
+ // msgp is a borrowed pointer (pointing into the context), so we don't need to free it
+ let msgp = unsafe { raw::err_msg(null_mut(), self.inner.as_ptr(), null_mut()) };
+ // Turn the i8 pointer into a Rust string by copying
+ let msg: &str = unsafe { core::ffi::CStr::from_ptr(msgp).to_str()? };
+ bail!("{}", msg);
+ }
+ Ok(())
+ }
+
+ pub fn clear(&mut self) {
+ unsafe {
+ raw::set_err_msg(self.inner.as_ptr(), raw::err_NIX_OK, c"".as_ptr());
+ }
+ }
+
+ pub fn check_err_and_clear(&mut self) -> Result<()> {
+ let r = self.check_err();
+ if r.is_err() {
+ self.clear();
+ }
+ r
+ }
+
+ pub fn check_one_call_or_key_none T>(
+ &mut self,
+ f: F,
+ ) -> Result> {
+ let t = f(self.ptr());
+ if unsafe { raw::err_code(self.inner.as_ptr()) == raw::err_NIX_ERR_KEY } {
+ self.clear();
+ return Ok(None);
+ }
+ self.check_err_and_clear()?;
+ Ok(Some(t))
+ }
+}
+
+impl Drop for Context {
+ fn drop(&mut self) {
+ unsafe {
+ raw::c_context_free(self.inner.as_ptr());
+ }
+ }
+}
+
+#[macro_export]
+macro_rules! check_call {
+ ($($f:ident)::+($ctx:expr $(, $arg:expr)*)) => {
+ {
+ let ctx : &mut $crate::context::Context = $ctx;
+ let ret = $($f)::*(ctx.ptr() $(, $arg)*);
+ match ctx.check_err() {
+ Ok(_) => Ok(ret),
+ Err(e) => {
+ ctx.clear();
+ Err(e)
+ }
+ }
+ }
+ }
+}
+
+pub use check_call;
+
+// TODO: Generalize this macro to work with any error code or any error handling logic
+#[macro_export]
+macro_rules! check_call_opt_key {
+ ($($f:ident)::+($ctx:expr, $($arg:expr),*)) => {
+ {
+ let ctx : &mut $crate::context::Context = $ctx;
+ let ret = $($f)::*(ctx.ptr(), $($arg,)*);
+ if unsafe { $crate::raw_sys::err_code(ctx.ptr()) == $crate::raw_sys::err_NIX_ERR_KEY } {
+ ctx.clear();
+ return Ok(None);
+ }
+ match ctx.check_err() {
+ Ok(_) => Ok(Some(ret)),
+ Err(e) => {
+ ctx.clear();
+ Err(e)
+ }
+ }
+ }
+ }
+}
+
+pub use check_call_opt_key;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn context_new_and_drop() {
+ // don't crash
+ let _c = Context::new();
+ }
+
+ fn set_dummy_err(ctx_ptr: *mut raw::c_context) {
+ unsafe {
+ raw::set_err_msg(
+ ctx_ptr,
+ raw::err_NIX_ERR_UNKNOWN,
+ c"dummy error message".as_ptr(),
+ );
+ }
+ }
+
+ #[test]
+ fn check_call_dynamic_context() {
+ let r = check_call!(set_dummy_err(&mut Context::new()));
+ assert!(r.is_err());
+ assert_eq!(r.unwrap_err().to_string(), "dummy error message");
+ }
+}
diff --git a/nix-bindings-util/src/lib.rs b/nix-bindings-util/src/lib.rs
new file mode 100644
index 0000000..f626d96
--- /dev/null
+++ b/nix-bindings-util/src/lib.rs
@@ -0,0 +1,8 @@
+pub mod context;
+pub mod settings;
+#[macro_use]
+pub mod string_return;
+pub mod nix_version;
+
+// Re-export for use in macros
+pub use nix_bindings_util_sys as raw_sys;
diff --git a/nix-bindings-util/src/nix_version.rs b/nix-bindings-util/src/nix_version.rs
new file mode 100644
index 0000000..b6da8ce
--- /dev/null
+++ b/nix-bindings-util/src/nix_version.rs
@@ -0,0 +1,101 @@
+//! Nix version parsing and conditional compilation support.
+
+/// Emit [`cargo:rustc-cfg`] directives for Nix version-based conditional compilation.
+///
+/// Call from build.rs with the Nix version and desired version gates.
+///
+/// [`cargo:rustc-cfg`]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-cfg
+///
+/// # Example
+///
+/// ```
+/// use nix_bindings_util::nix_version::emit_version_cfg;
+/// # // Stub pkg_config so that we can render a full usage example
+/// # mod pkg_config { pub fn probe_library(_: &str) -> Result { Ok(Library { version: "2.33.0pre".into() }) }
+/// # pub struct Library { pub version: String } }
+///
+/// let nix_version = pkg_config::probe_library("nix-store-c").unwrap().version;
+/// emit_version_cfg(&nix_version, &["2.26", "2.33.0pre", "2.33"]);
+/// ```
+///
+/// Emits `nix_at_least="2.26"` and `nix_at_least="2.33.0pre"` for version 2.33.0pre,
+/// usable as `#[cfg(nix_at_least = "2.26")]`.
+pub fn emit_version_cfg(nix_version: &str, relevant_versions: &[&str]) {
+ // Declare the known versions for cargo check-cfg
+ let versions = relevant_versions
+ .iter()
+ .map(|v| format!("\"{}\"", v))
+ .collect::>()
+ .join(",");
+
+ println!(
+ "cargo:rustc-check-cfg=cfg(nix_at_least,values({}))",
+ versions
+ );
+
+ let nix_version = parse_version(nix_version);
+
+ for version_str in relevant_versions {
+ let version = parse_version(version_str);
+ if nix_version >= version {
+ println!("cargo:rustc-cfg=nix_at_least=\"{}\"", version_str);
+ }
+ }
+}
+
+/// Parse a Nix version string into a comparable tuple `(major, minor, patch)`.
+///
+/// Pre-release versions (containing `"pre"`) get patch = -1, sorting before stable releases.
+/// Omitted patch defaults to 0.
+///
+/// # Examples
+///
+/// ```
+/// use nix_bindings_util::nix_version::parse_version;
+///
+/// assert_eq!(parse_version("2.26"), (2, 26, 0));
+/// assert_eq!(parse_version("2.33.0pre"), (2, 33, -1));
+/// assert_eq!(parse_version("2.33"), (2, 33, 0));
+/// assert_eq!(parse_version("2.33.1"), (2, 33, 1));
+///
+/// // Pre-release versions sort before stable
+/// assert!(parse_version("2.33.0pre") < parse_version("2.33"));
+/// ```
+pub fn parse_version(version_str: &str) -> (u32, u32, i32) {
+ let parts = version_str.split('.').collect::>();
+ let major = parts[0].parse::().unwrap();
+ let minor = parts[1].parse::().unwrap();
+ let patch = if parts.get(2).is_some_and(|s| s.contains("pre")) {
+ -1i32
+ } else {
+ parts
+ .get(2)
+ .and_then(|s| s.parse::().ok())
+ .unwrap_or(0)
+ };
+ (major, minor, patch)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_version() {
+ assert_eq!(parse_version("2.26"), (2, 26, 0));
+ assert_eq!(parse_version("2.33.0pre"), (2, 33, -1));
+ assert_eq!(parse_version("2.33"), (2, 33, 0));
+ assert_eq!(parse_version("2.33.1"), (2, 33, 1));
+ }
+
+ #[test]
+ fn test_version_ordering() {
+ // Pre-release versions should sort before stable
+ assert!(parse_version("2.33.0pre") < parse_version("2.33"));
+ assert!(parse_version("2.33.0pre") < parse_version("2.33.0"));
+
+ // Normal version ordering
+ assert!(parse_version("2.26") < parse_version("2.33"));
+ assert!(parse_version("2.33") < parse_version("2.33.1"));
+ }
+}
diff --git a/nix-bindings-util/src/settings.rs b/nix-bindings-util/src/settings.rs
new file mode 100644
index 0000000..8c213aa
--- /dev/null
+++ b/nix-bindings-util/src/settings.rs
@@ -0,0 +1,104 @@
+use anyhow::Result;
+use nix_bindings_util_sys as raw;
+use std::sync::Mutex;
+
+use crate::{
+ check_call, context, result_string_init,
+ string_return::{callback_get_result_string, callback_get_result_string_data},
+};
+
+// Global mutex to protect concurrent access to Nix settings
+// See the documentation on `set()` for important thread safety information.
+static SETTINGS_MUTEX: Mutex<()> = Mutex::new(());
+
+/// Set a Nix setting.
+///
+/// # Thread Safety
+///
+/// This function uses a mutex to serialize access through the Rust API.
+/// However, the underlying Nix settings system uses global mutable state
+/// without internal synchronization.
+///
+/// The mutex provides protection between Rust callers but cannot prevent:
+/// - C++ Nix code from modifying settings concurrently
+/// - Other Nix operations from reading settings during modification
+///
+/// For multi-threaded applications, ensure that no other Nix operations
+/// are running while changing settings. Settings are best modified during
+/// single-threaded initialization.
+pub fn set(key: &str, value: &str) -> Result<()> {
+ // Lock the mutex to ensure thread-safe access to global settings
+ let guard = SETTINGS_MUTEX.lock().unwrap();
+
+ let mut ctx = context::Context::new();
+ let key = std::ffi::CString::new(key)?;
+ let value = std::ffi::CString::new(value)?;
+ unsafe {
+ check_call!(raw::setting_set(&mut ctx, key.as_ptr(), value.as_ptr()))?;
+ }
+ drop(guard);
+ Ok(())
+}
+
+/// Get a Nix setting.
+///
+/// # Thread Safety
+///
+/// See the documentation on [`set()`] for important thread safety information.
+pub fn get(key: &str) -> Result {
+ // Lock the mutex to ensure thread-safe access to global settings
+ let guard = SETTINGS_MUTEX.lock().unwrap();
+
+ let mut ctx = context::Context::new();
+ let key = std::ffi::CString::new(key)?;
+ let mut r: Result = result_string_init!();
+ unsafe {
+ check_call!(raw::setting_get(
+ &mut ctx,
+ key.as_ptr(),
+ Some(callback_get_result_string),
+ callback_get_result_string_data(&mut r)
+ ))?;
+ }
+ drop(guard);
+ r
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::check_call;
+
+ use super::*;
+
+ #[ctor::ctor]
+ fn setup() {
+ let mut ctx = context::Context::new();
+ unsafe {
+ check_call!(nix_bindings_util_sys::libutil_init(&mut ctx)).unwrap();
+ }
+ }
+
+ #[test]
+ fn set_get() {
+ // Something that shouldn't matter if it's a different value temporarily
+ let key = "json-log-path";
+
+ // Save the old value, in case it's important. Probably not.
+ // If this doesn't work, pick a different setting to test with
+ let old_value = get(key).unwrap();
+
+ let new_value = "/just/a/path/that/we/are/storing/into/some/option/for/testing/purposes";
+
+ let res_e = (|| {
+ set(key, new_value)?;
+ get(key)
+ })();
+
+ // Restore immediately; try not to affect other tests (if relevant).
+ set(key, old_value.as_str()).unwrap();
+
+ let res = res_e.unwrap();
+
+ assert_eq!(res, new_value);
+ }
+}
diff --git a/nix-bindings-util/src/string_return.rs b/nix-bindings-util/src/string_return.rs
new file mode 100644
index 0000000..205cebe
--- /dev/null
+++ b/nix-bindings-util/src/string_return.rs
@@ -0,0 +1,88 @@
+use anyhow::Result;
+
+/// Callback for nix_store_get_uri and other functions that return a string.
+///
+/// This function is used by the other nix_* crates, and you should never need to call it yourself.
+///
+/// Some functions in the nix library "return" strings without giving you ownership over them, by letting you pass a callback function that gets to look at that string. This callback simply turns that string pointer into an owned rust String.
+///
+/// # Safety
+///
+/// _Manual memory management_
+///
+/// Only for passing to the nix C API. Do not call this function directly.
+pub unsafe extern "C" fn callback_get_result_string(
+ start: *const ::std::os::raw::c_char,
+ n: std::os::raw::c_uint,
+ user_data: *mut std::os::raw::c_void,
+) {
+ let ret = user_data as *mut Result