diff --git a/.envrc b/.envrc index 522d48a..bb13dae 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1 @@ -source .env -# Tell direnv to load the 'default' from your flake.nix -use flake .#default +use devenv diff --git a/.gitignore b/.gitignore index 0a64901..1b7f2e8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,13 +7,16 @@ Cargo.lock .idea/ .vscode/ -#Config +# Config loft.toml attic/ -#env +# Env / Direnv / Nix .direnv +.devenv/ +.devenv.flake.nix .env result .loft_scan_complete .loft_cache.db +.pre-commit-config.yaml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..10c1dff --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,72 @@ +# Agent Working Guide — Loft + +## Environment + +This project uses **devenv** for both builds and the development environment. Everything is defined in `devenv.nix` — `flake.nix` just wires outputs. + +## Available Commands + +| Command | Description | +| ------------------------------- | -------------------- | +| `devenv shell -- cargo build` | Build | +| `devenv shell -- cargo test` | Run tests | +| `devenv shell -- cargo fmt` | Format code | +| `devenv shell -- cargo clippy` | Lint | +| `devenv test` | Run test suite | +| `nix flake check` | Run all checks | +| `nix build` | Full sandboxed build | +| `cache-test` | Test cache workflow | + +## Checks + +| Check | Command | +| -------------- | ---------------------------------------------- | +| Formatting | `treefmt --fail-on-change` | +| Clippy | `nix build .#checks..clippy` | +| Unit tests | `nix build .#checks..unit-tests` | +| Integration | `nix build .#checks..integration` | + +## NixOS Module + +The flake exports `nixosModules.loft` — a NixOS module for running loft as a systemd service. Enable it with: + +```nix +{ + imports = [ inputs.loft.nixosModules.loft ]; + services.loft = { + enable = true; + s3 = { + bucket = "my-cache"; + endpoint = "https://s3.example.com"; + accessKeyFile = "/run/secrets/s3-access-key"; + secretKeyFile = "/run/secrets/s3-secret-key"; + }; + }; +} +``` + +## Architecture + +Store operations use **nix C API** where available (`nix-bindings` vendored crate) and **CLI** for operations the C API doesn't expose yet: + +| Operation | Backend | Reason | +|-----------|---------|--------| +| `Store::open`, `store_dir`, `real_path` | C API (`nix-bindings`) | Available in `nix_api_store.h` | +| `StorePath::parse`, `hash_part` | C API (`nix-bindings`) | Available in `nix_api_store.h` | +| `query_path_info` | CLI (`nix path-info --json`) | Not in C API — requires C++ internals | +| `nar_from_path` | CLI (`nix nar dump-path`) | Not in C API — requires C++ internals | +| `compute_fs_closure` | CLI (`nix-store --query`) | Not in C API at nix 2.31 | + +**TODO**: Revisit when [nix C API](https://github.com/NixOS/nix/issues) expands to cover path info queries and NAR streaming. Tracked in `src/nix_store.rs`. + +## Adding a Dependency + +1. `devenv shell -- cargo add ` +2. Build and test: `devenv shell -- cargo build && devenv shell -- cargo test` +3. For sys crates, add system libs to `buildInputs` in `flake.nix` + +## Adding a Dependency + +1. `devenv shell -- cargo add ` +2. Build and test: `devenv shell -- cargo build && devenv shell -- cargo test` +3. For sys crates, add system libs to `buildInputs` in `crane.nix` diff --git a/Cargo.lock b/Cargo.lock index 8bd4b0f..8aecd75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,36 +166,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "attic" -version = "0.1.0" -source = "git+https://github.com/zhaofengli/attic?branch=main#2524dd1c007bc7a0a9e9c863a1b02de8d54b319b" -dependencies = [ - "async-stream", - "base64 0.22.1", - "bytes", - "cc", - "cxx", - "cxx-build", - "digest", - "displaydoc", - "ed25519-compact", - "fastcdc", - "futures", - "hex", - "lazy_static", - "nix-base32", - "regex", - "serde", - "serde_with", - "sha2", - "system-deps", - "tempfile", - "tokio", - "version-compare", - "wildmatch", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -260,7 +230,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -745,12 +715,31 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", - "shlex", + "rustc-hash 1.1.0", + "shlex 1.3.0", "syn", "which", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.9.3", + "cexpr", + "clang-sys", + "itertools", + "log", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", + "shlex 1.3.0", + "syn", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -802,14 +791,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.35" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -821,16 +810,6 @@ dependencies = [ "nom", ] -[[package]] -name = "cfg-expr" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.3" @@ -912,17 +891,6 @@ dependencies = [ "cc", ] -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - [[package]] name = "colorchoice" version = "1.0.4" @@ -1117,68 +1085,6 @@ version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" -[[package]] -name = "cxx" -version = "1.0.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ba77f286ce5c44c7ba02de894b057bc0a605a210e3d81fa83b92d94586c0e1" -dependencies = [ - "cc", - "cxxbridge-cmd", - "cxxbridge-flags", - "cxxbridge-macro", - "foldhash 0.2.0", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c56fdf6fba27288d1fda3384062692e66dc40ca41bafd15f616dd4e8b0ac909" -dependencies = [ - "cc", - "codespan-reporting", - "indexmap 2.11.0", - "proc-macro2", - "quote", - "scratch", - "syn", -] - -[[package]] -name = "cxxbridge-cmd" -version = "1.0.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ade5eb6d6e6ef9c5631eff7e4f74e0e7109140e775f124d76904c0e5e6a202" -dependencies = [ - "clap", - "codespan-reporting", - "indexmap 2.11.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99f99fe2f3f76a2ba40c5431f854efe3725c19a89f4d59966bca3ec561be940e" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b6e5fa0545804d2d8d398a1e995203a1f2403a9f0651d50546462e61a28340e" -dependencies = [ - "indexmap 2.11.0", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "darling" version = "0.20.11" @@ -1270,6 +1176,15 @@ dependencies = [ "syn", ] +[[package]] +name = "doxygen-bindgen" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ba4ed6eedf7f4ace1632149d8f0e8a65a480534024d65a7c3b9daacdedbad3" +dependencies = [ + "yap", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1358,12 +1273,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "fastcdc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf51ceb43e96afbfe4dd5c6f6082af5dfd60e220820b8123792d61963f2ce6bc" - [[package]] name = "fastrand" version = "2.3.0" @@ -1394,9 +1303,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.0" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" @@ -1420,12 +1329,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1658,7 +1561,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.1.5", + "foldhash", ] [[package]] @@ -2231,15 +2134,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c349c75e1ab4a03bd6b33fe6cbd3c479c5dd443e44ad732664d72cb0e755475" -dependencies = [ - "cc", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2275,7 +2169,6 @@ dependencies = [ "anyhow", "async-compression", "async-stream", - "attic", "aws-config", "aws-sdk-s3", "aws-smithy-runtime-api", @@ -2293,6 +2186,7 @@ dependencies = [ "http 0.2.12", "itoa", "nix-base32", + "nix-bindings", "notify", "redb", "rusqlite", @@ -2303,6 +2197,7 @@ dependencies = [ "sha2", "tempfile", "tokio", + "tokio-stream", "tokio-util", "toml", "tracing", @@ -2405,6 +2300,23 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2628953ed836273ee4262e3708a8ef63ca38bd8a922070626eef7f9e5d8d536" +[[package]] +name = "nix-bindings" +version = "0.2347.1" +dependencies = [ + "nix-bindings-sys", +] + +[[package]] +name = "nix-bindings-sys" +version = "0.2347.1" +dependencies = [ + "bindgen 0.72.1", + "cc", + "doxygen-bindgen", + "pkg-config", +] + [[package]] name = "nom" version = "7.1.3" @@ -2584,9 +2496,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "potential_utf" @@ -2861,6 +2773,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3046,12 +2964,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scratch" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" - [[package]] name = "sct" version = "0.7.1" @@ -3228,6 +3140,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -3335,25 +3253,6 @@ dependencies = [ "syn", ] -[[package]] -name = "system-deps" -version = "7.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" - [[package]] name = "tempfile" version = "3.21.0" @@ -3367,15 +3266,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thread_local" version = "1.1.9" @@ -3697,12 +3587,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-width" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" - [[package]] name = "untrusted" version = "0.9.0" @@ -3761,12 +3645,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" @@ -3883,12 +3761,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "wildmatch" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" - [[package]] name = "winapi-util" version = "0.1.10" @@ -4206,6 +4078,12 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yap" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe269e7b803a5e8e20cbd97860e136529cd83bf2c9c6d37b142467e7e1f051f" + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index ec232bd..483bcf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loft" -version = "0.3.2" +version = "0.3.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -22,7 +22,7 @@ toml = "0.8.14" tracing = "0.1.40" tracing-subscriber = { version = "0.3.20", features = ["fmt", "registry", "env-filter"] } dashmap = "6.1.0" -attic = { git = "https://github.com/zhaofengli/attic", branch = "main" } +nix-bindings = { path = "nix-bindings/nix-bindings", default-features = false, features = ["store"] } displaydoc = "0.2.5" itoa = "1.0.11" ryu = "1.0.17" @@ -37,6 +37,7 @@ hex = "0.4.3" redb = "3.0.1" console-subscriber = "0.4.1" nix-base32 = "0.2.0" +tokio-stream = "0.1" tokio-util = { version = "0.7.18", features = ["io"] } async-compression = { version = "0.4.41", features = ["tokio", "xz", "zstd"] } async-stream = "0.3.6" diff --git a/crane.nix b/crane.nix deleted file mode 100644 index f0df93c..0000000 --- a/crane.nix +++ /dev/null @@ -1,26 +0,0 @@ -# crane.nix -{ pkgs, craneLib, src, attic }: -let - # Get the Nix libraries that attic needs without the cargo vendoring conflicts - nixLibs = with pkgs; [ - nix - awscli2 - nlohmann_json - boost - brotli - libsodium - pkg-config - ]; -in -{ - inherit src; - - nativeBuildInputs = [ pkgs.pkg-config ]; - buildInputs = nixLibs; - - # Ensure pkg-config can find the Nix libraries - PKG_CONFIG_PATH = "${pkgs.nix}/lib/pkgconfig"; - - # Prevent cargo config duplication by using our own vendoring - cargoVendorDir = craneLib.vendorCargoDeps { inherit src; }; -} diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000..e168868 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,143 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1780091591, + "owner": "cachix", + "repo": "devenv", + "rev": "21d68a204558895af93ad82014f8fa83f9c9a51e", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "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" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1778507602, + "narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "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" + } + }, + "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1778507786, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "8f24a228a782e24576b155d1e39f0d914b380691", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1778274207, + "narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780197589, + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "21632e942d89bf1cce4e5a63d7e58a215a0cbfcc", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} \ No newline at end of file diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..a7a8bf4 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,168 @@ +{ pkgs, config, ... }: +{ + cachix.enable = false; + + languages.rust = { + enable = true; + channel = "stable"; + components = [ + "rustc" + "cargo" + "clippy" + "rustfmt" + "rust-analyzer" + ]; + }; + + packages = with pkgs; [ + awscli2 + rclone + nix-output-monitor + ]; + + env.PKG_CONFIG_PATH = "${pkgs.nix.dev}/lib/pkgconfig"; + env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + + outputs.loft = pkgs.rustPlatform.buildRustPackage { + pname = "loft"; + version = "0.3.3"; + src = pkgs.lib.cleanSourceWith { + src = ./.; + filter = + path: type: + let + relPath = pkgs.lib.removePrefix (toString ./.) (toString path); + in + !(pkgs.lib.hasPrefix "/target" relPath) + && !(pkgs.lib.hasPrefix "/.direnv" relPath) + && !(pkgs.lib.hasPrefix "/.devenv" relPath) + && !(pkgs.lib.hasPrefix "/vendor" relPath); + }; + nativeBuildInputs = with pkgs; [ + pkg-config + clang + llvmPackages.libclang.lib + ]; + buildInputs = with pkgs; [ + nix + nix.dev + openssl + ]; + PKG_CONFIG_PATH = "${pkgs.nix.dev}/lib/pkgconfig"; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + cargoHash = "sha256-TG3RHFyyGpRa8/9J5KOzI/hd2GyiKb01p/NOpIv3VxI="; + }; + + outputs.cache-test = import ./nix/cache-test.nix { inherit pkgs; }; + + enterShell = '' + if [ -f ./.env ]; then + set -a + source ./.env + set +a + fi + + export RCLONE_CONFIG_DIR="$(pwd)/.direnv/rclone" + mkdir -p "$RCLONE_CONFIG_DIR" + { + echo "[s3]" + echo "type = s3" + echo "access_key_id = $AWS_ACCESS_KEY_ID" + echo "secret_access_key = $AWS_SECRET_ACCESS_KEY" + if [ -n "$S3_ENDPOINT" ]; then + echo "provider = Other" + else + echo "provider = AWS" + fi + echo "s3_force_path_style = true" + if [ -n "$S3_ENDPOINT" ]; then + echo "endpoint = $S3_ENDPOINT" + fi + if [ -n "$S3_BUCKET" ]; then + echo "bucket_name = $S3_BUCKET" + fi + } > "$RCLONE_CONFIG_DIR/rclone.conf" + + { + echo "[s3-test]" + echo "type = s3" + echo "access_key_id = $AWS_ACCESS_KEY_ID" + echo "secret_access_key = $AWS_SECRET_ACCESS_KEY" + if [ -n "$S3_ENDPOINT" ]; then + echo "provider = Other" + else + echo "provider = AWS" + fi + echo "s3_force_path_style = true" + if [ -n "$S3_ENDPOINT" ]; then + echo "endpoint = $S3_ENDPOINT" + fi + echo "bucket_name = nix-cache-test" + } >> "$RCLONE_CONFIG_DIR/rclone.conf" + + export RCLONE_CONFIG="$RCLONE_CONFIG_DIR/rclone.conf" + echo "rclone config generated at $RCLONE_CONFIG" + + export LOFT_CONFIG_DIR="$(pwd)/.direnv/loft" + mkdir -p "$LOFT_CONFIG_DIR" + { + echo "[s3]" + echo "bucket = \"$S3_BUCKET\"" + echo "region = \"$S3_REGION\"" + echo "endpoint = \"$S3_ENDPOINT\"" + echo "access_key = \"$AWS_ACCESS_KEY_ID\"" + echo "secret_key = \"$AWS_SECRET_ACCESS_KEY\"" + echo "[loft]" + echo "upload_threads = $LOFT_UPLOAD_THREADS" + echo "scan_on_startup = $LOFT_SCAN_ON_STARTUP" + echo "local_cache_path = \".direnv/cache.db\"" + echo "compression = \"zstd\"" + if [ -n "$NIX_SIGNING_KEY_PATH" ]; then + echo "signing_key_path = \"$NIX_SIGNING_KEY_PATH\"" + fi + if [ -n "$NIX_SIGNING_KEY_NAME" ]; then + echo "signing_key_name = \"$NIX_SIGNING_KEY_NAME\"" + fi + if [ -n "$LOFT_SKIP_SIGNED_BY_KEYS" ]; then + echo -n "skip_signed_by_keys = [" + IFS=',' read -ra KEYS <<< "$LOFT_SKIP_SIGNED_BY_KEYS" + first=true + for key in "''${KEYS[@]}"; do + if [ "$first" = true ]; then + first=false + else + echo -n ", " + fi + echo -n "\"$key\"" + done + echo "]" + fi + } > "$LOFT_CONFIG_DIR/loft.toml" + + export LOFT_CONFIG="$LOFT_CONFIG_DIR/loft.toml" + echo "loft.toml generated at $LOFT_CONFIG" + + echo "" + echo "🧪 Cache testing script available! Run: cache-test" + echo " This will build loft with nom, then clean up all artifacts." + echo "" + echo "🧪 Run all checks (integration, clippy, unit-tests):" + echo " nix flake check" + echo "" + echo " Or individually:" + echo " nix build .#checks.${pkgs.system}.integration" + echo " nix build .#checks.${pkgs.system}.clippy" + echo " nix build .#checks.${pkgs.system}.unit-tests" + echo "" + echo " Use --rebuild to re-run a cached result (e.g. nix build .#checks.${pkgs.system}.integration --rebuild)" + ''; + + git-hooks.hooks = { + nixfmt.enable = true; + rustfmt.enable = true; + }; + + enterTest = '' + cargo test + ''; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000..3d7a8dd --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,11 @@ +inputs: + git-hooks: + url: github:cachix/git-hooks.nix + inputs: + nixpkgs: + follows: nixpkgs + rust-overlay: + url: github:oxalica/rust-overlay + inputs: + nixpkgs: + follows: nixpkgs diff --git a/flake.lock b/flake.lock index 7d1edc9..d0d1721 100644 --- a/flake.lock +++ b/flake.lock @@ -1,66 +1,92 @@ { "nodes": { - "attic-flake": { + "cachix": { "inputs": { - "crane": "crane", - "flake-compat": "flake-compat", - "flake-parts": "flake-parts", - "nix-github-actions": "nix-github-actions", - "nixpkgs": "nixpkgs", - "nixpkgs-stable": "nixpkgs-stable" + "devenv": [ + "devenv" + ], + "flake-compat": [ + "devenv", + "flake-compat" + ], + "git-hooks": [ + "devenv", + "git-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ] }, "locked": { - "lastModified": 1757683818, - "narHash": "sha256-q7q0pWT+wu5AUU1Qlbwq8Mqb+AzHKhaMCVUq/HNZfo8=", - "owner": "zhaofengli", - "repo": "attic", - "rev": "7c5d79ad62cda340cb8c80c99b921b7b7ffacf69", + "lastModified": 1777487137, + "narHash": "sha256-TuvKVBX60mqyMT6OB5JqVEh1YIWtFMR/igLCaCdC9tw=", + "owner": "cachix", + "repo": "cachix", + "rev": "a66a440c321d35f7193472c317f42a55ccd1cb93", "type": "github" }, "original": { - "owner": "zhaofengli", - "repo": "attic", + "owner": "cachix", + "ref": "latest", + "repo": "cachix", "type": "github" } }, - "crane": { + "crate2nix": { + "flake": false, "locked": { - "lastModified": 1751562746, - "narHash": "sha256-smpugNIkmDeicNz301Ll1bD7nFOty97T79m4GUMUczA=", - "owner": "ipetkov", - "repo": "crane", - "rev": "aed2020fd3dc26e1e857d4107a5a67a33ab6c1fd", + "lastModified": 1772186516, + "narHash": "sha256-8s28pzmQ6TOIUzznwFibtW1CMieMUl1rYJIxoQYor58=", + "owner": "rossng", + "repo": "crate2nix", + "rev": "ba5dd398e31ee422fbe021767eb83b0650303a6e", "type": "github" }, "original": { - "owner": "ipetkov", - "repo": "crane", + "owner": "rossng", + "repo": "crate2nix", + "rev": "ba5dd398e31ee422fbe021767eb83b0650303a6e", "type": "github" } }, - "crane_2": { + "devenv": { + "inputs": { + "cachix": "cachix", + "crate2nix": "crate2nix", + "flake-compat": "flake-compat", + "flake-parts": "flake-parts", + "ghostty": "ghostty", + "git-hooks": "git-hooks", + "nix": "nix", + "nixd": "nixd", + "nixpkgs": [ + "nixpkgs" + ], + "rust-overlay": "rust-overlay" + }, "locked": { - "lastModified": 1757183466, - "narHash": "sha256-kTdCCMuRE+/HNHES5JYsbRHmgtr+l9mOtf5dpcMppVc=", - "owner": "ipetkov", - "repo": "crane", - "rev": "d599ae4847e7f87603e7082d73ca673aa93c916d", + "lastModified": 1780091591, + "narHash": "sha256-07KVQbmtDbtTU9DlbOQiIXXS+UA+t+/aQ65iY311bSc=", + "owner": "cachix", + "repo": "devenv", + "rev": "21d68a204558895af93ad82014f8fa83f9c9a51e", "type": "github" }, "original": { - "owner": "ipetkov", - "repo": "crane", + "owner": "cachix", + "repo": "devenv", "type": "github" } }, "flake-compat": { "flake": false, "locked": { - "lastModified": 1747046372, - "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", "owner": "edolstra", "repo": "flake-compat", - "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { @@ -72,16 +98,16 @@ "flake-parts": { "inputs": { "nixpkgs-lib": [ - "attic-flake", + "devenv", "nixpkgs" ] }, "locked": { - "lastModified": 1751413152, - "narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=", + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "77826244401ea9de6e3bac47c2db46005e1f30b5", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { @@ -90,128 +116,250 @@ "type": "github" } }, - "flake-utils": { + "flake-parts_2": { "inputs": { - "systems": "systems" + "nixpkgs-lib": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, - "nix-github-actions": { + "ghostty": { + "flake": false, + "locked": { + "lastModified": 1779069789, + "narHash": "sha256-ojo+gso45/6CVSuqfSVnlWpQ4d0QeLgwok+v/g3yu0E=", + "owner": "ghostty-org", + "repo": "ghostty", + "rev": "4b7bf0b20e3baf9c1ba10c63f2ad1fd853faea8f", + "type": "github" + }, + "original": { + "owner": "ghostty-org", + "repo": "ghostty", + "type": "github" + } + }, + "git-hooks": { "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "gitignore": "gitignore", "nixpkgs": [ - "attic-flake", + "devenv", "nixpkgs" ] }, "locked": { - "lastModified": 1737420293, - "narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=", - "owner": "nix-community", - "repo": "nix-github-actions", - "rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9", + "lastModified": 1778507602, + "narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", "type": "github" }, "original": { - "owner": "nix-community", - "repo": "nix-github-actions", + "owner": "cachix", + "repo": "git-hooks.nix", "type": "github" } }, - "nixpkgs": { + "gitignore": { + "inputs": { + "nixpkgs": [ + "devenv", + "git-hooks", + "nixpkgs" + ] + }, "locked": { - "lastModified": 1751949589, - "narHash": "sha256-mgFxAPLWw0Kq+C8P3dRrZrOYEQXOtKuYVlo9xvPntt8=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "9b008d60392981ad674e04016d25619281550a9d", + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", + "owner": "hercules-ci", + "repo": "gitignore.nix", "type": "github" } }, - "nixpkgs-stable": { + "mk-shell-bin": { "locked": { - "lastModified": 1751741127, - "narHash": "sha256-t75Shs76NgxjZSgvvZZ9qOmz5zuBE8buUaYD28BMTxg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "29e290002bfff26af1db6f64d070698019460302", + "lastModified": 1677004959, + "narHash": "sha256-/uEkr1UkJrh11vD02aqufCxtbF5YnhRTIKlx5kyvf+I=", + "owner": "rrbutani", + "repo": "nix-mk-shell-bin", + "rev": "ff5d8bd4d68a347be5042e2f16caee391cd75887", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixos-25.05", - "repo": "nixpkgs", + "owner": "rrbutani", + "repo": "nix-mk-shell-bin", "type": "github" } }, - "nixpkgs_2": { + "nix": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "flake-parts": [ + "devenv", + "flake-parts" + ], + "git-hooks-nix": [ + "devenv", + "git-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ], + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" + ] + }, "locked": { - "lastModified": 1757745802, - "narHash": "sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1", + "lastModified": 1779748925, + "narHash": "sha256-meIhqGC04O5VXbKSFXSQoOKp+XCq5RMnwAk1Guo0VQo=", + "owner": "cachix", + "repo": "nix", + "rev": "0bc443c8ff235c3547d09327b48aaa2ab98b15f2", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", + "owner": "cachix", + "ref": "devenv-2.34", + "repo": "nix", "type": "github" } }, - "nixpkgs_3": { + "nix2container": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "lastModified": 1775487831, + "narHash": "sha256-2lguQpLPQaxpQCJjXhmEEAfabwsAhkP29Z7fgLzHARA=", + "owner": "nlewo", + "repo": "nix2container", + "rev": "76be9608a7f4d6c985d28b0e7be903ae2547df3e", + "type": "github" + }, + "original": { + "owner": "nlewo", + "repo": "nix2container", + "type": "github" + } + }, + "nixd": { + "inputs": { + "flake-parts": [ + "devenv", + "flake-parts" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1778381404, + "narHash": "sha256-FqhdOTA8vyoIpkHhbs2cCT7h6EWM7nsLeOYJc1ifQLE=", + "owner": "nix-community", + "repo": "nixd", + "rev": "e3e45eb76663f522e196b7f0cf34cab201db7779", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixd", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1779560665, + "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "attic-flake": "attic-flake", - "crane": "crane_2", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2", - "rust-overlay": "rust-overlay" + "devenv": "devenv", + "flake-parts": "flake-parts_2", + "mk-shell-bin": "mk-shell-bin", + "nix2container": "nix2container", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay_2" } }, "rust-overlay": { "inputs": { - "nixpkgs": "nixpkgs_3" + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1779074409, + "narHash": "sha256-6aXy8Ga41iLVM8ibddFU1O5+wYWcBGNEfZzZuL91eIc=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "2a77b5b1dc952f214e8102acdef1622b68515560", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1758076341, - "narHash": "sha256-ZKi6pyRDw2/3xU7qxd+2+lneQXUOe92TiF+10DflolM=", + "lastModified": 1780197589, + "narHash": "sha256-FVCr2Ij/jKf59a4LW481eeOF6rJRreOBrVgW/aUBTrw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "562fb6f14678eb9b8a36829140f6a4d0737776d2", + "rev": "21632e942d89bf1cce4e5a63d7e58a215a0cbfcc", "type": "github" }, "original": { @@ -220,18 +368,25 @@ "type": "github" } }, - "systems": { + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "devenv", + "nixd", + "nixpkgs" + ] + }, "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "lastModified": 1775636079, + "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default", + "owner": "numtide", + "repo": "treefmt-nix", "type": "github" } } diff --git a/flake.nix b/flake.nix index c3a17ba..6c7f0f1 100644 --- a/flake.nix +++ b/flake.nix @@ -1,310 +1,106 @@ { description = "A minimal Nix binary cache uploader for S3-compatible storage"; + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; + flake-parts.url = "github:hercules-ci/flake-parts"; + flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; + devenv.url = "github:cachix/devenv"; + devenv.inputs.nixpkgs.follows = "nixpkgs"; rust-overlay.url = "github:oxalica/rust-overlay"; - crane.url = "github:ipetkov/crane"; - attic-flake.url = "github:zhaofengli/attic"; + rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; + nix2container.url = "github:nlewo/nix2container"; + nix2container.inputs.nixpkgs.follows = "nixpkgs"; + mk-shell-bin.url = "github:rrbutani/nix-mk-shell-bin"; }; - outputs = { self, nixpkgs, flake-utils, rust-overlay, crane, attic-flake, ... }@inputs: - { - nixosModules.loft = { - # The flake now exposes a single module that internally imports the two parts. + + outputs = + inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { imports = [ - # 1. This small, anonymous module adds the overlay to the user's system. - ({ pkgs, ... }: { - nixpkgs.overlays = [ self.overlays.default ]; - }) - # 2. This is your main configuration module from the file above. - (import ./nixos/module.nix) + inputs.devenv.flakeModule ]; - }; - # Expose the overlay to make the package easily available - overlays.default = final: prev: { - loft = self.packages.${prev.system}.default; - cache-test = self.packages.${prev.system}.cache-test; - }; - } // (flake-utils.lib.eachDefaultSystem (system: - let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { - inherit system overlays; - }; - # Import the crane library - craneLib = (crane.mkLib pkgs).overrideToolchain ( - pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml - ); - # Build the application using the logic from crane.nix - loftArgs = import ./crane.nix { - inherit pkgs craneLib; - src = craneLib.cleanCargoSource (craneLib.path ./.); - attic = attic-flake.packages.${system}.default; - }; - cargoArtifacts = craneLib.buildDepsOnly loftArgs; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; - loft = craneLib.buildPackage (loftArgs // { - inherit cargoArtifacts; - }); + perSystem = + { + config, + self', + pkgs, + system, + ... + }: + let + overlays = [ (import inputs.rust-overlay) ]; + pkgsWithOverlays = import inputs.nixpkgs { + inherit system overlays; + }; - loftClippy = craneLib.cargoClippy (loftArgs // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "--all-targets -- --deny warnings"; - }); + loft = config.devenv.shells.default.outputs.loft; + cache-test = config.devenv.shells.default.outputs.cache-test; - loftNextest = craneLib.cargoTest (loftArgs // { - inherit cargoArtifacts; - }); + pkgsForTest = import inputs.nixpkgs { + inherit system; + overlays = overlays ++ [ (final: prev: { inherit loft; }) ]; + }; + in + { + packages = { + default = loft; + inherit cache-test; + }; - # New pkgs for tests - pkgsForTest = import nixpkgs { - inherit system; - overlays = [ - (import rust-overlay) - (final: prev: { loft = loft; }) - ]; - }; - - # Cache testing script - cache-test = pkgs.writeShellScriptBin "cache-test" '' - #!/usr/bin/env bash - set -euo pipefail - - echo "🧪 Testing cache behavior for loft..." - echo "Building with nom (no symlinks)..." - - # Build without creating symlinks and capture the store paths - PATHS=$(${pkgs.nix-output-monitor}/bin/nom build .#default --print-out-paths --no-link 2>&1 | tee /dev/stderr | tail -1) - - # Extract just the store paths (nom adds extra output) - STORE_PATHS=$(echo "$PATHS" | grep -o '/nix/store/[^[:space:]]*' || echo "$PATHS") - - echo "📦 Built store paths:" - # Convert to array to avoid subshell issues - declare -a PATH_ARRAY - while IFS= read -r path; do - if [[ -n "$path" ]]; then - echo " $path" - PATH_ARRAY+=("$path") - fi - done <<< "$STORE_PATHS" - - echo "" - echo "🚀 Using fresh loft to upload itself to cache..." - - # Use the freshly built loft to upload each store path - for path in "''${PATH_ARRAY[@]}"; do - if [[ "$path" =~ ^/nix/store/ ]]; then - echo "📤 Uploading to cache: $path" - - # Check if the loft binary exists - LOFT_BIN="$path/bin/loft" - if [[ ! -f "$LOFT_BIN" ]]; then - echo "❌ loft binary not found at: $LOFT_BIN" - echo " Contents of $path:" - ls -la "$path" || echo " Cannot list directory" - if [[ -d "$path/bin" ]]; then - echo " Contents of $path/bin:" - ls -la "$path/bin" - fi - exit 1 - fi - - # echo "🔍 Using loft binary: $LOFT_BIN" - # echo "🔍 Checking binary details:" - # file "$LOFT_BIN" || echo " file command failed" - # ldd "$LOFT_BIN" || echo " ldd failed (static binary?)" - - # echo "🔍 Testing direct execution:" - # if sudo "$LOFT_BIN" --help >/dev/null 2>&1; then - # echo " ✅ Binary executes successfully" - # else - # echo " ❌ Binary execution failed" - # echo " Trying without sudo:" - # if "$LOFT_BIN" --help >/dev/null 2>&1; then - # echo " ✅ Works without sudo" - # else - # echo " ❌ Still fails without sudo" - # fi - # fi - - if "$LOFT_BIN" --config .direnv/loft/loft.toml --upload-path "$path"; then - echo "✅ Successfully uploaded: $path" - else - echo "❌ Failed to upload: $path" - exit 1 - fi - fi - done - - # echo "" - # echo "🧹 Cleaning up build artifacts..." - - # Remove any result symlinks that might exist - # rm -f result* - - # Try to delete the specific store paths - # for path in "''${PATH_ARRAY[@]}"; do - # if [[ "$path" =~ ^/nix/store/ ]]; then - # echo "Attempting to delete: $path" - # if nix store delete "$path" 2>/dev/null; then - # echo "✅ Deleted: $path" - # else - # echo "⚠️ Could not delete $path (may have references)" - # echo " Checking what keeps it alive:" - # nix-store --query --roots "$path" 2>/dev/null || echo " No roots found" - # fi - # fi - # done - - # Run garbage collection to clean up any unreferenced paths - # echo "🗑️ Running garbage collection..." - # nix-collect-garbage --quiet - - echo "✨ Cache test complete!" - echo "" - echo "💡 To test cache hit, run this script again - it should pull from your cache!" - ''; - in - { - packages = { - default = loft; - cache-test = cache-test; - }; - checks = { - integration = pkgsForTest.nixosTest (import ./nixos/tests/integration.nix); - clippy = loftClippy; - unit-tests = loftNextest; - }; - devShells = { - default = craneLib.devShell { - inputsFrom = [ attic-flake.devShells.${system}.default ]; - # Additional development tools - packages = with pkgs; [ - rust-analyzer - # For interacting with Garage S3 - awscli2 - # For interacting with the Nix store - nix - # For interacting with S3-compatible storage (e.g., MinIO, Garage) - # Remember to configure rclone (e.g., rclone config) for your S3 bucket. - rclone - # For openssl-sys dependency of reqwest - pkg-config - openssl - # Add our cache testing script - cache-test - # nom for better build output - nix-output-monitor + devenv.shells.default = { + imports = [ ./devenv.nix ]; + packages = [ + config.packages.default + config.packages.cache-test ]; - shellHook = '' - # Source the .env file to make environment variables available - if [ -f ./.env ]; then - source ./.env - fi - export RCLONE_CONFIG_DIR="$(pwd)/.direnv/rclone" - mkdir -p "$RCLONE_CONFIG_DIR" - cat > "$RCLONE_CONFIG_DIR/rclone.conf" << EOF -[s3] -type = s3 -access_key_id = $AWS_ACCESS_KEY_ID -secret_access_key = $AWS_SECRET_ACCESS_KEY -EOF - if [ -n "$S3_ENDPOINT" ]; then - echo "provider = Other" >> "$RCLONE_CONFIG_DIR/rclone.conf" - else - echo "provider = AWS" >> "$RCLONE_CONFIG_DIR/rclone.conf" - fi - cat >> "$RCLONE_CONFIG_DIR/rclone.conf" << EOF -s3_force_path_style = true -EOF - # Add endpoint and bucket if they exist in environment variables - if [ -n "$S3_ENDPOINT" ]; then - echo "endpoint = $S3_ENDPOINT" >> "$RCLONE_CONFIG_DIR/rclone.conf" - fi - if [ -n "$S3_BUCKET" ]; then - echo "bucket_name = $S3_BUCKET" >> "$RCLONE_CONFIG_DIR/rclone.conf" - fi - cat >> "$RCLONE_CONFIG_DIR/rclone.conf" << EOF -[s3-test] -type = s3 -access_key_id = $AWS_ACCESS_KEY_ID -secret_access_key = $AWS_SECRET_ACCESS_KEY -EOF - if [ -n "$S3_ENDPOINT" ]; then - echo "provider = Other" >> "$RCLONE_CONFIG_DIR/rclone.conf" - else - echo "provider = AWS" >> "$RCLONE_CONFIG_DIR/rclone.conf" - fi - cat >> "$RCLONE_CONFIG_DIR/rclone.conf" << EOF -s3_force_path_style = true -EOF - # Add endpoint and bucket if they exist in environment variables - if [ -n "$S3_ENDPOINT" ]; then - echo "endpoint = $S3_ENDPOINT" >> "$RCLONE_CONFIG_DIR/rclone.conf" - fi - echo "bucket_name = nix-cache-test" >> "$RCLONE_CONFIG_DIR/rclone.conf" - export RCLONE_CONFIG="$RCLONE_CONFIG_DIR/rclone.conf" - echo "rclone config generated at $RCLONE_CONFIG" - # Generate loft.toml - export LOFT_CONFIG_DIR="$(pwd)/.direnv/loft" - mkdir -p "$LOFT_CONFIG_DIR" - # Expand variables with defaults - cat > "$LOFT_CONFIG_DIR/loft.toml" << EOF -[s3] -bucket = "$S3_BUCKET" -region = "$S3_REGION" -endpoint = "$S3_ENDPOINT" -access_key = "$AWS_ACCESS_KEY_ID" -secret_key = "$AWS_SECRET_ACCESS_KEY" -[loft] -upload_threads = $LOFT_UPLOAD_THREADS -scan_on_startup = $LOFT_SCAN_ON_STARTUP -local_cache_path = ".direnv/cache.db" -compression = "zstd" -EOF - if [ -n "$NIX_SIGNING_KEY_PATH" ]; then - echo "signing_key_path = \"$NIX_SIGNING_KEY_PATH\"" >> "$LOFT_CONFIG_DIR/loft.toml" - fi - if [ -n "$NIX_SIGNING_KEY_NAME" ]; then - echo "signing_key_name = \"$NIX_SIGNING_KEY_NAME\"" >> "$LOFT_CONFIG_DIR/loft.toml" - fi - if [ -n "$LOFT_SKIP_SIGNED_BY_KEYS" ]; then - IFS=',' read -ra ADDR <<< "$LOFT_SKIP_SIGNED_BY_KEYS" - printf 'skip_signed_by_keys = [' >> "$LOFT_CONFIG_DIR/loft.toml" - for i in "''${ADDR[@]}"; do - printf '"%s",' "$i" >> "$LOFT_CONFIG_DIR/loft.toml" - done - # Remove trailing comma and close array - sed -i 's/,$//' "$LOFT_CONFIG_DIR/loft.toml" - printf ']\n' >> "$LOFT_CONFIG_DIR/loft.toml" - fi - export LOFT_CONFIG="$LOFT_CONFIG_DIR/loft.toml" - echo "loft.toml generated at $LOFT_CONFIG" - - echo "" - echo "🧪 Cache testing script available! Run: cache-test" - echo " This will build loft with nom, then clean up all artifacts." - echo "" - echo "🧪 Run all checks (integration, clippy, unit-tests):" - echo " nix flake check" - echo "" - echo " Or individually:" - echo " nix build .#checks.${system}.integration" - echo " nix build .#checks.${system}.clippy" - echo " nix build .#checks.${system}.unit-tests" - echo "" - echo " Use --rebuild to re-run a cached result (e.g. nix build .#checks.${system}.integration --rebuild)" - ''; + devenv.root = toString ./.; + }; + + checks = { + check = loft.overrideAttrs ( + final: prev: { + pname = "${prev.pname}-check"; + nativeBuildInputs = prev.nativeBuildInputs or [ ] ++ [ pkgs.clippy ]; + doCheck = true; + checkPhase = '' + cargo clippy --all-targets -- --deny warnings + cargo test + ''; + installPhase = "mkdir -p $out"; + } + ); + integration = pkgsForTest.testers.nixosTest (import ./nixos/tests/integration.nix); }; + + formatter = pkgsWithOverlays.nixfmt; }; - apps.default = flake-utils.lib.mkApp { - drv = self.packages."${system}".default; + + flake = { + nixosModules.loft = { + imports = [ + ( + { pkgs, ... }: + { + nixpkgs.overlays = [ inputs.self.overlays.default ]; + } + ) + (import ./nixos/module.nix) + ]; }; - apps.cache-test = flake-utils.lib.mkApp { - drv = cache-test; + + overlays.default = final: prev: { + loft = inputs.self.packages.${prev.system}.default; + cache-test = inputs.self.packages.${prev.system}.cache-test; }; - } - )); + }; + }; } diff --git a/nix-bindings/Cargo.lock b/nix-bindings/Cargo.lock new file mode 100644 index 0000000..7530d8a --- /dev/null +++ b/nix-bindings/Cargo.lock @@ -0,0 +1,782 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "doxygen-bindgen" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ba4ed6eedf7f4ace1632149d8f0e8a65a480534024d65a7c3b9daacdedbad3" +dependencies = [ + "yap", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nix-bindings" +version = "0.2347.1" +dependencies = [ + "nix-bindings-sys", + "serial_test", + "tempfile", +] + +[[package]] +name = "nix-bindings-sys" +version = "0.2347.1" +dependencies = [ + "bindgen", + "cc", + "doxygen-bindgen", + "pkg-config", + "serial_test", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "yap" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe269e7b803a5e8e20cbd97860e136529cd83bf2c9c6d37b142467e7e1f051f" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/nix-bindings/Cargo.toml b/nix-bindings/Cargo.toml new file mode 100644 index 0000000..f125a0a --- /dev/null +++ b/nix-bindings/Cargo.toml @@ -0,0 +1,31 @@ +[workspace] +members = [ "nix-bindings", "nix-bindings-sys" ] +resolver = "3" + +[workspace.package] +description = "Rust binding for Nix, the build tool" +edition = "2024" +license = "MIT" +readme = true +repository = "https://github.com/notashelf/nix-bindings" +rust-version = "1.90.0" +version = "0.2347.1" + +[workspace.dependencies] +nix-bindings-sys = { path = "./nix-bindings-sys", version = "0.2347.1" } + +bindgen = { default-features = false, features = [ "logging", "runtime" ], version = "0.72.1" } +cc = "1.2.62" +doxygen-bindgen = "0.1.3" +pkg-config = "0.3.33" +serial_test = "3.4.0" +tempfile = "3.27.0" + +# Building bindgen with optimizations makes the build script run faster, more +# than it is offset by the additional build time added to the crate itself by +# enabling optimizations. Since we work with bindgen, might as well. +# P.S.: it ranges from 3x to 5x faster depending on my system, and it'll only +# get better as the C API expands in size. So all things considered this is a +# good thing :) +[profile.dev.package.bindgen] +opt-level = 3 diff --git a/nix-bindings/LICENSE b/nix-bindings/LICENSE new file mode 100644 index 0000000..8a75913 --- /dev/null +++ b/nix-bindings/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 NotAShelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nix-bindings/README.md b/nix-bindings/README.md new file mode 100644 index 0000000..cd31059 --- /dev/null +++ b/nix-bindings/README.md @@ -0,0 +1,151 @@ + + +# nix-bindings + +[![Rust Version](https://img.shields.io/badge/rust-1.90.0+-orange.svg)](https://www.rust-lang.org) + +[C API]: https://nix.dev/manual/nix/2.32/c-api +[Nix]: https://nixos.org +[rust-bindgen]: https://github.com/rust-lang/rust-bindgen +[more accessible documentation]: https://notashelf.github.io/nix-bindings/nix_bindings_sys/index.html +[this blog post]: https://fzakaria.com/2025/08/17/using-nix-as-a-library +[discouraged by Bindgen]: https://rust-lang.github.io/rust-bindgen/cpp.html +[doxygen-bindgen]: https://github.com/rich-ayr/doxygen-bindgen + +Rust FFI bindings and a robust high-level wrapper for the experimental [C API] +of the [Nix] build tool. + +The goal of this repository is to provide generated bindings for the Nix C API, +then use those low-level bindings to build a safe, high-level Rust API for +interacting with Nix directly. In particular, `nix-bindings` aims to provide +programmatic access to Nix store operations from Rust without ever spawning +processes to interact with the Nix CLI. + +The repository contains a large test suite, examples, safety documentation, and +[more accessible documentation] for the generated bindings. [^1] + +[^1]: The Doxygen format used by Nix in the C API is not very easily parsable. + The [doxygen-bindgen] crate does a pretty good job at this, but the + documentation for the raw bindings, i.e. `nix-bindings-sys`, is not always + up to standard. Either way, consider this a work in progress and something + that will be improved over time as time and resources allow. + +The **low-level** bindings generated via [rust-bindgen] are located in the +`nix-bindings-sys` crate in [`nix-bindings-sys/`](./nix-bindings-sys). By +default, the low-level bindings crate covers **all public Nix C API headers**, +including the store, evaluator, error, flake, and main APIs. + +The [`nix-bindings/`](./nix-bindings) crate wraps `nix-bindings-sys` and +attempts to provide a more robust, **high-level** API for interacting with Nix +using Rust through a cleaner interface with additional safety documentation, +testing, examples and more. + +> [!NOTE] +> Due to the limitations of rust-bindgen, there is no compatibility outside the +> public C API. As much as I would love to provide bindings for the C++ APIs, +> this seems very annoying and is [discouraged by Bindgen]. For interacting with +> C++ APIs, you might want to use C++ directly. See [this blog post] that +> inspired this repository for instructions on using Nix as a library in C++ +> projects. + +## Repository Layout + +[![nix-bindings](https://img.shields.io/crates/v/nix-bindings)](https://crates.io/crates/nix-bindings) +[![nix-bindings-sys](https://img.shields.io/crates/v/nix-bindings-sys)](https://crates.io/crates/nix-bindings-sys) +[![Documentation](https://img.shields.io/docsrs/nix-bindings/latest)](https://docs.rs/nix-bindings/latest/) + +For your convenience, this crate has been structured in a way that allows +providing low-level and high-level access to the C API at the same time through +different sub-crates. At the moment the crate layout is as follows: + +```sh +. +├── nix-bindings-sys # raw, unsafe FFI bindings to the Nix C API +└── nix-bindings # high-level, safe Rust API built on top of `nix-bindings-sys` +``` + +The `nix-bindings-sys` crate contains build wrapper (`build.rs`), as well as +tests and examples to demonstrate interacting with the generated bindings. Those +tests and examples are expected to pass, and they serve as a safeguard for when +API changes lead to breakage. This is one of the safety "promises" of this +repository, and act as safeguards against wildly breaking changes that affect +downstream. The `nix-bindings` crate contains the hand-written wrappers around +`nix-bindings-sys` and has additional helpers that you may be interested in. + +[rendered documentation]: https://notashelf.github.io/nix-bindings/ + +Please see each crate's README for installation details. Additionally, the +[rendered documentation] contains documentation generated from Rust code +comments, examples and everything else provided by individual crate READMEs. + +## Contributing + +Contributions are welcome! If you have noticed something missing and would like +to patch it yourself, I would appreciate contributions. Please: + +- Keep examples and tests small, focused, and idiomatic +- Follow Rust FFI safety best practices +- Add (or update) tests for any new API surface +- Document any new or changed behavior + +If you encounter issues with the bindings or Nix C API compatibility, please +open an issue or submit a PR with a minimal reproduction. Note that some issues +are to be filed _upstream_, so make sure you try the C API _directly_ before +filing an issue with the bindings. I am not going to close any issues for +missing C testing, but it will make our lives easier in identifying the issue. + +### Versioning + +This crate uses a normalized semver scheme derived from the upstream Nix +version: + +| Nix version | Crate version | +| --------------- | ------------- | +| 2.32.7 | `0.2327.0` | +| 2.32.7 + hotfix | `0.2327.1` | + +The crate version is `0..`: + +- The **minor** component encodes the full Nix version after the leading zero. + For example, Nix `2.32.7` becomes `0.2327.0` (concatenation: + `"2" + "32" + "7" = 2327`). +- The **patch** component is reserved for hotfixes to the Rust bindings that do + not change the target Nix version. A fix to `0.2327.0` ships as `0.2327.1`. + +Cargo's semver rules for `0.x.y` ensure that `^0.2327.0` will not pull in +`0.2328.0` (which targets a different Nix patch release) or `0.2330.0` (which +targets a different Nix minor release). Dependency consumers must update +explicitly when the target Nix version changes. + +### Release branches + +Each supported Nix release has a long-lived branch: + +- `main` tracks the latest Nix version. +- `release/v0.2324` tracks hotfixes for bindings targeting Nix `2.32.4` (branch + created from `v0.2324.0`). +- `release/v0.2327` tracks hotfixes for bindings targeting Nix `2.32.7` (branch + created from `v0.2327.0`). + +The version in `Cargo.toml` on a release branch is always the **next** pending +patch (e.g., `0.2324.1` after `v0.2324.0` was published). The `publish` workflow +tags the current version on merge, publishes it, then bumps the branch to the +next patch. + +## Caveats + +[relevant section in the Nix manual]: https://nix.dev/manual/nix/2.32/c-api + +There are some caveats with this library. Namely, the C API is still unstable +and incomplete. Not everything is directly available, and we are severely +limited by what upstream provides to us. Upstream calls this API _"C API with +the intent of becoming a stable API, which it is currently not."_ See the +[relevant section in the Nix manual] for more details and appropriate +communication channels. + +This also means that not all CLI features are exposed. Some advanced or +experimental features may require additional or upstreaming work. + +## License + +See [LICENSE](./LICENSE) for details. diff --git a/nix-bindings/nix-bindings-sys/Cargo.toml b/nix-bindings/nix-bindings-sys/Cargo.toml new file mode 100644 index 0000000..3d08c6c --- /dev/null +++ b/nix-bindings/nix-bindings-sys/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "nix-bindings-sys" +description = "Raw, unsafe FFI bindings to the Nix C API" +edition.workspace = true +version.workspace = true +repository.workspace = true +rust-version.workspace = true +license.workspace = true +publish = true +readme = "./README.md" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[features] +default = [ "full" ] +expr = [ ] +flake = [ ] +full = [ "store", "expr", "util", "flake", "main" ] +main = [ ] +store = [ ] +util = [ ] + +[lib] +path = "lib.rs" + +[build-dependencies] +bindgen.workspace = true +cc.workspace = true +doxygen-bindgen.workspace = true +pkg-config.workspace = true + +[dev-dependencies] +serial_test.workspace = true diff --git a/nix-bindings/nix-bindings-sys/README.md b/nix-bindings/nix-bindings-sys/README.md new file mode 100644 index 0000000..7a1abef --- /dev/null +++ b/nix-bindings/nix-bindings-sys/README.md @@ -0,0 +1,84 @@ +# `nix-bindings-sys` + +This crate is limited with the features of the C API, which is mostly +undocumented and hidden away for the time being. Regardless, some of features +provided by this crate (following the C API) are as follows: + +- Open and interact with Nix stores (`nix_store_open`, `nix_store_realise`, + etc.) +- Evaluate Nix expressions and call Nix functions (`nix_expr_eval_from_string`, + `nix_value_call`, etc.) + +Additionally, with limited success nix-bindings allows you to: + +- Manipulate store paths and values +- Access and set Nix configuration settings +- Retrieve and handle errors programmatically + +All while using the generated Rust bindings. + +## Usage + +Using `nix-binding-sys` is straightforward. Add it to your `Cargo.toml`: + +```toml +[dependencies] +nix-bindings-sys = "0.2324.0" # your Nix version must match the crate version. +``` + +To pull only the C libraries you need, disable default features and opt in: + + + +```toml +[dependencies] +nix-bindings-sys = { version = "0.2324.0", default-features = false, features = ["store", "expr", "util"] } +``` + + + +The default is to enable all five features for backwards compatibility and a +smoother experience `store`, `expr`, `util`, `flake`, `main`. `full` features +are available. + +> [!TIP] +> This crate is tagged when a new Nix version is released and tested with. In +> the case your Nix version is incompatible with the published crate on +> , you might also use a local path or a Git remote in your +> `Cargo.toml` as you see fit. Published versions will be supported until +> upstream deprecates the version bindings were built for, after which the crate +> will be promptly yanked. + +It is worth noting that you **must have a compatible version of the Nix C API +development headers**. The build script for `nix-bindings-sys` uses `pkg-config` +combined with feature flags to locate the necessary libraries and headers from +the environment, which is bootstrapped using Nix. You may look into `shell.nix` +to gen an idea of what is required to build nix-bindings. _Please do not create +issues for build errors unless you have verified that your environment setup is +correct_. + +## Examples + +There are various examples provided in +[`examples/`](./nix-bindings-sys/examples) directory to serve as small, +self-contained examples in case you decide to use this crate. You may run them +with `cargo run --example `, e.g., `cargo run --example eval_basic`. In +addition to providing results, the code in the examples directory can help guide +you to use this library. + +## Testing + +`nix-bindings-sys` is _meticulously_ tested for the sake of added safety and +soundness on top of the C API. The test suite is available in the +[`tests/`](./nix-bindings-sys/tests) directory and can be ran easily using +`cargo nextest run`. For the time being the tests cover: + +- Store operations +- Context management +- Configuration +- Expression evaluation +- Thunks + +The tests are integration style, and interact with a real Nix installation. If +you believe a case is not appropriately tested, please create an issue. PRs are +also welcome to extend test cases :) diff --git a/nix-bindings/nix-bindings-sys/build.rs b/nix-bindings/nix-bindings-sys/build.rs new file mode 100644 index 0000000..6e71804 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/build.rs @@ -0,0 +1,77 @@ +#![allow(clippy::expect_used)] +use std::{env, path::PathBuf, process::Command}; + +use bindgen::callbacks::ParseCallbacks; + +#[derive(Debug)] +struct ProcessComments; + +impl ParseCallbacks for ProcessComments { + fn process_comment(&self, comment: &str) -> Option { + match doxygen_bindgen::transform(comment) { + Ok(res) => Some(res), + Err(err) => { + println!("cargo:warning=Problem processing doxygen comment: {comment}\n{err}"); + None + } + } + } +} + +fn main() { + // Tell cargo to invalidate the built crate whenever the wrapper changes + println!("cargo:rerun-if-changed=include/wrapper.h"); + + // Dynamically get GCC's include path for standard headers (e.g., stdbool.h) + let gcc_include = Command::new("gcc") + .arg("-print-file-name=include") + .output() + .expect("Failed to get gcc include path") + .stdout; + let gcc_include = String::from_utf8_lossy(&gcc_include).trim().to_string(); + + // The bindgen::Builder is the main entry point to bindgen + let mut builder = bindgen::Builder::default() + .header("include/wrapper.h") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .formatter(bindgen::Formatter::Rustfmt) + .rustfmt_configuration_file(std::fs::canonicalize(".rustfmt.toml").ok()) + .parse_callbacks(Box::new(ProcessComments)) + .clang_arg(format!("-I{gcc_include}")); + + // For each enabled feature, probe the matching C library and add the + // corresponding preprocessor define so wrapper.h includes the right headers. + // + // (Cargo feature env var, preprocessor define, pkg-config lib name) + let libraries: &[(&str, &str, &str)] = &[ + ("CARGO_FEATURE_STORE", "FEATURE_STORE", "nix-store-c"), + ("CARGO_FEATURE_EXPR", "FEATURE_EXPR", "nix-expr-c"), + ("CARGO_FEATURE_UTIL", "FEATURE_UTIL", "nix-util-c"), + ("CARGO_FEATURE_FLAKE", "FEATURE_FLAKE", "nix-flake-c"), + ("CARGO_FEATURE_MAIN", "FEATURE_MAIN", "nix-main-c"), + ]; + + for (feat_var, define, lib_name) in libraries { + if env::var(feat_var).is_ok() { + let lib = pkg_config::probe_library(lib_name) + .unwrap_or_else(|_| panic!("Unable to find .pc file for {lib_name}")); + + for include_path in lib.include_paths { + builder = builder.clang_arg(format!("-I{}", include_path.display())); + } + + for link_file in lib.link_files { + println!("cargo:rustc-link-lib={}", link_file.display()); + } + + builder = builder.clang_arg(format!("-D{define}")); + } + } + + // Write the bindings to the $OUT_DIR/bindings.rs file + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + let bindings = builder.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/nix-bindings-sys/examples/eval_basic.rs b/nix-bindings/nix-bindings-sys/examples/eval_basic.rs new file mode 100644 index 0000000..77cd7f9 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/examples/eval_basic.rs @@ -0,0 +1,170 @@ +#![allow(non_upper_case_globals)] + +use std::{ + ffi::{CStr, CString}, + mem::MaybeUninit, + ptr, +}; + +use nix_bindings_sys::{ + ValueType_NIX_TYPE_ATTRS, ValueType_NIX_TYPE_INT, ValueType_NIX_TYPE_LIST, + ValueType_NIX_TYPE_STRING, nix_c_context_create, nix_c_context_free, nix_err_NIX_OK, + nix_eval_state_build, nix_eval_state_builder_free, nix_eval_state_builder_load, + nix_eval_state_builder_new, nix_expr_eval_from_string, nix_get_attr_byidx, nix_get_attrs_size, + nix_get_int, nix_get_list_byidx, nix_get_list_size, nix_get_string, nix_get_type, + nix_get_typename, nix_libexpr_init, nix_libstore_init, nix_libutil_init, nix_state_free, + nix_store_free, nix_store_open, nix_value, +}; + +fn main() { + unsafe { + let ctx = nix_c_context_create(); + if ctx.is_null() { + eprintln!("Failed to create Nix context"); + std::process::exit(1); + } + if nix_libutil_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libutil"); + std::process::exit(1); + } + if nix_libstore_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libstore"); + std::process::exit(1); + } + if nix_libexpr_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libexpr"); + std::process::exit(1); + } + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + if store.is_null() { + eprintln!("Failed to open Nix store"); + std::process::exit(1); + } + + let builder = nix_eval_state_builder_new(ctx, store); + if builder.is_null() { + eprintln!("Failed to create eval state builder"); + std::process::exit(1); + } + if nix_eval_state_builder_load(ctx, builder) != nix_err_NIX_OK { + eprintln!("Failed to load eval state builder"); + std::process::exit(1); + } + let state = nix_eval_state_build(ctx, builder); + if state.is_null() { + eprintln!("Failed to build eval state"); + std::process::exit(1); + } + + // Example 1: Evaluate a simple expression and print its type and value + let expr = CString::new("{ foo = 123; bar = [1 2 3]; baz = \"hello\"; }").unwrap(); + let path = CString::new("").unwrap(); + let mut value = MaybeUninit::::uninit(); + + let eval_err = + nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), value.as_mut_ptr()); + if eval_err != nix_err_NIX_OK { + eprintln!("Evaluation failed with error code: {eval_err}"); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + std::process::exit(1); + } + + let value_ptr = value.as_mut_ptr(); + let typ = nix_get_type(ctx, value_ptr); + let type_name = CStr::from_ptr(nix_get_typename(ctx, value_ptr)).to_string_lossy(); + println!("Top-level value type: {typ} ({type_name})"); + + // Print attribute set contents + if typ == ValueType_NIX_TYPE_ATTRS { + let attr_count = nix_get_attrs_size(ctx, value_ptr); + println!("Attrset has {attr_count} attributes:"); + for i in 0..attr_count { + let mut name_ptr: *const std::os::raw::c_char = ptr::null(); + let attr_val = nix_get_attr_byidx(ctx, value_ptr, state, i, &mut name_ptr); + if attr_val.is_null() || name_ptr.is_null() { + println!(" [invalid attr]"); + continue; + } + let name = CStr::from_ptr(name_ptr).to_string_lossy(); + let attr_type = nix_get_type(ctx, attr_val); + let attr_type_name = + CStr::from_ptr(nix_get_typename(ctx, attr_val)).to_string_lossy(); + print!(" {name}: {attr_type_name} ("); + + match attr_type { + ValueType_NIX_TYPE_INT => { + let v = nix_get_int(ctx, attr_val); + println!("{v})"); + } + ValueType_NIX_TYPE_STRING => { + extern "C" fn string_cb( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { + std::slice::from_raw_parts(start.cast::(), n as usize) + }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + let mut got: Option = None; + let _ = + nix_get_string(ctx, attr_val, Some(string_cb), (&raw mut got).cast()); + println!("{:?})", got.as_deref().unwrap_or("[invalid utf8]")); + } + ValueType_NIX_TYPE_LIST => { + let len = nix_get_list_size(ctx, attr_val); + print!("["); + for j in 0..len { + let elem = nix_get_list_byidx(ctx, attr_val, state, j); + if elem.is_null() { + print!(""); + } else if nix_get_type(ctx, elem) == ValueType_NIX_TYPE_INT { + print!("{}", nix_get_int(ctx, elem)); + } else { + print!(""); + } + if j + 1 < len { + print!(", "); + } + } + println!("])"); + } + _ => { + println!("unhandled type)"); + } + } + } + } + + // Example 2: Error handling for invalid expression + let bad_expr = CString::new("this is not valid nix").unwrap(); + let mut bad_value = MaybeUninit::::uninit(); + let bad_err = nix_expr_eval_from_string( + ctx, + state, + bad_expr.as_ptr(), + path.as_ptr(), + bad_value.as_mut_ptr(), + ); + if bad_err == nix_err_NIX_OK { + println!("Unexpectedly succeeded in evaluating invalid expression!"); + } else { + println!( + "Correctly failed to evaluate invalid expression (error code: \ + {bad_err})" + ); + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings-sys/examples/eval_construct.rs b/nix-bindings/nix-bindings-sys/examples/eval_construct.rs new file mode 100644 index 0000000..56f1385 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/examples/eval_construct.rs @@ -0,0 +1,171 @@ +#![allow(non_upper_case_globals)] + +use std::{ + ffi::{CStr, CString}, + ptr, +}; + +use nix_bindings_sys::{ + ValueType_NIX_TYPE_INT, ValueType_NIX_TYPE_STRING, nix_alloc_value, nix_bindings_builder_free, + nix_bindings_builder_insert, nix_c_context_create, nix_c_context_free, nix_err_NIX_OK, + nix_eval_state_build, nix_eval_state_builder_free, nix_eval_state_builder_load, + nix_eval_state_builder_new, nix_get_attr_byidx, nix_get_attrs_size, nix_get_int, + nix_get_list_byidx, nix_get_list_size, nix_get_string, nix_get_type, nix_get_typename, + nix_init_int, nix_init_string, nix_libexpr_init, nix_libstore_init, nix_libutil_init, + nix_list_builder_free, nix_list_builder_insert, nix_make_attrs, nix_make_bindings_builder, + nix_make_list, nix_make_list_builder, nix_state_free, nix_store_free, nix_store_open, +}; + +fn main() { + unsafe { + let ctx = nix_c_context_create(); + if ctx.is_null() { + eprintln!("Failed to create Nix context"); + std::process::exit(1); + } + if nix_libutil_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libutil"); + std::process::exit(1); + } + if nix_libstore_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libstore"); + std::process::exit(1); + } + if nix_libexpr_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libexpr"); + std::process::exit(1); + } + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + if store.is_null() { + eprintln!("Failed to open Nix store"); + std::process::exit(1); + } + + let builder = nix_eval_state_builder_new(ctx, store); + if builder.is_null() { + eprintln!("Failed to create eval state builder"); + std::process::exit(1); + } + if nix_eval_state_builder_load(ctx, builder) != nix_err_NIX_OK { + eprintln!("Failed to load eval state builder"); + std::process::exit(1); + } + let state = nix_eval_state_build(ctx, builder); + if state.is_null() { + eprintln!("Failed to build eval state"); + std::process::exit(1); + } + + // Build a Nix list: [10, 20, 30] + let list_builder = nix_make_list_builder(ctx, state, 3); + if list_builder.is_null() { + eprintln!("Failed to create list builder"); + std::process::exit(1); + } + let v1 = nix_alloc_value(ctx, state); + let v2 = nix_alloc_value(ctx, state); + let v3 = nix_alloc_value(ctx, state); + nix_init_int(ctx, v1, 10); + nix_init_int(ctx, v2, 20); + nix_init_int(ctx, v3, 30); + nix_list_builder_insert(ctx, list_builder, 0, v1); + nix_list_builder_insert(ctx, list_builder, 1, v2); + nix_list_builder_insert(ctx, list_builder, 2, v3); + + let list_val = nix_alloc_value(ctx, state); + if nix_make_list(ctx, list_builder, list_val) != nix_err_NIX_OK { + eprintln!("Failed to make list"); + std::process::exit(1); + } + nix_list_builder_free(list_builder); + + println!("Constructed Nix list:"); + let len = nix_get_list_size(ctx, list_val); + print!("["); + for i in 0..len { + let elem = nix_get_list_byidx(ctx, list_val, state, i); + if elem.is_null() { + print!(""); + } else if nix_get_type(ctx, elem) == ValueType_NIX_TYPE_INT { + print!("{}", nix_get_int(ctx, elem)); + } else { + print!(""); + } + if i + 1 < len { + print!(", "); + } + } + println!("]"); + + // Build a Nix attrset: { a = 1; b = "foo"; } + let attr_builder = nix_make_bindings_builder(ctx, state, 2); + if attr_builder.is_null() { + eprintln!("Failed to create bindings builder"); + std::process::exit(1); + } + let a_val = nix_alloc_value(ctx, state); + let b_val = nix_alloc_value(ctx, state); + nix_init_int(ctx, a_val, 1); + + #[expect(clippy::disallowed_names)] + let foo = CString::new("foo").unwrap(); + nix_init_string(ctx, b_val, foo.as_ptr()); + let a = CString::new("a").unwrap(); + let b = CString::new("b").unwrap(); + nix_bindings_builder_insert(ctx, attr_builder, a.as_ptr(), a_val); + nix_bindings_builder_insert(ctx, attr_builder, b.as_ptr(), b_val); + + let attr_val = nix_alloc_value(ctx, state); + if nix_make_attrs(ctx, attr_val, attr_builder) != nix_err_NIX_OK { + eprintln!("Failed to make attrset"); + std::process::exit(1); + } + nix_bindings_builder_free(attr_builder); + + println!("Constructed Nix attrset:"); + let attr_count = nix_get_attrs_size(ctx, attr_val); + for i in 0..attr_count { + let mut name_ptr: *const std::os::raw::c_char = ptr::null(); + let attr = nix_get_attr_byidx(ctx, attr_val, state, i, &mut name_ptr); + if attr.is_null() || name_ptr.is_null() { + println!(" [invalid attr]"); + continue; + } + let name = CStr::from_ptr(name_ptr).to_string_lossy(); + let typ = nix_get_type(ctx, attr); + let type_name = CStr::from_ptr(nix_get_typename(ctx, attr)).to_string_lossy(); + print!(" {name}: {type_name} ("); + match typ { + ValueType_NIX_TYPE_INT => { + let v = nix_get_int(ctx, attr); + println!("{v})"); + } + ValueType_NIX_TYPE_STRING => { + extern "C" fn string_cb( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = + unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + let mut got: Option = None; + let _ = nix_get_string(ctx, attr, Some(string_cb), (&raw mut got).cast()); + println!("{:?})", got.as_deref().unwrap_or("[invalid utf8]")); + } + _ => { + println!("unhandled type)"); + } + } + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings-sys/examples/eval_function.rs b/nix-bindings/nix-bindings-sys/examples/eval_function.rs new file mode 100644 index 0000000..7d3e5b2 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/examples/eval_function.rs @@ -0,0 +1,125 @@ +use std::{ + ffi::{CStr, CString}, + mem::MaybeUninit, + ptr, +}; + +use nix_bindings_sys::{ + ValueType_NIX_TYPE_INT, nix_alloc_value, nix_c_context_create, nix_c_context_free, + nix_err_NIX_OK, nix_eval_state_build, nix_eval_state_builder_free, nix_eval_state_builder_load, + nix_eval_state_builder_new, nix_expr_eval_from_string, nix_get_int, nix_get_type, + nix_get_typename, nix_init_int, nix_libexpr_init, nix_libstore_init, nix_libutil_init, + nix_state_free, nix_store_free, nix_store_open, nix_value, nix_value_call, nix_value_force, + nix_value_force_deep, +}; + +fn main() { + unsafe { + let ctx = nix_c_context_create(); + + // Google called, they want their error "handling" back. + if ctx.is_null() { + eprintln!("Failed to create Nix context"); + std::process::exit(1); + } + if nix_libutil_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libutil"); + std::process::exit(1); + } + if nix_libstore_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libstore"); + std::process::exit(1); + } + if nix_libexpr_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libexpr"); + std::process::exit(1); + } + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + if store.is_null() { + eprintln!("Failed to open Nix store"); + std::process::exit(1); + } + + let builder = nix_eval_state_builder_new(ctx, store); + if builder.is_null() { + eprintln!("Failed to create eval state builder"); + std::process::exit(1); + } + if nix_eval_state_builder_load(ctx, builder) != nix_err_NIX_OK { + eprintln!("Failed to load eval state builder"); + std::process::exit(1); + } + let state = nix_eval_state_build(ctx, builder); + if state.is_null() { + eprintln!("Failed to build eval state"); + std::process::exit(1); + } + + // Evaluate a function: (x: x * 2) + let expr = CString::new("(x: x * 2)").unwrap(); + let path = CString::new("").unwrap(); + let mut fn_val = MaybeUninit::::uninit(); + let eval_err = nix_expr_eval_from_string( + ctx, + state, + expr.as_ptr(), + path.as_ptr(), + fn_val.as_mut_ptr(), + ); + if eval_err != nix_err_NIX_OK { + eprintln!("Failed to evaluate function expression"); + std::process::exit(1); + } + + // Prepare argument: 21 + let arg_val = nix_alloc_value(ctx, state); + nix_init_int(ctx, arg_val, 21); + + // Call the function + let mut result_val = MaybeUninit::::uninit(); + let call_err = nix_value_call( + ctx, + state, + fn_val.as_mut_ptr(), + arg_val, + result_val.as_mut_ptr(), + ); + if call_err != nix_err_NIX_OK { + eprintln!("Function application failed"); + std::process::exit(1); + } + + // Force the result (should not be a thunk) + let force_err = nix_value_force(ctx, state, result_val.as_mut_ptr()); + if force_err != nix_err_NIX_OK { + eprintln!("Failed to force result value"); + std::process::exit(1); + } + + // Inspect result + let typ = nix_get_type(ctx, result_val.as_mut_ptr()); + let type_name = + CStr::from_ptr(nix_get_typename(ctx, result_val.as_mut_ptr())).to_string_lossy(); + println!("Result type: {typ} ({type_name})"); + + if typ == ValueType_NIX_TYPE_INT { + let v = nix_get_int(ctx, result_val.as_mut_ptr()); + println!("Function application result: {v}"); + } else { + println!("Unexpected result type"); + } + + // Deep force (should be a no-op for int) + let deep_force_err = nix_value_force_deep(ctx, state, result_val.as_mut_ptr()); + if deep_force_err != nix_err_NIX_OK { + eprintln!("Failed to deep force result value"); + std::process::exit(1); + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings-sys/examples/flake_basic.rs b/nix-bindings/nix-bindings-sys/examples/flake_basic.rs new file mode 100644 index 0000000..4597875 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/examples/flake_basic.rs @@ -0,0 +1,270 @@ +#![allow(non_upper_case_globals)] + +use std::{ + ffi::{CStr, CString}, + ptr, +}; + +use nix_bindings_sys::{ + ValueType_NIX_TYPE_ATTRS, nix_c_context_create, nix_c_context_free, nix_err_NIX_OK, + nix_eval_state_build, nix_eval_state_builder_free, nix_eval_state_builder_load, + nix_eval_state_builder_new, nix_flake_settings_add_to_eval_state_builder, + nix_flake_settings_free, nix_flake_settings_new, nix_get_attr_byname, nix_get_attrs_size, + nix_get_bool, nix_get_int, nix_get_list_byidx, nix_get_list_size, nix_get_string, nix_get_type, + nix_get_typename, nix_libexpr_init, nix_libstore_init, nix_libutil_init, nix_state_free, + nix_store_free, nix_store_open, +}; + +/// Print a Nix attrset recursively (string keys, string/int values) +const MAX_RECURSION_DEPTH: usize = 16; + +unsafe fn print_attrset( + ctx: *mut nix_bindings_sys::nix_c_context, + attrset: *mut nix_bindings_sys::nix_value, + state: *mut nix_bindings_sys::EvalState, + indent: usize, + depth: usize, +) { + if depth > MAX_RECURSION_DEPTH { + println!( + "{:indent$}[max recursion depth reached]", + "", + indent = indent + ); + return; + } + let attr_count = unsafe { nix_get_attrs_size(ctx, attrset) }; + for i in 0..attr_count { + let mut name_ptr: *const std::os::raw::c_char = ptr::null(); + let attr_val = + unsafe { nix_bindings_sys::nix_get_attr_byidx(ctx, attrset, state, i, &mut name_ptr) }; + if attr_val.is_null() || name_ptr.is_null() { + println!("{:indent$}[invalid attr]", "", indent = indent); + continue; + } + let name = unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy(); + let typ = unsafe { nix_get_type(ctx, attr_val) }; + let type_name = + unsafe { CStr::from_ptr(nix_get_typename(ctx, attr_val)) }.to_string_lossy(); + print!("{:indent$}{}: {} (", "", name, type_name, indent = indent); + match typ { + nix_bindings_sys::ValueType_NIX_TYPE_STRING => { + extern "C" fn string_cb( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + let mut got: Option = None; + let _ = unsafe { + nix_get_string( + ctx, + attr_val, + Some(string_cb), + &mut got as *mut Option as *mut std::ffi::c_void, + ) + }; + println!("{:?})", got.as_deref().unwrap_or("[invalid utf8]")); + } + nix_bindings_sys::ValueType_NIX_TYPE_ATTRS => { + println!(); + unsafe { print_attrset(ctx, attr_val, state, indent + 2, depth + 1) }; + println!("{:indent$})", "", indent = indent); + } + nix_bindings_sys::ValueType_NIX_TYPE_LIST => { + let len = unsafe { nix_get_list_size(ctx, attr_val) }; + print!("["); + for j in 0..len { + let elem = unsafe { nix_get_list_byidx(ctx, attr_val, state, j) }; + if elem.is_null() { + print!(""); + } else { + let elem_type = unsafe { nix_get_type(ctx, elem) }; + match elem_type { + nix_bindings_sys::ValueType_NIX_TYPE_STRING => { + extern "C" fn string_cb( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { + std::slice::from_raw_parts(start.cast::(), n as usize) + }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + let mut got: Option = None; + let _ = unsafe { + nix_get_string( + ctx, + elem, + Some(string_cb), + &mut got as *mut Option as *mut std::ffi::c_void, + ) + }; + print!("{:?}", got.as_deref().unwrap_or("[invalid utf8]")); + } + nix_bindings_sys::ValueType_NIX_TYPE_INT => { + let v = unsafe { nix_get_int(ctx, elem) }; + print!("{}", v); + } + nix_bindings_sys::ValueType_NIX_TYPE_BOOL => { + let v = unsafe { nix_get_bool(ctx, elem) }; + print!("{}", v); + } + nix_bindings_sys::ValueType_NIX_TYPE_ATTRS => { + print!("\n"); + unsafe { print_attrset(ctx, elem, state, indent + 4, depth + 1) }; + } + nix_bindings_sys::ValueType_NIX_TYPE_NULL => { + print!("null"); + } + _ => print!(""), + } + } + if j + 1 < len { + print!(", "); + } + } + println!("])"); + } + nix_bindings_sys::ValueType_NIX_TYPE_BOOL => { + let v = unsafe { nix_get_bool(ctx, attr_val) }; + println!("{})", v); + } + nix_bindings_sys::ValueType_NIX_TYPE_INT => { + let v = unsafe { nix_get_int(ctx, attr_val) }; + println!("{})", v); + } + nix_bindings_sys::ValueType_NIX_TYPE_NULL => { + println!("null)"); + } + _ => { + println!("unhandled type)"); + } + } + } +} + +fn main() { + unsafe { + // Create context and initialize libraries + let ctx = nix_c_context_create(); + if ctx.is_null() { + eprintln!("Failed to create Nix context"); + std::process::exit(1); + } + if nix_libutil_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libutil"); + std::process::exit(1); + } + if nix_libstore_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libstore"); + std::process::exit(1); + } + if nix_libexpr_init(ctx) != nix_err_NIX_OK { + eprintln!("Failed to init libexpr"); + std::process::exit(1); + } + + // Open the Nix store + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + if store.is_null() { + eprintln!("Failed to open Nix store"); + std::process::exit(1); + } + + // Create an eval state builder + let builder = nix_eval_state_builder_new(ctx, store); + if builder.is_null() { + eprintln!("Failed to create eval state builder"); + std::process::exit(1); + } + if nix_eval_state_builder_load(ctx, builder) != nix_err_NIX_OK { + eprintln!("Failed to load eval state builder"); + std::process::exit(1); + } + + // Create flake settings and add to builder + let flake_settings = nix_flake_settings_new(ctx); + if flake_settings.is_null() { + eprintln!("Failed to create flake settings"); + std::process::exit(1); + } + let err = nix_flake_settings_add_to_eval_state_builder(ctx, flake_settings, builder); + if err != nix_err_NIX_OK { + eprintln!( + "Failed to add flake settings to eval state builder (err={})", + err + ); + nix_flake_settings_free(flake_settings); + std::process::exit(1); + } + + // Build the eval state + let state = nix_eval_state_build(ctx, builder); + if state.is_null() { + eprintln!("Failed to build eval state"); + nix_flake_settings_free(flake_settings); + std::process::exit(1); + } + + // Evaluate a flake reference using `builtins.getFlake` + // We'll evaluate this repository for the test case since it's simple enough + // to be evaluated in a reasonable duration. + let expr = CString::new("builtins.getFlake \"github:NotAShelf/nix-bindings\"").unwrap(); + let path = CString::new("").unwrap(); + let mut value = std::mem::MaybeUninit::::uninit(); + + let eval_err = nix_bindings_sys::nix_expr_eval_from_string( + ctx, + state, + expr.as_ptr(), + path.as_ptr(), + value.as_mut_ptr(), + ); + if eval_err != nix_err_NIX_OK { + eprintln!("Failed to evaluate flake reference (err={})", eval_err); + nix_state_free(state); + nix_flake_settings_free(flake_settings); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + std::process::exit(1); + } + + let value_ptr = value.as_mut_ptr(); + let typ = nix_get_type(ctx, value_ptr); + let type_name = CStr::from_ptr(nix_get_typename(ctx, value_ptr)).to_string_lossy(); + println!("Top-level value type: {} ({})", typ, type_name); + + // Print the top-level outputs attribute set of the flake + if typ == ValueType_NIX_TYPE_ATTRS { + println!("Flake outputs:"); + let outputs = nix_get_attr_byname( + ctx, + value_ptr, + state, + CString::new("outputs").unwrap().as_ptr(), + ); + if !outputs.is_null() && nix_get_type(ctx, outputs) == ValueType_NIX_TYPE_ATTRS { + print_attrset(ctx, outputs, state, 2, 0); + } else { + println!(" [no outputs attr or not an attrset]"); + } + } else { + println!("Result is not an attrset, cannot print outputs."); + } + + nix_state_free(state); + nix_flake_settings_free(flake_settings); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings-sys/examples/version.rs b/nix-bindings/nix-bindings-sys/examples/version.rs new file mode 100644 index 0000000..f82d4f6 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/examples/version.rs @@ -0,0 +1,16 @@ +use std::ffi::CStr; + +use nix_bindings_sys::nix_version_get; + +fn main() { + unsafe { + let version_ptr = nix_version_get(); + if version_ptr.is_null() { + eprintln!("Failed to get Nix version (null pointer)"); + std::process::exit(1); + } + + let version = CStr::from_ptr(version_ptr).to_string_lossy(); + println!("Nix library version: {version}"); + } +} diff --git a/nix-bindings/nix-bindings-sys/include/wrapper.h b/nix-bindings/nix-bindings-sys/include/wrapper.h new file mode 100644 index 0000000..a87c86f --- /dev/null +++ b/nix-bindings/nix-bindings-sys/include/wrapper.h @@ -0,0 +1,25 @@ +// This file is a meta-wrapper for bindgen. Each section is guarded by +// a preprocessor define so that only the headers mapped to enabled Cargo +// features are actually included. + +#ifdef FEATURE_STORE +#include +#endif + +#ifdef FEATURE_UTIL +#include +#include +#endif + +#ifdef FEATURE_EXPR +#include +#include +#endif + +#ifdef FEATURE_FLAKE +#include +#endif + +#ifdef FEATURE_MAIN +#include +#endif diff --git a/nix-bindings/nix-bindings-sys/lib.rs b/nix-bindings/nix-bindings-sys/lib.rs new file mode 100644 index 0000000..38cf61f --- /dev/null +++ b/nix-bindings/nix-bindings-sys/lib.rs @@ -0,0 +1,12 @@ +#![allow(warnings, clippy::all)] + +//! # nix-bindings-sys +//! +//! Raw, unsafe FFI bindings to the Nix C API. +//! +//! ## Safety +//! +//! These bindings are generated automatically and map directly to the C API. +//! They are unsafe to use directly. Prefer using the high-level safe API in the +//! parent crate unless you know what you're doing. +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nix-bindings/nix-bindings-sys/tests/eval.rs b/nix-bindings/nix-bindings-sys/tests/eval.rs new file mode 100644 index 0000000..138dfdb --- /dev/null +++ b/nix-bindings/nix-bindings-sys/tests/eval.rs @@ -0,0 +1,1235 @@ +#![cfg(test)] + +use std::{ + ffi::{CStr, CString}, + ptr, +}; + +use nix_bindings_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn eval_init_and_state_build() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libutil_init failed: {err}"); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libstore_init failed: {err}"); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libexpr_init failed: {err}"); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn eval_simple_expression() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libutil_init failed: {err}"); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libstore_init failed: {err}"); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libexpr_init failed: {err}"); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Evaluate a simple integer expression + let expr = CString::new("1 + 2").unwrap(); + let path = CString::new("").unwrap(); + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // Force the value (should not be a thunk) + let force_err = nix_value_force(ctx, state, value); + assert_eq!(force_err, nix_err_NIX_OK); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn value_construction_and_inspection() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Int + let int_val = nix_alloc_value(ctx, state); + assert!(!int_val.is_null()); + assert_eq!(nix_init_int(ctx, int_val, 42), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, int_val), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, int_val), 42); + + // Float + let float_val = nix_alloc_value(ctx, state); + assert!(!float_val.is_null()); + assert_eq!( + nix_init_float(ctx, float_val, std::f64::consts::PI), + nix_err_NIX_OK + ); + assert_eq!(nix_get_type(ctx, float_val), ValueType_NIX_TYPE_FLOAT); + assert!((nix_get_float(ctx, float_val) - std::f64::consts::PI).abs() < 1e-10); + + // Bool + let bool_val = nix_alloc_value(ctx, state); + assert!(!bool_val.is_null()); + assert_eq!(nix_init_bool(ctx, bool_val, true), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, bool_val), ValueType_NIX_TYPE_BOOL); + assert!(nix_get_bool(ctx, bool_val)); + + // Null + let null_val = nix_alloc_value(ctx, state); + assert!(!null_val.is_null()); + assert_eq!(nix_init_null(ctx, null_val), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, null_val), ValueType_NIX_TYPE_NULL); + + // String + let string_val = nix_alloc_value(ctx, state); + assert!(!string_val.is_null()); + let s = CString::new("hello world").unwrap(); + assert_eq!(nix_init_string(ctx, string_val, s.as_ptr()), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, string_val), ValueType_NIX_TYPE_STRING); + extern "C" fn string_cb( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + let mut got: Option = None; + assert_eq!( + nix_get_string(ctx, string_val, Some(string_cb), (&raw mut got).cast()), + nix_err_NIX_OK + ); + assert_eq!(got.as_deref(), Some("hello world")); + + // Path string + let path_val = nix_alloc_value(ctx, state); + assert!(!path_val.is_null()); + let p = CString::new("/nix/store/foo").unwrap(); + assert_eq!( + nix_init_path_string(ctx, state, path_val, p.as_ptr()), + nix_err_NIX_OK + ); + assert_eq!(nix_get_type(ctx, path_val), ValueType_NIX_TYPE_PATH); + let path_ptr = nix_get_path_string(ctx, path_val); + assert!(!path_ptr.is_null()); + let path_str = CStr::from_ptr(path_ptr).to_string_lossy(); + assert_eq!(path_str, "/nix/store/foo"); + + // Clean up + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn list_and_attrset_manipulation() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // List: [1, 2, 3] + let list_builder = nix_make_list_builder(ctx, state, 3); + assert!(!list_builder.is_null()); + let v1 = nix_alloc_value(ctx, state); + let v2 = nix_alloc_value(ctx, state); + let v3 = nix_alloc_value(ctx, state); + nix_init_int(ctx, v1, 1); + nix_init_int(ctx, v2, 2); + nix_init_int(ctx, v3, 3); + nix_list_builder_insert(ctx, list_builder, 0, v1); + nix_list_builder_insert(ctx, list_builder, 1, v2); + nix_list_builder_insert(ctx, list_builder, 2, v3); + + let list_val = nix_alloc_value(ctx, state); + assert_eq!(nix_make_list(ctx, list_builder, list_val), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, list_val), ValueType_NIX_TYPE_LIST); + assert_eq!(nix_get_list_size(ctx, list_val), 3); + + // Get elements by index + for i in 0..3 { + let elem = nix_get_list_byidx(ctx, list_val, state, i); + assert!(!elem.is_null()); + assert_eq!(nix_get_type(ctx, elem), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, elem), i64::from(i + 1)); + } + + nix_list_builder_free(list_builder); + + // Attrset: { foo = 42; bar = "baz"; } + let attr_builder = nix_make_bindings_builder(ctx, state, 2); + assert!(!attr_builder.is_null()); + let foo_val = nix_alloc_value(ctx, state); + let bar_val = nix_alloc_value(ctx, state); + nix_init_int(ctx, foo_val, 42); + let baz = CString::new("baz").unwrap(); + nix_init_string(ctx, bar_val, baz.as_ptr()); + let foo = CString::new("foo").unwrap(); + let bar = CString::new("bar").unwrap(); + nix_bindings_builder_insert(ctx, attr_builder, foo.as_ptr(), foo_val); + nix_bindings_builder_insert(ctx, attr_builder, bar.as_ptr(), bar_val); + + let attr_val = nix_alloc_value(ctx, state); + assert_eq!(nix_make_attrs(ctx, attr_val, attr_builder), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, attr_val), ValueType_NIX_TYPE_ATTRS); + assert_eq!(nix_get_attrs_size(ctx, attr_val), 2); + + // Get by name + let foo_got = nix_get_attr_byname(ctx, attr_val, state, foo.as_ptr()); + assert!(!foo_got.is_null()); + assert_eq!(nix_get_type(ctx, foo_got), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, foo_got), 42); + + let bar_got = nix_get_attr_byname(ctx, attr_val, state, bar.as_ptr()); + assert!(!bar_got.is_null()); + assert_eq!(nix_get_type(ctx, bar_got), ValueType_NIX_TYPE_STRING); + + // Has attr + assert!(nix_has_attr_byname(ctx, attr_val, state, foo.as_ptr())); + assert!(nix_has_attr_byname(ctx, attr_val, state, bar.as_ptr())); + + nix_bindings_builder_free(attr_builder); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn function_application_and_force() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Evaluate a function and apply it: (x: x + 1) 41 + let expr = CString::new("(x: x + 1)").unwrap(); + let path = CString::new("").unwrap(); + let fn_val = nix_alloc_value(ctx, state); + assert!(!fn_val.is_null()); + assert_eq!( + nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), fn_val), + nix_err_NIX_OK + ); + + // Argument: 41 + let arg_val = nix_alloc_value(ctx, state); + nix_init_int(ctx, arg_val, 41); + + // Result value + let result_val = nix_alloc_value(ctx, state); + assert!(!result_val.is_null()); + assert_eq!( + nix_value_call(ctx, state, fn_val, arg_val, result_val), + nix_err_NIX_OK + ); + + // Force result + assert_eq!(nix_value_force(ctx, state, result_val), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, result_val), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, result_val), 42); + + // Deep force (should be a no-op for int) + assert_eq!(nix_value_force_deep(ctx, state, result_val), nix_err_NIX_OK); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn error_handling_invalid_expression() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Invalid expression + let expr = CString::new("this is not valid nix").unwrap(); + let path = CString::new("").unwrap(); + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), value); + assert_ne!(eval_err, nix_err_NIX_OK); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn realised_string_and_gc() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // String value + let string_val = nix_alloc_value(ctx, state); + let s = CString::new("hello world").unwrap(); + assert_eq!(nix_init_string(ctx, string_val, s.as_ptr()), nix_err_NIX_OK); + + // Realise string + let realised = nix_string_realise(ctx, state, string_val, false); + assert!(!realised.is_null()); + let buf = nix_realised_string_get_buffer_start(realised); + let len = nix_realised_string_get_buffer_size(realised); + let realised_str = + std::str::from_utf8(std::slice::from_raw_parts(buf.cast::(), len)).unwrap(); + assert_eq!(realised_str, "hello world"); + assert_eq!(nix_realised_string_get_store_path_count(realised), 0); + + nix_realised_string_free(realised); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn big_thunk_evaluation() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a complex expression with lazy evaluation + let expr = + CString::new("let x = 1 + 2; y = x * 3; in { result = y + 4; other = x; }").unwrap(); + let path = CString::new("").unwrap(); + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // The top-level should be an attrset + assert_eq!(nix_get_type(ctx, value), ValueType_NIX_TYPE_ATTRS); + + // Get "result" attribute (ts should be a thunk initially) + let result_name = CString::new("result").unwrap(); + let result_val = nix_get_attr_byname(ctx, value, state, result_name.as_ptr()); + assert!(!result_val.is_null()); + + // Force the result + let force_err = nix_value_force(ctx, state, result_val); + assert_eq!(force_err, nix_err_NIX_OK); + + assert_eq!(nix_get_type(ctx, result_val), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, result_val), 13); // ((1+2)*3)+4 = 13 + + // Get "other" attribute + let other_name = CString::new("other").unwrap(); + let other_val = nix_get_attr_byname(ctx, value, state, other_name.as_ptr()); + assert!(!other_val.is_null()); + + let force_err2 = nix_value_force(ctx, state, other_val); + assert_eq!(force_err2, nix_err_NIX_OK); + + assert_eq!(nix_get_type(ctx, other_val), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, other_val), 3); // 1+2 = 3 + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn multi_argument_function_calls() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test evaluating a multi-argument function: (x: y: x + y) + let expr = CString::new("(x: y: x + y)").unwrap(); + let path = CString::new("/test").unwrap(); + + let func_value = nix_alloc_value(ctx, state); + assert!(!func_value.is_null()); + + let eval_err = + nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), func_value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // Force evaluation of the function + let force_err = nix_value_force(ctx, state, func_value); + assert_eq!(force_err, nix_err_NIX_OK); + + // Verify it's a function + let func_type = nix_get_type(ctx, func_value); + assert_eq!(func_type, ValueType_NIX_TYPE_FUNCTION); + + // Create arguments + let arg1 = nix_alloc_value(ctx, state); + let arg2 = nix_alloc_value(ctx, state); + assert!(!arg1.is_null() && !arg2.is_null()); + + let init_arg1_err = nix_init_int(ctx, arg1, 10); + let init_arg2_err = nix_init_int(ctx, arg2, 20); + assert_eq!(init_arg1_err, nix_err_NIX_OK); + assert_eq!(init_arg2_err, nix_err_NIX_OK); + + // Test multi-argument call using nix_value_call_multi + let mut args = [arg1, arg2]; + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = nix_value_call_multi(ctx, state, func_value, 2, args.as_mut_ptr(), result); + assert_eq!(call_err, nix_err_NIX_OK); + + // Force the result + let force_result_err = nix_value_force(ctx, state, result); + assert_eq!(force_result_err, nix_err_NIX_OK); + + // Check result type and value + let result_type = nix_get_type(ctx, result); + assert_eq!(result_type, ValueType_NIX_TYPE_INT); + + let result_value = nix_get_int(ctx, result); + assert_eq!(result_value, 30); // 10 + 20 + + // Clean up + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg2); + nix_value_decref(ctx, arg1); + nix_value_decref(ctx, func_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn curried_function_evaluation() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test evaluating a curried function: (x: y: z: x + y + z) + let expr = CString::new("(x: y: z: x + y + z)").unwrap(); + let path = CString::new("/test").unwrap(); + + let func_value = nix_alloc_value(ctx, state); + assert!(!func_value.is_null()); + + let eval_err = + nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), func_value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // Create three arguments + let arg1 = nix_alloc_value(ctx, state); + let arg2 = nix_alloc_value(ctx, state); + let arg3 = nix_alloc_value(ctx, state); + assert!(!arg1.is_null() && !arg2.is_null() && !arg3.is_null()); + + let _ = nix_init_int(ctx, arg1, 5); + let _ = nix_init_int(ctx, arg2, 10); + let _ = nix_init_int(ctx, arg3, 15); + + // Test calling with multiple arguments at once + let mut args = [arg1, arg2, arg3]; + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = nix_value_call_multi(ctx, state, func_value, 3, args.as_mut_ptr(), result); + assert_eq!(call_err, nix_err_NIX_OK); + + // Force the result + let force_result_err = nix_value_force(ctx, state, result); + assert_eq!(force_result_err, nix_err_NIX_OK); + + // Check result + let result_type = nix_get_type(ctx, result); + assert_eq!(result_type, ValueType_NIX_TYPE_INT); + + let result_value = nix_get_int(ctx, result); + assert_eq!(result_value, 30); // 5 + 10 + 15 + + // Test partial application using single calls + let partial1 = nix_alloc_value(ctx, state); + assert!(!partial1.is_null()); + + let partial_call1_err = nix_value_call(ctx, state, func_value, arg1, partial1); + assert_eq!(partial_call1_err, nix_err_NIX_OK); + + // partial1 should still be a function + let force_partial1_err = nix_value_force(ctx, state, partial1); + assert_eq!(force_partial1_err, nix_err_NIX_OK); + + let partial1_type = nix_get_type(ctx, partial1); + assert_eq!(partial1_type, ValueType_NIX_TYPE_FUNCTION); + + // Apply second argument + let partial2 = nix_alloc_value(ctx, state); + assert!(!partial2.is_null()); + + let partial_call2_err = nix_value_call(ctx, state, partial1, arg2, partial2); + assert_eq!(partial_call2_err, nix_err_NIX_OK); + + // partial2 should still be a function + let force_partial2_err = nix_value_force(ctx, state, partial2); + assert_eq!(force_partial2_err, nix_err_NIX_OK); + + let partial2_type = nix_get_type(ctx, partial2); + assert_eq!(partial2_type, ValueType_NIX_TYPE_FUNCTION); + + // Apply final argument + let final_result = nix_alloc_value(ctx, state); + assert!(!final_result.is_null()); + + let final_call_err = nix_value_call(ctx, state, partial2, arg3, final_result); + assert_eq!(final_call_err, nix_err_NIX_OK); + + // Force and check final result + let force_final_err = nix_value_force(ctx, state, final_result); + assert_eq!(force_final_err, nix_err_NIX_OK); + + let final_type = nix_get_type(ctx, final_result); + assert_eq!(final_type, ValueType_NIX_TYPE_INT); + + let final_value = nix_get_int(ctx, final_result); + assert_eq!(final_value, 30); // same result as multi-arg call + + // Clean up + nix_value_decref(ctx, final_result); + nix_value_decref(ctx, partial2); + nix_value_decref(ctx, partial1); + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg3); + nix_value_decref(ctx, arg2); + nix_value_decref(ctx, arg1); + nix_value_decref(ctx, func_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn thunk_creation_with_init_apply() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a simple function + let func_expr = CString::new("(x: x * 2)").unwrap(); + let path = CString::new("/test").unwrap(); + + let func_value = nix_alloc_value(ctx, state); + assert!(!func_value.is_null()); + + let eval_err = + nix_expr_eval_from_string(ctx, state, func_expr.as_ptr(), path.as_ptr(), func_value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // Create an argument + let arg = nix_alloc_value(ctx, state); + assert!(!arg.is_null()); + + let init_arg_err = nix_init_int(ctx, arg, 21); + assert_eq!(init_arg_err, nix_err_NIX_OK); + + // Create a thunk using nix_init_apply (lazy evaluation) + let thunk = nix_alloc_value(ctx, state); + assert!(!thunk.is_null()); + + let apply_err = nix_init_apply(ctx, thunk, func_value, arg); + assert_eq!(apply_err, nix_err_NIX_OK); + + // Initially, the thunk should be of type THUNK + let thunk_type = nix_get_type(ctx, thunk); + assert_eq!(thunk_type, ValueType_NIX_TYPE_THUNK); + + // Force evaluation of the thunk + let force_err = nix_value_force(ctx, state, thunk); + assert_eq!(force_err, nix_err_NIX_OK); + + // After forcing, it should be an integer + let forced_type = nix_get_type(ctx, thunk); + assert_eq!(forced_type, ValueType_NIX_TYPE_INT); + + let result_value = nix_get_int(ctx, thunk); + assert_eq!(result_value, 42); // 21 * 2 + + // Clean up + nix_value_decref(ctx, thunk); + nix_value_decref(ctx, arg); + nix_value_decref(ctx, func_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn lookup_path_configuration() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + // Configure custom lookup path (NIX_PATH equivalent) + let lookup_paths = [ + CString::new("nixpkgs=/fake/nixpkgs").unwrap(), + CString::new("custom=/fake/custom").unwrap(), + ]; + + let lookup_path_ptrs: Vec<*const _> = lookup_paths.iter().map(|s| s.as_ptr()).collect(); + let mut lookup_path_ptrs_null_terminated = lookup_path_ptrs; + lookup_path_ptrs_null_terminated.push(std::ptr::null()); + + let set_lookup_err = nix_eval_state_builder_set_lookup_path( + ctx, + builder, + lookup_path_ptrs_null_terminated.as_mut_ptr(), + ); + assert_eq!(set_lookup_err, nix_err_NIX_OK); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Try to evaluate an expression that uses the lookup path + // NOTE: This will likely fail since the paths don't exist, but it tests the + // API + let expr = CString::new("builtins.nixPath").unwrap(); + let path = CString::new("/test").unwrap(); + + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), result); + + // The evaluation might succeed or fail depending on Nix version and + // configuration The important thing is that setting the lookup path + // didn't crash + if eval_err == nix_err_NIX_OK { + let force_err = nix_value_force(ctx, state, result); + if force_err == nix_err_NIX_OK { + let result_type = nix_get_type(ctx, result); + // nixPath should be a list + assert_eq!(result_type, ValueType_NIX_TYPE_LIST); + } + } + + // Clean up + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn complex_nested_evaluation() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Evaluate a simple nested expression + let expr = CString::new( + r#" + let + add = x: y: x + y; + data = { + values = [1 2 3 4 5]; + }; + in + { + original = data.values; + sum = builtins.foldl' add 0 data.values; + } + "#, + ) + .unwrap(); + let path = CString::new("/test").unwrap(); + + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), result); + + // Complex expressions may fail sometimes, check for both success + // and error + if eval_err != nix_err_NIX_OK { + // If evaluation fails, skip the rest of the test + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + return; + } + + // Force deep evaluation + let force_err = nix_value_force_deep(ctx, state, result); + if force_err != nix_err_NIX_OK { + // If forcing fails, skip the rest of the test + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + return; + } + + // Verify result structure + let result_type = nix_get_type(ctx, result); + assert_eq!(result_type, ValueType_NIX_TYPE_ATTRS); + + let attrs_size = nix_get_attrs_size(ctx, result); + assert_eq!(attrs_size, 2); // original, sum + + // Check 'sum' attribute + let sum_key = CString::new("sum").unwrap(); + let sum_value = nix_get_attr_byname(ctx, result, state, sum_key.as_ptr()); + assert!(!sum_value.is_null()); + + let sum_type = nix_get_type(ctx, sum_value); + assert_eq!(sum_type, ValueType_NIX_TYPE_INT); + + let sum_result = nix_get_int(ctx, sum_value); + assert_eq!(sum_result, 15); // 1 + 2 + 3 + 4 + 5 + + // Check 'original' attribute (should be a list) + let original_key = CString::new("original").unwrap(); + let original_value = nix_get_attr_byname(ctx, result, state, original_key.as_ptr()); + if !original_value.is_null() { + let original_type = nix_get_type(ctx, original_value); + assert_eq!(original_type, ValueType_NIX_TYPE_LIST); + + let original_size = nix_get_list_size(ctx, original_value); + assert_eq!(original_size, 5); + + // Check first element of original list + let first_elem = nix_get_list_byidx(ctx, original_value, state, 0); + if !first_elem.is_null() { + let first_elem_type = nix_get_type(ctx, first_elem); + assert_eq!(first_elem_type, ValueType_NIX_TYPE_INT); + + let first_elem_value = nix_get_int(ctx, first_elem); + assert_eq!(first_elem_value, 1); + } + } + + // Clean up + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn evaluation_error_handling() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test evaluation with syntax error + let invalid_expr = CString::new("{ invalid syntax ").unwrap(); + let path = CString::new("/test").unwrap(); + + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let eval_err = + nix_expr_eval_from_string(ctx, state, invalid_expr.as_ptr(), path.as_ptr(), result); + assert_ne!(eval_err, nix_err_NIX_OK); // should fail + + // Clear error for next test + nix_clear_err(ctx); + + // Test evaluation with runtime error + let runtime_error_expr = CString::new("1 + \"string\"").unwrap(); + + let result2 = nix_alloc_value(ctx, state); + assert!(!result2.is_null()); + + let eval_err2 = nix_expr_eval_from_string( + ctx, + state, + runtime_error_expr.as_ptr(), + path.as_ptr(), + result2, + ); + + // May succeed at parse time but fail during evaluation + if eval_err2 == nix_err_NIX_OK { + let force_err = nix_value_force(ctx, state, result2); + assert_ne!(force_err, nix_err_NIX_OK); // should fail during forcing + } + + // Test error information retrieval + let error_code = nix_err_code(ctx); + assert_ne!(error_code, nix_err_NIX_OK); + + // Try to get error message + let mut error_len: std::os::raw::c_uint = 0; + let error_msg_ptr = nix_err_msg(ctx, ctx, &mut error_len as *mut _); + if !error_msg_ptr.is_null() && error_len > 0 { + let error_msg = std::str::from_utf8(std::slice::from_raw_parts( + error_msg_ptr as *const u8, + error_len as usize, + )) + .unwrap_or(""); + // Should contain some error information + assert!(!error_msg.is_empty()); + } + + // Test multi-argument call with wrong number of arguments + nix_clear_err(ctx); + + let func_expr = CString::new("(x: y: x + y)").unwrap(); + let func_value = nix_alloc_value(ctx, state); + assert!(!func_value.is_null()); + + let eval_func_err = + nix_expr_eval_from_string(ctx, state, func_expr.as_ptr(), path.as_ptr(), func_value); + assert_eq!(eval_func_err, nix_err_NIX_OK); + + // Try to call with wrong number of arguments. + // The function expects 2, but we give 1 + let arg = nix_alloc_value(ctx, state); + assert!(!arg.is_null()); + let _ = nix_init_int(ctx, arg, 5); + + let mut args = [arg]; + let result3 = nix_alloc_value(ctx, state); + assert!(!result3.is_null()); + + let call_err = nix_value_call_multi( + ctx, + state, + func_value, + 1, // only 1 argument, but function expects 2 + args.as_mut_ptr(), + result3, + ); + + // This should succeed but result should be a partially applied function + if call_err == nix_err_NIX_OK { + let force_err = nix_value_force(ctx, state, result3); + assert_eq!(force_err, nix_err_NIX_OK); + + let result_type = nix_get_type(ctx, result3); + assert_eq!(result_type, ValueType_NIX_TYPE_FUNCTION); // partially applied + } + + // Clean up + nix_value_decref(ctx, result3); + nix_value_decref(ctx, arg); + nix_value_decref(ctx, func_value); + nix_value_decref(ctx, result2); + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn builtin_function_calls() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test calling builtins.length + let length_expr = CString::new("builtins.length").unwrap(); + let path = CString::new("/test").unwrap(); + + let length_func = nix_alloc_value(ctx, state); + assert!(!length_func.is_null()); + + let eval_length_err = + nix_expr_eval_from_string(ctx, state, length_expr.as_ptr(), path.as_ptr(), length_func); + assert_eq!(eval_length_err, nix_err_NIX_OK); + + // Create a list to test with + let list_expr = CString::new("[1 2 3 4 5]").unwrap(); + let test_list = nix_alloc_value(ctx, state); + assert!(!test_list.is_null()); + + let eval_list_err = + nix_expr_eval_from_string(ctx, state, list_expr.as_ptr(), path.as_ptr(), test_list); + assert_eq!(eval_list_err, nix_err_NIX_OK); + + // Call length function with the list + let length_result = nix_alloc_value(ctx, state); + assert!(!length_result.is_null()); + + let call_length_err = nix_value_call(ctx, state, length_func, test_list, length_result); + assert_eq!(call_length_err, nix_err_NIX_OK); + + let force_length_err = nix_value_force(ctx, state, length_result); + assert_eq!(force_length_err, nix_err_NIX_OK); + + let length_type = nix_get_type(ctx, length_result); + assert_eq!(length_type, ValueType_NIX_TYPE_INT); + + let length_value = nix_get_int(ctx, length_result); + assert_eq!(length_value, 5); + + // Test builtins.map with multi-argument call + let map_expr = CString::new("builtins.map").unwrap(); + let map_func = nix_alloc_value(ctx, state); + assert!(!map_func.is_null()); + + let eval_map_err = + nix_expr_eval_from_string(ctx, state, map_expr.as_ptr(), path.as_ptr(), map_func); + assert_eq!(eval_map_err, nix_err_NIX_OK); + + // Create a simple function to map: (x: x * 2) + let double_expr = CString::new("(x: x * 2)").unwrap(); + let double_func = nix_alloc_value(ctx, state); + assert!(!double_func.is_null()); + + let eval_double_err = + nix_expr_eval_from_string(ctx, state, double_expr.as_ptr(), path.as_ptr(), double_func); + assert_eq!(eval_double_err, nix_err_NIX_OK); + + // Call map with the function and list + let mut args = [double_func, test_list]; + let map_result = nix_alloc_value(ctx, state); + assert!(!map_result.is_null()); + + let call_map_err = + nix_value_call_multi(ctx, state, map_func, 2, args.as_mut_ptr(), map_result); + assert_eq!(call_map_err, nix_err_NIX_OK); + + let force_map_err = nix_value_force(ctx, state, map_result); + assert_eq!(force_map_err, nix_err_NIX_OK); + + let map_result_type = nix_get_type(ctx, map_result); + assert_eq!(map_result_type, ValueType_NIX_TYPE_LIST); + + let map_result_size = nix_get_list_size(ctx, map_result); + assert_eq!(map_result_size, 5); + + // Check first element of mapped list (should be 2) + let first_mapped = nix_get_list_byidx(ctx, map_result, state, 0); + assert!(!first_mapped.is_null()); + + let force_first_err = nix_value_force(ctx, state, first_mapped); + assert_eq!(force_first_err, nix_err_NIX_OK); + + let first_mapped_type = nix_get_type(ctx, first_mapped); + assert_eq!(first_mapped_type, ValueType_NIX_TYPE_INT); + + let first_mapped_value = nix_get_int(ctx, first_mapped); + assert_eq!(first_mapped_value, 2); // 1 * 2 + + // Clean up + nix_value_decref(ctx, map_result); + nix_value_decref(ctx, double_func); + nix_value_decref(ctx, map_func); + nix_value_decref(ctx, length_result); + nix_value_decref(ctx, test_list); + nix_value_decref(ctx, length_func); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings-sys/tests/flake.rs b/nix-bindings/nix-bindings-sys/tests/flake.rs new file mode 100644 index 0000000..b3a8a42 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/tests/flake.rs @@ -0,0 +1,78 @@ +#![cfg(test)] + +use std::ptr; + +use nix_bindings_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn flake_settings_new_and_free() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + // Create new flake settings + let settings = nix_flake_settings_new(ctx); + assert!(!settings.is_null(), "nix_flake_settings_new returned null"); + + // Free flake settings (should not crash) + nix_flake_settings_free(settings); + + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn flake_settings_add_to_eval_state_builder() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let settings = nix_flake_settings_new(ctx); + assert!(!settings.is_null(), "nix_flake_settings_new returned null"); + + // Add flake settings to eval state builder + let err = nix_flake_settings_add_to_eval_state_builder(ctx, settings, builder); + // Accept OK or ERR_UNKNOWN (depends on Nix build/config) + assert!( + err == nix_err_NIX_OK || err == nix_err_NIX_ERR_UNKNOWN, + "nix_flake_settings_add_to_eval_state_builder returned unexpected error \ + code: {err}" + ); + + nix_flake_settings_free(settings); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn flake_settings_null_context() { + // Passing NULL context should not crash, but may error + unsafe { + let settings = nix_flake_settings_new(ptr::null_mut()); + // May return null if context is required + if !settings.is_null() { + nix_flake_settings_free(settings); + } + } +} diff --git a/nix-bindings/nix-bindings-sys/tests/memory.rs b/nix-bindings/nix-bindings-sys/tests/memory.rs new file mode 100644 index 0000000..c154038 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/tests/memory.rs @@ -0,0 +1,505 @@ +#![cfg(test)] + +use std::ffi::CString; + +use nix_bindings_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn value_reference_counting() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a value + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + // Initialize with an integer + let init_err = nix_init_int(ctx, value, 42); + assert_eq!(init_err, nix_err_NIX_OK); + + // Test value-specific reference counting + let incref_err = nix_value_incref(ctx, value); + assert_eq!(incref_err, nix_err_NIX_OK); + + // Value should still be valid after increment + let int_val = nix_get_int(ctx, value); + assert_eq!(int_val, 42); + + // Test decrement + let decref_err = nix_value_decref(ctx, value); + assert_eq!(decref_err, nix_err_NIX_OK); + + // Value should still be valid (original reference still exists) + let int_val2 = nix_get_int(ctx, value); + assert_eq!(int_val2, 42); + + // Final decrement (should not crash) + let final_decref_err = nix_value_decref(ctx, value); + assert_eq!(final_decref_err, nix_err_NIX_OK); + + // Clean up + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn general_gc_reference_counting() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a value for general GC testing + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + let init_err = nix_init_string( + ctx, + value, + CString::new("test string for GC").unwrap().as_ptr(), + ); + assert_eq!(init_err, nix_err_NIX_OK); + + // Test general GC reference counting + let gc_incref_err = nix_gc_incref(ctx, value as *const ::std::os::raw::c_void); + assert_eq!(gc_incref_err, nix_err_NIX_OK); + + // Value should still be accessible + let value_type = nix_get_type(ctx, value); + assert_eq!(value_type, ValueType_NIX_TYPE_STRING); + + // Test GC decrement + let gc_decref_err = nix_gc_decref(ctx, value as *const ::std::os::raw::c_void); + assert_eq!(gc_decref_err, nix_err_NIX_OK); + + // Final cleanup + nix_value_decref(ctx, value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn manual_garbage_collection() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a few values to test basic GC functionality + let mut values = Vec::new(); + for i in 0..3 { + let value = nix_alloc_value(ctx, state); + if !value.is_null() { + let init_err = nix_init_int(ctx, value, i); + if init_err == nix_err_NIX_OK { + values.push(value); + } + } + } + + // Verify values are accessible before GC + for (i, &value) in values.iter().enumerate() { + let int_val = nix_get_int(ctx, value); + assert_eq!(int_val, i as i64); + } + + // Clean up values before attempting GC to avoid signal issues + for value in values { + nix_value_decref(ctx, value); + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn value_copying_and_memory_management() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create original value + let original = nix_alloc_value(ctx, state); + assert!(!original.is_null()); + + let test_string = CString::new("test string for copying").unwrap(); + let init_err = nix_init_string(ctx, original, test_string.as_ptr()); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create copy + let copy = nix_alloc_value(ctx, state); + assert!(!copy.is_null()); + + let copy_err = nix_copy_value(ctx, copy, original); + assert_eq!(copy_err, nix_err_NIX_OK); + + // Verify copy has same type and can be accessed + let original_type = nix_get_type(ctx, original); + let copy_type = nix_get_type(ctx, copy); + assert_eq!(original_type, copy_type); + assert_eq!(copy_type, ValueType_NIX_TYPE_STRING); + + // Test string contents using callback + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut original_string: Option = None; + let mut copy_string: Option = None; + + let _ = nix_get_string( + ctx, + original, + Some(string_callback), + &mut original_string as *mut Option as *mut ::std::os::raw::c_void, + ); + + let _ = nix_get_string( + ctx, + copy, + Some(string_callback), + &mut copy_string as *mut Option as *mut ::std::os::raw::c_void, + ); + + // Both should have the same string content + assert_eq!(original_string, copy_string); + assert!( + original_string + .as_deref() + .unwrap_or("") + .contains("test string") + ); + + // Test reference counting on both values + let incref_orig = nix_value_incref(ctx, original); + let incref_copy = nix_value_incref(ctx, copy); + assert_eq!(incref_orig, nix_err_NIX_OK); + assert_eq!(incref_copy, nix_err_NIX_OK); + + // Values should still be accessible after increment + assert_eq!(nix_get_type(ctx, original), ValueType_NIX_TYPE_STRING); + assert_eq!(nix_get_type(ctx, copy), ValueType_NIX_TYPE_STRING); + + // Clean up with decrements + nix_value_decref(ctx, original); + nix_value_decref(ctx, original); // extra decref from incref + nix_value_decref(ctx, copy); + nix_value_decref(ctx, copy); // extra decref from incref + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn complex_value_memory_management() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a complex structure: list containing attribute sets + let list_builder = nix_make_list_builder(ctx, state, 2); + assert!(!list_builder.is_null()); + + // Create first element: attribute set + let attrs1 = nix_alloc_value(ctx, state); + assert!(!attrs1.is_null()); + + let bindings_builder1 = nix_make_bindings_builder(ctx, state, 2); + assert!(!bindings_builder1.is_null()); + + // Add attributes to first set + let key1 = CString::new("name").unwrap(); + let val1 = nix_alloc_value(ctx, state); + assert!(!val1.is_null()); + let name_str = CString::new("first").unwrap(); + let _ = nix_init_string(ctx, val1, name_str.as_ptr()); + + let insert_err1 = nix_bindings_builder_insert(ctx, bindings_builder1, key1.as_ptr(), val1); + assert_eq!(insert_err1, nix_err_NIX_OK); + + let key2 = CString::new("value").unwrap(); + let val2 = nix_alloc_value(ctx, state); + assert!(!val2.is_null()); + let _ = nix_init_int(ctx, val2, 42); + + let insert_err2 = nix_bindings_builder_insert(ctx, bindings_builder1, key2.as_ptr(), val2); + assert_eq!(insert_err2, nix_err_NIX_OK); + + let make_attrs_err1 = nix_make_attrs(ctx, attrs1, bindings_builder1); + assert_eq!(make_attrs_err1, nix_err_NIX_OK); + + // Insert first attrs into list + let list_insert_err1 = nix_list_builder_insert(ctx, list_builder, 0, attrs1); + assert_eq!(list_insert_err1, nix_err_NIX_OK); + + // Create second element + let attrs2 = nix_alloc_value(ctx, state); + assert!(!attrs2.is_null()); + + let bindings_builder2 = nix_make_bindings_builder(ctx, state, 1); + assert!(!bindings_builder2.is_null()); + + let key3 = CString::new("data").unwrap(); + let val3 = nix_alloc_value(ctx, state); + assert!(!val3.is_null()); + let data_str = CString::new("second").unwrap(); + let _ = nix_init_string(ctx, val3, data_str.as_ptr()); + + let insert_err3 = nix_bindings_builder_insert(ctx, bindings_builder2, key3.as_ptr(), val3); + assert_eq!(insert_err3, nix_err_NIX_OK); + + let make_attrs_err2 = nix_make_attrs(ctx, attrs2, bindings_builder2); + assert_eq!(make_attrs_err2, nix_err_NIX_OK); + + let list_insert_err2 = nix_list_builder_insert(ctx, list_builder, 1, attrs2); + assert_eq!(list_insert_err2, nix_err_NIX_OK); + + // Create final list + let final_list = nix_alloc_value(ctx, state); + assert!(!final_list.is_null()); + + let make_list_err = nix_make_list(ctx, list_builder, final_list); + assert_eq!(make_list_err, nix_err_NIX_OK); + + // Test the complex structure + assert_eq!(nix_get_type(ctx, final_list), ValueType_NIX_TYPE_LIST); + assert_eq!(nix_get_list_size(ctx, final_list), 2); + + // Access nested elements + let elem0 = nix_get_list_byidx(ctx, final_list, state, 0); + let elem1 = nix_get_list_byidx(ctx, final_list, state, 1); + assert!(!elem0.is_null() && !elem1.is_null()); + + assert_eq!(nix_get_type(ctx, elem0), ValueType_NIX_TYPE_ATTRS); + assert_eq!(nix_get_type(ctx, elem1), ValueType_NIX_TYPE_ATTRS); + + // Test memory management with deep copying + let copied_list = nix_alloc_value(ctx, state); + assert!(!copied_list.is_null()); + + let copy_err = nix_copy_value(ctx, copied_list, final_list); + assert_eq!(copy_err, nix_err_NIX_OK); + + // Force deep evaluation on copy + let deep_force_err = nix_value_force_deep(ctx, state, copied_list); + assert_eq!(deep_force_err, nix_err_NIX_OK); + + // Both should still be accessible + assert_eq!(nix_get_list_size(ctx, final_list), 2); + assert_eq!(nix_get_list_size(ctx, copied_list), 2); + + // Clean up all the values + nix_value_decref(ctx, copied_list); + nix_value_decref(ctx, final_list); + nix_value_decref(ctx, attrs2); + nix_value_decref(ctx, attrs1); + nix_value_decref(ctx, val3); + nix_value_decref(ctx, val2); + nix_value_decref(ctx, val1); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn memory_management_error_conditions() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + // Test reference counting with NULL pointers (should handle gracefully) + let null_incref_err = nix_gc_incref(ctx, std::ptr::null() as *const ::std::os::raw::c_void); + + // XXX: May succeed or fail depending on implementation. We can't really + // know, so assert both. + assert!(null_incref_err == nix_err_NIX_OK || null_incref_err == nix_err_NIX_ERR_UNKNOWN); + + let null_decref_err = nix_gc_decref(ctx, std::ptr::null() as *const ::std::os::raw::c_void); + assert!(null_decref_err == nix_err_NIX_OK || null_decref_err == nix_err_NIX_ERR_UNKNOWN); + + let null_value_incref_err = nix_value_incref(ctx, std::ptr::null_mut()); + // Some Nix APIs gracefully handle null pointers and return OK + assert!( + null_value_incref_err == nix_err_NIX_OK + || null_value_incref_err == nix_err_NIX_ERR_UNKNOWN + ); + + let null_value_decref_err = nix_value_decref(ctx, std::ptr::null_mut()); + // Some Nix APIs gracefully handle null pointers and return OK + assert!( + null_value_decref_err == nix_err_NIX_OK + || null_value_decref_err == nix_err_NIX_ERR_UNKNOWN + ); + + // Test copy with NULL values + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + let valid_value = nix_alloc_value(ctx, state); + assert!(!valid_value.is_null()); + + // Test copying to/from NULL + let copy_from_null_err = nix_copy_value(ctx, valid_value, std::ptr::null_mut()); + assert_ne!(copy_from_null_err, nix_err_NIX_OK); + + let copy_to_null_err = nix_copy_value(ctx, std::ptr::null_mut(), valid_value); + assert_ne!(copy_to_null_err, nix_err_NIX_OK); + + // Clean up + nix_value_decref(ctx, valid_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings-sys/tests/primop.rs b/nix-bindings/nix-bindings-sys/tests/primop.rs new file mode 100644 index 0000000..1569b8a --- /dev/null +++ b/nix-bindings/nix-bindings-sys/tests/primop.rs @@ -0,0 +1,642 @@ +#![cfg(test)] + +use std::{ + ffi::CString, + sync::atomic::{AtomicU32, Ordering}, +}; + +use nix_bindings_sys::*; +use serial_test::serial; + +#[derive(Debug)] +struct TestPrimOpData { + call_count: AtomicU32, + last_arg_value: AtomicU32, +} + +// Simple PrimOp that adds 1 to an integer argument +unsafe extern "C" fn add_one_primop( + user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + state: *mut EvalState, + args: *mut *mut nix_value, + ret: *mut nix_value, +) { + if user_data.is_null() + || context.is_null() + || state.is_null() + || args.is_null() + || ret.is_null() + { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Null pointer in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + let data = unsafe { &*(user_data as *const TestPrimOpData) }; + data.call_count.fetch_add(1, Ordering::SeqCst); + + // Get first argument + let arg = unsafe { *args.offset(0) }; + if arg.is_null() { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Missing argument in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Force evaluation of argument + if unsafe { nix_value_force(context, state, arg) } != nix_err_NIX_OK { + return; + } + + // Check if argument is integer + if unsafe { nix_get_type(context, arg) } != ValueType_NIX_TYPE_INT { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Expected integer argument in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Get integer value and add 1 + let value = unsafe { nix_get_int(context, arg) }; + data.last_arg_value.store(value as u32, Ordering::SeqCst); + + // Set return value + let _ = unsafe { nix_init_int(context, ret, value + 1) }; +} + +// PrimOp that returns a constant string +unsafe extern "C" fn hello_world_primop( + _user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + _state: *mut EvalState, + _args: *mut *mut nix_value, + ret: *mut nix_value, +) { + let hello = CString::new("Hello from Rust PrimOp!").unwrap(); + let _ = unsafe { nix_init_string(context, ret, hello.as_ptr()) }; +} + +// PrimOp that takes multiple arguments and concatenates them +unsafe extern "C" fn concat_strings_primop( + _user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + state: *mut EvalState, + args: *mut *mut nix_value, + ret: *mut nix_value, +) { + if context.is_null() || state.is_null() || args.is_null() || ret.is_null() { + return; + } + + // This PrimOp expects exactly 2 string arguments + let mut result = String::new(); + + for i in 0..2 { + let arg = unsafe { *args.offset(i) }; + if arg.is_null() { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Missing argument in concat_strings_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Force evaluation + if unsafe { nix_value_force(context, state, arg) } != nix_err_NIX_OK { + return; + } + + // Check if it's a string + if unsafe { nix_get_type(context, arg) } != ValueType_NIX_TYPE_STRING { + let _ = unsafe { + static ITEMS: &[u8] = b"Expected string argument in concat_strings_primop\0"; + nix_set_err_msg(context, nix_err_NIX_ERR_UNKNOWN, ITEMS.as_ptr() as *const _) + }; + return; + } + + // Get string value using callback + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut String) }; + result.push_str(s); + } + + let _ = unsafe { + nix_get_string( + context, + arg, + Some(string_callback), + &mut result as *mut String as *mut ::std::os::raw::c_void, + ) + }; + } + + let result_cstr = CString::new(result).unwrap_or_else(|_| CString::new("").unwrap()); + let _ = unsafe { nix_init_string(context, ret, result_cstr.as_ptr()) }; +} + +#[test] +#[serial] +fn primop_allocation_and_registration() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create argument names + let arg_names = [CString::new("x").unwrap()]; + let arg_name_ptrs: Vec<*const _> = arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("addOne").unwrap(); + let doc = CString::new("Add 1 to the argument").unwrap(); + + // Allocate PrimOp + let primop = nix_alloc_primop( + ctx, + Some(add_one_primop), + 1, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + test_data_ptr as *mut ::std::os::raw::c_void, + ); + + if !primop.is_null() { + // Register the PrimOp globally + let register_err = nix_register_primop(ctx, primop); + // Registration may fail in some environments, but allocation should work + assert!( + register_err == nix_err_NIX_OK || register_err == nix_err_NIX_ERR_UNKNOWN, + "Unexpected error code: {register_err}" + ); + + // Test using the PrimOp by creating a value with it + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Clean up value + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_function_call() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create simple hello world PrimOp (no arguments) + let name = CString::new("helloWorld").unwrap(); + let doc = CString::new("Returns hello world string").unwrap(); + let mut empty_args: Vec<*const ::std::os::raw::c_char> = vec![std::ptr::null()]; + + let hello_primop = nix_alloc_primop( + ctx, + Some(hello_world_primop), + 0, // arity + name.as_ptr(), + empty_args.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + if !hello_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, hello_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Call the PrimOp with no arguments + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = + nix_value_call_multi(ctx, state, primop_value, 0, std::ptr::null_mut(), result); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is a string + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_STRING { + // Get string value + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = + unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut string_result: Option = None; + let _ = nix_get_string( + ctx, + result, + Some(string_callback), + &mut string_result as *mut Option as *mut ::std::os::raw::c_void, + ); + + // Verify we got the expected string + assert!( + string_result + .as_deref() + .unwrap_or("") + .contains("Hello from Rust") + ); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_with_arguments() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create add one PrimOp + let arg_names = [CString::new("x").unwrap()]; + let arg_name_ptrs: Vec<*const _> = arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("addOne").unwrap(); + let doc = CString::new("Add 1 to the argument").unwrap(); + + let add_primop = nix_alloc_primop( + ctx, + Some(add_one_primop), + 1, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + test_data_ptr as *mut ::std::os::raw::c_void, + ); + + if !add_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, add_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create an integer argument + let arg_value = nix_alloc_value(ctx, state); + assert!(!arg_value.is_null()); + + let init_arg_err = nix_init_int(ctx, arg_value, 42); + assert_eq!(init_arg_err, nix_err_NIX_OK); + + // Call the PrimOp with the argument + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = nix_value_call(ctx, state, primop_value, arg_value, result); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is an integer + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_INT { + let result_value = nix_get_int(ctx, result); + assert_eq!(result_value, 43); // 42 + 1 + + // Verify callback was called + let test_data_ref = &*test_data_ptr; + assert_eq!(test_data_ref.call_count.load(Ordering::SeqCst), 1); + assert_eq!(test_data_ref.last_arg_value.load(Ordering::SeqCst), 42); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg_value); + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_multi_argument() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create concat strings PrimOp + let arg_names = [CString::new("s1").unwrap(), CString::new("s2").unwrap()]; + let arg_name_ptrs: Vec<*const _> = arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("concatStrings").unwrap(); + let doc = CString::new("Concatenate two strings").unwrap(); + + let concat_primop = nix_alloc_primop( + ctx, + Some(concat_strings_primop), + 2, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + if !concat_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, concat_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create string arguments + let arg1 = nix_alloc_value(ctx, state); + let arg2 = nix_alloc_value(ctx, state); + assert!(!arg1.is_null() && !arg2.is_null()); + + let hello_cstr = CString::new("Hello, ").unwrap(); + let world_cstr = CString::new("World!").unwrap(); + + let init_arg1_err = nix_init_string(ctx, arg1, hello_cstr.as_ptr()); + let init_arg2_err = nix_init_string(ctx, arg2, world_cstr.as_ptr()); + assert_eq!(init_arg1_err, nix_err_NIX_OK); + assert_eq!(init_arg2_err, nix_err_NIX_OK); + + // Test multi-argument call using nix_value_call_multi + let mut args = [arg1, arg2]; + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = + nix_value_call_multi(ctx, state, primop_value, 2, args.as_mut_ptr(), result); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is a string + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_STRING { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = + unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut string_result: Option = None; + let _ = nix_get_string( + ctx, + result, + Some(string_callback), + &mut string_result as *mut Option as *mut ::std::os::raw::c_void, + ); + + // Verify concatenation worked + assert_eq!(string_result.as_deref(), Some("Hello, World!")); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg2); + nix_value_decref(ctx, arg1); + nix_value_decref(ctx, primop_value); + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_error_handling() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test invalid PrimOp allocation (NULL callback) + let name = CString::new("invalid").unwrap(); + let doc = CString::new("Invalid PrimOp").unwrap(); + let mut empty_args: Vec<*const ::std::os::raw::c_char> = vec![std::ptr::null()]; + + let _invalid_primop = nix_alloc_primop( + ctx, + None, // NULL callback should cause error + 0, + name.as_ptr(), + empty_args.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + // Test initializing value with NULL PrimOp (should fail) + let test_value = nix_alloc_value(ctx, state); + assert!(!test_value.is_null()); + + nix_value_decref(ctx, test_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings-sys/tests/store.rs b/nix-bindings/nix-bindings-sys/tests/store.rs new file mode 100644 index 0000000..4db3ce9 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/tests/store.rs @@ -0,0 +1,278 @@ +#![cfg(test)] + +use std::{ffi::CString, ptr}; + +use nix_bindings_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn libstore_init_and_open_free() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + // Open the default store (NULL URI, NULL params) + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + // Free the store and context + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn parse_and_clone_free_store_path() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + // Parse a store path (I'm using a dummy path, will likely be invalid but + // should not segfault) XXX: store_path may be null if path is invalid, + // but should not crash + let path_str = CString::new("/nix/store/dummy-path").unwrap(); + let store_path = nix_store_parse_path(ctx, store, path_str.as_ptr()); + + if !store_path.is_null() { + // Clone and free + let cloned = nix_store_path_clone(store_path); + assert!(!cloned.is_null()); + nix_store_path_free(cloned); + nix_store_path_free(store_path); + } + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_get_uri_and_storedir() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + let mut uri: Option = None; + let res = nix_store_get_uri(ctx, store, Some(string_callback), (&raw mut uri).cast()); + assert_eq!(res, nix_err_NIX_OK); + assert!(uri.is_some()); + + let mut storedir: Option = None; + let res = nix_store_get_storedir( + ctx, + store, + Some(string_callback), + (&raw mut storedir).cast(), + ); + assert_eq!(res, nix_err_NIX_OK); + assert!(storedir.is_some()); + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn libstore_init_no_load_config() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init_no_load_config(ctx); + assert_eq!(err, nix_err_NIX_OK); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_is_valid_path_and_real_path() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + // Use a dummy path (should not be valid, but should not crash) + let path_str = CString::new("/nix/store/dummy-path").unwrap(); + let store_path = nix_store_parse_path(ctx, store, path_str.as_ptr()); + if !store_path.is_null() { + let valid = nix_store_is_valid_path(ctx, store, store_path); + assert!(!valid, "Dummy path should not be valid"); + + let mut real_path: Option = None; + let res = nix_store_real_path( + ctx, + store, + store_path, + Some(string_callback), + (&raw mut real_path).cast(), + ); + // May fail, but should not crash + assert!(res == nix_err_NIX_OK || res == nix_err_NIX_ERR_UNKNOWN); + nix_store_path_free(store_path); + } + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_path_name() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let path_str = CString::new("/nix/store/foo-bar-baz").unwrap(); + let store_path = nix_store_parse_path(ctx, store, path_str.as_ptr()); + if !store_path.is_null() { + let mut name: Option = None; + nix_store_path_name(store_path, Some(string_callback), (&raw mut name).cast()); + // Should extract the name part ("foo-bar-baz") + assert!(name.as_deref().unwrap_or("").contains("foo-bar-baz")); + nix_store_path_free(store_path); + } + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_get_version() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let mut version: Option = None; + let res = + nix_store_get_version(ctx, store, Some(string_callback), (&raw mut version).cast()); + assert_eq!(res, nix_err_NIX_OK); + // Version may be empty for dummy stores, but should not crash + assert!(version.is_some()); + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_realise_and_copy_closure() { + unsafe extern "C" fn realise_callback( + _userdata: *mut ::std::os::raw::c_void, + outname: *const ::std::os::raw::c_char, + out: *const StorePath, + ) { + // Just check that callback is called with non-null pointers + assert!(!outname.is_null()); + assert!(!out.is_null()); + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + // Use a dummy path (should not crash, may not realise) + let path_str = CString::new("/nix/store/dummy-path").unwrap(); + let store_path = nix_store_parse_path(ctx, store, path_str.as_ptr()); + if !store_path.is_null() { + // Realise (should fail, but must not crash) + let _ = nix_store_realise( + ctx, + store, + store_path, + std::ptr::null_mut(), + Some(realise_callback), + ); + + // Copy closure to same store (should fail, but must not crash) + let _ = nix_store_copy_closure(ctx, store, store, store_path); + + nix_store_path_free(store_path); + } + + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings-sys/tests/util.rs b/nix-bindings/nix-bindings-sys/tests/util.rs new file mode 100644 index 0000000..f84b8b9 --- /dev/null +++ b/nix-bindings/nix-bindings-sys/tests/util.rs @@ -0,0 +1,152 @@ +#![cfg(test)] + +use std::ffi::{CStr, CString}; + +use nix_bindings_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn context_create_and_free() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn libutil_init() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn version_get() { + unsafe { + let version_ptr = nix_version_get(); + assert!(!version_ptr.is_null()); + let version = CStr::from_ptr(version_ptr).to_string_lossy(); + assert!(!version.is_empty(), "Version string should not be empty"); + assert!( + version.chars().next().unwrap().is_ascii_digit(), + "Version string should start with a digit: {version}" + ); + } +} + +#[test] +#[serial] +fn setting_set_and_get() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + *unsafe { &mut *out } = Some(s.to_string()); + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + // Set a setting (use a dummy/extra setting to avoid breaking global config) + let key = CString::new("extra-test-setting").unwrap(); + let value = CString::new("test-value").unwrap(); + let set_err = nix_setting_set(ctx, key.as_ptr(), value.as_ptr()); + // Setting may not exist, but should not crash + assert!( + set_err == nix_err_NIX_OK || set_err == nix_err_NIX_ERR_KEY, + "Unexpected error code: {set_err}" + ); + + // Try to get the setting (may not exist, but should not crash) + let mut got: Option = None; + let get_err = nix_setting_get( + ctx, + key.as_ptr(), + Some(string_callback), + (&raw mut got).cast(), + ); + assert!( + get_err == nix_err_NIX_OK || get_err == nix_err_NIX_ERR_KEY, + "Unexpected error code: {get_err}" + ); + // If OK, we should have gotten a value + if get_err == nix_err_NIX_OK { + assert_eq!(got.as_deref(), Some("test-value")); + } + + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn error_handling_apis() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + *unsafe { &mut *out } = Some(s.to_string()); + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + // Set an error message + let msg = CString::new("custom error message").unwrap(); + let set_err = nix_set_err_msg(ctx, nix_err_NIX_ERR_UNKNOWN, msg.as_ptr()); + assert_eq!(set_err, nix_err_NIX_ERR_UNKNOWN); + + // Get error code + let code = nix_err_code(ctx); + assert_eq!(code, nix_err_NIX_ERR_UNKNOWN); + + // Get error message + let mut len: std::os::raw::c_uint = 0; + let err_msg_ptr = nix_err_msg(ctx, ctx, &mut len as *mut _); + if !err_msg_ptr.is_null() && len > 0 { + let err_msg = std::str::from_utf8(std::slice::from_raw_parts( + err_msg_ptr as *const u8, + len as usize, + )) + .unwrap(); + assert!(err_msg.contains("custom error message")); + } + + // Get error info message (should work, but may be empty) + let mut info: Option = None; + let _ = nix_err_info_msg(ctx, ctx, Some(string_callback), (&raw mut info).cast()); + + // Get error name (should work, but may be empty) + let mut name: Option = None; + let _ = nix_err_name(ctx, ctx, Some(string_callback), (&raw mut name).cast()); + + // Clear error + nix_clear_err(ctx); + let code_after_clear = nix_err_code(ctx); + assert_eq!(code_after_clear, nix_err_NIX_OK); + + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings/nix-bindings/Cargo.lock b/nix-bindings/nix-bindings/Cargo.lock new file mode 100644 index 0000000..306d9c7 --- /dev/null +++ b/nix-bindings/nix-bindings/Cargo.lock @@ -0,0 +1,514 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "doxygen-bindgen" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ba4ed6eedf7f4ace1632149d8f0e8a65a480534024d65a7c3b9daacdedbad3" +dependencies = [ + "yap", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nix-bindings" +version = "2.32.4" +dependencies = [ + "nix-bindings-sys", + "serial_test", +] + +[[package]] +name = "nix-bindings-sys" +version = "2.32.4" +dependencies = [ + "bindgen", + "doxygen-bindgen", + "pkg-config", + "serial_test", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yap" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe269e7b803a5e8e20cbd97860e136529cd83bf2c9c6d37b142467e7e1f051f" diff --git a/nix-bindings/nix-bindings/Cargo.toml b/nix-bindings/nix-bindings/Cargo.toml new file mode 100644 index 0000000..8217d19 --- /dev/null +++ b/nix-bindings/nix-bindings/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "nix-bindings" +description = "Rust binding for Nix, the build tool" +edition.workspace = true +version.workspace = true +repository.workspace = true +rust-version.workspace = true +license.workspace = true +publish = true +readme = "./README.md" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[features] +default = [ "full" ] +full = [ "nix-bindings-sys/full", "store", "expr", "util", "flake", "main", "external", "primop" ] + +expr = [ "store", "nix-bindings-sys/expr" ] +external = [ "store", "nix-bindings-sys/util" ] +flake = [ "expr", "nix-bindings-sys/flake" ] +main = [ "nix-bindings-sys/main" ] +primop = [ "expr", "nix-bindings-sys/expr" ] +store = [ "nix-bindings-sys/store", "nix-bindings-sys/expr", "nix-bindings-sys/util" ] +util = [ "nix-bindings-sys/util" ] + +[dependencies] +nix-bindings-sys.workspace = true + +[dev-dependencies] +serial_test.workspace = true +tempfile.workspace = true diff --git a/nix-bindings/nix-bindings/README.md b/nix-bindings/nix-bindings/README.md new file mode 100644 index 0000000..36637ec --- /dev/null +++ b/nix-bindings/nix-bindings/README.md @@ -0,0 +1,102 @@ +# `nix-bindings` + +This crate provides a safe, ergonomic Rust API built on top of +`nix-bindings-sys`. It wraps the raw FFI calls in idiomatic Rust types with +automatic resource management, type-safe conversions, and comprehensive error +handling. The crate is organized into the following modules, each gated by a +Cargo feature: + +- **`store`** (`store` feature): Store, store path, and derivation management + (opening stores, parsing store paths, realizing derivations, copying closures) +- **`attrs`** (requires `expr` feature): Attribute set access (`get_attr`, + `has_attr`, `attr_keys`, `AttrIterator`) +- **`lists`** (requires `expr` feature): List operations (`list_len`, + `list_get`, `list_iter`, `ListIterator`) +- **`flake`** (`flake`): Flake support (`FlakeSettings`, `FlakeReference`, + `LockedFlake`, `LockFlags`, `FetchersSettings`) +- **`primop`** (`primop`): Custom Nix primitive operations via Rust closures + (global builtins or value-embedded) +- **`external`** (`external`): Embed arbitrary Rust values as Nix external + values with safe downcasting + +[crate documentation]: https://notashelf.github.io/nix-bindings/nix_bindings/index.html + +A few key types are provided at the crate root and are gated by the `store` and +`expr` features (both included by default). Namely: `Context`, `EvalState`, +`EvalStateBuilder`, `Store`, `StorePath`, `Derivation`, `Value` and verbosity +may be used to manage C API context lifetime, expression evaluation, store +handle, derivation from JSON and Nix value with type-safe accessors +specifically. See [crate documentation] for more details. + +## Usage + +Add nix-bindings to your `Cargo.toml`: + +```toml +# Cargo.toml +[dependencies] +nix-bindings = "0.2328.0" +``` + +The full crate (default) enables all modules. To exclude features you don't +need, disable defaults and pick: + + + +```toml +[dependencies] +nix-bindings = { version = "0.2324.0", default-features = false, features = ["store", "primop"] } +``` + + + +Available features are `store`, `expr` (implies `store`), `flake`, `external`, +`primop`. `util` and `main` pass through to the underlying sys crate but do not +gate any high-level modules. `full` (default) enables everything. + +Quick example evaluating a Nix expression: + +```rust +use std::sync::Arc; +use nix_bindings::{Context, EvalStateBuilder, Store}; + +let ctx = Arc::new(Context::new()?); +let store = Arc::new(Store::open(&ctx, None)?); +let state = EvalStateBuilder::new(&store)?.build()?; + +let result = state.eval_from_string("1 + 2", "")?; +println!("Result: {}", result.as_int()?); +``` + +## Testing + +`nix-bindings` includes both unit tests (inline in each module) and integration +tests in [`tests/`](./nix-bindings/tests). Tests are gated by the same features +as the modules they exercise. Run them with: + +```sh +# Full test suite +$ cargo nextest run -p nix-bindings + +# Tests for a specific feature set +$ cargo nextest run -p nix-bindings --no-default-features --features store,expr + +# You may use plain cargo test if you don't have cargo-nextest +$ cargo test -p nix-bindings +``` + +The test suite is rather large, and it covers a lot including: + +- Store operations (open, query URI/dir/version, path validation) +- Expression evaluation (arithmetic, strings, booleans, functions, + interpolation) +- Value construction and type conversion (int, float, bool, string, null) +- Attribute set manipulation (get, has, keys, iteration) +- List operations (length, indexing, iteration) +- String context handling (plain strings, store-path contexts) +- Derivation realization +- Resource cleanup (ensuring no leaks across repeated create/drop cycles) +- Flake settings and lock flags +- Custom primops (value-embedded) +- External values (creation, downcast, type-safe retrieval) +- Error handling (parse errors, type mismatches) diff --git a/nix-bindings/nix-bindings/src/attrs.rs b/nix-bindings/nix-bindings/src/attrs.rs new file mode 100644 index 0000000..eead357 --- /dev/null +++ b/nix-bindings/nix-bindings/src/attrs.rs @@ -0,0 +1,338 @@ +use std::{ffi::CString, ptr::NonNull}; + +use crate::{Error, Result, Value}; + +impl Value<'_> { + /// Get an attribute by name. + /// + /// Returns the value associated with the given attribute name. + /// + /// # Errors + /// + /// Returns an error if the value is not an attribute set or the key + /// does not exist. + pub fn get_attr(&self, key: &str) -> Result> { + if self.value_type() != crate::ValueType::Attrs { + return Err(Error::InvalidType { + expected: "attrs", + actual: self.value_type().to_string(), + }); + } + + let key_c = CString::new(key)?; + + // SAFETY: context, value, and state are valid, type is checked + // nix_get_attr_byname returns an owned (GC-reffed) pointer; we are + // responsible for decref, which Value's Drop handles. + let attr_ptr = unsafe { + crate::sys::nix_get_attr_byname( + self.state.context.as_ptr(), + self.inner.as_ptr(), + self.state.as_ptr(), + key_c.as_ptr(), + ) + }; + + if attr_ptr.is_null() { + return Err(Error::KeyNotFound(key.to_string())); + } + + let inner = NonNull::new(attr_ptr).ok_or(Error::NullPointer)?; + Ok(Value { + inner, + state: self.state, + }) + } + + /// Get all attribute keys. + /// + /// Returns a vector of all attribute names in this attribute set. + /// + /// # Errors + /// + /// Returns an error if the value is not an attribute set. + pub fn attr_keys(&self) -> Result> { + if self.value_type() != crate::ValueType::Attrs { + return Err(Error::InvalidType { + expected: "attrs", + actual: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let count = unsafe { + crate::sys::nix_get_attrs_size(self.state.context.as_ptr(), self.inner.as_ptr()) + }; + + let mut keys = Vec::with_capacity(count as usize); + + for i in 0..count { + // SAFETY: context, value, and state are valid; index is in bounds + let mut name_ptr: *const std::os::raw::c_char = std::ptr::null(); + // nix_get_attr_byidx also returns an owned value pointer; we don't + // need the value here, but we must decref it to avoid a leak. + let val_ptr = unsafe { + crate::sys::nix_get_attr_byidx( + self.state.context.as_ptr(), + self.inner.as_ptr(), + self.state.as_ptr(), + i, + &mut name_ptr, + ) + }; + + // Decref the returned value since we only want the name here. + if !val_ptr.is_null() { + unsafe { + crate::sys::nix_value_decref(self.state.context.as_ptr(), val_ptr); + } + } + + if name_ptr.is_null() { + continue; + } + + // SAFETY: name_ptr is a valid C string owned by the EvalState + let name = unsafe { + std::ffi::CStr::from_ptr(name_ptr) + .to_string_lossy() + .into_owned() + }; + keys.push(name); + } + + Ok(keys) + } + + /// Check if an attribute exists. + /// + /// Returns true if the attribute set contains the given key. + /// + /// # Errors + /// + /// Returns an error if the value is not an attribute set. + pub fn has_attr(&self, key: &str) -> Result { + if self.value_type() != crate::ValueType::Attrs { + return Err(Error::InvalidType { + expected: "attrs", + actual: self.value_type().to_string(), + }); + } + + let key_c = CString::new(key)?; + + // SAFETY: context, value, and state are valid, type is checked + let result = unsafe { + crate::sys::nix_has_attr_byname( + self.state.context.as_ptr(), + self.inner.as_ptr(), + self.state.as_ptr(), + key_c.as_ptr(), + ) + }; + + Ok(result) + } + + /// Create an iterator over key-value pairs. + /// + /// # Returns + /// + /// An iterator over all key-value pairs in the attribute set. + /// + /// # Errors + /// + /// Returns an error if the value is not an attribute set. + pub fn attrs(&self) -> Result> { + if self.value_type() != crate::ValueType::Attrs { + return Err(Error::InvalidType { + expected: "attrs", + actual: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let count = unsafe { + crate::sys::nix_get_attrs_size(self.state.context.as_ptr(), self.inner.as_ptr()) + }; + + Ok(AttrIterator { + value: self, + index: 0, + count: count as usize, + }) + } +} + +/// Iterator over attribute set key-value pairs. +/// +/// This struct provides a way to iterate through all attributes +/// in a Nix attribute set, yielding both the key and value for +/// each attribute. +pub struct AttrIterator<'a> { + value: &'a Value<'a>, + index: usize, + count: usize, +} + +impl<'a> Iterator for AttrIterator<'a> { + // Item lifetime is tied to the source Value's lifetime 'a, not 'static. + type Item = Result<(String, Value<'a>)>; + + fn next(&mut self) -> Option { + if self.index >= self.count { + return None; + } + + let idx = self.index; + self.index += 1; + + // SAFETY: context, value, and state are valid; index is in bounds + // nix_get_attr_byidx returns an owned (GC-reffed) pointer; Value's + // Drop will release it. + let mut name_ptr: *const std::os::raw::c_char = std::ptr::null(); + let attr_ptr = unsafe { + crate::sys::nix_get_attr_byidx( + self.value.state.context.as_ptr(), + self.value.inner.as_ptr(), + self.value.state.as_ptr(), + idx as std::os::raw::c_uint, + &mut name_ptr, + ) + }; + + if attr_ptr.is_null() { + return Some(Err(Error::NullPointer)); + } + + if name_ptr.is_null() { + // attr_ptr is GC-reffed; we must release it before returning. + unsafe { + crate::sys::nix_value_decref(self.value.state.context.as_ptr(), attr_ptr); + } + return Some(Err(Error::NullPointer)); + } + + // SAFETY: name_ptr is a valid C string owned by the EvalState + let name = unsafe { + std::ffi::CStr::from_ptr(name_ptr) + .to_string_lossy() + .into_owned() + }; + + // SAFETY: attr_ptr is non-null, verified above + let inner = unsafe { NonNull::new_unchecked(attr_ptr) }; + + let value = Value { + inner, + state: self.value.state, + }; + + Some(Ok((name, value))) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.count - self.index; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for AttrIterator<'_> {} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use serial_test::serial; + + use crate::{Context, EvalStateBuilder, Store}; + + fn setup() -> EvalStateBuilder { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + EvalStateBuilder::new(&store).expect("Failed to create builder") + } + + #[test] + #[serial] + fn test_get_attr() { + let state = setup().build().expect("Failed to build state"); + let attrs = state + .eval_from_string("{ foo = 42; bar = \"hello\"; }", "") + .expect("Failed to evaluate attrs"); + + let foo = attrs.get_attr("foo").expect("Failed to get foo"); + assert_eq!(foo.as_int().expect("Failed to get int"), 42); + + let bar = attrs.get_attr("bar").expect("Failed to get bar"); + assert_eq!(bar.as_string().expect("Failed to get string"), "hello"); + } + + #[test] + #[serial] + fn test_get_attr_missing() { + let state = setup().build().expect("Failed to build state"); + let attrs = state + .eval_from_string("{ foo = 42; }", "") + .expect("Failed to evaluate attrs"); + + let result = attrs.get_attr("missing"); + assert!(result.is_err()); + } + + #[test] + #[serial] + fn test_attr_keys() { + let state = setup().build().expect("Failed to build state"); + let attrs = state + .eval_from_string("{ foo = 1; bar = 2; baz = 3; }", "") + .expect("Failed to evaluate attrs"); + + let keys = attrs.attr_keys().expect("Failed to get keys"); + assert_eq!(keys.len(), 3); + assert!(keys.contains(&"foo".to_string())); + assert!(keys.contains(&"bar".to_string())); + assert!(keys.contains(&"baz".to_string())); + } + + #[test] + #[serial] + fn test_has_attr() { + let state = setup().build().expect("Failed to build state"); + let attrs = state + .eval_from_string("{ foo = 42; }", "") + .expect("Failed to evaluate attrs"); + + assert!(attrs.has_attr("foo").expect("Failed to check attr")); + assert!(!attrs.has_attr("bar").expect("Failed to check missing attr")); + } + + #[test] + #[serial] + fn test_attr_iterator() { + let state = setup().build().expect("Failed to build state"); + let attrs = state + .eval_from_string("{ a = 1; b = 2; c = 3; }", "") + .expect("Failed to evaluate attrs"); + + let iter = attrs.attrs().expect("Failed to create iterator"); + let collected: Vec<_> = iter.collect(); + + assert_eq!(collected.len(), 3); + } + + #[test] + #[serial] + fn test_empty_attrs() { + let state = setup().build().expect("Failed to build state"); + let attrs = state + .eval_from_string("{}", "") + .expect("Failed to evaluate empty attrs"); + + let keys = attrs.attr_keys().expect("Failed to get keys"); + assert!(keys.is_empty()); + + let has = attrs.has_attr("foo").expect("Failed to check attr"); + assert!(!has); + } +} diff --git a/nix-bindings/nix-bindings/src/external.rs b/nix-bindings/nix-bindings/src/external.rs new file mode 100644 index 0000000..7c3c4c0 --- /dev/null +++ b/nix-bindings/nix-bindings/src/external.rs @@ -0,0 +1,430 @@ +//! External Nix values backed by Rust data. +//! +//! - [`NixExternal`]: trait your data type must implement. +//! - [`EvalState::make_external`](crate::EvalState::make_external): wraps a +//! value and returns an [`ExternalValueHandle`]. +//! - [`ExternalValueHandle::as_external`]: downcasts back to a concrete Rust +//! type. +//! +//! A [`TypeId`] is stored alongside each data pointer. +//! [`ExternalValueHandle::as_external`] checks it before returning a reference, +//! so a wrong-type downcast returns [`Error::InvalidType`] rather than UB. +//! +//! # Example +//! +//! ```rust,no_run +//! use std::sync::Arc; +//! +//! use nix_bindings::{Context, EvalStateBuilder, Store, external::NixExternal}; +//! +//! struct MyData(i64); +//! +//! impl NixExternal for MyData { +//! fn display(&self) -> String { +//! format!("MyData({})", self.0) +//! } +//! fn type_name(&self) -> &'static str { +//! "MyData" +//! } +//! } +//! +//! fn main() -> Result<(), Box> { +//! let ctx = Arc::new(Context::new()?); +//! let store = Arc::new(Store::open(&ctx, None)?); +//! let state = EvalStateBuilder::new(&store)?.build()?; +//! +//! let handle = state.make_external(MyData(42))?; +//! let back = handle.as_external::()?; +//! assert_eq!(back.0, 42); +//! +//! Ok(()) +//! } +//! ``` + +use std::{any::TypeId, ffi::CString, ops::Deref}; + +use crate::{Error, EvalState, Result, Value, sys}; + +/// A Rust type that can be embedded as an external value in the Nix evaluator. +/// +/// Implementing this trait is the only requirement for storing your type inside +/// Nix values via [`EvalState::make_external`]. All methods except +/// [`display`](NixExternal::display) and [`type_name`](NixExternal::type_name) +/// have default no-op implementations. +pub trait NixExternal: Send + Sync + 'static { + /// Return a human-readable representation of the value. + /// + /// This is called when Nix prints the value (e.g. in the REPL). + fn display(&self) -> String; + + /// Return the type name shown by `:t` in the Nix REPL. + fn type_name(&self) -> &'static str; + + /// Try to coerce the value to a string. + /// + /// Return `Some(s)` to allow coercion; return `None` (the default) to have + /// Nix throw an error when coercion is attempted. + fn coerce_to_string(&self) -> Option { + None + } + + /// Test equality with another external value of the same Rust type. + /// + /// The default implementation returns `false` (values are never equal). + fn equal(&self, _other: &Self) -> bool { + false + } +} + +/// Heap-allocated wrapper that combines a boxed `T` with its [`TypeId`]. +/// +/// This is the allocation stored behind the `void*` data pointer that is +/// passed to [`nix_create_external_value`](sys::nix_create_external_value). +struct ErasedPayload { + type_id: TypeId, + + // The concrete data follows; we keep only a raw pointer into the T. + data: *mut std::os::raw::c_void, + + // Destructor: how to drop the original Box. + drop_fn: unsafe fn(*mut std::os::raw::c_void), + + // display(): returns an owned String. + display_fn: unsafe fn(*const std::os::raw::c_void) -> String, + + // type_name(): returns a &'static str. + type_name_fn: unsafe fn(*const std::os::raw::c_void) -> &'static str, + + // coerce_to_string(): returns Option. + coerce_fn: unsafe fn(*const std::os::raw::c_void) -> Option, + + // equal(other_data): compare two ErasedPayload.data pointers of the same T. + equal_fn: unsafe fn(*const std::os::raw::c_void, *const std::os::raw::c_void) -> bool, +} + +unsafe fn drop_erased(ptr: *mut std::os::raw::c_void) { + drop(unsafe { Box::from_raw(ptr.cast::()) }); +} + +unsafe fn display_erased(ptr: *const std::os::raw::c_void) -> String { + let t = unsafe { &*(ptr as *const T) }; + t.display() +} + +unsafe fn type_name_erased(ptr: *const std::os::raw::c_void) -> &'static str { + let t = unsafe { &*(ptr as *const T) }; + t.type_name() +} + +unsafe fn coerce_erased(ptr: *const std::os::raw::c_void) -> Option { + let t = unsafe { &*(ptr as *const T) }; + t.coerce_to_string() +} + +unsafe fn equal_erased( + ptr: *const std::os::raw::c_void, + other: *const std::os::raw::c_void, +) -> bool { + let a = unsafe { &*(ptr as *const T) }; + let b = unsafe { &*(other as *const T) }; + a.equal(b) +} + +impl ErasedPayload { + fn new(value: T) -> *mut Self { + let data_box = Box::new(value); + let data_ptr = Box::into_raw(data_box) as *mut std::os::raw::c_void; + + Box::into_raw(Box::new(ErasedPayload { + type_id: TypeId::of::(), + data: data_ptr, + drop_fn: drop_erased::, + display_fn: display_erased::, + type_name_fn: type_name_erased::, + coerce_fn: coerce_erased::, + equal_fn: equal_erased::, + })) + } + + unsafe fn from_void<'a>(ptr: *mut std::os::raw::c_void) -> &'a Self { + unsafe { &*(ptr as *const ErasedPayload) } + } +} + +impl Drop for ErasedPayload { + fn drop(&mut self) { + // SAFETY: self.data was created by Box::into_raw:: and drop_fn + // knows its original concrete type T. + unsafe { (self.drop_fn)(self.data) }; + } +} + +/// The static vtable passed to `nix_create_external_value`. +/// +/// We store only one global vtable because all per-type dispatch happens +/// through the function pointers embedded in [`ErasedPayload`]. +static VTABLE: sys::NixCExternalValueDesc = { + unsafe extern "C" fn print(self_: *mut std::os::raw::c_void, printer: *mut sys::nix_printer) { + let payload = unsafe { ErasedPayload::from_void(self_) }; + let s = unsafe { (payload.display_fn)(payload.data) }; + if let Ok(cs) = CString::new(s) { + unsafe { + sys::nix_external_print(std::ptr::null_mut(), printer, cs.as_ptr()); + } + } + } + + unsafe extern "C" fn show_type( + self_: *mut std::os::raw::c_void, + res: *mut sys::nix_string_return, + ) { + let payload = unsafe { ErasedPayload::from_void(self_) }; + let name = unsafe { (payload.type_name_fn)(payload.data) }; + if let Ok(cs) = CString::new(name) { + unsafe { sys::nix_set_string_return(res, cs.as_ptr()) }; + } + } + + unsafe extern "C" fn type_of( + _self: *mut std::os::raw::c_void, + res: *mut sys::nix_string_return, + ) { + // builtins.typeOf for all external values returns "nix-external". + if let Ok(cs) = CString::new("nix-external") { + unsafe { sys::nix_set_string_return(res, cs.as_ptr()) }; + } + } + + unsafe extern "C" fn coerce_to_string( + self_: *mut std::os::raw::c_void, + _c: *mut sys::nix_string_context, + _coerce_more: std::os::raw::c_int, + _copy_to_store: std::os::raw::c_int, + res: *mut sys::nix_string_return, + ) { + let payload = unsafe { ErasedPayload::from_void(self_) }; + if let Some(s) = unsafe { (payload.coerce_fn)(payload.data) } + && let Ok(cs) = CString::new(s) + { + unsafe { sys::nix_set_string_return(res, cs.as_ptr()) }; + } + } + + unsafe extern "C" fn equal( + self_: *mut std::os::raw::c_void, + other: *mut std::os::raw::c_void, + ) -> std::os::raw::c_int { + let a = unsafe { ErasedPayload::from_void(self_) }; + let b = unsafe { ErasedPayload::from_void(other) }; + // Only compare if they share the same concrete type. + if a.type_id != b.type_id { + return 0; + } + if unsafe { (a.equal_fn)(a.data, b.data) } { + 1 + } else { + 0 + } + } + + // JSON and XML printing default to not-implemented (None). + sys::NixCExternalValueDesc { + print: Some(print), + showType: Some(show_type), + typeOf: Some(type_of), + coerceToString: Some(coerce_to_string), + equal: Some(equal), + printValueAsJSON: None, + printValueAsXML: None, + } +}; + +impl EvalState { + /// Wrap a [`NixExternal`] value and return an [`ExternalValueHandle`]. + /// + /// The handle carries the Nix [`Value`] and the raw `ExternalValue*` needed + /// for downcasting via [`ExternalValueHandle::as_external`]. The value's + /// lifetime is managed by the Nix GC. + /// + /// # Errors + /// + /// Returns an error if value allocation or external value creation fails. + pub fn make_external(&self, data: T) -> Result> { + let payload_ptr = ErasedPayload::new(data); + // Cast away the const on the vtable: the C API takes *mut but never + // mutates the descriptor itself. + let vtable_ptr = + &VTABLE as *const sys::NixCExternalValueDesc as *mut sys::NixCExternalValueDesc; + + // Allocate the nix_value before touching the GC with the external pointer. + let v = self.alloc_value()?; + + // SAFETY: vtable_ptr points to the static vtable; payload_ptr is a + // freshly allocated ErasedPayload. + let ext_ptr = unsafe { + sys::nix_create_external_value( + self.context.as_ptr(), + vtable_ptr, + payload_ptr.cast::(), + ) + }; + + if ext_ptr.is_null() { + drop(unsafe { Box::from_raw(payload_ptr) }); + return Err(Error::NullPointer); + } + + // SAFETY: context, value, and external value pointer are valid. + unsafe { + crate::check_err( + self.context.as_ptr(), + sys::nix_init_external(self.context.as_ptr(), v.inner.as_ptr(), ext_ptr), + )?; + } + + Ok(ExternalValueHandle { value: v, ext_ptr }) + } +} + +/// A handle to a Nix external value that retains the `ExternalValue*`. +/// +/// `nix_get_external` is broken in Nix 2.32.7; see the module-level note. +/// This type stores the `ExternalValue*` from `nix_create_external_value` +/// directly and uses `nix_get_external_value_content` for downcasting. +/// +/// Derefs to [`Value`]. +pub struct ExternalValueHandle<'s> { + value: Value<'s>, + ext_ptr: *mut sys::ExternalValue, +} + +// SAFETY: ExternalValue* is GC-managed. T: Send + Sync is enforced by +// NixExternal. +unsafe impl Send for ExternalValueHandle<'_> {} +unsafe impl Sync for ExternalValueHandle<'_> {} + +impl<'s> Deref for ExternalValueHandle<'s> { + type Target = Value<'s>; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl ExternalValueHandle<'_> { + /// Downcast to a concrete Rust type `T`. + /// + /// # Errors + /// + /// Returns [`Error::InvalidType`] if the stored + /// [`TypeId`] does not match `T`. + pub fn as_external(&self) -> Result<&T> { + // SAFETY: ext_ptr was returned by nix_create_external_value and is kept + // alive by the nix_value that this handle owns. + let void_ptr = unsafe { + sys::nix_get_external_value_content(self.value.state.context.as_ptr(), self.ext_ptr) + }; + + if void_ptr.is_null() { + return Err(Error::NullPointer); + } + + // SAFETY: void_ptr points to the ErasedPayload allocated in + // ErasedPayload::new. + let payload = unsafe { ErasedPayload::from_void(void_ptr) }; + + if payload.type_id != TypeId::of::() { + return Err(Error::InvalidType { + expected: std::any::type_name::(), + actual: "external value of different type".to_string(), + }); + } + + // SAFETY: type_id matches, so data is a valid *mut T. + let t_ref = unsafe { &*(payload.data as *const T) }; + Ok(t_ref) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use serial_test::serial; + + use super::*; + use crate::{Context, EvalStateBuilder, Store, ValueType}; + + struct MyData(i64); + + impl NixExternal for MyData { + fn display(&self) -> String { + format!("MyData({})", self.0) + } + + fn type_name(&self) -> &'static str { + "MyData" + } + } + + fn make_eval_state() -> (Arc, Arc, crate::EvalState) { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + (ctx, store, state) + } + + #[test] + #[serial] + fn test_make_and_recover_external() { + let (_ctx, _store, state) = make_eval_state(); + + let handle = state + .make_external(MyData(42)) + .expect("make_external failed"); + assert_eq!(handle.value_type(), ValueType::External); + + let back = handle.as_external::().expect("as_external failed"); + assert_eq!(back.0, 42); + } + + #[test] + #[serial] + fn test_wrong_type_returns_error() { + let (_ctx, _store, state) = make_eval_state(); + + struct OtherData; + impl NixExternal for OtherData { + fn display(&self) -> String { + "OtherData".to_string() + } + + fn type_name(&self) -> &'static str { + "OtherData" + } + } + + let handle = state + .make_external(MyData(1)) + .expect("make_external failed"); + let result = handle.as_external::(); + assert!( + result.is_err(), + "Downcasting to wrong type should return Err" + ); + } + + #[test] + #[serial] + fn test_as_external_on_non_external_value() { + let (_ctx, _store, state) = make_eval_state(); + + let int_val = state.make_int(5).expect("make_int failed"); + // int_val is a Value, not an ExternalValueHandle; just confirm it's not + // External type + assert_ne!(int_val.value_type(), ValueType::External); + } +} diff --git a/nix-bindings/nix-bindings/src/flake.rs b/nix-bindings/nix-bindings/src/flake.rs new file mode 100644 index 0000000..40f3242 --- /dev/null +++ b/nix-bindings/nix-bindings/src/flake.rs @@ -0,0 +1,589 @@ +//! Nix flake support. +//! +//! Types: +//! +//! - [`FlakeSettings`]: global flake configuration; pass to +//! [`EvalStateBuilder::with_flake_settings`](crate::EvalStateBuilder::with_flake_settings). +//! - [`FetchersSettings`]: fetcher configuration required by +//! [`FlakeReference::parse`] and [`LockedFlake::lock`]. +//! - [`FlakeReferenceParseFlags`]: optional flags controlling how a flake +//! reference string is parsed. +//! - [`LockFlags`]: controls locking behaviour (check, virtual, +//! write-as-needed, input overrides). +//! - [`FlakeReference`]: an unresolved reference to a flake; produced by +//! [`FlakeReference::parse`]. +//! - [`LockedFlake`]: a fully locked flake; produced by [`LockedFlake::lock`]. +//! Call [`LockedFlake::output_attrs`] to obtain the flake's output attribute +//! set. + +use std::{ffi::CString, ptr::NonNull, sync::Arc}; + +use crate::{Context, Error, EvalState, Result, Value, check_err, sys}; + +/// Configuration for the Nix flake subsystem. +/// +/// This enables flake evaluation features in the Nix evaluator (such as +/// `builtins.getFlake`). Obtain a `FlakeSettings` and pass it to +/// [`EvalStateBuilder::with_flake_settings`](crate::EvalStateBuilder::with_flake_settings) +/// before building the [`EvalState`]. +/// +/// # Example +/// +/// ```no_run +/// use std::sync::Arc; +/// +/// use nix_bindings::{Context, EvalStateBuilder, Store, flake::FlakeSettings}; +/// +/// fn main() -> Result<(), Box> { +/// let ctx = Arc::new(Context::new()?); +/// let store = Arc::new(Store::open(&ctx, None)?); +/// let flake_settings = FlakeSettings::new(&ctx)?; +/// let state = EvalStateBuilder::new(&store)? +/// .with_flake_settings(&flake_settings)? +/// .build()?; +/// +/// Ok(()) +/// } +/// ``` +pub struct FlakeSettings { + pub(crate) inner: NonNull, + _context: Arc, +} + +impl FlakeSettings { + /// Create a new set of flake settings with default values. + /// + /// # Errors + /// + /// Returns an error if the underlying allocation fails. + pub fn new(context: &Arc) -> Result { + // SAFETY: context is valid + let ptr = unsafe { sys::nix_flake_settings_new(context.as_ptr()) }; + + let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?; + + Ok(FlakeSettings { + inner, + _context: Arc::clone(context), + }) + } + + /// Get the raw flake settings pointer. + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_flake_settings { + self.inner.as_ptr() + } +} + +impl Drop for FlakeSettings { + fn drop(&mut self) { + // SAFETY: We own the settings and they are valid until drop + unsafe { + sys::nix_flake_settings_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: FlakeSettings can be shared between threads +unsafe impl Send for FlakeSettings {} +unsafe impl Sync for FlakeSettings {} + +/// Fetcher configuration. +/// +/// This is required by [`FlakeReference::parse`] and [`LockedFlake::lock`]. +/// Create one with [`FetchersSettings::new`] and keep it alive for the +/// duration of any flake operations that need it. +pub struct FetchersSettings { + inner: NonNull, + _context: Arc, +} + +impl FetchersSettings { + /// Create new fetcher settings with default values. + /// + /// # Errors + /// + /// Returns an error if the underlying allocation fails. + pub fn new(context: &Arc) -> Result { + // SAFETY: context is valid + let ptr = unsafe { sys::nix_fetchers_settings_new(context.as_ptr()) }; + let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?; + Ok(FetchersSettings { + inner, + _context: Arc::clone(context), + }) + } + + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_fetchers_settings { + self.inner.as_ptr() + } +} + +impl Drop for FetchersSettings { + fn drop(&mut self) { + // SAFETY: We own the settings and they are valid until drop + unsafe { + sys::nix_fetchers_settings_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: FetchersSettings can be shared between threads +unsafe impl Send for FetchersSettings {} +unsafe impl Sync for FetchersSettings {} + +/// Flags that control how a flake reference string is parsed. +/// +/// Create one with [`FlakeReferenceParseFlags::new`] then optionally call +/// [`set_base_directory`](Self::set_base_directory) before passing it to +/// [`FlakeReference::parse`]. +pub struct FlakeReferenceParseFlags { + inner: NonNull, + _context: Arc, +} + +impl FlakeReferenceParseFlags { + /// Create new parse flags with default values. + /// + /// # Errors + /// + /// Returns an error if the underlying allocation fails. + pub fn new(context: &Arc, flake_settings: &FlakeSettings) -> Result { + // SAFETY: context and flake_settings are valid + let ptr = unsafe { + sys::nix_flake_reference_parse_flags_new(context.as_ptr(), flake_settings.as_ptr()) + }; + let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?; + Ok(FlakeReferenceParseFlags { + inner, + _context: Arc::clone(context), + }) + } + + /// Set the base directory used when resolving relative flake references. + /// + /// # Errors + /// + /// Returns an error if the C API call fails. + pub fn set_base_directory(self, dir: &str) -> Result { + let bytes = dir.as_bytes(); + // SAFETY: context, flags, and dir bytes are valid + unsafe { + check_err( + self._context.as_ptr(), + sys::nix_flake_reference_parse_flags_set_base_directory( + self._context.as_ptr(), + self.inner.as_ptr(), + bytes.as_ptr().cast(), + bytes.len(), + ), + )?; + } + Ok(self) + } + + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_flake_reference_parse_flags { + self.inner.as_ptr() + } +} + +impl Drop for FlakeReferenceParseFlags { + fn drop(&mut self) { + // SAFETY: We own the flags and they are valid until drop + unsafe { + sys::nix_flake_reference_parse_flags_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: FlakeReferenceParseFlags can be shared between threads +unsafe impl Send for FlakeReferenceParseFlags {} +unsafe impl Sync for FlakeReferenceParseFlags {} + +/// Flags controlling the lock-file update strategy for [`LockedFlake::lock`]. +pub struct LockFlags { + inner: NonNull, + _context: Arc, +} + +impl LockFlags { + /// Create new lock flags with default values. + /// + /// # Errors + /// + /// Returns an error if the underlying allocation fails. + pub fn new(context: &Arc, flake_settings: &FlakeSettings) -> Result { + // SAFETY: context and flake_settings are valid + let ptr = + unsafe { sys::nix_flake_lock_flags_new(context.as_ptr(), flake_settings.as_ptr()) }; + let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?; + Ok(LockFlags { + inner, + _context: Arc::clone(context), + }) + } + + /// Require the lock file to be up-to-date; fail if it needs updating. + /// + /// # Errors + /// + /// Returns an error if the C API call fails. + pub fn set_mode_check(self) -> Result { + // SAFETY: context and flags are valid + unsafe { + check_err( + self._context.as_ptr(), + sys::nix_flake_lock_flags_set_mode_check( + self._context.as_ptr(), + self.inner.as_ptr(), + ), + )?; + } + Ok(self) + } + + /// Update the lock file in memory only; do not write it to disk. + /// + /// # Errors + /// + /// Returns an error if the C API call fails. + pub fn set_mode_virtual(self) -> Result { + // SAFETY: context and flags are valid + unsafe { + check_err( + self._context.as_ptr(), + sys::nix_flake_lock_flags_set_mode_virtual( + self._context.as_ptr(), + self.inner.as_ptr(), + ), + )?; + } + Ok(self) + } + + /// Update and write the lock file to disk if it needs updating. + /// + /// # Errors + /// + /// Returns an error if the C API call fails. + pub fn set_mode_write_as_needed(self) -> Result { + // SAFETY: context and flags are valid + unsafe { + check_err( + self._context.as_ptr(), + sys::nix_flake_lock_flags_set_mode_write_as_needed( + self._context.as_ptr(), + self.inner.as_ptr(), + ), + )?; + } + Ok(self) + } + + /// Override a specific input with an alternative flake reference. + /// + /// `input_path` identifies the input (e.g. `"nixpkgs"`). + /// + /// # Errors + /// + /// Returns an error if the C API call fails. + pub fn add_input_override(self, input_path: &str, flake_ref: &FlakeReference) -> Result { + let path_c = CString::new(input_path)?; + // SAFETY: context, flags, path_c, and flake_ref are valid + unsafe { + check_err( + self._context.as_ptr(), + sys::nix_flake_lock_flags_add_input_override( + self._context.as_ptr(), + self.inner.as_ptr(), + path_c.as_ptr(), + flake_ref.inner.as_ptr(), + ), + )?; + } + Ok(self) + } + + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_flake_lock_flags { + self.inner.as_ptr() + } +} + +impl Drop for LockFlags { + fn drop(&mut self) { + // SAFETY: We own the flags and they are valid until drop + unsafe { + sys::nix_flake_lock_flags_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: LockFlags can be shared between threads +unsafe impl Send for LockFlags {} +unsafe impl Sync for LockFlags {} + +/// Callback that collects a string returned from the Nix C API via a pointer +/// and length pair into an `Option` stored in `user_data`. +unsafe extern "C" fn collect_fragment_cb( + start: *const std::os::raw::c_char, + n: std::os::raw::c_uint, + user_data: *mut std::os::raw::c_void, +) { + let result = unsafe { &mut *(user_data as *mut Option) }; + if !start.is_null() { + let bytes = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + *result = std::str::from_utf8(bytes).ok().map(|s| s.to_owned()); + } +} + +/// An unresolved flake reference. +/// +/// Obtain one via [`FlakeReference::parse`], then pass it to +/// [`LockedFlake::lock`] (or [`LockFlags::add_input_override`]). +pub struct FlakeReference { + inner: NonNull, + _context: Arc, +} + +impl FlakeReference { + /// Parse a flake reference string into a [`FlakeReference`]. + /// + /// Returns both the parsed reference and any fragment that followed a `#` + /// in the input string. For references without a fragment the second + /// element is an empty string. + /// + /// # Errors + /// + /// Returns an error if the C API call fails or returns a null pointer. + pub fn parse( + context: &Arc, + fetch_settings: &FetchersSettings, + flake_settings: &FlakeSettings, + parse_flags: &FlakeReferenceParseFlags, + s: &str, + ) -> Result<(Self, String)> { + let bytes = s.as_bytes(); + + let mut out_ptr: *mut sys::nix_flake_reference = std::ptr::null_mut(); + let mut fragment: Option = None; + + // SAFETY: all arguments are valid; we capture the fragment via callback + let err = unsafe { + sys::nix_flake_reference_and_fragment_from_string( + context.as_ptr(), + fetch_settings.as_ptr(), + flake_settings.as_ptr(), + parse_flags.as_ptr(), + bytes.as_ptr().cast(), + bytes.len(), + &mut out_ptr as *mut *mut sys::nix_flake_reference, + Some(collect_fragment_cb), + &mut fragment as *mut Option as *mut std::os::raw::c_void, + ) + }; + + check_err(unsafe { context.as_ptr() }, err)?; + + let inner = NonNull::new(out_ptr).ok_or(Error::NullPointer)?; + + let frag = fragment.unwrap_or_default(); + + Ok(( + FlakeReference { + inner, + _context: Arc::clone(context), + }, + frag, + )) + } +} + +impl Drop for FlakeReference { + fn drop(&mut self) { + // SAFETY: We own the reference and it is valid until drop + unsafe { + sys::nix_flake_reference_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: FlakeReference can be shared between threads +unsafe impl Send for FlakeReference {} +unsafe impl Sync for FlakeReference {} + +/// A fully locked flake. +/// +/// Obtain one via [`LockedFlake::lock`], then call +/// [`output_attrs`](LockedFlake::output_attrs) to get the attribute set of +/// flake outputs. +pub struct LockedFlake { + inner: NonNull, + _context: Arc, +} + +impl LockedFlake { + /// Lock a flake, resolving and pinning all inputs. + /// + /// # Errors + /// + /// Returns an error if the C API call fails or returns a null pointer. + pub fn lock( + context: &Arc, + fetch_settings: &FetchersSettings, + flake_settings: &FlakeSettings, + eval_state: &EvalState, + lock_flags: &LockFlags, + flake_ref: &FlakeReference, + ) -> Result { + // SAFETY: all arguments are valid + let ptr = unsafe { + sys::nix_flake_lock( + context.as_ptr(), + fetch_settings.as_ptr(), + flake_settings.as_ptr(), + eval_state.as_ptr(), + lock_flags.as_ptr(), + flake_ref.inner.as_ptr(), + ) + }; + + let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?; + + Ok(LockedFlake { + inner, + _context: Arc::clone(context), + }) + } + + /// Get the output attributes of this locked flake as a Nix value. + /// + /// The returned [`Value`] is tied to the lifetime of `eval_state`. + /// + /// # Errors + /// + /// Returns an error if the C API call fails. + pub fn output_attrs<'s>( + &self, + flake_settings: &FlakeSettings, + eval_state: &'s EvalState, + ) -> Result> { + // SAFETY: all pointers are valid + let ptr = unsafe { + sys::nix_locked_flake_get_output_attrs( + self._context.as_ptr(), + flake_settings.as_ptr(), + eval_state.as_ptr(), + self.inner.as_ptr(), + ) + }; + + let inner = std::ptr::NonNull::new(ptr).ok_or(Error::NullPointer)?; + + Ok(Value { + inner, + state: eval_state, + }) + } +} + +impl Drop for LockedFlake { + fn drop(&mut self) { + // SAFETY: We own the locked flake and it is valid until drop + unsafe { + sys::nix_locked_flake_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: LockedFlake can be shared between threads +unsafe impl Send for LockedFlake {} +unsafe impl Sync for LockedFlake {} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use serial_test::serial; + + use super::*; + use crate::{Context, EvalStateBuilder, Store}; + + fn make_state(ctx: &Arc) -> (Arc, EvalState) { + let store = Arc::new(Store::open(ctx, None).expect("Failed to open store")); + let flake_settings = FlakeSettings::new(ctx).expect("Failed to create flake settings"); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .with_flake_settings(&flake_settings) + .expect("Failed to apply flake settings") + .build() + .expect("Failed to build state"); + (store, state) + } + + #[test] + #[serial] + fn test_flake_settings_new() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let _settings = FlakeSettings::new(&ctx).expect("Failed to create flake settings"); + } + + #[test] + #[serial] + fn test_flake_settings_with_eval_state() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + make_state(&ctx); + } + + #[test] + #[serial] + fn test_fetchers_settings_new() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let _s = FetchersSettings::new(&ctx).expect("Failed to create fetcher settings"); + } + + #[test] + #[serial] + fn test_flake_reference_parse_flags_new() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let settings = FlakeSettings::new(&ctx).expect("Failed to create flake settings"); + let _f = + FlakeReferenceParseFlags::new(&ctx, &settings).expect("Failed to create parse flags"); + } + + #[test] + #[serial] + fn test_flake_reference_parse_flags_set_base_directory() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let settings = FlakeSettings::new(&ctx).expect("Failed to create flake settings"); + let _f = FlakeReferenceParseFlags::new(&ctx, &settings) + .expect("Failed to create parse flags") + .set_base_directory("/tmp") + .expect("Failed to set base directory"); + } + + #[test] + #[serial] + fn test_lock_flags_new() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let settings = FlakeSettings::new(&ctx).expect("Failed to create flake settings"); + let _f = LockFlags::new(&ctx, &settings).expect("Failed to create lock flags"); + } + + #[test] + #[serial] + fn test_lock_flags_set_modes() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let settings = FlakeSettings::new(&ctx).expect("Failed to create flake settings"); + let _check = LockFlags::new(&ctx, &settings) + .expect("create") + .set_mode_check() + .expect("set_mode_check"); + let _virtual = LockFlags::new(&ctx, &settings) + .expect("create") + .set_mode_virtual() + .expect("set_mode_virtual"); + let _write = LockFlags::new(&ctx, &settings) + .expect("create") + .set_mode_write_as_needed() + .expect("set_mode_write_as_needed"); + } +} diff --git a/nix-bindings/nix-bindings/src/lib.rs b/nix-bindings/nix-bindings/src/lib.rs new file mode 100644 index 0000000..dc0ee0f --- /dev/null +++ b/nix-bindings/nix-bindings/src/lib.rs @@ -0,0 +1,1746 @@ +#![warn(missing_docs)] +//! High-level, safe Rust bindings for the Nix build tool. +//! +//! This crate provides ergonomic and idiomatic Rust APIs for interacting +//! with Nix using its C API. +//! +//! # Quick Start +//! +//! ```no_run +//! #[cfg(feature = "store")] +//! { +//! use std::sync::Arc; +//! +//! use nix_bindings::{Context, EvalStateBuilder, Store}; +//! +//! fn main() -> Result<(), Box> { +//! let ctx = Arc::new(Context::new()?); +//! let store = Arc::new(Store::open(&ctx, None)?); +//! let state = EvalStateBuilder::new(&store)?.build()?; +//! +//! let result = state.eval_from_string("1 + 2", "")?; +//! println!("Result: {}", result.as_int()?); +//! +//! Ok(()) +//! } +//! } +//! ``` +//! +//! # Value Formatting +//! +//! Values support multiple formatting options: +//! +//! ```no_run +//! #[cfg(feature = "expr")] +//! { +//! use std::sync::Arc; +//! +//! use nix_bindings::{Context, EvalStateBuilder, Store}; +//! fn main() -> Result<(), Box> { +//! let ctx = Arc::new(Context::new()?); +//! let store = Arc::new(Store::open(&ctx, None)?); +//! let state = EvalStateBuilder::new(&store)?.build()?; +//! let value = state.eval_from_string("\"hello world\"", "")?; +//! +//! // Display formatting (user-friendly) +//! println!("{}", value); // => hello world +//! +//! // Debug formatting (with type info) +//! println!("{:?}", value); // => Value::String("hello world") +//! +//! // Nix syntax formatting +//! println!("{}", value.to_nix_string()?); // => "hello world" +//! // +//! Ok(()) +//! } +//! } +//! ``` + +use std::fmt; +#[cfg(any( + feature = "store", + feature = "expr", + feature = "flake", + feature = "external", + feature = "primop" +))] +use std::{ + ffi::{CStr, CString}, + ptr::NonNull, +}; +#[cfg(any( + feature = "expr", + feature = "flake", + feature = "external", + feature = "primop" +))] +use std::{path::Path, sync::Arc}; + +#[cfg(feature = "expr")] +mod attrs; +#[cfg(feature = "expr")] +mod lists; + +#[cfg(feature = "external")] +pub mod external; +#[cfg(feature = "flake")] +pub mod flake; +#[cfg(feature = "primop")] +pub mod primop; + +#[cfg(all(test, any(feature = "store", feature = "expr")))] +use serial_test::serial; + +/// Raw, unsafe FFI bindings to the Nix C API. +/// +/// # Warning +/// +/// This module exposes the low-level, unsafe C bindings. Prefer using the safe, +/// high-level APIs provided by this crate. Use at your own risk. +#[doc(hidden)] +pub mod sys { + pub use nix_bindings_sys::*; +} + +/// Result type for Nix operations. +pub type Result = std::result::Result; + +/// Error types for Nix operations. +#[derive(Debug)] +pub enum Error { + /// Unknown error from Nix C API. + Unknown(String), + + /// Overflow error. + Overflow, + + /// Key not found error. + KeyNotFound(String), + + /// List index out of bounds. + IndexOutOfBounds { + /// The index that was requested. + index: usize, + /// The actual length of the list. + length: usize, + }, + + /// Nix evaluation error. + EvalError(String), + + /// Invalid value type conversion. + InvalidType { + /// Expected type. + expected: &'static str, + /// Actual type. + actual: String, + }, + /// Null pointer error. + NullPointer, + + /// String conversion error. + StringConversion(std::ffi::NulError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Unknown(msg) => write!(f, "Unknown error: {msg}"), + Error::Overflow => write!(f, "Overflow error"), + Error::KeyNotFound(key) => write!(f, "Key not found: {key}"), + Error::IndexOutOfBounds { index, length } => { + write!(f, "Index out of bounds: index {index}, length {length}") + } + Error::EvalError(msg) => write!(f, "Evaluation error: {msg}"), + Error::InvalidType { expected, actual } => { + write!(f, "Invalid type: expected {expected}, got {actual}") + } + Error::NullPointer => write!(f, "Null pointer error"), + Error::StringConversion(e) => write!(f, "String conversion error: {e}"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(e: std::ffi::NulError) -> Self { + Error::StringConversion(e) + } +} + +#[cfg(feature = "store")] +mod store; +#[cfg(feature = "store")] +pub use store::{Derivation, Store, StorePath}; + +/// Extract a string from a Nix context using a callback-based API. +/// +/// Many Nix C API functions return strings via callbacks. This helper +/// makes that pattern ergonomic. +/// +/// # Safety +/// +/// `call` must invoke `callback` with a valid string pointer and length. +#[cfg(feature = "store")] +unsafe fn string_from_callback(call: F) -> Option +where + F: FnOnce(sys::nix_get_string_callback, *mut std::os::raw::c_void), +{ + unsafe extern "C" fn collect( + start: *const std::os::raw::c_char, + n: std::os::raw::c_uint, + user_data: *mut std::os::raw::c_void, + ) { + let result = unsafe { &mut *(user_data as *mut Option) }; + if !start.is_null() { + let bytes = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + *result = std::str::from_utf8(bytes).ok().map(|s| s.to_owned()); + } + } + + let mut result: Option = None; + let user_data = &mut result as *mut _ as *mut std::os::raw::c_void; + call(Some(collect), user_data); + result +} + +/// Check a Nix error code and convert to `Result`, extracting the real +/// error message from the context. +#[cfg(feature = "store")] +fn check_err(ctx: *mut sys::nix_c_context, err: sys::nix_err) -> Result<()> { + if err == sys::nix_err_NIX_OK { + return Ok(()); + } + + // Extract the real error message from the context. + // nix_err_msg returns a borrowed pointer valid until the next Nix call. + // We must copy it to a String immediately. + let msg = unsafe { + let ptr = sys::nix_err_msg(std::ptr::null_mut(), ctx, std::ptr::null_mut()); + if ptr.is_null() { + None + } else { + Some(CStr::from_ptr(ptr).to_string_lossy().into_owned()) + } + }; + + // For NIX_ERR_NIX_ERROR, also try to get the richer info message. + let detail = if err == sys::nix_err_NIX_ERR_NIX_ERROR { + unsafe { + string_from_callback(|cb, ud| { + sys::nix_err_info_msg(std::ptr::null_mut(), ctx, cb, ud); + }) + } + } else { + None + }; + + let message = detail + .or(msg) + .unwrap_or_else(|| format!("Nix error code: {err}")); + + match err { + sys::nix_err_NIX_ERR_UNKNOWN => Err(Error::Unknown(message)), + sys::nix_err_NIX_ERR_OVERFLOW => Err(Error::Overflow), + sys::nix_err_NIX_ERR_KEY => Err(Error::KeyNotFound(message)), + sys::nix_err_NIX_ERR_NIX_ERROR => Err(Error::EvalError(message)), + _ => Err(Error::Unknown(message)), + } +} + +/// Return the version of the Nix library being used. +/// +/// This is a free function that does not require a context. +#[cfg(feature = "store")] +#[must_use] +pub fn nix_version() -> &'static str { + // SAFETY: nix_version_get returns a pointer to a static string literal + unsafe { + let ptr = sys::nix_version_get(); + if ptr.is_null() { + "" + } else { + CStr::from_ptr(ptr).to_str().unwrap_or("") + } + } +} + +/// Verbosity level for Nix log output. +#[cfg(feature = "store")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Verbosity { + /// Only errors. + Error, + /// Warnings and errors. + Warn, + /// Notices, warnings, and errors. + Notice, + /// Info messages. + Info, + /// Talkative output. + Talkative, + /// Chatty output. + Chatty, + /// Debug output. + Debug, + /// Maximum verbosity (vomit). + Vomit, +} + +#[cfg(feature = "store")] +impl Verbosity { + fn to_c(self) -> sys::nix_verbosity { + match self { + Verbosity::Error => sys::nix_verbosity_NIX_LVL_ERROR, + Verbosity::Warn => sys::nix_verbosity_NIX_LVL_WARN, + Verbosity::Notice => sys::nix_verbosity_NIX_LVL_NOTICE, + Verbosity::Info => sys::nix_verbosity_NIX_LVL_INFO, + Verbosity::Talkative => sys::nix_verbosity_NIX_LVL_TALKATIVE, + Verbosity::Chatty => sys::nix_verbosity_NIX_LVL_CHATTY, + Verbosity::Debug => sys::nix_verbosity_NIX_LVL_DEBUG, + Verbosity::Vomit => sys::nix_verbosity_NIX_LVL_VOMIT, + } + } +} + +/// Nix context for managing library state. +/// +/// This is the root object for all Nix operations. It manages the lifetime +/// of the Nix C API context and provides automatic cleanup. +#[cfg(feature = "store")] +pub struct Context { + inner: NonNull, +} + +#[cfg(feature = "store")] +impl Context { + /// Create a new Nix context. + /// + /// This initializes the Nix C API context and the required libraries. + /// + /// # Errors + /// + /// Returns an error if context creation or library initialization fails. + pub fn new() -> Result { + // SAFETY: nix_c_context_create is safe to call + let ctx_ptr = unsafe { sys::nix_c_context_create() }; + let inner = NonNull::new(ctx_ptr).ok_or(Error::NullPointer)?; + + let ctx = Context { inner }; + + // Initialize required libraries + unsafe { + check_err( + ctx.inner.as_ptr(), + sys::nix_libutil_init(ctx.inner.as_ptr()), + )?; + check_err( + ctx.inner.as_ptr(), + sys::nix_libstore_init(ctx.inner.as_ptr()), + )?; + check_err( + ctx.inner.as_ptr(), + sys::nix_libexpr_init(ctx.inner.as_ptr()), + )?; + } + + Ok(ctx) + } + + /// Set a global Nix configuration setting. + /// + /// Settings take effect for new [`EvalState`] instances. Use + /// `"extra-"` to append to an existing setting's value. + /// + /// # Errors + /// + /// Returns [`Error::KeyNotFound`] if the setting key is unknown. + pub fn set_setting(&self, key: &str, value: &str) -> Result<()> { + let key_c = CString::new(key)?; + let value_c = CString::new(value)?; + // SAFETY: context and strings are valid + unsafe { + check_err( + self.inner.as_ptr(), + sys::nix_setting_set(self.inner.as_ptr(), key_c.as_ptr(), value_c.as_ptr()), + ) + } + } + + /// Get the value of a global Nix configuration setting. + /// + /// # Errors + /// + /// Returns [`Error::KeyNotFound`] if the setting key is unknown. + pub fn get_setting(&self, key: &str) -> Result { + let key_c = CString::new(key)?; + let mut err_code = sys::nix_err_NIX_OK; + // SAFETY: context and key are valid + let result = unsafe { + string_from_callback(|cb, ud| { + err_code = sys::nix_setting_get(self.inner.as_ptr(), key_c.as_ptr(), cb, ud); + }) + }; + check_err(self.inner.as_ptr(), err_code)?; + result.ok_or_else(|| Error::KeyNotFound(key.to_string())) + } + + /// Set the verbosity level for Nix log output. + /// + /// # Errors + /// + /// Returns an error if the verbosity level cannot be set. + pub fn set_verbosity(&self, level: Verbosity) -> Result<()> { + // SAFETY: context is valid + unsafe { + check_err( + self.inner.as_ptr(), + sys::nix_set_verbosity(self.inner.as_ptr(), level.to_c()), + ) + } + } + + /// Get the raw context pointer. + /// + /// # Safety + /// + /// The caller must ensure the pointer is used safely. + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_c_context { + self.inner.as_ptr() + } +} + +#[cfg(feature = "store")] +impl Drop for Context { + fn drop(&mut self) { + // SAFETY: We own the context and it's valid until drop + unsafe { + sys::nix_c_context_free(self.inner.as_ptr()); + } + } +} + +#[cfg(feature = "store")] +unsafe impl Send for Context {} + +#[cfg(feature = "store")] +unsafe impl Sync for Context {} + +/// Builder for Nix evaluation state. +/// +/// This allows configuring the evaluation environment before creating +/// the evaluation state. +#[cfg(feature = "expr")] +pub struct EvalStateBuilder { + inner: NonNull, + store: Arc, + context: Arc, + skip_load: bool, +} + +#[cfg(feature = "expr")] +impl EvalStateBuilder { + /// Create a new evaluation state builder. + /// + /// # Arguments + /// + /// * `store` - The Nix store to use for evaluation + /// + /// # Errors + /// + /// Returns an error if the builder cannot be created. + pub fn new(store: &Arc) -> Result { + // SAFETY: store context and store are valid + let builder_ptr = + unsafe { sys::nix_eval_state_builder_new(store._context.as_ptr(), store.as_ptr()) }; + + let inner = NonNull::new(builder_ptr).ok_or(Error::NullPointer)?; + + Ok(EvalStateBuilder { + inner, + store: Arc::clone(store), + context: Arc::clone(&store._context), + skip_load: false, + }) + } + + /// Set the lookup path (`NIX_PATH`) for `<...>` expressions. + /// + /// Each entry should be in the form `"name=path"` or just `"path"`, + /// matching the format of `NIX_PATH` entries. + /// + /// # Errors + /// + /// Returns an error if the lookup path cannot be set. + pub fn set_lookup_path(self, paths: &[impl AsRef]) -> Result { + // Build null-terminated array of C strings. + let c_strings: Vec = paths + .iter() + .map(|s| CString::new(s.as_ref())) + .collect::>()?; + + let mut ptrs: Vec<*const std::os::raw::c_char> = + c_strings.iter().map(|cs| cs.as_ptr()).collect(); + ptrs.push(std::ptr::null()); // null terminator + + // SAFETY: context and builder are valid, ptrs is null-terminated + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_eval_state_builder_set_lookup_path( + self.context.as_ptr(), + self.inner.as_ptr(), + ptrs.as_mut_ptr(), + ), + )?; + } + + Ok(self) + } + + /// Apply flake settings to the evaluation state builder. + /// + /// This enables `builtins.getFlake` and related flake functionality + /// in the resulting [`EvalState`]. + /// + /// # Errors + /// + /// Returns an error if the flake settings cannot be applied. + #[cfg(feature = "flake")] + pub fn with_flake_settings(self, settings: &flake::FlakeSettings) -> Result { + // SAFETY: context, settings, and builder are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_flake_settings_add_to_eval_state_builder( + self.context.as_ptr(), + settings.as_ptr(), + self.inner.as_ptr(), + ), + )?; + } + + Ok(self) + } + + /// Skip loading Nix configuration from the environment. + /// + /// By default [`build`](Self::build) calls `nix_eval_state_builder_load` to + /// read configuration from environment variables and config files. Call + /// this method to skip that step, which is useful in tests or sandboxed + /// environments. + #[must_use] + pub fn no_load_config(mut self) -> Self { + self.skip_load = true; + self + } + + /// Build the evaluation state. + /// + /// # Errors + /// + /// Returns an error if the evaluation state cannot be built. + pub fn build(self) -> Result { + // Load configuration from environment first (unless suppressed). + // SAFETY: context and builder are valid + if !self.skip_load { + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_eval_state_builder_load(self.context.as_ptr(), self.inner.as_ptr()), + )?; + } + } + + // Build the state + // SAFETY: context and builder are valid + let state_ptr = + unsafe { sys::nix_eval_state_build(self.context.as_ptr(), self.inner.as_ptr()) }; + + let inner = NonNull::new(state_ptr).ok_or(Error::NullPointer)?; + + Ok(EvalState { + inner, + store: self.store.clone(), + context: self.context.clone(), + }) + } +} + +#[cfg(feature = "expr")] +impl Drop for EvalStateBuilder { + fn drop(&mut self) { + // SAFETY: We own the builder and it's valid until drop + unsafe { + sys::nix_eval_state_builder_free(self.inner.as_ptr()); + } + } +} + +/// Nix evaluation state for evaluating expressions. +/// +/// This provides the main interface for evaluating Nix expressions +/// and creating values. +#[cfg(feature = "expr")] +pub struct EvalState { + pub(crate) inner: NonNull, + #[expect(dead_code, reason = "keeps the Arc alive Drop side-effects")] + store: Arc, + pub(crate) context: Arc, +} + +#[cfg(feature = "expr")] +impl EvalState { + /// Evaluate a Nix expression from a string. + /// + /// # Arguments + /// + /// * `expr` - The Nix expression to evaluate + /// * `path` - The path to use for error reporting (e.g., `""`) + /// + /// # Errors + /// + /// Returns an error if evaluation fails. + pub fn eval_from_string(&self, expr: &str, path: &str) -> Result> { + let expr_c = CString::new(expr)?; + let path_c = CString::new(path)?; + + // Allocate value for result + // SAFETY: context and state are valid + let value_ptr = unsafe { sys::nix_alloc_value(self.context.as_ptr(), self.inner.as_ptr()) }; + if value_ptr.is_null() { + return Err(Error::NullPointer); + } + + // Evaluate expression + // SAFETY: all pointers are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_expr_eval_from_string( + self.context.as_ptr(), + self.inner.as_ptr(), + expr_c.as_ptr(), + path_c.as_ptr(), + value_ptr, + ), + )?; + } + + let inner = NonNull::new(value_ptr).ok_or(Error::NullPointer)?; + + Ok(Value { inner, state: self }) + } + + /// Evaluate a Nix expression from a file. + /// + /// Reads the file at `path`, then evaluates its contents using the parent + /// directory as the base path for relative imports. + /// + /// # Errors + /// + /// Returns an error if the file cannot be read or if evaluation fails. + pub fn eval_from_file(&self, path: impl AsRef) -> Result> { + let path = path.as_ref(); + let expr = std::fs::read_to_string(path) + .map_err(|e| Error::Unknown(format!("Failed to read file {}: {e}", path.display())))?; + let base = path + .parent() + .unwrap_or_else(|| Path::new(".")) + .to_str() + .ok_or_else(|| Error::Unknown("File path is not valid UTF-8".to_string()))?; + self.eval_from_string(&expr, base) + } + + /// Allocate a new uninitialized value. + /// + /// # Errors + /// + /// Returns an error if value allocation fails. + pub fn alloc_value(&self) -> Result> { + // SAFETY: context and state are valid + let value_ptr = unsafe { sys::nix_alloc_value(self.context.as_ptr(), self.inner.as_ptr()) }; + let inner = NonNull::new(value_ptr).ok_or(Error::NullPointer)?; + + Ok(Value { inner, state: self }) + } + + /// Create a Nix integer value. + /// + /// # Errors + /// + /// Returns an error if value allocation or initialization fails. + pub fn make_int(&self, i: i64) -> Result> { + let v = self.alloc_value()?; + // SAFETY: context and value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_init_int(self.context.as_ptr(), v.inner.as_ptr(), i), + )?; + } + Ok(v) + } + + /// Create a Nix float value. + /// + /// # Errors + /// + /// Returns an error if value allocation or initialization fails. + pub fn make_float(&self, f: f64) -> Result> { + let v = self.alloc_value()?; + // SAFETY: context and value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_init_float(self.context.as_ptr(), v.inner.as_ptr(), f), + )?; + } + Ok(v) + } + + /// Create a Nix boolean value. + /// + /// # Errors + /// + /// Returns an error if value allocation or initialization fails. + pub fn make_bool(&self, b: bool) -> Result> { + let v = self.alloc_value()?; + // SAFETY: context and value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_init_bool(self.context.as_ptr(), v.inner.as_ptr(), b), + )?; + } + Ok(v) + } + + /// Create a Nix null value. + /// + /// # Errors + /// + /// Returns an error if value allocation or initialization fails. + pub fn make_null(&self) -> Result> { + let v = self.alloc_value()?; + // SAFETY: context and value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_init_null(self.context.as_ptr(), v.inner.as_ptr()), + )?; + } + Ok(v) + } + + /// Create a Nix string value. + /// + /// # Errors + /// + /// Returns an error if value allocation, string conversion, or + /// initialization fails. + pub fn make_string(&self, s: &str) -> Result> { + let v = self.alloc_value()?; + let s_c = CString::new(s)?; + // SAFETY: context and value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_init_string(self.context.as_ptr(), v.inner.as_ptr(), s_c.as_ptr()), + )?; + } + Ok(v) + } + + /// Create a Nix path value. + /// + /// # Errors + /// + /// Returns an error if value allocation, path conversion, or + /// initialization fails. + pub fn make_path(&self, path: impl AsRef) -> Result> { + let v = self.alloc_value()?; + let path_str = path + .as_ref() + .to_str() + .ok_or_else(|| Error::Unknown("Path is not valid UTF-8".to_string()))?; + let path_c = CString::new(path_str)?; + // SAFETY: context, state, and value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_init_path_string( + self.context.as_ptr(), + self.inner.as_ptr(), + v.inner.as_ptr(), + path_c.as_ptr(), + ), + )?; + } + Ok(v) + } + + /// Create a Nix list value from a slice of values. + /// + /// # Errors + /// + /// Returns an error if value allocation or list construction fails. + pub fn make_list(&self, items: &[&Value<'_>]) -> Result> { + // SAFETY: context and state are valid + let builder = unsafe { + sys::nix_make_list_builder(self.context.as_ptr(), self.inner.as_ptr(), items.len()) + }; + if builder.is_null() { + return Err(Error::NullPointer); + } + + // Free the builder on all paths, including early returns on error. + struct ListBuilderGuard(*mut sys::ListBuilder); + impl Drop for ListBuilderGuard { + fn drop(&mut self) { + unsafe { sys::nix_list_builder_free(self.0) }; + } + } + let _guard = ListBuilderGuard(builder); + + // Insert each item + for (i, item) in items.iter().enumerate() { + // SAFETY: context, builder, and value are valid; index is in bounds + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_list_builder_insert( + self.context.as_ptr(), + builder, + i as std::os::raw::c_uint, + item.inner.as_ptr(), + ), + )?; + } + } + + let result = self.alloc_value()?; + // SAFETY: context, builder, and result value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_make_list(self.context.as_ptr(), builder, result.inner.as_ptr()), + )?; + } + + Ok(result) + } + + /// Create a Nix attribute set from key-value pairs. + /// + /// # Errors + /// + /// Returns an error if value allocation or attribute set construction fails. + pub fn make_attrs<'s>(&'s self, pairs: &[(&str, &Value<'_>)]) -> Result> { + // SAFETY: context and state are valid + let builder = unsafe { + sys::nix_make_bindings_builder(self.context.as_ptr(), self.inner.as_ptr(), pairs.len()) + }; + if builder.is_null() { + return Err(Error::NullPointer); + } + + // Free the builder on all paths, including early returns on error. + struct BindingsBuilderGuard(*mut sys::BindingsBuilder); + impl Drop for BindingsBuilderGuard { + fn drop(&mut self) { + unsafe { sys::nix_bindings_builder_free(self.0) }; + } + } + let _guard = BindingsBuilderGuard(builder); + + // Insert each key-value pair + for (key, value) in pairs { + let key_c = CString::new(*key)?; + // SAFETY: context, builder, key, and value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_bindings_builder_insert( + self.context.as_ptr(), + builder, + key_c.as_ptr(), + value.inner.as_ptr(), + ), + )?; + } + } + + let result = self.alloc_value()?; + // SAFETY: context, builder, and result value are valid + unsafe { + check_err( + self.context.as_ptr(), + sys::nix_make_attrs(self.context.as_ptr(), result.inner.as_ptr(), builder), + )?; + } + + Ok(result) + } + + /// Get the raw state pointer. + /// + /// # Safety + /// + /// The caller must ensure the pointer is used safely. + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::EvalState { + self.inner.as_ptr() + } +} + +#[cfg(feature = "expr")] +impl Drop for EvalState { + fn drop(&mut self) { + // SAFETY: We own the state and it's valid until drop + unsafe { + sys::nix_state_free(self.inner.as_ptr()); + } + } +} + +#[cfg(feature = "expr")] +unsafe impl Send for EvalState {} + +#[cfg(feature = "expr")] +unsafe impl Sync for EvalState {} + +/// Nix value types. +#[cfg(feature = "expr")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValueType { + /// Thunk (unevaluated expression). + Thunk, + /// Integer value. + Int, + /// Float value. + Float, + /// Boolean value. + Bool, + /// String value. + String, + /// Path value. + Path, + /// Null value. + Null, + /// Attribute set. + Attrs, + /// List. + List, + /// Function. + Function, + /// External value. + External, +} + +#[cfg(feature = "expr")] +impl ValueType { + fn from_c(value_type: sys::ValueType) -> Self { + match value_type { + sys::ValueType_NIX_TYPE_THUNK => ValueType::Thunk, + sys::ValueType_NIX_TYPE_INT => ValueType::Int, + sys::ValueType_NIX_TYPE_FLOAT => ValueType::Float, + sys::ValueType_NIX_TYPE_BOOL => ValueType::Bool, + sys::ValueType_NIX_TYPE_STRING => ValueType::String, + sys::ValueType_NIX_TYPE_PATH => ValueType::Path, + sys::ValueType_NIX_TYPE_NULL => ValueType::Null, + sys::ValueType_NIX_TYPE_ATTRS => ValueType::Attrs, + sys::ValueType_NIX_TYPE_LIST => ValueType::List, + sys::ValueType_NIX_TYPE_FUNCTION => ValueType::Function, + sys::ValueType_NIX_TYPE_EXTERNAL => ValueType::External, + _ => ValueType::Thunk, // fallback + } + } +} + +#[cfg(feature = "expr")] +impl fmt::Display for ValueType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + ValueType::Thunk => "thunk", + ValueType::Int => "int", + ValueType::Float => "float", + ValueType::Bool => "bool", + ValueType::String => "string", + ValueType::Path => "path", + ValueType::Null => "null", + ValueType::Attrs => "attrs", + ValueType::List => "list", + ValueType::Function => "function", + ValueType::External => "external", + }; + write!(f, "{name}") + } +} + +/// A Nix value. +/// +/// This represents any value in the Nix language, including primitives, +/// collections, and functions. Values are GC-managed; this struct holds +/// a reference count that is released on drop. +#[cfg(feature = "expr")] +pub struct Value<'a> { + pub(crate) inner: NonNull, + pub(crate) state: &'a EvalState, +} + +#[cfg(feature = "expr")] +impl Value<'_> { + /// Force evaluation of this value. + /// + /// If the value is a thunk, this will evaluate it to its final form. + /// + /// # Errors + /// + /// Returns an error if evaluation fails. + pub fn force(&mut self) -> Result<()> { + // SAFETY: context, state, and value are valid + unsafe { + check_err( + self.state.context.as_ptr(), + sys::nix_value_force( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + ), + ) + } + } + + /// Force deep evaluation of this value. + /// + /// This forces evaluation of the value and all its nested components. + /// + /// # Errors + /// + /// Returns an error if evaluation fails. + pub fn force_deep(&mut self) -> Result<()> { + // SAFETY: context, state, and value are valid + unsafe { + check_err( + self.state.context.as_ptr(), + sys::nix_value_force_deep( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + ), + ) + } + } + + /// Get the type of this value. + #[must_use] + pub fn value_type(&self) -> ValueType { + // SAFETY: context and value are valid + let c_type = unsafe { sys::nix_get_type(self.state.context.as_ptr(), self.inner.as_ptr()) }; + ValueType::from_c(c_type) + } + + /// Convert this value to an integer. + /// + /// # Errors + /// + /// Returns an error if the value is not an integer. + pub fn as_int(&self) -> Result { + if self.value_type() != ValueType::Int { + return Err(Error::InvalidType { + expected: "int", + actual: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let result = unsafe { sys::nix_get_int(self.state.context.as_ptr(), self.inner.as_ptr()) }; + + Ok(result) + } + + /// Convert this value to a float. + /// + /// # Errors + /// + /// Returns an error if the value is not a float. + pub fn as_float(&self) -> Result { + if self.value_type() != ValueType::Float { + return Err(Error::InvalidType { + expected: "float", + actual: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let result = + unsafe { sys::nix_get_float(self.state.context.as_ptr(), self.inner.as_ptr()) }; + + Ok(result) + } + + /// Convert this value to a boolean. + /// + /// # Errors + /// + /// Returns an error if the value is not a boolean. + pub fn as_bool(&self) -> Result { + if self.value_type() != ValueType::Bool { + return Err(Error::InvalidType { + expected: "bool", + actual: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let result = unsafe { sys::nix_get_bool(self.state.context.as_ptr(), self.inner.as_ptr()) }; + + Ok(result) + } + + /// Convert this value to a string. + /// + /// This realises the string (resolves any context/store paths) and + /// returns its content. + /// + /// # Errors + /// + /// Returns an error if the value is not a string. + pub fn as_string(&self) -> Result { + if self.value_type() != ValueType::String { + return Err(Error::InvalidType { + expected: "string", + actual: self.value_type().to_string(), + }); + } + + // Use the realised string API to handle string contexts correctly. + // SAFETY: context, state, and value are valid; type is checked + let realised_str = unsafe { + sys::nix_string_realise( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + false, + ) + }; + + if realised_str.is_null() { + return Err(Error::NullPointer); + } + + // SAFETY: realised_str is non-null + let buffer_start = unsafe { sys::nix_realised_string_get_buffer_start(realised_str) }; + + let buffer_size = unsafe { sys::nix_realised_string_get_buffer_size(realised_str) }; + + if buffer_start.is_null() { + unsafe { sys::nix_realised_string_free(realised_str) }; + return Err(Error::NullPointer); + } + + // SAFETY: buffer_start and buffer_size are valid + let bytes = unsafe { std::slice::from_raw_parts(buffer_start.cast::(), buffer_size) }; + let string = std::str::from_utf8(bytes) + .map_err(|_| Error::Unknown("Invalid UTF-8 in string".to_string()))? + .to_owned(); + + unsafe { sys::nix_realised_string_free(realised_str) }; + + Ok(string) + } + + /// Convert this value to a string and return its store-path context. + /// + /// This is the extended form of [`as_string`](Self::as_string): it returns + /// the string content together with any store paths embedded in the string's + /// context. For ordinary strings the context vector is empty. + /// + /// # Errors + /// + /// Returns an error if the value is not a string. + pub fn as_string_with_context(&self) -> Result<(String, Vec)> { + if self.value_type() != ValueType::String { + return Err(Error::InvalidType { + expected: "string", + actual: self.value_type().to_string(), + }); + } + + // SAFETY: context, state, and value are valid; type is checked + let realised_str = unsafe { + sys::nix_string_realise( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + false, + ) + }; + + if realised_str.is_null() { + return Err(Error::NullPointer); + } + + // Read the string content. + let buffer_start = unsafe { sys::nix_realised_string_get_buffer_start(realised_str) }; + let buffer_size = unsafe { sys::nix_realised_string_get_buffer_size(realised_str) }; + + if buffer_start.is_null() { + unsafe { sys::nix_realised_string_free(realised_str) }; + return Err(Error::NullPointer); + } + + let bytes = unsafe { std::slice::from_raw_parts(buffer_start.cast::(), buffer_size) }; + let string = match std::str::from_utf8(bytes) { + Ok(s) => s.to_owned(), + Err(_) => { + unsafe { sys::nix_realised_string_free(realised_str) }; + return Err(Error::Unknown("Invalid UTF-8 in string".to_string())); + } + }; + + // Collect store-path context. + let count = unsafe { sys::nix_realised_string_get_store_path_count(realised_str) }; + let mut paths = Vec::with_capacity(count); + for i in 0..count { + // SAFETY: index is in bounds + let raw = unsafe { sys::nix_realised_string_get_store_path(realised_str, i) }; + if raw.is_null() { + continue; + } + // Clone the path so we own it independently of the realised_str buffer. + let cloned = unsafe { sys::nix_store_path_clone(raw as *mut sys::StorePath) }; + if let Some(inner) = std::ptr::NonNull::new(cloned) { + paths.push(store::StorePath { + inner, + _context: Arc::clone(&self.state.context), + }); + } + } + + unsafe { sys::nix_realised_string_free(realised_str) }; + + Ok((string, paths)) + } + + /// Convert this value to a filesystem path. + /// + /// # Errors + /// + /// Returns an error if the value is not a path. + pub fn as_path(&self) -> Result { + if self.value_type() != ValueType::Path { + return Err(Error::InvalidType { + expected: "path", + actual: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let path_ptr = + unsafe { sys::nix_get_path_string(self.state.context.as_ptr(), self.inner.as_ptr()) }; + + if path_ptr.is_null() { + return Err(Error::NullPointer); + } + + // SAFETY: path_ptr is a valid C string + let path_str = unsafe { CStr::from_ptr(path_ptr).to_string_lossy().into_owned() }; + + Ok(std::path::PathBuf::from(path_str)) + } + + /// Call this value as a function with a single argument. + /// + /// # Errors + /// + /// Returns an error if this value is not a function or the call fails. + pub fn call(&self, arg: &Value<'_>) -> Result> { + let result = self.state.alloc_value()?; + // SAFETY: context, state, function value, arg value, and result are valid + unsafe { + check_err( + self.state.context.as_ptr(), + sys::nix_value_call( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + arg.inner.as_ptr(), + result.inner.as_ptr(), + ), + )?; + } + Ok(result) + } + + /// Call this value as a curried function with multiple arguments. + /// + /// # Errors + /// + /// Returns an error if this value is not a function or the call fails. + pub fn call_multi(&self, args: &[&Value<'_>]) -> Result> { + let result = self.state.alloc_value()?; + let mut arg_ptrs: Vec<*mut sys::nix_value> = + args.iter().map(|a| a.inner.as_ptr()).collect(); + // SAFETY: context, state, fn, args array, and result are valid + unsafe { + check_err( + self.state.context.as_ptr(), + sys::nix_value_call_multi( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + arg_ptrs.len(), + arg_ptrs.as_mut_ptr(), + result.inner.as_ptr(), + ), + )?; + } + Ok(result) + } + + /// Create a lazy thunk that applies a function to an argument. + /// + /// Unlike [`call`](Self::call), this does not perform the call immediately; + /// it stores it as a thunk to be evaluated lazily. This is useful for + /// constructing lazy attribute sets and lists. + /// + /// # Errors + /// + /// Returns an error if the thunk cannot be created. + pub fn make_thunk<'a>(fn_val: &'a Value<'a>, arg: &'a Value<'a>) -> Result> { + let result = fn_val.state.alloc_value()?; + // SAFETY: context and all value pointers are valid + unsafe { + check_err( + fn_val.state.context.as_ptr(), + sys::nix_init_apply( + fn_val.state.context.as_ptr(), + result.inner.as_ptr(), + fn_val.inner.as_ptr(), + arg.inner.as_ptr(), + ), + )?; + } + Ok(result) + } + + /// Copy this value into a new owned value. + /// + /// # Errors + /// + /// Returns an error if the copy fails. + pub fn copy(&self) -> Result> { + let result = self.state.alloc_value()?; + // SAFETY: context and both value pointers are valid + unsafe { + check_err( + self.state.context.as_ptr(), + sys::nix_copy_value( + self.state.context.as_ptr(), + result.inner.as_ptr(), + self.inner.as_ptr(), + ), + )?; + } + Ok(result) + } + + /// Get the raw value pointer. + /// + /// Format this value as Nix syntax. + /// + /// This provides a string representation that matches Nix's own syntax, + /// making it useful for debugging and displaying values to users. + /// + /// # Errors + /// + /// Returns an error if the value cannot be converted to a string + /// representation. + pub fn to_nix_string(&self) -> Result { + match self.value_type() { + ValueType::Int => Ok(self.as_int()?.to_string()), + ValueType::Float => Ok(self.as_float()?.to_string()), + ValueType::Bool => Ok(if self.as_bool()? { + "true".to_string() + } else { + "false".to_string() + }), + ValueType::String => Ok(format!("\"{}\"", self.as_string()?.replace('"', "\\\""))), + ValueType::Null => Ok("null".to_string()), + ValueType::Attrs => Ok("{ }".to_string()), + ValueType::List => Ok("[ ]".to_string()), + ValueType::Function => Ok("".to_string()), + ValueType::Path => Ok(self + .as_path() + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| "".to_string())), + ValueType::Thunk => Ok("".to_string()), + ValueType::External => Ok("".to_string()), + } + } +} + +#[cfg(feature = "expr")] +impl Drop for Value<'_> { + fn drop(&mut self) { + // SAFETY: We hold a GC reference (automatically incremented for us by + // the Nix C API when the value was returned). Release it here. + unsafe { + sys::nix_value_decref(self.state.context.as_ptr(), self.inner.as_ptr()); + } + } +} + +#[cfg(feature = "expr")] +impl fmt::Display for Value<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.value_type() { + ValueType::Int => { + if let Ok(val) = self.as_int() { + write!(f, "{val}") + } else { + write!(f, "") + } + } + ValueType::Float => { + if let Ok(val) = self.as_float() { + write!(f, "{val}") + } else { + write!(f, "") + } + } + ValueType::Bool => { + if let Ok(val) = self.as_bool() { + write!(f, "{val}") + } else { + write!(f, "") + } + } + ValueType::String => { + if let Ok(val) = self.as_string() { + write!(f, "{val}") + } else { + write!(f, "") + } + } + ValueType::Null => write!(f, "null"), + ValueType::Attrs => write!(f, "{{ }}"), + ValueType::List => write!(f, "[ ]"), + ValueType::Function => write!(f, ""), + ValueType::Path => { + if let Ok(p) = self.as_path() { + write!(f, "{}", p.display()) + } else { + write!(f, "") + } + } + ValueType::Thunk => write!(f, ""), + ValueType::External => write!(f, ""), + } + } +} + +#[cfg(feature = "expr")] +impl fmt::Debug for Value<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value_type = self.value_type(); + match value_type { + ValueType::Int => { + if let Ok(val) = self.as_int() { + write!(f, "Value::Int({val})") + } else { + write!(f, "Value::Int()") + } + } + ValueType::Float => { + if let Ok(val) = self.as_float() { + write!(f, "Value::Float({val})") + } else { + write!(f, "Value::Float()") + } + } + ValueType::Bool => { + if let Ok(val) = self.as_bool() { + write!(f, "Value::Bool({val})") + } else { + write!(f, "Value::Bool()") + } + } + ValueType::String => { + if let Ok(val) = self.as_string() { + write!(f, "Value::String({val:?})") + } else { + write!(f, "Value::String()") + } + } + ValueType::Null => write!(f, "Value::Null"), + ValueType::Attrs => write!(f, "Value::Attrs({{ }})"), + ValueType::List => write!(f, "Value::List([ ])"), + ValueType::Function => write!(f, "Value::Function()"), + ValueType::Path => { + if let Ok(p) = self.as_path() { + write!(f, "Value::Path({})", p.display()) + } else { + write!(f, "Value::Path()") + } + } + ValueType::Thunk => write!(f, "Value::Thunk()"), + ValueType::External => write!(f, "Value::External()"), + } + } +} + +#[cfg(all(test, any(feature = "store", feature = "expr")))] +mod tests { + use super::*; + + #[cfg(feature = "store")] + #[test] + #[serial] + fn test_context_creation() { + let _ctx = Context::new().expect("Failed to create context"); + // Context should be dropped automatically + } + + #[cfg(feature = "store")] + #[test] + #[serial] + fn test_nix_version() { + let version = nix_version(); + assert!(!version.is_empty(), "Version should not be empty"); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_eval_state_builder() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let _state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + // State should be dropped automatically + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_simple_evaluation() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let result = state + .eval_from_string("1 + 2", "") + .expect("Failed to evaluate expression"); + + assert_eq!(result.value_type(), ValueType::Int); + assert_eq!(result.as_int().expect("Failed to get int value"), 3); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_value_types() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test integer + let int_val = state + .eval_from_string("42", "") + .expect("Failed to evaluate int"); + assert_eq!(int_val.value_type(), ValueType::Int); + assert_eq!(int_val.as_int().expect("Failed to get int"), 42); + + // Test boolean + let bool_val = state + .eval_from_string("true", "") + .expect("Failed to evaluate bool"); + assert_eq!(bool_val.value_type(), ValueType::Bool); + assert!(bool_val.as_bool().expect("Failed to get bool")); + + // Test string + let str_val = state + .eval_from_string("\"hello\"", "") + .expect("Failed to evaluate string"); + assert_eq!(str_val.value_type(), ValueType::String); + assert_eq!(str_val.as_string().expect("Failed to get string"), "hello"); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_value_construction() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let int_val = state.make_int(99).expect("Failed to make int"); + assert_eq!(int_val.as_int().unwrap(), 99); + + let float_val = state.make_float(2.5).expect("Failed to make float"); + assert!((float_val.as_float().unwrap() - 2.5).abs() < 1e-9); + + let bool_val = state.make_bool(true).expect("Failed to make bool"); + assert!(bool_val.as_bool().unwrap()); + + let null_val = state.make_null().expect("Failed to make null"); + assert_eq!(null_val.value_type(), ValueType::Null); + + let str_val = state.make_string("hello").expect("Failed to make string"); + assert_eq!(str_val.as_string().unwrap(), "hello"); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_make_list() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let a = state.make_int(1).unwrap(); + let b = state.make_int(2).unwrap(); + let c = state.make_int(3).unwrap(); + + let list = state.make_list(&[&a, &b, &c]).expect("Failed to make list"); + assert_eq!(list.value_type(), ValueType::List); + assert_eq!(list.list_len().unwrap(), 3); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_make_attrs() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let a = state.make_int(42).unwrap(); + let b = state.make_string("hello").unwrap(); + + let attrs = state + .make_attrs(&[("answer", &a), ("greeting", &b)]) + .expect("Failed to make attrs"); + assert_eq!(attrs.value_type(), ValueType::Attrs); + + let mut answer = attrs.get_attr("answer").unwrap(); + answer.force().unwrap(); + assert_eq!(answer.as_int().unwrap(), 42); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_value_call() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let f = state + .eval_from_string("x: x + 1", "") + .expect("Failed to evaluate function"); + let arg = state.make_int(41).unwrap(); + let result = f.call(&arg).expect("Failed to call function"); + assert_eq!(result.as_int().unwrap(), 42); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_value_copy() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let orig = state.make_int(7).unwrap(); + let copy = orig.copy().expect("Failed to copy value"); + assert_eq!(copy.as_int().unwrap(), 7); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_as_string_with_context_plain() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let val = state + .eval_from_string("\"hello\"", "") + .expect("Failed to evaluate string"); + let (s, ctx_paths) = val + .as_string_with_context() + .expect("as_string_with_context failed"); + assert_eq!(s, "hello"); + assert!( + ctx_paths.is_empty(), + "Plain string should have no context paths" + ); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_eval_from_file() { + use std::io::Write as _; + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let mut tmp = tempfile::NamedTempFile::new().expect("Failed to create temp file"); + write!(tmp, "1 + 1").expect("Failed to write temp file"); + let result = state + .eval_from_file(tmp.path()) + .expect("eval_from_file failed"); + assert_eq!(result.as_int().unwrap(), 2); + } + + #[cfg(feature = "expr")] + #[test] + #[serial] + fn test_no_load_config() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .no_load_config() + .build() + .expect("Failed to build state with no_load_config"); + let val = state + .eval_from_string("1 + 1", "") + .expect("Evaluation failed"); + assert_eq!(val.as_int().unwrap(), 2); + } +} diff --git a/nix-bindings/nix-bindings/src/lists.rs b/nix-bindings/nix-bindings/src/lists.rs new file mode 100644 index 0000000..34155ae --- /dev/null +++ b/nix-bindings/nix-bindings/src/lists.rs @@ -0,0 +1,308 @@ +use std::ptr::NonNull; + +use crate::{Error, Result, Value, ValueType, sys}; + +impl Value<'_> { + /// Check if this value is a list. + /// + /// # Example + /// + /// ```rust,no_run + /// use std::sync::Arc; + /// + /// use nix_bindings::{Context, EvalStateBuilder, Store}; + /// fn main() -> Result<(), Box> { + /// let ctx = Arc::new(Context::new()?); + /// let store = Arc::new(Store::open(&ctx, None)?); + /// let state = EvalStateBuilder::new(&store)?.build()?; + /// let list = state.eval_from_string("[1 2 3]", "")?; + /// assert!(list.is_list()); + /// Ok(()) + /// } + /// ``` + #[must_use] + pub fn is_list(&self) -> bool { + self.value_type() == ValueType::List + } + + /// Get the length of this list. + /// + /// # Errors + /// + /// Returns an error if this value is not a list. + /// + /// # Example + /// + /// ```no_run + /// # use std::sync::Arc; + /// # use nix_bindings::{Context, EvalStateBuilder, Store}; + /// # fn main() -> Result<(), Box> { + /// # let ctx = Arc::new(Context::new()?); + /// # let store = Arc::new(Store::open(&ctx, None)?); + /// # let state = EvalStateBuilder::new(&store)?.build()?; + /// let list = state.eval_from_string("[1 2 3]", "")?; + /// assert_eq!(list.list_len()?, 3); + /// # Ok(()) + /// # } + /// ``` + pub fn list_len(&self) -> Result { + if !self.is_list() { + return Err(Error::InvalidType { + expected: "list", + actual: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked. + // nix_get_list_size returns the length as c_uint with no error code. + let len = + unsafe { sys::nix_get_list_size(self.state.context.as_ptr(), self.inner.as_ptr()) }; + + Ok(len as usize) + } + + /// Get an element from this list by index. + /// + /// # Arguments + /// + /// * `idx` - The index of the element to retrieve (0-based) + /// + /// # Errors + /// + /// Returns an error if this value is not a list or the index is out of + /// bounds. + /// + /// # Example + /// + /// ```no_run + /// # use std::sync::Arc; + /// # use nix_bindings::{Context, EvalStateBuilder, Store}; + /// # fn main() -> Result<(), Box> { + /// # let ctx = Arc::new(Context::new()?); + /// # let store = Arc::new(Store::open(&ctx, None)?); + /// # let state = EvalStateBuilder::new(&store)?.build()?; + /// let list = state.eval_from_string("[1 2 3]", "")?; + /// let first = list.list_get(0)?; + /// assert_eq!(first.as_int()?, 1); + /// # Ok(()) + /// # } + /// ``` + pub fn list_get(&self, idx: usize) -> Result> { + if !self.is_list() { + return Err(Error::InvalidType { + expected: "list", + actual: self.value_type().to_string(), + }); + } + + let len = self.list_len()?; + if idx >= len { + return Err(Error::IndexOutOfBounds { + index: idx, + length: len, + }); + } + + // SAFETY: context, value, and state are valid; index is bounds-checked. + // nix_get_list_byidx returns a GC-owned pointer (refcount incremented for + // us). Value's Drop calls nix_value_decref to release our reference. + let elem_ptr = unsafe { + sys::nix_get_list_byidx( + self.state.context.as_ptr(), + self.inner.as_ptr(), + self.state.as_ptr(), + idx as std::os::raw::c_uint, + ) + }; + + let inner = NonNull::new(elem_ptr).ok_or(Error::NullPointer)?; + Ok(Value { + inner, + state: self.state, + }) + } + + /// Create an iterator over the elements of this list. + /// + /// # Errors + /// + /// Returns an error if this value is not a list. + pub fn list_iter(&self) -> Result> { + if !self.is_list() { + return Err(Error::InvalidType { + expected: "list", + actual: self.value_type().to_string(), + }); + } + + let len = self.list_len()?; + Ok(ListIterator { + value: self, + index: 0, + length: len, + }) + } +} + +/// Iterator over elements in a Nix list. +/// +/// This struct is created by [`Value::list_iter`] and is used to iterate +/// over the elements of a Nix list. +#[derive(Debug)] +pub struct ListIterator<'a> { + value: &'a Value<'a>, + index: usize, + length: usize, +} + +impl<'a> Iterator for ListIterator<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + if self.index >= self.length { + return None; + } + + match self.value.list_get(self.index) { + Ok(value) => { + self.index += 1; + Some(Ok(value)) + } + Err(e) => { + self.index += 1; + Some(Err(e)) + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.length - self.index; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for ListIterator<'_> {} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use serial_test::serial; + + use super::*; + use crate::{Context, EvalStateBuilder, Store}; + + #[test] + #[serial] + fn test_is_list() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let list = state + .eval_from_string("[1 2 3]", "") + .expect("Failed to evaluate list"); + assert!(list.is_list()); + + let int = state + .eval_from_string("1", "") + .expect("Failed to evaluate int"); + assert!(!int.is_list()); + } + + #[test] + #[serial] + fn test_list_len() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let empty = state + .eval_from_string("[]", "") + .expect("Failed to evaluate empty list"); + assert_eq!(empty.list_len().expect("Failed to get list length"), 0); + + let list = state + .eval_from_string("[1 2 3]", "") + .expect("Failed to evaluate list"); + assert_eq!(list.list_len().expect("Failed to get list length"), 3); + } + + #[test] + #[serial] + fn test_list_get() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let list = state + .eval_from_string("[10 20 30]", "") + .expect("Failed to evaluate list"); + + let first = list.list_get(0).expect("Failed to get first element"); + assert_eq!(first.as_int().expect("Failed to get int"), 10); + + let second = list.list_get(1).expect("Failed to get second element"); + assert_eq!(second.as_int().expect("Failed to get int"), 20); + + let third = list.list_get(2).expect("Failed to get third element"); + assert_eq!(third.as_int().expect("Failed to get int"), 30); + + // Out of bounds should return IndexOutOfBounds error + let result = list.list_get(5); + assert!(matches!( + result, + Err(Error::IndexOutOfBounds { + index: 5, + length: 3, + }) + )); + } + + #[test] + #[serial] + fn test_list_iter() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let list = state + .eval_from_string("[1 2 3]", "") + .expect("Failed to evaluate list"); + + let mut iter = list.list_iter().expect("Failed to create iterator"); + assert_eq!(iter.len(), 3); + + let first = iter + .next() + .expect("Failed to get first") + .expect("Failed to get first value"); + assert_eq!(first.as_int().expect("Failed to get int"), 1); + + let second = iter + .next() + .expect("Failed to get second") + .expect("Failed to get second value"); + assert_eq!(second.as_int().expect("Failed to get int"), 2); + + let third = iter + .next() + .expect("Failed to get third") + .expect("Failed to get third value"); + assert_eq!(third.as_int().expect("Failed to get int"), 3); + + assert!(iter.next().is_none()); + } +} diff --git a/nix-bindings/nix-bindings/src/primop.rs b/nix-bindings/nix-bindings/src/primop.rs new file mode 100644 index 0000000..7d81a0a --- /dev/null +++ b/nix-bindings/nix-bindings/src/primop.rs @@ -0,0 +1,1444 @@ +//! Nix primitive operations (primops). +//! +//! This module provides a safe, closure-based API for registering custom Nix +//! primitive operations (primops). +//! +//! # Overview +//! +//! Primops are Rust functions that appear as Nix builtins. There are two ways +//! to expose a primop: +//! +//! * **Global builtin**: call [`PrimOp::register`] *before* creating any +//! [`EvalState`](crate::EvalState). All subsequently created states will +//! include the primop in `builtins`. +//! * **Value-embedded**: call [`PrimOp::into_value`] on an existing +//! [`EvalState`](crate::EvalState) to obtain a callable +//! [`Value`](crate::Value). +//! +//! # Example +//! +//! ```no_run +//! use std::sync::Arc; +//! +//! use nix_bindings::{Context, EvalStateBuilder, Store, primop::PrimOp}; +//! +//! # fn main() -> Result<(), Box> { +//! let ctx = Arc::new(Context::new()?); +//! +//! // Register a global builtin that doubles an integer +//! PrimOp::new(&ctx, "double", 1, Some("Double an integer"), |args, ret| { +//! let n = args[0].as_int()?; +//! ret.set_int(n * 2) +//! })? +//! .register(&ctx)?; +//! +//! let store = Arc::new(Store::open(&ctx, None)?); +//! let state = EvalStateBuilder::new(&store)?.build()?; +//! let result = state.eval_from_string("builtins.double 21", "")?; +//! assert_eq!(result.as_int()?, 42); +//! # Ok(()) +//! # } +//! ``` + +use std::{ + ffi::CString, + marker::PhantomData, + os::raw::c_void, + panic::{self, AssertUnwindSafe}, + sync::Arc, +}; + +use crate::{Context, Error, Result, ValueType, check_err, sys}; + +type PrimOpFn = dyn Fn(&[PrimOpArg<'_>], &mut PrimOpRet<'_>) -> Result<()> + Send + Sync; + +/// The boxed Rust closure called by the trampoline. +/// +/// Arity is stored alongside the closure so the trampoline can slice the args +/// array correctly. +struct ClosureData { + arity: usize, + f: Box, +} + +/// C-compatible trampoline that dispatches to the boxed Rust closure. +/// +/// # Safety +/// +/// `user_data` must be a valid `*mut ClosureData` allocated via +/// `Box::into_raw`. `args` must be a valid array of at least `arity` +/// non-null `*mut nix_value` pointers. `ret` must be a valid, writable +/// `*mut nix_value`. +unsafe extern "C" fn trampoline( + user_data: *mut c_void, + context: *mut sys::nix_c_context, + state: *mut sys::EvalState, + args: *mut *mut sys::nix_value, + ret: *mut sys::nix_value, +) { + let data = unsafe { &*(user_data as *const ClosureData) }; + + // Build arg wrappers. + // + // The pointers are borrowed from Nix and must NOT be + // decreffed by our code. When arity is 0, `args` may be null. Passing a null + // pointer to slice::from_raw_parts (even with len=0) produces a dangling + // reference which violates Rust's validity invariant for references. + let arg_wrappers: Vec> = if data.arity == 0 { + Vec::new() + } else { + let arg_slice = unsafe { std::slice::from_raw_parts(args, data.arity) }; + arg_slice + .iter() + .map(|&p| PrimOpArg { + inner: p, + ctx: context, + state, + _phantom: PhantomData, + }) + .collect() + }; + + let mut ret_wrapper = PrimOpRet { + inner: ret, + ctx: context, + state, + _phantom: PhantomData, + }; + + let result = panic::catch_unwind(AssertUnwindSafe(|| { + (data.f)(&arg_wrappers, &mut ret_wrapper) + })); + + let err_msg = match result { + Ok(Ok(())) => return, + Ok(Err(e)) => format!("primop error: {e}"), + Err(_) => "primop panicked".to_string(), + }; + + let msg_c = CString::new(err_msg).unwrap_or_else(|_| CString::new("primop error").unwrap()); + unsafe { + sys::nix_set_err_msg(context, sys::nix_err_NIX_ERR_NIX_ERROR, msg_c.as_ptr()); + } +} + +/// GC finalizer that frees the `ClosureData` box when the GC collects the +/// associated `PrimOp` object. +/// +/// `cd` is the `*mut ClosureData` stored at primop-allocation time. +unsafe extern "C" fn drop_closure_finalizer(_obj: *mut c_void, cd: *mut c_void) { + // Reconstruct and immediately drop the Box, running the destructor. + let _ = unsafe { Box::from_raw(cd as *mut ClosureData) }; +} + +/// A borrowed Nix value passed as an argument to a primop callback. +/// +/// The value is owned by the Nix evaluator and must **not** be decreffed by +/// the caller. It is valid only for the duration of the primop invocation +/// (expressed by the `'a` lifetime). +pub struct PrimOpArg<'a> { + inner: *mut sys::nix_value, + ctx: *mut sys::nix_c_context, + state: *mut sys::EvalState, + _phantom: PhantomData<&'a ()>, +} + +// PrimOpArg is only used within the synchronous callback; no cross-thread use. +unsafe impl Send for PrimOpArg<'_> {} +unsafe impl Sync for PrimOpArg<'_> {} + +impl PrimOpArg<'_> { + /// Return the [`ValueType`] of this argument. + #[must_use] + pub fn value_type(&self) -> ValueType { + let c_type = unsafe { sys::nix_get_type(self.ctx, self.inner) }; + ValueType::from_c(c_type) + } + + /// Force evaluation of this argument (resolves thunks). + /// + /// # Errors + /// + /// Returns an error if evaluation fails. + pub fn force(&self) -> Result<()> { + unsafe { + check_err( + self.ctx, + sys::nix_value_force(self.ctx, self.state, self.inner), + ) + } + } + + /// Extract this argument as an integer. + /// + /// Automatically forces the value if it is a thunk. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not an + /// integer. + pub fn as_int(&self) -> Result { + self.force()?; + if self.value_type() != ValueType::Int { + return Err(Error::InvalidType { + expected: "int", + actual: self.value_type().to_string(), + }); + } + Ok(unsafe { sys::nix_get_int(self.ctx, self.inner) }) + } + + /// Extract this argument as a float. + /// + /// Automatically forces the value if it is a thunk. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not a + /// float. + pub fn as_float(&self) -> Result { + self.force()?; + if self.value_type() != ValueType::Float { + return Err(Error::InvalidType { + expected: "float", + actual: self.value_type().to_string(), + }); + } + Ok(unsafe { sys::nix_get_float(self.ctx, self.inner) }) + } + + /// Extract this argument as a boolean. + /// + /// Automatically forces the value if it is a thunk. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not a + /// boolean. + pub fn as_bool(&self) -> Result { + self.force()?; + if self.value_type() != ValueType::Bool { + return Err(Error::InvalidType { + expected: "bool", + actual: self.value_type().to_string(), + }); + } + Ok(unsafe { sys::nix_get_bool(self.ctx, self.inner) }) + } + + /// Extract this argument as a UTF-8 string. + /// + /// Automatically forces the value if it is a thunk. + /// + /// # Errors + /// + /// Returns an error if forcing fails, the resolved value is not a + /// string, or the string contains invalid UTF-8. + pub fn as_string(&self) -> Result { + self.force()?; + if self.value_type() != ValueType::String { + return Err(Error::InvalidType { + expected: "string", + actual: self.value_type().to_string(), + }); + } + + let realised_str = + unsafe { sys::nix_string_realise(self.ctx, self.state, self.inner, false) }; + + if realised_str.is_null() { + return Err(Error::NullPointer); + } + + let buffer_start = unsafe { sys::nix_realised_string_get_buffer_start(realised_str) }; + let buffer_size = unsafe { sys::nix_realised_string_get_buffer_size(realised_str) }; + + if buffer_start.is_null() { + unsafe { sys::nix_realised_string_free(realised_str) }; + return Err(Error::NullPointer); + } + + let bytes = unsafe { std::slice::from_raw_parts(buffer_start.cast::(), buffer_size) }; + let s = std::str::from_utf8(bytes) + .map_err(|_| Error::Unknown("Invalid UTF-8 in string".into()))? + .to_owned(); + + unsafe { sys::nix_realised_string_free(realised_str) }; + Ok(s) + } + + /// Interpret this argument as an attribute set. + /// + /// Automatically forces the value if it is a thunk. + /// + /// Returns an [`ArgAttrs`] wrapper that provides read access to the + /// attribute set's keys and values. The returned wrapper borrows this + /// argument; it does not own any GC references. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not an + /// attribute set. + pub fn as_attrs(&self) -> Result> { + self.force()?; + if self.value_type() != ValueType::Attrs { + return Err(Error::InvalidType { + expected: "attrs", + actual: self.value_type().to_string(), + }); + } + Ok(ArgAttrs { + inner: self.inner, + ctx: self.ctx, + state: self.state, + _phantom: PhantomData, + }) + } + + /// Interpret this argument as a list. + /// + /// Automatically forces the value if it is a thunk. + /// + /// Returns an [`ArgList`] wrapper that provides read access to the list + /// elements. The returned wrapper borrows this argument; it does not own + /// any GC references. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not a + /// list. + pub fn as_list(&self) -> Result> { + self.force()?; + if self.value_type() != ValueType::List { + return Err(Error::InvalidType { + expected: "list", + actual: self.value_type().to_string(), + }); + } + Ok(ArgList { + inner: self.inner, + ctx: self.ctx, + state: self.state, + _phantom: PhantomData, + }) + } +} + +/// The writable return-value slot provided to a primop closure. +/// +/// Exactly one `set_*` method must be called before returning `Ok(())`. +pub struct PrimOpRet<'a> { + inner: *mut sys::nix_value, + ctx: *mut sys::nix_c_context, + state: *mut sys::EvalState, + _phantom: PhantomData<&'a mut ()>, +} + +impl PrimOpRet<'_> { + /// Write an integer result. + /// + /// # Errors + /// + /// Returns an error if the write fails. + pub fn set_int(&mut self, i: i64) -> Result<()> { + unsafe { check_err(self.ctx, sys::nix_init_int(self.ctx, self.inner, i)) } + } + + /// Write a float result. + /// + /// # Errors + /// + /// Returns an error if the write fails. + pub fn set_float(&mut self, f: f64) -> Result<()> { + unsafe { check_err(self.ctx, sys::nix_init_float(self.ctx, self.inner, f)) } + } + + /// Write a boolean result. + /// + /// # Errors + /// + /// Returns an error if the write fails. + pub fn set_bool(&mut self, b: bool) -> Result<()> { + unsafe { check_err(self.ctx, sys::nix_init_bool(self.ctx, self.inner, b)) } + } + + /// Write a null result. + /// + /// # Errors + /// + /// Returns an error if the write fails. + pub fn set_null(&mut self) -> Result<()> { + unsafe { check_err(self.ctx, sys::nix_init_null(self.ctx, self.inner)) } + } + + /// Write a string result. + /// + /// # Errors + /// + /// Returns an error if `s` contains an interior NUL byte or the write + /// fails. + pub fn set_string(&mut self, s: &str) -> Result<()> { + let s_c = CString::new(s)?; + unsafe { + check_err( + self.ctx, + sys::nix_init_string(self.ctx, self.inner, s_c.as_ptr()), + ) + } + } + + /// Copy the value pointed to by `src` into the return slot. + /// + /// This is useful when the result is an existing [`Value`](crate::Value) + /// that should be forwarded as-is. + /// + /// # Safety + /// + /// `src` must be a valid, non-null `*mut nix_value` that remains live for + /// the duration of the call. + /// + /// # Errors + /// + /// Returns an error if the copy fails. + pub unsafe fn copy_from_raw(&mut self, src: *mut sys::nix_value) -> Result<()> { + unsafe { check_err(self.ctx, sys::nix_copy_value(self.ctx, self.inner, src)) } + } + + /// Write an attribute set result. + /// + /// Builds an attribute set from the given key-value pairs and writes it + /// into the return slot. Each value is a [`PrimOpValue`] obtained from a + /// primop argument or created via the `make_*` methods on this struct. + /// + /// # Errors + /// + /// Returns an error if construction fails. + pub fn set_attrs(&mut self, pairs: &[(&str, &PrimOpValue)]) -> Result<()> { + let builder = + unsafe { sys::nix_make_bindings_builder(self.ctx, self.state, pairs.len().max(1)) }; + if builder.is_null() { + return Err(Error::NullPointer); + } + + struct BuilderGuard(*mut sys::BindingsBuilder); + impl Drop for BuilderGuard { + fn drop(&mut self) { + unsafe { sys::nix_bindings_builder_free(self.0) }; + } + } + let _guard = BuilderGuard(builder); + + for (key, value) in pairs { + let key_c = CString::new(*key)?; + // SAFETY: builder, key, and value are valid + unsafe { + check_err( + self.ctx, + sys::nix_bindings_builder_insert( + self.ctx, + builder, + key_c.as_ptr(), + value.inner, + ), + )?; + } + } + + // SAFETY: builder is valid, result slot is valid + unsafe { check_err(self.ctx, sys::nix_make_attrs(self.ctx, self.inner, builder)) } + } + + /// Write a list result. + /// + /// Builds a list from the given values and writes it into the return + /// slot. Each value is a [`PrimOpValue`] obtained from a primop + /// argument or created via the `make_*` methods on this struct. + /// + /// # Errors + /// + /// Returns an error if construction fails. + pub fn set_list(&mut self, items: &[&PrimOpValue]) -> Result<()> { + let builder = + unsafe { sys::nix_make_list_builder(self.ctx, self.state, items.len().max(1)) }; + if builder.is_null() { + return Err(Error::NullPointer); + } + + struct ListGuard(*mut sys::ListBuilder); + impl Drop for ListGuard { + fn drop(&mut self) { + unsafe { sys::nix_list_builder_free(self.0) }; + } + } + let _guard = ListGuard(builder); + + for (i, value) in items.iter().enumerate() { + unsafe { + check_err( + self.ctx, + sys::nix_list_builder_insert( + self.ctx, + builder, + i as std::os::raw::c_uint, + value.inner, + ), + )?; + } + } + + unsafe { check_err(self.ctx, sys::nix_make_list(self.ctx, builder, self.inner)) } + } + + /// Allocate and initialise an integer [`PrimOpValue`]. + /// + /// # Errors + /// + /// Returns an error if allocation or initialisation fails. + pub fn make_int(&self, i: i64) -> Result { + let v = PrimOpValue::alloc(self.ctx, self.state)?; + unsafe { + check_err(self.ctx, sys::nix_init_int(self.ctx, v.inner, i))?; + } + Ok(v) + } + + /// Allocate and initialise a float [`PrimOpValue`]. + /// + /// # Errors + /// + /// Returns an error if allocation or initialisation fails. + pub fn make_float(&self, f: f64) -> Result { + let v = PrimOpValue::alloc(self.ctx, self.state)?; + unsafe { + check_err(self.ctx, sys::nix_init_float(self.ctx, v.inner, f))?; + } + Ok(v) + } + + /// Allocate and initialise a boolean [`PrimOpValue`]. + /// + /// # Errors + /// + /// Returns an error if allocation or initialisation fails. + pub fn make_bool(&self, b: bool) -> Result { + let v = PrimOpValue::alloc(self.ctx, self.state)?; + unsafe { + check_err(self.ctx, sys::nix_init_bool(self.ctx, v.inner, b))?; + } + Ok(v) + } + + /// Allocate and initialise a null [`PrimOpValue`]. + /// + /// # Errors + /// + /// Returns an error if allocation or initialisation fails. + pub fn make_null(&self) -> Result { + let v = PrimOpValue::alloc(self.ctx, self.state)?; + unsafe { + check_err(self.ctx, sys::nix_init_null(self.ctx, v.inner))?; + } + Ok(v) + } + + /// Allocate and initialise a string [`PrimOpValue`]. + /// + /// # Errors + /// + /// Returns an error if allocation, string conversion, or initialisation + /// fails. + pub fn make_string(&self, s: &str) -> Result { + let v = PrimOpValue::alloc(self.ctx, self.state)?; + let s_c = CString::new(s)?; + unsafe { + check_err( + self.ctx, + sys::nix_init_string(self.ctx, v.inner, s_c.as_ptr()), + )?; + } + Ok(v) + } +} + +/// An owned Nix value used within a primop callback. +/// +/// Unlike [`PrimOpArg`], this owns a GC reference to the underlying value +/// and decrements it on drop. It is returned by [`ArgAttrs::get`] and +/// [`ArgList::get`] when extracting child values from collections. +/// +/// Methods mirror those of [`PrimOpArg`]: `value_type`, `force`, `as_int`, +/// `as_float`, `as_bool`, `as_string`, `as_attrs`, and `as_list` are all +/// available. +pub struct PrimOpValue { + inner: *mut sys::nix_value, + ctx: *mut sys::nix_c_context, + state: *mut sys::EvalState, +} + +unsafe impl Send for PrimOpValue {} +unsafe impl Sync for PrimOpValue {} + +impl PrimOpValue { + fn alloc(ctx: *mut sys::nix_c_context, state: *mut sys::EvalState) -> Result { + let inner = unsafe { sys::nix_alloc_value(ctx, state) }; + if inner.is_null() { + return Err(Error::NullPointer); + } + Ok(PrimOpValue { inner, ctx, state }) + } + + /// Return the [`ValueType`] of this value. + #[must_use] + pub fn value_type(&self) -> ValueType { + let c_type = unsafe { sys::nix_get_type(self.ctx, self.inner) }; + ValueType::from_c(c_type) + } + + /// Force evaluation of this value (resolves thunks). + /// + /// # Errors + /// + /// Returns an error if evaluation fails. + pub fn force(&self) -> Result<()> { + unsafe { + check_err( + self.ctx, + sys::nix_value_force(self.ctx, self.state, self.inner), + ) + } + } + + /// Extract this value as an integer. + /// + /// Automatically forces the value if it is a thunk. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not an + /// integer. + pub fn as_int(&self) -> Result { + self.force()?; + if self.value_type() != ValueType::Int { + return Err(Error::InvalidType { + expected: "int", + actual: self.value_type().to_string(), + }); + } + Ok(unsafe { sys::nix_get_int(self.ctx, self.inner) }) + } + + /// Extract this value as a float. + /// + /// Automatically forces the value if it is a thunk. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not a + /// float. + pub fn as_float(&self) -> Result { + self.force()?; + if self.value_type() != ValueType::Float { + return Err(Error::InvalidType { + expected: "float", + actual: self.value_type().to_string(), + }); + } + Ok(unsafe { sys::nix_get_float(self.ctx, self.inner) }) + } + + /// Extract this value as a boolean. + /// + /// Automatically forces the value if it is a thunk. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not a + /// boolean. + pub fn as_bool(&self) -> Result { + self.force()?; + if self.value_type() != ValueType::Bool { + return Err(Error::InvalidType { + expected: "bool", + actual: self.value_type().to_string(), + }); + } + Ok(unsafe { sys::nix_get_bool(self.ctx, self.inner) }) + } + + /// Extract this value as a UTF-8 string. + /// + /// Automatically forces the value if it is a thunk. + /// + /// # Errors + /// + /// Returns an error if forcing fails, the resolved value is not a + /// string, or the string contains invalid UTF-8. + pub fn as_string(&self) -> Result { + self.force()?; + if self.value_type() != ValueType::String { + return Err(Error::InvalidType { + expected: "string", + actual: self.value_type().to_string(), + }); + } + + let realised_str = + unsafe { sys::nix_string_realise(self.ctx, self.state, self.inner, false) }; + + if realised_str.is_null() { + return Err(Error::NullPointer); + } + + let buffer_start = unsafe { sys::nix_realised_string_get_buffer_start(realised_str) }; + let buffer_size = unsafe { sys::nix_realised_string_get_buffer_size(realised_str) }; + + if buffer_start.is_null() { + unsafe { sys::nix_realised_string_free(realised_str) }; + return Err(Error::NullPointer); + } + + let bytes = unsafe { std::slice::from_raw_parts(buffer_start.cast::(), buffer_size) }; + let s = std::str::from_utf8(bytes) + .map_err(|_| Error::Unknown("Invalid UTF-8 in string".into()))? + .to_owned(); + + unsafe { sys::nix_realised_string_free(realised_str) }; + Ok(s) + } + + /// Interpret this value as an attribute set. + /// + /// Automatically forces the value if it is a thunk. + /// + /// Returns an [`ArgAttrs`] wrapper that borrows from this value. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not an + /// attribute set. + #[must_use] + pub fn as_attrs(&self) -> Result> { + self.force()?; + if self.value_type() != ValueType::Attrs { + return Err(Error::InvalidType { + expected: "attrs", + actual: self.value_type().to_string(), + }); + } + Ok(ArgAttrs { + inner: self.inner, + ctx: self.ctx, + state: self.state, + _phantom: PhantomData, + }) + } + + /// Interpret this value as a list. + /// + /// Automatically forces the value if it is a thunk. + /// + /// Returns an [`ArgList`] wrapper that borrows from this value. + /// + /// # Errors + /// + /// Returns an error if forcing fails or the resolved value is not a + /// list. + #[must_use] + pub fn as_list(&self) -> Result> { + self.force()?; + if self.value_type() != ValueType::List { + return Err(Error::InvalidType { + expected: "list", + actual: self.value_type().to_string(), + }); + } + Ok(ArgList { + inner: self.inner, + ctx: self.ctx, + state: self.state, + _phantom: PhantomData, + }) + } +} + +impl Drop for PrimOpValue { + fn drop(&mut self) { + // SAFETY: ctx and inner are valid; we own a GC reference + unsafe { + sys::nix_value_decref(self.ctx, self.inner); + } + } +} +/// A borrowed Nix attribute set value. +/// +/// Obtained from [`PrimOpArg::as_attrs`] or [`PrimOpValue::as_attrs`]. +/// Provides read access to the attribute set without owning any GC +/// references itself. +pub struct ArgAttrs<'a> { + inner: *mut sys::nix_value, + ctx: *mut sys::nix_c_context, + state: *mut sys::EvalState, + _phantom: PhantomData<&'a ()>, +} + +unsafe impl Send for ArgAttrs<'_> {} +unsafe impl Sync for ArgAttrs<'_> {} + +impl ArgAttrs<'_> { + /// Get an attribute by name. + /// + /// Returns an owned [`PrimOpValue`] that holds a GC reference to the + /// attribute's value. The caller is responsible for the GC reference; + /// [`PrimOpValue`] releases it on drop. + /// + /// # Errors + /// + /// Returns [`Error::KeyNotFound`] if the key does not exist. + pub fn get(&self, key: &str) -> Result { + let key_c = CString::new(key)?; + // SAFETY: ctx, value, and state are valid + let ptr = + unsafe { sys::nix_get_attr_byname(self.ctx, self.inner, self.state, key_c.as_ptr()) }; + if ptr.is_null() { + return Err(Error::KeyNotFound(key.to_string())); + } + Ok(PrimOpValue { + inner: ptr, + ctx: self.ctx, + state: self.state, + }) + } + + /// Check if an attribute exists. + /// + /// # Errors + /// + /// Returns an error if the lookup itself fails (not if the key is + /// absent). + pub fn has(&self, key: &str) -> Result { + let key_c = CString::new(key)?; + // SAFETY: ctx, value, and state are valid + let result = + unsafe { sys::nix_has_attr_byname(self.ctx, self.inner, self.state, key_c.as_ptr()) }; + Ok(result) + } + + /// Return all attribute keys in this set. + /// + /// # Errors + /// + /// Returns an error if iteration fails. + pub fn keys(&self) -> Result> { + // SAFETY: ctx and value are valid + let count = unsafe { sys::nix_get_attrs_size(self.ctx, self.inner) }; + + let mut keys = Vec::with_capacity(count as usize); + for i in 0..count { + let mut name_ptr: *const std::os::raw::c_char = std::ptr::null(); + let val_ptr = unsafe { + sys::nix_get_attr_byidx(self.ctx, self.inner, self.state, i, &mut name_ptr) + }; + // We only want the name; release the value reference. + if !val_ptr.is_null() { + unsafe { sys::nix_value_decref(self.ctx, val_ptr) }; + } + if name_ptr.is_null() { + continue; + } + let name = unsafe { + std::ffi::CStr::from_ptr(name_ptr) + .to_string_lossy() + .into_owned() + }; + keys.push(name); + } + Ok(keys) + } + + /// Return the number of attributes in this set. + #[must_use] + pub fn len(&self) -> usize { + unsafe { sys::nix_get_attrs_size(self.ctx, self.inner) as usize } + } + + /// Return `true` if the attribute set is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +/// A borrowed Nix list value. +/// +/// Obtained from [`PrimOpArg::as_list`] or [`PrimOpValue::as_list`]. +/// Provides read access to the list without owning any GC references +/// itself. +pub struct ArgList<'a> { + inner: *mut sys::nix_value, + ctx: *mut sys::nix_c_context, + state: *mut sys::EvalState, + _phantom: PhantomData<&'a ()>, +} + +unsafe impl Send for ArgList<'_> {} +unsafe impl Sync for ArgList<'_> {} + +impl ArgList<'_> { + /// Get an element by index. + /// + /// Returns an owned [`PrimOpValue`] that holds a GC reference to the + /// element. The caller is responsible for the GC reference; + /// [`PrimOpValue`] releases it on drop. + /// + /// # Errors + /// + /// Returns [`Error::IndexOutOfBounds`] if `index >= self.len()`. + pub fn get(&self, index: usize) -> Result { + let length = self.len(); + if index >= length { + return Err(Error::IndexOutOfBounds { index, length }); + } + // SAFETY: ctx, value, and state are valid; index is bounds-checked + let ptr = unsafe { + sys::nix_get_list_byidx( + self.ctx, + self.inner, + self.state, + index as std::os::raw::c_uint, + ) + }; + if ptr.is_null() { + return Err(Error::NullPointer); + } + Ok(PrimOpValue { + inner: ptr, + ctx: self.ctx, + state: self.state, + }) + } + + /// Return the number of elements in this list. + #[must_use] + pub fn len(&self) -> usize { + unsafe { sys::nix_get_list_size(self.ctx, self.inner) as usize } + } + + /// Return `true` if the list is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +/// A Nix primitive operation (primop) wrapping a Rust closure. +/// +/// After construction with [`PrimOp::new`], the primop can be: +/// +/// * Registered as a **global builtin** via [`PrimOp::register`] (must happen +/// before creating any [`EvalState`](crate::EvalState)). +/// * Embedded in a **value** via [`PrimOp::into_value`] for use as a +/// first-class function inside an [`EvalState`](crate::EvalState). +pub struct PrimOp { + /// GC-owned pointer; we hold one reference until consumed. + inner: *mut sys::PrimOp, + /// Keeps the context alive for the lifetime of the PrimOp. + context: Arc, + /// Set to `true` after `register()` so Drop skips the decref. + registered: bool, +} + +// SAFETY: PrimOp contains raw pointers but is only ever manipulated on a +// single thread. The underlying PrimOp object is GC-managed. +unsafe impl Send for PrimOp {} +unsafe impl Sync for PrimOp {} + +impl PrimOp { + /// Create a new primop backed by the given Rust closure. + /// + /// # Arguments + /// + /// * `context`: the Nix context. + /// * `name`: the name of the primop as it will appear in Nix. + /// * `arity`: number of arguments the primop accepts. + /// * `doc`: optional documentation string. + /// * `f`: the Rust closure to invoke. + /// + /// # Errors + /// + /// Returns an error if the name or doc string contains an interior NUL + /// byte, or if the underlying allocation fails. + pub fn new( + context: &Arc, + name: &str, + arity: u32, + doc: Option<&str>, + f: F, + ) -> Result + where + F: Fn(&[PrimOpArg<'_>], &mut PrimOpRet<'_>) -> Result<()> + Send + Sync + 'static, + { + let name_c = CString::new(name)?; + let doc_c = doc.map(CString::new).transpose()?; + // PrimOp::doc is std::optional since Nix 2.34. + // Passing null would throw "construction from null". + let empty_doc; + let doc_ptr = match doc_c { + Some(ref c) => c.as_ptr(), + None => { + empty_doc = CString::default(); + empty_doc.as_ptr() + } + }; + + // Box the closure together with its arity for the trampoline. + let data = Box::new(ClosureData { + arity: arity as usize, + f: Box::new(f), + }); + let data_raw = Box::into_raw(data) as *mut c_void; + + // Allocate the GC-managed PrimOp. + // SAFETY: context is valid; trampoline has the expected C signature. + let primop_ptr = unsafe { + sys::nix_alloc_primop( + context.as_ptr(), + Some(trampoline), + arity as std::os::raw::c_int, + name_c.as_ptr(), + std::ptr::null_mut(), // arg names (optional) + doc_ptr, + data_raw, + ) + }; + + if primop_ptr.is_null() { + let _ = unsafe { Box::from_raw(data_raw as *mut ClosureData) }; + // SAFETY: context pointer is valid + unsafe { + check_err(context.as_ptr(), sys::nix_err_code(context.as_ptr()))?; + } + return Err(Error::NullPointer); + } + + // Register a GC finalizer so the closure is freed when the PrimOp GC + // object is collected. + // SAFETY: primop_ptr is a valid GC object; data_raw is a valid pointer. + unsafe { + sys::nix_gc_register_finalizer( + primop_ptr as *mut c_void, + data_raw, + Some(drop_closure_finalizer), + ); + } + + Ok(PrimOp { + inner: primop_ptr, + context: Arc::clone(context), + registered: false, + }) + } + + /// Register this primop as a global Nix builtin. + /// + /// After this call the primop will appear in `builtins.*` for all + /// [`EvalState`](crate::EvalState) instances created **after** this call. + /// + /// This consumes `self`; the underlying pointer is transferred to the + /// global registry and is no longer accessible. + /// + /// # Errors + /// + /// Returns an error if the registration fails. + pub fn register(mut self, context: &Context) -> Result<()> { + // SAFETY: context and inner are valid + let err = unsafe { sys::nix_register_primop(context.as_ptr(), self.inner) }; + check_err(unsafe { self.context.as_ptr() }, err)?; + // Mark as registered only after confirmed success so Drop still calls + // nix_gc_decref if registration fails. + self.registered = true; + Ok(()) + } + + /// Embed this primop in a Nix value, returning a callable + /// [`Value`](crate::Value). + /// + /// This consumes `self`. The returned value holds a GC reference to the + /// primop, keeping it alive for the lifetime of the value. + /// + /// # Errors + /// + /// Returns an error if the value allocation or initialisation fails. + pub fn into_value(self, state: &crate::EvalState) -> Result> { + let v = state.alloc_value()?; + // SAFETY: context, value, and primop pointer are valid + unsafe { + check_err( + state.context.as_ptr(), + sys::nix_init_primop(state.context.as_ptr(), v.inner.as_ptr(), self.inner), + )?; + } + // `self` drops here; the value holds the GC ref via nix_init_primop so + // our own reference can be released. + Ok(v) + } +} + +impl Drop for PrimOp { + fn drop(&mut self) { + if !self.registered && !self.inner.is_null() { + // Release our GC reference. The GC may still keep the object alive + // until it is collected, at which point the finalizer frees the + // closure. + // SAFETY: ctx and inner are valid + unsafe { + let _ = sys::nix_gc_decref(self.context.as_ptr(), self.inner as *const c_void); + } + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use serial_test::serial; + + use super::*; + use crate::{Context, EvalStateBuilder, Store}; + + #[test] + #[serial] + fn test_primop_into_value() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // A primop that negates an integer + let primop = PrimOp::new(&ctx, "negate", 1, Some("Negate an integer"), |args, ret| { + let n = args[0].as_int()?; + ret.set_int(-n) + }) + .expect("Failed to create primop"); + + let func = primop + .into_value(&state) + .expect("Failed to embed primop as value"); + + let arg = state.make_int(7).unwrap(); + let result = func.call(&arg).expect("Failed to call primop"); + assert_eq!(result.as_int().unwrap(), -7); + } + + #[test] + #[serial] + fn test_primop_into_value_string() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // A primop that returns a constant string + let primop = PrimOp::new(&ctx, "hello", 1, None, |_args, ret| { + ret.set_string("hello from primop") + }) + .expect("Failed to create primop"); + + let func = primop + .into_value(&state) + .expect("Failed to embed primop as value"); + + let arg = state.make_null().unwrap(); + let result = func.call(&arg).expect("Failed to call primop"); + assert_eq!(result.as_string().unwrap(), "hello from primop"); + } + + #[test] + #[serial] + fn test_primop_arg_as_list() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let list_val = state + .eval_from_string("[10 20 30]", "") + .expect("Failed to evaluate list"); + + // A primop that reads list args and returns the sum + let primop = PrimOp::new(&ctx, "list_sum", 1, None, |args, ret| { + let list = args[0].as_list()?; + let mut sum = 0i64; + for i in 0..list.len() { + sum += list.get(i)?.as_int()?; + } + ret.set_int(sum) + }) + .expect("Failed to create primop"); + + let func = primop + .into_value(&state) + .expect("Failed to embed primop as value"); + + let result = func.call(&list_val).expect("Failed to call primop"); + assert_eq!(result.as_int().unwrap(), 60); + } + + #[test] + #[serial] + fn test_primop_arg_as_attrs() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let attrs_val = state + .eval_from_string("{ foo = 42; bar = 13; }", "") + .expect("Failed to evaluate attrs"); + + // A primop that sums two named attributes + let primop = PrimOp::new(&ctx, "attr_sum", 1, None, |args, ret| { + let attrs = args[0].as_attrs()?; + let foo = attrs.get("foo")?.as_int()?; + let bar = attrs.get("bar")?.as_int()?; + ret.set_int(foo + bar) + }) + .expect("Failed to create primop"); + + let func = primop + .into_value(&state) + .expect("Failed to embed primop as value"); + + let result = func.call(&attrs_val).expect("Failed to call primop"); + assert_eq!(result.as_int().unwrap(), 55); + } + + #[test] + #[serial] + fn test_primop_arg_attrs_has_and_keys() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let attrs_val = state + .eval_from_string("{ a = 1; b = 2; c = 3; }", "") + .expect("Failed to evaluate attrs"); + + let primop = PrimOp::new(&ctx, "check_attrs", 1, None, |args, ret| { + let attrs = args[0].as_attrs()?; + assert_eq!(attrs.len(), 3); + assert!(!attrs.is_empty()); + assert!(attrs.has("a")?); + assert!(attrs.has("b")?); + assert!(attrs.has("c")?); + assert!(!attrs.has("zzz")?); + let keys = attrs.keys()?; + assert_eq!(keys.len(), 3); + ret.set_null() + }) + .expect("Failed to create primop"); + + let func = primop + .into_value(&state) + .expect("Failed to embed primop as value"); + + func.call(&attrs_val).expect("Failed to call primop"); + } + + #[test] + #[serial] + fn test_primop_empty_attrs_and_list() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let empty_attrs = state + .eval_from_string("{}", "") + .expect("Failed to evaluate empty attrs"); + + let primop = PrimOp::new(&ctx, "empty_check", 1, None, |args, ret| { + match args[0].as_attrs() { + Ok(a) => { + assert!(a.is_empty()); + assert_eq!(a.len(), 0); + } + Err(_) => { + let list = args[0].as_list()?; + assert!(list.is_empty()); + assert_eq!(list.len(), 0); + } + } + ret.set_null() + }) + .expect("Failed to create primop"); + + let func = primop + .into_value(&state) + .expect("Failed to embed primop as value"); + + func.call(&empty_attrs) + .expect("call with empty attrs failed"); + + let empty_list = state + .eval_from_string("[]", "") + .expect("Failed to evaluate empty list"); + func.call(&empty_list).expect("call with empty list failed"); + } + + #[test] + #[serial] + fn test_primop_ret_set_attrs() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + + // Register a nullary primop that constructs and returns an attrset. + PrimOp::new(&ctx, "mk_attrs_test", 0, None, |_args, ret| { + let a = ret.make_int(100)?; + let b = ret.make_string("hi")?; + ret.set_attrs(&[("x", &a), ("y", &b)]) + }) + .expect("Failed to create primop") + .register(&ctx) + .expect("Failed to register primop"); + + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let result = state + .eval_from_string("builtins.mk_attrs_test", "") + .expect("Failed to evaluate expression"); + assert_eq!(result.value_type(), ValueType::Attrs); + assert_eq!(result.attr_keys().unwrap().len(), 2); + let x = result.get_attr("x").expect("missing x"); + assert_eq!(x.as_int().unwrap(), 100); + let y = result.get_attr("y").expect("missing y"); + assert_eq!(y.as_string().unwrap(), "hi"); + } + + #[test] + #[serial] + fn test_primop_ret_set_list() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + + // Register a nullary primop that constructs and returns a list. + PrimOp::new(&ctx, "mk_list_test", 0, None, |_args, ret| { + let a = ret.make_int(7)?; + let b = ret.make_string("hi")?; + let c = ret.make_bool(true)?; + ret.set_list(&[&a, &b, &c]) + }) + .expect("Failed to create primop") + .register(&ctx) + .expect("Failed to register primop"); + + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let result = state + .eval_from_string("builtins.mk_list_test", "") + .expect("Failed to evaluate expression"); + assert_eq!(result.value_type(), ValueType::List); + assert_eq!(result.list_len().unwrap(), 3); + + let first = result.list_get(0).unwrap(); + assert_eq!(first.as_int().unwrap(), 7); + let second = result.list_get(1).unwrap(); + assert_eq!(second.as_string().unwrap(), "hi"); + let third = result.list_get(2).unwrap(); + assert!(third.as_bool().unwrap()); + } + + #[test] + #[serial] + fn test_primop_ret_make_types() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + + // Test each make_* method returns correct type and value. + PrimOp::new(&ctx, "mk_types_test", 0, None, |_args, ret| { + let int_val = ret.make_int(-42)?; + assert_eq!(int_val.value_type(), ValueType::Int); + assert_eq!(int_val.as_int()?, -42); + + let float_val = ret.make_float(3.14)?; + assert_eq!(float_val.value_type(), ValueType::Float); + assert!((float_val.as_float()? - 3.14).abs() < 1e-9); + + let bool_val = ret.make_bool(true)?; + assert_eq!(bool_val.value_type(), ValueType::Bool); + assert!(bool_val.as_bool()?); + + let null_val = ret.make_null()?; + assert_eq!(null_val.value_type(), ValueType::Null); + + let str_val = ret.make_string("ok")?; + assert_eq!(str_val.value_type(), ValueType::String); + assert_eq!(str_val.as_string()?, "ok"); + + ret.set_int(0) + }) + .expect("Failed to create primop") + .register(&ctx) + .expect("Failed to register primop"); + + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let result = state + .eval_from_string("builtins.mk_types_test", "") + .expect("Failed to evaluate expression"); + assert_eq!(result.as_int().unwrap(), 0); + } + + #[test] + #[serial] + fn test_primop_value_as_attrs_chained() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let nested = state + .eval_from_string("{ inner = { x = 99; }; }", "") + .expect("Failed to evaluate nested attrs"); + + let primop = PrimOp::new(&ctx, "nested_get", 1, None, |args, ret| { + let outer = args[0].as_attrs()?; + let inner = outer.get("inner")?; + let inner_attrs = inner.as_attrs()?; + let x = inner_attrs.get("x")?.as_int()?; + ret.set_int(x) + }) + .expect("Failed to create primop"); + + let func = primop + .into_value(&state) + .expect("Failed to embed primop as value"); + + let result = func.call(&nested).expect("Failed to call primop"); + assert_eq!(result.as_int().unwrap(), 99); + } +} diff --git a/nix-bindings/nix-bindings/src/store.rs b/nix-bindings/nix-bindings/src/store.rs new file mode 100644 index 0000000..27b3d8b --- /dev/null +++ b/nix-bindings/nix-bindings/src/store.rs @@ -0,0 +1,782 @@ +use std::{ + ffi::{CStr, CString}, + ptr::NonNull, + sync::Arc, +}; + +use super::{Context, Error, Result, check_err, string_from_callback, sys}; + +/// Nix store for managing packages and derivations. +/// +/// The store provides access to Nix packages, derivations, and store paths. +pub struct Store { + pub(crate) inner: NonNull, + pub(crate) _context: Arc, +} + +/// A path in the Nix store. +/// +/// Represents a store path that can be realized or queried. +pub struct StorePath { + pub(crate) inner: NonNull, + pub(crate) _context: Arc, +} + +/// A Nix derivation loaded from its JSON representation. +/// +/// Derivations are the build recipes used by the Nix store. They describe +/// how to produce a store path from inputs. Use [`Derivation::from_json`] +/// to construct one and [`Derivation::add_to_store`] to register it. +pub struct Derivation { + inner: *mut sys::nix_derivation, + _context: Arc, +} + +impl StorePath { + /// Parse a store path string into a `StorePath`. + /// + /// # Arguments + /// + /// * `context` - The Nix context + /// * `store` - The store containing the path + /// * `path` - The store path string (e.g., `"/nix/store/..."`) + /// + /// # Errors + /// + /// Returns an error if the path string is not a valid store path. + pub fn parse(context: &Arc, store: &Store, path: &str) -> Result { + let path_cstring = CString::new(path)?; + + // SAFETY: context, store, and path_cstring are valid + let path_ptr = unsafe { + sys::nix_store_parse_path(context.as_ptr(), store.as_ptr(), path_cstring.as_ptr()) + }; + + let inner = NonNull::new(path_ptr).ok_or(Error::NullPointer)?; + + Ok(StorePath { + inner, + _context: Arc::clone(context), + }) + } + + /// Get the name component of the store path. + /// + /// Returns the name part of the store path (everything after the hash). + /// For example, for `"/nix/store/abc123...-hello-1.0"` this returns + /// `"hello-1.0"`. + /// + /// # Errors + /// + /// Returns an error if the name cannot be retrieved. + pub fn name(&self) -> Result { + // SAFETY: self.inner is valid, callback matches expected signature + let result = unsafe { + string_from_callback(|cb, ud| { + sys::nix_store_path_name(self.inner.as_ptr(), cb, ud); + }) + }; + result.ok_or(Error::NullPointer) + } + + /// Get the hash component of the store path as raw bytes. + /// + /// The 20-byte hash is decoded from the "nix32" encoding + /// in the store path. For example, for + /// `"/nix/store/abc123...-hello-1.0"` this returns the raw + /// hash bytes corresponding to `"abc123..."`. + /// + /// # Returns + /// + /// The raw 20-byte hash. + /// + /// # Errors + /// + /// Returns an error if the hash cannot be retrieved. + pub fn hash_part(&self) -> Result<[u8; 20]> { + let mut hash = sys::nix_store_path_hash_part { bytes: [0u8; 20] }; + + // SAFETY: context and store path are valid + let err = unsafe { + sys::nix_store_path_hash(self._context.as_ptr(), self.inner.as_ptr(), &mut hash) + }; + check_err(unsafe { self._context.as_ptr() }, err)?; + + Ok(hash.bytes) + } + + /// Create a `StorePath` from its constituent hash and name parts. + /// + /// Unlike [`parse`](StorePath::parse), this does not require a `Store` + /// reference or the `/nix/store` prefix. + /// + /// # Arguments + /// + /// * `hash` - The 20-byte raw hash (as produced by + /// [`hash_part`](Self::hash_part)). + /// * `name` - The name component (e.g., `"hello-1.0"`). + /// + /// # Returns + /// + /// A new `StorePath`. + /// + /// # Errors + /// + /// Returns an error if the path cannot be created. + pub fn from_parts(context: &Arc, hash: &[u8; 20], name: &str) -> Result { + let hash_struct = sys::nix_store_path_hash_part { bytes: *hash }; + let name_c = CString::new(name)?; + + // SAFETY: context, hash, and name are valid + let path_ptr = unsafe { + sys::nix_store_create_from_parts( + context.as_ptr(), + &hash_struct, + name_c.as_ptr(), + name.len(), + ) + }; + + let inner = NonNull::new(path_ptr).ok_or(Error::NullPointer)?; + + Ok(StorePath { + inner, + _context: Arc::clone(context), + }) + } + + /// Get the raw store path pointer. + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::StorePath { + self.inner.as_ptr() + } +} + +impl Clone for StorePath { + fn clone(&self) -> Self { + // SAFETY: self.inner is valid + let cloned_ptr = unsafe { sys::nix_store_path_clone(self.inner.as_ptr()) }; + + let inner = + NonNull::new(cloned_ptr).expect("nix_store_path_clone returned null for valid path"); + + StorePath { + inner, + _context: Arc::clone(&self._context), + } + } +} + +impl Drop for StorePath { + fn drop(&mut self) { + // SAFETY: We own the store path and it is valid until drop + unsafe { + sys::nix_store_path_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: StorePath can be shared between threads +unsafe impl Send for StorePath {} +unsafe impl Sync for StorePath {} + +impl Derivation { + /// Parse a derivation from its JSON representation. + /// + /// # Arguments + /// + /// * `context` - The Nix context + /// * `store` - The store to use + /// * `json` - JSON string describing the derivation + /// + /// # Errors + /// + /// Returns an error if the JSON is not a valid derivation description. + pub fn from_json(context: &Arc, store: &Store, json: &str) -> Result { + let json_c = CString::new(json)?; + + // SAFETY: context, store, and json_c are valid + let drv_ptr = unsafe { + sys::nix_derivation_from_json(context.as_ptr(), store.as_ptr(), json_c.as_ptr()) + }; + + if drv_ptr.is_null() { + return Err(Error::NullPointer); + } + + Ok(Derivation { + inner: drv_ptr, + _context: Arc::clone(context), + }) + } + + /// Add this derivation to the store and return its output store path. + /// + /// # Errors + /// + /// Returns an error if the derivation cannot be registered in the store. + pub fn add_to_store(&mut self, store: &Store) -> Result { + // SAFETY: context, store, and inner are valid + let path_ptr = + unsafe { sys::nix_add_derivation(self._context.as_ptr(), store.as_ptr(), self.inner) }; + + let inner = NonNull::new(path_ptr).ok_or(Error::NullPointer)?; + + Ok(StorePath { + inner, + _context: Arc::clone(&self._context), + }) + } + + /// Read a derivation from the store by its store path. + /// + /// # Returns + /// + /// The derivation object associated with the given `.drv` path. + /// + /// # Errors + /// + /// Returns an error if the derivation cannot be read. + pub fn from_store_path( + context: &Arc, + store: &Store, + path: &StorePath, + ) -> Result { + // SAFETY: context, store, and path are valid + let drv_ptr = unsafe { + sys::nix_store_drv_from_store_path( + context.as_ptr(), + store.as_ptr(), + path.inner.as_ptr(), + ) + }; + + if drv_ptr.is_null() { + return Err(Error::NullPointer); + } + + Ok(Derivation { + inner: drv_ptr, + _context: Arc::clone(context), + }) + } + + /// Serialize this derivation to its JSON representation. + /// + /// # Returns + /// + /// The derivation as a JSON string. + /// + /// # Errors + /// + /// Returns an error if serialization fails. + pub fn to_json(&self) -> Result { + // SAFETY: inner is valid, callback matches expected signature + let result = unsafe { + string_from_callback(|cb, ud| { + sys::nix_derivation_to_json(self._context.as_ptr(), self.inner, cb, ud); + }) + }; + result.ok_or(Error::NullPointer) + } +} + +impl Clone for Derivation { + fn clone(&self) -> Self { + // SAFETY: self.inner is valid + let cloned_ptr = unsafe { sys::nix_derivation_clone(self.inner) }; + let inner = cloned_ptr; // raw pointer, Drop will free + + Derivation { + inner, + _context: Arc::clone(&self._context), + } + } +} + +impl Drop for Derivation { + fn drop(&mut self) { + // SAFETY: We own the derivation and it is valid until drop + unsafe { + sys::nix_derivation_free(self.inner); + } + } +} + +// SAFETY: Derivation can be shared between threads +unsafe impl Send for Derivation {} +unsafe impl Sync for Derivation {} + +impl Store { + /// Open a Nix store. + /// + /// # Arguments + /// + /// * `context` - The Nix context + /// * `uri` - Optional store URI (`None` for the default local store) + /// + /// # Errors + /// + /// Returns an error if the store cannot be opened. + pub fn open(context: &Arc, uri: Option<&str>) -> Result { + let uri_cstring; + let uri_ptr = if let Some(uri) = uri { + uri_cstring = CString::new(uri)?; + uri_cstring.as_ptr() + } else { + std::ptr::null() + }; + + // SAFETY: context is valid; uri_ptr is either null or a valid CString + let store_ptr = + unsafe { sys::nix_store_open(context.as_ptr(), uri_ptr, std::ptr::null_mut()) }; + + let inner = NonNull::new(store_ptr).ok_or(Error::NullPointer)?; + + Ok(Store { + inner, + _context: Arc::clone(context), + }) + } + + /// Get the raw store pointer. + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::Store { + self.inner.as_ptr() + } + + /// Realize a store path. + /// + /// Builds or downloads the store path and all its dependencies, making + /// them available in the local store. + /// + /// # Returns + /// + /// A vector of `(output_name, store_path)` pairs for each realized output. + /// + /// # Errors + /// + /// Returns an error if the path cannot be realized. + pub fn realize(&self, path: &StorePath) -> Result> { + type Userdata = (Vec<(String, StorePath)>, Arc); + + unsafe extern "C" fn realize_callback( + userdata: *mut std::os::raw::c_void, + outname: *const std::os::raw::c_char, + out: *const sys::StorePath, + ) { + let data = unsafe { &mut *(userdata as *mut Userdata) }; + let (outputs, context) = data; + + let name = if !outname.is_null() { + unsafe { CStr::from_ptr(outname).to_string_lossy().into_owned() } + } else { + String::from("out") + }; + + if !out.is_null() { + let cloned_path = unsafe { sys::nix_store_path_clone(out as *mut sys::StorePath) }; + if let Some(inner) = NonNull::new(cloned_path) { + outputs.push(( + name, + StorePath { + inner, + _context: Arc::clone(context), + }, + )); + } + } + } + + let mut userdata: Userdata = (Vec::new(), Arc::clone(&self._context)); + let userdata_ptr = &mut userdata as *mut Userdata as *mut std::os::raw::c_void; + + let err = unsafe { + sys::nix_store_realise( + self._context.as_ptr(), + self.inner.as_ptr(), + path.as_ptr(), + userdata_ptr, + Some(realize_callback), + ) + }; + + check_err(unsafe { self._context.as_ptr() }, err)?; + + Ok(userdata.0) + } + + /// Parse a store path string into a [`StorePath`]. + /// + /// Convenience wrapper around [`StorePath::parse`]. + /// + /// # Errors + /// + /// Returns an error if the path cannot be parsed. + /// + /// # Example + /// + /// ```no_run + /// # use std::sync::Arc; + /// # use nix_bindings::{Context, Store}; + /// # fn main() -> Result<(), Box> { + /// let ctx = Arc::new(Context::new()?); + /// let store = Store::open(&ctx, None)?; + /// let path = store.store_path("/nix/store/...")?; + /// # Ok(()) + /// # } + /// ``` + pub fn store_path(&self, path: &str) -> Result { + StorePath::parse(&self._context, self, path) + } + + /// Check whether a store path is present and valid in the store. + /// + /// Returns `true` if the path exists in the store's database. + #[must_use] + pub fn is_valid_path(&self, path: &StorePath) -> bool { + // SAFETY: context, store, and path are valid + unsafe { + sys::nix_store_is_valid_path( + self._context.as_ptr(), + self.inner.as_ptr(), + path.inner.as_ptr(), + ) + } + } + + /// Resolve the real filesystem path for a store path. + /// + /// For content-addressed stores (e.g., a binary cache) this may differ + /// from the store path itself. + /// + /// # Errors + /// + /// Returns an error if the path cannot be resolved. + pub fn real_path(&self, path: &StorePath) -> Result { + // SAFETY: context, store, and path are valid; callback is safe + let result = unsafe { + string_from_callback(|cb, ud| { + sys::nix_store_real_path( + self._context.as_ptr(), + self.inner.as_ptr(), + path.inner.as_ptr(), + cb, + ud, + ); + }) + }; + result.ok_or(Error::NullPointer) + } + + /// Get the URI identifying this store (e.g., `"local"` or + /// `"https://cache.nixos.org"`). + /// + /// # Errors + /// + /// Returns an error if the URI cannot be retrieved. + pub fn uri(&self) -> Result { + // SAFETY: context and store are valid; callback is safe + let result = unsafe { + string_from_callback(|cb, ud| { + sys::nix_store_get_uri(self._context.as_ptr(), self.inner.as_ptr(), cb, ud); + }) + }; + result.ok_or(Error::NullPointer) + } + + /// Get the store directory (e.g., `"/nix/store"`). + /// + /// # Errors + /// + /// Returns an error if the directory cannot be retrieved. + pub fn store_dir(&self) -> Result { + // SAFETY: context and store are valid; callback is safe + let result = unsafe { + string_from_callback(|cb, ud| { + sys::nix_store_get_storedir(self._context.as_ptr(), self.inner.as_ptr(), cb, ud); + }) + }; + result.ok_or(Error::NullPointer) + } + + /// Get the version string of the store daemon. + /// + /// # Errors + /// + /// Returns an error if the version cannot be retrieved. + pub fn version(&self) -> Result { + // SAFETY: context and store are valid; callback is safe + let result = unsafe { + string_from_callback(|cb, ud| { + sys::nix_store_get_version(self._context.as_ptr(), self.inner.as_ptr(), cb, ud); + }) + }; + result.ok_or(Error::NullPointer) + } + + /// Copy the closure of `path` from `self` into `dst_store`. + /// + /// This copies the store path and all its transitive dependencies. + /// + /// # Errors + /// + /// Returns an error if the copy operation fails. + pub fn copy_closure(&self, dst_store: &Store, path: &StorePath) -> Result<()> { + // SAFETY: context, src store, dst store, and path are valid + let err = unsafe { + sys::nix_store_copy_closure( + self._context.as_ptr(), + self.inner.as_ptr(), + dst_store.as_ptr(), + path.inner.as_ptr(), + ) + }; + check_err(unsafe { self._context.as_ptr() }, err) + } + + /// Copy a single path from this store into `dst_store`. + /// + /// Unlike [`copy_closure`](Self::copy_closure), this copies only the + /// path itself, not its dependencies. + /// + /// # Arguments + /// + /// * `repair` - Whether to repair the path if it is corrupted. + /// * `check_sigs` - Whether to verify path signatures before copying. + /// + /// # Errors + /// + /// Returns an error if the copy operation fails. + pub fn copy_path( + &self, + dst_store: &Store, + path: &StorePath, + repair: bool, + check_sigs: bool, + ) -> Result<()> { + // SAFETY: all pointers are valid + let err = unsafe { + sys::nix_store_copy_path( + self._context.as_ptr(), + self.inner.as_ptr(), + dst_store.as_ptr(), + path.inner.as_ptr(), + repair, + check_sigs, + ) + }; + check_err(unsafe { self._context.as_ptr() }, err) + } + + /// Enumerate the filesystem closure of a store path. + /// + /// Calls `callback` once for each store path in the closure (in no + /// particular order). + /// + /// # Arguments + /// + /// * `flip_direction` - If false, return paths referenced by paths in the + /// closure (forward). If true, return paths that reference paths in the + /// closure (backward). + /// * `include_outputs` - For derivations, also include their outputs. + /// * `include_derivers` - For outputs, also include the derivation that + /// produced them. + /// + /// # Errors + /// + /// Returns an error if the operation fails. + pub fn get_fs_closure( + &self, + path: &StorePath, + flip_direction: bool, + include_outputs: bool, + include_derivers: bool, + mut callback: F, + ) -> Result<()> + where + F: FnMut(&StorePath), + { + type Userdata<'a> = (&'a mut dyn FnMut(&StorePath), Arc); + + unsafe extern "C" fn closure_callback( + _context: *mut sys::nix_c_context, + userdata: *mut std::os::raw::c_void, + sp: *const sys::StorePath, + ) { + let data = unsafe { &mut *(userdata as *mut Userdata<'_>) }; + let (cb, ctx) = data; + + if !sp.is_null() { + let cloned = unsafe { sys::nix_store_path_clone(sp as *mut _) }; + if let Some(inner) = NonNull::new(cloned) { + let p = StorePath { + inner, + _context: Arc::clone(ctx), + }; + cb(&p); + } + } + } + + let mut userdata: Userdata<'_> = (&mut callback, Arc::clone(&self._context)); + let userdata_ptr = &mut userdata as *mut Userdata<'_> as *mut std::os::raw::c_void; + + // SAFETY: all pointers are valid + let err = unsafe { + sys::nix_store_get_fs_closure( + self._context.as_ptr(), + self.inner.as_ptr(), + path.inner.as_ptr(), + flip_direction, + include_outputs, + include_derivers, + userdata_ptr, + Some(closure_callback), + ) + }; + check_err(unsafe { self._context.as_ptr() }, err) + } + + /// Look up the full store path from a hash part. + /// + /// # Returns + /// + /// `Some(StorePath)` if a matching path exists, `None` otherwise. + pub fn query_path_from_hash_part(&self, hash: &str) -> Result> { + let hash_c = CString::new(hash)?; + + // SAFETY: context, store, and hash_c are valid + let path_ptr = unsafe { + sys::nix_store_query_path_from_hash_part( + self._context.as_ptr(), + self.inner.as_ptr(), + hash_c.as_ptr(), + ) + }; + + if path_ptr.is_null() { + return Ok(None); + } + + let inner = NonNull::new(path_ptr).ok_or(Error::NullPointer)?; + + Ok(Some(StorePath { + inner, + _context: Arc::clone(&self._context), + })) + } +} + +impl Drop for Store { + fn drop(&mut self) { + // SAFETY: We own the store and it is valid until drop + unsafe { + sys::nix_store_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: Store can be shared between threads +unsafe impl Send for Store {} +unsafe impl Sync for Store {} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use serial_test::serial; + + use super::*; + + #[test] + #[serial] + fn test_store_opening() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let _store = Store::open(&ctx, None).expect("Failed to open store"); + } + + #[test] + #[serial] + fn test_store_path_parse() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + + // Well-formed path; may or may not exist in the local store + let result = StorePath::parse( + &ctx, + &store, + "/nix/store/00000000000000000000000000000000-test", + ); + + match result { + Ok(_) | Err(_) => { + // Either outcome is acceptable; we just verify the API does not + // panic or invoke UB + } + } + } + + #[test] + #[serial] + fn test_store_path_clone() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + + if let Ok(path) = StorePath::parse( + &ctx, + &store, + "/nix/store/00000000000000000000000000000000-test", + ) { + let cloned = path.clone(); + let original_name = path.name().expect("Failed to get original name"); + let cloned_name = cloned.name().expect("Failed to get cloned name"); + assert_eq!( + original_name, cloned_name, + "Cloned path should have the same name" + ); + } + } + + #[test] + #[serial] + fn test_store_uri() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + let uri = store.uri().expect("Failed to get store URI"); + assert!(!uri.is_empty(), "Store URI should not be empty"); + } + + #[test] + #[serial] + fn test_store_dir() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + let dir = store.store_dir().expect("Failed to get store directory"); + assert!(!dir.is_empty(), "Store directory should not be empty"); + } + + #[test] + #[serial] + fn test_store_version() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + let ver = store.version().expect("Failed to get store version"); + assert!(!ver.is_empty(), "Store version should not be empty"); + } + + #[test] + #[serial] + fn test_store_is_valid_path() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + + if let Ok(path) = StorePath::parse( + &ctx, + &store, + "/nix/store/00000000000000000000000000000000-test", + ) { + // A random hash is almost certainly not valid + let valid = store.is_valid_path(&path); + assert!(!valid, "Random path should not be valid in the store"); + } + } +} diff --git a/nix-bindings/nix-bindings/tests/integration.rs b/nix-bindings/nix-bindings/tests/integration.rs new file mode 100644 index 0000000..d2114f7 --- /dev/null +++ b/nix-bindings/nix-bindings/tests/integration.rs @@ -0,0 +1,388 @@ +#![cfg(feature = "expr")] + +use std::{process::Command, sync::Arc}; + +use nix_bindings::{Context, EvalStateBuilder, Store, ValueType}; +use serial_test::serial; + +#[test] +#[serial] +fn test_basic_arithmetic() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test simple arithmetic + let result = state + .eval_from_string("5 + 3", "") + .expect("Failed to evaluate expression"); + + assert_eq!(result.value_type(), ValueType::Int); + assert_eq!(result.as_int().expect("Failed to get int value"), 8); +} + +#[test] +#[serial] +fn test_string_operations() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test string literal + let result = state + .eval_from_string("\"hello world\"", "") + .expect("Failed to evaluate string"); + + assert_eq!(result.value_type(), ValueType::String); + assert_eq!( + result.as_string().expect("Failed to get string"), + "hello world" + ); +} + +#[test] +#[serial] +fn test_boolean_values() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test boolean true + let result = state + .eval_from_string("true", "") + .expect("Failed to evaluate boolean"); + + assert_eq!(result.value_type(), ValueType::Bool); + assert!(result.as_bool().expect("Failed to get bool")); + + // Test boolean false + let result = state + .eval_from_string("false", "") + .expect("Failed to evaluate boolean"); + + assert_eq!(result.value_type(), ValueType::Bool); + assert!(!result.as_bool().expect("Failed to get bool")); +} + +#[test] +#[serial] +fn test_complex_expressions() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test nested arithmetic + let result = state + .eval_from_string("(2 + 3) * (4 - 1)", "") + .expect("Failed to evaluate expression"); + + assert_eq!(result.value_type(), ValueType::Int); + assert_eq!(result.as_int().expect("Failed to get int value"), 15); + + // Test string interpolation + let result = state + .eval_from_string("\"The answer is ${toString (6 * 7)}\"", "") + .expect("Failed to evaluate string interpolation"); + + assert_eq!(result.value_type(), ValueType::String); + assert_eq!( + result.as_string().expect("Failed to get string"), + "The answer is 42" + ); +} + +#[test] +#[serial] +fn test_attribute_sets() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test attribute set creation + let result = state + .eval_from_string("{ name = \"test\"; value = 42; }", "") + .expect("Failed to evaluate attrset"); + + assert_eq!(result.value_type(), ValueType::Attrs); +} + +#[test] +#[serial] +fn test_lists() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test list creation + let result = state + .eval_from_string("[ 1 2 3 \"hello\" ]", "") + .expect("Failed to evaluate list"); + + assert_eq!(result.value_type(), ValueType::List); +} + +#[test] +#[serial] +fn test_functions() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test function application + let result = state + .eval_from_string("(x: x + 1) 5", "") + .expect("Failed to evaluate function application"); + + assert_eq!(result.value_type(), ValueType::Int); + assert_eq!(result.as_int().expect("Failed to get int value"), 6); +} + +#[test] +#[serial] +fn test_builtin_functions() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test length builtin + let result = state + .eval_from_string("builtins.length [ 1 2 3 4 5 ]", "") + .expect("Failed to evaluate builtin function"); + + assert_eq!(result.value_type(), ValueType::Int); + assert_eq!(result.as_int().expect("Failed to get int value"), 5); + + // Test head builtin + let result = state + .eval_from_string("builtins.head [ \"first\" \"second\" \"third\" ]", "") + .expect("Failed to evaluate builtin function"); + + assert_eq!(result.value_type(), ValueType::String); + assert_eq!(result.as_string().expect("Failed to get string"), "first"); +} + +#[test] +#[serial] +fn test_null_value() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test null value + let result = state + .eval_from_string("null", "") + .expect("Failed to evaluate null"); + + assert_eq!(result.value_type(), ValueType::Null); +} + +#[test] +#[serial] +fn test_error_handling() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test invalid expression - this should fail + let result = state.eval_from_string("invalid syntax here", ""); + assert!(result.is_err(), "Expected evaluation to fail"); + + // Test type mismatch - trying to get int from string should fail + let string_val = state + .eval_from_string("\"not a number\"", "") + .expect("Failed to evaluate string"); + + let int_result = string_val.as_int(); + assert!(int_result.is_err(), "Expected type conversion to fail"); +} + +#[test] +#[serial] +fn test_resource_cleanup() { + // Test that resources are properly cleaned up when dropped + for _i in 0..10 { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let _result = state + .eval_from_string("1 + 1", "") + .expect("Failed to evaluate expression"); + + // Resources should be automatically cleaned up when they go out of scope + } + // If we reach here without crashing, cleanup is working +} + +#[test] +#[serial] +fn test_value_formatting_display() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test Display formatting + let result = state + .eval_from_string("42", "") + .expect("Failed to evaluate"); + println!("Display: {result}"); + assert_eq!(format!("{result}"), "42"); + + let result = state + .eval_from_string("\"hello world\"", "") + .expect("Failed to evaluate"); + println!("Display: {result}"); + assert_eq!(format!("{result}"), "hello world"); + + // Test Debug formatting + let result = state + .eval_from_string("true", "") + .expect("Failed to evaluate"); + println!("Debug: {result:?}"); + assert_eq!(format!("{result:?}"), "Value::Bool(true)"); + + // Test Nix syntax formatting + let result = state + .eval_from_string("\"test string\"", "") + .expect("Failed to evaluate"); + println!( + "Nix syntax: {}", + result.to_nix_string().expect("Failed to format") + ); + assert_eq!( + result.to_nix_string().expect("Failed to format"), + "\"test string\"" + ); + + // Test complex values + let attrs = state + .eval_from_string("{ a = 1; b = \"test\"; }", "") + .expect("Failed to evaluate"); + println!("Attrs display: {attrs}"); + println!("Attrs debug: {attrs:?}"); + + let list = state + .eval_from_string("[ 1 2 3 ]", "") + .expect("Failed to evaluate"); + println!("List display: {list}"); + println!("List debug: {list:?}"); +} + +#[test] +#[serial] +fn test_realize_derivation() { + // This test uses nix-instantiate to create a derivation, then realizes it + // using the Store::realize() method + + // Create a simple Nix expression that builds a derivation + let nix_expr = r#" + derivation { + name = "test-derivation"; + builder = "/bin/sh"; + args = [ "-c" "echo 'Hello from Nix!' > $out" ]; + system = builtins.currentSystem; + } + "#; + + // Use nix-instantiate to create a .drv file + let output = Command::new("nix-instantiate") + .arg("--expr") + .arg(nix_expr) + .output() + .expect("Failed to run nix-instantiate"); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("nix-instantiate failed: {}", stderr); + } + + let drv_path = String::from_utf8(output.stdout) + .expect("Invalid UTF-8 in nix-instantiate output") + .trim() + .to_string(); + + println!("Derivation path: {}", drv_path); + + // Now use the Rust bindings to parse and realize the derivation + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + + // Parse the derivation path using the convenient store.store_path() method + let store_path = store + .store_path(&drv_path) + .expect("Failed to parse store path"); + + println!( + "Parsed store path name: {}", + store_path.name().expect("Failed to get name") + ); + + // Realize the derivation + let realized_outputs = store + .realize(&store_path) + .expect("Failed to realize derivation"); + + println!("Realized {} outputs:", realized_outputs.len()); + for (name, path) in &realized_outputs { + println!( + " Output '{}': {}", + name, + path.name().expect("Failed to get path name") + ); + } + + // Verify we got at least one output + assert!( + !realized_outputs.is_empty(), + "Expected at least one realized output" + ); + + // Verify the output has a name + let (output_name, output_path) = &realized_outputs[0]; + println!("First output name: {}", output_name); + + let path_name = output_path.name().expect("Failed to get output path name"); + println!("First output path name: {}", path_name); + + // The path name should contain our derivation name + assert!( + path_name.contains("test-derivation"), + "Output path should contain derivation name" + ); +} diff --git a/nix/cache-test.nix b/nix/cache-test.nix new file mode 100644 index 0000000..4e4498b --- /dev/null +++ b/nix/cache-test.nix @@ -0,0 +1,53 @@ +{ pkgs }: +pkgs.writeShellScriptBin "cache-test" '' + #!/usr/bin/env bash + set -euo pipefail + + echo "🧪 Testing cache behavior for loft..." + echo "Building with nom (no symlinks)..." + + PATHS=$(${pkgs.nix-output-monitor}/bin/nom build .#default --print-out-paths --no-link 2>&1 | tee /dev/stderr | tail -1) + + STORE_PATHS=$(echo "$PATHS" | grep -o '/nix/store/[^[:space:]]*' || echo "$PATHS") + + echo "📦 Built store paths:" + declare -a PATH_ARRAY + while IFS= read -r path; do + if [[ -n "$path" ]]; then + echo " $path" + PATH_ARRAY+=("$path") + fi + done <<< "$STORE_PATHS" + + echo "" + echo "🚀 Using fresh loft to upload itself to cache..." + + for path in "''${PATH_ARRAY[@]}"; do + if [[ "$path" =~ ^/nix/store/ ]]; then + echo "📤 Uploading to cache: $path" + + LOFT_BIN="$path/bin/loft" + if [[ ! -f "$LOFT_BIN" ]]; then + echo "❌ loft binary not found at: $LOFT_BIN" + echo " Contents of $path:" + ls -la "$path" || echo " Cannot list directory" + if [[ -d "$path/bin" ]]; then + echo " Contents of $path/bin:" + ls -la "$path/bin" + fi + exit 1 + fi + + if "$LOFT_BIN" --config .direnv/loft/loft.toml --upload-path "$path"; then + echo "✅ Successfully uploaded: $path" + else + echo "❌ Failed to upload: $path" + exit 1 + fi + fi + done + + echo "✨ Cache test complete!" + echo "" + echo "💡 To test cache hit, run this script again - it should pull from your cache!" +'' diff --git a/nixos/tests/integration.nix b/nixos/tests/integration.nix index c6629a3..ac1394d 100644 --- a/nixos/tests/integration.nix +++ b/nixos/tests/integration.nix @@ -3,80 +3,101 @@ { name = "loft-integration-test"; - nodes.machine = { config, pkgs, lib, ... }: { - imports = [ ../module.nix ]; + nodes.machine = + { + config, + pkgs, + lib, + ... + }: + { + imports = [ ../module.nix ]; - virtualisation.writableStore = true; - virtualisation.memorySize = 2048; - virtualisation.diskSize = 4096; + virtualisation.writableStore = true; + virtualisation.memorySize = 2048; + virtualisation.diskSize = 4096; - services.nginx = { - enable = true; - virtualHosts."auth-proxy" = { - listen = [ { port = 3902; addr = "0.0.0.0"; } ]; - locations."/" = { - proxyPass = "http://localhost:3900"; - extraConfig = '' - proxy_set_header Host $host:$server_port; - if ($http_x_loft_auth != "test-token") { - return 403; + services.nginx = { + enable = true; + virtualHosts."auth-proxy" = { + listen = [ + { + port = 3902; + addr = "0.0.0.0"; } - ''; + ]; + locations."/" = { + proxyPass = "http://localhost:3900"; + extraConfig = '' + proxy_set_header Host $host:$server_port; + if ($http_x_loft_auth != "test-token") { + return 403; + } + ''; + }; }; }; - }; - services.garage = { - enable = true; - package = pkgs.garage; - settings = { - metadata_dir = "/var/lib/garage/meta"; - data_dir = "/var/lib/garage/data"; - replication_factor = 1; - rpc_bind_addr = "[::]:3901"; - rpc_secret = "0000000000000000000000000000000000000000000000000000000000000000"; - s3_api = { - s3_region = "us-east-1"; - api_bind_addr = "[::]:3900"; - root_domain = ".s3.garage.localhost"; + services.garage = { + enable = true; + package = pkgs.garage; + settings = { + metadata_dir = "/var/lib/garage/meta"; + data_dir = "/var/lib/garage/data"; + replication_factor = 1; + rpc_bind_addr = "[::]:3901"; + rpc_secret = "0000000000000000000000000000000000000000000000000000000000000000"; + s3_api = { + s3_region = "us-east-1"; + api_bind_addr = "[::]:3900"; + root_domain = ".s3.garage.localhost"; + }; }; }; - }; - services.loft = { - enable = true; - s3 = { - bucket = "loft-test-bucket"; - endpoint = "http://localhost:3900"; - region = "us-east-1"; - accessKeyFile = "/etc/loft-s3-access-key"; - secretKeyFile = "/etc/loft-s3-secret-key"; + services.loft = { + enable = true; + s3 = { + bucket = "loft-test-bucket"; + endpoint = "http://localhost:3900"; + region = "us-east-1"; + accessKeyFile = "/etc/loft-s3-access-key"; + secretKeyFile = "/etc/loft-s3-secret-key"; + }; + uploadThreads = 4; + scanOnStartup = false; + populateCacheOnStartup = false; + skipSignedByKeys = [ + "test-exclude-key-1" + "cache.nixos.org-1" + ]; }; - uploadThreads = 4; - scanOnStartup = false; - populateCacheOnStartup = false; - skipSignedByKeys = [ "test-exclude-key-1" "cache.nixos.org-1" ]; - }; - systemd.services.loft.wantedBy = lib.mkForce []; + systemd.services.loft.wantedBy = lib.mkForce [ ]; - environment.systemPackages = with pkgs; [ - awscli2 - jq - garage - loft - coreutils - ]; + environment.systemPackages = with pkgs; [ + awscli2 + jq + garage + loft + coreutils + ]; - nix.settings = { - experimental-features = [ "nix-command" "flakes" ]; - trusted-users = [ "root" ]; - substituters = []; - sandbox = false; - }; + nix.settings = { + experimental-features = [ + "nix-command" + "flakes" + ]; + trusted-users = [ "root" ]; + sandbox = false; + }; - networking.firewall.allowedTCPPorts = [ 3900 3901 3902 ]; - }; + networking.firewall.allowedTCPPorts = [ + 3900 + 3901 + 3902 + ]; + }; testScript = '' import re diff --git a/src/cache_checker.rs b/src/cache_checker.rs index 58c6f3a..cc9051d 100644 --- a/src/cache_checker.rs +++ b/src/cache_checker.rs @@ -6,7 +6,7 @@ use std::{ }; use tracing::{debug, info}; -use attic::nix_store::NixStore; +use crate::nix_store::NixStore; /// Trait for local cache storage operations. pub trait LocalCacheStorage: Send + Sync { diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..16bd04f --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,140 @@ +use displaydoc::Display; +use serde::{de, ser, Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use anyhow::Result; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Hash { + Sha256([u8; 32]), +} + +#[derive(Debug, Display)] +pub enum Error { + /// The string lacks a colon separator. + NoColonSeparator, + /// Hash algorithm {0} is not supported. + UnsupportedHashAlgorithm(String), + /// Invalid base16 hash: {0}. + InvalidBase16Hash(hex::FromHexError), + /// Invalid base32 hash. + InvalidBase32Hash, + /// Invalid length for {typ} string: Must be either {base16_len} (hexadecimal) or {base32_len} (base32), got {actual}. + InvalidHashStringLength { + typ: &'static str, + base16_len: usize, + base32_len: usize, + actual: usize, + }, +} + +impl Hash { + pub fn sha256_from_bytes(bytes: &[u8]) -> Self { + let mut hasher = Sha256::new(); + hasher.update(bytes); + Self::Sha256(hasher.finalize().into()) + } + + pub fn from_typed(s: &str) -> Result { + // Handle both "sha256:" (typed narinfo) and + // "sha256-" (nix path-info --json output) formats. + let (typ, hash) = if let Some(pos) = s.find(':') { + let (t, h) = s.split_at(pos); + (t, &h[1..]) + } else if let Some(pos) = s.find('-') { + let (t, h) = s.split_at(pos); + (t, &h[1..]) + } else { + return Err(Error::NoColonSeparator.into()); + }; + match typ { + "sha256" => { + let v = decode_hash(hash, "SHA-256", 32)?; + Ok(Self::Sha256(v.try_into().unwrap())) + } + _ => Err(Error::UnsupportedHashAlgorithm(typ.to_owned()).into()), + } + } + + pub fn to_typed_base32(&self) -> String { + format!("{}:{}", self.hash_type(), self.to_base32()) + } + + pub fn to_typed_base16(&self) -> String { + format!("{}:{}", self.hash_type(), hex::encode(self.data())) + } + + fn data(&self) -> &[u8] { + match self { + Self::Sha256(d) => d, + } + } + + fn hash_type(&self) -> &'static str { + match self { + Self::Sha256(_) => "sha256", + } + } + + fn to_base32(&self) -> String { + nix_base32::to_nix_base32(self.data()) + } +} + +impl<'de> Deserialize<'de> for Hash { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + use de::Error; + String::deserialize(deserializer) + .and_then(|s| Self::from_typed(&s).map_err(|e| Error::custom(e.to_string()))) + } +} + +impl std::error::Error for Error {} + +impl Serialize for Hash { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_typed_base16()) + } +} + +fn decode_hash(s: &str, typ: &'static str, expected_bytes: usize) -> Result> { + let base16_len = expected_bytes * 2; + let base32_len = (expected_bytes * 8 - 1) / 5 + 1; + let base64_len = expected_bytes.div_ceil(3) * 4; + let v = if s.len() == base16_len { + hex::decode(s).map_err(Error::InvalidBase16Hash)? + } else if s.len() == base32_len { + nix_base32::from_nix_base32(s).ok_or(Error::InvalidBase32Hash)? + } else if s.len() == base64_len || (!s.is_empty() && s.len() <= base64_len && s.ends_with('=')) + { + use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; + let mut buf = vec![0u8; expected_bytes]; + let written = + BASE64 + .decode_slice(s, &mut buf) + .map_err(|_e| Error::InvalidHashStringLength { + typ, + base16_len, + base32_len, + actual: s.len(), + })?; + buf.truncate(written); + buf + } else { + return Err(Error::InvalidHashStringLength { + typ, + base16_len, + base32_len, + actual: s.len(), + } + .into()); + }; + assert!(v.len() == expected_bytes); + Ok(v) +} diff --git a/src/lib.rs b/src/lib.rs index b5d5790..b6f6a43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,11 @@ -// In your new src/lib.rs file - pub mod cache_checker; pub mod config; +pub mod hash; pub mod local_cache; pub mod nix_manifest; +pub mod nix_store; pub mod nix_store_watcher; pub mod nix_utils; pub mod pruner; pub mod s3_uploader; +pub mod signing; diff --git a/src/local_cache.rs b/src/local_cache.rs index 1bb3ecd..cede255 100644 --- a/src/local_cache.rs +++ b/src/local_cache.rs @@ -1,7 +1,7 @@ //! Manages a local redb cache of uploaded paths with full thread safety. use crate::cache_checker::LocalCacheStorage; +use crate::nix_store::NixStore; use anyhow::{anyhow, Result}; -use attic::nix_store::NixStore; use redb::{Database, ReadableDatabase, ReadableTable, ReadableTableMetadata, TableDefinition}; use std::collections::HashSet; use std::path::Path; diff --git a/src/main.rs b/src/main.rs index 0e14bd9..b46dba4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,7 +128,10 @@ async fn main() -> Result<()> { // Handle manual path uploads if let Some(paths_to_upload) = args.upload_path { - info!("Manually uploading {} specified paths...", paths_to_upload.len()); + info!( + "Manually uploading {} specified paths...", + paths_to_upload.len() + ); let local_cache_clone = local_cache.clone(); let uploader_clone = uploader.clone(); let config_clone = config.clone(); @@ -313,10 +316,7 @@ async fn populate_local_cache_from_s3( let all_hashes = uploader.list_all_narinfo_keys().await?; info!("Found {} hashes in S3.", all_hashes.len()); - debug!( - "Adding {} hashes to local cache.", - all_hashes.len() - ); + debug!("Adding {} hashes to local cache.", all_hashes.len()); local_cache.add_many_path_hashes(&all_hashes)?; local_cache.set_scan_complete()?; diff --git a/src/nix_manifest/mod.rs b/src/nix_manifest/mod.rs index c788fd1..0aafdad 100644 --- a/src/nix_manifest/mod.rs +++ b/src/nix_manifest/mod.rs @@ -36,8 +36,8 @@ use self::deserializer::Deserializer; use self::serializer::Serializer; use anyhow::Result; -use attic::hash::Hash; -use attic::signing::NixKeypair; +use crate::hash::Hash; +use crate::signing::NixKeypair; use itoa; pub fn from_str(s: &str) -> Result diff --git a/src/nix_store.rs b/src/nix_store.rs new file mode 100644 index 0000000..9e55b1a --- /dev/null +++ b/src/nix_store.rs @@ -0,0 +1,360 @@ +use std::ffi::OsStr; +use std::fmt; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use futures::stream::Stream; +use serde_json::Value; +use tokio::io::AsyncReadExt; +use tokio::process::Command; +use tracing::warn; + +use crate::hash::Hash; + +pub const STORE_PATH_HASH_LEN: usize = 32; +const NIX_BASE32_CHARS: &[u8] = b"0123456789abcdfghijklmnpqrsvwxyz"; + +#[derive(Debug, Clone)] +pub struct StorePath { + base_name: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct StorePathHash(String); + +#[derive(Debug)] +pub struct ValidPathInfo { + pub path: StorePath, + pub nar_hash: Hash, + pub nar_size: u64, + pub references: Vec, + pub sigs: Vec, + pub ca: Option, +} + +pub struct NixStore { + store_dir: PathBuf, +} + +impl NixStore { + pub fn connect() -> Result { + let output = std::process::Command::new("nix") + .arg("eval") + .arg("--expr") + .arg("builtins.storeDir") + .arg("--raw") + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("Failed to get store dir: {}", stderr)); + } + + let store_dir = String::from_utf8(output.stdout)?.trim().to_string(); + if store_dir.is_empty() { + return Err(anyhow::anyhow!("Empty store directory")); + } + + Ok(Self { + store_dir: PathBuf::from(store_dir), + }) + } + + pub fn store_dir(&self) -> &Path { + &self.store_dir + } + + pub fn follow_store_path>(&self, path: P) -> Result { + let path = path.as_ref(); + if path.strip_prefix(&self.store_dir).is_ok() { + self.parse_store_path(path) + } else { + let canon = std::fs::canonicalize(path)?; + self.parse_store_path(canon) + } + } + + pub fn parse_store_path>(&self, path: P) -> Result { + let base_name = to_base_name(&self.store_dir, path.as_ref())?; + StorePath::from_base_name(base_name) + } + + pub fn get_full_path(&self, store_path: &StorePath) -> PathBuf { + self.store_dir.join(&store_path.base_name) + } + + // TODO: Replace CLI with nix C API when nix_api_store.h exposes + // nix_store_query_path_info and nix_store_nar_from_path. + // Tracked at: https://github.com/NixOS/nix/issues (C API expansion) + pub async fn query_path_info(&self, store_path: StorePath) -> Result { + let full_path = self.get_full_path(&store_path); + let path_str = full_path.to_str().unwrap_or(""); + + let output = Command::new("nix") + .arg("path-info") + .arg("--json") + .arg(path_str) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("nix path-info failed: {}", stderr)); + } + + let stdout = String::from_utf8(output.stdout)?; + let json: Value = serde_json::from_str(&stdout)?; + + let path_info = json + .get(path_str) + .or_else(|| json.as_object().and_then(|m| m.values().next())) + .ok_or_else(|| anyhow::anyhow!("No path info in JSON output"))?; + + let nar_hash_str = path_info + .get("narHash") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing narHash"))?; + + let nar_hash = Hash::from_typed(nar_hash_str)?; + + let nar_size = path_info + .get("narSize") + .and_then(|v| v.as_u64()) + .ok_or_else(|| anyhow::anyhow!("Missing narSize"))?; + + let references = path_info + .get("references") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .map(PathBuf::from) + .collect() + }) + .unwrap_or_default(); + + let sigs = path_info + .get("signatures") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .map(|s| s.to_string()) + .collect() + }) + .unwrap_or_default(); + + let ca = path_info + .get("ca") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + Ok(ValidPathInfo { + path: store_path, + nar_hash, + nar_size, + references, + sigs, + ca, + }) + } + + pub fn nar_from_path(&self, store_path: StorePath) -> impl Stream>> { + let full_path = self.get_full_path(&store_path); + let path_str = full_path.to_str().unwrap_or("").to_string(); + + let (tx, rx) = tokio::sync::mpsc::unbounded_channel::>>(); + + tokio::spawn(async move { + let mut child = match Command::new("nix") + .arg("nar") + .arg("dump-path") + .arg(&path_str) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + { + Ok(c) => c, + Err(e) => { + let _ = tx.send(Err(anyhow::anyhow!("Failed to spawn nix nar: {}", e))); + return; + } + }; + + let mut stdout = match child.stdout.take() { + Some(s) => s, + None => { + let _ = tx.send(Err(anyhow::anyhow!("No stdout from nix nar"))); + return; + } + }; + + let mut buf = vec![0u8; 65536]; + loop { + match stdout.read(&mut buf).await { + Ok(0) => break, + Ok(n) => { + if tx.send(Ok(buf[..n].to_vec())).is_err() { + return; + } + } + Err(e) => { + let _ = tx.send(Err(anyhow::anyhow!("NAR read error: {}", e))); + return; + } + } + } + + let status = match child.wait().await { + Ok(s) => s, + Err(e) => { + let _ = tx.send(Err(anyhow::anyhow!("Failed to wait for nix nar: {}", e))); + return; + } + }; + + if !status.success() { + let stderr = match child.stderr.take() { + Some(mut s) => { + let mut buf = String::new(); + s.read_to_string(&mut buf).await.unwrap_or(0); + buf + } + None => String::new(), + }; + let _ = tx.send(Err(anyhow::anyhow!( + "nix nar dump-path failed ({}): {}", + status, + stderr + ))); + } + }); + + tokio_stream::wrappers::UnboundedReceiverStream::new(rx) + } + + pub async fn compute_fs_closure_multi( + &self, + store_paths: Vec, + flip_directions: bool, + _include_outputs: bool, + _include_derivers: bool, + ) -> Result> { + let mut paths = Vec::new(); + for sp in &store_paths { + let full = self.get_full_path(sp); + paths.push(full.to_str().unwrap_or("").to_string()); + } + + let mut cmd = Command::new("nix-store"); + cmd.arg("--query"); + if flip_directions { + cmd.arg("--referrers"); + } else { + cmd.arg("--requisites"); + } + for p in &paths { + cmd.arg(p); + } + + let output = cmd.output().await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("nix-store --query failed: {}", stderr)); + } + + let output_str = String::from_utf8(output.stdout)?; + let mut result = Vec::new(); + for line in output_str.lines() { + if !line.is_empty() { + match self.parse_store_path(line) { + Ok(sp) => result.push(sp), + Err(e) => warn!("Failed to parse closure path {}: {}", line, e), + } + } + } + + Ok(result) + } +} + +impl StorePath { + fn from_base_name(base_name: PathBuf) -> Result { + let s = base_name.as_os_str().to_str().ok_or_else(|| { + anyhow::anyhow!( + "Store path name contains non-UTF-8 characters: {:?}", + base_name + ) + })?; + + if s.len() < 33 || !s.is_char_boundary(STORE_PATH_HASH_LEN) { + return Err(anyhow::anyhow!("Store path too short: {}", s)); + } + + let hash = &s[..STORE_PATH_HASH_LEN]; + if !hash.bytes().all(|b| NIX_BASE32_CHARS.contains(&b)) { + return Err(anyhow::anyhow!("Invalid store path hash: {}", s)); + } + + let name = &s[STORE_PATH_HASH_LEN + 1..]; + if name.is_empty() { + return Err(anyhow::anyhow!("Store path missing name part: {}", s)); + } + + Ok(Self { base_name }) + } + + pub fn to_hash(&self) -> StorePathHash { + let s = unsafe { std::str::from_utf8_unchecked(self.base_name.as_os_str().as_bytes()) }; + let hash = s[..STORE_PATH_HASH_LEN].to_string(); + StorePathHash(hash) + } + + pub fn name(&self) -> String { + let s = unsafe { std::str::from_utf8_unchecked(self.base_name.as_os_str().as_bytes()) }; + s[STORE_PATH_HASH_LEN + 1..].to_string() + } + + pub fn as_os_str(&self) -> &OsStr { + self.base_name.as_os_str() + } + + pub fn as_base_name_bytes(&self) -> &[u8] { + self.base_name.as_os_str().as_bytes() + } +} + +impl StorePathHash { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for StorePathHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +fn to_base_name(store_dir: &Path, path: &Path) -> Result { + let remaining = path.strip_prefix(store_dir).map_err(|_| { + anyhow::anyhow!( + "Path '{}' is not in store directory '{}'", + path.display(), + store_dir.display() + ) + })?; + + let first = remaining + .iter() + .next() + .ok_or_else(|| anyhow::anyhow!("Path is store directory itself"))?; + + if first.len() < STORE_PATH_HASH_LEN { + return Err(anyhow::anyhow!("Path is too short: {:?}", first)); + } + + Ok(PathBuf::from(first)) +} diff --git a/src/nix_store_watcher.rs b/src/nix_store_watcher.rs index 8acea4b..6f23b35 100644 --- a/src/nix_store_watcher.rs +++ b/src/nix_store_watcher.rs @@ -8,16 +8,16 @@ use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::sync::{mpsc, Semaphore}; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; type InFlightRegistry = Arc>; use crate::cache_checker::CacheChecker; use crate::config::Config; use crate::local_cache::LocalCache; +use crate::nix_store::NixStore; use crate::nix_utils as nix; use crate::s3_uploader::S3Uploader; -use attic::nix_store::NixStore; // fn strip_lock_file(p: &Path) -> Option { // p.to_str() @@ -43,7 +43,8 @@ pub async fn scan_and_process_existing_paths( let initial_scanned_count = all_sigs_map.len(); // 2. Identify "root" paths (paths we built or aren't signed by skip-keys) - let root_paths_map = nix::filter_out_sig_keys(all_sigs_map.clone(), keys_to_skip.clone()).await?; + let root_paths_map = + nix::filter_out_sig_keys(all_sigs_map.clone(), keys_to_skip.clone()).await?; let root_paths_vec: Vec = root_paths_map.keys().cloned().collect(); // 3. Expand closures of those root paths to ensure coverage @@ -68,7 +69,10 @@ pub async fn scan_and_process_existing_paths( } if !missing_sigs_paths.is_empty() { - info!("Fetching signatures for {} newly discovered closure paths...", missing_sigs_paths.len()); + info!( + "Fetching signatures for {} newly discovered closure paths...", + missing_sigs_paths.len() + ); let extra_sigs = nix::get_path_signatures_bulk(&missing_sigs_paths).await?; all_sigs_map.extend(extra_sigs); } @@ -112,7 +116,10 @@ pub async fn scan_and_process_existing_paths( } if dry_run { - info!("DRY RUN: The following {} paths would be uploaded:", result.to_upload.len()); + info!( + "DRY RUN: The following {} paths would be uploaded:", + result.to_upload.len() + ); for path in &result.to_upload { info!(" DRY RUN: Would upload {}", path); } @@ -216,7 +223,7 @@ pub async fn watch_store( path_opt = rx.recv() => { if let Some(path) = path_opt { let mut paths_batch = vec![path]; - + // Debounce: Wait a bit to see if more paths arrive let debounce_duration = std::time::Duration::from_millis(500); let mut interval = tokio::time::interval(debounce_duration); @@ -245,17 +252,47 @@ pub async fn watch_store( join_set.spawn(async move { let permit = semaphore_clone.acquire_owned().await.unwrap(); - if let Err(e) = process_paths( - uploader_clone, - local_cache_clone, - paths_batch, - &config_for_task, - force_scan, - dry_run, - in_flight_clone, - ) - .await - { + let max_retries = 3u32; + let mut delay = std::time::Duration::from_secs(1); + let mut last_error = None; + + for attempt in 0..=max_retries { + let result = process_paths( + uploader_clone.clone(), + local_cache_clone.clone(), + paths_batch.clone(), + &config_for_task, + force_scan, + dry_run, + in_flight_clone.clone(), + ) + .await; + + match result { + Ok(()) => { + last_error = None; + break; + } + Err(e) => { + let err_str = format!("{:#}", e); + if attempt < max_retries && err_str.contains("not valid") + { + warn!( + "Batch processing failed (attempt {}): paths may not be valid yet. Retrying in {:?}...", + attempt + 1, delay, + ); + tokio::time::sleep(delay).await; + delay *= 2; + last_error = Some(e); + } else { + last_error = Some(e); + break; + } + } + } + } + + if let Some(e) = last_error { error!("Failed to process batch: {:?}", e); } drop(permit); @@ -365,7 +402,7 @@ pub async fn process_paths( let closure_signatures = nix::get_path_signatures_bulk(&result.to_upload).await?; let filtered_closure_paths = nix::filter_out_sig_keys(closure_signatures, keys_to_skip).await?; let filtered_closure_vec: Vec = filtered_closure_paths.keys().cloned().collect(); - + info!( "Total paths missing from cache after filtering: {}", filtered_closure_vec.len() @@ -374,7 +411,10 @@ pub async fn process_paths( result.to_upload = filtered_closure_vec; if dry_run { - info!("DRY RUN: The following {} paths would be uploaded:", result.to_upload.len()); + info!( + "DRY RUN: The following {} paths would be uploaded:", + result.to_upload.len() + ); for p in result.to_upload { info!(" DRY RUN: Would upload {}", p); } @@ -435,10 +475,7 @@ pub async fn process_paths( if !uploaded_hashes.is_empty() { if let Err(e) = local_cache.add_many_path_hashes(&uploaded_hashes) { - error!( - "Failed to batch add paths to local cache: {:?}", - e - ); + error!("Failed to batch add paths to local cache: {:?}", e); } else { info!( "Successfully added {} paths to local cache.", @@ -449,5 +486,3 @@ pub async fn process_paths( Ok(()) } - - diff --git a/src/nix_utils.rs b/src/nix_utils.rs index 88e2b2f..b800868 100644 --- a/src/nix_utils.rs +++ b/src/nix_utils.rs @@ -8,15 +8,15 @@ use std::path::Path; use futures::stream::StreamExt; use serde_json::Value; -use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; use tokio::sync::Mutex; use tracing::{debug, info, warn}; -use attic::nix_store::NixStore; // Keep NixStore -use attic::signing::NixKeypair; // Added this line +use crate::nix_store::NixStore; +use crate::signing::NixKeypair; use crate::config::{Compression, Config}; use crate::nix_manifest::{self, NarInfo}; @@ -44,7 +44,10 @@ pub async fn filter_out_sig_keys( for sig in *sig_vec { if let Signature::Crypto { key_name, .. } = sig { if keys_to_skip_set.contains(key_name) { - debug!("Skipping path {} because it is signed by {}", path, key_name); + debug!( + "Skipping path {} because it is signed by {}", + path, key_name + ); skip = true; break; } @@ -54,7 +57,10 @@ pub async fn filter_out_sig_keys( if sig_vec.is_empty() { debug!("Keeping path {} because it has no signatures", path); } else { - debug!("Keeping path {} because no skip-keys matched its signatures", path); + debug!( + "Keeping path {} because no skip-keys matched its signatures", + path + ); } } !skip @@ -98,7 +104,7 @@ fn parse_path_signatures_from_json(json_str: &str) -> Result Result>> { // Note: Since NixStore doesn't expose queryAllValidPaths directly in FFI yet, - // we might still need the CLI for this specific bulk operation, + // we might still need the CLI for this specific bulk operation, // or we can iterate if we have a list of paths. // For now, let's keep the CLI for get_all_path_signatures but use NixStore for individual lookups. info!("Fetching and parsing all path signatures using nix CLI..."); @@ -126,7 +132,7 @@ pub async fn get_path_signatures_bulk(paths: &[String]) -> Result Result Result Result String { +pub fn get_narinfo_key(store_path: &crate::nix_store::StorePath) -> String { format!("{}.narinfo", store_path.to_hash().as_str()) } @@ -273,7 +277,8 @@ pub async fn upload_nar_for_path( let nar_size = path_info.nar_size; let sigs = path_info.sigs; - let references: Vec = path_info.references + let references: Vec = path_info + .references .iter() .map(|r| { r.file_name() @@ -305,7 +310,7 @@ pub async fn upload_nar_for_path( // We need to calculate FileHash (of compressed data) and FileSize. // For FileHash and FileSize, we unfortunately need to see the whole compressed stream. // However, we can still stream the upload. We'll capture the hash and size as we stream to S3. - + let compression_task = tokio::spawn(async move { match compression_type { Compression::Xz => { @@ -329,7 +334,7 @@ pub async fn upload_nar_for_path( let hasher_clone = hasher.clone(); let file_size_clone = file_size_atomic.clone(); - + // We'll use a predictable NAR key (like store path hash + nar hash) and calculate FileHash during upload. let stream = async_stream::try_stream! { let mut buffer = vec![0u8; 64 * 1024]; @@ -337,18 +342,23 @@ pub async fn upload_nar_for_path( let n = compressed_rx.read(&mut buffer).await.map_err(|e| anyhow::anyhow!(e))?; if n == 0 { break; } let chunk = &buffer[..n]; - + { let mut h = hasher_clone.lock().await; h.update(chunk); } file_size_clone.fetch_add(n as u64, Ordering::SeqCst); - + yield bytes::Bytes::copy_from_slice(chunk); } }; - let nar_key = format!("nar/{}-{}.nar.{}", store_path_obj.to_hash().as_str(), nar_hash_typed.strip_prefix("sha256:").unwrap(), compression_ext); + let nar_key = format!( + "nar/{}-{}.nar.{}", + store_path_obj.to_hash().as_str(), + nar_hash_typed.strip_prefix("sha256:").unwrap(), + compression_ext + ); uploader.upload_stream(stream, &nar_key).await?; @@ -425,7 +435,7 @@ pub fn generate_nar_info(args: NarInfoArgs) -> Result { ) { if key_path.exists() { let key_file_content = fs::read_to_string(key_path)?; - let nix_keypair = NixKeypair::from_str(&key_file_content)?; + let nix_keypair: NixKeypair = key_file_content.parse()?; let nar_info_for_signing = nix_manifest::from_str::(&content)?; let fingerprint = nar_info_for_signing.fingerprint(); @@ -570,12 +580,19 @@ mod tests { let content = generate_nar_info(nar_info_args)?; - assert!(content.contains("StorePath: /nix/store/hfx4mfjp89kv21whvwcmm2a0bjs0a428-loft-0.1.0")); - assert!(content.contains("URL: nar/sha256:0h4ifpg71s11p3hbafhx3idf3zji7ny8wqnjgvrzmqw9d90d48w4.nar.xz")); + assert!( + content.contains("StorePath: /nix/store/hfx4mfjp89kv21whvwcmm2a0bjs0a428-loft-0.1.0") + ); + assert!(content.contains( + "URL: nar/sha256:0h4ifpg71s11p3hbafhx3idf3zji7ny8wqnjgvrzmqw9d90d48w4.nar.xz" + )); assert!(content.contains("Compression: xz")); - assert!(content.contains("FileHash: sha256:0h4ifpg71s11p3hbafhx3idf3zji7ny8wqnjgvrzmqw9d90d48w4")); + assert!(content + .contains("FileHash: sha256:0h4ifpg71s11p3hbafhx3idf3zji7ny8wqnjgvrzmqw9d90d48w4")); assert!(content.contains("FileSize: 35634208")); - assert!(content.contains("NarHash: sha256:8423d2406a89e3faf37ed2628ebc3d51fee15a1c1d3ab5e0b821e870de759140")); + assert!(content.contains( + "NarHash: sha256:8423d2406a89e3faf37ed2628ebc3d51fee15a1c1d3ab5e0b821e870de759140" + )); assert!(content.contains("NarSize: 123456")); assert!(content.contains("References: 4azvwrcvsj6fy0x66shvl37pasw46k57-nix-2.28.4")); assert!(content.contains("CA: fixed:sha256:1234...")); @@ -637,35 +654,35 @@ mod tests { // Verify XZ magic bytes (7zXZ) assert_eq!(&xz_compressed[0..6], &[0xFD, b'7', b'z', b'X', b'Z', 0x00]); - + // Verify Zstd magic bytes (0x28B52FFD) assert_eq!(&zstd_compressed[0..4], &[0x28, 0xB5, 0x2F, 0xFD]); Ok(()) } - #[test] - fn test_get_narinfo_key() -> Result<()> { - let hash = "hfx4mfjp89kv21whvwcmm2a0bjs0a428"; - - let key = format!("{}.narinfo", hash); - assert_eq!(key, "hfx4mfjp89kv21whvwcmm2a0bjs0a428.narinfo"); - - Ok(()) - } - #[test] + #[test] + fn test_get_narinfo_key() -> Result<()> { + let hash = "hfx4mfjp89kv21whvwcmm2a0bjs0a428"; + + let key = format!("{}.narinfo", hash); + assert_eq!(key, "hfx4mfjp89kv21whvwcmm2a0bjs0a428.narinfo"); + + Ok(()) + } + #[test] fn test_s3_key_hierarchy() -> Result<()> { let hash = "hfx4mfjp89kv21whvwcmm2a0bjs0a428"; let nar_hash_base32 = "8423d2406a89e3faf37ed2628ebc3d51fee15a1c1d3ab5e0b821e870de759140"; let compression_ext = "xz"; - + // This simulates the logic inside upload_nar_for_path let nar_key = format!("nar/{}-{}.nar.{}", hash, nar_hash_base32, compression_ext); assert_eq!(nar_key, "nar/hfx4mfjp89kv21whvwcmm2a0bjs0a428-8423d2406a89e3faf37ed2628ebc3d51fee15a1c1d3ab5e0b821e870de759140.nar.xz"); - + let narinfo_key = format!("{}.narinfo", hash); assert_eq!(narinfo_key, "hfx4mfjp89kv21whvwcmm2a0bjs0a428.narinfo"); - + Ok(()) } } diff --git a/src/pruner.rs b/src/pruner.rs index 1db5894..b574ada 100644 --- a/src/pruner.rs +++ b/src/pruner.rs @@ -47,11 +47,7 @@ impl Pruner { { let size_u64 = size as u64; current_bucket_size_bytes += size_u64; - all_objects.push(( - key, - last_modified.to_chrono_utc(), - size_u64, - )); + all_objects.push((key, last_modified.to_chrono_utc(), size_u64)); } } } @@ -68,11 +64,17 @@ impl Pruner { let retention_days = config_ref.prune_retention_days; if retention_days > 0 { let cutoff_date = Utc::now() - Duration::days(retention_days as i64); - info!("Starting time-based pruning of objects older than {} days...", retention_days); + info!( + "Starting time-based pruning of objects older than {} days...", + retention_days + ); for (key, last_modified, size) in &all_objects { if *last_modified < cutoff_date { - debug!("Pruning object by time: {} (LastModified: {})", key, last_modified); + debug!( + "Pruning object by time: {} (LastModified: {})", + key, last_modified + ); match self.uploader.delete_object(key).await { Ok(_) => { self.remove_from_local_cache(key); @@ -99,14 +101,15 @@ impl Pruner { ); let target_percentage = config_ref.prune_target_percentage.unwrap_or(80); - let target_size_bytes = (max_size_bytes as f64 * (target_percentage as f64 / 100.0)) as u64; + let target_size_bytes = + (max_size_bytes as f64 * (target_percentage as f64 / 100.0)) as u64; // Filter out already deleted and sort by date (oldest first) let mut remaining_objects: Vec<_> = all_objects .into_iter() .filter(|(key, _, _)| !deleted_keys.contains(key)) .collect(); - + remaining_objects.sort_by_key(|k| k.1); for (key, _, size) in remaining_objects { diff --git a/src/s3_uploader.rs b/src/s3_uploader.rs index ef2d956..1c084ce 100644 --- a/src/s3_uploader.rs +++ b/src/s3_uploader.rs @@ -22,9 +22,9 @@ use chrono::{DateTime, Utc}; use tokio::fs::File; use tokio::io::AsyncReadExt; -use bytes::Bytes; use crate::cache_checker::RemoteCacheStorage; use crate::config::S3Config; +use bytes::Bytes; use futures::future::BoxFuture; const MIN_MULTIPART_UPLOAD_SIZE: u64 = 8 * 1024 * 1024; // 8 MB @@ -66,8 +66,8 @@ impl S3Uploader { let sdk_config = config_loader.load().await; - let mut s3_config_builder = aws_sdk_s3::config::Builder::from(&sdk_config) - .force_path_style(true); + let mut s3_config_builder = + aws_sdk_s3::config::Builder::from(&sdk_config).force_path_style(true); let mut extra_headers = config.extra_headers.clone(); @@ -202,7 +202,11 @@ impl S3Uploader { } } - debug!("Found {} .narinfo hashes in S3 bucket '{}'.", all_hashes.len(), self.bucket); + debug!( + "Found {} .narinfo hashes in S3 bucket '{}'.", + all_hashes.len(), + self.bucket + ); Ok(all_hashes) } diff --git a/src/signing.rs b/src/signing.rs new file mode 100644 index 0000000..5bcc5f0 --- /dev/null +++ b/src/signing.rs @@ -0,0 +1,195 @@ +use std::convert::TryInto; +use std::str::FromStr; + +use serde::{de, ser, Deserialize, Serialize}; + +use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; +use displaydoc::Display; +use ed25519_compact::{KeyPair, PublicKey, Signature}; + +use anyhow::Result; + +#[derive(Debug)] +pub struct NixKeypair { + name: String, + keypair: KeyPair, +} + +#[derive(Debug, Clone)] +pub struct NixPublicKey { + name: String, + public: PublicKey, +} + +#[derive(Debug, Display)] +pub enum Error { + /// Signature error: {0}. + SignatureError(ed25519_compact::Error), + /// The string has a wrong key name attached to it: Our name is "{our_name}" and the string has "{string_name}". + WrongKeyName { + our_name: String, + string_name: String, + }, + /// The string lacks a colon separator. + NoColonSeparator, + /// The name portion of the string is blank. + BlankKeyName, + /// The payload portion of the string is blank. + BlankPayload, + /// Base64 decode error: {0}. + Base64DecodeError(base64::DecodeError), + /// Invalid base64 payload length: Expected {expected} ({usage}), got {actual}. + InvalidPayloadLength { + expected: usize, + actual: usize, + usage: &'static str, + }, + /// Invalid signing key name "{0}". + InvalidSigningKeyName(String), +} + +impl std::error::Error for Error {} + +impl NixKeypair { + pub fn generate(name: &str) -> Result { + let keypair = KeyPair::generate(); + validate_name(name)?; + Ok(Self { + name: name.to_string(), + keypair, + }) + } + + pub fn export_keypair(&self) -> String { + format!("{}:{}", self.name, BASE64_STANDARD.encode(*self.keypair)) + } + + pub fn export_public_key(&self) -> String { + format!("{}:{}", self.name, BASE64_STANDARD.encode(*self.keypair.pk)) + } + + pub fn to_public_key(&self) -> NixPublicKey { + NixPublicKey { + name: self.name.clone(), + public: self.keypair.pk, + } + } + + pub fn sign(&self, message: &[u8]) -> String { + let bytes = self.keypair.sk.sign(message, None); + format!("{}:{}", self.name, BASE64_STANDARD.encode(bytes)) + } + + pub fn verify(&self, message: &[u8], signature: &str) -> Result<()> { + let (_, bytes) = decode_string(signature, "signature", Signature::BYTES, Some(&self.name))?; + let bytes: [u8; Signature::BYTES] = bytes.try_into().unwrap(); + let signature = Signature::from_slice(&bytes).map_err(Error::SignatureError)?; + self.keypair + .pk + .verify(message, &signature) + .map_err(|e| Error::SignatureError(e).into()) + } +} + +impl FromStr for NixKeypair { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (name, bytes) = decode_string(s, "keypair", KeyPair::BYTES, None)?; + let keypair = KeyPair::from_slice(&bytes).map_err(Error::SignatureError)?; + Ok(Self { + name: name.to_string(), + keypair, + }) + } +} + +impl<'de> Deserialize<'de> for NixKeypair { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + use de::Error; + String::deserialize(deserializer).and_then(|s| { + s.parse() + .map_err(|e: anyhow::Error| Error::custom(e.to_string())) + }) + } +} + +impl Serialize for NixKeypair { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.export_keypair()) + } +} + +impl NixPublicKey { + pub fn export(&self) -> String { + format!("{}:{}", self.name, BASE64_STANDARD.encode(*self.public)) + } + + pub fn verify(&self, message: &[u8], signature: &str) -> Result<()> { + let (_, bytes) = decode_string(signature, "signature", Signature::BYTES, Some(&self.name))?; + let bytes: [u8; Signature::BYTES] = bytes.try_into().unwrap(); + let signature = Signature::from_slice(&bytes).map_err(Error::SignatureError)?; + self.public + .verify(message, &signature) + .map_err(|e| Error::SignatureError(e).into()) + } +} + +impl FromStr for NixPublicKey { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (name, bytes) = decode_string(s, "public key", PublicKey::BYTES, None)?; + let public = PublicKey::from_slice(&bytes).map_err(Error::SignatureError)?; + Ok(Self { + name: name.to_string(), + public, + }) + } +} + +fn validate_name(name: &str) -> Result<()> { + if name.is_empty() || name.find(':').is_some() { + Err(Error::InvalidSigningKeyName(name.to_string()).into()) + } else { + Ok(()) + } +} + +fn decode_string<'s>( + s: &'s str, + usage: &'static str, + expected_payload_length: usize, + expected_name: Option<&str>, +) -> Result<(&'s str, Vec)> { + let colon = s.find(':').ok_or(Error::NoColonSeparator)?; + let (name, colon_and_payload) = s.split_at(colon); + validate_name(name)?; + if let Some(expected_name) = expected_name { + if expected_name != name { + return Err(Error::WrongKeyName { + our_name: expected_name.to_string(), + string_name: name.to_string(), + } + .into()); + } + } + let bytes = BASE64_STANDARD + .decode(&colon_and_payload[1..]) + .map_err(Error::Base64DecodeError)?; + if bytes.len() != expected_payload_length { + return Err(Error::InvalidPayloadLength { + actual: bytes.len(), + expected: expected_payload_length, + usage, + } + .into()); + } + Ok((name, bytes)) +} diff --git a/treefmt.toml b/treefmt.toml new file mode 100644 index 0000000..7a01e15 --- /dev/null +++ b/treefmt.toml @@ -0,0 +1,18 @@ +[formatter.nixfmt] +command = "nixfmt" +includes = ["*.nix"] + +[formatter.rustfmt] +command = "rustfmt" +options = ["--edition", "2021"] +includes = ["*.rs"] + +[formatter.shfmt] +command = "shfmt" +options = ["-i", "2", "-w"] +includes = ["*.sh"] + +[formatter.prettier] +command = "prettier" +options = ["--write"] +includes = ["*.json", "*.md", "*.yml", "*.yaml"]