diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index e6857d8..f5a46ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,19 @@ +# direnv +.direnv + # Nix result result-* +# pre-commit +.pre-commit-config.yaml + # Rust -/target +**/target + +# VSCode +# We have an opinionated setup for VSCode, so we want to keep the settings in the repo. +!.vscode +!.vscode/settings.json +# Extension recommendations should be kept in the repo. +!.vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..cc8f965 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "mkhl.direnv", + "rust-lang.rust-analyzer" + ], + "unwantedRecommendations": [ + "llvm-vs-code-extensions.vscode-clangd" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..79f97da --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + // Take from PATH, direnv + "rust-analyzer.server.path": "rust-analyzer", + + "[nix]": { + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.detectIndentation": false + }, + + "[rust]": { + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.detectIndentation": false + }, +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..307a96b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,90 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- `primop::RecoverableError` for primop errors that should not be memoized in the thunk, allowing retry on next force. Required by Nix >= 2.34 ([release note](https://nix.dev/manual/nix/2.34/release-notes/rl-2.34.html#c-api-changes)) for recoverable errors to remain recoverable, as Nix 2.34 memoizes errors by default. + +## [0.2.0] - 2026-01-13 + +### Added + +- Workaround for automatic C library input propagation in downstream Nix builds. ([#27] by [@roberth]) +- `EvalStateBuilder::load_ambient_settings()` to control whether global Nix settings are loaded. ([#36] by [@roberth]) + +### Fixed + +- Path coercion failing with "path does not exist" errors due to missing `eval_state_builder_load()` call. ([#36] by [@aanderse]) + +### Changed + +- Split `nix-bindings-util-sys` (which contained all low-level FFI bindings) into separate per-library `*-sys` crates. ([#27] by [@Ericson2314]) + This allows downstream crates to depend on just the low-level bindings they need without pulling in higher-level crates. + +## [0.1.0] - 2026-01-12 + +Initial release, extracted from the [nixops4 repository](https://github.com/nixops4/nixops4). + +### Added + +- `nix-bindings-store`: Rust bindings for Nix store operations + - Store opening (auto, from URI, from environment) + - Store path parsing and manipulation + - `Store::get_fs_closure` ([#12] by [@RossComputerGuy], [@roberth]) + - `Clone` for `Derivation` ([#25] by [@Ericson2314]) + - Store deduplication workaround for [nix#11979] + - aarch64 ABI support ([#26] by [@RossComputerGuy]) +- `nix-bindings-expr`: Rust bindings for Nix expression evaluation + - `EvalState` for evaluating Nix expressions + - Value creation (int, string, attrs, thunks, primops, etc.) + - Value inspection/extraction (`require_*` functions) + - Attribute selection and manipulation + - Thread registration for GC safety +- `nix-bindings-fetchers`: Rust bindings for Nix fetchers +- `nix-bindings-flake`: Rust bindings for Nix flake operations + - Flake locking + - Flake overriding +- `nix-bindings-util`: Shared utilities + - Context management for Nix C API error handling + - Settings access +- `nix-bindings-util-sys`: Low-level FFI bindings for all Nix C libraries + +### Contributors + +Thanks to everyone who contributed to the initial development, some of whom may not be listed with individual changes above: + +- [@aanderse] +- [@Ericson2314] +- [@ErinvanderVeen] +- [@numinit] +- [@prednaz] +- [@Radvendii] +- [@roberth] +- [@RossComputerGuy] + + + +[@aanderse]: https://github.com/aanderse +[@Ericson2314]: https://github.com/Ericson2314 +[@ErinvanderVeen]: https://github.com/ErinvanderVeen +[@numinit]: https://github.com/numinit +[@prednaz]: https://github.com/prednaz +[@Radvendii]: https://github.com/Radvendii +[@roberth]: https://github.com/roberth +[@RossComputerGuy]: https://github.com/RossComputerGuy + +[#12]: https://github.com/nixops4/nix-bindings-rust/pull/12 +[#25]: https://github.com/nixops4/nix-bindings-rust/pull/25 +[#26]: https://github.com/nixops4/nix-bindings-rust/pull/26 +[#27]: https://github.com/nixops4/nix-bindings-rust/pull/27 +[#36]: https://github.com/nixops4/nix-bindings-rust/pull/36 +[Unreleased]: https://github.com/nixops4/nix-bindings-rust/compare/0.2.0...HEAD +[0.2.0]: https://github.com/nixops4/nix-bindings-rust/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/nixops4/nix-bindings-rust/releases/tag/0.1.0 +[nix#11979]: https://github.com/NixOS/nix/issues/11979 diff --git a/Cargo.lock b/Cargo.lock index 9af7062..7e1d1f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,35 +12,81 @@ dependencies = [ ] [[package]] -name = "bindgen" -version = "0.72.1" +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools", + "lazy_static", + "lazycell", "log", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", + "which", ] [[package]] name = "bitflags" -version = "2.11.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] [[package]] name = "cc" -version = "1.2.56" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -61,6 +107,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -73,14 +131,120 @@ dependencies = [ ] [[package]] -name = "doxygen-bindgen" -version = "0.1.3" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ba4ed6eedf7f4ace1632149d8f0e8a65a480534024d65a7c3b9daacdedbad3" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "yap", + "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -88,44 +252,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "futures-core" -version = "0.3.32" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "futures-task" -version = "0.3.32" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "futures-util" -version = "0.3.32" +name = "find-msvc-tools" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", ] [[package]] @@ -135,19 +315,182 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "itertools" -version = "0.13.0" +name = "harmonia-store-core" +version = "0.0.0-alpha.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "983e1ba97729aded802a93f0fb5b61e2eae09efee77c9fa3d62c76e60b621bf0" +dependencies = [ + "bytes", + "data-encoding", + "derive_more", + "harmonia-utils-base-encoding", + "harmonia-utils-hash", + "num_enum", + "ring", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tokio", + "tracing", + "zerocopy", +] + +[[package]] +name = "harmonia-utils-base-encoding" +version = "0.0.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e8a868920d6ab01dfa2c5abce20ecd2f6d7e1f3c6a9e1941426883ce15a52c" +dependencies = [ + "data-encoding", + "derive_more", + "serde", +] + +[[package]] +name = "harmonia-utils-hash" +version = "0.0.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301feeb6f34b380ab13df6bb264c461704dbbb7f81b45b46c406c0d8c2d8d7f4" +dependencies = [ + "data-encoding", + "derive_more", + "harmonia-utils-base-encoding", + "md5", + "ring", + "serde", + "thiserror", + "tokio", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] -name = "libc" -version = "0.2.183" +name = "itoa" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libloading" @@ -160,13 +503,16 @@ dependencies = [ ] [[package]] -name = "lock_api" -version = "0.4.14" +name = "linux-raw-sys" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" @@ -175,10 +521,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "memchr" -version = "2.8.0" +name = "md5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minimal-lexical" @@ -187,24 +539,143 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "nixide" -version = "0.1.0" +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "nixide-sys", - "serial_test", - "stdext", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "nixide-sys" -version = "0.1.0" +name = "nix-bindings-bdwgc-sys" +version = "0.2.1" +dependencies = [ + "bindgen", + "pkg-config", +] + +[[package]] +name = "nix-bindings-expr" +version = "0.2.1" +dependencies = [ + "anyhow", + "cstr", + "ctor", + "nix-bindings-bdwgc-sys", + "nix-bindings-expr-sys", + "nix-bindings-store", + "nix-bindings-store-sys", + "nix-bindings-util", + "nix-bindings-util-sys", + "pkg-config", + "tempfile", +] + +[[package]] +name = "nix-bindings-expr-sys" +version = "0.2.1" +dependencies = [ + "bindgen", + "nix-bindings-store-sys", + "nix-bindings-util-sys", + "pkg-config", +] + +[[package]] +name = "nix-bindings-fetchers" +version = "0.2.1" +dependencies = [ + "anyhow", + "cstr", + "ctor", + "nix-bindings-fetchers-sys", + "nix-bindings-store", + "nix-bindings-util", + "tempfile", +] + +[[package]] +name = "nix-bindings-fetchers-sys" +version = "0.2.1" +dependencies = [ + "bindgen", + "nix-bindings-util-sys", + "pkg-config", +] + +[[package]] +name = "nix-bindings-flake" +version = "0.2.1" +dependencies = [ + "anyhow", + "cstr", + "ctor", + "nix-bindings-expr", + "nix-bindings-fetchers", + "nix-bindings-flake-sys", + "nix-bindings-store", + "nix-bindings-util", + "tempfile", +] + +[[package]] +name = "nix-bindings-flake-sys" +version = "0.2.1" +dependencies = [ + "bindgen", + "nix-bindings-bdwgc-sys", + "nix-bindings-expr-sys", + "nix-bindings-fetchers-sys", + "nix-bindings-store-sys", + "nix-bindings-util-sys", + "pkg-config", +] + +[[package]] +name = "nix-bindings-store" +version = "0.2.1" +dependencies = [ + "anyhow", + "ctor", + "harmonia-store-core", + "hex-literal", + "nix-bindings-store-sys", + "nix-bindings-util", + "nix-bindings-util-sys", + "pkg-config", + "serde_json", + "tempfile", + "zerocopy", +] + +[[package]] +name = "nix-bindings-store-sys" +version = "0.2.1" +dependencies = [ + "bindgen", + "nix-bindings-util-sys", + "pkg-config", + "zerocopy", +] + +[[package]] +name = "nix-bindings-util" +version = "0.2.1" +dependencies = [ + "anyhow", + "ctor", + "nix-bindings-util-sys", +] + +[[package]] +name = "nix-bindings-util-sys" +version = "0.2.1" dependencies = [ "bindgen", - "cc", - "doxygen-bindgen", "pkg-config", - "serial_test", ] [[package]] @@ -217,40 +688,54 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "once_cell" -version = "1.21.4" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[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", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pin-project-lite" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pkg-config" @@ -258,6 +743,31 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[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-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -269,27 +779,44 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ - "bitflags", + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -299,9 +826,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -310,58 +837,170 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] [[package]] name = "rustc-hash" -version = "2.1.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "scc" -version = "2.4.0" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "sdd", + "semver", ] [[package]] -name = "scopeguard" +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" 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.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ - "futures-executor", - "futures-util", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] -name = "serial_test_derive" -version = "3.4.0" +name = "semver" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", + "serde_derive", +] + +[[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 = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", "proc-macro2", "quote", "syn", @@ -374,28 +1013,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "slab" -version = "0.4.12" +name = "signal-hook-registry" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] [[package]] -name = "smallvec" -version = "1.15.1" +name = "socket2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] [[package]] -name = "stdext" -version = "0.3.3" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4af28eeb7c18ac2dbdb255d40bee63f203120e1db6b0024b177746ebec7049c1" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -403,10 +1050,287 @@ dependencies = [ ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "tempfile" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "windows-link" @@ -415,7 +1339,225 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "yap" -version = "0.12.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe269e7b803a5e8e20cbd97860e136529cd83bf2c9c6d37b142467e7e1f051f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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 = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Cargo.toml b/Cargo.toml index 4b249ed..67da147 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,24 @@ [workspace] -resolver = "3" -members = [ - "nixide", - "nixide-sys" +members = [ + "nix-bindings-bdwgc-sys", + "nix-bindings-util-sys", + "nix-bindings-store-sys", + "nix-bindings-expr-sys", + "nix-bindings-fetchers-sys", + "nix-bindings-flake-sys", + "nix-bindings-expr", + "nix-bindings-fetchers", + "nix-bindings-flake", + "nix-bindings-store", + "nix-bindings-util", ] +resolver = "2" -[workspace.dependencies] -cc = "1.2.56" -pkg-config = "0.3.32" +[workspace.lints.rust] +warnings = "deny" +dead-code = "allow" -# 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 +[workspace.lints.clippy] +type-complexity = "allow" +# We're still trying to make Nix more thread-safe, want forward-compat +arc-with-non-send-sync = "allow" diff --git a/LICENSE b/LICENSE index 4aee4d1..f6683e7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,232 +1,501 @@ -GNU GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright © 2007 Free Software Foundation, Inc. - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -Preamble - -The GNU General Public License is a free, copyleft license for software and other kinds of works. - -The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. - -Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. - -Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and modification follow. - -TERMS AND CONDITIONS - -0. Definitions. - -“This License” refers to version 3 of the GNU General Public License. - -“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. - -“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. - -To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. - -A “covered work” means either the unmodified Program or a work based on the Program. - -To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. - -To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. - -An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. - -1. Source Code. -The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. - -A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. - -The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. - -The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -2. Basic Permissions. -All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. -No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. - -4. Conveying Verbatim Copies. -You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. - -5. Conveying Modified Source Versions. -You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. - - c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. - -A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. - -6. Conveying Non-Source Forms. -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: - - a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. - - d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. - -A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. - -“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. - -7. Additional Terms. -“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or authors of the material; or - - e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. - -All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. - -8. Termination. -You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). - -However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. - -9. Acceptance Not Required for Having Copies. -You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. - -10. Automatic Licensing of Downstream Recipients. -Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. - -An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. - -11. Patents. -A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. - -A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. - -In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. - -A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - -13. Use with the GNU Affero General Public License. -Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. - -14. Revised Versions of this License. -The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. - -Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. - -15. Disclaimer of Warranty. -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. Limitation of Liability. -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -17. Interpretation of Sections 15 and 16. -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. - - nixide - Copyright (C) 2026 luminary - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program. If not, see . + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . Also add information on how to contact you by electronic and paper mail. -If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: - nixide Copyright (C) 2026 luminary - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. -The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + , 1 April 1990 + Moe Ghoul, President of Vice -You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . - -The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . +That's all there is to it! diff --git a/README.md b/README.md index f40390a..e02b585 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,225 @@ -# nixide +# Nix Bindings for Rust -rust wrapper for libnix :3 \ No newline at end of file +Rust bindings for the Nix [C API], providing safe, idiomatic Rust interfaces to Nix's core functionality including store operations, expression evaluation, and flake management. + +## Overview + +This workspace provides multiple crates that wrap different layers of the Nix C API: + +- **`nix-bindings-util`** - Utility types and helpers (settings, context, version detection, string handling) +- **`nix-bindings-store`** - Store operations (paths, derivations, store management) +- **`nix-bindings-expr`** - Expression evaluation and type extraction +- **`nix-bindings-flake`** - Flake operations +- **`nix-bindings-fetchers`** - Fetcher functionality (requires Nix ≥ 2.29) + +The `*-sys` crates contain generated FFI bindings and are not intended for direct use. + +## Features + +- **Nix evaluation** - Evaluate Nix expressions and create and extract values +- **Store integration** - Interact with the Nix store, manage paths, build derivations +- **Threading** - GC registration and memory management via `Drop` +- **Lazy evaluation** - Fine-grained control over evaluation strictness +- **Version compatibility** - Conditional compilation for different Nix versions + +## Quick Start + +Add the crates you need to your `Cargo.toml`: + +```toml +[dependencies] +nix-bindings-store = { git = "https://github.com/nixops4/nix-bindings-rust" } +nix-bindings-expr = { git = "https://github.com/nixops4/nix-bindings-rust" } +``` + +Basic example: + +```rust +use nix_bindings_expr::eval_state::{EvalState, init, gc_register_my_thread}; +use nix_bindings_store::store::Store; +use std::collections::HashMap; + +fn main() -> anyhow::Result<()> { + // Initialize Nix library and register thread with GC + init()?; + let guard = gc_register_my_thread()?; + + // Open a store connection and create an evaluation state + let store = Store::open(None, HashMap::new())?; + let mut eval_state = EvalState::new(store, [])?; + + // Evaluate a Nix expression + let value = eval_state.eval_from_string("[1 2 3]", "")?; + + // Extract typed values + let elements: Vec<_> = eval_state.require_list_strict(&value)?; + for element in elements { + let num = eval_state.require_int(&element)?; + println!("Element: {}", num); + } + + drop(guard); + Ok(()) +} +``` + +## Usage Examples + +### Evaluating Nix Expressions + +```rust +use nix_bindings_expr::eval_state::EvalState; + +// Evaluate and extract different types +let int_value = eval_state.eval_from_string("42", "")?; +let num = eval_state.require_int(&int_value)?; + +let str_value = eval_state.eval_from_string("\"hello\"", "")?; +let text = eval_state.require_string(&str_value)?; + +let attr_value = eval_state.eval_from_string("{ x = 1; y = 2; }", "")?; +let attrs = eval_state.require_attrs(&attr_value)?; +``` + +### Working with Lists + +```rust +let list_value = eval_state.eval_from_string("[1 2 3 4 5]", "")?; + +// Lazy: check size without evaluating elements +let size = eval_state.require_list_size(&list_value)?; + +// Selective: evaluate only accessed elements +if let Some(first) = eval_state.require_list_select_idx_strict(&list_value, 0)? { + let value = eval_state.require_int(&first)?; +} + +// Strict: evaluate all elements +let all_elements: Vec<_> = eval_state.require_list_strict(&list_value)?; +``` + +### Thread Safety + +Before using `EvalState` in a thread, register with the garbage collector: + +```rust +use nix_bindings_expr::eval_state::{init, gc_register_my_thread}; + +init()?; // Once per process +let guard = gc_register_my_thread()?; // Once per thread +// ... use EvalState ... +drop(guard); // Unregister when done +``` + +For more examples, see the documentation in each crate's source code. + +## Nix Version Compatibility + +The crates use conditional compilation to support multiple Nix versions: + +- **`nix-bindings-fetchers`** requires Nix ≥ 2.29 +- Some features in other crates require specific Nix versions + +The build system automatically detects the Nix version and enables appropriate features. + +## Integration with Nix Projects + +These crates use [nix-cargo-integration] for seamless integration with Nix builds. To use them in your Nix project: + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + nix-cargo-integration.url = "github:90-008/nix-cargo-integration"; + nix-bindings-rust.url = "github:nixops4/nix-bindings-rust"; + }; + + outputs = inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + inputs.nix-cargo-integration.flakeModule + inputs.nix-bindings-rust.modules.flake.default + ]; + + perSystem = { config, pkgs, ... }: { + # Optional: override Nix package + nix-bindings-rust.nixPackage = pkgs.nix; + + nci.projects."myproject" = { + depsDrvConfig = { + imports = [ config.nix-bindings-rust.nciBuildConfig ]; + }; + }; + }; + }; +} +``` + +See the [nix-cargo-integration documentation][nix-cargo-integration] for more options. + +## Development + +### Getting Started + +```console +$ nix develop +``` + +### Building + +```bash +# Build specific crates (release mode) +nix build .#nix-bindings-store-release +nix build .#nix-bindings-expr-release + +# Build with Cargo (in dev shell) +cargo build +cargo build --release +``` + +### Testing + +```bash +# Run tests for specific crates via Nix (recommended - includes proper store setup) +nix build .#checks.x86_64-linux.nix-bindings-store-tests +nix build .#checks.x86_64-linux.nix-bindings-expr-tests + +# Run all checks (tests + clippy + formatting) +nix flake check + +# Run tests with Cargo (in dev shell) +cargo test + +# Run specific test +cargo test test_name +``` + +### Memory Testing + +For FFI memory leak testing with valgrind, see [doc/hacking/test-ffi.md](doc/hacking/test-ffi.md). + +### Code Formatting + +```bash +treefmt +``` + +### IDE Setup + +For VSCode, load the dev shell via Nix Env Selector extension or direnv. + +## Documentation + +- [API Reference](https://nixops4.github.io/nix-bindings-rust/development/) +- [Changelog](CHANGELOG.md) +- [Nix C API Reference][C API] +- [nix-cargo-integration][nix-cargo-integration] +- [Hacking Guide](doc/hacking/test-ffi.md) + +## License + +See [LICENSE](LICENSE) file in the repository. + +[C API]: https://nix.dev/manual/nix/latest/c-api.html +[nix-cargo-integration]: https://github.com/90-008/nix-cargo-integration#readme diff --git a/TODO.md b/TODO.md deleted file mode 100644 index ffeff7f..0000000 --- a/TODO.md +++ /dev/null @@ -1,24 +0,0 @@ -- [ ] rename `AsInnerPtr::as_ptr` to `AsInnerPtr::as_mut_ptr` - -- [ ] add NixError::from_nonnull that replaces calls to NonNull::new(...).ok_or(...) -- [ ] replace all `use nixide_sys as sys;` -> `use crate::sys;` -- [ ] store NonNull pointers in structs! -- [ ] improve documentation situation on context.rs - -- [ ] rename `as_ptr()` to `as_inner_ptr()` or `inner_ptr()`? -- [ ] ^^^ this fn should be added to a trait (maybe just `trait NixStructWrapper : AsPtr { ... }`) -- [ ] ^^^ also make `as_ptr()` public - -- [ ] add mutexs and make the library thread safe!! - -- [ ] grep all `self.inner.as_ptr()` calls and replace them with `self.as_ptr()` - - -- [ ] `ErrorContext::peak` should return `Result<(), NixideError>` **not** `Option` -- [ ] `self.expect_type` should instead be a macro to preserve the trace macro location - -- [ ] make `Value` an enum instead because like duhh - -- [ ] ensure we're always calling `ctx.peak()` unless it's ACTUALLY not necessary - -- [ ] replace *most* calls to `ErrorContext::peak()` with `ErrorContext::pop()` diff --git a/bindgen-gcc.sh b/bindgen-gcc.sh new file mode 100644 index 0000000..923a278 --- /dev/null +++ b/bindgen-gcc.sh @@ -0,0 +1,18 @@ + +# Rust bindgen uses Clang to generate bindings, but that means that it can't +# find the "system" or compiler headers when the stdenv compiler is GCC. +# This script tells it where to find them. + +echo "Extending BINDGEN_EXTRA_CLANG_ARGS with system include paths..." 2>&1 +BINDGEN_EXTRA_CLANG_ARGS="${BINDGEN_EXTRA_CLANG_ARGS:-}" +export BINDGEN_EXTRA_CLANG_ARGS +include_paths=$( + echo | $NIX_CC_UNWRAPPED -v -E -x c - 2>&1 \ + | awk '/#include <...> search starts here:/{flag=1;next} \ + /End of search list./{flag=0} \ + flag==1 {print $1}' +) +for path in $include_paths; do + echo " - $path" 2>&1 + BINDGEN_EXTRA_CLANG_ARGS="$BINDGEN_EXTRA_CLANG_ARGS -I$path" +done diff --git a/dev/flake-module.nix b/dev/flake-module.nix new file mode 100644 index 0000000..4ca895a --- /dev/null +++ b/dev/flake-module.nix @@ -0,0 +1,259 @@ +{ + inputs, + withSystem, + ... +}: +{ + imports = [ + inputs.pre-commit-hooks-nix.flakeModule + inputs.hercules-ci-effects.flakeModule + inputs.treefmt-nix.flakeModule + ]; + perSystem = + { + config, + pkgs, + inputs', + ... + }: + { + nix-bindings-rust.nixPackage = inputs'.nix.packages.default; + + treefmt = { + # Used to find the project root + projectRootFile = "flake.lock"; + + programs.rustfmt = { + enable = true; + edition = "2021"; + }; + programs.nixfmt.enable = true; + programs.deadnix.enable = true; + #programs.clang-format.enable = true; + }; + + pre-commit.settings.hooks.treefmt.enable = true; + # Temporarily disable rustfmt due to configuration issues + # pre-commit.settings.hooks.rustfmt.enable = true; + pre-commit.settings.settings.rust.cargoManifestPath = "./Cargo.toml"; + + # Check that we're using ///-style doc comments in Rust code. + # + # Unfortunately, rustfmt won't do this for us yet - at least not + # without nightly, and it might do too much. + pre-commit.settings.hooks.rust-doc-comments = { + enable = true; + files = "\\.rs$"; + entry = "${pkgs.writeScript "rust-doc-comments" '' + #!${pkgs.runtimeShell} + set -uxo pipefail + grep -n -C3 --color=always -F '/**' "$@" + r=$? + set -e + if [ $r -eq 0 ]; then + echo "Please replace /**-style comments by /// style comments in Rust code." + exit 1 + fi + ''}"; + }; + + # Combined rustdoc for all crates with cross-linking. + # NOTE: nci.outputs.nix-bindings.docs uses doc-merge which doesn't support + # rustdoc's new sharded search index format (Rust 1.78+). + # See https://github.com/90-008/nix-cargo-integration/issues/198 + # Instead, we build all workspace crates together so rustdoc can link them. + packages.docs = + let + # Use nix-bindings-flake (has most transitive deps) as base + base = config.nci.outputs.nix-bindings-flake.packages.release; + crates = [ + "nix-bindings-bdwgc-sys" + "nix-bindings-util-sys" + "nix-bindings-util" + "nix-bindings-store-sys" + "nix-bindings-store" + "nix-bindings-expr-sys" + "nix-bindings-expr" + "nix-bindings-fetchers-sys" + "nix-bindings-fetchers" + "nix-bindings-flake-sys" + "nix-bindings-flake" + ]; + packageFlags = pkgs.lib.concatMapStringsSep " " (c: "-p ${c}") crates; + in + (base.extendModules { + modules = [ + { + mkDerivation = { + # Build docs for all crates together (enabling cross-crate linking) + buildPhase = pkgs.lib.mkForce '' + cargo doc $cargoBuildFlags --no-deps --profile $cargoBuildProfile ${packageFlags} + ''; + checkPhase = pkgs.lib.mkForce ":"; + installPhase = pkgs.lib.mkForce '' + mv target/$CARGO_BUILD_TARGET/doc $out + + # Find rustdoc assets (have hashes in filenames) + find_asset() { + local pattern="$1" + local matches=($out/static.files/$pattern) + if [[ ''${#matches[@]} -ne 1 || ! -e "''${matches[0]}" ]]; then + echo "Expected exactly one match for $pattern, found: ''${matches[*]}" >&2 + exit 1 + fi + basename "''${matches[0]}" + } + rustdoc_css=$(find_asset 'rustdoc-*.css') + normalize_css=$(find_asset 'normalize-*.css') + storage_js=$(find_asset 'storage-*.js') + + cat > $out/index.html < + + + + nix-bindings-rust + + + + + + +

nix-bindings-rust

+

Rust bindings for the Nix C API

+

Crates

+ +
+

Low-level bindings

+

+ These -sys crates provide raw FFI bindings generated by + bindgen. + They expose the C API directly without safety wrappers. + Most users should prefer the high-level crates above. +

+ +
+ + + EOF + ''; + }; + } + ]; + }).config.public; + + devShells.default = pkgs.mkShell { + name = "nix-bindings-devshell"; + strictDeps = true; + inputsFrom = [ config.nci.outputs.nix-bindings.devShell ]; + inherit (config.nci.outputs.nix-bindings.devShell.env) + LIBCLANG_PATH + NIX_CC_UNWRAPPED + ; + NIX_DEBUG_INFO_DIRS = + let + # TODO: add to Nixpkgs lib + getDebug = + pkg: + if pkg ? debug then + pkg.debug + else if pkg ? lib then + pkg.lib + else + pkg; + in + "${getDebug config.packages.nix}/lib/debug"; + buildInputs = [ + config.packages.nix + ]; + nativeBuildInputs = [ + config.treefmt.build.wrapper + + pkgs.rust-analyzer + pkgs.nixfmt + pkgs.rustfmt + pkgs.pkg-config + pkgs.clang-tools # clangd + pkgs.valgrind + pkgs.gdb + pkgs.hci + # TODO: set up cargo-valgrind in shell and build + # currently both this and `cargo install cargo-valgrind` + # produce a binary that says ENOENT. + # pkgs.cargo-valgrind + ]; + shellHook = '' + ${config.pre-commit.shellHook} + echo 1>&2 "Welcome to the development shell!" + ''; + # rust-analyzer needs a NIX_PATH for some reason + NIX_PATH = "nixpkgs=${inputs.nixpkgs}"; + }; + }; + herculesCI = + hci@{ lib, ... }: + { + ciSystems = [ "x86_64-linux" ]; + onPush.default.outputs = { + effects.pushDocs = lib.optionalAttrs (hci.config.repo.branch == "main") ( + withSystem "x86_64-linux" ( + { config, hci-effects, ... }: + hci-effects.gitWriteBranch { + git.checkout.remote.url = hci.config.repo.remoteHttpUrl; + git.checkout.forgeType = "github"; + git.checkout.user = "x-access-token"; + git.update.branch = "gh-pages"; + contents = config.packages.docs; + destination = "development"; # directory + } + ) + ); + }; + }; + hercules-ci.flake-update = { + enable = true; + baseMerge.enable = true; + autoMergeMethod = "merge"; + when = { + dayOfMonth = 1; + }; + flakes = { + "." = { }; + "dev" = { }; + }; + }; + hercules-ci.cargo-publish = { + enable = true; + secretName = "crates-io"; + assertVersions = true; + }; + flake = { }; +} diff --git a/dev/flake.lock b/dev/flake.lock new file mode 100644 index 0000000..1604bb7 --- /dev/null +++ b/dev/flake.lock @@ -0,0 +1,143 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "hercules-ci-effects", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "type": "github" + }, + "original": { + "id": "flake-parts", + "type": "indirect" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "hercules-ci-effects": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1771131391, + "narHash": "sha256-HPBNYf7HiKtBVy7/69vKpLYHX6wTcUxndxmybzDlXP8=", + "owner": "hercules-ci", + "repo": "hercules-ci-effects", + "rev": "0b152e0f7c5cc265a529cd63374b80e2771b207b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "hercules-ci-effects", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks-nix": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [] + }, + "locked": { + "lastModified": 1772024342, + "narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "hercules-ci-effects": "hercules-ci-effects", + "pre-commit-hooks-nix": "pre-commit-hooks-nix", + "treefmt-nix": "treefmt-nix" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [] + }, + "locked": { + "lastModified": 1770228511, + "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/dev/flake.nix b/dev/flake.nix new file mode 100644 index 0000000..09a06a1 --- /dev/null +++ b/dev/flake.nix @@ -0,0 +1,11 @@ +{ + description = "dependencies only"; + inputs = { + pre-commit-hooks-nix.url = "github:cachix/pre-commit-hooks.nix"; + pre-commit-hooks-nix.inputs.nixpkgs.follows = ""; + hercules-ci-effects.url = "github:hercules-ci/hercules-ci-effects"; + treefmt-nix.url = "github:numtide/treefmt-nix"; + treefmt-nix.inputs.nixpkgs.follows = ""; + }; + outputs = { ... }: { }; +} diff --git a/doc/hacking/test-ffi.md b/doc/hacking/test-ffi.md new file mode 100644 index 0000000..c19c485 --- /dev/null +++ b/doc/hacking/test-ffi.md @@ -0,0 +1,25 @@ + +# Testing FFI code + +If `cargo-valgrind` is broken, you may run `valgrind` manually. + +1. `cargo test -v` +2. find the relevant test suite executable in the log + - example: `/home/user/src/nix-bindings-rust/target/debug/deps/nix_util-036ec381a9e3fd6d` +3. `valgrind --leak-check=full ` +4. check that + - `definitely lost: 0 bytes in 0 blocks` + +## Paranoid check + +Although normal valgrind tends to catch things, you may choose to enable `--show-leak-kinds=all`. +This will print a few false positive. + +Acceptable leaks are those involving (and this may be Linux-specific) +- `call_init`: static initializers + - `nix::GlobalConfig::Register::Register` + - `_GLOBAL__sub_I_logging.cc` + - ... +- `new`: a leak in the rust test framework + +When in doubt, compare the log to a run with your new test case commented out. diff --git a/doc/maintainers/release.md b/doc/maintainers/release.md new file mode 100644 index 0000000..2d04d96 --- /dev/null +++ b/doc/maintainers/release.md @@ -0,0 +1,23 @@ +# Release process + +This project uses simple tags, that trigger a release of all crates using Hercules CI. +Based on the [HCI Effects cargo publish workflow]. + +## Steps + +1. Create a `release` branch +2. Decide the version bump (patch for fixes, minor for features, major for breaking changes) +3. Update `CHANGELOG.md`: make sure the Unreleased section is up to date, then change it to the new version and release date +4. Open a draft release PR and wait for CI to pass +5. Create and push a tag matching the version +6. Add a new Unreleased section to `CHANGELOG.md` +7. Bump version in all `Cargo.toml` files to the next patch version (e.g., `0.2.0` → `0.2.1`) + and run `cargo update --workspace` to update `Cargo.lock`, + so that `cargo publish --dry-run` passes on subsequent commits +8. Merge the release PR + +--- + +Dissatisfied with the coarse grained release process? Complain to @roberth and he'll get it done for you. + +[HCI Effects cargo publish workflow]: https://docs.hercules-ci.com/hercules-ci-effects/reference/flake-parts/cargo-publish/#_releasing_a_version diff --git a/docs/ref.md b/docs/ref.md deleted file mode 100644 index db53d00..0000000 --- a/docs/ref.md +++ /dev/null @@ -1,3 +0,0 @@ -# Nix Bindgen-rs References -- https://github.com/NotAShelf/nix-bindings -- https://github.com/nixops4/nix-bindings-rust diff --git a/flake.lock b/flake.lock index b306085..da94010 100644 --- a/flake.lock +++ b/flake.lock @@ -1,39 +1,414 @@ { "nodes": { - "nixpkgs": { + "crane": { + "flake": false, "locked": { - "lastModified": 1773222311, - "narHash": "sha256-BHoB/XpbqoZkVYZCfXJXfkR+GXFqwb/4zbWnOr2cRcU=", + "lastModified": 1758758545, + "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", + "owner": "ipetkov", + "repo": "crane", + "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "ref": "v0.21.1", + "repo": "crane", + "type": "github" + } + }, + "dream2nix": { + "inputs": { + "nixpkgs": [ + "nix-cargo-integration", + "nixpkgs" + ], + "purescript-overlay": "purescript-overlay", + "pyproject-nix": "pyproject-nix" + }, + "locked": { + "lastModified": 1765953015, + "narHash": "sha256-5FBZbbWR1Csp3Y2icfRkxMJw/a/5FGg8hCXej2//bbI=", + "owner": "nix-community", + "repo": "dream2nix", + "rev": "69eb01fa0995e1e90add49d8ca5bcba213b0416f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "dream2nix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", "owner": "NixOS", - "repo": "nixpkgs", - "rev": "0590cd39f728e129122770c029970378a79d076a", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.11", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks-nix": { + "inputs": { + "flake-compat": [ + "nix" + ], + "gitignore": [ + "nix" + ], + "nixpkgs": [ + "nix", + "nixpkgs" + ], + "nixpkgs-stable": [ + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1734279981, + "narHash": "sha256-NdaCraHPp8iYMWzdXAt5Nv6sA3MUzlCiGiR586TCwo0=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "aa9f40c906904ebd83da78e7f328cd8aeaeae785", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "mk-naked-shell": { + "flake": false, + "locked": { + "lastModified": 1681286841, + "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", + "owner": "90-008", + "repo": "mk-naked-shell", + "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", + "type": "github" + }, + "original": { + "owner": "90-008", + "repo": "mk-naked-shell", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-compat": "flake-compat", + "flake-parts": "flake-parts_2", + "git-hooks-nix": "git-hooks-nix", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-23-11": "nixpkgs-23-11", + "nixpkgs-regression": "nixpkgs-regression" + }, + "locked": { + "lastModified": 1772224943, + "narHash": "sha256-jJIlRLPPVYu860MVFx4gsRx3sskmLDSRWXXue5tYncw=", + "owner": "NixOS", + "repo": "nix", + "rev": "0acd0566e85e4597269482824711bcde7b518600", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nix", + "type": "github" + } + }, + "nix-cargo-integration": { + "inputs": { + "crane": "crane", + "dream2nix": "dream2nix", + "mk-naked-shell": "mk-naked-shell", + "nixpkgs": [ + "nixpkgs" + ], + "parts": "parts", + "rust-overlay": "rust-overlay", + "treefmt": "treefmt" + }, + "locked": { + "lastModified": 1772260057, + "narHash": "sha256-NaUqM0i6XIGdgRNxxQ9sfgCAVeE2Ko9rz7e19RsNUKw=", + "owner": "90-008", + "repo": "nix-cargo-integration", + "rev": "c783c5dff02c06f2af6226d4dd4d494542d0a4d2", + "type": "github" + }, + "original": { + "owner": "90-008", + "repo": "nix-cargo-integration", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1772198003, + "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", + "owner": "NixOS", "repo": "nixpkgs", + "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-23-11": { + "locked": { + "lastModified": 1717159533, + "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1769909678, + "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "72716169fe93074c333e8d0173151350670b824c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-regression": { + "locked": { + "lastModified": 1643052045, + "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + } + }, + "parts": { + "inputs": { + "nixpkgs-lib": [ + "nix-cargo-integration", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "purescript-overlay": { + "inputs": { + "flake-compat": "flake-compat_2", + "nixpkgs": [ + "nix-cargo-integration", + "dream2nix", + "nixpkgs" + ], + "slimlock": "slimlock" + }, + "locked": { + "lastModified": 1728546539, + "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=", + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "nix-cargo-integration", + "dream2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1763017646, + "narHash": "sha256-Z+R2lveIp6Skn1VPH3taQIuMhABg1IizJd8oVdmdHsQ=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "47bd6f296502842643078d66128f7b5e5370790c", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", "type": "github" } }, "root": { "inputs": { - "nixpkgs": "nixpkgs", - "systems": "systems" + "flake-parts": "flake-parts", + "nix": "nix", + "nix-cargo-integration": "nix-cargo-integration", + "nixpkgs": "nixpkgs" } }, - "systems": { + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nix-cargo-integration", + "nixpkgs" + ] + }, "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "lastModified": 1772247314, + "narHash": "sha256-x6IFQ9bL7YYfW2m2z8D3Em2YtAA3HE8kiCFwai2fwrw=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "a1ab5e89ab12e1a37c0b264af6386a7472d68a15", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default", + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "slimlock": { + "inputs": { + "nixpkgs": [ + "nix-cargo-integration", + "dream2nix", + "purescript-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1688756706, + "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", + "owner": "thomashoneyman", + "repo": "slimlock", + "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "slimlock", + "type": "github" + } + }, + "treefmt": { + "inputs": { + "nixpkgs": [ + "nix-cargo-integration", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770228511, + "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", "type": "github" } } diff --git a/flake.nix b/flake.nix index f52bd4c..a8295dd 100644 --- a/flake.nix +++ b/flake.nix @@ -1,128 +1,227 @@ { - description = "rust wrapper for libnix"; + description = "Rust bindings for the Nix C API"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; - systems.url = "github:nix-systems/default"; + flake-parts.url = "github:hercules-ci/flake-parts"; + nix.url = "github:NixOS/nix"; + nix.inputs.nixpkgs.follows = "nixpkgs"; + nix-cargo-integration.url = "github:90-008/nix-cargo-integration"; + nix-cargo-integration.inputs.nixpkgs.follows = "nixpkgs"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; - outputs = { - self, - nixpkgs, - ... - } @ inputs: let - systems = import inputs.systems; - - mkPkgs = system: repo: - import repo { - inherit system; - allowUnfree = false; - allowBroken = false; - overlays = builtins.attrValues self.overlays or {}; - }; - - forAllSystems = f: - nixpkgs.lib.genAttrs systems (system: - f rec { - inherit system; - inherit (pkgs) lib; - pkgs = mkPkgs system nixpkgs; - }); - in { - overlays.default = self: super: { - libclang = super.llvmPackages_21.libclang; - }; - - devShells = forAllSystems ( - { - pkgs, + outputs = + inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } ( + toplevel@{ lib, ... - }: { - default = let - nixForBindings = pkgs.nixVersions.nix_2_32; - inherit (pkgs.rustc) llvmPackages; - in - pkgs.mkShell rec { - name = "nixide"; - shell = "${pkgs.bash}/bin/bash"; - strictDeps = true; + }: + let + /** + Makes perSystem.nix-bindings-rust available. + */ + flake-parts-modules.basic = + { + flake-parts-lib, + ... + }: + { + _file = ./flake.nix; + imports = [ ./input-propagation-workaround.nix ]; + options.perSystem = flake-parts-lib.mkPerSystemOption ( + { pkgs, ... }: + { + options.nix-bindings-rust = { + nixPackage = lib.mkOption { + type = lib.types.package; + default = pkgs.nix; + defaultText = lib.literalMD "pkgs.nix"; + description = '' + The Nix package to use when building the `nix-bindings-...` crates. + ''; + }; + nciBuildConfig = lib.mkOption { + type = lib.types.deferredModule; + description = '' + A module to load into your nix-cargo-integration + [`perSystem.nci.projects..depsDrvConfig`](https://flake.parts/options/nix-cargo-integration.html#opt-perSystem.nci.projects._name_.depsDrvConfig) or similar such options. - # packages we need at runtime - packages = with pkgs; [ - rustc - llvmPackages.lld - lldb + This provides common build configuration (pkg-config, libclang, etc.) and + automatically adds Nix C library build inputs based on which nix-bindings + crates are *direct* dependencies of your crate. - cargo - cargo-c - cargo-llvm-cov - cargo-nextest + To disable automatic build input detection: + ```nix + nix-bindings-rust.inputPropagationWorkaround.enable = false; + ``` - rust-analyzer-unwrapped - (rustfmt.override {asNightly = true;}) - clippy - taplo - ]; + Example: + ```nix + perSystem = perSystem@{ config, ... }: { + nci.projects."my_project".drvConfig = { + imports = [ perSystem.config.nix-bindings-rust.nciBuildConfig ]; + }; + } + ``` + ''; + }; + }; + config.nix-bindings-rust = { + nciBuildConfig = { + mkDerivation = rec { + buildInputs = [ + # stdbool.h + pkgs.stdenv.cc + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + # bindgen uses clang to generate bindings, but it doesn't know where to + # find our stdenv cc's headers, so when it's gcc, we need to tell it. + postConfigure = lib.optionalString pkgs.stdenv.cc.isGNU '' + source ${./bindgen-gcc.sh} + ''; + shellHook = postConfigure; + }; + # NOTE: duplicated in flake.nix devShell + env = { + LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.buildPackages.llvmPackages.clang-unwrapped ]; + BINDGEN_EXTRA_CLANG_ARGS = + # Work around missing [[deprecated]] in clang + "-x c++ -std=c++2a"; + } + // lib.optionalAttrs pkgs.stdenv.cc.isGNU { + # Avoid cc wrapper, because we only need to add the compiler/"system" dirs + NIX_CC_UNWRAPPED = "${pkgs.stdenv.cc.cc}/bin/gcc"; + }; + }; + }; + } + ); + }; - # packages we need at build time - nativeBuildInputs = with pkgs; [ - pkg-config - glibc.dev - nixForBindings.dev + /** + A flake-parts module for dependents to import. Also dogfooded locally + (extra, not required for normal CI). - rustPlatform.bindgenHook - ]; + Adds flake checks that test the nix-bindings crates with the + dependent's nix package. - # packages we link against - buildInputs = with pkgs; [ - stdenv.cc + See https://github.com/nixops4/nix-bindings-rust?tab=readme-ov-file#integration-with-nix-projects + */ + flake-parts-modules.tested = + # Consumer toplevel + { ... }: + { + _file = ./flake.nix; + imports = [ flake-parts-modules.basic ]; + config.perSystem = + # Consumer perSystem + { + lib, + config, + system, + pkgs, + ... + }: + let + # nix-bindings-rust's perSystem, but with the consumer's `pkgs` + nix-bindings-rust-perSystemConfig = + # Extending our own perSystem, not the consumer's perSystem! + toplevel.config.partitions.testing-support.module.nix-bindings-rust.internalWithSystem system + ({ extendModules, ... }: extendModules) + { + modules = [ + { + config = { + # Overriding our `perSystem` to use the consumer's `pkgs` + _module.args.pkgs = lib.mkForce pkgs; + # ... and `nixPackage` + nix-bindings-rust.nixPackage = lib.mkForce config.nix-bindings-rust.nixPackage; + }; + } + ]; + }; + in + { + key = "nix-bindings-rust-add-checks"; + # Exclude clippy checks; those are part of this repo's local CI. + # This module is for dependents (and local dogfooding), which + # don't need to run clippy on nix-bindings-rust. + config.checks = lib.concatMapAttrs ( + k: v: + lib.optionalAttrs (lib.strings.hasPrefix "nix-bindings-" k && !lib.strings.hasSuffix "-clippy" k) { + "dependency-${k}" = v; + } + ) nix-bindings-rust-perSystemConfig.config.checks; + }; + }; - nixForBindings - ]; + flake-parts-modules.default = flake-parts-modules.tested; - # bindgen uses clang to generate bindings, but it doesn't know where to - # find our stdenv cc's headers, so when it's gcc, we need to tell it. - postConfigure = lib.optionalString pkgs.stdenv.cc.isGNU '' - #!/usr/bin/env bash - # REF: https://github.com/nixops4/nix-bindings-rust/blob/main/bindgen-gcc.sh - # Rust bindgen uses Clang to generate bindings, but that means that it can't - # find the "system" or compiler headers when the stdenv compiler is GCC. - # This script tells it where to find them. + in + { + imports = [ + inputs.nix-cargo-integration.flakeModule + inputs.flake-parts.flakeModules.partitions + inputs.flake-parts.flakeModules.modules + # dogfood + flake-parts-modules.tested + ./nci.nix + ]; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + perSystem = + { + inputs', + ... + }: + { + packages.nix = inputs'.nix.packages.nix; + }; - echo "Extending BINDGEN_EXTRA_CLANG_ARGS with system include paths..." 2>&1 - BINDGEN_EXTRA_CLANG_ARGS="$${BINDGEN_EXTRA_CLANG_ARGS:-}" - export BINDGEN_EXTRA_CLANG_ARGS - include_paths=$( - echo | $NIX_CC_UNWRAPPED -v -E -x c - 2>&1 \ - | awk '/#include <...> search starts here:/{flag=1;next} \ - /End of search list./{flag=0} \ - flag==1 {print $1}' - ) - for path in $include_paths; do - echo " - $path" 2>&1 - BINDGEN_EXTRA_CLANG_ARGS="$BINDGEN_EXTRA_CLANG_ARGS -I$path" - done - ''; + partitionedAttrs.devShells = "dev"; + partitionedAttrs.checks = "dev"; + partitionedAttrs.herculesCI = "dev"; + # Packages are basically just checks in this project; a library by + # itself is not useful. That's just not how the Rust integration works. + # By taking `packages` from `dev` we benefit from this dev-only definition: + # nix-bindings-rust.nixPackage = inputs'.nix.packages.default; + partitionedAttrs.packages = "dev"; - shellHook = postConfigure; + partitions.dev.extraInputsFlake = ./dev; + partitions.dev.module = { + imports = [ ./dev/flake-module.nix ]; + }; - env = let - inherit (llvmPackages) llvm libclang; - in { - LD_LIBRARY_PATH = builtins.toString (lib.makeLibraryPath buildInputs); - LIBCLANG_PATH = "${libclang.lib}/lib"; - - RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; - BINDGEN_EXTRA_CLANG_ARGS = "--sysroot=${pkgs.glibc.dev}"; - - # `cargo-llvm-cov` reads these environment variables to find these binaries, - # which are needed to run the tests - LLVM_COV = "${llvm}/bin/llvm-cov"; - LLVM_PROFDATA = "${llvm}/bin/llvm-profdata"; + # A partition that doesn't dogfood the flake-parts-modules.tested module + # so that we can actually retrieve `checks` without infinite recursions + # from trying to include the dogfooded attrs. + partitions.testing-support.module = + { withSystem, ... }: + { + # Make a clean withSystem available for consumers + options.nix-bindings-rust.internalWithSystem = lib.mkOption { internal = true; }; + config = { + nix-bindings-rust.internalWithSystem = withSystem; + perSystem = { + # Remove dogfooded checks. This configuration's checks are + # *consumed* by nix-bindings-rust-add-checks, so they should + # *NOT* also be *produced* by it. + disabledModules = [ { key = "nix-bindings-rust-add-checks"; } ]; + }; }; }; + + # flake output attributes + flake = { + modules.flake = flake-parts-modules; + }; } ); - }; } diff --git a/input-propagation-workaround.nix b/input-propagation-workaround.nix new file mode 100644 index 0000000..c3ac92f --- /dev/null +++ b/input-propagation-workaround.nix @@ -0,0 +1,143 @@ +# Workaround for missing native input propagation in nix-cargo-integration +# +# Automatically adds Nix C library build inputs based on which nix-bindings +# crates are direct dependencies of the crate being built. The mapping is +# recursive, so depending on nix-bindings-flake will also bring in the +# transitive C library dependencies (nix-fetchers-c, nix-expr-c, etc.). +# +# Note: For multi-crate workspaces, if your crate A depends on your crate B +# which depends on nix-bindings, you'll need to add an A -> B mapping to +# `crateInputMapping` so that A also gets B's nix-bindings inputs. +{ + perSystem = + { + lib, + config, + pkgs, + ... + }: + let + cfg = config.nix-bindings-rust.inputPropagationWorkaround; + nixPackage = config.nix-bindings-rust.nixPackage; + + nixLibs = + if nixPackage ? libs then + nixPackage.libs + else + # Fallback for older Nix versions without split libs + { + nix-util-c = nixPackage; + nix-store-c = nixPackage; + nix-expr-c = nixPackage; + nix-fetchers-c = nixPackage; + nix-flake-c = nixPackage; + }; + + # A module for nciBuildConfig that sets buildInputs based on nix-bindings dependencies. + # Uses options inspection to detect drvConfig vs depsDrvConfig context. + workaroundModule = + { + lib, + config, + options, + ... + }: + let + # rust-cargo-lock exists in drvConfig but not depsDrvConfig + isDrvConfig = options ? rust-cargo-lock; + + dreamLock = config.rust-cargo-lock.dreamLock; + depsList = dreamLock.dependencies.${config.name}.${config.version} or [ ]; + + # Convert list of deps to attrset keyed by name for efficient lookup + deps = builtins.listToAttrs ( + map (dep: { + name = dep.name; + value = dep; + }) depsList + ); + + # Inputs for the crate itself if it's in the mapping + selfInputs = cfg.crateInputMapping.${config.name} or [ ]; + + # Inputs for direct dependencies that have mappings + depInputs = lib.concatLists (lib.attrValues (lib.intersectAttrs deps cfg.crateInputMapping)); + + allInputs = selfInputs ++ depInputs; + in + { + config = lib.optionalAttrs isDrvConfig { + mkDerivation.buildInputs = allInputs; + rust-crane.depsDrv.mkDerivation.buildInputs = allInputs; + }; + }; + in + { + options.nix-bindings-rust.inputPropagationWorkaround = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to automatically add Nix C library build inputs based on + which nix-bindings crates are direct dependencies. + + Set to `false` to disable automatic detection and specify buildInputs manually. + ''; + }; + + crateInputMapping = lib.mkOption { + type = lib.types.lazyAttrsOf (lib.types.listOf lib.types.package); + description = '' + Mapping from crate names to build inputs. Entries can reference + other entries for transitive dependencies. + + The input propagation workaround can see direct dependencies, so + if you have `my-crate -> nix-bindings`, that works out of the box. + If you have `my-other-crate -> my-crate -> nix-bindings`, then you + need to specify `my-other-crate -> my-crate` as follows: + + ```nix + nix-bindings-rust.inputPropagationWorkaround.crateInputMapping."my-other-crate" = + config.nix-bindings-rust.inputPropagationWorkaround.crateInputMapping."my-crate"; + ``` + ''; + default = { }; + }; + }; + + config = lib.mkIf cfg.enable { + nix-bindings-rust.inputPropagationWorkaround.crateInputMapping = { + # -sys crates with their transitive dependencies + "nix-bindings-bdwgc-sys" = [ pkgs.boehmgc ]; + "nix-bindings-util-sys" = [ nixLibs.nix-util-c.dev ]; + "nix-bindings-store-sys" = [ + nixLibs.nix-store-c.dev + ] + ++ cfg.crateInputMapping."nix-bindings-util-sys"; + "nix-bindings-expr-sys" = [ + nixLibs.nix-expr-c.dev + ] + ++ cfg.crateInputMapping."nix-bindings-store-sys" + ++ cfg.crateInputMapping."nix-bindings-bdwgc-sys"; + "nix-bindings-fetchers-sys" = [ + nixLibs.nix-fetchers-c.dev + ] + ++ cfg.crateInputMapping."nix-bindings-expr-sys"; + "nix-bindings-flake-sys" = [ + nixLibs.nix-flake-c.dev + ] + ++ cfg.crateInputMapping."nix-bindings-fetchers-sys" + ++ cfg.crateInputMapping."nix-bindings-bdwgc-sys"; + # High-level crates reference their -sys counterparts + "nix-bindings-bdwgc" = cfg.crateInputMapping."nix-bindings-bdwgc-sys"; + "nix-bindings-util" = cfg.crateInputMapping."nix-bindings-util-sys"; + "nix-bindings-store" = cfg.crateInputMapping."nix-bindings-store-sys"; + "nix-bindings-expr" = cfg.crateInputMapping."nix-bindings-expr-sys"; + "nix-bindings-fetchers" = cfg.crateInputMapping."nix-bindings-fetchers-sys"; + "nix-bindings-flake" = cfg.crateInputMapping."nix-bindings-flake-sys"; + }; + + nix-bindings-rust.nciBuildConfig.imports = [ workaroundModule ]; + }; + }; +} diff --git a/nci.nix b/nci.nix new file mode 100644 index 0000000..351ac77 --- /dev/null +++ b/nci.nix @@ -0,0 +1,72 @@ +{ + perSystem = + { config, ... }: + let + cfg = config.nix-bindings-rust; + in + { + # https://flake.parts/options/nix-cargo-integration + nci.projects.nix-bindings = { + path = ./.; + profiles = { + dev.drvConfig.env.RUSTFLAGS = "-D warnings"; + release.runTests = true; + }; + drvConfig = { + imports = [ + # Downstream projects import this into depsDrvConfig instead + cfg.nciBuildConfig + ]; + # Extra settings for running the tests + mkDerivation = { + # Prepare the environment for Nix to work. + # Nix does not provide a suitable environment for running itself in + # the sandbox - not by default. We configure it to use a relocated store. + preCheck = '' + # nix needs a home directory + export HOME="$(mktemp -d $TMPDIR/home.XXXXXX)" + + # configure a relocated store + store_data=$(mktemp -d $TMPDIR/store-data.XXXXXX) + export NIX_REMOTE="$store_data" + export NIX_BUILD_HOOK= + export NIX_CONF_DIR=$store_data/etc + export NIX_LOCALSTATE_DIR=$store_data/nix/var + export NIX_LOG_DIR=$store_data/nix/var/log/nix + export NIX_STATE_DIR=$store_data/nix/var/nix + + echo "Configuring relocated store at $NIX_REMOTE..." + + # Create nix.conf with experimental features enabled + mkdir -p "$NIX_CONF_DIR" + echo "experimental-features = ca-derivations flakes" > "$NIX_CONF_DIR/nix.conf" + + # Init ahead of time, because concurrent initialization is flaky + ${cfg.nixPackage}/bin/nix-store --init + + echo "Store initialized." + ''; + }; + }; + }; + nci.crates.nix-bindings-store = + let + addHarmoniaProfile = '' + cat >> Cargo.toml <<'EOF' + + [profile.harmonia] + inherits = "release" + EOF + ''; + in + { + profiles.harmonia = { + features = [ "harmonia" ]; + runTests = true; + # Add harmonia profile to Cargo.toml for both deps and main builds + depsDrvConfig.mkDerivation.postPatch = addHarmoniaProfile; + drvConfig.mkDerivation.postPatch = addHarmoniaProfile; + }; + }; + }; +} diff --git a/nix-bindings-bdwgc-sys/Cargo.toml b/nix-bindings-bdwgc-sys/Cargo.toml new file mode 100644 index 0000000..488f41e --- /dev/null +++ b/nix-bindings-bdwgc-sys/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nix-bindings-bdwgc-sys" +version = "0.2.1" +edition = "2021" +build = "build.rs" +license = "LGPL-2.1" +description = "Low-level FFI bindings to the Boehm-Demers-Weiser garbage collector" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_bdwgc_sys/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] + +[build-dependencies] +bindgen = "0.69" +pkg-config = "0.3" diff --git a/nix-bindings-bdwgc-sys/README.md b/nix-bindings-bdwgc-sys/README.md new file mode 100644 index 0000000..b49b5b4 --- /dev/null +++ b/nix-bindings-bdwgc-sys/README.md @@ -0,0 +1,10 @@ +# nix-bindings-bdwgc-sys + +This crate contains generated bindings for the Boehm-Demers-Weiser garbage collector (`bdw-gc`). +**You should not have to use this crate directly,** and so you should probably not add it to your dependencies. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_bdwgc_sys/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-bdwgc-sys/build.rs b/nix-bindings-bdwgc-sys/build.rs new file mode 100644 index 0000000..aed2a8c --- /dev/null +++ b/nix-bindings-bdwgc-sys/build.rs @@ -0,0 +1,27 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-changed=include/bdwgc.h"); + + let mut args = Vec::new(); + for path in pkg_config::probe_library("bdw-gc") + .unwrap() + .include_paths + .iter() + { + args.push(format!("-I{}", path.to_str().unwrap())); + } + + let bindings = bindgen::Builder::default() + .header("include/bdwgc.h") + .clang_args(args) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/nix-bindings-bdwgc-sys/include/bdwgc.h b/nix-bindings-bdwgc-sys/include/bdwgc.h new file mode 100644 index 0000000..2a70434 --- /dev/null +++ b/nix-bindings-bdwgc-sys/include/bdwgc.h @@ -0,0 +1,2 @@ +#define GC_THREADS +#include diff --git a/nix-bindings-bdwgc-sys/src/lib.rs b/nix-bindings-bdwgc-sys/src/lib.rs new file mode 100644 index 0000000..e722c20 --- /dev/null +++ b/nix-bindings-bdwgc-sys/src/lib.rs @@ -0,0 +1,30 @@ +//! Raw bindings to Nix C API +//! +//! This crate contains automatically generated bindings from the Nix C headers. +//! The bindings are generated by bindgen and include C-style naming conventions +//! and documentation comments that don't always conform to Rust standards. +//! +//! Normally you don't have to use this crate directly. + +// This file must only contain generated code, so that the module-level +// #![allow(...)] attributes don't suppress warnings in hand-written code. +// If you need to add hand-written code, use a submodule to isolate the +// generated code. See: +// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9 + +// Standard bindgen suppressions for C naming conventions +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// Clippy suppressions for generated C bindings +// bindgen doesn't generate safety docs +#![allow(clippy::missing_safety_doc)] +// Rustdoc suppressions for generated C documentation +// The C headers contain Doxygen-style documentation that doesn't translate +// well to Rust's rustdoc format, causing various warnings: +#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve +#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs +#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like +#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nix-bindings-expr-sys/Cargo.toml b/nix-bindings-expr-sys/Cargo.toml new file mode 100644 index 0000000..0cf3e12 --- /dev/null +++ b/nix-bindings-expr-sys/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nix-bindings-expr-sys" +version = "0.2.1" +edition = "2021" +build = "build.rs" +license = "LGPL-2.1" +description = "Low-level FFI bindings to the Nix expression evaluator" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_expr_sys/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" } +nix-bindings-store-sys = { path = "../nix-bindings-store-sys", version = "0.2.1" } + +[build-dependencies] +bindgen = "0.69" +pkg-config = "0.3" diff --git a/nix-bindings-expr-sys/README.md b/nix-bindings-expr-sys/README.md new file mode 100644 index 0000000..9fdb3a0 --- /dev/null +++ b/nix-bindings-expr-sys/README.md @@ -0,0 +1,11 @@ +# nix-bindings-expr-sys + +This crate contains generated bindings for the Nix C API (`nix-expr-c`). +**You should not have to use this crate directly,** and so you should probably not add it to your dependencies. +Instead, use the `nix-bindings-expr` crate, which _should_ be sufficient. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_expr_sys/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-expr-sys/build.rs b/nix-bindings-expr-sys/build.rs new file mode 100644 index 0000000..6e0eed2 --- /dev/null +++ b/nix-bindings-expr-sys/build.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +#[derive(Debug)] +struct StripNixPrefix; + +impl bindgen::callbacks::ParseCallbacks for StripNixPrefix { + fn item_name(&self, name: &str) -> Option { + name.strip_prefix("nix_").map(String::from) + } +} + +fn main() { + println!("cargo:rerun-if-changed=include/nix-c-expr.h"); + println!("cargo:rustc-link-lib=nixexprc"); + + let mut args = Vec::new(); + for path in pkg_config::probe_library("nix-expr-c") + .unwrap() + .include_paths + .iter() + { + args.push(format!("-I{}", path.to_str().unwrap())); + } + + let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + + let bindings = bindgen::Builder::default() + .header("include/nix-c-expr.h") + .clang_args(args) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .parse_callbacks(Box::new(StripNixPrefix)) + // Blocklist symbols from nix-bindings-util-sys + .blocklist_file(".*nix_api_util\\.h") + // Blocklist symbols from nix-bindings-store-sys + .blocklist_file(".*nix_api_store\\.h") + .generate() + .expect("Unable to generate bindings"); + + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/nix-bindings-expr-sys/include/nix-c-expr.h b/nix-bindings-expr-sys/include/nix-c-expr.h new file mode 100644 index 0000000..c2649a4 --- /dev/null +++ b/nix-bindings-expr-sys/include/nix-c-expr.h @@ -0,0 +1,2 @@ +#include +#include diff --git a/nix-bindings-expr-sys/src/lib.rs b/nix-bindings-expr-sys/src/lib.rs new file mode 100644 index 0000000..739f706 --- /dev/null +++ b/nix-bindings-expr-sys/src/lib.rs @@ -0,0 +1,34 @@ +//! Raw bindings to Nix C API +//! +//! This crate contains automatically generated bindings from the Nix C headers. +//! The bindings are generated by bindgen and include C-style naming conventions +//! and documentation comments that don't always conform to Rust standards. +//! +//! Normally you don't have to use this crate directly. +//! Instead use `nix-expr`. + +// This file must only contain generated code, so that the module-level +// #![allow(...)] attributes don't suppress warnings in hand-written code. +// If you need to add hand-written code, use a submodule to isolate the +// generated code. See: +// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9 + +// Standard bindgen suppressions for C naming conventions +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// Clippy suppressions for generated C bindings +// bindgen doesn't generate safety docs +#![allow(clippy::missing_safety_doc)] +// Rustdoc suppressions for generated C documentation +// The C headers contain Doxygen-style documentation that doesn't translate +// well to Rust's rustdoc format, causing various warnings: +#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve +#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs +#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like +#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes + +use nix_bindings_store_sys::*; +use nix_bindings_util_sys::*; + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nix-bindings-expr/Cargo.toml b/nix-bindings-expr/Cargo.toml new file mode 100644 index 0000000..ef9a373 --- /dev/null +++ b/nix-bindings-expr/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "nix-bindings-expr" +version = "0.2.1" +edition = "2021" +build = "build.rs" +license = "LGPL-2.1" +description = "Rust bindings to Nix expression evaluator" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_expr/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0" +nix-bindings-store = { path = "../nix-bindings-store", version = "0.2.1" } +nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" } +nix-bindings-bdwgc-sys = { path = "../nix-bindings-bdwgc-sys", version = "0.2.1" } +nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" } +nix-bindings-store-sys = { path = "../nix-bindings-store-sys", version = "0.2.1" } +nix-bindings-expr-sys = { path = "../nix-bindings-expr-sys", version = "0.2.1" } +ctor = "0.2" +tempfile = "3.10" +cstr = "0.2" + +[build-dependencies] +pkg-config = "0.3" +nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" } + +[lints.rust] +warnings = "deny" +dead-code = "allow" + +[lints.clippy] +type-complexity = "allow" +# We're still trying to make Nix more thread-safe, want forward-compat +arc-with-non-send-sync = "allow" diff --git a/nix-bindings-expr/README.md b/nix-bindings-expr/README.md new file mode 100644 index 0000000..cb92996 --- /dev/null +++ b/nix-bindings-expr/README.md @@ -0,0 +1,9 @@ +# nix-bindings-expr + +Rust bindings to the Nix expression evaluator. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_expr/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-expr/build.rs b/nix-bindings-expr/build.rs new file mode 100644 index 0000000..f56bc3e --- /dev/null +++ b/nix-bindings-expr/build.rs @@ -0,0 +1,6 @@ +use nix_bindings_util::nix_version::emit_version_cfg; + +fn main() { + let nix_version = pkg_config::probe_library("nix-expr-c").unwrap().version; + emit_version_cfg(&nix_version, &["2.26", "2.34.0pre"]); +} diff --git a/nix-bindings-expr/src/eval_state.rs b/nix-bindings-expr/src/eval_state.rs new file mode 100644 index 0000000..5d8599f --- /dev/null +++ b/nix-bindings-expr/src/eval_state.rs @@ -0,0 +1,2925 @@ +//! # Nix Expression Evaluation +//! +//! This module provides the core [`EvalState`] type for evaluating Nix expressions +//! and extracting typed values from the results. +//! +//! ## Overview +//! +//! The [`EvalState`] manages the evaluation context for Nix expressions, including: +//! - Expression parsing and evaluation with [`eval_from_string`](EvalState::eval_from_string) +//! - Type-safe value extraction with [`require_*`](EvalState#implementations) methods +//! - Memory management and garbage collection integration +//! - Store integration for derivations and store paths +//! - Custom function creation with [`new_value_primop`](EvalState::new_value_primop) and [`new_value_thunk`](EvalState::new_value_thunk) +//! +//! ### Construction +//! +//! Create an [`EvalState`] using [`EvalState::new`] or [`EvalStateBuilder`] for advanced configuration: +//! +//! ```rust +//! # use nix_bindings_expr::eval_state::{EvalState, EvalStateBuilder, test_init, gc_register_my_thread}; +//! # use nix_bindings_store::store::Store; +//! # use std::collections::HashMap; +//! # fn example() -> anyhow::Result<()> { +//! # test_init(); let guard = gc_register_my_thread()?; +//! let store = Store::open(None, HashMap::new())?; +//! +//! // Simple creation +//! let mut es = EvalState::new(store.clone(), [])?; +//! +//! // With custom lookup paths +//! let mut es = EvalStateBuilder::new(store)? +//! .lookup_path(["nixpkgs=/path/to/nixpkgs"])? +//! .build()?; +//! # drop(guard); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Value Extraction +//! +//! All `require_*` methods perform these steps: +//! 1. **Evaluation**: Force evaluation of thunks as needed +//! 2. **Type checking**: Verify the value matches the expected type +//! 3. **Extraction**: Return the typed Rust value or an error +//! +//! Methods with `_strict` in their name also evaluate their return values before returning them. +//! +//! ### Evaluation Strictness +//! +//! - **Lazy methods** (e.g., [`require_list_size`](EvalState::require_list_size)): +//! Evaluate only the structure needed +//! - **Strict methods** (e.g., [`require_list_strict`](EvalState::require_list_strict)): +//! Force full evaluation of all contained values +//! - **Selective methods** (e.g., [`require_list_select_idx_strict`](EvalState::require_list_select_idx_strict)): +//! Evaluate only the accessed elements +//! +//! ## Laziness and Strictness +//! +//! The terms "lazy" and "strict" in this API refer to Nix's [Weak Head Normal Form (WHNF)](https://nix.dev/manual/nix/latest/language/evaluation.html#values) +//! evaluation model, not the kind of deep strictness that is exercised by functions such as `builtins.toJSON` or `builtins.deepSeq`. +//! +//! - **WHNF evaluation**: Values are evaluated just enough to determine their type and basic structure +//! - **Deep evaluation**: All nested values are recursively forced (like `builtins.deepSeq`) +//! +//! For example, a list in WHNF has its length determined but individual elements may remain unevaluated thunks. +//! Methods marked as "strict" in this API force WHNF evaluation of their results, but do not perform deep evaluation +//! of arbitrarily nested structures unless explicitly documented otherwise. +//! +//! ### Thread Safety and Memory Management +//! +//! Before using [`EvalState`] in a thread, register it with the (process memory) garbage collector: +//! +//! ```rust,no_run +//! # use nix_bindings_expr::eval_state::{init, gc_register_my_thread, test_init}; +//! # fn example() -> anyhow::Result<()> { +//! # test_init(); // Use test_init() in tests +//! init()?; // Initialize Nix library +//! let guard = gc_register_my_thread()?; // Register thread with GC +//! // Now safe to use EvalState in this thread +//! drop(guard); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Error Handling +//! +//! Evaluation methods return [`Result`] types. Common error scenarios include: +//! - **Type mismatches**: Expected type doesn't match actual value type +//! - **Evaluation errors**: Nix expressions that throw or have undefined behavior +//! - **Bounds errors**: Out-of-range access for indexed operations +//! +//! ## Examples +//! +//! ```rust +//! use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread}; +//! use nix_bindings_store::store::Store; +//! use std::collections::HashMap; +//! +//! # fn main() -> anyhow::Result<()> { +//! test_init(); // init() in non-test code +//! let guard = gc_register_my_thread()?; +//! +//! let store = Store::open(None, HashMap::new())?; +//! let mut es = EvalState::new(store, [])?; +//! +//! // Evaluate a list expression +//! let list_value = es.eval_from_string("[1 2 3]", "")?; +//! +//! // Check the size (lazy - doesn't evaluate elements) +//! let size = es.require_list_size(&list_value)?; +//! println!("List has {} elements", size); +//! +//! // Access specific elements (evaluates only accessed elements) +//! if let Some(first) = es.require_list_select_idx_strict(&list_value, 0)? { +//! let value = es.require_int(&first)?; +//! println!("First element: {}", value); +//! } +//! +//! // Process all elements (evaluates all elements) +//! let all_elements: Vec<_> = es.require_list_strict(&list_value)?; +//! for element in all_elements { +//! let value = es.require_int(&element)?; +//! println!("Element: {}", value); +//! } +//! +//! drop(guard); +//! # Ok(()) +//! # } +//! ``` + +use crate::primop; +use crate::value::{Int, Value, ValueType}; +use anyhow::Context as _; +use anyhow::{bail, Result}; +use cstr::cstr; +use nix_bindings_bdwgc_sys as gc; +use nix_bindings_expr_sys as raw; +use nix_bindings_store::path::StorePath; +use nix_bindings_store::store::{Store, StoreWeak}; +use nix_bindings_store_sys as raw_store; +use nix_bindings_util::context::Context; +use nix_bindings_util::string_return::{ + callback_get_result_string, callback_get_result_string_data, +}; +use nix_bindings_util::{check_call, check_call_opt_key, result_string_init}; +use std::ffi::{c_char, CString}; +use std::iter::FromIterator; +use std::os::raw::c_uint; +use std::ptr::{null, null_mut, NonNull}; +use std::sync::{Arc, LazyLock, Weak}; + +static INIT: LazyLock> = LazyLock::new(|| unsafe { + gc::GC_allow_register_threads(); + check_call!(raw::libexpr_init(&mut Context::new()))?; + Ok(()) +}); + +pub fn init() -> Result<()> { + let x = INIT.as_ref(); + match x { + Ok(_) => Ok(()), + Err(e) => { + // Couldn't just clone the error, so we have to print it here. + Err(anyhow::format_err!("nix_bindings_expr::init error: {}", e)) + } + } +} + +/// A string value with its associated [store paths](https://nix.dev/manual/nix/stable/store/store-path.html). +/// +/// Represents a Nix string with references to store paths. +pub struct RealisedString { + /// The string content. + pub s: String, + /// Store paths referenced by the string. + pub paths: Vec, +} + +/// A [Weak] reference to an [EvalState]. +pub struct EvalStateWeak { + inner: Weak, + store: StoreWeak, +} +impl EvalStateWeak { + /// Upgrade the weak reference to a proper [EvalState]. + /// + /// If no normal reference to the [EvalState] is around anymore elsewhere, this fails by returning `None`. + pub fn upgrade(&self) -> Option { + self.inner.upgrade().and_then(|eval_state| { + self.store.upgrade().map(|store| EvalState { + eval_state, + store, + context: Context::new(), + }) + }) + } +} + +struct EvalStateRef { + eval_state: NonNull, +} +impl EvalStateRef { + /// Returns a raw pointer to the underlying EvalState. + /// + /// # Safety + /// + /// The caller must ensure that the pointer is not used beyond the lifetime of the underlying [raw::EvalState]. + unsafe fn as_ptr(&self) -> *mut raw::EvalState { + self.eval_state.as_ptr() + } +} +impl Drop for EvalStateRef { + fn drop(&mut self) { + unsafe { + raw::state_free(self.eval_state.as_ptr()); + } + } +} +/// Builder for configuring and creating an [`EvalState`]. +/// +/// Provides advanced configuration options for evaluation context setup. +/// Use [`EvalState::new`] for simple cases or this builder for custom configuration. +/// +/// Requires Nix 2.26.0 or later. +/// +/// # Examples +/// +/// ```rust +/// # use nix_bindings_expr::eval_state::{EvalState, EvalStateBuilder, test_init, gc_register_my_thread}; +/// # use nix_bindings_store::store::Store; +/// # use std::collections::HashMap; +/// # fn example() -> anyhow::Result<()> { +/// # test_init(); +/// # let guard = gc_register_my_thread()?; +/// let store = Store::open(None, HashMap::new())?; +/// +/// let mut es: EvalState = EvalStateBuilder::new(store)? +/// .lookup_path(["nixpkgs=/path/to/nixpkgs", "home-manager=/path/to/hm"])? +/// .build()?; +/// +/// let value = es.eval_from_string("", /* path display: */ "in-memory")?; +/// # drop(guard); +/// # Ok(()) +/// # } +/// ``` +#[cfg(nix_at_least = "2.26")] +pub struct EvalStateBuilder { + eval_state_builder: *mut raw::eval_state_builder, + lookup_path: Vec, + load_ambient_settings: bool, + store: Store, +} +#[cfg(nix_at_least = "2.26")] +impl Drop for EvalStateBuilder { + fn drop(&mut self) { + unsafe { + raw::eval_state_builder_free(self.eval_state_builder); + } + } +} +#[cfg(nix_at_least = "2.26")] +impl EvalStateBuilder { + /// Creates a new [`EvalStateBuilder`]. + pub fn new(store: Store) -> Result { + let mut context = Context::new(); + let eval_state_builder = + unsafe { check_call!(raw::eval_state_builder_new(&mut context, store.raw_ptr())) }?; + Ok(EvalStateBuilder { + store, + eval_state_builder, + lookup_path: Vec::new(), + load_ambient_settings: true, + }) + } + /// Sets the [lookup path](https://nix.dev/manual/nix/latest/language/constructs/lookup-path.html) for Nix expression evaluation. + pub fn lookup_path<'a>(mut self, path: impl IntoIterator) -> Result { + let lookup_path: Vec = path + .into_iter() + .map(|path| { + CString::new(path).with_context(|| { + format!("EvalStateBuilder::lookup_path: path `{path}` contains null byte") + }) + }) + .collect::>()?; + self.lookup_path = lookup_path; + Ok(self) + } + /// Sets whether to load settings from the ambient environment. + /// + /// When enabled (default), calls `nix_eval_state_builder_load` to load settings + /// from NIX_CONFIG and other environment variables. When disabled, only the + /// explicitly configured settings are used. + pub fn load_ambient_settings(mut self, load: bool) -> Self { + self.load_ambient_settings = load; + self + } + /// Builds the configured [`EvalState`]. + pub fn build(&self) -> Result { + // Make sure the library is initialized + init()?; + + let mut context = Context::new(); + + // Load settings from global configuration (including readOnlyMode = false). + // This is necessary for path coercion to work (adding files to the store). + if self.load_ambient_settings { + unsafe { + check_call!(raw::eval_state_builder_load( + &mut context, + self.eval_state_builder + ))?; + } + } + + // Note: these raw C string pointers borrow from self.lookup_path + let mut lookup_path: Vec<*const c_char> = self + .lookup_path + .iter() + .map(|s| s.as_ptr()) + .chain(std::iter::once(null())) // signal the end of the array + .collect(); + + unsafe { + check_call!(raw::eval_state_builder_set_lookup_path( + &mut context, + self.eval_state_builder, + lookup_path.as_mut_ptr() + ))?; + } + + let eval_state = + unsafe { check_call!(raw::eval_state_build(&mut context, self.eval_state_builder)) }?; + Ok(EvalState { + eval_state: Arc::new(EvalStateRef { + eval_state: NonNull::new(eval_state).unwrap_or_else(|| { + panic!("nix_state_create returned a null pointer without an error") + }), + }), + store: self.store.clone(), + context, + }) + } + /// Returns a raw pointer to the underlying eval state builder. + /// + /// # Safety + /// + /// The caller must ensure that the pointer is not used beyond the lifetime of this builder. + // TODO: This function should be marked `unsafe`. + pub fn raw_ptr(&self) -> *mut raw::eval_state_builder { + self.eval_state_builder + } +} + +pub struct EvalState { + eval_state: Arc, + store: Store, + pub(crate) context: Context, +} +impl EvalState { + /// Creates a new EvalState with basic configuration. + /// + /// For more options, use [EvalStateBuilder]. + pub fn new<'a>(store: Store, lookup_path: impl IntoIterator) -> Result { + EvalStateBuilder::new(store)? + .lookup_path(lookup_path)? + .build() + } + + /// Returns a raw pointer to the raw Nix C API EvalState. + /// + /// # Safety + /// + /// The caller must ensure that the pointer is not used beyond the lifetime of this `EvalState`. + pub unsafe fn raw_ptr(&self) -> *mut raw::EvalState { + self.eval_state.as_ptr() + } + + /// Returns a reference to the Store that's used for instantiation, import from derivation, etc. + pub fn store(&self) -> &Store { + &self.store + } + + /// Creates a weak reference to this EvalState. + pub fn weak_ref(&self) -> EvalStateWeak { + EvalStateWeak { + inner: Arc::downgrade(&self.eval_state), + store: self.store.weak_ref(), + } + } + + /// Parses and evaluates a Nix expression `expr`. + /// + /// Expressions can contain relative paths such as `./.` that are resolved relative to the given `path`. + /// + /// # Examples + /// + /// ``` + /// # use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread}; + /// use nix_bindings_store::store::Store; + /// use nix_bindings_expr::value::Value; + /// use std::collections::HashMap; + /// + /// # fn main() -> anyhow::Result<()> { + /// # test_init(); + /// # let guard = gc_register_my_thread()?; + /// # let mut es = EvalState::new(Store::open(None, HashMap::new())?, [])?; + /// let v: Value = es.eval_from_string("42", ".")?; + /// assert_eq!(es.require_int(&v)?, 42); + /// # drop(guard); + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "nix_expr_eval_from_string")] + #[doc(alias = "parse")] + #[doc(alias = "eval")] + #[doc(alias = "evaluate")] + pub fn eval_from_string(&mut self, expr: &str, path: &str) -> Result { + let expr_ptr = + CString::new(expr).with_context(|| "eval_from_string: expr contains null byte")?; + let path_ptr = + CString::new(path).with_context(|| "eval_from_string: path contains null byte")?; + unsafe { + let value = self.new_value_uninitialized()?; + check_call!(raw::expr_eval_from_string( + &mut self.context, + self.eval_state.as_ptr(), + expr_ptr.as_ptr(), + path_ptr.as_ptr(), + value.raw_ptr() + ))?; + Ok(value) + } + } + + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) of a value to [weak head normal form](https://nix.dev/manual/nix/latest/language/evaluation.html?highlight=WHNF#values). + /// + /// Converts [thunks](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) to their evaluated form. Does not modify already-evaluated values. + /// + /// Does not perform deep evaluation of nested structures. + /// + /// See also: [Shared Evaluation State](Value#shared-evaluation-state) + #[doc(alias = "evaluate")] + #[doc(alias = "strict")] + pub fn force(&mut self, v: &Value) -> Result<()> { + unsafe { + check_call!(raw::value_force( + &mut self.context, + self.eval_state.as_ptr(), + v.raw_ptr() + )) + }?; + Ok(()) + } + + /// Returns the type of a value without forcing [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html). + /// + /// Returns [`None`] if the value is an unevaluated [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness). + /// + /// Returns [`Some`] if the value is already evaluated. + /// + /// See also: [Shared Evaluation State](Value#shared-evaluation-state) + #[doc(alias = "type_of")] + #[doc(alias = "value_type_lazy")] + #[doc(alias = "nix_get_type")] + #[doc(alias = "get_type")] + #[doc(alias = "nix_value_type")] + pub fn value_type_unforced(&mut self, value: &Value) -> Option { + let r = unsafe { check_call!(raw::get_type(&mut self.context, value.raw_ptr())) }; + // .unwrap(): no reason for this to fail, as it does not evaluate + ValueType::from_raw(r.unwrap()) + } + /// Returns the [type][`ValueType`] of a value, [forcing][`EvalState::force`] [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) if necessary. + /// + /// Forces evaluation if the value is an unevaluated [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness). + /// + /// Evaluation may fail, producing an [`Err`]. + /// + /// Guarantees a definitive result if [`Ok`], thanks to the language being [pure](https://nix.dev/manual/nix/latest/language/index.html?highlight=pure#nix-language) and [lazy](https://nix.dev/manual/nix/latest/language/index.html?highlight=lazy#nix-language). + #[doc(alias = "type_of")] + #[doc(alias = "value_type_strict")] + #[doc(alias = "nix_get_type")] + #[doc(alias = "get_type")] + #[doc(alias = "nix_value_type_strict")] + pub fn value_type(&mut self, value: &Value) -> Result { + match self.value_type_unforced(value) { + Some(a) => Ok(a), + None => { + self.force(value)?; + match self.value_type_unforced(value) { + Some(a) => Ok(a), + None => { + panic!("Nix value must not be thunk after being forced.") + } + } + } + } + } + /// Extracts the value from an [integer][`ValueType::Int`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is an integer. + /// + /// Returns the integer value if successful, or an [`Err`] if evaluation failed or the value is not an integer. + /// + /// # Examples + /// + /// ```rust + /// # use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread}; + /// # use nix_bindings_store::store::Store; + /// # use std::collections::HashMap; + /// # fn example() -> anyhow::Result<()> { + /// # test_init(); + /// # let guard = gc_register_my_thread()?; + /// let store = Store::open(None, HashMap::new())?; + /// let mut es = EvalState::new(store, [])?; + /// + /// let value = es.eval_from_string("42", "")?; + /// let int_val = es.require_int(&value)?; + /// assert_eq!(int_val, 42); + /// # drop(guard); + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "integer")] + #[doc(alias = "number")] + #[doc(alias = "nix_get_int")] + #[doc(alias = "get_int")] + pub fn require_int(&mut self, v: &Value) -> Result { + let t = self.value_type(v)?; + if t != ValueType::Int { + bail!("expected an int, but got a {:?}", t); + } + unsafe { check_call!(raw::get_int(&mut self.context, v.raw_ptr())) } + } + + /// Extracts the value from a [boolean][`ValueType::Bool`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is a boolean. + /// + /// Returns the boolean value if successful, or an [`Err`] if evaluation failed or the value is not a boolean. + #[doc(alias = "boolean")] + #[doc(alias = "nix_get_bool")] + #[doc(alias = "get_bool")] + pub fn require_bool(&mut self, v: &Value) -> Result { + let t = self.value_type(v)?; + if t != ValueType::Bool { + bail!("expected a bool, but got a {:?}", t); + } + unsafe { check_call!(raw::get_bool(&mut self.context, v.raw_ptr())) } + } + + /// Extracts all elements from a [list][`ValueType::List`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is a list. + /// + /// Returns the contained values in the specified container type (e.g., [`Vec`], [`VecDeque`][`std::collections::VecDeque`], etc.). + /// + /// This is [strict](https://nix.dev/manual/nix/latest/language/evaluation.html#strictness) - all list elements will be evaluated. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use nix_bindings_expr::value::Value; + /// # use std::collections::{VecDeque, LinkedList}; + /// # fn example(es: &mut nix_bindings_expr::eval_state::EvalState, list_value: &Value) -> anyhow::Result<()> { + /// let vec: Vec = es.require_list_strict(&list_value)?; + /// let deque: VecDeque = es.require_list_strict(&list_value)?; + /// let linked_list = es.require_list_strict::>(&list_value)?; + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "collect")] + #[doc(alias = "to_vec")] + #[doc(alias = "all")] + #[doc(alias = "nix_get_list_size")] + #[doc(alias = "nix_get_list_byidx")] + pub fn require_list_strict(&mut self, value: &Value) -> Result + where + C: FromIterator, + { + let t = self.value_type(value)?; + if t != ValueType::List { + bail!("expected a list, but got a {:?}", t); + } + let size = unsafe { check_call!(raw::get_list_size(&mut self.context, value.raw_ptr())) }?; + + (0..size) + .map(|i| { + let element_ptr = unsafe { + check_call!(raw::get_list_byidx( + &mut self.context, + value.raw_ptr(), + self.eval_state.as_ptr(), + i + )) + }?; + Ok(unsafe { Value::new(element_ptr) }) + }) + .collect() + } + + /// Evaluate, and require that the [`Value`] is a Nix [`ValueType::AttrSet`]. + /// + /// Returns a list of the keys in the attrset. + /// + /// NOTE: this currently implements its own sorting, which probably matches Nix's implementation, but is not guaranteed. + #[doc(alias = "keys")] + #[doc(alias = "attributes")] + #[doc(alias = "fields")] + pub fn require_attrs_names(&mut self, v: &Value) -> Result> { + self.require_attrs_names_unsorted(v).map(|mut v| { + v.sort(); + v + }) + } + + /// For when [`EvalState::require_attrs_names`] isn't fast enough. + /// + /// Only use when it's ok that the keys are returned in an arbitrary order. + #[doc(alias = "keys_unsorted")] + #[doc(alias = "attributes_unsorted")] + pub fn require_attrs_names_unsorted(&mut self, v: &Value) -> Result> { + let t = self.value_type(v)?; + if t != ValueType::AttrSet { + bail!("expected an attrset, but got a {:?}", t); + } + let n = unsafe { check_call!(raw::get_attrs_size(&mut self.context, v.raw_ptr())) }?; + let mut attrs = Vec::with_capacity(n as usize); + for i in 0..n { + let cstr_ptr: *const c_char = unsafe { + check_call!(raw::get_attr_name_byidx( + &mut self.context, + v.raw_ptr(), + self.eval_state.as_ptr(), + i as c_uint + )) + }?; + let cstr = unsafe { std::ffi::CStr::from_ptr(cstr_ptr) }; + let s = cstr + .to_str() + .map_err(|e| anyhow::format_err!("Nix attrset key is not valid UTF-8: {}", e))?; + attrs.insert(i as usize, s.to_owned()); + } + Ok(attrs) + } + + /// Extracts an attribute value from an [attribute set][`ValueType::AttrSet`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is an attribute set. + /// + /// Returns the attribute value if found, or an [`Err`] if evaluation failed, the attribute doesn't exist, or the value is not an attribute set. + #[doc(alias = "get_attr")] + #[doc(alias = "attribute")] + #[doc(alias = "field")] + pub fn require_attrs_select(&mut self, v: &Value, attr_name: &str) -> Result { + let t = self.value_type(v)?; + if t != ValueType::AttrSet { + bail!("expected an attrset, but got a {:?}", t); + } + let attr_name = CString::new(attr_name) + .with_context(|| "require_attrs_select: attrName contains null byte")?; + unsafe { + let v2 = check_call!(raw::get_attr_byname( + &mut self.context, + v.raw_ptr(), + self.eval_state.as_ptr(), + attr_name.as_ptr() + )); + match v2 { + Ok(v2) => Ok(Value::new(v2)), + Err(e) => { + // As of Nix 2.26, the error message is not helpful when it + // is simply missing, so we provide a better one. (Note that + // missing attributes requested by Nix expressions OTOH is a + // different error message which works fine.) + if e.to_string() == "missing attribute" { + bail!("attribute `{}` not found", attr_name.to_string_lossy()); + } else { + Err(e) + } + } + } + } + } + + /// Extracts an optional attribute value from an [attribute set][`ValueType::AttrSet`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is an attribute set. + /// + /// Returns [`Err`] if evaluation failed or the value is not an attribute set. + /// + /// Returns `Ok(None)` if the attribute is not present. + /// + /// Returns `Ok(Some(value))` if the attribute is present. + #[doc(alias = "nix_get_attr_byname")] + #[doc(alias = "get_attr_byname")] + #[doc(alias = "get_attr_opt")] + #[doc(alias = "try_get")] + #[doc(alias = "maybe_get")] + pub fn require_attrs_select_opt( + &mut self, + v: &Value, + attr_name: &str, + ) -> Result> { + let t = self.value_type(v)?; + if t != ValueType::AttrSet { + bail!("expected an attrset, but got a {:?}", t); + } + let attr_name = CString::new(attr_name) + .with_context(|| "require_attrs_select_opt: attrName contains null byte")?; + let v2 = unsafe { + check_call_opt_key!(raw::get_attr_byname( + &mut self.context, + v.raw_ptr(), + self.eval_state.as_ptr(), + attr_name.as_ptr() + )) + }?; + Ok(v2.map(|x| unsafe { Value::new(x) })) + } + + /// Returns the number of elements in a [list][`ValueType::List`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) of the list structure and verifies the value is a list. + /// + /// Individual elements remain as lazy [thunks](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) and are not evaluated. + #[doc(alias = "length")] + #[doc(alias = "count")] + #[doc(alias = "len")] + #[doc(alias = "nix_get_list_size")] + #[doc(alias = "get_list_size")] + pub fn require_list_size(&mut self, v: &Value) -> Result { + let t = self.value_type(v)?; + if t != ValueType::List { + bail!("expected a list, but got a {:?}", t); + } + let ret = unsafe { check_call!(raw::get_list_size(&mut self.context, v.raw_ptr())) }?; + Ok(ret) + } + + /// Extracts an element from a [list][`ValueType::List`] Nix value by index. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is a list. + /// Forces evaluation of the selected element, similar to [`Self::require_attrs_select`]. + /// + /// Returns `Ok(Some(value))` if the element is found. + /// + /// Returns `Ok(None)` if the index is out of bounds. + /// + /// Returns [`Err`] if evaluation failed, the element contains an error (e.g., `throw`), or the value is not a list. + #[doc(alias = "get")] + #[doc(alias = "index")] + #[doc(alias = "at")] + #[doc(alias = "nix_get_list_byidx")] + #[doc(alias = "get_list_byidx")] + pub fn require_list_select_idx_strict(&mut self, v: &Value, idx: u32) -> Result> { + let t = self.value_type(v)?; + if t != ValueType::List { + bail!("expected a list, but got a {:?}", t); + } + + // TODO: Remove this bounds checking once https://github.com/NixOS/nix/pull/14030 + // is merged, which will add proper bounds checking to the underlying C API. + // Currently we perform bounds checking in Rust to avoid undefined behavior. + let size = unsafe { check_call!(raw::get_list_size(&mut self.context, v.raw_ptr())) }?; + + if idx >= size { + return Ok(None); + } + + let v2 = unsafe { + check_call_opt_key!(raw::get_list_byidx( + &mut self.context, + v.raw_ptr(), + self.eval_state.as_ptr(), + idx + )) + }?; + Ok(v2.map(|x| unsafe { Value::new(x) })) + } + + /// Creates a new [string][`ValueType::String`] Nix value. + /// + /// Returns a string value without any [string context](https://nix.dev/manual/nix/latest/language/string-context.html). + #[doc(alias = "make_string")] + #[doc(alias = "create_string")] + #[doc(alias = "string_value")] + pub fn new_value_str(&mut self, s: &str) -> Result { + let s = CString::new(s).with_context(|| "new_value_str: contains null byte")?; + let v = unsafe { + let value = self.new_value_uninitialized()?; + check_call!(raw::init_string( + &mut self.context, + value.raw_ptr(), + s.as_ptr() + ))?; + value + }; + Ok(v) + } + + /// Creates a new [integer][`ValueType::Int`] Nix value. + #[doc(alias = "make_int")] + #[doc(alias = "create_int")] + #[doc(alias = "int_value")] + #[doc(alias = "integer_value")] + pub fn new_value_int(&mut self, i: Int) -> Result { + let v = unsafe { + let value = self.new_value_uninitialized()?; + check_call!(raw::init_int(&mut self.context, value.raw_ptr(), i))?; + value + }; + Ok(v) + } + + /// Creates a new [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) Nix value. + /// + /// The [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) will lazily evaluate to the result of the given Rust function when forced. + /// The Rust function will be called with the current [`EvalState`] and must not return a thunk. + /// + /// The name is shown in stack traces. + #[doc(alias = "make_thunk")] + #[doc(alias = "create_thunk")] + #[doc(alias = "lazy_value")] + pub fn new_value_thunk( + &mut self, + name: &str, + f: Box Result>, + ) -> Result { + // Nix doesn't have a function for creating a thunk, so we have to + // create a function and pass it a dummy argument. + let name = CString::new(name).with_context(|| "new_thunk: name contains null byte")?; + let primop = primop::PrimOp::new( + self, + primop::PrimOpMeta { + // name is observable in stack traces, ie if the thunk returns Err + name: name.as_c_str(), + // doc is unlikely to be observable, so we provide a constant one for simplicity. + doc: cstr!("Performs an on demand computation, implemented outside the Nix language in native code."), + // like doc, unlikely to be observed + args: [CString::new("internal_unused").unwrap().as_c_str()], + }, + Box::new(move |eval_state, _dummy: &[Value; 1]| f(eval_state)), + )?; + + let p = self.new_value_primop(primop)?; + self.new_value_apply(&p, &p) + } + + /// Not exposed, because the caller must always explicitly handle the context or not accept one at all. + fn get_string(&mut self, value: &Value) -> Result { + let mut r = result_string_init!(); + unsafe { + check_call!(raw::get_string( + &mut self.context, + value.raw_ptr(), + Some(callback_get_result_string), + callback_get_result_string_data(&mut r) + ))?; + }; + r + } + /// Extracts a string value from a [string][`ValueType::String`] Nix value. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) and verifies the value is a string. + /// Returns the string value if successful, or an [`Err`] if evaluation failed or the value is not a string. + /// + /// NOTE: this will be replaced by two methods, one that also returns the context, and one that checks that the context is empty. + #[doc(alias = "str")] + #[doc(alias = "text")] + #[doc(alias = "nix_get_string")] + #[doc(alias = "get_string")] + pub fn require_string(&mut self, value: &Value) -> Result { + let t = self.value_type(value)?; + if t != ValueType::String { + bail!("expected a string, but got a {:?}", t); + } + self.get_string(value) + } + /// Realises a [string][`ValueType::String`] Nix value with context information. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html), verifies the value is a string, and builds any derivations + /// referenced in the [string context](https://nix.dev/manual/nix/latest/language/string-context.html) if required. + #[doc(alias = "realize_string")] + #[doc(alias = "string_with_context")] + #[doc(alias = "build_string")] + pub fn realise_string( + &mut self, + value: &Value, + is_import_from_derivation: bool, + ) -> Result { + let t = self.value_type(value)?; + if t != ValueType::String { + bail!("expected a string, but got a {:?}", t); + } + + let rs = unsafe { + check_call!(raw::string_realise( + &mut self.context, + self.eval_state.as_ptr(), + value.raw_ptr(), + is_import_from_derivation + )) + }?; + + let s = unsafe { + let start = raw::realised_string_get_buffer_start(rs) as *const u8; + let size = raw::realised_string_get_buffer_size(rs); + let slice = std::slice::from_raw_parts(start, size); + String::from_utf8(slice.to_vec()) + .map_err(|e| anyhow::format_err!("Nix string is not valid UTF-8: {}", e))? + }; + + let paths = unsafe { + let n = raw::realised_string_get_store_path_count(rs); + let mut paths = Vec::with_capacity(n as usize); + for i in 0..n { + let path = raw::realised_string_get_store_path(rs, i); + let path = NonNull::new(path as *mut raw_store::StorePath).ok_or_else(|| { + anyhow::format_err!( + "nix_realised_string_get_store_path returned a null pointer" + ) + })?; + paths.push(StorePath::new_raw_clone(path)); + } + paths + }; + + // We've converted the nix_realised_string to a native struct containing copies, so we can free it now. + unsafe { + raw::realised_string_free(rs); + } + + Ok(RealisedString { s, paths }) + } + + /// Applies a function to an argument and returns the result. + /// + /// Forces [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) of the function application. + /// For a lazy version, see [`Self::new_value_apply`]. + #[doc(alias = "nix_value_call")] + #[doc(alias = "value_call")] + #[doc(alias = "apply")] + #[doc(alias = "invoke")] + #[doc(alias = "execute")] + pub fn call(&mut self, f: Value, a: Value) -> Result { + let value = self.new_value_uninitialized()?; + unsafe { + check_call!(raw::value_call( + &mut self.context, + self.eval_state.as_ptr(), + f.raw_ptr(), + a.raw_ptr(), + value.raw_ptr() + )) + }?; + Ok(value) + } + + /// Apply a sequence of [function applications](https://nix.dev/manual/nix/latest/language/operators.html#function-application). + /// + /// When argument `f` is a curried function, this applies each argument in sequence. + /// Equivalent to the Nix expression `f arg1 arg2 arg3`. + /// + /// Returns a [`Value`] in at least weak head normal form if successful. + /// + /// Returns an [`Err`] + /// - if `f` did not evaluate to a function + /// - if `f arg1` had any problems + /// - if `f arg1` did not evaluate to a function (for `(f arg1) arg2`) + /// - etc + /// + /// # Examples + /// + /// ```rust + /// # use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread}; + /// # use nix_bindings_store::store::Store; + /// # use std::collections::HashMap; + /// # fn example() -> anyhow::Result<()> { + /// # test_init(); + /// # let guard = gc_register_my_thread()?; + /// let store = Store::open(None, HashMap::new())?; + /// let mut es = EvalState::new(store, [])?; + /// + /// // Create a curried function: x: y: x + y + /// let f = es.eval_from_string("x: y: x + y", "")?; + /// let arg1 = es.eval_from_string("5", "")?; + /// let arg2 = es.eval_from_string("3", "")?; + /// + /// // Equivalent to: (x: y: x + y) 5 3 + /// let result = es.call_multi(&f, &[arg1, arg2])?; + /// let value = es.require_int(&result)?; + /// assert_eq!(value, 8); + /// # drop(guard); + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "nix_value_call_multi")] + #[doc(alias = "value_call_multi")] + #[doc(alias = "apply_multi")] + #[doc(alias = "curry")] + #[doc(alias = "call_with_args")] + pub fn call_multi(&mut self, f: &Value, args: &[Value]) -> Result { + let value = self.new_value_uninitialized()?; + unsafe { + let mut args_ptrs = args.iter().map(|a| a.raw_ptr()).collect::>(); + check_call!(raw::value_call_multi( + &mut self.context, + self.eval_state.as_ptr(), + f.raw_ptr(), + args_ptrs.len(), + args_ptrs.as_mut_ptr(), + value.raw_ptr() + )) + }?; + Ok(value) + } + + /// Applies a function to an argument lazily, creating a [thunk](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness). + /// + /// Does not force [evaluation](https://nix.dev/manual/nix/latest/language/evaluation.html) of the function application. + /// For an eager version, see [`Self::call`]. + #[doc(alias = "lazy_apply")] + #[doc(alias = "thunk_apply")] + #[doc(alias = "defer_call")] + pub fn new_value_apply(&mut self, f: &Value, a: &Value) -> Result { + let value = self.new_value_uninitialized()?; + unsafe { + check_call!(raw::init_apply( + &mut self.context, + value.raw_ptr(), + f.raw_ptr(), + a.raw_ptr() + )) + }?; + Ok(value) + } + + fn new_value_uninitialized(&mut self) -> Result { + unsafe { + let value = check_call!(raw::alloc_value( + &mut self.context, + self.eval_state.as_ptr() + ))?; + Ok(Value::new(value)) + } + } + + /// Creates a new [function][`ValueType::Function`] Nix value implemented by a Rust function. + /// + /// This is also known as a "primop" in Nix, short for primitive operation. + /// Most of the `builtins.*` values are examples of primops, but this function + /// does not affect `builtins`. + #[doc(alias = "make_primop")] + #[doc(alias = "create_function")] + #[doc(alias = "builtin")] + pub fn new_value_primop(&mut self, primop: primop::PrimOp) -> Result { + let value = self.new_value_uninitialized()?; + unsafe { + check_call!(raw::init_primop( + &mut self.context, + value.raw_ptr(), + primop.ptr + ))?; + }; + Ok(value) + } + + /// Creates a new [attribute set][`ValueType::AttrSet`] Nix value from an iterator of name-value pairs. + /// + /// Accepts any iterator that yields `(String, Value)` pairs and has an exact size. + /// Common usage includes [`Vec`], [`std::collections::HashMap`], and array literals. + /// + /// # Examples + /// + /// ```rust + /// # use nix_bindings_expr::eval_state::{EvalState, test_init, gc_register_my_thread}; + /// # use nix_bindings_store::store::Store; + /// # use std::collections::HashMap; + /// # fn example() -> anyhow::Result<()> { + /// # test_init(); + /// # let guard = gc_register_my_thread()?; + /// let store = Store::open(None, HashMap::new())?; + /// let mut es = EvalState::new(store, [])?; + /// let a = es.new_value_int(1)?; + /// let b = es.new_value_int(2)?; + /// let c = es.new_value_int(3)?; + /// let d = es.new_value_int(4)?; + /// + /// // From array + /// let attrs1 = es.new_value_attrs([ + /// ("x".to_string(), a), + /// ("y".to_string(), b) + /// ])?; + /// + /// // From HashMap + /// let mut map = HashMap::new(); + /// map.insert("foo".to_string(), c); + /// map.insert("bar".to_string(), d); + /// let attrs2 = es.new_value_attrs(map)?; + /// # drop(guard); + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "make_attrs")] + #[doc(alias = "create_attrset")] + #[doc(alias = "object")] + #[doc(alias = "record")] + pub fn new_value_attrs(&mut self, attrs: I) -> Result + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + let iter = attrs.into_iter(); + let size = iter.len(); + let bindings_builder = BindingsBuilder::new(self, size)?; + for (name, value) in iter { + let name = + CString::new(name).with_context(|| "new_value_attrs: name contains null byte")?; + unsafe { + check_call!(raw::bindings_builder_insert( + &mut self.context, + bindings_builder.ptr, + name.as_ptr(), + value.raw_ptr() + ))?; + } + } + let value = self.new_value_uninitialized()?; + unsafe { + check_call!(raw::make_attrs( + &mut self.context, + value.raw_ptr(), + bindings_builder.ptr + ))?; + } + Ok(value) + } +} + +// Internal RAII helper; could be refactored and made pub +struct BindingsBuilder { + ptr: *mut raw::BindingsBuilder, +} +impl Drop for BindingsBuilder { + fn drop(&mut self) { + unsafe { + raw::bindings_builder_free(self.ptr); + } + } +} +impl BindingsBuilder { + fn new(eval_state: &mut EvalState, capacity: usize) -> Result { + let ptr = unsafe { + check_call!(raw::make_bindings_builder( + &mut eval_state.context, + eval_state.eval_state.as_ptr(), + capacity + )) + }?; + Ok(BindingsBuilder { ptr }) + } +} + +/// Triggers garbage collection immediately. +#[doc(alias = "garbage_collect")] +#[doc(alias = "collect")] +#[doc(alias = "gc")] +pub fn gc_now() { + unsafe { + raw::gc_now(); + } +} + +/// RAII guard for thread registration with the garbage collector. +/// +/// Automatically unregisters the thread when dropped. +pub struct ThreadRegistrationGuard { + must_unregister: bool, +} +impl Drop for ThreadRegistrationGuard { + fn drop(&mut self) { + if self.must_unregister { + unsafe { + gc::GC_unregister_my_thread(); + } + } + } +} + +fn gc_register_my_thread_do_it() -> Result<()> { + unsafe { + let mut sb: gc::GC_stack_base = gc::GC_stack_base { + mem_base: null_mut(), + }; + let r = gc::GC_get_stack_base(&mut sb); + if r as u32 != gc::GC_SUCCESS { + Err(anyhow::format_err!("GC_get_stack_base failed: {}", r))?; + } + gc::GC_register_my_thread(&sb); + Ok(()) + } +} + +#[doc(alias = "register_thread")] +#[doc(alias = "thread_setup")] +#[doc(alias = "gc_register")] +pub fn gc_register_my_thread() -> Result { + init()?; + unsafe { + let already_done = gc::GC_thread_is_registered(); + if already_done != 0 { + return Ok(ThreadRegistrationGuard { + must_unregister: false, + }); + } + gc_register_my_thread_do_it()?; + Ok(ThreadRegistrationGuard { + must_unregister: true, + }) + } +} + +impl Clone for EvalState { + fn clone(&self) -> Self { + EvalState { + eval_state: self.eval_state.clone(), + store: self.store.clone(), + context: Context::new(), + } + } +} + +/// Initialize the Nix library for testing. This includes some modifications to the Nix settings, that must not be used in production. +/// Use at your own peril, in rust test suites. +#[doc(alias = "test_initialize")] +#[doc(alias = "test_setup")] +pub fn test_init() { + init().unwrap(); + + // During development, we encountered a problem where the build hook + // would cause the test suite to reinvokes itself, causing an infinite loop. + // While _NIX_TEST_NO_SANDBOX=1 should prevent this, we may also set the + // build hook to "" to prevent this. + nix_bindings_util::settings::set("build-hook", "").unwrap(); + + // When testing in the sandbox, the default build dir would be a parent of the storeDir, + // which causes an error. So we set a custom build dir here. + // Only available on linux + if cfg!(target_os = "linux") { + nix_bindings_util::settings::set("sandbox-build-dir", "/custom-build-dir-for-test") + .unwrap(); + } + std::env::set_var("_NIX_TEST_NO_SANDBOX", "1"); + + // The tests run offline + nix_bindings_util::settings::set("substituters", "").unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + use cstr::cstr; + use ctor::ctor; + use std::collections::HashMap; + use std::fs::read_dir; + use std::io::Write as _; + use std::sync::{Arc, Mutex}; + + #[ctor] + fn setup() { + test_init(); + + // Configure Nix settings for the test suite + // Set max-call-depth to 1000 (lower than default 10000) for the + // eval_state_builder_loads_max_call_depth test case, while + // giving other tests sufficient room for normal evaluation. + std::env::set_var("NIX_CONFIG", "max-call-depth = 1000"); + } + + /// Run a function while making sure that the current thread is registered with the GC. + pub fn gc_registering_current_thread(f: F) -> Result + where + F: FnOnce() -> R, + { + let guard = gc_register_my_thread()?; + let r = f(); + drop(guard); + Ok(r) + } + + #[test] + fn eval_state_new_and_drop() { + gc_registering_current_thread(|| { + // very basic test: make sure initialization doesn't crash + let store = Store::open(None, HashMap::new()).unwrap(); + let _e = EvalState::new(store, []).unwrap(); + }) + .unwrap(); + } + + #[test] + fn weak_ref() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + let weak = es.weak_ref(); + let _es = weak.upgrade().unwrap(); + }) + .unwrap(); + } + + #[test] + fn weak_ref_gone() { + gc_registering_current_thread(|| { + let weak = { + // Use a slightly different URL which is unique in the test suite, to bypass the global store cache + let store = Store::open(Some("auto?foo=bar"), HashMap::new()).unwrap(); + let es = EvalState::new(store, []).unwrap(); + es.weak_ref() + }; + assert!(weak.upgrade().is_none()); + assert!(weak.store.upgrade().is_none()); + assert!(weak.inner.upgrade().is_none()); + }) + .unwrap(); + } + + #[test] + fn eval_state_lookup_path() { + let import_expression = "import + import "; + let integer0 = 83; + let integer1 = 103; + let mut test_file0 = tempfile::NamedTempFile::new().unwrap(); + let mut test_file1 = tempfile::NamedTempFile::new().unwrap(); + writeln!(test_file0, "{integer0}").unwrap(); + writeln!(test_file1, "{integer1}").unwrap(); + gc_registering_current_thread(|| { + let mut es = EvalState::new(Store::open(None, HashMap::new()).unwrap(), []).unwrap(); + assert!(es.eval_from_string(import_expression, "").is_err()); + + let mut es = EvalState::new( + Store::open(None, HashMap::new()).unwrap(), + [ + format!("test_file0={}", test_file0.path().to_str().unwrap()).as_str(), + format!("test_file1={}", test_file1.path().to_str().unwrap()).as_str(), + ], + ) + .unwrap(); + let ie = &es.eval_from_string(import_expression, "").unwrap(); + let v = es.require_int(ie).unwrap(); + assert_eq!(v, integer0 + integer1); + }) + .unwrap(); + test_file0.close().unwrap(); + test_file1.close().unwrap(); + } + + #[test] + fn eval_state_eval_from_string() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("1", "").unwrap(); + let v2 = v.clone(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); + let t2 = es.value_type_unforced(&v2); + assert!(t2 == Some(ValueType::Int)); + gc_now(); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_bool() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("true", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Bool)); + let b = es.require_bool(&v).unwrap(); + assert!(b); + + let v = es.eval_from_string("false", "").unwrap(); + es.require_bool(&v).unwrap(); + let b = es.require_bool(&v).unwrap(); + assert!(!b); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_int() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("1", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v).unwrap(); + assert!(t == ValueType::Int); + let i = es.require_int(&v).unwrap(); + assert!(i == 1); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_int_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let v = es.new_value_apply(&f, &a).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t.is_none()); + let i = es.require_int(&v).unwrap(); + assert!(i == 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_bool_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: !x", "").unwrap(); + let a = es.eval_from_string("true", "").unwrap(); + let v = es.new_value_apply(&f, &a).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t.is_none()); + let i = es.require_bool(&v).unwrap(); + assert!(!i); + }) + .unwrap(); + } + + /// A helper that turns an expression into a thunk. + fn make_thunk(es: &mut EvalState, expr: &str) -> Value { + // This would be silly in real code, but it works for the current Nix implementation. + // A Nix implementation that applies the identity function eagerly would be a valid + // Nix implementation, but annoying because we'll have to change this helper to do + // something more complicated that isn't optimized away. + let f = es.eval_from_string("x: x", "").unwrap(); + let v = es.eval_from_string(expr, "").unwrap(); + es.new_value_apply(&f, &v).unwrap() + } + + #[test] + fn make_thunk_helper_works() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "1"); + let t = es.value_type_unforced(&v); + assert!(t.is_none()); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_attrs_names_empty() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("{ }", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::AttrSet)); + let attrs = es.require_attrs_names_unsorted(&v).unwrap(); + assert_eq!(attrs.len(), 0); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_names_unsorted_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "{ a = 1; b = 2; }"); + let t = es.value_type_unforced(&v); + assert!(t.is_none()); + let attrs = es.require_attrs_names_unsorted(&v).unwrap(); + assert_eq!(attrs.len(), 2); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_names_unsorted_bad_type() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("1", "").unwrap(); + es.force(&v).unwrap(); + let r = es.require_attrs_names_unsorted(&v); + assert!(r.is_err()); + assert_eq!( + r.unwrap_err().to_string(), + "expected an attrset, but got a Int" + ); + }) + .unwrap() + } + + #[test] + fn eval_state_value_attrs_names_example() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = throw "nope a"; b = throw "nope b"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let attrs = es.require_attrs_names(&v).unwrap(); + assert_eq!(attrs.len(), 2); + assert_eq!(attrs[0], "a"); + assert_eq!(attrs[1], "b"); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_attrs_select() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = "aye"; b = "bee"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let a = es.require_attrs_select(&v, "a").unwrap(); + let b = es.require_attrs_select(&v, "b").unwrap(); + assert_eq!(es.require_string(&a).unwrap(), "aye"); + assert_eq!(es.require_string(&b).unwrap(), "bee"); + let missing = es.require_attrs_select(&v, "c"); + match missing { + Ok(_) => panic!("expected an error"), + Err(e) => { + let s = format!("{e:#}"); + if !s.contains("attribute `c` not found") { + eprintln!("unexpected error message: {}", s); + panic!(); + } + } + } + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = "aye"; b = "bee"; }"#; + let v = make_thunk(&mut es, expr); + assert!(es.value_type_unforced(&v).is_none()); + let r = es.require_attrs_select(&v, "a"); + assert!(r.is_ok()); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = throw "oh no the error"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let r = es.require_attrs_select(&v, "a"); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("oh no the error") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + } + } + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_opt() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = "aye"; b = "bee"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let a = es.require_attrs_select_opt(&v, "a").unwrap().unwrap(); + let b = es.require_attrs_select_opt(&v, "b").unwrap().unwrap(); + assert_eq!(es.require_string(&a).unwrap(), "aye"); + assert_eq!(es.require_string(&b).unwrap(), "bee"); + let c = es.require_attrs_select_opt(&v, "c").unwrap(); + assert!(c.is_none()); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_opt_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = "aye"; b = "bee"; }"#; + let v = make_thunk(&mut es, expr); + assert!(es.value_type_unforced(&v).is_none()); + let r = es.require_attrs_select_opt(&v, "a"); + assert!(r.is_ok()); + }) + .unwrap() + } + + #[test] + fn eval_state_require_attrs_select_opt_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let expr = r#"{ a = throw "oh no the error"; }"#; + let v = es.eval_from_string(expr, "").unwrap(); + let r = es.require_attrs_select_opt(&v, "a"); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("oh no the error") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + } + } + }) + .unwrap() + } + + #[test] + fn eval_state_value_string() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("\"hello\"", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); + let s = es.require_string(&v).unwrap(); + assert!(s == "hello"); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_string_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "\"hello\""); + assert!(es.value_type_unforced(&v).is_none()); + let s = es.require_string(&v).unwrap(); + assert!(s == "hello"); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_string_unexpected_bool() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("true", "").unwrap(); + es.force(&v).unwrap(); + let r = es.require_string(&v); + assert!(r.is_err()); + // TODO: safe print value (like Nix would) + assert_eq!( + r.unwrap_err().to_string(), + "expected a string, but got a Bool" + ); + }) + .unwrap() + } + + #[test] + fn eval_state_value_string_unexpected_path_value() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("/foo", "").unwrap(); + es.force(&v).unwrap(); + let r = es.require_string(&v); + assert!(r.is_err()); + assert_eq!( + r.unwrap_err().to_string(), + "expected a string, but got a Path" + ); + }) + .unwrap() + } + + #[test] + fn eval_state_value_string_bad_utf() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es + .eval_from_string("builtins.substring 0 1 \"ü\"", "") + .unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); + let r = es.require_string(&v); + assert!(r.is_err()); + assert!(r + .unwrap_err() + .to_string() + .contains("Nix string is not valid UTF-8")); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_string_unexpected_context() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es + .eval_from_string("(derivation { name = \"hello\"; system = \"dummy\"; builder = \"cmd.exe\"; }).outPath", "") + .unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); + // TODO + // let r = es.require_string_without_context(&v); + // assert!(r.is_err()); + // assert!(r.unwrap_err().to_string().contains("unexpected context")); + }) + .unwrap(); + } + + #[test] + fn eval_state_new_string() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.new_value_str("hello").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); + let s = es.require_string(&v).unwrap(); + assert!(s == "hello"); + }) + .unwrap(); + } + + #[test] + fn eval_state_new_string_empty() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.new_value_str("").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::String)); + let s = es.require_string(&v).unwrap(); + assert!(s.is_empty()); + }) + .unwrap(); + } + + #[test] + fn eval_state_new_string_invalid() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let r = es.new_value_str("hell\0no"); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("contains null byte") { + eprintln!("{}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_new_int() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.new_value_int(42).unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); + let i = es.require_int(&v).unwrap(); + assert!(i == 42); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_attrset() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("{ }", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::AttrSet)); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[ ]", "").unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::List)); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_empty() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[]", "").unwrap(); + es.force(&v).unwrap(); + let list: Vec = es.require_list_strict(&v).unwrap(); + assert_eq!(list.len(), 0); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_int() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[42]", "").unwrap(); + es.force(&v).unwrap(); + let list: Vec = es.require_list_strict(&v).unwrap(); + assert_eq!(list.len(), 1); + assert_eq!(es.require_int(&list[0]).unwrap(), 42); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_int_bool() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[42 true]", "").unwrap(); + es.force(&v).unwrap(); + let list: Vec = es.require_list_strict(&v).unwrap(); + assert_eq!(list.len(), 2); + assert_eq!(es.require_int(&list[0]).unwrap(), 42); + assert!(es.require_bool(&list[1]).unwrap()); + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string(r#"[(throw "_evaluated_item_")]"#, "").unwrap(); + es.force(&v).unwrap(); + // This should fail because require_list_strict evaluates all elements + let result: Result, _> = es.require_list_strict(&v); + assert!(result.is_err()); + match result { + Err(error_msg) => { + let error_str = error_msg.to_string(); + assert!(error_str.contains("_evaluated_item_")); + } + Ok(_) => panic!("unexpected success. The item should have been evaluated and its error propagated.") + } + }) + .unwrap(); + } + + #[test] + fn eval_state_value_list_strict_generic_container() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[1 2 3]", "").unwrap(); + + // Test with Vec + let vec: Vec = es.require_list_strict(&v).unwrap(); + assert_eq!(vec.len(), 3); + + // Test with VecDeque + let deque: std::collections::VecDeque = es.require_list_strict(&v).unwrap(); + assert_eq!(deque.len(), 3); + + // Verify contents are the same + assert_eq!(es.require_int(&vec[0]).unwrap(), 1); + assert_eq!(es.require_int(&deque[0]).unwrap(), 1); + }) + .unwrap(); + } + + #[test] + fn eval_state_realise_string() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let expr = r#" + '' + a derivation output: ${ + derivation { name = "letsbuild"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + }} + a path: ${builtins.toFile "just-a-file" "ooh file good"} + a derivation path by itself: ${ + builtins.unsafeDiscardOutputDependency + (derivation { + name = "not-actually-built-yet"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + }).drvPath} + '' + "#; + let v = es.eval_from_string(expr, "").unwrap(); + es.force(&v).unwrap(); + let rs = es.realise_string(&v, false).unwrap(); + + assert!(rs.s.starts_with("a derivation output:")); + assert!(rs.s.contains("-letsbuild\n")); + assert!(!rs.s.contains("-letsbuild.drv")); + assert!(rs.s.contains("a path:")); + assert!(rs.s.contains("-just-a-file")); + assert!(!rs.s.contains("-just-a-file.drv")); + assert!(!rs.s.contains("ooh file good")); + assert!(rs.s.ends_with("-not-actually-built-yet.drv\n")); + + assert_eq!(rs.paths.len(), 3); + let mut names: Vec = rs.paths.iter().map(|p| p.name().unwrap()).collect(); + names.sort(); + assert_eq!(names[0], "just-a-file"); + assert_eq!(names[1], "letsbuild"); + assert_eq!(names[2], "not-actually-built-yet.drv"); + }) + .unwrap(); + } + + #[test] + fn eval_state_call() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let v = es.call(f, a).unwrap(); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); + let i = es.require_int(&v).unwrap(); + assert!(i == 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_call_multi() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + // This is a function that takes two arguments. + let f = es.eval_from_string("x: y: x - y", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let b = es.eval_from_string("3", "").unwrap(); + let v = es.call_multi(&f, &[a, b]).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); + let i = es.require_int(&v).unwrap(); + assert!(i == -1); + }) + .unwrap(); + } + + #[test] + fn eval_state_apply() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + // This is a function that takes two arguments. + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let v = es.new_value_apply(&f, &a).unwrap(); + assert!(es.value_type_unforced(&v).is_none()); + es.force(&v).unwrap(); + let t = es.value_type_unforced(&v); + assert!(t == Some(ValueType::Int)); + let i = es.require_int(&v).unwrap(); + assert!(i == 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_call_fail_body() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("true", "").unwrap(); + let r = es.call(f, a); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("cannot coerce") { + eprintln!("{}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_call_multi_fail_body() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + // This is a function that takes two arguments. + let f = es.eval_from_string("x: y: x - y", "").unwrap(); + let a = es.eval_from_string("2", "").unwrap(); + let b = es.eval_from_string("true", "").unwrap(); + let r = es.call_multi(&f, &[a, b]); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("expected an integer but found") { + eprintln!("{}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_apply_fail_body() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("x: x + 1", "").unwrap(); + let a = es.eval_from_string("true", "").unwrap(); + // Lazy => no error + let r = es.new_value_apply(&f, &a).unwrap(); + // Force it => error + let res = es.force(&r); + match res { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("cannot coerce") { + eprintln!("{}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + /// This tests the behavior of `call`, which is strict, unlike `new_value_apply`. + #[test] + fn eval_state_call_fail_args() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("{x}: x + 1", "").unwrap(); + let a = es.eval_from_string("{}", "").unwrap(); + let r = es.call(f, a); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("called without required argument") { + eprintln!("{}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_call_multi_fail_args() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + // This is a function that takes two arguments. + let f = es.eval_from_string("{x}: {y}: x - y", "").unwrap(); + let a = es.eval_from_string("{x = 2;}", "").unwrap(); + let b = es.eval_from_string("{}", "").unwrap(); + let r = es.call_multi(&f, &[a, b]); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("called without required argument") { + eprintln!("{}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + /// This tests the behavior of `new_value_apply`, which is lazy, unlike `call`. + #[test] + fn eval_state_apply_fail_args_lazy() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = es.eval_from_string("{x}: x + 1", "").unwrap(); + let a = es.eval_from_string("{}", "").unwrap(); + // Lazy => no error + let r = es.new_value_apply(&f, &a).unwrap(); + // Force it => error + let res = es.force(&r); + match res { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("called without required argument") { + eprintln!("{}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + fn store_open_params() { + gc_registering_current_thread(|| { + let store = tempfile::tempdir().unwrap(); + let store_path = store.path().to_str().unwrap(); + let state = tempfile::tempdir().unwrap(); + let state_path = state.path().to_str().unwrap(); + let log = tempfile::tempdir().unwrap(); + let log_path = log.path().to_str().unwrap(); + + let mut es = EvalState::new( + Store::open( + Some("local"), + HashMap::from([ + ("store", store_path), + ("state", state_path), + ("log", log_path), + ]) + .iter() + .map(|(a, b)| (*a, *b)), + ) + .unwrap(), + [], + ) + .unwrap(); + + let expr = r#" + '' + a derivation output: ${ + derivation { name = "letsbuild"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + }} + a path: ${builtins.toFile "just-a-file" "ooh file good"} + a derivation path by itself: ${ + builtins.unsafeDiscardOutputDependency + (derivation { + name = "not-actually-built-yet"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + }).drvPath} + '' + "#; + let derivations: [&[u8]; 3] = [ + b"letsbuild.drv", + b"just-a-file", + b"not-actually-built-yet.drv", + ]; + let _ = es.eval_from_string(expr, "").unwrap(); + + // assert that all three `derivations` are inside the store and the `state` directory is not empty either. + let store_contents: Vec<_> = read_dir(store.path()) + .unwrap() + .map(|dir_entry| dir_entry.unwrap().file_name()) + .collect(); + for derivation in derivations { + assert!(store_contents + .iter() + .any(|f| f.as_encoded_bytes().ends_with(derivation))); + } + assert!(!empty(read_dir(state.path()).unwrap())); + + store.close().unwrap(); + state.close().unwrap(); + log.close().unwrap(); + }) + .unwrap(); + } + + fn empty(foldable: impl IntoIterator) -> bool { + foldable.into_iter().all(|_| false) + } + + #[test] + fn eval_state_primop_anon_call() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let bias: Arc> = Arc::new(Mutex::new(0)); + let bias_control = bias.clone(); + + let primop = primop::PrimOp::new( + &mut es, + primop::PrimOpMeta { + name: cstr!("testFunction"), + args: [cstr!("a"), cstr!("b")], + doc: cstr!("anonymous test function"), + }, + Box::new(move |es, [a, b]| { + let a = es.require_int(a)?; + let b = es.require_int(b)?; + let c = *bias.lock().unwrap(); + es.new_value_int(a + b + c) + }), + ) + .unwrap(); + + let f = es.new_value_primop(primop).unwrap(); + + { + *bias_control.lock().unwrap() = 10; + } + let a = es.new_value_int(2).unwrap(); + let b = es.new_value_int(3).unwrap(); + let fa = es.call(f, a).unwrap(); + let v = es.call(fa, b).unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v).unwrap(); + assert!(t == ValueType::Int); + let i = es.require_int(&v).unwrap(); + assert!(i == 15); + }) + .unwrap(); + } + + #[test] + fn eval_state_primop_anon_call_throw() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let f = { + let es: &mut EvalState = &mut es; + let prim = primop::PrimOp::new( + es, + primop::PrimOpMeta { + name: cstr!("throwingTestFunction"), + args: [cstr!("arg")], + doc: cstr!("anonymous test function"), + }, + Box::new(move |es, [a]| { + let a = es.require_int(a)?; + bail!("error with arg [{}]", a); + }), + ) + .unwrap(); + + es.new_value_primop(prim) + } + .unwrap(); + let a = es.new_value_int(2).unwrap(); + let r = es.call(f, a); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("error with arg [2]") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_primop_anon_call_no_args() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es + .new_value_thunk( + "test_thunk", + Box::new(move |es: &mut EvalState| es.new_value_int(42)), + ) + .unwrap(); + es.force(&v).unwrap(); + let t = es.value_type(&v).unwrap(); + eprintln!("{:?}", t); + assert!(t == ValueType::Int); + let i = es.require_int(&v).unwrap(); + assert!(i == 42); + }) + .unwrap(); + } + + #[test] + fn eval_state_primop_anon_call_no_args_lazy() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es + .new_value_thunk( + "test_thunk", + Box::new(move |_| { + bail!("error message in test case eval_state_primop_anon_call_no_args_lazy") + }), + ) + .unwrap(); + let r = es.force(&v); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains( + "error message in test case eval_state_primop_anon_call_no_args_lazy", + ) { + eprintln!("unexpected error message: {}", e); + panic!(); + } + if !e.to_string().contains("test_thunk") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + pub fn eval_state_primop_custom() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let primop = primop::PrimOp::new( + &mut es, + primop::PrimOpMeta { + name: cstr!("frobnicate"), + doc: cstr!("Frobnicates widgets"), + args: [cstr!("x"), cstr!("y")], + }, + Box::new(|es, args| { + let a = es.require_int(&args[0])?; + let b = es.require_int(&args[1])?; + es.new_value_int(a + b) + }), + ) + .unwrap(); + let f = es.new_value_primop(primop).unwrap(); + let a = es.new_value_int(2).unwrap(); + let b = es.new_value_int(3).unwrap(); + let fa = es.call(f, a).unwrap(); + let fb = es.call(fa, b).unwrap(); + es.force(&fb).unwrap(); + let t = es.value_type(&fb).unwrap(); + assert!(t == ValueType::Int); + let i = es.require_int(&fb).unwrap(); + assert!(i == 5); + }) + .unwrap(); + } + + #[test] + pub fn eval_state_primop_custom_throw() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let primop = primop::PrimOp::new( + &mut es, + primop::PrimOpMeta { + name: cstr!("frobnicate"), + doc: cstr!("Frobnicates widgets"), + args: [cstr!("x")], + }, + Box::new(|_es, _args| bail!("The frob unexpectedly fizzled")), + ) + .unwrap(); + let f = es.new_value_primop(primop).unwrap(); + let a = es.new_value_int(0).unwrap(); + match es.call(f, a) { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("The frob unexpectedly fizzled") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + if !e.to_string().contains("frobnicate") { + eprintln!("unexpected error message: {}", e); + panic!(); + } + } + } + }) + .unwrap(); + } + + #[test] + pub fn eval_state_new_value_attrs_from_slice_empty() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let attrs = es.new_value_attrs([]).unwrap(); + let t = es.value_type(&attrs).unwrap(); + assert!(t == ValueType::AttrSet); + let names = es.require_attrs_names(&attrs).unwrap(); + assert!(names.is_empty()); + }) + .unwrap(); + } + + #[test] + pub fn eval_state_new_value_attrs_from_vec() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let attrs = { + let a = es.new_value_int(1).unwrap(); + let b = es.new_value_int(2).unwrap(); + es.new_value_attrs(vec![("a".to_string(), a), ("b".to_string(), b)]) + .unwrap() + }; + let t = es.value_type(&attrs).unwrap(); + assert!(t == ValueType::AttrSet); + let names = es.require_attrs_names(&attrs).unwrap(); + assert_eq!(names.len(), 2); + assert_eq!(names[0], "a"); + assert_eq!(names[1], "b"); + let a = es.require_attrs_select(&attrs, "a").unwrap(); + let b = es.require_attrs_select(&attrs, "b").unwrap(); + let i = es.require_int(&a).unwrap(); + assert_eq!(i, 1); + let i = es.require_int(&b).unwrap(); + assert_eq!(i, 2); + }) + .unwrap(); + } + + #[test] + pub fn eval_state_new_value_attrs_from_hashmap() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let attrs = { + let a = es.new_value_int(1).unwrap(); + let b = es.new_value_int(2).unwrap(); + es.new_value_attrs(HashMap::from([("a".to_string(), a), ("b".to_string(), b)])) + .unwrap() + }; + let t = es.value_type(&attrs).unwrap(); + assert!(t == ValueType::AttrSet); + let names = es.require_attrs_names(&attrs).unwrap(); + assert_eq!(names.len(), 2); + assert_eq!(names[0], "a"); + assert_eq!(names[1], "b"); + let a = es.require_attrs_select(&attrs, "a").unwrap(); + let b = es.require_attrs_select(&attrs, "b").unwrap(); + let i = es.require_int(&a).unwrap(); + assert_eq!(i, 1); + let i = es.require_int(&b).unwrap(); + assert_eq!(i, 2); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_basic() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[ 10 20 30 ]", "").unwrap(); + + let elem0 = es.require_list_select_idx_strict(&v, 0).unwrap().unwrap(); + let elem1 = es.require_list_select_idx_strict(&v, 1).unwrap().unwrap(); + let elem2 = es.require_list_select_idx_strict(&v, 2).unwrap().unwrap(); + + assert_eq!(es.require_int(&elem0).unwrap(), 10); + assert_eq!(es.require_int(&elem1).unwrap(), 20); + assert_eq!(es.require_int(&elem2).unwrap(), 30); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_out_of_bounds() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[ 1 2 3 ]", "").unwrap(); + + let out_of_bounds = es.require_list_select_idx_strict(&v, 3).unwrap(); + assert!(out_of_bounds.is_none()); + + // Test boundary case - the last valid index + let last_elem = es.require_list_select_idx_strict(&v, 2).unwrap().unwrap(); + assert_eq!(es.require_int(&last_elem).unwrap(), 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_empty_list() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("[ ]", "").unwrap(); + + // Test that the safe version properly handles empty list access + let elem = es.require_list_select_idx_strict(&v, 0).unwrap(); + assert!(elem.is_none()); + + // Verify we can get the size of an empty list + let size = es.require_list_size(&v).unwrap(); + assert_eq!(size, 0); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "[ 42 ]"); + assert!(es.value_type_unforced(&v).is_none()); + + let elem = es.require_list_select_idx_strict(&v, 0).unwrap().unwrap(); + assert_eq!(es.require_int(&elem).unwrap(), 42); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_error_element() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let v = es + .eval_from_string("[ (1 + 1) (throw \"error\") (3 + 3) ]", "") + .unwrap(); + + let elem0 = es.require_list_select_idx_strict(&v, 0).unwrap().unwrap(); + assert_eq!(es.require_int(&elem0).unwrap(), 2); + + let elem2 = es.require_list_select_idx_strict(&v, 2).unwrap().unwrap(); + assert_eq!(es.require_int(&elem2).unwrap(), 6); + + let elem1_result = es.require_list_select_idx_strict(&v, 1); + match elem1_result { + Ok(_) => panic!("expected an error from throw during selection"), + Err(e) => { + assert!(e.to_string().contains("error")); + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_select_idx_strict_wrong_type() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("42", "").unwrap(); + + let r = es.require_list_select_idx_strict(&v, 0); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + let err_msg = e.to_string(); + assert!(err_msg.contains("expected a list, but got a")); + } + } + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_size_basic() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let empty = es.eval_from_string("[ ]", "").unwrap(); + assert_eq!(es.require_list_size(&empty).unwrap(), 0); + + let three_elem = es.eval_from_string("[ 1 2 3 ]", "").unwrap(); + assert_eq!(es.require_list_size(&three_elem).unwrap(), 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_size_forces_thunk() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = make_thunk(&mut es, "[ 1 2 3 4 5 ]"); + assert!(es.value_type_unforced(&v).is_none()); + + let size = es.require_list_size(&v).unwrap(); + assert_eq!(size, 5); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_size_lazy_elements() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let v = es + .eval_from_string( + "[ (throw \"error1\") (throw \"error2\") (throw \"error3\") ]", + "", + ) + .unwrap(); + + let size = es.require_list_size(&v).unwrap(); + assert_eq!(size, 3); + }) + .unwrap(); + } + + #[test] + fn eval_state_require_list_size_wrong_type() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let v = es.eval_from_string("\"not a list\"", "").unwrap(); + + let r = es.require_list_size(&v); + match r { + Ok(_) => panic!("expected an error"), + Err(e) => { + let err_msg = e.to_string(); + assert!(err_msg.contains("expected a list, but got a")); + } + } + }) + .unwrap(); + } + + /// Test for path coercion fix (commit 8f6ec2e, ). + /// + /// This test verifies that path coercion works correctly with EvalStateBuilder. + /// Path coercion requires readOnlyMode = false, which is loaded from global + /// settings by calling eval_state_builder_load(). + /// + /// # Background + /// + /// Without the eval_state_builder_load() call, settings from global Nix + /// configuration are never loaded, leaving readOnlyMode = true (the default). + /// This prevents Nix from adding paths to the store during evaluation, + /// which could cause errors like: "error: path '/some/local/path' does not exist" + /// + /// # Test Coverage + /// + /// This test exercises store file creation: + /// 1. builtins.toFile successfully creates files in the store + /// 2. Files are actually written to /nix/store + /// 3. Content is written correctly + /// + /// Note: This test may not reliably fail without the fix in all environments. + /// Use eval_state_builder_loads_max_call_depth for a deterministic test. + #[test] + #[cfg(nix_at_least = "2.26" /* real_path, eval_state_builder_load */)] + fn eval_state_builder_path_coercion() { + gc_registering_current_thread(|| { + let mut store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalStateBuilder::new(store.clone()) + .unwrap() + .build() + .unwrap(); + + // Use builtins.toFile to create a file in the store. + // This operation requires readOnlyMode = false to succeed. + let expr = r#"builtins.toFile "test-file.txt" "test content""#; + + // Evaluate the expression + let value = es.eval_from_string(expr, "").unwrap(); + + // Realise the string to get the path and associated store paths + let realised = es.realise_string(&value, false).unwrap(); + + // Verify we got exactly one store path + assert_eq!( + realised.paths.len(), + 1, + "Expected 1 store path, got {}", + realised.paths.len() + ); + + // Get the physical filesystem path for the store path + // In a relocated store, this differs from realised.s + let physical_path = store.real_path(&realised.paths[0]).unwrap(); + + // Verify the store path actually exists on disk + assert!( + std::path::Path::new(&physical_path).exists(), + "Store path should exist: {}", + physical_path + ); + + // Verify the content was written correctly + let store_content = std::fs::read_to_string(&physical_path).unwrap(); + assert_eq!(store_content, "test content"); + }) + .unwrap(); + } + + /// Test that eval_state_builder_load() loads settings. + /// + /// Uses max-call-depth as the test setting. The test suite sets + /// max-call-depth = 1000 via NIX_CONFIG in setup() for the purpose of this test case. + /// This test creates a recursive function that calls itself 1100 times. + /// + /// - WITH the fix: Settings are loaded, max-call-depth=1000 is enforced, + /// recursion fails at depth 1000 + /// - WITHOUT the fix: Settings aren't loaded, default max-call-depth=10000 + /// is used, recursion of 1100 succeeds when it should fail + /// + /// Complementary to eval_state_builder_ignores_ambient_when_disabled which verifies + /// that ambient settings are NOT loaded when disabled. + #[test] + #[cfg(nix_at_least = "2.26")] + fn eval_state_builder_loads_max_call_depth() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalStateBuilder::new(store).unwrap().build().unwrap(); + + // Create a recursive function that calls itself 1100 times + // This should fail because max-call-depth is 1000 (set in setup()) + let expr = r#" + let + recurse = n: if n == 0 then "done" else recurse (n - 1); + in + recurse 1100 + "#; + + let result = es.eval_from_string(expr, ""); + + match result { + Err(e) => { + let err_str = e.to_string(); + assert!( + err_str.contains("max-call-depth"), + "Expected max-call-depth error, got: {}", + err_str + ); + } + Ok(_) => { + panic!( + "Expected recursion to fail with max-call-depth=1000, but it succeeded. \ + This indicates eval_state_builder_load() was not called." + ); + } + } + }) + .unwrap(); + } + + /// Test that load_ambient_settings(false) ignores the ambient environment. + /// + /// The test suite sets max-call-depth = 1000 via NIX_CONFIG in setup(). + /// When we disable loading ambient settings, this should be ignored and + /// the default max-call-depth = 10000 should be used instead. + /// + /// Complementary to eval_state_builder_loads_max_call_depth which verifies + /// that ambient settings ARE loaded when enabled. + #[test] + #[cfg(nix_at_least = "2.26")] + fn eval_state_builder_ignores_ambient_when_disabled() { + gc_registering_current_thread(|| { + let store = Store::open(None, HashMap::new()).unwrap(); + let mut es = EvalStateBuilder::new(store) + .unwrap() + .load_ambient_settings(false) + .build() + .unwrap(); + + // Create a recursive function that calls itself 1100 times + // With ambient settings disabled, default max-call-depth=10000 is used, + // so this should succeed (unlike eval_state_builder_loads_max_call_depth) + let expr = r#" + let + recurse = n: if n == 0 then "done" else recurse (n - 1); + in + recurse 1100 + "#; + + let result = es.eval_from_string(expr, ""); + + match result { + Ok(value) => { + // Success expected - ambient NIX_CONFIG was ignored + let result_str = es.require_string(&value).unwrap(); + assert_eq!(result_str, "done"); + } + Err(e) => { + panic!( + "Expected recursion to succeed with default max-call-depth=10000, \ + but it failed: {}. This indicates ambient settings were not ignored.", + e + ); + } + } + }) + .unwrap(); + } + + #[test] + #[cfg(nix_at_least = "2.34.0pre")] + fn eval_state_primop_recoverable_error() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let call_count = std::cell::Cell::new(0u32); + let v = es + .new_value_thunk( + "recoverable_test", + Box::new(move |es: &mut EvalState| { + let count = call_count.get(); + call_count.set(count + 1); + if count == 0 { + Err(primop::RecoverableError::new("transient failure").into()) + } else { + es.new_value_int(42) + } + }), + ) + .unwrap(); + + // First force should fail with the recoverable error + let r = es.force(&v); + assert!(r.is_err()); + assert!( + r.unwrap_err().to_string().contains("transient failure"), + "Error message should contain 'transient failure'" + ); + + // Second force should succeed because the error was recoverable + es.force(&v).unwrap(); + let i = es.require_int(&v).unwrap(); + assert_eq!(i, 42); + }) + .unwrap(); + } + + #[test] + #[cfg(nix_at_least = "2.34.0pre")] + fn eval_state_primop_recoverable_error_in_chain() { + gc_registering_current_thread(|| { + let store = Store::open(None, []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + + let call_count = std::cell::Cell::new(0u32); + let v = es + .new_value_thunk( + "recoverable_chain_test", + Box::new(move |es: &mut EvalState| { + let count = call_count.get(); + call_count.set(count + 1); + if count == 0 { + // Wrap RecoverableError in .context(), pushing it down the chain + use anyhow::Context; + Err(primop::RecoverableError::new("transient failure")) + .context("wrapper context")? + } else { + es.new_value_int(42) + } + }), + ) + .unwrap(); + + // First force should fail + let r = es.force(&v); + assert!(r.is_err()); + + // Second force should succeed if RecoverableError is found in the chain + es.force(&v).unwrap(); + let i = es.require_int(&v).unwrap(); + assert_eq!(i, 42); + }) + .unwrap(); + } +} diff --git a/nix-bindings-expr/src/lib.rs b/nix-bindings-expr/src/lib.rs new file mode 100644 index 0000000..41c4b39 --- /dev/null +++ b/nix-bindings-expr/src/lib.rs @@ -0,0 +1,3 @@ +pub mod eval_state; +pub mod primop; +pub mod value; diff --git a/nix-bindings-expr/src/primop.rs b/nix-bindings-expr/src/primop.rs new file mode 100644 index 0000000..1504405 --- /dev/null +++ b/nix-bindings-expr/src/primop.rs @@ -0,0 +1,163 @@ +use crate::eval_state::{EvalState, EvalStateWeak}; +use crate::value::Value; +use anyhow::Result; +use nix_bindings_expr_sys as raw; +use nix_bindings_util::check_call; +use nix_bindings_util_sys as raw_util; +use std::ffi::{c_int, c_void, CStr, CString}; +use std::mem::ManuallyDrop; +use std::ptr::{null, null_mut}; + +/// A primop error that is not memoized in the thunk that triggered it, +/// allowing the thunk to be forced again. +/// +/// Since [Nix 2.34](https://nix.dev/manual/nix/2.34/release-notes/rl-2.34.html#c-api-changes), +/// primop errors are memoized by default: once a thunk fails, forcing it +/// again returns the same error. Use `RecoverableError` for errors that +/// are transient, so the caller can retry. +/// +/// On Nix < 2.34, all errors are already recoverable, so this type has +/// no additional effect. +/// +/// Available since nix-bindings-expr 0.2.1. +#[derive(Debug)] +pub struct RecoverableError(String); + +impl RecoverableError { + pub fn new(msg: impl Into) -> Self { + RecoverableError(msg.into()) + } +} + +impl std::fmt::Display for RecoverableError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for RecoverableError {} + +/// Metadata for a primop, used with `PrimOp::new`. +pub struct PrimOpMeta<'a, const N: usize> { + /// Name of the primop. Note that primops do not have to be registered as + /// builtins. Nonetheless, a name is required for documentation purposes, e.g. + /// :doc in the repl. + pub name: &'a CStr, + + /// Documentation for the primop. This is displayed in the repl when using + /// :doc. The format is markdown. + pub doc: &'a CStr, + + /// The number of arguments the function takes, as well as names for the + /// arguments, to be presented in the documentation (if applicable, e.g. + /// :doc in the repl). + pub args: [&'a CStr; N], +} + +pub struct PrimOp { + pub(crate) ptr: *mut raw::PrimOp, +} +impl Drop for PrimOp { + fn drop(&mut self) { + unsafe { + raw::gc_decref(null_mut(), self.ptr as *mut c_void); + } + } +} +impl PrimOp { + /// Create a new primop with the given metadata and implementation. + /// + /// When `f` returns an `Err`, the error is propagated to the Nix evaluator. + /// To return a [recoverable error](RecoverableError), include it in the + /// error chain (e.g. `Err(RecoverableError::new("...").into())`). + pub fn new( + eval_state: &mut EvalState, + meta: PrimOpMeta, + f: Box Result>, + ) -> Result { + assert!(N != 0); + + let mut args = Vec::new(); + for arg in meta.args { + args.push(arg.as_ptr()); + } + args.push(null()); + + // Primops weren't meant to be dynamically created, as of writing. + // This leaks, and so do the primop fields in Nix internally. + let user_data = { + // We'll be leaking this Box. + // TODO: Use the GC with finalizer, if possible. + let user_data = ManuallyDrop::new(Box::new(PrimOpContext { + arity: N, + function: Box::new(move |eval_state, args| f(eval_state, args.try_into().unwrap())), + eval_state: eval_state.weak_ref(), + })); + user_data.as_ref() as *const PrimOpContext as *mut c_void + }; + let op = unsafe { + check_call!(raw::alloc_primop( + &mut eval_state.context, + FUNCTION_ADAPTER, + N as c_int, + meta.name.as_ptr(), + args.as_mut_ptr(), /* TODO add an extra const to bindings to avoid mut here. */ + meta.doc.as_ptr(), + user_data + ))? + }; + Ok(PrimOp { ptr: op }) + } +} + +/// The user_data for our Nix primops +struct PrimOpContext { + arity: usize, + function: Box Result>, + eval_state: EvalStateWeak, +} + +unsafe extern "C" fn function_adapter( + user_data: *mut ::std::os::raw::c_void, + context_out: *mut raw_util::c_context, + _state: *mut raw::EvalState, + args: *mut *mut raw::Value, + ret: *mut raw::Value, +) { + let primop_info = (user_data as *const PrimOpContext).as_ref().unwrap(); + let mut eval_state = primop_info.eval_state.upgrade().unwrap_or_else(|| { + panic!("Nix primop called after EvalState was dropped"); + }); + let args_raw_slice = unsafe { std::slice::from_raw_parts(args, primop_info.arity) }; + let args_vec: Vec = args_raw_slice + .iter() + .map(|v| Value::new_borrowed(*v)) + .collect(); + let args_slice = args_vec.as_slice(); + + let r = primop_info.function.as_ref()(&mut eval_state, args_slice); + + match r { + Ok(v) => unsafe { + raw::copy_value(context_out, ret, v.raw_ptr()); + }, + Err(e) => unsafe { + let err_code = error_code(&e); + let cstr = CString::new(e.to_string()).unwrap_or_else(|_e| { + CString::new("") + .unwrap() + }); + raw_util::set_err_msg(context_out, err_code, cstr.as_ptr()); + }, + } +} + +fn error_code(e: &anyhow::Error) -> raw_util::err { + #[cfg(nix_at_least = "2.34.0pre")] + if e.downcast_ref::().is_some() { + return raw_util::err_NIX_ERR_RECOVERABLE; + } + raw_util::err_NIX_ERR_UNKNOWN +} + +static FUNCTION_ADAPTER: raw::PrimOpFun = Some(function_adapter); diff --git a/nix-bindings-expr/src/value.rs b/nix-bindings-expr/src/value.rs new file mode 100644 index 0000000..68bb444 --- /dev/null +++ b/nix-bindings-expr/src/value.rs @@ -0,0 +1,141 @@ +pub mod __private; + +use nix_bindings_expr_sys as raw; +use nix_bindings_util::{check_call, context::Context}; +use std::ptr::{null_mut, NonNull}; + +// TODO: test: cloning a thunk does not duplicate the evaluation. + +pub type Int = i64; + +/// The type discriminator of a [`Value`] that has successfully evaluated to at least [weak head normal form](https://nix.dev/manual/nix/latest/language/evaluation.html?highlight=WHNF#values). +/// +/// Typically acquired with [`EvalState::value_type`][`crate::eval_state::EvalState::value_type`] +#[derive(Eq, PartialEq, Debug)] +pub enum ValueType { + /// A Nix [attribute set](https://nix.dev/manual/nix/stable/language/types.html#type-attrs) + AttrSet, + /// A Nix [boolean](https://nix.dev/manual/nix/stable/language/types.html#type-bool) + Bool, + /// A Nix external value (mostly-opaque value for plugins, linked applications) + External, + /// A Nix [float](https://nix.dev/manual/nix/stable/language/types.html#type-float) + Float, + /// A Nix [function](https://nix.dev/manual/nix/stable/language/types.html#type-function) + Function, + /// A Nix [integer](https://nix.dev/manual/nix/stable/language/types.html#type-int) + Int, + /// A Nix [list](https://nix.dev/manual/nix/stable/language/types.html#type-list) + List, + /// A Nix [`null`](https://nix.dev/manual/nix/stable/language/types.html#type-null) + Null, + /// A Nix [path value](https://nix.dev/manual/nix/stable/language/types.html#type-path) + Path, + /// A Nix [string](https://nix.dev/manual/nix/stable/language/types.html#type-string) + String, + /// An unknown value, presumably from a new, partially unsupported version of Nix + Unknown, +} + +impl ValueType { + /// Convert a raw value type to a [`ValueType`]. + /// + /// Return [`None`] if the Value is still a thunk (i.e. not yet evaluated). + /// + /// Return `Some(`[`ValueType::Unknown`]`)` if the value type is not recognized. + pub(crate) fn from_raw(raw: raw::ValueType) -> Option { + match raw { + raw::ValueType_NIX_TYPE_ATTRS => Some(ValueType::AttrSet), + raw::ValueType_NIX_TYPE_BOOL => Some(ValueType::Bool), + raw::ValueType_NIX_TYPE_EXTERNAL => Some(ValueType::External), + raw::ValueType_NIX_TYPE_FLOAT => Some(ValueType::Float), + raw::ValueType_NIX_TYPE_FUNCTION => Some(ValueType::Function), + raw::ValueType_NIX_TYPE_INT => Some(ValueType::Int), + raw::ValueType_NIX_TYPE_LIST => Some(ValueType::List), + raw::ValueType_NIX_TYPE_NULL => Some(ValueType::Null), + raw::ValueType_NIX_TYPE_PATH => Some(ValueType::Path), + raw::ValueType_NIX_TYPE_STRING => Some(ValueType::String), + + raw::ValueType_NIX_TYPE_THUNK => None, + + // This would happen if a new type of value is added in Nix. + _ => Some(ValueType::Unknown), + } + } +} + +/// A pointer to a [value](https://nix.dev/manual/nix/latest/language/types.html) or [thunk](https://nix.dev/manual/nix/2.31/language/evaluation.html?highlight=thunk#laziness), to be used with [`EvalState`][`crate::eval_state::EvalState`] methods. +/// +/// # Shared Evaluation State +/// +/// Multiple `Value` instances can reference the same underlying Nix value. +/// This occurs when a `Value` is [cloned](Clone), or when multiple Nix +/// expressions reference the same binding. +/// +/// When any reference to a thunk is evaluated—whether through +/// [`force`](crate::eval_state::EvalState::force), other `EvalState` methods, +/// or indirectly as a consequence of evaluating something else—all references +/// observe the evaluated result. This means +/// [`value_type_unforced`](crate::eval_state::EvalState::value_type_unforced) +/// can return `None` (thunk) initially but a specific type later, even without +/// directly operating on that `Value`. The state will not regress back to a +/// less determined state. +pub struct Value { + inner: NonNull, +} +impl Value { + /// Take ownership of a new [`Value`]. + /// + /// This does not call [`nix_bindings_util_sys::gc_incref`], but does call [`nix_bindings_util_sys::gc_decref`] when [dropped][`Drop`]. + /// + /// # Safety + /// + /// The caller must ensure that the provided `inner` has a positive reference count, and that `inner` is not used after the returned `Value` is dropped. + pub(crate) unsafe fn new(inner: *mut raw::Value) -> Self { + Value { + inner: NonNull::new(inner).unwrap(), + } + } + + /// Borrow a reference to a [`Value`]. + /// + /// This calls [`nix_bindings_util_sys::value_incref`], and the returned Value will call [`nix_bindings_util_sys::value_decref`] when dropped. + /// + /// # Safety + /// + /// The caller must ensure that the provided `inner` has a positive reference count. + pub(crate) unsafe fn new_borrowed(inner: *mut raw::Value) -> Self { + let v = Value::new(inner); + unsafe { raw::value_incref(null_mut(), inner) }; + v + } + + /// # Safety + /// + /// The caller must ensure that the returned pointer is not used after the `Value` is dropped. + pub(crate) unsafe fn raw_ptr(&self) -> *mut raw::Value { + self.inner.as_ptr() + } +} +impl Drop for Value { + fn drop(&mut self) { + unsafe { + // ignoring error because the only failure mode is leaking memory + raw::value_decref(null_mut(), self.inner.as_ptr()); + } + } +} +impl Clone for Value { + fn clone(&self) -> Self { + // TODO: Is it worth allocating a new Context here? Ideally cloning is cheap. + // this is very unlikely to error, and it is not recoverable + // Maybe try without, and try again with context to report details? + unsafe { + check_call!(raw::value_incref(&mut Context::new(), self.inner.as_ptr())).unwrap(); + } + // can't return an error here, but we don't want to ignore the error either as it means we could use-after-free + Value { inner: self.inner } + } +} + +// Tested in eval_state.rs diff --git a/nix-bindings-expr/src/value/__private.rs b/nix-bindings-expr/src/value/__private.rs new file mode 100644 index 0000000..770a217 --- /dev/null +++ b/nix-bindings-expr/src/value/__private.rs @@ -0,0 +1,26 @@ +//! Functions that are relevant for other bindings modules, but normally not end users. +use super::Value; +use nix_bindings_expr_sys as raw; + +/// Take ownership of a new [`Value`]. +/// +/// This does not call `nix_gc_incref`, but does call `nix_gc_decref` when dropped. +/// +/// # Safety +/// +/// The caller must ensure that the provided `ptr` has a positive reference count, +/// and that `ptr` is not used after the returned `Value` is dropped. +pub unsafe fn raw_value_new(ptr: *mut raw::Value) -> Value { + Value::new(ptr) +} + +/// Borrow a reference to a [`Value`]. +/// +/// This calls `value_incref`, and the returned Value will call `value_decref` when dropped. +/// +/// # Safety +/// +/// The caller must ensure that the provided `ptr` has a positive reference count. +pub unsafe fn raw_value_new_borrowed(ptr: *mut raw::Value) -> Value { + Value::new_borrowed(ptr) +} diff --git a/nix-bindings-fetchers-sys/Cargo.toml b/nix-bindings-fetchers-sys/Cargo.toml new file mode 100644 index 0000000..2dfaa3b --- /dev/null +++ b/nix-bindings-fetchers-sys/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nix-bindings-fetchers-sys" +version = "0.2.1" +edition = "2021" +build = "build.rs" +license = "LGPL-2.1" +description = "Low-level FFI bindings to the nix-fetchers library" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_fetchers_sys/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" } + +[build-dependencies] +bindgen = "0.69" +pkg-config = "0.3" diff --git a/nix-bindings-fetchers-sys/README.md b/nix-bindings-fetchers-sys/README.md new file mode 100644 index 0000000..8230b81 --- /dev/null +++ b/nix-bindings-fetchers-sys/README.md @@ -0,0 +1,11 @@ +# nix-bindings-fetchers-sys + +This crate contains generated bindings for the Nix C API (`nix-fetchers-c`). +**You should not have to use this crate directly,** and so you should probably not add it to your dependencies. +Instead, use the `nix-bindings-fetchers` crate, which _should_ be sufficient. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_fetchers_sys/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-fetchers-sys/build.rs b/nix-bindings-fetchers-sys/build.rs new file mode 100644 index 0000000..34f6640 --- /dev/null +++ b/nix-bindings-fetchers-sys/build.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +#[derive(Debug)] +struct StripNixPrefix; + +impl bindgen::callbacks::ParseCallbacks for StripNixPrefix { + fn item_name(&self, name: &str) -> Option { + name.strip_prefix("nix_").map(String::from) + } +} + +fn main() { + println!("cargo:rerun-if-changed=include/nix-c-fetchers.h"); + println!("cargo:rustc-link-lib=nixfetchersc"); + + let mut args = Vec::new(); + for path in pkg_config::probe_library("nix-fetchers-c") + .unwrap() + .include_paths + .iter() + { + args.push(format!("-I{}", path.to_str().unwrap())); + } + + let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + + let bindings = bindgen::Builder::default() + .header("include/nix-c-fetchers.h") + .clang_args(args) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .parse_callbacks(Box::new(StripNixPrefix)) + // Blocklist symbols from nix-bindings-util-sys + .blocklist_file(".*nix_api_util\\.h") + .generate() + .expect("Unable to generate bindings"); + + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/nix-bindings-fetchers-sys/include/nix-c-fetchers.h b/nix-bindings-fetchers-sys/include/nix-c-fetchers.h new file mode 100644 index 0000000..2f4c542 --- /dev/null +++ b/nix-bindings-fetchers-sys/include/nix-c-fetchers.h @@ -0,0 +1 @@ +#include diff --git a/nix-bindings-fetchers-sys/src/lib.rs b/nix-bindings-fetchers-sys/src/lib.rs new file mode 100644 index 0000000..97a30ed --- /dev/null +++ b/nix-bindings-fetchers-sys/src/lib.rs @@ -0,0 +1,33 @@ +//! Raw bindings to Nix C API +//! +//! This crate contains automatically generated bindings from the Nix C headers. +//! The bindings are generated by bindgen and include C-style naming conventions +//! and documentation comments that don't always conform to Rust standards. +//! +//! Normally you don't have to use this crate directly. +//! Instead use `nix-fetchers`. + +// This file must only contain generated code, so that the module-level +// #![allow(...)] attributes don't suppress warnings in hand-written code. +// If you need to add hand-written code, use a submodule to isolate the +// generated code. See: +// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9 + +// Standard bindgen suppressions for C naming conventions +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// Clippy suppressions for generated C bindings +// bindgen doesn't generate safety docs +#![allow(clippy::missing_safety_doc)] +// Rustdoc suppressions for generated C documentation +// The C headers contain Doxygen-style documentation that doesn't translate +// well to Rust's rustdoc format, causing various warnings: +#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve +#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs +#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like +#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes + +use nix_bindings_util_sys::*; + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nix-bindings-fetchers/Cargo.toml b/nix-bindings-fetchers/Cargo.toml new file mode 100644 index 0000000..4ae993d --- /dev/null +++ b/nix-bindings-fetchers/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "nix-bindings-fetchers" +version = "0.2.1" +edition = "2021" +license = "LGPL-2.1" +description = "Rust bindings to Nix fetchers" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_fetchers/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0" +nix-bindings-store = { path = "../nix-bindings-store", version = "0.2.1" } +nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" } +nix-bindings-fetchers-sys = { path = "../nix-bindings-fetchers-sys", version = "0.2.1" } +ctor = "0.2" +tempfile = "3.10" +cstr = "0.2" + +[lints.rust] +warnings = "deny" +dead-code = "allow" + +[lints.clippy] +type-complexity = "allow" +# We're still trying to make Nix more thread-safe, want forward-compat +arc-with-non-send-sync = "allow" diff --git a/nix-bindings-fetchers/README.md b/nix-bindings-fetchers/README.md new file mode 100644 index 0000000..b8f3941 --- /dev/null +++ b/nix-bindings-fetchers/README.md @@ -0,0 +1,9 @@ +# nix-bindings-fetchers + +Rust bindings to the nix-fetchers library. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_fetchers/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-fetchers/src/lib.rs b/nix-bindings-fetchers/src/lib.rs new file mode 100644 index 0000000..0b7b6cc --- /dev/null +++ b/nix-bindings-fetchers/src/lib.rs @@ -0,0 +1,38 @@ +use anyhow::{Context as _, Result}; +use nix_bindings_fetchers_sys as raw; +use nix_bindings_util::context::{self, Context}; +use std::ptr::NonNull; + +pub struct FetchersSettings { + pub(crate) ptr: NonNull, +} +impl Drop for FetchersSettings { + fn drop(&mut self) { + unsafe { + raw::fetchers_settings_free(self.ptr.as_ptr()); + } + } +} +impl FetchersSettings { + pub fn new() -> Result { + let mut ctx = Context::new(); + let ptr = unsafe { context::check_call!(raw::fetchers_settings_new(&mut ctx))? }; + Ok(FetchersSettings { + ptr: NonNull::new(ptr).context("fetchers_settings_new unexpectedly returned null")?, + }) + } + + pub fn raw_ptr(&self) -> *mut raw::fetchers_settings { + self.ptr.as_ptr() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fetchers_settings_new() { + let _ = FetchersSettings::new().unwrap(); + } +} diff --git a/nix-bindings-flake-sys/Cargo.toml b/nix-bindings-flake-sys/Cargo.toml new file mode 100644 index 0000000..987cd6d --- /dev/null +++ b/nix-bindings-flake-sys/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "nix-bindings-flake-sys" +version = "0.2.1" +edition = "2021" +build = "build.rs" +license = "LGPL-2.1" +description = "Low-level FFI bindings to Nix flakes" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_flake_sys/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" } +nix-bindings-store-sys = { path = "../nix-bindings-store-sys", version = "0.2.1" } +nix-bindings-expr-sys = { path = "../nix-bindings-expr-sys", version = "0.2.1" } +nix-bindings-fetchers-sys = { path = "../nix-bindings-fetchers-sys", version = "0.2.1" } +nix-bindings-bdwgc-sys = { path = "../nix-bindings-bdwgc-sys", version = "0.2.1" } + +[build-dependencies] +bindgen = "0.69" +pkg-config = "0.3" diff --git a/nix-bindings-flake-sys/README.md b/nix-bindings-flake-sys/README.md new file mode 100644 index 0000000..1d26c8d --- /dev/null +++ b/nix-bindings-flake-sys/README.md @@ -0,0 +1,11 @@ +# nix-bindings-flake-sys + +This crate contains generated bindings for the Nix C API (`nix-flake-c`). +**You should not have to use this crate directly,** and so you should probably not add it to your dependencies. +Instead, use the `nix-bindings-flake` crate, which _should_ be sufficient. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_flake_sys/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-flake-sys/build.rs b/nix-bindings-flake-sys/build.rs new file mode 100644 index 0000000..1fb3233 --- /dev/null +++ b/nix-bindings-flake-sys/build.rs @@ -0,0 +1,56 @@ +use std::path::PathBuf; + +#[derive(Debug)] +struct StripNixPrefix; + +impl bindgen::callbacks::ParseCallbacks for StripNixPrefix { + fn item_name(&self, name: &str) -> Option { + name.strip_prefix("nix_").map(String::from) + } +} + +fn main() { + println!("cargo:rerun-if-changed=include/nix-c-flake.h"); + println!("cargo:rustc-link-lib=nixflakec"); + + let mut args = Vec::new(); + for path in pkg_config::probe_library("nix-flake-c") + .unwrap() + .include_paths + .iter() + { + args.push(format!("-I{}", path.to_str().unwrap())); + } + for path in pkg_config::probe_library("bdw-gc") + .unwrap() + .include_paths + .iter() + { + args.push(format!("-I{}", path.to_str().unwrap())); + } + + let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + + let bindings = bindgen::Builder::default() + .header("include/nix-c-flake.h") + .clang_args(args) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .parse_callbacks(Box::new(StripNixPrefix)) + // Blocklist symbols from nix-bindings-util-sys + .blocklist_file(".*nix_api_util\\.h") + // Blocklist symbols from nix-bindings-store-sys + .blocklist_file(".*nix_api_store\\.h") + // Blocklist symbols from nix-bindings-expr-sys + .blocklist_file(".*nix_api_expr\\.h") + .blocklist_file(".*nix_api_value\\.h") + // Blocklist symbols from nix-bindings-fetchers-sys + .blocklist_file(".*nix_api_fetchers\\.h") + // Blocklist symbols from nix-bindings-bdwgc-sys + .blocklist_file(".*/gc\\.h") + .generate() + .expect("Unable to generate bindings"); + + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/nix-bindings-flake-sys/include/nix-c-flake.h b/nix-bindings-flake-sys/include/nix-c-flake.h new file mode 100644 index 0000000..89a45f1 --- /dev/null +++ b/nix-bindings-flake-sys/include/nix-c-flake.h @@ -0,0 +1,3 @@ +#define GC_THREADS +#include +#include diff --git a/nix-bindings-flake-sys/src/lib.rs b/nix-bindings-flake-sys/src/lib.rs new file mode 100644 index 0000000..2e9209b --- /dev/null +++ b/nix-bindings-flake-sys/src/lib.rs @@ -0,0 +1,35 @@ +//! Raw bindings to Nix C API +//! +//! This crate contains automatically generated bindings from the Nix C headers. +//! The bindings are generated by bindgen and include C-style naming conventions +//! and documentation comments that don't always conform to Rust standards. +//! +//! Normally you don't have to use this crate directly. +//! Instead use `nix-flake`. + +// This file must only contain generated code, so that the module-level +// #![allow(...)] attributes don't suppress warnings in hand-written code. +// If you need to add hand-written code, use a submodule to isolate the +// generated code. See: +// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9 + +// Standard bindgen suppressions for C naming conventions +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// Clippy suppressions for generated C bindings +// bindgen doesn't generate safety docs +#![allow(clippy::missing_safety_doc)] +// Rustdoc suppressions for generated C documentation +// The C headers contain Doxygen-style documentation that doesn't translate +// well to Rust's rustdoc format, causing various warnings: +#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve +#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs +#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like +#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes + +use nix_bindings_expr_sys::*; +use nix_bindings_fetchers_sys::*; +use nix_bindings_util_sys::*; + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nix-bindings-flake/Cargo.toml b/nix-bindings-flake/Cargo.toml new file mode 100644 index 0000000..5fbb42a --- /dev/null +++ b/nix-bindings-flake/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "nix-bindings-flake" +version = "0.2.1" +edition = "2021" +license = "LGPL-2.1" +description = "Rust bindings to Nix flakes" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_flake/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0" +nix-bindings-expr = { path = "../nix-bindings-expr", version = "0.2.1" } +nix-bindings-fetchers = { path = "../nix-bindings-fetchers", version = "0.2.1" } +nix-bindings-store = { path = "../nix-bindings-store", version = "0.2.1" } +nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" } +nix-bindings-flake-sys = { path = "../nix-bindings-flake-sys", version = "0.2.1" } +ctor = "0.2" +tempfile = "3.10" +cstr = "0.2" + +[lints.rust] +warnings = "deny" +dead-code = "allow" + +[lints.clippy] +type-complexity = "allow" +# We're still trying to make Nix more thread-safe, want forward-compat +arc-with-non-send-sync = "allow" diff --git a/nix-bindings-flake/README.md b/nix-bindings-flake/README.md new file mode 100644 index 0000000..43dd11b --- /dev/null +++ b/nix-bindings-flake/README.md @@ -0,0 +1,9 @@ +# nix-bindings-flake + +Rust bindings to Nix flakes. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_flake/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nixide/src/flake/locked_flake.rs b/nix-bindings-flake/src/lib.rs similarity index 55% rename from nixide/src/flake/locked_flake.rs rename to nix-bindings-flake/src/lib.rs index f11732c..beb6b31 100644 --- a/nixide/src/flake/locked_flake.rs +++ b/nix-bindings-flake/src/lib.rs @@ -1,18 +1,217 @@ -use std::ptr::NonNull; +use std::{ffi::CString, os::raw::c_char, ptr::NonNull}; -use super::{FetchersSettings, FlakeLockFlags, FlakeReference, FlakeSettings}; -use crate::errors::{new_nixide_error, ErrorContext}; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::{EvalState, NixideError, Value}; +use anyhow::{Context as _, Result}; +use nix_bindings_expr::eval_state::EvalState; +use nix_bindings_fetchers::FetchersSettings; +use nix_bindings_flake_sys as raw; +use nix_bindings_util::{ + context::{self, Context}, + result_string_init, + string_return::{callback_get_result_string, callback_get_result_string_data}, +}; + +/// Store settings for the flakes feature. +pub struct FlakeSettings { + pub(crate) ptr: *mut raw::flake_settings, +} +impl Drop for FlakeSettings { + fn drop(&mut self) { + unsafe { + raw::flake_settings_free(self.ptr); + } + } +} +impl FlakeSettings { + pub fn new() -> Result { + let mut ctx = Context::new(); + let s = unsafe { context::check_call!(raw::flake_settings_new(&mut ctx)) }?; + Ok(FlakeSettings { ptr: s }) + } + fn add_to_eval_state_builder( + &self, + builder: &mut nix_bindings_expr::eval_state::EvalStateBuilder, + ) -> Result<()> { + let mut ctx = Context::new(); + unsafe { + context::check_call!(raw::flake_settings_add_to_eval_state_builder( + &mut ctx, + self.ptr, + builder.raw_ptr() + )) + }?; + Ok(()) + } +} + +pub trait EvalStateBuilderExt { + /// Configures the eval state to provide flakes features such as `builtins.getFlake`. + fn flakes( + self, + settings: &FlakeSettings, + ) -> Result; +} +impl EvalStateBuilderExt for nix_bindings_expr::eval_state::EvalStateBuilder { + /// Configures the eval state to provide flakes features such as `builtins.getFlake`. + fn flakes( + mut self, + settings: &FlakeSettings, + ) -> Result { + settings.add_to_eval_state_builder(&mut self)?; + Ok(self) + } +} + +/// Parameters for parsing a flake reference. +pub struct FlakeReferenceParseFlags { + pub(crate) ptr: NonNull, +} +impl Drop for FlakeReferenceParseFlags { + fn drop(&mut self) { + unsafe { + raw::flake_reference_parse_flags_free(self.ptr.as_ptr()); + } + } +} +impl FlakeReferenceParseFlags { + pub fn new(settings: &FlakeSettings) -> Result { + let mut ctx = Context::new(); + let ptr = unsafe { + context::check_call!(raw::flake_reference_parse_flags_new(&mut ctx, settings.ptr)) + }?; + let ptr = NonNull::new(ptr) + .context("flake_reference_parse_flags_new unexpectedly returned null")?; + Ok(FlakeReferenceParseFlags { ptr }) + } + /// Sets the [base directory](https://nix.dev/manual/nix/latest/glossary#gloss-base-directory) + /// for resolving local flake references. + pub fn set_base_directory(&mut self, base_directory: &str) -> Result<()> { + let mut ctx = Context::new(); + unsafe { + context::check_call!(raw::flake_reference_parse_flags_set_base_directory( + &mut ctx, + self.ptr.as_ptr(), + base_directory.as_ptr() as *const c_char, + base_directory.len() + )) + }?; + Ok(()) + } +} + +#[derive(Clone)] +pub struct FlakeReference { + pub(crate) ptr: NonNull, +} +impl Drop for FlakeReference { + fn drop(&mut self) { + unsafe { + raw::flake_reference_free(self.ptr.as_ptr()); + } + } +} +impl FlakeReference { + /// Parse a flake reference from a string. + /// The string must be a valid flake reference, such as `github:owner/repo`. + /// It may also be suffixed with a `#` and a fragment, such as `github:owner/repo#something`, + /// in which case, the returned string will contain the fragment. + pub fn parse_with_fragment( + fetch_settings: &FetchersSettings, + flake_settings: &FlakeSettings, + flags: &FlakeReferenceParseFlags, + reference: &str, + ) -> Result<(FlakeReference, String)> { + let mut ctx = Context::new(); + let mut r = result_string_init!(); + let mut ptr: *mut raw::flake_reference = std::ptr::null_mut(); + unsafe { + context::check_call!(raw::flake_reference_and_fragment_from_string( + &mut ctx, + fetch_settings.raw_ptr(), + flake_settings.ptr, + flags.ptr.as_ptr(), + reference.as_ptr() as *const c_char, + reference.len(), + // pointer to ptr + &mut ptr, + Some(callback_get_result_string), + callback_get_result_string_data(&mut r) + )) + }?; + let ptr = NonNull::new(ptr) + .context("flake_reference_and_fragment_from_string unexpectedly returned null")?; + Ok((FlakeReference { ptr }, r?)) + } +} + +/// Parameters that affect the locking of a flake. +pub struct FlakeLockFlags { + pub(crate) ptr: *mut raw::flake_lock_flags, +} +impl Drop for FlakeLockFlags { + fn drop(&mut self) { + unsafe { + raw::flake_lock_flags_free(self.ptr); + } + } +} +impl FlakeLockFlags { + pub fn new(settings: &FlakeSettings) -> Result { + let mut ctx = Context::new(); + let s = unsafe { context::check_call!(raw::flake_lock_flags_new(&mut ctx, settings.ptr)) }?; + Ok(FlakeLockFlags { ptr: s }) + } + /// Configures [LockedFlake::lock] to make incremental changes to the lock file as needed. Changes are written to file. + pub fn set_mode_write_as_needed(&mut self) -> Result<()> { + let mut ctx = Context::new(); + unsafe { + context::check_call!(raw::flake_lock_flags_set_mode_write_as_needed( + &mut ctx, self.ptr + )) + }?; + Ok(()) + } + /// Make [LockedFlake::lock] check if the lock file is up to date. If not, an error is returned. + pub fn set_mode_check(&mut self) -> Result<()> { + let mut ctx = Context::new(); + unsafe { context::check_call!(raw::flake_lock_flags_set_mode_check(&mut ctx, self.ptr)) }?; + Ok(()) + } + /// Like `set_mode_write_as_needed`, but does not write to the lock file. + pub fn set_mode_virtual(&mut self) -> Result<()> { + let mut ctx = Context::new(); + unsafe { + context::check_call!(raw::flake_lock_flags_set_mode_virtual(&mut ctx, self.ptr)) + }?; + Ok(()) + } + /// Adds an input override to the lock file that will be produced. The [LockedFlake::lock] operation will not write to the lock file. + pub fn add_input_override( + &mut self, + override_path: &str, + override_ref: &FlakeReference, + ) -> Result<()> { + let mut ctx = Context::new(); + unsafe { + context::check_call!(raw::flake_lock_flags_add_input_override( + &mut ctx, + self.ptr, + CString::new(override_path) + .context("Failed to create CString for override_path")? + .as_ptr(), + override_ref.ptr.as_ptr() + )) + }?; + Ok(()) + } +} pub struct LockedFlake { - pub(crate) ptr: NonNull, + pub(crate) ptr: NonNull, } impl Drop for LockedFlake { fn drop(&mut self) { unsafe { - sys::nix_locked_flake_free(self.ptr.as_ptr()); + raw::locked_flake_free(self.ptr.as_ptr()); } } } @@ -23,24 +222,20 @@ impl LockedFlake { eval_state: &EvalState, flags: &FlakeLockFlags, flake_ref: &FlakeReference, - ) -> Result { - let ctx = ErrorContext::new(); - - let ptr = NonNull::new(unsafe { - sys::nix_flake_lock( - ctx.as_ptr(), - fetch_settings.as_ptr(), - flake_settings.as_ptr(), - eval_state.as_ptr(), - flags.as_ptr(), - flake_ref.as_ptr(), - ) - }); - - match ptr { - Some(ptr) => Ok(LockedFlake { ptr }), - None => Err(new_nixide_error!(NullPtr)), - } + ) -> Result { + let mut ctx = Context::new(); + let ptr = unsafe { + context::check_call!(raw::flake_lock( + &mut ctx, + fetch_settings.raw_ptr(), + flake_settings.ptr, + eval_state.raw_ptr(), + flags.ptr, + flake_ref.ptr.as_ptr() + )) + }?; + let ptr = NonNull::new(ptr).context("flake_lock unexpectedly returned null")?; + Ok(LockedFlake { ptr }) } /// Returns the outputs of the flake - the result of calling the `outputs` attribute. @@ -48,27 +243,24 @@ impl LockedFlake { &self, flake_settings: &FlakeSettings, eval_state: &mut EvalState, - ) -> Result { - let ctx = ErrorContext::new(); - - let r = unsafe { - sys::nix_locked_flake_get_output_attrs( - ctx.as_ptr(), - flake_settings.as_ptr(), - eval_state.as_ptr(), - self.ptr.as_ptr(), - ) - }; - Ok(nix_bindings_expr::value::__private::raw_value_new(r)) + ) -> Result { + let mut ctx = Context::new(); + unsafe { + let r = context::check_call!(raw::locked_flake_get_output_attrs( + &mut ctx, + flake_settings.ptr, + eval_state.raw_ptr(), + self.ptr.as_ptr() + ))?; + Ok(nix_bindings_expr::value::__private::raw_value_new(r)) + } } } #[cfg(test)] mod tests { - // use nix_bindings_expr::eval_state::{gc_register_my_thread, EvalStateBuilder}; - - use crate::flake::{FlakeLockMode, FlakeReferenceParseFlags}; - use crate::{EvalStateBuilder, Store}; + use nix_bindings_expr::eval_state::{gc_register_my_thread, EvalStateBuilder}; + use nix_bindings_store::store::Store; use super::*; use std::sync::Once; @@ -263,9 +455,7 @@ mod tests { // Step 1: Do not update (check), fails - flake_lock_flags - .set_lock_mode(&FlakeLockMode::Check) - .unwrap(); + flake_lock_flags.set_mode_check().unwrap(); let locked_flake = LockedFlake::lock( &fetchers_settings, @@ -282,9 +472,7 @@ mod tests { }; // Step 2: Update but do not write, succeeds - flake_lock_flags - .set_lock_mode(&FlakeLockMode::Virtual) - .unwrap(); + flake_lock_flags.set_mode_virtual().unwrap(); let locked_flake = LockedFlake::lock( &fetchers_settings, @@ -306,9 +494,7 @@ mod tests { // Step 3: The lock was not written, so Step 1 would fail again - flake_lock_flags - .set_lock_mode(&FlakeLockMode::Check) - .unwrap(); + flake_lock_flags.set_mode_check().unwrap(); let locked_flake = LockedFlake::lock( &fetchers_settings, @@ -328,9 +514,7 @@ mod tests { // Step 4: Update and write, succeeds - flake_lock_flags - .set_lock_mode(&FlakeLockMode::WriteAsNeeded) - .unwrap(); + flake_lock_flags.set_mode_write_as_needed().unwrap(); let locked_flake = LockedFlake::lock( &fetchers_settings, @@ -350,9 +534,7 @@ mod tests { // Step 5: Lock was written, so Step 1 succeeds - flake_lock_flags - .set_lock_mode(&FlakeLockMode::Check) - .unwrap(); + flake_lock_flags.set_mode_check().unwrap(); let locked_flake = LockedFlake::lock( &fetchers_settings, @@ -373,9 +555,7 @@ mod tests { // Step 6: Lock with override, do not write // This shouldn't matter; write_as_needed will be overridden - flake_lock_flags - .set_lock_mode(&FlakeLockMode::WriteAsNeeded) - .unwrap(); + flake_lock_flags.set_mode_write_as_needed().unwrap(); let (flake_ref_c, fragment) = FlakeReference::parse_with_fragment( &fetchers_settings, @@ -386,7 +566,9 @@ mod tests { .unwrap(); assert_eq!(fragment, ""); - flake_lock_flags.override_input("b", &flake_ref_c).unwrap(); + flake_lock_flags + .add_input_override("b", &flake_ref_c) + .unwrap(); let locked_flake = LockedFlake::lock( &fetchers_settings, @@ -409,9 +591,7 @@ mod tests { // Step 7: Override was not written; lock still points to b - flake_lock_flags - .set_lock_mode(&FlakeLockMode::Check) - .unwrap(); + flake_lock_flags.set_mode_check().unwrap(); let locked_flake = LockedFlake::lock( &fetchers_settings, diff --git a/nix-bindings-store-sys/Cargo.toml b/nix-bindings-store-sys/Cargo.toml new file mode 100644 index 0000000..9e8c374 --- /dev/null +++ b/nix-bindings-store-sys/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nix-bindings-store-sys" +version = "0.2.1" +edition = "2021" +build = "build.rs" +license = "LGPL-2.1" +description = "Low-level FFI bindings to the Nix store library" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_store_sys/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" } +zerocopy = { version = "0.8", features = ["derive"] } + +[build-dependencies] +bindgen = "0.69" +pkg-config = "0.3" diff --git a/nix-bindings-store-sys/README.md b/nix-bindings-store-sys/README.md new file mode 100644 index 0000000..936505d --- /dev/null +++ b/nix-bindings-store-sys/README.md @@ -0,0 +1,11 @@ +# nix-bindings-store-sys + +This crate contains generated bindings for the Nix C API (`nix-store-c`). +**You should not have to use this crate directly,** and so you should probably not add it to your dependencies. +Instead, use the `nix-bindings-store` crate, which _should_ be sufficient. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_store_sys/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-store-sys/build.rs b/nix-bindings-store-sys/build.rs new file mode 100644 index 0000000..8702756 --- /dev/null +++ b/nix-bindings-store-sys/build.rs @@ -0,0 +1,57 @@ +use std::path::PathBuf; + +#[derive(Debug)] +struct StripNixPrefix; + +impl bindgen::callbacks::ParseCallbacks for StripNixPrefix { + fn item_name(&self, name: &str) -> Option { + name.strip_prefix("nix_").map(String::from) + } +} + +#[derive(Debug)] +struct AddZerocopyDerives {} +impl bindgen::callbacks::ParseCallbacks for AddZerocopyDerives { + fn add_derives(&self, info: &bindgen::callbacks::DeriveInfo<'_>) -> Vec { + if info.name == "store_path_hash_part" { + vec![ + "zerocopy::FromBytes".to_string(), + "zerocopy::IntoBytes".to_string(), + "zerocopy::Immutable".to_string(), + ] + } else { + vec![] + } + } +} + +fn main() { + println!("cargo:rerun-if-changed=include/nix-c-store.h"); + println!("cargo:rustc-link-lib=nixstorec"); + + let mut args = Vec::new(); + for path in pkg_config::probe_library("nix-store-c") + .unwrap() + .include_paths + .iter() + { + args.push(format!("-I{}", path.to_str().unwrap())); + } + + let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + + let bindings = bindgen::Builder::default() + .header("include/nix-c-store.h") + .clang_args(args) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .parse_callbacks(Box::new(StripNixPrefix)) + .parse_callbacks(Box::new(AddZerocopyDerives {})) + // Blocklist symbols from nix-bindings-util-sys + .blocklist_file(".*nix_api_util\\.h") + .generate() + .expect("Unable to generate bindings"); + + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/nix-bindings-store-sys/include/nix-c-store.h b/nix-bindings-store-sys/include/nix-c-store.h new file mode 100644 index 0000000..2c04d1d --- /dev/null +++ b/nix-bindings-store-sys/include/nix-c-store.h @@ -0,0 +1 @@ +#include diff --git a/nix-bindings-store-sys/src/lib.rs b/nix-bindings-store-sys/src/lib.rs new file mode 100644 index 0000000..eb41931 --- /dev/null +++ b/nix-bindings-store-sys/src/lib.rs @@ -0,0 +1,33 @@ +//! Raw bindings to Nix C API +//! +//! This crate contains automatically generated bindings from the Nix C headers. +//! The bindings are generated by bindgen and include C-style naming conventions +//! and documentation comments that don't always conform to Rust standards. +//! +//! Normally you don't have to use this crate directly. +//! Instead use `nix-store`. + +// This file must only contain generated code, so that the module-level +// #![allow(...)] attributes don't suppress warnings in hand-written code. +// If you need to add hand-written code, use a submodule to isolate the +// generated code. See: +// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9 + +// Standard bindgen suppressions for C naming conventions +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// Clippy suppressions for generated C bindings +// bindgen doesn't generate safety docs +#![allow(clippy::missing_safety_doc)] +// Rustdoc suppressions for generated C documentation +// The C headers contain Doxygen-style documentation that doesn't translate +// well to Rust's rustdoc format, causing various warnings: +#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve +#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs +#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like +#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes + +use nix_bindings_util_sys::*; + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nix-bindings-store/Cargo.toml b/nix-bindings-store/Cargo.toml new file mode 100644 index 0000000..24336db --- /dev/null +++ b/nix-bindings-store/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "nix-bindings-store" +version = "0.2.1" +edition = "2021" +build = "build.rs" +license = "LGPL-2.1" +description = "Rust bindings to Nix store library" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_store/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0" +nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" } +nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" } +nix-bindings-store-sys = { path = "../nix-bindings-store-sys", version = "0.2.1" } +zerocopy = "0.8" +harmonia-store-core = { version = "0.0.0-alpha.0", optional = true } +serde_json = { version = "1.0", optional = true } + +[dev-dependencies] +ctor = "0.2" +hex-literal = "0.4" +tempfile = "3.10" +serde_json = "1.0" + +[build-dependencies] +pkg-config = "0.3" +# Needed for version parsing in build.rs +nix-bindings-util = { path = "../nix-bindings-util", version = "0.2.1" } + +[features] +harmonia = [ "dep:harmonia-store-core", "dep:serde_json" ] + +[lints.rust] +warnings = "deny" +dead-code = "allow" + +[lints.clippy] +type-complexity = "allow" +# We're still trying to make Nix more thread-safe, want forward-compat +arc-with-non-send-sync = "allow" diff --git a/nix-bindings-store/README.md b/nix-bindings-store/README.md new file mode 100644 index 0000000..8002364 --- /dev/null +++ b/nix-bindings-store/README.md @@ -0,0 +1,9 @@ +# nix-bindings-store + +Rust bindings to the Nix store library. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_store/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-store/build.rs b/nix-bindings-store/build.rs new file mode 100644 index 0000000..85a20d6 --- /dev/null +++ b/nix-bindings-store/build.rs @@ -0,0 +1,6 @@ +use nix_bindings_util::nix_version::emit_version_cfg; + +fn main() { + let nix_version = pkg_config::probe_library("nix-store-c").unwrap().version; + emit_version_cfg(&nix_version, &["2.26", "2.33.0pre", "2.33"]); +} diff --git a/nix-bindings-store/src/derivation/harmonia.rs b/nix-bindings-store/src/derivation/harmonia.rs new file mode 100644 index 0000000..44b34a2 --- /dev/null +++ b/nix-bindings-store/src/derivation/harmonia.rs @@ -0,0 +1,100 @@ +use anyhow::Context as _; + +use super::Derivation; + +impl Derivation { + /// Convert harmonia Derivation to nix-bindings Derivation. + /// + /// This requires a Store instance because the Nix C API needs it for internal validation. + pub fn from_harmonia( + store: &mut crate::store::Store, + harmonia_drv: &harmonia_store_core::derivation::Derivation, + ) -> anyhow::Result { + let json = serde_json::to_string(harmonia_drv) + .context("Failed to serialize harmonia Derivation to JSON")?; + + store.derivation_from_json(&json) + } +} + +impl TryFrom<&Derivation> for harmonia_store_core::derivation::Derivation { + type Error = anyhow::Error; + + fn try_from(nix_drv: &Derivation) -> anyhow::Result { + let json = nix_drv + .to_json_string() + .context("Failed to convert nix Derivation to JSON")?; + + serde_json::from_str(&json).context("Failed to parse JSON as harmonia Derivation") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_harmonia_derivation() -> harmonia_store_core::derivation::Derivation { + use harmonia_store_core::derivation::{Derivation, DerivationOutput}; + use harmonia_store_core::derived_path::OutputName; + use harmonia_store_core::store_path::StorePath; + use std::collections::{BTreeMap, BTreeSet}; + use std::str::FromStr; + + let system = format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS); + let out_path = "8bs8sd27bzzy6w94fznjd2j8ldmdg7x6-myname"; + + let env = BTreeMap::from([ + ("builder".into(), "/bin/sh".into()), + ("name".into(), "myname".into()), + ("out".into(), format!("/{out_path}").into()), + ("system".into(), system.clone().into()), + ]); + let mut outputs = BTreeMap::new(); + outputs.insert( + OutputName::from_str("out").unwrap(), + DerivationOutput::InputAddressed(StorePath::from_base_path(out_path).unwrap()), + ); + + Derivation { + args: vec!["-c".into(), "echo $name foo > $out".into()], + builder: "/bin/sh".into(), + env, + inputs: BTreeSet::new(), + name: b"myname".as_slice().try_into().unwrap(), + outputs, + platform: system.into(), + structured_attrs: None, + } + } + + #[test] + fn derivation_round_trip_harmonia() { + let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap(); + let harmonia_drv = create_harmonia_derivation(); + + // Convert to nix-bindings Derivation + let nix_drv = Derivation::from_harmonia(&mut store, &harmonia_drv).unwrap(); + + // Convert back to harmonia Derivation + let harmonia_round_trip: harmonia_store_core::derivation::Derivation = + (&nix_drv).try_into().unwrap(); + + assert_eq!(harmonia_drv, harmonia_round_trip); + } + + #[test] + fn derivation_clone() { + let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap(); + let harmonia_drv = create_harmonia_derivation(); + + let derivation = Derivation::from_harmonia(&mut store, &harmonia_drv).unwrap(); + let cloned_derivation = derivation.clone(); + + let original_harmonia: harmonia_store_core::derivation::Derivation = + (&derivation).try_into().unwrap(); + let cloned_harmonia: harmonia_store_core::derivation::Derivation = + (&cloned_derivation).try_into().unwrap(); + + assert_eq!(original_harmonia, cloned_harmonia); + } +} diff --git a/nix-bindings-store/src/derivation/mod.rs b/nix-bindings-store/src/derivation/mod.rs new file mode 100644 index 0000000..557994e --- /dev/null +++ b/nix-bindings-store/src/derivation/mod.rs @@ -0,0 +1,92 @@ +#![cfg(nix_at_least = "2.33.0pre")] + +use nix_bindings_store_sys as raw; +#[cfg(nix_at_least = "2.33")] +use nix_bindings_util::{ + check_call, + context::Context, + result_string_init, + string_return::{callback_get_result_string, callback_get_result_string_data}, +}; +use std::ptr::NonNull; + +/// A Nix derivation +/// +/// **Requires Nix 2.33 or later.** +pub struct Derivation { + pub(crate) inner: NonNull, +} + +impl Derivation { + pub(crate) fn new_raw(inner: NonNull) -> Self { + Derivation { inner } + } + + /// Convert the derivation to JSON (which is encoded to a string). + /// + /// **Requires Nix 2.33 or later.** + /// + /// The JSON format follows the [Nix derivation JSON schema](https://nix.dev/manual/nix/latest/protocols/json/derivation.html). + /// Note that this format is experimental as of writing. + #[cfg(nix_at_least = "2.33")] + pub fn to_json_string(&self) -> anyhow::Result { + let mut ctx = Context::new(); + + unsafe { + let mut r = result_string_init!(); + check_call!(raw::derivation_to_json( + &mut ctx, + self.inner.as_ptr(), + Some(callback_get_result_string), + callback_get_result_string_data(&mut r) + ))?; + r + } + } + + /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings. + /// + /// Construct a new `Derivation` by first cloning the C derivation. + /// + /// # Safety + /// + /// This does not take ownership of the C derivation, so it should be a borrowed pointer, or you should free it. + pub unsafe fn new_raw_clone(inner: NonNull) -> Self { + Self::new_raw( + NonNull::new(raw::derivation_clone(inner.as_ptr())) + .or_else(|| panic!("nix_derivation_clone returned a null pointer")) + .unwrap(), + ) + } + + /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings. + /// + /// Get a pointer to the underlying Nix C API derivation. + /// + /// # Safety + /// + /// This function is unsafe because it returns a raw pointer. The caller must ensure that the pointer is not used beyond the lifetime of this `Derivation`. + pub unsafe fn as_ptr(&self) -> *mut raw::derivation { + self.inner.as_ptr() + } +} + +impl Clone for Derivation { + fn clone(&self) -> Self { + unsafe { Self::new_raw_clone(self.inner) } + } +} + +impl Drop for Derivation { + fn drop(&mut self) { + unsafe { + raw::derivation_free(self.inner.as_ptr()); + } + } +} + +#[cfg(feature = "harmonia")] +mod harmonia; + +#[cfg(test)] +mod tests {} diff --git a/nix-bindings-store/src/lib.rs b/nix-bindings-store/src/lib.rs new file mode 100644 index 0000000..6010f2e --- /dev/null +++ b/nix-bindings-store/src/lib.rs @@ -0,0 +1,3 @@ +pub mod derivation; +pub mod path; +pub mod store; diff --git a/nix-bindings-store/src/path/harmonia.rs b/nix-bindings-store/src/path/harmonia.rs new file mode 100644 index 0000000..660fa1f --- /dev/null +++ b/nix-bindings-store/src/path/harmonia.rs @@ -0,0 +1,65 @@ +use anyhow::{Context as _, Result}; + +use super::{StorePath, STORE_PATH_HASH_SIZE}; + +impl TryFrom<&harmonia_store_core::store_path::StorePath> for StorePath { + type Error = anyhow::Error; + + fn try_from(harmonia_path: &harmonia_store_core::store_path::StorePath) -> Result { + let hash: &[u8; STORE_PATH_HASH_SIZE] = harmonia_path.hash().as_ref(); + StorePath::from_parts(hash, harmonia_path.name().as_ref()) + } +} + +impl TryFrom<&StorePath> for harmonia_store_core::store_path::StorePath { + type Error = anyhow::Error; + + fn try_from(nix_path: &StorePath) -> Result { + let hash = nix_path + .hash() + .context("Failed to get hash from nix StorePath")?; + let harmonia_hash = harmonia_store_core::store_path::StorePathHash::new(hash); + + let name = nix_path + .name() + .context("Failed to get name from nix StorePath")?; + + let harmonia_name: harmonia_store_core::store_path::StorePathName = name + .parse() + .context("Failed to parse name as StorePathName")?; + + Ok(harmonia_store_core::store_path::StorePath::from(( + harmonia_hash, + harmonia_name, + ))) + } +} + +#[cfg(test)] +mod tests { + + #[test] + fn store_path_round_trip_harmonia() { + let harmonia_path: harmonia_store_core::store_path::StorePath = + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo.drv".parse().unwrap(); + + let nix_path: crate::path::StorePath = (&harmonia_path).try_into().unwrap(); + + let harmonia_round_trip: harmonia_store_core::store_path::StorePath = + (&nix_path).try_into().unwrap(); + + assert_eq!(harmonia_path, harmonia_round_trip); + } + + #[test] + fn store_path_harmonia_clone() { + let harmonia_path: harmonia_store_core::store_path::StorePath = + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo.drv".parse().unwrap(); + + let nix_path: crate::path::StorePath = (&harmonia_path).try_into().unwrap(); + let cloned_path = nix_path.clone(); + + assert_eq!(nix_path.name().unwrap(), cloned_path.name().unwrap()); + assert_eq!(nix_path.hash().unwrap(), cloned_path.hash().unwrap()); + } +} diff --git a/nix-bindings-store/src/path/mod.rs b/nix-bindings-store/src/path/mod.rs new file mode 100644 index 0000000..c3478fa --- /dev/null +++ b/nix-bindings-store/src/path/mod.rs @@ -0,0 +1,160 @@ +use std::ptr::NonNull; + +use anyhow::{Context as _, Result}; +use nix_bindings_store_sys as raw; +#[cfg(nix_at_least = "2.33")] +use nix_bindings_util::{check_call, context::Context}; +use nix_bindings_util::{ + result_string_init, + string_return::{callback_get_result_string, callback_get_result_string_data}, +}; + +/// The size of a store path hash in bytes (20 bytes, decoded from nix32). +pub const STORE_PATH_HASH_SIZE: usize = 20; + +#[cfg(nix_at_least = "2.33")] +const _: () = assert!(std::mem::size_of::() == STORE_PATH_HASH_SIZE); + +pub struct StorePath { + raw: NonNull, +} + +impl StorePath { + /// Get the name of the store path. + /// + /// For a store path like `/nix/store/abc1234...-foo-1.2`, this function will return `foo-1.2`. + pub fn name(&self) -> Result { + unsafe { + let mut r = result_string_init!(); + raw::store_path_name( + self.as_ptr(), + Some(callback_get_result_string), + callback_get_result_string_data(&mut r), + ); + r + } + } + + /// Get the hash part of the store path. + /// + /// This returns the decoded hash (not the nix32-encoded string). + #[cfg(nix_at_least = "2.33")] + pub fn hash(&self) -> Result<[u8; STORE_PATH_HASH_SIZE]> { + let mut result = [0u8; STORE_PATH_HASH_SIZE]; + let hash_part: &mut raw::store_path_hash_part = zerocopy::transmute_mut!(&mut result); + + let mut ctx = Context::new(); + + unsafe { + check_call!(raw::store_path_hash(&mut ctx, self.as_ptr(), hash_part))?; + } + Ok(result) + } + + /// Create a StorePath from hash and name components. + #[cfg(nix_at_least = "2.33")] + pub fn from_parts(hash: &[u8; STORE_PATH_HASH_SIZE], name: &str) -> Result { + let hash_part: &raw::store_path_hash_part = zerocopy::transmute_ref!(hash); + + let mut ctx = Context::new(); + + let out_path = unsafe { + check_call!(raw::store_create_from_parts( + &mut ctx, + hash_part, + name.as_ptr() as *const i8, + name.len() + ))? + }; + + NonNull::new(out_path) + .map(|ptr| unsafe { Self::new_raw(ptr) }) + .context("store_create_from_parts returned null") + } + + /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings. + /// + /// Construct a new `StorePath` by first cloning the C store path. + /// + /// # Safety + /// + /// This does not take ownership of the C store path, so it should be a borrowed pointer, or you should free it. + pub unsafe fn new_raw_clone(raw: NonNull) -> Self { + Self::new_raw( + NonNull::new(raw::store_path_clone(raw.as_ptr())) + .or_else(|| panic!("nix_store_path_clone returned a null pointer")) + .unwrap(), + ) + } + + /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings. + /// + /// Takes ownership of a C `nix_store_path`. It will be freed when the `StorePath` is dropped. + /// + /// # Safety + /// + /// The caller must ensure that the provided `NonNull` is valid and that the ownership + /// semantics are correctly followed. The `raw` pointer must not be used after being passed to this function. + pub unsafe fn new_raw(raw: NonNull) -> Self { + StorePath { raw } + } + + /// This is a low level function that you shouldn't have to call unless you are developing the Nix bindings. + /// + /// Get a pointer to the underlying Nix C API store path. + /// + /// # Safety + /// + /// This function is unsafe because it returns a raw pointer. The caller must ensure that the pointer is not used beyond the lifetime of this `StorePath`. + pub unsafe fn as_ptr(&self) -> *mut raw::StorePath { + self.raw.as_ptr() + } +} + +impl Clone for StorePath { + fn clone(&self) -> Self { + unsafe { Self::new_raw_clone(self.raw) } + } +} + +impl Drop for StorePath { + fn drop(&mut self) { + unsafe { + raw::store_path_free(self.as_ptr()); + } + } +} + +#[cfg(all(feature = "harmonia", nix_at_least = "2.33"))] +mod harmonia; + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + #[cfg(nix_at_least = "2.26" /* get_storedir */)] + fn store_path_name() { + let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap(); + let store_dir = store.get_storedir().unwrap(); + let store_path_string = + format!("{store_dir}/rdd4pnr4x9rqc9wgbibhngv217w2xvxl-bash-interactive-5.2p26"); + let store_path = store.parse_store_path(store_path_string.as_str()).unwrap(); + assert_eq!(store_path.name().unwrap(), "bash-interactive-5.2p26"); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn store_path_round_trip() { + let original_hash: [u8; STORE_PATH_HASH_SIZE] = + hex!("0123456789abcdef0011223344556677deadbeef"); + let original_name = "foo.drv"; + + let store_path = StorePath::from_parts(&original_hash, original_name).unwrap(); + + // Round trip gets back what we started with + assert_eq!(store_path.hash().unwrap(), original_hash); + assert_eq!(store_path.name().unwrap(), original_name); + } +} diff --git a/nix-bindings-store/src/store.rs b/nix-bindings-store/src/store.rs new file mode 100644 index 0000000..cdd0977 --- /dev/null +++ b/nix-bindings-store/src/store.rs @@ -0,0 +1,1020 @@ +use anyhow::{bail, Error, Result}; +use nix_bindings_store_sys as raw; +use nix_bindings_util::context::Context; +use nix_bindings_util::string_return::{ + callback_get_result_string, callback_get_result_string_data, +}; +use nix_bindings_util::{check_call, result_string_init}; +use nix_bindings_util_sys as raw_util; +#[cfg(nix_at_least = "2.33.0pre")] +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::ffi::{c_char, CString}; +use std::ptr::null_mut; +use std::ptr::NonNull; +use std::sync::{Arc, LazyLock, Mutex, Weak}; + +#[cfg(nix_at_least = "2.33.0pre")] +use crate::derivation::Derivation; +use crate::path::StorePath; + +/* TODO make Nix itself thread safe */ +static INIT: LazyLock> = LazyLock::new(|| unsafe { + check_call!(raw::libstore_init(&mut Context::new()))?; + Ok(()) +}); + +struct StoreRef { + inner: NonNull, +} +impl StoreRef { + /// # Safety + /// + /// The returned pointer is only valid as long as the `StoreRef` is alive. + pub unsafe fn ptr(&self) -> *mut raw::Store { + self.inner.as_ptr() + } +} +impl Drop for StoreRef { + fn drop(&mut self) { + unsafe { + raw::store_free(self.inner.as_ptr()); + } + } +} +unsafe impl Send for StoreRef {} +/// Unlike pointers in general, operations on raw::Store are thread safe and it is therefore safe to share them between threads. +unsafe impl Sync for StoreRef {} + +/// A [Weak] reference to a store. +pub struct StoreWeak { + inner: Weak, +} +impl StoreWeak { + /// Upgrade the weak reference to a proper [Store]. + /// + /// If no normal reference to the [Store] is around anymore elsewhere, this fails by returning `None`. + pub fn upgrade(&self) -> Option { + self.inner.upgrade().map(|inner| Store { + inner, + context: Context::new(), + }) + } +} + +/// Protects against https://github.com/NixOS/nix/issues/11979 (unless different parameters are passed, in which case it's up to luck, but you do get your own parameters as you asked for). +type StoreCacheMap = HashMap<(Option, Vec<(String, String)>), StoreWeak>; + +static STORE_CACHE: LazyLock>> = + LazyLock::new(|| Arc::new(Mutex::new(HashMap::new()))); + +#[cfg(nix_at_least = "2.33.0pre")] +unsafe extern "C" fn callback_get_result_store_path_set( + _context: *mut raw_util::c_context, + user_data: *mut std::os::raw::c_void, + store_path: *const raw::StorePath, +) { + let ret = user_data as *mut Vec; + let ret: &mut Vec = &mut *ret; + + let store_path = raw::store_path_clone(store_path); + + let store_path = + NonNull::new(store_path).expect("nix_store_parse_path returned a null pointer"); + let store_path = StorePath::new_raw(store_path); + ret.push(store_path); +} + +#[cfg(nix_at_least = "2.33.0pre")] +fn callback_get_result_store_path_set_data(vec: &mut Vec) -> *mut std::os::raw::c_void { + vec as *mut Vec as *mut std::os::raw::c_void +} + +pub struct Store { + inner: Arc, + /* An error context to reuse. This way we don't have to allocate them for each store operation. */ + context: Context, +} +impl Store { + /// Open a store. + /// + /// See [`nix_bindings_store_sys::store_open`] for more information. + #[doc(alias = "nix_store_open")] + pub fn open<'a, 'b>( + url: Option<&str>, + params: impl IntoIterator, + ) -> Result { + let params = params + .into_iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect::>(); + let params2 = params.clone(); + let mut store_cache = STORE_CACHE + .lock() + .map_err(|_| Error::msg("Failed to lock store cache. This should never happen."))?; + match store_cache.entry((url.map(Into::into), params)) { + std::collections::hash_map::Entry::Occupied(mut e) => { + if let Some(store) = e.get().upgrade() { + Ok(store) + } else { + let store = Self::open_uncached( + url, + params2.iter().map(|(k, v)| (k.as_str(), v.as_str())), + )?; + e.insert(store.weak_ref()); + Ok(store) + } + } + std::collections::hash_map::Entry::Vacant(e) => { + let store = Self::open_uncached( + url, + params2.iter().map(|(k, v)| (k.as_str(), v.as_str())), + )?; + e.insert(store.weak_ref()); + Ok(store) + } + } + } + fn open_uncached<'a, 'b>( + url: Option<&str>, + params: impl IntoIterator, + ) -> Result { + let x = INIT.as_ref(); + match x { + Ok(_) => {} + Err(e) => { + // Couldn't just clone the error, so we have to print it here. + bail!("nix_libstore_init error: {}", e); + } + } + + let mut context: Context = Context::new(); + + let uri_cstring = match url { + Some(url) => Some(CString::new(url)?), + None => None, + }; + let uri_ptr = uri_cstring + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(null_mut()); + + // this intermediate value must be here and must not be moved + // because it owns the data the `*const c_char` pointers point to. + let params: Vec<(CString, CString)> = params + .into_iter() + .map(|(k, v)| Ok((CString::new(k)?, CString::new(v)?))) // to do. context + .collect::>()?; + // this intermediate value owns the data the `*mut *const c_char` pointer points to. + let mut params: Vec<_> = params + .iter() + .map(|(k, v)| [k.as_ptr(), v.as_ptr()]) + .collect(); + // this intermediate value owns the data the `*mut *mut *const c_char` pointer points to. + let mut params: Vec<*mut *const c_char> = params + .iter_mut() + .map(|t| t.as_mut_ptr()) + .chain(std::iter::once(null_mut())) // signal the end of the array + .collect(); + + let store = + unsafe { check_call!(raw::store_open(&mut context, uri_ptr, params.as_mut_ptr())) }?; + if store.is_null() { + panic!("nix_c_store_open returned a null pointer without an error"); + } + let store = Store { + inner: Arc::new(StoreRef { + inner: NonNull::new(store).unwrap(), + }), + context, + }; + Ok(store) + } + + /// # Safety + /// + /// The returned pointer is only valid as long as the `Store` is alive. + pub unsafe fn raw_ptr(&self) -> *mut raw::Store { + self.inner.ptr() + } + + #[doc(alias = "nix_store_get_uri")] + pub fn get_uri(&mut self) -> Result { + let mut r = result_string_init!(); + unsafe { + check_call!(raw::store_get_uri( + &mut self.context, + self.inner.ptr(), + Some(callback_get_result_string), + callback_get_result_string_data(&mut r) + )) + }?; + r + } + + #[cfg(nix_at_least = "2.26")] + #[doc(alias = "nix_store_get_storedir")] + pub fn get_storedir(&mut self) -> Result { + let mut r = result_string_init!(); + unsafe { + check_call!(raw::store_get_storedir( + &mut self.context, + self.inner.ptr(), + Some(callback_get_result_string), + callback_get_result_string_data(&mut r) + )) + }?; + r + } + + #[doc(alias = "nix_store_parse_path")] + pub fn parse_store_path(&mut self, path: &str) -> Result { + let path = CString::new(path)?; + unsafe { + let store_path = check_call!(raw::store_parse_path( + &mut self.context, + self.inner.ptr(), + path.as_ptr() + ))?; + let store_path = + NonNull::new(store_path).expect("nix_store_parse_path returned a null pointer"); + Ok(StorePath::new_raw(store_path)) + } + } + + #[doc(alias = "nix_store_real_path")] + pub fn real_path(&mut self, path: &StorePath) -> Result { + let mut r = result_string_init!(); + unsafe { + check_call!(raw::store_real_path( + &mut self.context, + self.inner.ptr(), + path.as_ptr(), + Some(callback_get_result_string), + callback_get_result_string_data(&mut r) + )) + }?; + r + } + + /// Parse a derivation from JSON. + /// + /// **Requires Nix 2.33 or later.** + /// + /// The JSON format follows the [Nix derivation JSON schema](https://nix.dev/manual/nix/latest/protocols/json/derivation.html). + /// Note that this format is experimental as of writing. + /// The derivation is not added to the store; use [`Store::add_derivation`] for that. + /// + /// # Parameters + /// - `json`: A JSON string representing the derivation + /// + /// # Returns + /// A [`Derivation`] object if parsing succeeds, or an error if the JSON is invalid + /// or malformed. + #[cfg(nix_at_least = "2.33.0pre")] + #[doc(alias = "nix_derivation_from_json")] + pub fn derivation_from_json(&mut self, json: &str) -> Result { + let json_cstr = CString::new(json)?; + unsafe { + let drv = check_call!(raw::derivation_from_json( + &mut self.context, + self.inner.ptr(), + json_cstr.as_ptr() + ))?; + let inner = NonNull::new(drv) + .ok_or_else(|| Error::msg("derivation_from_json returned null"))?; + Ok(Derivation::new_raw(inner)) + } + } + + /// Add a derivation to the store. + /// + /// **Requires Nix 2.33 or later.** + /// + /// This computes the store path for the derivation and registers it in the store. + /// The derivation itself is written to the store as a `.drv` file. + /// + /// # Parameters + /// - `drv`: The derivation to add + /// + /// # Returns + /// The store path of the derivation (ending in `.drv`). + #[cfg(nix_at_least = "2.33.0pre")] + #[doc(alias = "nix_add_derivation")] + pub fn add_derivation(&mut self, drv: &Derivation) -> Result { + unsafe { + let path = check_call!(raw::add_derivation( + &mut self.context, + self.inner.ptr(), + drv.inner.as_ptr() + ))?; + let path = + NonNull::new(path).ok_or_else(|| Error::msg("add_derivation returned null"))?; + Ok(StorePath::new_raw(path)) + } + } + + /// Build a derivation and return its outputs. + /// + /// **Requires Nix 2.33 or later.** + /// + /// This builds the derivation at the given store path and returns a map of output + /// names to their realized store paths. The derivation must already exist in the store + /// (see [`Store::add_derivation`]). + /// + /// # Parameters + /// - `path`: The store path of the derivation to build (typically ending in `.drv`) + /// + /// # Returns + /// A [`BTreeMap`] mapping output names (e.g., "out", "dev", "doc") to their store paths. + /// The map is ordered alphabetically by output name for deterministic iteration. + #[cfg(nix_at_least = "2.33.0pre")] + #[doc(alias = "nix_store_realise")] + pub fn realise(&mut self, path: &StorePath) -> Result> { + let mut outputs = BTreeMap::new(); + let userdata = + &mut outputs as *mut BTreeMap as *mut std::os::raw::c_void; + + unsafe extern "C" fn callback( + userdata: *mut std::os::raw::c_void, + outname: *const c_char, + out_path: *const raw::StorePath, + ) { + let outputs = userdata as *mut BTreeMap; + let outputs = &mut *outputs; + + let name = std::ffi::CStr::from_ptr(outname) + .to_string_lossy() + .into_owned(); + + let path = raw::store_path_clone(out_path); + let path = NonNull::new(path).expect("store_path_clone returned null"); + let path = StorePath::new_raw(path); + + outputs.insert(name, path); + } + + unsafe { + check_call!(raw::store_realise( + &mut self.context, + self.inner.ptr(), + path.as_ptr(), + userdata, + Some(callback) + ))?; + } + + Ok(outputs) + } + + /// Get the closure of a specific store path. + /// + /// **Requires Nix 2.33 or later.** + /// + /// Computes the filesystem closure (dependency graph) of a store path, with options + /// to control the direction and which related paths to include. + /// + /// # Parameters + /// - `store_path`: The path to compute the closure from + /// - `flip_direction`: If false, compute the forward closure (paths referenced by this path). + /// If true, compute the backward closure (paths that reference this path). + /// - `include_outputs`: When `flip_direction` is false: for any derivation in the closure, include its outputs. + /// When `flip_direction` is true: for any output in the closure, include derivations that produce it. + /// - `include_derivers`: When `flip_direction` is false: for any output in the closure, include the derivation that produced it. + /// When `flip_direction` is true: for any derivation in the closure, include its outputs. + /// + /// # Returns + /// A vector of store paths in the closure, in no particular order. + #[cfg(nix_at_least = "2.33.0pre")] + #[doc(alias = "nix_store_get_fs_closure")] + pub fn get_fs_closure( + &mut self, + store_path: &StorePath, + flip_direction: bool, + include_outputs: bool, + include_derivers: bool, + ) -> Result> { + let mut r = Vec::new(); + unsafe { + check_call!(raw::store_get_fs_closure( + &mut self.context, + self.inner.ptr(), + store_path.as_ptr(), + flip_direction, + include_outputs, + include_derivers, + callback_get_result_store_path_set_data(&mut r), + Some(callback_get_result_store_path_set) + )) + }?; + Ok(r) + } + + pub fn weak_ref(&self) -> StoreWeak { + StoreWeak { + inner: Arc::downgrade(&self.inner), + } + } +} + +impl Clone for Store { + fn clone(&self) -> Self { + Store { + inner: self.inner.clone(), + context: Context::new(), + } + } +} + +#[cfg(test)] +mod tests { + use ctor::ctor; + use std::collections::HashMap; + + use super::*; + + #[ctor] + fn test_setup() { + // Initialize settings for tests + let _ = INIT.as_ref(); + + // Enable ca-derivations for all tests + nix_bindings_util::settings::set("experimental-features", "ca-derivations").ok(); + + // Disable build hooks to prevent test recursion + nix_bindings_util::settings::set("build-hook", "").ok(); + + // Set custom build dir for sandbox + if cfg!(target_os = "linux") { + nix_bindings_util::settings::set("sandbox-build-dir", "/custom-build-dir-for-test") + .ok(); + } + + std::env::set_var("_NIX_TEST_NO_SANDBOX", "1"); + + // Tests run offline + nix_bindings_util::settings::set("substituters", "").ok(); + } + + #[test] + fn none_works() { + let res = Store::open(None, HashMap::new()); + res.unwrap(); + } + + #[test] + fn auto_works() { + // This is not actually a given. + // Maybe whatever is in NIX_REMOTE or nix.conf is really important. + let res = Store::open(Some("auto"), HashMap::new()); + res.unwrap(); + } + + #[test] + fn invalid_uri_fails() { + let res = Store::open(Some("invalid://uri"), HashMap::new()); + assert!(res.is_err()); + } + + #[test] + fn get_uri() { + let mut store = Store::open(None, HashMap::new()).unwrap(); + let uri = store.get_uri().unwrap(); + assert!(!uri.is_empty()); + // must be ascii + assert!(uri.is_ascii()); + // usually something like "daemon", but that's not something we can check here. + println!("uri: {}", uri); + } + + #[test] + #[ignore] // Needs network access + fn get_uri_nixos_cache() { + let mut store = Store::open(Some("https://cache.nixos.org/"), HashMap::new()).unwrap(); + let uri = store.get_uri().unwrap(); + assert_eq!(uri, "https://cache.nixos.org"); + } + + #[test] + #[cfg(nix_at_least = "2.26" /* get_storedir */)] + fn parse_store_path_ok() { + let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap(); + let store_dir = store.get_storedir().unwrap(); + let store_path_string = + format!("{store_dir}/rdd4pnr4x9rqc9wgbibhngv217w2xvxl-bash-interactive-5.2p26"); + let store_path = store.parse_store_path(store_path_string.as_str()).unwrap(); + let real_store_path = store.real_path(&store_path).unwrap(); + assert_eq!(store_path.name().unwrap(), "bash-interactive-5.2p26"); + assert_eq!(real_store_path, store_path_string); + } + + #[test] + fn parse_store_path_fail() { + let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap(); + let store_path_string = "bash-interactive-5.2p26".to_string(); + let r = store.parse_store_path(store_path_string.as_str()); + match r { + Err(e) => { + assert!(e.to_string().contains("bash-interactive-5.2p26")); + } + _ => panic!("Expected error"), + } + } + + #[test] + fn weak_ref() { + let mut store = Store::open(None, HashMap::new()).unwrap(); + let uri = store.get_uri().unwrap(); + let weak = store.weak_ref(); + let mut store2 = weak.upgrade().unwrap(); + assert_eq!(store2.get_uri().unwrap(), uri); + } + #[test] + fn weak_ref_gone() { + let weak = { + // Concurrent tests calling Store::open will keep the weak reference to auto alive, + // so for this test we need to bypass the global cache. + let store = Store::open_uncached(None, HashMap::new()).unwrap(); + store.weak_ref() + }; + assert!(weak.upgrade().is_none()); + assert!(weak.inner.upgrade().is_none()); + } + + #[cfg(nix_at_least = "2.33.0pre")] + fn create_temp_store() -> (Store, tempfile::TempDir) { + let temp_dir = tempfile::tempdir().unwrap(); + + let store_dir = temp_dir.path().join("store"); + let state_dir = temp_dir.path().join("state"); + let log_dir = temp_dir.path().join("log"); + + let store_dir_str = store_dir.to_str().unwrap(); + let state_dir_str = state_dir.to_str().unwrap(); + let log_dir_str = log_dir.to_str().unwrap(); + + let params = vec![ + ("store", store_dir_str), + ("state", state_dir_str), + ("log", log_dir_str), + ]; + + let store = Store::open(Some("local"), params).unwrap(); + (store, temp_dir) + } + + fn current_system() -> Result { + nix_bindings_util::settings::get("system") + } + + #[cfg(nix_at_least = "2.33")] + fn create_test_derivation_json() -> serde_json::Value { + let system = current_system().unwrap_or_else(|_| { + // Fallback to Rust's platform detection + format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS) + }); + serde_json::json!({ + "args": ["-c", "echo $name foo > $out"], + "builder": "/bin/sh", + "env": { + "builder": "/bin/sh", + "name": "myname", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "system": system + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "myname", + "outputs": { + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "system": system, + "version": 4 + }) + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn derivation_from_json() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + // If we got here, parsing succeeded + drop(drv); + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33.0pre")] + fn derivation_from_invalid_json() { + let (mut store, temp_dir) = create_temp_store(); + let result = store.derivation_from_json("not valid json"); + assert!(result.is_err()); + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn derivation_to_json_round_trip() { + let (mut store, _temp_dir) = create_temp_store(); + let original_value = create_test_derivation_json(); + + // Parse JSON to Derivation + let drv = store + .derivation_from_json(&original_value.to_string()) + .unwrap(); + + // Convert back to JSON + let round_trip_json = drv.to_json_string().unwrap(); + let round_trip_value: serde_json::Value = serde_json::from_str(&round_trip_json).unwrap(); + + // Verify the round-trip JSON matches the original + assert_eq!( + original_value, round_trip_value, + "Round-trip JSON should match original.\nOriginal: {}\nRound-trip: {}", + original_value, round_trip_value + ); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn add_derivation() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Verify we got a .drv path + let name = drv_path.name().unwrap(); + assert!(name.ends_with(".drv")); + + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn realise() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation + let outputs = store.realise(&drv_path).unwrap(); + + // Verify we got the expected output + assert!(outputs.contains_key("out")); + let out_path = &outputs["out"]; + let out_name = out_path.name().unwrap(); + assert_eq!(out_name, "myname"); + + drop(store); + drop(temp_dir); + } + + #[cfg(nix_at_least = "2.33")] + fn create_multi_output_derivation_json() -> serde_json::Value { + let system = current_system() + .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)); + + serde_json::json!({ + "version": 4, + "name": "multi-output-test", + "system": system, + "builder": "/bin/sh", + "args": ["-c", "echo a > $outa; echo b > $outb; echo c > $outc; echo d > $outd; echo e > $oute; echo f > $outf; echo g > $outg; echo h > $outh; echo i > $outi; echo j > $outj"], + "env": { + "builder": "/bin/sh", + "name": "multi-output-test", + "system": system, + "outf": "/1vkfzqpwk313b51x0xjyh5s7w1lx141mr8da3dr9wqz5aqjyr2fh", + "outd": "/1ypxifgmbzp5sd0pzsp2f19aq68x5215260z3lcrmy5fch567lpm", + "outi": "/1wmasjnqi12j1mkjbxazdd0qd0ky6dh1qry12fk8qyp5kdamhbdx", + "oute": "/1f9r2k1s168js509qlw8a9di1qd14g5lqdj5fcz8z7wbqg11qp1f", + "outh": "/1rkx1hmszslk5nq9g04iyvh1h7bg8p92zw0hi4155hkjm8bpdn95", + "outc": "/1rj4nsf9pjjqq9jsq58a2qkwa7wgvgr09kgmk7mdyli6h1plas4w", + "outb": "/1p7i1dxifh86xq97m5kgb44d7566gj7rfjbw7fk9iij6ca4akx61", + "outg": "/14f8qi0r804vd6a6v40ckylkk1i6yl6fm243qp6asywy0km535lc", + "outj": "/0gkw1366qklqfqb2lw1pikgdqh3cmi3nw6f1z04an44ia863nxaz", + "outa": "/039akv9zfpihrkrv4pl54f3x231x362bll9afblsgfqgvx96h198" + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "outputs": { + "outd": { "hashAlgo": "sha256", "method": "nar" }, + "outf": { "hashAlgo": "sha256", "method": "nar" }, + "outg": { "hashAlgo": "sha256", "method": "nar" }, + "outb": { "hashAlgo": "sha256", "method": "nar" }, + "outc": { "hashAlgo": "sha256", "method": "nar" }, + "outi": { "hashAlgo": "sha256", "method": "nar" }, + "outj": { "hashAlgo": "sha256", "method": "nar" }, + "outh": { "hashAlgo": "sha256", "method": "nar" }, + "outa": { "hashAlgo": "sha256", "method": "nar" }, + "oute": { "hashAlgo": "sha256", "method": "nar" } + } + }) + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn realise_multi_output_ordering() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_multi_output_derivation_json(); + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation + let outputs = store.realise(&drv_path).unwrap(); + + // Verify outputs are complete (BTreeMap guarantees ordering) + let output_names: Vec<&String> = outputs.keys().collect(); + let expected_order = vec![ + "outa", "outb", "outc", "outd", "oute", "outf", "outg", "outh", "outi", "outj", + ]; + assert_eq!(output_names, expected_order); + + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn realise_invalid_system() { + let (mut store, temp_dir) = create_temp_store(); + + // Create a derivation with an invalid system + let system = "bogus65-bogusos"; + let drv_json = serde_json::json!({ + "args": ["-c", "echo $name foo > $out"], + "builder": "/bin/sh", + "env": { + "builder": "/bin/sh", + "name": "myname", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "system": system + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "myname", + "outputs": { + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "system": system, + "version": 4 + }); + + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Try to build - should fail + let result = store.realise(&drv_path); + let err = match result { + Ok(_) => panic!("Build should fail with invalid system"), + Err(e) => e.to_string(), + }; + assert!( + err.contains("required system or feature not available") + || err.contains("platform mismatch"), + "Error should mention system not available, got: {}", + err + ); + + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn realise_builder_fails() { + let (mut store, temp_dir) = create_temp_store(); + + let system = current_system() + .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)); + + // Create a derivation where the builder exits with error + let drv_json = serde_json::json!({ + "args": ["-c", "exit 1"], + "builder": "/bin/sh", + "env": { + "builder": "/bin/sh", + "name": "failing", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "system": system + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "failing", + "outputs": { + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "system": system, + "version": 4 + }); + + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Try to build - should fail + let result = store.realise(&drv_path); + let err = match result { + Ok(_) => panic!("Build should fail when builder exits with error"), + Err(e) => e.to_string(), + }; + assert!( + err.contains("builder failed with exit code 1"), + "Error should mention builder failed with exit code, got: {}", + err + ); + + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn realise_builder_no_output() { + let (mut store, temp_dir) = create_temp_store(); + + let system = current_system() + .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)); + + // Create a derivation where the builder succeeds but produces no output + let drv_json = serde_json::json!({ + "args": ["-c", "true"], + "builder": "/bin/sh", + "env": { + "builder": "/bin/sh", + "name": "no-output", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "system": system + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "no-output", + "outputs": { + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "system": system, + "version": 4 + }); + + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Try to build - should fail + let result = store.realise(&drv_path); + let err = match result { + Ok(_) => panic!("Build should fail when builder produces no output"), + Err(e) => e.to_string(), + }; + assert!( + err.contains("failed to produce output path"), + "Error should mention failed to produce output, got: {}", + err + ); + + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn get_fs_closure_with_outputs() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation to get the output path + let outputs = store.realise(&drv_path).unwrap(); + let out_path = &outputs["out"]; + let out_path_name = out_path.name().unwrap(); + + // Get closure with include_outputs=true + let closure = store.get_fs_closure(&drv_path, false, true, false).unwrap(); + + // The closure should contain at least the derivation and its output + assert!( + closure.len() >= 2, + "Closure should contain at least drv and output" + ); + + // Verify the output path is in the closure + let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name); + assert!( + out_in_closure, + "Output path should be in closure when include_outputs=true" + ); + + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn get_fs_closure_without_outputs() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation to get the output path + let outputs = store.realise(&drv_path).unwrap(); + let out_path = &outputs["out"]; + let out_path_name = out_path.name().unwrap(); + + // Get closure with include_outputs=false + let closure = store + .get_fs_closure(&drv_path, false, false, false) + .unwrap(); + + // Verify the output path is NOT in the closure + let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name); + assert!( + !out_in_closure, + "Output path should not be in closure when include_outputs=false" + ); + + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn get_fs_closure_flip_direction() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + + // Build the derivation to get the output path + let outputs = store.realise(&drv_path).unwrap(); + let out_path = &outputs["out"]; + let out_path_name = out_path.name().unwrap(); + + // Get closure with flip_direction=true (reverse dependencies) + let closure = store.get_fs_closure(&drv_path, true, true, false).unwrap(); + + // Verify the output path is NOT in the closure when direction is flipped + let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name); + assert!( + !out_in_closure, + "Output path should not be in closure when flip_direction=true" + ); + + drop(store); + drop(temp_dir); + } + + #[test] + #[cfg(nix_at_least = "2.33")] + fn get_fs_closure_include_derivers() { + let (mut store, temp_dir) = create_temp_store(); + let drv_json = create_test_derivation_json(); + let drv = store.derivation_from_json(&drv_json.to_string()).unwrap(); + let drv_path = store.add_derivation(&drv).unwrap(); + let drv_path_name = drv_path.name().unwrap(); + + // Build the derivation to get the output path + let outputs = store.realise(&drv_path).unwrap(); + let out_path = &outputs["out"]; + + // Get closure of the output path with include_derivers=true + let closure = store.get_fs_closure(out_path, false, false, true).unwrap(); + + // Verify the derivation path is in the closure + let drv_in_closure = closure.iter().any(|p| p.name().unwrap() == drv_path_name); + assert!( + drv_in_closure, + "Derivation should be in closure when include_derivers=true" + ); + + drop(store); + drop(temp_dir); + } +} diff --git a/nix-bindings-util-sys/Cargo.toml b/nix-bindings-util-sys/Cargo.toml new file mode 100644 index 0000000..2441074 --- /dev/null +++ b/nix-bindings-util-sys/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nix-bindings-util-sys" +version = "0.2.1" +edition = "2021" +build = "build.rs" +license = "LGPL-2.1" +description = "Low-level FFI bindings to Nix utility library" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_util_sys/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] + +[build-dependencies] +bindgen = "0.69" +pkg-config = "0.3" diff --git a/nix-bindings-util-sys/README.md b/nix-bindings-util-sys/README.md new file mode 100644 index 0000000..b504cb8 --- /dev/null +++ b/nix-bindings-util-sys/README.md @@ -0,0 +1,11 @@ +# nix-bindings-util-sys + +This crate contains generated bindings for the Nix C API (`nix-util-c`). +**You should not have to use this crate directly,** and so you should probably not add it to your dependencies. +Instead, use the `nix-bindings-util` crate, which _should_ be sufficient. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_util_sys/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-util-sys/build.rs b/nix-bindings-util-sys/build.rs new file mode 100644 index 0000000..2851ee2 --- /dev/null +++ b/nix-bindings-util-sys/build.rs @@ -0,0 +1,52 @@ +use std::env; +use std::path::PathBuf; + +#[derive(Debug)] +struct StripNixPrefix {} +impl bindgen::callbacks::ParseCallbacks for StripNixPrefix { + fn item_name(&self, name: &str) -> Option { + name.strip_prefix("nix_").map(String::from) + } +} + +fn main() { + // Tell cargo to invalidate the built crate whenever the wrapper changes + println!("cargo:rerun-if-changed=include/nix-c-util.h"); + println!("cargo:rustc-link-lib=nixutil"); + + // https://rust-lang.github.io/rust-bindgen/library-usage.html + let bindings = bindgen::Builder::default() + .header("include/nix-c-util.h") + // Find the includes + .clang_args(c_headers()) + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .parse_callbacks(Box::new(StripNixPrefix {})) + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} + +fn c_headers() -> Vec { + let mut args = Vec::new(); + // args.push("-isystem".to_string()); + for path in pkg_config::probe_library("nix-util-c") + .unwrap() + .include_paths + .iter() + { + args.push(format!("-I{}", path.to_str().unwrap())); + } + + // write to stderr for debugging + eprintln!("c_headers: {:?}", args); + args +} diff --git a/nix-bindings-util-sys/include/nix-c-util.h b/nix-bindings-util-sys/include/nix-c-util.h new file mode 100644 index 0000000..7fd0bc6 --- /dev/null +++ b/nix-bindings-util-sys/include/nix-c-util.h @@ -0,0 +1 @@ +#include diff --git a/nix-bindings-util-sys/src/lib.rs b/nix-bindings-util-sys/src/lib.rs new file mode 100644 index 0000000..b7c06dc --- /dev/null +++ b/nix-bindings-util-sys/src/lib.rs @@ -0,0 +1,31 @@ +//! Raw bindings to Nix C API +//! +//! This crate contains automatically generated bindings from the Nix C headers. +//! The bindings are generated by bindgen and include C-style naming conventions +//! and documentation comments that don't always conform to Rust standards. +//! +//! Normally you don't have to use this crate directly. +//! Instead use `nix-util`. + +// This file must only contain generated code, so that the module-level +// #![allow(...)] attributes don't suppress warnings in hand-written code. +// If you need to add hand-written code, use a submodule to isolate the +// generated code. See: +// https://github.com/nixops4/nixops4/pull/138/commits/330c3881be3d3cf3e59adebbe0ab1c0f15f6d2c9 + +// Standard bindgen suppressions for C naming conventions +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// Clippy suppressions for generated C bindings +// bindgen doesn't generate safety docs +#![allow(clippy::missing_safety_doc)] +// Rustdoc suppressions for generated C documentation +// The C headers contain Doxygen-style documentation that doesn't translate +// well to Rust's rustdoc format, causing various warnings: +#![allow(rustdoc::broken_intra_doc_links)] // @param[in]/[out] references don't resolve +#![allow(rustdoc::bare_urls)] // C docs may contain unescaped URLs +#![allow(rustdoc::invalid_html_tags)] // Doxygen HTML tags like +#![allow(rustdoc::invalid_codeblock_attributes)] // C code examples may use unsupported attributes + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nix-bindings-util/Cargo.toml b/nix-bindings-util/Cargo.toml new file mode 100644 index 0000000..78e8353 --- /dev/null +++ b/nix-bindings-util/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nix-bindings-util" +version = "0.2.1" +edition = "2021" +license = "LGPL-2.1" +description = "Rust bindings to Nix utility library" +repository = "https://github.com/nixops4/nix-bindings-rust" +documentation = "https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_util/" +readme = "README.md" + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0" +nix-bindings-util-sys = { path = "../nix-bindings-util-sys", version = "0.2.1" } + +[dev-dependencies] +ctor = "0.2" + +[lints.rust] +warnings = "deny" +dead-code = "allow" + +[lints.clippy] +type-complexity = "allow" +# We're still trying to make Nix more thread-safe, want forward-compat +arc-with-non-send-sync = "allow" diff --git a/nix-bindings-util/README.md b/nix-bindings-util/README.md new file mode 100644 index 0000000..0a29f29 --- /dev/null +++ b/nix-bindings-util/README.md @@ -0,0 +1,9 @@ +# nix-bindings-util + +Rust bindings to the Nix utility library. + +[API Documentation](https://nixops4.github.io/nix-bindings-rust/development/nix_bindings_util/) + +## Changelog + +See the [nix-bindings-rust changelog](https://github.com/nixops4/nix-bindings-rust/blob/main/CHANGELOG.md). diff --git a/nix-bindings-util/src/context.rs b/nix-bindings-util/src/context.rs new file mode 100644 index 0000000..dbb333e --- /dev/null +++ b/nix-bindings-util/src/context.rs @@ -0,0 +1,159 @@ +use anyhow::{bail, Result}; +use nix_bindings_util_sys as raw; +use std::ptr::null_mut; +use std::ptr::NonNull; + +/// A context for error handling, when interacting directly with the generated bindings for the C API in [nix_bindings_util_sys]. +/// +/// The `nix-store` and `nix-expr` libraries that consume this type internally store a private context in their `EvalState` and `Store` structs to avoid allocating a new context for each operation. The state of a context is irrelevant when used correctly (e.g. with [check_call!]), so it's safe to reuse, and safe to allocate more contexts in methods such as [Clone::clone]. +pub struct Context { + inner: NonNull, +} + +impl Default for Context { + fn default() -> Self { + Self::new() + } +} + +impl Context { + pub fn new() -> Self { + let ctx = unsafe { raw::c_context_create() }; + if ctx.is_null() { + // We've failed to allocate a (relatively small) Context struct. + // We're almost certainly going to crash anyways. + panic!("nix_c_context_create returned a null pointer"); + } + Context { + inner: NonNull::new(ctx).unwrap(), + } + } + + /// Access the C context pointer. + /// + /// We recommend to use `check_call!` if possible. + pub fn ptr(&mut self) -> *mut raw::c_context { + self.inner.as_ptr() + } + + /// Check the error code and return an error if it's not `NIX_OK`. + /// + /// We recommend to use `check_call!` if possible. + pub fn check_err(&self) -> Result<()> { + let err = unsafe { raw::err_code(self.inner.as_ptr()) }; + if err != raw::err_NIX_OK { + // msgp is a borrowed pointer (pointing into the context), so we don't need to free it + let msgp = unsafe { raw::err_msg(null_mut(), self.inner.as_ptr(), null_mut()) }; + // Turn the i8 pointer into a Rust string by copying + let msg: &str = unsafe { core::ffi::CStr::from_ptr(msgp).to_str()? }; + bail!("{}", msg); + } + Ok(()) + } + + pub fn clear(&mut self) { + unsafe { + raw::set_err_msg(self.inner.as_ptr(), raw::err_NIX_OK, c"".as_ptr()); + } + } + + pub fn check_err_and_clear(&mut self) -> Result<()> { + let r = self.check_err(); + if r.is_err() { + self.clear(); + } + r + } + + pub fn check_one_call_or_key_none T>( + &mut self, + f: F, + ) -> Result> { + let t = f(self.ptr()); + if unsafe { raw::err_code(self.inner.as_ptr()) == raw::err_NIX_ERR_KEY } { + self.clear(); + return Ok(None); + } + self.check_err_and_clear()?; + Ok(Some(t)) + } +} + +impl Drop for Context { + fn drop(&mut self) { + unsafe { + raw::c_context_free(self.inner.as_ptr()); + } + } +} + +#[macro_export] +macro_rules! check_call { + ($($f:ident)::+($ctx:expr $(, $arg:expr)*)) => { + { + let ctx : &mut $crate::context::Context = $ctx; + let ret = $($f)::*(ctx.ptr() $(, $arg)*); + match ctx.check_err() { + Ok(_) => Ok(ret), + Err(e) => { + ctx.clear(); + Err(e) + } + } + } + } +} + +pub use check_call; + +// TODO: Generalize this macro to work with any error code or any error handling logic +#[macro_export] +macro_rules! check_call_opt_key { + ($($f:ident)::+($ctx:expr, $($arg:expr),*)) => { + { + let ctx : &mut $crate::context::Context = $ctx; + let ret = $($f)::*(ctx.ptr(), $($arg,)*); + if unsafe { $crate::raw_sys::err_code(ctx.ptr()) == $crate::raw_sys::err_NIX_ERR_KEY } { + ctx.clear(); + return Ok(None); + } + match ctx.check_err() { + Ok(_) => Ok(Some(ret)), + Err(e) => { + ctx.clear(); + Err(e) + } + } + } + } +} + +pub use check_call_opt_key; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn context_new_and_drop() { + // don't crash + let _c = Context::new(); + } + + fn set_dummy_err(ctx_ptr: *mut raw::c_context) { + unsafe { + raw::set_err_msg( + ctx_ptr, + raw::err_NIX_ERR_UNKNOWN, + c"dummy error message".as_ptr(), + ); + } + } + + #[test] + fn check_call_dynamic_context() { + let r = check_call!(set_dummy_err(&mut Context::new())); + assert!(r.is_err()); + assert_eq!(r.unwrap_err().to_string(), "dummy error message"); + } +} diff --git a/nix-bindings-util/src/lib.rs b/nix-bindings-util/src/lib.rs new file mode 100644 index 0000000..f626d96 --- /dev/null +++ b/nix-bindings-util/src/lib.rs @@ -0,0 +1,8 @@ +pub mod context; +pub mod settings; +#[macro_use] +pub mod string_return; +pub mod nix_version; + +// Re-export for use in macros +pub use nix_bindings_util_sys as raw_sys; diff --git a/nix-bindings-util/src/nix_version.rs b/nix-bindings-util/src/nix_version.rs new file mode 100644 index 0000000..b6da8ce --- /dev/null +++ b/nix-bindings-util/src/nix_version.rs @@ -0,0 +1,101 @@ +//! Nix version parsing and conditional compilation support. + +/// Emit [`cargo:rustc-cfg`] directives for Nix version-based conditional compilation. +/// +/// Call from build.rs with the Nix version and desired version gates. +/// +/// [`cargo:rustc-cfg`]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-cfg +/// +/// # Example +/// +/// ``` +/// use nix_bindings_util::nix_version::emit_version_cfg; +/// # // Stub pkg_config so that we can render a full usage example +/// # mod pkg_config { pub fn probe_library(_: &str) -> Result { Ok(Library { version: "2.33.0pre".into() }) } +/// # pub struct Library { pub version: String } } +/// +/// let nix_version = pkg_config::probe_library("nix-store-c").unwrap().version; +/// emit_version_cfg(&nix_version, &["2.26", "2.33.0pre", "2.33"]); +/// ``` +/// +/// Emits `nix_at_least="2.26"` and `nix_at_least="2.33.0pre"` for version 2.33.0pre, +/// usable as `#[cfg(nix_at_least = "2.26")]`. +pub fn emit_version_cfg(nix_version: &str, relevant_versions: &[&str]) { + // Declare the known versions for cargo check-cfg + let versions = relevant_versions + .iter() + .map(|v| format!("\"{}\"", v)) + .collect::>() + .join(","); + + println!( + "cargo:rustc-check-cfg=cfg(nix_at_least,values({}))", + versions + ); + + let nix_version = parse_version(nix_version); + + for version_str in relevant_versions { + let version = parse_version(version_str); + if nix_version >= version { + println!("cargo:rustc-cfg=nix_at_least=\"{}\"", version_str); + } + } +} + +/// Parse a Nix version string into a comparable tuple `(major, minor, patch)`. +/// +/// Pre-release versions (containing `"pre"`) get patch = -1, sorting before stable releases. +/// Omitted patch defaults to 0. +/// +/// # Examples +/// +/// ``` +/// use nix_bindings_util::nix_version::parse_version; +/// +/// assert_eq!(parse_version("2.26"), (2, 26, 0)); +/// assert_eq!(parse_version("2.33.0pre"), (2, 33, -1)); +/// assert_eq!(parse_version("2.33"), (2, 33, 0)); +/// assert_eq!(parse_version("2.33.1"), (2, 33, 1)); +/// +/// // Pre-release versions sort before stable +/// assert!(parse_version("2.33.0pre") < parse_version("2.33")); +/// ``` +pub fn parse_version(version_str: &str) -> (u32, u32, i32) { + let parts = version_str.split('.').collect::>(); + let major = parts[0].parse::().unwrap(); + let minor = parts[1].parse::().unwrap(); + let patch = if parts.get(2).is_some_and(|s| s.contains("pre")) { + -1i32 + } else { + parts + .get(2) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0) + }; + (major, minor, patch) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_version() { + assert_eq!(parse_version("2.26"), (2, 26, 0)); + assert_eq!(parse_version("2.33.0pre"), (2, 33, -1)); + assert_eq!(parse_version("2.33"), (2, 33, 0)); + assert_eq!(parse_version("2.33.1"), (2, 33, 1)); + } + + #[test] + fn test_version_ordering() { + // Pre-release versions should sort before stable + assert!(parse_version("2.33.0pre") < parse_version("2.33")); + assert!(parse_version("2.33.0pre") < parse_version("2.33.0")); + + // Normal version ordering + assert!(parse_version("2.26") < parse_version("2.33")); + assert!(parse_version("2.33") < parse_version("2.33.1")); + } +} diff --git a/nix-bindings-util/src/settings.rs b/nix-bindings-util/src/settings.rs new file mode 100644 index 0000000..8c213aa --- /dev/null +++ b/nix-bindings-util/src/settings.rs @@ -0,0 +1,104 @@ +use anyhow::Result; +use nix_bindings_util_sys as raw; +use std::sync::Mutex; + +use crate::{ + check_call, context, result_string_init, + string_return::{callback_get_result_string, callback_get_result_string_data}, +}; + +// Global mutex to protect concurrent access to Nix settings +// See the documentation on `set()` for important thread safety information. +static SETTINGS_MUTEX: Mutex<()> = Mutex::new(()); + +/// Set a Nix setting. +/// +/// # Thread Safety +/// +/// This function uses a mutex to serialize access through the Rust API. +/// However, the underlying Nix settings system uses global mutable state +/// without internal synchronization. +/// +/// The mutex provides protection between Rust callers but cannot prevent: +/// - C++ Nix code from modifying settings concurrently +/// - Other Nix operations from reading settings during modification +/// +/// For multi-threaded applications, ensure that no other Nix operations +/// are running while changing settings. Settings are best modified during +/// single-threaded initialization. +pub fn set(key: &str, value: &str) -> Result<()> { + // Lock the mutex to ensure thread-safe access to global settings + let guard = SETTINGS_MUTEX.lock().unwrap(); + + let mut ctx = context::Context::new(); + let key = std::ffi::CString::new(key)?; + let value = std::ffi::CString::new(value)?; + unsafe { + check_call!(raw::setting_set(&mut ctx, key.as_ptr(), value.as_ptr()))?; + } + drop(guard); + Ok(()) +} + +/// Get a Nix setting. +/// +/// # Thread Safety +/// +/// See the documentation on [`set()`] for important thread safety information. +pub fn get(key: &str) -> Result { + // Lock the mutex to ensure thread-safe access to global settings + let guard = SETTINGS_MUTEX.lock().unwrap(); + + let mut ctx = context::Context::new(); + let key = std::ffi::CString::new(key)?; + let mut r: Result = result_string_init!(); + unsafe { + check_call!(raw::setting_get( + &mut ctx, + key.as_ptr(), + Some(callback_get_result_string), + callback_get_result_string_data(&mut r) + ))?; + } + drop(guard); + r +} + +#[cfg(test)] +mod tests { + use crate::check_call; + + use super::*; + + #[ctor::ctor] + fn setup() { + let mut ctx = context::Context::new(); + unsafe { + check_call!(nix_bindings_util_sys::libutil_init(&mut ctx)).unwrap(); + } + } + + #[test] + fn set_get() { + // Something that shouldn't matter if it's a different value temporarily + let key = "json-log-path"; + + // Save the old value, in case it's important. Probably not. + // If this doesn't work, pick a different setting to test with + let old_value = get(key).unwrap(); + + let new_value = "/just/a/path/that/we/are/storing/into/some/option/for/testing/purposes"; + + let res_e = (|| { + set(key, new_value)?; + get(key) + })(); + + // Restore immediately; try not to affect other tests (if relevant). + set(key, old_value.as_str()).unwrap(); + + let res = res_e.unwrap(); + + assert_eq!(res, new_value); + } +} diff --git a/nix-bindings-util/src/string_return.rs b/nix-bindings-util/src/string_return.rs new file mode 100644 index 0000000..205cebe --- /dev/null +++ b/nix-bindings-util/src/string_return.rs @@ -0,0 +1,88 @@ +use anyhow::Result; + +/// Callback for nix_store_get_uri and other functions that return a string. +/// +/// This function is used by the other nix_* crates, and you should never need to call it yourself. +/// +/// Some functions in the nix library "return" strings without giving you ownership over them, by letting you pass a callback function that gets to look at that string. This callback simply turns that string pointer into an owned rust String. +/// +/// # Safety +/// +/// _Manual memory management_ +/// +/// Only for passing to the nix C API. Do not call this function directly. +pub unsafe extern "C" fn callback_get_result_string( + start: *const ::std::os::raw::c_char, + n: std::os::raw::c_uint, + user_data: *mut std::os::raw::c_void, +) { + let ret = user_data as *mut Result; + + if start.is_null() { + if n != 0 { + panic!("callback_get_result_string: start is null but n is not zero"); + } + *ret = Ok(String::new()); + return; + } + + let slice = std::slice::from_raw_parts(start as *const u8, n as usize); + + if (*ret).is_ok() { + panic!( + "callback_get_result_string: Result must be initialized to Err. Did Nix call us twice?" + ); + } + + *ret = String::from_utf8(slice.to_vec()) + .map_err(|e| anyhow::format_err!("Nix string is not valid UTF-8: {}", e)); +} + +pub fn callback_get_result_string_data(vec: &mut Result) -> *mut std::os::raw::c_void { + vec as *mut Result as *mut std::os::raw::c_void +} + +#[macro_export] +macro_rules! result_string_init { + () => { + Err(anyhow::anyhow!("String was not set by Nix C API")) + }; +} + +#[cfg(test)] +mod tests { + use super::*; + use nix_bindings_util_sys as raw; + + /// Typecheck the function signature against the generated bindings in nix_bindings_util_sys. + static _CALLBACK_GET_RESULT_STRING: raw::get_string_callback = Some(callback_get_result_string); + + #[test] + fn test_callback_get_result_string_empty() { + let mut ret: Result = result_string_init!(); + let start: *const std::os::raw::c_char = std::ptr::null(); + let n: std::os::raw::c_uint = 0; + let user_data: *mut std::os::raw::c_void = callback_get_result_string_data(&mut ret); + + unsafe { + callback_get_result_string(start, n, user_data); + } + + let s = ret.unwrap(); + assert_eq!(s, ""); + } + + #[test] + fn test_callback_result_string() { + let mut ret: Result = result_string_init!(); + let start = b"helloGARBAGE".as_ptr() as *const std::os::raw::c_char; + let n: std::os::raw::c_uint = 5; + let user_data: *mut std::os::raw::c_void = callback_get_result_string_data(&mut ret); + unsafe { + callback_get_result_string(start, n, user_data); + } + + let s = ret.unwrap(); + assert_eq!(s, "hello"); + } +} diff --git a/nixide-sys/Cargo.toml b/nixide-sys/Cargo.toml deleted file mode 100644 index 0be064c..0000000 --- a/nixide-sys/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "nixide-sys" -description = "Unsafe direct FFI bindings to libnix C API" -version = "0.1.0" -readme = "../README.md" -license = "GPL-3.0" -repository = "https://codeberg.org/luminary/nixide" -authors = [ - "_cry64 ", - "foxxyora " -] - -edition = "2024" -build = "build.rs" - -[package.metadata.docs.rs] -targets = [ "x86_64-unknown-linux-gnu" ] - -[lib] -path = "lib.rs" - -[features] -default = ["util"] -expr = [] -fetchers = [] -flakes = [] -store = [] -util = [] -gc = [] - -[build-dependencies] -bindgen = { default-features = false, features = [ "logging", "runtime" ], version = "0.72.1" } -doxygen-bindgen = "0.1.3" -pkg-config.workspace = true -cc.workspace = true - -[dev-dependencies] -serial_test = "3.4.0" diff --git a/nixide-sys/build.rs b/nixide-sys/build.rs deleted file mode 100644 index 6631264..0000000 --- a/nixide-sys/build.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::env; -use std::path::PathBuf; - -use bindgen::callbacks::ParseCallbacks; - -#[derive(Debug)] -struct DoxygenCallbacks; - -impl ParseCallbacks for DoxygenCallbacks { - 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() { - // Invalidate the built crate whenever the wrapper changes - println!("cargo:rerun-if-changed=include/wrapper.h"); - - // Use pkg-config to find nix-store include and link paths - // This NEEDS to be included, or otherwise `nix_api_store.h` cannot - // be found. - let libs = [ - "nix-main-c", - "nix-expr-c", - "nix-store-c", - "nix-util-c", - "nix-flake-c", - ]; - - let lib_args: Vec = libs - .iter() - .map(|&name| { - let lib = pkg_config::probe_library(name) - .expect(&format!("Unable to find .pc file for {}", name)); - - for p in lib.link_files { - println!("cargo:rustc-link-lib={}", p.display()); - } - - lib.include_paths - .into_iter() - .map(|p| format!("-I{}", p.display())) - }) - .flatten() - .collect(); - - let bindings = bindgen::Builder::default() - .clang_args(lib_args) - // The input header we would like to generate bindings for - .header("include/wrapper.h") - // Invalidate the built crate when an included header file changes - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) - // Add `doxygen_bindgen` callbacks - .parse_callbacks(Box::new(DoxygenCallbacks)) - // Format generated bindings with rustfmt - .formatter(bindgen::Formatter::Rustfmt) - .rustfmt_configuration_file(std::fs::canonicalize(".rustfmt.toml").ok()) - // Finish the builder and generate the bindings - .generate() - // Unwrap the Result and panic on failure - .expect("Unable to generate bindings"); - - // Write the bindings to the $OUT_DIR/bindings.rs file - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - bindings - .write_to_file(out_path.join("bindings.rs")) - .expect("Couldn't write bindings!"); -} diff --git a/nixide-sys/include/wrapper.h b/nixide-sys/include/wrapper.h deleted file mode 100644 index 30dd94e..0000000 --- a/nixide-sys/include/wrapper.h +++ /dev/null @@ -1,23 +0,0 @@ -// Pure C API for store operations -#include - -// Pure C API for error handling -#include - -// Pure C API for the Nix evaluator -#include - -// Pure C API for external values -#include - -// Pure C API for value manipulation -#include - -// Pure C API for fetcher operations -#include - -// Pure C API for flake support -#include - -// Pure C API for main/CLI support -#include diff --git a/nixide-sys/lib.rs b/nixide-sys/lib.rs deleted file mode 100644 index 4c744cd..0000000 --- a/nixide-sys/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! # nixide-sys -//! -//! Unsafe direct FFI bindings to libnix 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. - -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] -#![allow(rustdoc::bare_urls)] -#![allow(rustdoc::invalid_html_tags)] - -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nixide-sys/tests/eval.rs b/nixide-sys/tests/eval.rs deleted file mode 100644 index 742b4d0..0000000 --- a/nixide-sys/tests/eval.rs +++ /dev/null @@ -1,1235 +0,0 @@ -#![cfg(test)] - -use std::{ - ffi::{CStr, CString}, - ptr, -}; - -use nixide_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/nixide-sys/tests/flake.rs b/nixide-sys/tests/flake.rs deleted file mode 100644 index 01fed21..0000000 --- a/nixide-sys/tests/flake.rs +++ /dev/null @@ -1,78 +0,0 @@ -#![cfg(test)] - -use std::ptr; - -use nixide_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/nixide-sys/tests/memory.rs b/nixide-sys/tests/memory.rs deleted file mode 100644 index 229b6bd..0000000 --- a/nixide-sys/tests/memory.rs +++ /dev/null @@ -1,503 +0,0 @@ -#![cfg(test)] - -use std::ffi::CString; - -use nixide_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/nixide-sys/tests/primop.rs b/nixide-sys/tests/primop.rs deleted file mode 100644 index ecfacf8..0000000 --- a/nixide-sys/tests/primop.rs +++ /dev/null @@ -1,639 +0,0 @@ -#![cfg(test)] - -use std::{ - ffi::CString, - sync::atomic::{AtomicU32, Ordering}, -}; - -use nixide_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 (no arguments) - let result = nix_alloc_value(ctx, state); - assert!(!result.is_null()); - - let call_err = nix_value_call(ctx, state, primop_value, 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/nixide-sys/tests/store.rs b/nixide-sys/tests/store.rs deleted file mode 100644 index bb12a33..0000000 --- a/nixide-sys/tests/store.rs +++ /dev/null @@ -1,278 +0,0 @@ -#![cfg(test)] - -use std::{ffi::CString, ptr}; - -use nixide_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/nixide-sys/tests/util.rs b/nixide-sys/tests/util.rs deleted file mode 100644 index 038d30b..0000000 --- a/nixide-sys/tests/util.rs +++ /dev/null @@ -1,152 +0,0 @@ -#![cfg(test)] - -use std::ffi::{CStr, CString}; - -use nixide_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/nixide/Cargo.toml b/nixide/Cargo.toml deleted file mode 100644 index 7a1b4e4..0000000 --- a/nixide/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "nixide" -description = "Safe & oxidized bindings to the Nix API" -version = "0.1.0" -readme = "../README.md" -license = "GPL-3.0" -repository = "https://codeberg.org/luminary/nixide" -authors = [ - "_cry64 ", - "foxxyora ", -] -edition = "2024" - -[lib] -path = "src/lib.rs" - -[features] -default = ["util"] -expr = [] -fetchers = [] -flakes = [] -store = [] -util = [] -gc = [] - -[dependencies] -libc = "0.2.183" -stdext = "0.3.3" -nixide-sys = { path = "../nixide-sys", version = "0.1.0" } - -[dev-dependencies] -serial_test = "3.4.0" diff --git a/nixide/src/errors/context.rs b/nixide/src/errors/context.rs deleted file mode 100644 index 87d4b8e..0000000 --- a/nixide/src/errors/context.rs +++ /dev/null @@ -1,357 +0,0 @@ -// XXX: TODO: create wrappers methods to access more than just `info->msg()` -// struct ErrorInfo -// { -// Verbosity level; -// HintFmt msg; -// std::shared_ptr pos; -// std::list traces; -// /** -// * Some messages are generated directly by expressions; notably `builtins.warn`, `abort`, `throw`. -// * These may be rendered differently, so that users can distinguish them. -// */ -// bool isFromExpr = false; - -// /** -// * Exit status. -// */ -// unsigned int status = 1; - -// Suggestions suggestions; - -// static std::optional programName; -// }; - -use std::ffi::c_uint; -use std::ffi::c_void; -use std::ptr::NonNull; - -use super::{NixError, NixideResult}; -use crate::stdext::CCharPtrExt as _; -use crate::sys; -use crate::util::panic_issue_call_failed; -use crate::util::wrap; -use crate::util::wrappers::AsInnerPtr; - -/// This object stores error state. -/// -/// Passed as a first parameter to functions that can fail, to store error -/// information. -/// -/// # Warning -/// -/// These can be reused between different function calls, -/// but make sure not to use them for multiple calls simultaneously -/// (which can happen in callbacks). -/// -/// # Nix C++ API Internals -/// -/// ```cpp -/// struct nix_c_context -/// { -/// nix_err last_err_code = NIX_OK; -/// /* WARNING: The last error message. Always check last_err_code. -/// WARNING: This may not have been cleared, so that clearing is fast. */ -/// std::optional last_err = {}; -/// std::optional info = {}; -/// std::string name = ""; -/// }; -/// ``` -/// -/// The [sys::nix_c_context] struct is laid out so that it can also be -/// cast to a [sys::nix_err] to inspect directly: -/// ```c -/// assert(*(nix_err*)ctx == NIX_OK); -/// ``` -pub(crate) struct ErrorContext { - // XXX: TODO: add a RwLock to this (maybe Arc? or is that excessive?) - inner: NonNull, -} - -impl AsInnerPtr for ErrorContext { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::nix_c_context { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::nix_c_context { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::nix_c_context { - unsafe { self.inner.as_mut() } - } -} - -impl Into> for &ErrorContext { - /// # Panics - /// - /// This function will panic in the event that `context.get_err() == Some(err) && err == sys::nix_err_NIX_OK` - /// since `nixide::ErrorContext::get_err` is expected to return `None` to indicate `sys::nix_err_NIX_OK`. - /// - /// This function will panic in the event that `value != sys::nix_err_NIX_OK` - /// but that `context.get_code() == sys::nix_err_NIX_OK` - fn into(self) -> NixideResult<()> { - let inner = match self.get_err() { - Some(err) => err, - None => return Ok(()), - }; - let msg = match self.get_msg() { - Some(msg) => msg, - None => return Ok(()), - }; - - let err = match inner { - sys::nix_err_NIX_OK => unreachable!(), - - sys::nix_err_NIX_ERR_OVERFLOW => NixError::Overflow, - sys::nix_err_NIX_ERR_KEY => NixError::KeyNotFound(None), - sys::nix_err_NIX_ERR_NIX_ERROR => NixError::ExprEval { - name: self - .get_nix_err_name() - .unwrap_or_else(|| panic_issue_call_failed!()), - - info_msg: self - .get_nix_err_info_msg() - .unwrap_or_else(|| panic_issue_call_failed!()), - }, - - sys::nix_err_NIX_ERR_UNKNOWN => NixError::Unknown, - err => NixError::Undocumented(err), - }; - - Err(new_nixide_error!(NixError, inner, err, msg)) - } -} - -impl ErrorContext { - /// Create a new error context. - /// - /// # Errors - /// - /// Returns an error if no memory can be allocated for - /// the underlying [sys::nix_c_context] struct. - pub fn new() -> Self { - match NonNull::new(unsafe { sys::nix_c_context_create() }) { - Some(inner) => ErrorContext { inner }, - None => panic!("[nixide] CRITICAL FAILURE: Out-Of-Memory condition reached - `sys::nix_c_context_create` allocation failed!"), - } - - // Initialize required libraries - // XXX: TODO: move this to a separate init function (maybe a Nix::init() function) - // unsafe { - // NixErrorCode::from( - // sys::nix_libutil_init(ctx.inner.as_ptr()), - // "nix_libutil_init", - // )?; - // NixErrorCode::from( - // sys::nix_libstore_init(ctx.inner.as_ptr()), - // "nix_libstore_init", - // )?; - // NixErrorCode::from( - // sys::nix_libexpr_init(ctx.inner.as_ptr()), - // "nix_libexpr_init", - // )?; - // }; - } - - /// Check the error code and return an error if it's not `NIX_OK`. - pub fn peak(&self) -> NixideResult<()> { - self.into() - } - - /// - /// Equivalent to running `self.peak()` then `self.clear()` - pub fn pop(&mut self) -> NixideResult<()> { - self.peak().and_then(|_| Ok(self.clear())) - } - - /// # Nix C++ API Internals - /// - /// ```cpp - /// void nix_clear_err(nix_c_context * context) - /// { - /// if (context) - /// context->last_err_code = NIX_OK; - /// } - /// ``` - /// - /// `nix_clear_err` only modifies the `last_err_code`, it does not - /// clear all attributes of a `nix_c_context` struct. Hence all uses - /// of `nix_c_context` must be careful to check the `last_err_code` regularly. - pub fn clear(&mut self) { - unsafe { - // NOTE: previous nixops4 used the line: (maybe for compatability with old versions?) - // sys::nix_set_err_msg(self.inner.as_ptr(), sys::nix_err_NIX_OK, c"".as_ptr()); - sys::nix_clear_err(self.as_ptr()); - } - } - - /// Returns [None] if [self.code] is [sys::nix_err_NIX_OK], and [Some] otherwise. - /// - /// # Nix C++ API Internals - /// ```cpp - /// nix_err nix_err_code(const nix_c_context * read_context) - /// { - /// return read_context->last_err_code; - /// } - /// ``` - /// This function **never fails**. - pub(super) fn get_err(&self) -> Option { - let err = unsafe { sys::nix_err_code(self.as_ptr()) }; - - match err { - sys::nix_err_NIX_OK => None, - _ => Some(err), - } - } - - /// Returns [None] if [self.code] is [sys::nix_err_NIX_OK], and [Some] otherwise. - /// - /// # Nix C++ API Internals - /// ```cpp - /// const char * nix_err_msg(nix_c_context * context, const nix_c_context * read_context, unsigned int * n) - /// { - /// if (context) - /// context->last_err_code = NIX_OK; - /// if (read_context->last_err && read_context->last_err_code != NIX_OK) { - /// if (n) - /// *n = read_context->last_err->size(); - /// return read_context->last_err->c_str(); - /// } - /// nix_set_err_msg(context, NIX_ERR_UNKNOWN, "No error message"); - /// return nullptr; - /// } - /// ``` - /// - /// # Note - /// On failure [sys::nix_err_name] does the following if the error - /// has the error code [sys::nix_err_NIX_OK]: - /// ```cpp - /// nix_set_err_msg(context, NIX_ERR_UNKNOWN, "No error message"); - /// return nullptr; - /// ``` - /// Hence we can just test whether the returned pointer is a `NULL` pointer, - /// and avoid passing in a [sys::nix_c_context] struct. - pub(super) fn get_msg(&self) -> Option { - let ctx = ErrorContext::new(); - unsafe { - // NOTE: an Err here only occurs when `self.get_code() == Ok(())` - let mut n: c_uint = 0; - sys::nix_err_msg(ctx.as_ptr(), self.as_ptr(), &mut n) - .to_utf8_string_n(n as usize) - .ok() - } - } - - /// Returns [None] if [self.code] is [sys::nix_err_NIX_OK], and [Some] otherwise. - /// - /// # Nix C++ API Internals - /// - /// ```cpp - /// // NOTE(nixide): the implementation of `nix_err_info_msg` is identical to `nix_err_name` - /// nix_err nix_err_info_msg( - /// nix_c_context * context, - /// const nix_c_context * read_context, - /// nix_get_string_callback callback, - /// void * user_data) - /// { - /// if (context) - /// context->last_err_code = NIX_OK; - /// if (read_context->last_err_code != NIX_ERR_NIX_ERROR) { - /// // NOTE(nixide): `nix_set_err_msg` throws a `nix::Error` exception if `context == nullptr` - /// return nix_set_err_msg(context, NIX_ERR_UNKNOWN, "Last error was not a nix error"); - /// } - /// // NOTE(nixide): `call_nix_get_string_callback` always returns `NIX_OK` - /// return call_nix_get_string_callback(read_context->info->msg.str(), callback, user_data); - /// } - /// ``` - /// - /// `nix_err_info_msg` accepts two `nix_c_context*`: - /// * `nix_c_context* context` - errors from the function call are logged here - /// * `const nix_c_context* read_context` - the context to read `info_msg` from - /// - /// `nix_set_err_msg` will cause undefined behaviour if `context` is a null pointer (see below) - /// due to [https://github.com/rust-lang/rust-bindgen/issues/1208]. - /// So we should never assigned it [std::ptr::null_mut]. - /// ```cpp - /// if (context == nullptr) { - /// throw nix::Error("Nix C api error: %s", msg); - /// } - /// ``` - pub(super) fn get_nix_err_name(&self) -> Option { - #[allow(unused_unsafe)] // XXX: TODO: remove this `unused_unsafe` - unsafe { - // NOTE: an Err here only occurs when "Last error was not a nix error" - wrap::nix_string_callback!(|callback, userdata: *mut __UserData, ctx: &ErrorContext| { - sys::nix_err_name( - ctx.as_ptr(), - self.as_ptr(), - Some(callback), - userdata as *mut c_void, - ) - }) - .ok() - } - } - - /// Returns [None] if [self.code] is [sys::nix_err_NIX_OK], and [Some] otherwise. - /// - /// # Nix C++ API Internals - /// - /// ```cpp - /// // NOTE(nixide): the implementation of `nix_err_info_msg` is identical to `nix_err_name` - /// nix_err nix_err_info_msg( - /// nix_c_context * context, - /// const nix_c_context * read_context, - /// nix_get_string_callback callback, - /// void * user_data) - /// { - /// if (context) - /// context->last_err_code = NIX_OK; - /// if (read_context->last_err_code != NIX_ERR_NIX_ERROR) { - /// // NOTE(nixide): `nix_set_err_msg` throws a `nix::Error` exception if `context == nullptr` - /// return nix_set_err_msg(context, NIX_ERR_UNKNOWN, "Last error was not a nix error"); - /// } - /// // NOTE(nixide): `call_nix_get_string_callback` always returns `NIX_OK` - /// return call_nix_get_string_callback(read_context->info->msg.str(), callback, user_data); - /// } - /// ``` - /// - /// `nix_err_info_msg` accepts two `nix_c_context*`: - /// * `nix_c_context* context` - errors from the function call are logged here - /// * `const nix_c_context* read_context` - the context to read `info_msg` from - /// - /// `nix_set_err_msg` will cause undefined behaviour if `context` is a null pointer (see below) - /// due to [https://github.com/rust-lang/rust-bindgen/issues/1208]. - /// So we should never assigned it [std::ptr::null_mut]. - /// ```cpp - /// if (context == nullptr) { - /// throw nix::Error("Nix C api error: %s", msg); - /// } - /// ``` - pub(super) fn get_nix_err_info_msg(&self) -> Option { - #[allow(unused_unsafe)] // XXX: TODO: remove this `unused_unsafe` - unsafe { - // NOTE: an Err here only occurs when "Last error was not a nix error" - wrap::nix_string_callback!(|callback, userdata, ctx: &ErrorContext| { - sys::nix_err_info_msg( - ctx.as_ptr(), - self.as_ptr(), - Some(callback), - userdata as *mut c_void, - ) - }) - .ok() - } - } -} - -impl Drop for ErrorContext { - fn drop(&mut self) { - unsafe { - sys::nix_c_context_free(self.inner.as_ptr()); - } - } -} diff --git a/nixide/src/errors/error.rs b/nixide/src/errors/error.rs deleted file mode 100644 index 29696f1..0000000 --- a/nixide/src/errors/error.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::fmt::{Display, Formatter, Result as FmtResult}; - -use super::NixError; -use crate::sys; - -pub type NixideResult = Result; - -#[derive(Debug, Clone)] -pub enum NixideError { - /// # Warning - /// [NixideErrorVariant::NixError] is **not the same** as [sys::nix_err_NIX_ERR_NIX_ERROR], - /// that is instead mapped to [NixError::ExprEval] - NixError { - trace: String, - inner: sys::nix_err, - err: NixError, - msg: String, - }, - - /// Returned if a C string `*const c_char` contained a `\0` byte prematurely. - StringNulByte { trace: String }, - - /// Returned if a C string is not encoded in UTF-8. - StringNotUtf8 { trace: String }, - - /// Equivalent to the standard [std::ffi::NulError] type. - NullPtr { trace: String }, - - /// Invalid Argument - InvalidArg { - trace: String, - name: &'static str, - reason: String, - }, - - /// Invalid Type - InvalidType { - trace: String, - expected: &'static str, - got: String, - }, -} - -macro_rules! new_nixide_error { - (NixError, $inner:expr, $err:expr, $msg:expr) => {{ - crate::NixideError::NixError { - trace: ::stdext::debug_name!(), - inner: $inner, - err: $err, - msg: $msg, - } - }}; - (StringNulByte) => {{ - crate::NixideError::StringNulByte { - trace: ::stdext::debug_name!(), - } - }}; - (StringNotUtf8) => {{ - crate::NixideError::StringNotUtf8 { - trace: ::stdext::debug_name!(), - } - }}; - (NullPtr) => {{ - crate::NixideError::NullPtr { - trace: ::stdext::debug_name!(), - } - }}; - (InvalidArg, $name:expr, $reason:expr) => {{ - crate::NixideError::InvalidArg { - trace: ::stdext::debug_name!(), - name: $name, - reason: $reason, - } - }}; - (InvalidType, $expected:expr, $got:expr) => {{ - crate::NixideError::InvalidType { - trace: ::stdext::debug_name!(), - expected: $expected, - got: $got, - } - }}; -} -pub(crate) use new_nixide_error; - -#[allow(unused_macros)] -macro_rules! retrace_nixide_error { - ($x:expr) => {{ - crate::errors::new_nixide_error!($x.err) - }}; -} -pub(crate) use retrace_nixide_error; - -impl std::error::Error for NixideError {} - -impl Display for NixideError { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match self { - NixideError::NixError { - trace, - inner, - err, - msg, - } => { - write!(f, "[nixide ~ {trace}]{err} (nix_err={inner}): {msg}") - } - - NixideError::StringNulByte { trace } => { - write!(f, "[nixide ~ {trace}] Got premature `\\0` (NUL) byte") - } - - NixideError::StringNotUtf8 { trace } => { - write!(f, "[nixide ~ {trace}] Expected UTF-8 encoded string") - } - - NixideError::NullPtr { trace } => write!(f, "[nixide ~ {trace}] Got null pointer"), - - NixideError::InvalidArg { - trace, - name, - reason, - } => { - write!( - f, - "[nixide ~ {trace}] Invalid argument `{name}`: reason \"{reason}\"" - ) - } - - NixideError::InvalidType { - trace, - expected, - got, - } => write!( - f, - "[nixide ~ {trace}] Got invalid type: expected `{expected}` but got `{got}`" - ), - } - } -} - -// pub trait AsErr { -// fn as_err(self) -> Result<(), T>; -// } - -// impl AsErr for Option { -// fn as_err(self) -> Result<(), NixideError> { -// match self { -// Some(err) => Err(err), -// None => Ok(()), -// } -// } -// } diff --git a/nixide/src/errors/mod.rs b/nixide/src/errors/mod.rs deleted file mode 100644 index 4ab5ca5..0000000 --- a/nixide/src/errors/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[macro_use] -mod error; -mod context; -mod nix_error; - -pub(crate) use context::ErrorContext; -pub(crate) use error::{new_nixide_error, retrace_nixide_error}; -pub use error::{NixideError, NixideResult}; -pub use nix_error::NixError; diff --git a/nixide/src/errors/nix_error.rs b/nixide/src/errors/nix_error.rs deleted file mode 100644 index 5e3938e..0000000 --- a/nixide/src/errors/nix_error.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::fmt::{Display, Formatter, Result as FmtResult}; - -use crate::sys; - -/// Standard (nix_err) and some additional error codes -/// produced by the libnix C API. -#[derive(Debug, Clone)] -pub enum NixError { - /// A generic Nix error occurred. - /// - /// # Reason - /// - /// This error code is returned when a generic Nix error occurs - /// during nixexpr evaluation. - /// - /// # Nix C++ API Internals - /// - /// ```cpp - /// // `NIX_ERR_NIX_ERROR` variant of the `nix_err` enum type - /// NIX_ERR_NIX_ERROR = -4 - /// ``` - ExprEval { name: String, info_msg: String }, - - /// A key/index access error occurred in C API functions. - /// - /// # Reason - /// - /// This error code is returned when accessing a key, index, or identifier that - /// does not exist in C API functions. Common scenarios include: - /// - Setting keys that don't exist (nix_setting_get, nix_setting_set) - /// - List indices that are out of bounds (nix_get_list_byidx*) - /// - Attribute names that don't exist (nix_get_attr_byname*) - /// - Attribute indices that are out of bounds (nix_get_attr_byidx*, nix_get_attr_name_byidx) - /// - /// This error typically indicates incorrect usage or assumptions about data structure - /// contents, rather than internal Nix evaluation errors. - /// - /// # Note - /// - /// This error code should ONLY be returned by C API functions themselves, - /// not by underlying Nix evaluation. For example, evaluating `{}.foo` in Nix - /// will throw a normal error (NIX_ERR_NIX_ERROR), not NIX_ERR_KEY. - /// - /// # Nix C++ API Internals - /// - /// ```cpp - /// // `NIX_ERR_KEY` variant of the `nix_err` enum type - /// NIX_ERR_KEY = -3 - /// ``` - KeyNotFound(Option), - - /// An overflow error occurred. - /// - /// # Reason - /// - /// This error code is returned when an overflow error occurred during the - /// function execution. - /// - /// # Nix C++ API Internals - /// - /// ```cpp - /// // `NIX_ERR_OVERFLOW` variant of the `nix_err` enum type - /// NIX_ERR_OVERFLOW = -2 - /// ``` - Overflow, - - /// An unknown error occurred. - /// - /// # Reason - /// - /// This error code is returned when an unknown error occurred during the - /// function execution. - /// - /// # Nix C++ API Internals - /// - /// ```cpp - /// // `NIX_ERR_OVERFLOW` variant of the `nix_err` enum type - /// NIX_ERR_UNKNOWN = -1 - /// ``` - Unknown, - - /// - /// An undocumented error occurred. - /// - /// # Reason - /// - /// The libnix C API defines `enum nix_err` as a signed integer value. - /// In the (unexpected) event libnix returns an error code with an - /// invalid enum value, or one I new addition I didn't know existed, - /// then an [NixError::Undocumented] is considered to have occurred. - /// - /// # Nix C++ API Internals - /// - /// [NixError::Undocumented] has no equivalent in the `libnix` api. - /// This is solely a language difference between C++ and Rust, since - /// [sys::nix_err] is defined over the *"continuous" (not realy)* - /// type [std::os::raw::c_int]. - Undocumented(sys::nix_err), -} - -impl Display for NixError { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match self { - NixError::ExprEval { name, info_msg } => write!(f, "[libnix] NixExpr evaluation failed [name=\"{name}\", info_msg=\"{info_msg}\"]"), - NixError::KeyNotFound(Some(key)) => write!(f, "[libnix] Key not found \"{key}\""), - NixError::KeyNotFound(None) => write!(f, "[libnix] Key not found"), - NixError::Overflow => write!(f, "[libnix] Overflow error"), - NixError::Unknown => write!(f, "[libnix] Unknown error"), - NixError::Undocumented(err) => write!( - f, - "[libnix] An undocumented nix_err({err}) [please open an issue on https://github.com/cry128/nixide]" - ), - } - } -} diff --git a/nixide/src/expr/evalstate.rs b/nixide/src/expr/evalstate.rs deleted file mode 100644 index 304409a..0000000 --- a/nixide/src/expr/evalstate.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::ffi::CString; -use std::ptr::NonNull; -use std::sync::Arc; - -use crate::errors::new_nixide_error; - -use super::Value; -use crate::errors::ErrorContext; -use crate::sys; -use crate::util::wrap; -use crate::util::wrappers::AsInnerPtr; -use crate::{NixideResult, Store}; - -/// Nix evaluation state for evaluating expressions. -/// -/// This provides the main interface for evaluating Nix expressions -/// and creating values. -pub struct EvalState { - inner: NonNull, - - // XXX: TODO: is an `Arc` necessary or just a `Store` - store: Arc, -} - -impl AsInnerPtr for EvalState { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::EvalState { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::EvalState { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::EvalState { - unsafe { self.inner.as_mut() } - } -} - -impl EvalState { - /// Construct a new EvalState directly from its attributes - pub(super) fn new(inner: NonNull, store: Arc) -> Self { - Self { inner, store } - } - - #[inline] - pub(crate) unsafe fn store_ref(&self) -> &Store { - self.store.as_ref() - } - - /// 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) -> NixideResult { - let expr_c = CString::new(expr).or(Err(new_nixide_error!(StringNulByte)))?; - let path_c = CString::new(path).or(Err(new_nixide_error!(StringNulByte)))?; - - // Allocate value for result - let value = self.new_value()?; - - // Evaluate expression - wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_expr_eval_from_string( - ctx.as_ptr(), - self.as_ptr(), - expr_c.as_ptr(), - path_c.as_ptr(), - value.as_ptr(), - ); - value - }) - } - - /// Allocate a new value. - /// - /// # Errors - /// - /// Returns an error if value allocation fails. - pub(self) fn new_value(&self) -> NixideResult { - // XXX: TODO: should this function be `Value::new` instead? - let inner = wrap::nix_ptr_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_alloc_value(ctx.as_ptr(), self.as_ptr()) - })?; - - Ok(Value::new(inner, self)) - } -} - -impl Drop for EvalState { - fn drop(&mut self) { - // SAFETY: We own the state and it's valid until drop - unsafe { - sys::nix_state_free(self.inner.as_ptr()); - } - } -} - -// SAFETY: EvalState can be shared between threads -unsafe impl Send for EvalState {} -unsafe impl Sync for EvalState {} diff --git a/nixide/src/expr/evalstatebuilder.rs b/nixide/src/expr/evalstatebuilder.rs deleted file mode 100644 index 82908dc..0000000 --- a/nixide/src/expr/evalstatebuilder.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::ptr::NonNull; -use std::sync::Arc; - -use super::EvalState; -use crate::errors::{ErrorContext, NixideResult}; -use crate::sys; -use crate::util::wrap; -use crate::util::wrappers::AsInnerPtr; -use crate::Store; - -/// Builder for Nix evaluation state. -/// -/// This allows configuring the evaluation environment before creating -/// the evaluation state. -pub struct EvalStateBuilder { - inner: NonNull, - store: Arc, -} - -impl AsInnerPtr for EvalStateBuilder { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::nix_eval_state_builder { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::nix_eval_state_builder { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::nix_eval_state_builder { - unsafe { self.inner.as_mut() } - } -} - -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) -> NixideResult { - let inner = wrap::nix_ptr_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_eval_state_builder_new(ctx.as_ptr(), store.as_ptr()) - })?; - - Ok(EvalStateBuilder { - inner, - store: Arc::clone(store), - }) - } - - /// Build the evaluation state. - /// - /// # Errors - /// - /// Returns an error if the evaluation state cannot be built. - pub fn build(self) -> NixideResult { - // Load configuration first - wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_eval_state_builder_load(ctx.as_ptr(), self.as_ptr()) - })?; - - // Build the state - let inner = wrap::nix_ptr_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_eval_state_build(ctx.as_ptr(), self.as_ptr()) - })?; - - Ok(EvalState::new(inner, self.store.clone())) - } -} - -impl Drop for EvalStateBuilder { - fn drop(&mut self) { - // SAFETY: We own the builder and it's valid until drop - unsafe { - sys::nix_eval_state_builder_free(self.inner.as_ptr()); - } - } -} diff --git a/nixide/src/expr/mod.rs b/nixide/src/expr/mod.rs deleted file mode 100644 index 8487311..0000000 --- a/nixide/src/expr/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[cfg(test)] -mod tests; - -mod evalstate; -mod evalstatebuilder; -mod realised_string; -mod value; -mod values; -mod valuetype; - -pub use evalstate::EvalState; -pub use evalstatebuilder::EvalStateBuilder; -pub use realised_string::RealisedString; -pub use value::Value; -pub use values::{NixInt, NixValue}; -pub use valuetype::ValueType; diff --git a/nixide/src/expr/realised_string.rs b/nixide/src/expr/realised_string.rs deleted file mode 100644 index 5be5f8b..0000000 --- a/nixide/src/expr/realised_string.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::ffi::c_char; -use std::ptr::NonNull; -use std::sync::Arc; - -use crate::errors::ErrorContext; -use crate::expr::values::NixString; -use crate::stdext::CCharPtrExt; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::util::LazyArray; -use crate::util::{panic_issue_call_failed, wrap}; -use crate::{EvalState, NixideResult, StorePath}; - -pub struct RealisedString { - inner: NonNull, - // pub path: LazyCell StorePath>>, - pub path: StorePath, - pub children: LazyArray StorePath>, usize) -> StorePath>>, -} - -impl AsInnerPtr for RealisedString { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::nix_realised_string { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::nix_realised_string { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::nix_realised_string { - unsafe { self.inner.as_mut() } - } -} - -impl Drop for RealisedString { - fn drop(&mut self) { - unsafe { - sys::nix_realised_string_free(self.as_ptr()); - } - } -} - -impl RealisedString { - /// Realise a string context. - /// - /// This will - /// - realise the store paths referenced by the string's context, and - /// - perform the replacement of placeholders. - /// - create temporary garbage collection roots for the store paths, for - /// the lifetime of the current process. - /// - log to stderr - /// - /// # Arguments - /// - /// * value - Nix value, which must be a string - /// * state - Nix evaluator state - /// * isIFD - If true, disallow derivation outputs if setting `allow-import-from-derivation` is false. - /// You should set this to true when this call is part of a primop. - /// You should set this to false when building for your application's purpose. - /// - /// # Returns - /// - /// NULL if failed, or a new nix_realised_string, which must be freed with nix_realised_string_free - pub fn new(value: &NixString, state: &Arc) -> NixideResult { - let inner = wrap::nix_ptr_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_string_realise( - ctx.as_ptr(), - state.as_ptr(), - value.as_ptr(), - false, // don't copy more - ) - })?; - - fn delegate( - inner: &LazyArray StorePath>>, - index: usize, - ) -> StorePath { - // XXX: TODO - // inner[index] - StorePath::fake_path(unsafe { state.store_ref() }).unwrap() - } - - let size = unsafe { sys::nix_realised_string_get_store_path_count(inner.as_ptr()) }; - - Ok(Self { - inner, - path: Self::parse_path(inner.as_ptr(), state), - // children: LazyArray::new(size, delegate as fn(usize) -> StorePath), - children: LazyArray:: StorePath>>::new( - size, - Box::new(delegate), - ), - }) - } - - fn parse_path( - realised_string: *mut sys::nix_realised_string, - state: &Arc, - ) -> StorePath { - let buffer_ptr = unsafe { sys::nix_realised_string_get_buffer_start(realised_string) }; - let buffer_size = unsafe { sys::nix_realised_string_get_buffer_size(realised_string) }; - - let path_str = (buffer_ptr as *const c_char) - .to_utf8_string_n(buffer_size) - .unwrap_or_else(|err| { - panic_issue_call_failed!( - "`sys::nix_realised_string_get_buffer_(start|size)` invalid UTF-8 ({})", - err - ) - }); - StorePath::parse(unsafe { state.store_ref() }, &path_str).unwrap_or_else(|err| { - panic_issue_call_failed!( - "`sys::nix_realised_string_get_buffer_(start|size)` invalid store path ({})", - err - ) - }) - } -} diff --git a/nixide/src/expr/tests.rs b/nixide/src/expr/tests.rs deleted file mode 100644 index 702b44d..0000000 --- a/nixide/src/expr/tests.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::sync::Arc; - -use serial_test::serial; - -use super::{EvalStateBuilder, ValueType}; -use crate::Store; - -#[test] -#[serial] -fn test_eval_state_builder() { - let store = Arc::new(Store::open(None).expect("Failed to open store")); - let _state = EvalStateBuilder::new(&store) - .expect("Failed to create builder") - .build() - .expect("Failed to build state"); - // State should be dropped automatically -} - -#[test] -#[serial] -fn test_simple_evaluation() { - let store = Arc::new(Store::open(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); -} - -#[test] -#[serial] -fn test_value_types() { - let store = Arc::new(Store::open(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"); -} - -#[test] -#[serial] -fn test_value_formatting() { - let store = Arc::new(Store::open(None).expect("Failed to open store")); - let state = EvalStateBuilder::new(&store) - .expect("Failed to create builder") - .build() - .expect("Failed to build state"); - - // Test integer formatting - let int_val = state - .eval_from_string("42", "") - .expect("Failed to evaluate int"); - assert_eq!(format!("{int_val}"), "42"); - assert_eq!(format!("{int_val:?}"), "Value::Int(42)"); - assert_eq!(int_val.to_nix_string().expect("Failed to format"), "42"); - - // Test boolean formatting - let bool_val = state - .eval_from_string("true", "") - .expect("Failed to evaluate bool"); - assert_eq!(format!("{bool_val}"), "true"); - assert_eq!(format!("{bool_val:?}"), "Value::Bool(true)"); - assert_eq!(bool_val.to_nix_string().expect("Failed to format"), "true"); - - let false_val = state - .eval_from_string("false", "") - .expect("Failed to evaluate bool"); - assert_eq!(format!("{false_val}"), "false"); - assert_eq!( - false_val.to_nix_string().expect("Failed to format"), - "false" - ); - - // Test string formatting - let str_val = state - .eval_from_string("\"hello world\"", "") - .expect("Failed to evaluate string"); - assert_eq!(format!("{str_val}"), "hello world"); - assert_eq!(format!("{str_val:?}"), "Value::String(\"hello world\")"); - assert_eq!( - str_val.to_nix_string().expect("Failed to format"), - "\"hello world\"" - ); - - // Test string with quotes - let quoted_str = state - .eval_from_string("\"say \\\"hello\\\"\"", "") - .expect("Failed to evaluate quoted string"); - assert_eq!(format!("{quoted_str}"), "say \"hello\""); - assert_eq!( - quoted_str.to_nix_string().expect("Failed to format"), - "\"say \\\"hello\\\"\"" - ); - - // Test null formatting - let null_val = state - .eval_from_string("null", "") - .expect("Failed to evaluate null"); - assert_eq!(format!("{null_val}"), "null"); - assert_eq!(format!("{null_val:?}"), "Value::Null"); - assert_eq!(null_val.to_nix_string().expect("Failed to format"), "null"); - - // Test collection formatting - let attrs_val = state - .eval_from_string("{ a = 1; }", "") - .expect("Failed to evaluate attrs"); - assert_eq!(format!("{attrs_val}"), "{ }"); - assert_eq!(format!("{attrs_val:?}"), "Value::Attrs({ })"); - - let list_val = state - .eval_from_string("[ 1 2 3 ]", "") - .expect("Failed to evaluate list"); - assert_eq!(format!("{list_val}"), "[ ]"); - assert_eq!(format!("{list_val:?}"), "Value::List([ ])"); -} diff --git a/nixide/src/expr/value.rs b/nixide/src/expr/value.rs deleted file mode 100644 index ce7237c..0000000 --- a/nixide/src/expr/value.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; -use std::ptr::NonNull; - -use super::{EvalState, ValueType}; -use crate::errors::{ErrorContext, NixideResult}; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::util::{panic_issue_call_failed, wrap}; - -/// A Nix value -/// -/// This represents any value in the Nix language, including primitives, -/// collections, and functions. -pub struct Value<'a> { - pub(crate) inner: NonNull, - state: &'a EvalState, -} - -impl<'a> AsInnerPtr for Value<'a> { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::nix_value { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::nix_value { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::nix_value { - unsafe { self.inner.as_mut() } - } -} - -impl<'a> Value<'a> { - pub(crate) fn new(inner: NonNull, state: &'a EvalState) -> Self { - Value { inner, state } - } - - /// 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) -> NixideResult<()> { - // XXX: TODO: move force and force_deep to the EvalState - wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_value_force(ctx.as_ptr(), self.state.as_ptr(), self.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) -> NixideResult<()> { - // XXX: TODO: move force and force_deep to the EvalState - wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_value_force_deep(ctx.as_ptr(), self.state.as_ptr(), self.as_ptr()); - }) - } - - /// Get the type of this value. - #[must_use] - pub fn value_type(&self) -> ValueType { - // NOTE: an error here only occurs if `nix_get_type` catches an error, - // NOTE: which in turn only happens if the `sys::nix_value*` is a null pointer - // NOTE: or points to an uninitialised `nix_value` struct. - ValueType::from({ - wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_get_type(ctx.as_ptr(), self.as_ptr()) - }) - .unwrap_or_else(|err| panic_issue_call_failed!("{}", err)) - }) - } - - // XXX: TODO: rewrite `expr/value.rs` to make this redundant - // fn expect_type(&self, expected: ValueType) -> NixideResult<()> { - // let got = self.value_type(); - // if got != expected { - // return Err(new_nixide_error!( - // InvalidType, - // expected.to_string(), - // got.to_string() - // )); - // } - // Ok(()) - // } - - /// 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) -> NixideResult { - 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("".to_string()), - ValueType::Thunk => Ok("".to_string()), - ValueType::External => Ok("".to_string()), - } - } -} - -impl Drop for Value { - fn drop(&mut self) { - let ctx = ErrorContext::new(); - unsafe { - sys::nix_value_decref(ctx.as_ptr(), self.as_ptr()); - } - } -} - -impl Display for Value { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match self.value_type() { - ValueType::Int => { - if let Ok(val) = self.as_int() { - write!(f, "{val}") - } else { - write!(f, "") - } - } - 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 => write!(f, ""), - ValueType::Thunk => write!(f, ""), - ValueType::External => write!(f, ""), - } - } -} - -impl Debug for Value { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - let value_type = self.value_type(); - match value_type { - ValueType::Int => { - if let Ok(val) = self.as_int() { - write!(f, "Value::Int({val})") - } else { - write!(f, "Value::Int()") - } - } - 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 => write!(f, "Value::Path()"), - ValueType::Thunk => write!(f, "Value::Thunk()"), - ValueType::External => write!(f, "Value::External()"), - } - } -} diff --git a/nixide/src/expr/values/attrs.rs b/nixide/src/expr/values/attrs.rs deleted file mode 100644 index e69de29..0000000 diff --git a/nixide/src/expr/values/bool.rs b/nixide/src/expr/values/bool.rs deleted file mode 100644 index fd5598b..0000000 --- a/nixide/src/expr/values/bool.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; -use std::ptr::NonNull; - -use super::NixValue; -use crate::errors::ErrorContext; -use crate::sys; -use crate::util::panic_issue_call_failed; -use crate::util::wrap; -use crate::util::wrappers::AsInnerPtr; - -pub struct NixBool { - inner: NonNull, - value: bool, -} - -impl Display for NixBool { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "") - } -} - -impl Debug for NixBool { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "NixBool(${})", self.value) - } -} - -impl AsInnerPtr for NixBool { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::nix_value { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::nix_value { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::nix_value { - unsafe { self.inner.as_mut() } - } -} - -impl NixValue for NixBool { - #[inline] - fn get_enum_id(&self) -> sys::ValueType { - sys::ValueType_NIX_TYPE_BOOL - } - - fn new(inner: NonNull) -> Self { - let value = wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_get_bool(ctx.as_ptr(), inner.as_ptr()) - }) - .unwrap_or_else(|err| { - panic_issue_call_failed!("`sys::nix_get_bool` failed for valid `NixBool` ({})", err) - }); - - Self { inner, value } - } -} - -impl NixBool { - /// Returns a shared reference to the underlying value. - /// - #[inline] - fn value(&self) -> &bool { - &self.value - } -} diff --git a/nixide/src/expr/values/external.rs b/nixide/src/expr/values/external.rs deleted file mode 100644 index e69de29..0000000 diff --git a/nixide/src/expr/values/float.rs b/nixide/src/expr/values/float.rs deleted file mode 100644 index 84c5539..0000000 --- a/nixide/src/expr/values/float.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; -use std::ptr::NonNull; - -use super::NixValue; -use crate::errors::ErrorContext; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::util::{panic_issue_call_failed, wrap}; - -pub struct NixFloat { - inner: NonNull, - value: f64, -} - -impl Display for NixFloat { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "") - } -} - -impl Debug for NixFloat { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "NixFloat(${})", self.value()) - } -} - -impl AsInnerPtr for NixFloat { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::nix_value { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::nix_value { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::nix_value { - unsafe { self.inner.as_mut() } - } -} - -impl NixValue for NixFloat { - #[inline] - fn get_enum_id(&self) -> sys::ValueType { - sys::ValueType_NIX_TYPE_FLOAT - } - - fn new(inner: NonNull) -> Self { - let value = wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_get_float(ctx.as_ptr(), inner.as_ptr()) - }) - .unwrap_or_else(|err| { - panic_issue_call_failed!("`sys::nix_get_float` failed for valid `NixFloat` ({})", err) - }); - - Self { inner, value } - } -} - -impl NixFloat { - /// Returns a shared reference to the underlying value. - /// - #[inline] - fn value(&self) -> &f64 { - &self.value - } -} diff --git a/nixide/src/expr/values/function.rs b/nixide/src/expr/values/function.rs deleted file mode 100644 index e69de29..0000000 diff --git a/nixide/src/expr/values/int.rs b/nixide/src/expr/values/int.rs deleted file mode 100644 index e9a6264..0000000 --- a/nixide/src/expr/values/int.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; -use std::ptr::NonNull; - -use super::NixValue; -use crate::errors::ErrorContext; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::util::{panic_issue_call_failed, wrap}; - -pub struct NixInt { - inner: NonNull, - value: i64, -} - -impl Display for NixInt { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "") - } -} - -impl Debug for NixInt { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "NixInt(${})", self.value()) - } -} - -impl AsInnerPtr for NixInt { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::nix_value { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::nix_value { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::nix_value { - unsafe { self.inner.as_mut() } - } -} - -impl NixValue for NixInt { - #[inline] - fn get_enum_id(&self) -> sys::ValueType { - sys::ValueType_NIX_TYPE_INT - } - - fn new(inner: NonNull) -> Self { - let value = wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_get_int(ctx.as_ptr(), inner.as_ptr()) - }) - .unwrap_or_else(|err| { - panic_issue_call_failed!("`sys::nix_get_int` failed for valid `NixInt` ({})", err) - }); - - Self { inner, value } - } -} - -impl NixInt { - /// Returns a shared reference to the underlying value. - /// - #[inline] - fn value(&self) -> &i64 { - &self.value - } -} diff --git a/nixide/src/expr/values/list.rs b/nixide/src/expr/values/list.rs deleted file mode 100644 index e69de29..0000000 diff --git a/nixide/src/expr/values/mod.rs b/nixide/src/expr/values/mod.rs deleted file mode 100644 index bc503d1..0000000 --- a/nixide/src/expr/values/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -mod attrs; -mod bool; -mod external; -mod float; -mod function; -mod int; -mod list; -mod null; -mod path; -mod string; -mod thunk; - -pub use bool::NixBool; -pub use float::NixFloat; -pub use int::NixInt; -pub use string::NixString; - -use std::fmt::{Debug, Display}; -use std::ptr::NonNull; - -use crate::sys; -use crate::util::wrappers::AsInnerPtr; - -pub trait NixValue: Display + Debug + AsInnerPtr { - /// TODO - fn get_enum_id(&self) -> sys::ValueType; - - fn new(inner: NonNull) -> Self; -} diff --git a/nixide/src/expr/values/null.rs b/nixide/src/expr/values/null.rs deleted file mode 100644 index e69de29..0000000 diff --git a/nixide/src/expr/values/path.rs b/nixide/src/expr/values/path.rs deleted file mode 100644 index e69de29..0000000 diff --git a/nixide/src/expr/values/string.rs b/nixide/src/expr/values/string.rs deleted file mode 100644 index 78fc272..0000000 --- a/nixide/src/expr/values/string.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::cell::LazyCell; -use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; -use std::ptr::NonNull; - -use super::NixValue; -use crate::expr::RealisedString; -use crate::util::panic_issue_call_failed; -use crate::util::wrap; -use crate::util::wrappers::AsInnerPtr; -use crate::{sys, NixideResult}; - -pub struct NixString { - inner: NonNull, - value: String, -} - -impl Display for NixString { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "") - } -} - -impl Debug for NixString { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "NixString(\"${}\")", self.value()) - } -} - -impl AsInnerPtr for NixString { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::nix_value { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::nix_value { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::nix_value { - unsafe { self.inner.as_mut() } - } -} - -impl NixValue for NixString { - #[inline] - fn get_enum_id(&self) -> sys::ValueType { - sys::ValueType_NIX_TYPE_STRING - } - - fn new(inner: NonNull) -> Self { - // wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - // sys::nix_get_int(ctx.as_ptr(), inner.as_ptr()) - // }) - // .unwrap_or_else(|err| { - // panic_issue_call_failed!( - // "`sys::nix_get_int` failed for valid `NixString` ({})", - // err - // ) - // }) - // }; - - Self { inner, value } - } -} - -impl NixString { - /// Returns a shared reference to the underlying value. - /// - #[inline] - fn value(&self) -> &String { - &self.value - } -} diff --git a/nixide/src/expr/values/thunk.rs b/nixide/src/expr/values/thunk.rs deleted file mode 100644 index e69de29..0000000 diff --git a/nixide/src/expr/valuetype.rs b/nixide/src/expr/valuetype.rs deleted file mode 100644 index ad7b194..0000000 --- a/nixide/src/expr/valuetype.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::fmt::{Display, Formatter, Result as FmtResult}; - -use crate::sys; - -/// Nix value types. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValueType { - /// Thunk (unevaluated expression). - Thunk, - /// Integer value. - Int, - /// Float value. - Float, - /// Boolean value. - Bool, - /// String value. - String, - /// Path value. - Path, - /// Null value. - Null, - /// Attribute set. - Attrs, - /// List. - List, - /// Function. - Function, - /// External value. - External, -} - -impl From for ValueType { - fn from(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, - _ => unreachable!("call to `nixide::ValueType::from_c` failed: please open an issue on https://github.com/cry128/nixide"), - } - } -} - -impl Display for ValueType { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - let name = match self { - ValueType::Thunk => "thunk", - ValueType::Int => "int", - ValueType::Float => "float", - ValueType::Bool => "bool", - ValueType::String => "string", - ValueType::Path => "path", - ValueType::Null => "null", - ValueType::Attrs => "attrs", - ValueType::List => "list", - ValueType::Function => "function", - ValueType::External => "external", - }; - write!(f, "{name}") - } -} diff --git a/nixide/src/flake/eval_state_builder_ext.rs b/nixide/src/flake/eval_state_builder_ext.rs deleted file mode 100644 index ff8598f..0000000 --- a/nixide/src/flake/eval_state_builder_ext.rs +++ /dev/null @@ -1,14 +0,0 @@ -use super::FlakeSettings; -use crate::{EvalStateBuilder, NixideError}; - -pub trait EvalStateBuilderExt { - /// Configures the eval state to provide flakes features such as `builtins.getFlake`. - fn flakes(self, settings: &FlakeSettings) -> Result; -} - -impl EvalStateBuilderExt for EvalStateBuilder { - /// Configures the eval state to provide flakes features such as `builtins.getFlake`. - fn flakes(mut self, settings: &FlakeSettings) -> Result { - settings.add_to_eval_state_builder(&mut self).map(|_| self) - } -} diff --git a/nixide/src/flake/fetchers_settings.rs b/nixide/src/flake/fetchers_settings.rs deleted file mode 100644 index ac4f5ca..0000000 --- a/nixide/src/flake/fetchers_settings.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::ptr::NonNull; - -use crate::errors::{new_nixide_error, ErrorContext}; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::NixideError; - -pub(super) struct FetchersSettings { - pub(super) ptr: NonNull, -} - -impl FetchersSettings { - pub fn new() -> Result { - let ctx = ErrorContext::new(); - let ptr = unsafe { sys::nix_fetchers_settings_new(ctx.as_ptr()) }; - Ok(FetchersSettings { - ptr: NonNull::new(ptr).ok_or(new_nixide_error!(NullPtr))?, - }) - } - - pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_fetchers_settings { - self.ptr.as_ptr() - } -} - -impl Drop for FetchersSettings { - fn drop(&mut self) { - unsafe { - sys::nix_fetchers_settings_free(self.as_ptr()); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn fetchers_settings_new() { - let _ = FetchersSettings::new().unwrap(); - } -} diff --git a/nixide/src/flake/flake_lock_flags.rs b/nixide/src/flake/flake_lock_flags.rs deleted file mode 100644 index bb066ec..0000000 --- a/nixide/src/flake/flake_lock_flags.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::ffi::CString; -use std::ptr::NonNull; - -use super::{FlakeReference, FlakeSettings}; -use crate::errors::new_nixide_error; -use crate::errors::ErrorContext; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::NixideError; - -#[derive(Debug, Clone)] -pub enum FlakeLockMode { - /// Configures [LockedFlake::lock] to make incremental changes to the lock file as needed. Changes are written to file. - WriteAsNeeded, - - /// Like [FlakeLockMode::WriteAsNeeded], but does not write to the lock file. - Virtual, - - /// Make [LockedFlake::lock] check if the lock file is up to date. If not, an error is returned. - Check, -} - -/// Parameters that affect the locking of a flake. -pub struct FlakeLockFlags { - pub(crate) inner: NonNull, -} -impl Drop for FlakeLockFlags { - fn drop(&mut self) { - unsafe { - sys::nix_flake_lock_flags_free(self.as_ptr()); - } - } -} -impl FlakeLockFlags { - // XXX: TODO: what is the default FlakeLockMode? - pub fn new(settings: &FlakeSettings) -> Result { - let ctx = ErrorContext::new(); - NonNull::new(unsafe { sys::nix_flake_lock_flags_new(ctx.as_ptr(), settings.as_ptr()) }) - .ok_or(new_nixide_error!(NullPtr)) - .map(|inner| FlakeLockFlags { inner }) - } - - pub(crate) fn as_ptr(&self) -> *mut sys::nix_flake_lock_flags { - self.inner.as_ptr() - } - - pub fn set_lock_mode(&mut self, mode: &FlakeLockMode) -> Result<(), NixideError> { - let ctx = ErrorContext::new(); - match mode { - FlakeLockMode::WriteAsNeeded => unsafe { - sys::nix_flake_lock_flags_set_mode_write_as_needed(ctx.as_ptr(), self.as_ptr()) - }, - FlakeLockMode::Virtual => unsafe { - sys::nix_flake_lock_flags_set_mode_virtual(ctx.as_ptr(), self.as_ptr()) - }, - FlakeLockMode::Check => unsafe { - sys::nix_flake_lock_flags_set_mode_check(ctx.as_ptr(), self.as_ptr()) - }, - }; - match ctx.peak() { - Some(err) => Err(err), - None => Ok(()), - } - } - - /// Configures [LockedFlake::lock] to make incremental changes to the lock file as needed. Changes are written to file. - // pub fn set_mode_write_as_needed(&mut self) -> Result<(), NixideError> { - // ErrorContext::new().and_then(|ctx| { - // NixideError::from( - // unsafe { - // sys::nix_flake_lock_flags_set_mode_write_as_needed(ctx.as_ptr(), self.as_ptr()) - // }, - // "nix_flake_lock_flags_set_mode_write_as_needed", - // ) - // }) - // } - - /// Make [LockedFlake::lock] check if the lock file is up to date. If not, an error is returned. - // pub fn set_mode_check(&mut self) -> Result<(), NixideError> { - // ErrorContext::new().and_then(|ctx| { - // NixideError::from( - // unsafe { sys::nix_flake_lock_flags_set_mode_check(ctx.as_ptr(), self.as_ptr()) }, - // "nix_flake_lock_flags_set_mode_check", - // ) - // }) - // } - - /// Like `set_mode_write_as_needed`, but does not write to the lock file. - // pub fn set_mode_virtual(&mut self) -> Result<(), NixideError> { - // ErrorContext::new().and_then(|ctx| { - // NixideError::from( - // unsafe { sys::nix_flake_lock_flags_set_mode_virtual(ctx.as_ptr(), self.as_ptr()) }, - // "nix_flake_lock_flags_set_mode_virtual", - // ) - // }) - // } - - /// Adds an input override to the lock file that will be produced. - /// The [LockedFlake::lock] operation will not write to the lock file. - /// - /// # Warning - /// - /// Calling this function will implicitly set the [FlakeLockMode] to - /// [FlakeLockMode::Virtual] if `self.mode` is not [FlakeLockMode::Check]. - /// - /// # Arguments - /// - /// * `path` - The input name/path to override (must not be empty) - /// * `flake_ref` - The flake reference to use as the override - pub fn override_input( - &mut self, - path: &str, - flakeref: &FlakeReference, - ) -> Result<(), NixideError> { - let input_path = CString::new(path).or_else(|_| Err(new_nixide_error!(StringNulByte))); - - let ctx = ErrorContext::new(); - unsafe { - sys::nix_flake_lock_flags_add_input_override( - ctx.as_ptr(), - self.as_ptr(), - input_path.as_ptr(), - flakeref.as_ptr(), - ) - }; - match ctx.peak() { - Some(err) => Err(err), - None => Ok(()), - } - } -} diff --git a/nixide/src/flake/flake_reference.rs b/nixide/src/flake/flake_reference.rs deleted file mode 100644 index c60a950..0000000 --- a/nixide/src/flake/flake_reference.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::os::raw::c_char; -use std::ptr::{null_mut, NonNull}; - -use super::{FetchersSettings, FlakeReferenceParseFlags, FlakeSettings}; -use crate::errors::new_nixide_error; -use crate::sys; -use crate::util::bindings::wrap_nix_string_callback; -use crate::util::wrappers::AsInnerPtr; -use crate::NixideError; - -pub struct FlakeReference { - pub(crate) ptr: NonNull, -} - -impl Drop for FlakeReference { - fn drop(&mut self) { - unsafe { - sys::nix_flake_reference_free(self.ptr.as_ptr()); - } - } -} - -impl FlakeReference { - pub fn as_ptr(&self) -> *mut sys::nix_flake_reference { - self.ptr.as_ptr() - } - - /// Parse a flake reference from a string. - /// The string must be a valid flake reference, such as `github:owner/repo`. - /// It may also be suffixed with a `#` and a fragment, such as `github:owner/repo#something`, - /// in which case, the returned string will contain the fragment. - pub fn parse_with_fragment( - fetch_settings: &FetchersSettings, - flake_settings: &FlakeSettings, - flags: &FlakeReferenceParseFlags, - reference: &str, - ) -> Result<(FlakeReference, String), NixideError> { - let mut ptr: *mut sys::nix_flake_reference = null_mut(); - let result = wrap_nix_string_callback(|ctx, callback, user_data| unsafe { - sys::nix_flake_reference_and_fragment_from_string( - ctx.as_ptr(), - fetch_settings.as_ptr(), - flake_settings.as_ptr(), - flags.as_ptr(), - reference.as_ptr() as *const c_char, - reference.len(), - &mut ptr, - Some(callback), - user_data, - ) - }); - - match NonNull::new(ptr) { - Some(ptr) => result.map(|s| (FlakeReference { ptr }, s)), - None => Err(new_nixide_error!(NullPtr)), - } - } -} diff --git a/nixide/src/flake/flake_reference_parse_flags.rs b/nixide/src/flake/flake_reference_parse_flags.rs deleted file mode 100644 index 39bbf78..0000000 --- a/nixide/src/flake/flake_reference_parse_flags.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::os::raw::c_char; -use std::ptr::NonNull; - -use super::FlakeSettings; -use crate::errors::{new_nixide_error, ErrorContext}; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::NixideError; - -/// Parameters for parsing a flake reference. -#[derive(Debug)] -pub struct FlakeReferenceParseFlags { - pub(crate) ptr: NonNull, -} - -impl Drop for FlakeReferenceParseFlags { - fn drop(&mut self) { - unsafe { - sys::nix_flake_reference_parse_flags_free(self.ptr.as_ptr()); - } - } -} - -impl FlakeReferenceParseFlags { - pub fn new(settings: &FlakeSettings) -> Result { - let ctx = ErrorContext::new(); - let ptr = - unsafe { sys::nix_flake_reference_parse_flags_new(ctx.as_ptr(), settings.as_ptr()) }; - match ctx.peak() { - Some(err) => Err(err), - None => NonNull::new(ptr).map_or(Err(new_nixide_error!(NullPtr)), |ptr| { - Ok(FlakeReferenceParseFlags { ptr }) - }), - } - } - - /// Sets the [base directory](https://nix.dev/manual/nix/latest/glossary#gloss-base-directory) - /// for resolving local flake references. - pub fn set_base_directory(&mut self, base_directory: &str) -> Result<(), NixideError> { - let ctx = ErrorContext::new(); - unsafe { - sys::nix_flake_reference_parse_flags_set_base_directory( - ctx.as_ptr(), - self.as_ptr(), - base_directory.as_ptr() as *const c_char, - base_directory.len(), - ) - }; - match ctx.peak() { - Some(err) => Err(err), - None => Ok(()), - } - } - - pub fn as_ptr(&self) -> *mut sys::nix_flake_reference_parse_flags { - self.ptr.as_ptr() - } -} diff --git a/nixide/src/flake/flake_settings.rs b/nixide/src/flake/flake_settings.rs deleted file mode 100644 index a16d68b..0000000 --- a/nixide/src/flake/flake_settings.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::ptr::NonNull; - -use crate::errors::{new_nixide_error, ErrorContext}; -use crate::sys; -use crate::util::wrappers::AsInnerPtr; -use crate::{EvalStateBuilder, NixideError}; - -/// Store settings for the flakes feature. -pub struct FlakeSettings { - pub(crate) inner: NonNull, -} - -impl AsInnerPtr for FlakeSettings { - unsafe fn as_ptr(&self) -> *mut sys::nix_flake_settings { - self.inner.as_ptr() - } -} - -impl FlakeSettings { - pub fn new() -> Result { - let ctx = ErrorContext::new(); - let opt = NonNull::new(unsafe { sys::nix_flake_settings_new(ctx.as_ptr()) }); - - match ctx.peak() { - Some(err) => Err(err), - None => match opt { - Some(inner) => Ok(FlakeSettings { inner }), - None => Err(new_nixide_error!(NullPtr)), - }, - } - } - - pub(super) fn add_to_eval_state_builder( - &self, - builder: &mut EvalStateBuilder, - ) -> Result<(), NixideError> { - let ctx = ErrorContext::new(); - unsafe { - sys::nix_flake_settings_add_to_eval_state_builder( - ctx.as_ptr(), - self.as_ptr(), - builder.as_ptr(), - ) - }; - match ctx.peak() { - Some(err) => Err(err), - None => Ok(()), - } - } -} - -impl Drop for FlakeSettings { - fn drop(&mut self) { - unsafe { - sys::nix_flake_settings_free(self.as_ptr()); - } - } -} diff --git a/nixide/src/flake/flakeref.rs b/nixide/src/flake/flakeref.rs deleted file mode 100644 index 7d6f719..0000000 --- a/nixide/src/flake/flakeref.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub struct FlakeRef {} - -impl FlakeRef {} diff --git a/nixide/src/flake/mod.rs b/nixide/src/flake/mod.rs deleted file mode 100644 index 51ae925..0000000 --- a/nixide/src/flake/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod eval_state_builder_ext; -mod fetchers_settings; -mod flake_lock_flags; -mod flake_reference; -mod flake_reference_parse_flags; -mod flake_settings; -mod locked_flake; - -pub(self) use eval_state_builder_ext::EvalStateBuilderExt; -pub(self) use fetchers_settings::FetchersSettings; -pub(self) use flake_lock_flags::{FlakeLockFlags, FlakeLockMode}; -pub(self) use flake_reference::FlakeReference; -pub(self) use flake_reference_parse_flags::FlakeReferenceParseFlags; -pub(self) use flake_settings::FlakeSettings; -pub(self) use locked_flake::LockedFlake; diff --git a/nixide/src/lib.rs b/nixide/src/lib.rs deleted file mode 100644 index c8f3b58..0000000 --- a/nixide/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -// #![warn(missing_docs)] - -// #[allow(unused_extern_crates)] -pub extern crate libc; -pub extern crate nixide_sys as sys; - -pub(crate) mod errors; -mod expr; -// mod flake; -mod stdext; -mod store; -pub(crate) mod util; -mod verbosity; -mod version; - -pub use errors::{NixError, NixideError, NixideResult}; -pub use expr::{EvalState, EvalStateBuilder, Value, ValueType}; -pub use store::{Store, StorePath}; -pub use verbosity::NixVerbosity; -pub use version::NixVersion; - -/// Sets the verbosity level -/// -/// # Arguments -/// -/// * `context` - additional error context, used as an output -/// * `level` - verbosity level -pub fn set_verbosity() { - // nix_err nix_set_verbosity(nix_c_context * context, nix_verbosity level); - // XXX: TODO: (implement Context first) -} diff --git a/nixide/src/stdext/cchar_ptr_ext.rs b/nixide/src/stdext/cchar_ptr_ext.rs deleted file mode 100644 index 5d506a9..0000000 --- a/nixide/src/stdext/cchar_ptr_ext.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::ffi::{c_char, CStr}; -use std::slice::from_raw_parts; -use std::str::from_utf8; - -use crate::errors::new_nixide_error; -use crate::NixideResult; - -pub trait CCharPtrExt { - fn to_utf8_string(self) -> NixideResult; - - fn to_utf8_string_n(self, n: usize) -> NixideResult; -} - -impl CCharPtrExt for *const c_char { - fn to_utf8_string(self) -> NixideResult { - if self.is_null() { - return Err(new_nixide_error!(NullPtr)); - } - let cstr = unsafe { CStr::from_ptr(self) }; - match cstr.to_str() { - Ok(s) => Ok(s.to_owned()), - Err(_) => Err(new_nixide_error!(StringNotUtf8)), - } - } - - fn to_utf8_string_n(self, n: usize) -> NixideResult { - if self.is_null() || n == 0 { - return Err(new_nixide_error!(NullPtr)); - } - let bytes = unsafe { from_raw_parts(self.cast::(), n as usize) }; - match from_utf8(bytes) { - Ok(s) => Ok(s.to_string()), - Err(_) => Err(new_nixide_error!(StringNotUtf8)), - } - } -} diff --git a/nixide/src/stdext/mod.rs b/nixide/src/stdext/mod.rs deleted file mode 100644 index fe17913..0000000 --- a/nixide/src/stdext/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod cchar_ptr_ext; - -pub(crate) use cchar_ptr_ext::CCharPtrExt; -pub(crate) use stdext::*; diff --git a/nixide/src/store/mod.rs b/nixide/src/store/mod.rs deleted file mode 100644 index 4bca2a8..0000000 --- a/nixide/src/store/mod.rs +++ /dev/null @@ -1,252 +0,0 @@ -// XXX: TODO: should I add support for `nix_libstore_init_no_load_config` -// XXX: TODO: add support for nix_realised_string_* family of functions -// nix_realised_string_get_store_path -// nix_realised_string_get_store_path_count -// # nix_store_real_path -// # nix_store_is_valid_path -// # nix_store_get_version -// # nix_store_get_uri -// # nix_store_get_storedir -// # nix_store_copy_closure -// nix_libstore_init_no_load_config - -#[cfg(test)] -mod tests; - -mod path; -pub use path::*; - -use std::ffi::{c_char, c_void, CString}; -use std::path::PathBuf; -use std::ptr::{null, null_mut, NonNull}; -use std::result::Result; - -use crate::errors::{new_nixide_error, ErrorContext}; -use crate::stdext::CCharPtrExt; -use crate::util::wrap; -use crate::util::wrappers::AsInnerPtr; -use crate::{NixideError, NixideResult}; -use nixide_sys as sys; - -/// Nix store for managing packages and derivations. -/// -/// The store provides access to Nix packages, derivations, and store paths. -pub struct Store { - pub(crate) inner: NonNull, -} - -impl AsInnerPtr for Store { - #[inline] - unsafe fn as_ptr(&self) -> *mut sys::Store { - self.inner.as_ptr() - } - - #[inline] - unsafe fn as_ref(&self) -> &sys::Store { - unsafe { self.inner.as_ref() } - } - - #[inline] - unsafe fn as_mut(&mut self) -> &mut sys::Store { - unsafe { self.inner.as_mut() } - } -} - -impl Store { - /// Open a Nix store. - /// - /// # Arguments - /// - /// * `context` - The Nix context - /// * `uri` - Optional store URI (None for default store) - /// - /// # Errors - /// - /// Returns an error if the store cannot be opened. - pub fn open(uri: Option<&str>) -> Result { - let uri_ptr = match uri.map(CString::new) { - Some(Ok(c_uri)) => c_uri.as_ptr(), - Some(Err(_)) => Err(new_nixide_error!(StringNulByte))?, - None => null(), - }; - - let inner = wrap::nix_ptr_fn!(|ctx: &ErrorContext| unsafe { - // XXX: TODO: allow args to be parsed instead of just `null_mut` - sys::nix_store_open(ctx.as_ptr(), uri_ptr, null_mut()) - })?; - - Ok(Store { inner }) - } - - /// Realize a store path. - /// - /// This builds/downloads the store path and all its dependencies, - /// making them available in the local store. - /// - /// # Arguments - /// - /// * `path` - The store path to realize - /// - /// # Returns - /// - /// A vector of (output_name, store_path) tuples for each realized output. - /// For example, a derivation might produce outputs like ("out", path1), ("dev", path2). - /// - /// # Errors - /// - /// Returns an error if the path cannot be realized. - pub fn realise( - &self, - path: &StorePath, - user_callback: fn(&str, &StorePath), - ) -> NixideResult> { - wrap::nix_callback!( - |; userdata: fn(&str, &StorePath); - output_name_ptr: *const c_char, - output_path_ptr: *const sys::StorePath| - -> Vec<(String, StorePath)> { - // XXX: TODO: test to see if this is ever null ("out" as a default feels unsafe...) - // NOTE: this also ensures `output_name_ptr` isn't null - let output_name = output_name_ptr.to_utf8_string().expect("IDK1"); - - let inner = wrap::nix_ptr_fn!(|ctx| unsafe { - sys::nix_store_path_clone(output_path_ptr as *mut sys::StorePath) - }).expect("IDK2"); - let store_path = StorePath { inner }; - - let callback = unsafe { (*userdata).inner }; - callback(output_name.as_ref(), &store_path); - - (output_name, store_path); - }, - |callback, - state: *mut __UserData, - ctx: &ErrorContext| unsafe { - // register userdata - // WARNING: Using `write` instead of assignment via `=` - // WARNING: to not call `drop` on the old, uninitialized value. - (&raw mut (*state).inner).write(user_callback); - - sys::nix_store_realise( - ctx.as_ptr(), - self.inner.as_ptr(), - path.as_ptr(), - (*state).inner_ptr() as *mut c_void, - Some(callback), - ); - } - ) - } - - /// Parse a store path string into a StorePath. - /// - /// This is a convenience method that wraps `StorePath::parse()`. - /// - /// # Arguments - /// - /// * `path` - The store path string (e.g., "/nix/store/...") - /// - /// # Errors - /// - /// Returns an error if the path cannot be parsed. - /// - /// # Example - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use nixide::Store; - /// # fn main() -> Result<(), Box> { - /// let store = Store::open(None)?; - /// let path = store.store_path("/nix/store/...")?; - /// # Ok(()) - /// # } - /// ``` - pub fn store_path(&self, path: &str) -> Result { - StorePath::parse(self, path) - } - - /// Get the version of a Nix store - /// - /// If the store doesn't have a version (like the dummy store), returns None - pub fn version(&self) -> Result { - wrap::nix_string_callback!( - |callback, userdata: *mut __UserData, ctx: &ErrorContext| unsafe { - sys::nix_store_get_version( - ctx.as_ptr(), - self.inner.as_ptr(), - Some(callback), - userdata as *mut c_void, - ) - } - ) - } - - /// Get the URI of a Nix store - pub fn uri(&self) -> Result { - wrap::nix_string_callback!( - |callback, userdata: *mut __UserData, ctx: &ErrorContext| unsafe { - sys::nix_store_get_uri( - ctx.as_ptr(), - self.inner.as_ptr(), - Some(callback), - userdata as *mut c_void, - ) - } - ) - } - - pub fn store_dir(&self) -> Result { - wrap::nix_pathbuf_callback!( - |callback, userdata: *mut __UserData, ctx: &ErrorContext| unsafe { - sys::nix_store_get_storedir( - ctx.as_ptr(), - self.inner.as_ptr(), - Some(callback), - userdata as *mut c_void, - ) - } - ) - } - - pub fn copy_closure_to( - &self, - dst_store: &Store, - store_path: &StorePath, - ) -> Result<(), NixideError> { - wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_store_copy_closure( - ctx.as_ptr(), - self.as_ptr(), - dst_store.as_ptr(), - store_path.as_ptr(), - ); // semi-colon to return () and not i32 - }) - } - - pub fn copy_closure_from( - &self, - src_store: &Store, - store_path: &StorePath, - ) -> Result<(), NixideError> { - wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_store_copy_closure( - ctx.as_ptr(), - src_store.as_ptr(), - self.as_ptr(), - store_path.inner.as_ptr(), - ); - }) // semi-colon to return () and not i32 - } -} - -impl Drop for Store { - fn drop(&mut self) { - 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 {} diff --git a/nixide/src/store/path.rs b/nixide/src/store/path.rs deleted file mode 100644 index 0c4e832..0000000 --- a/nixide/src/store/path.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::ffi::{c_void, CString}; -use std::path::PathBuf; -use std::ptr::NonNull; - -use super::Store; -use crate::errors::{new_nixide_error, ErrorContext}; -use crate::util::panic_issue_call_failed; -use crate::util::wrap; -use crate::util::wrappers::AsInnerPtr; -use crate::NixideResult; - -use nixide_sys as sys; - -/// A path in the Nix store. -/// -/// Represents a store path that can be realized, queried, or manipulated. -/// -pub struct StorePath { - pub(crate) inner: NonNull, -} - -impl AsInnerPtr for StorePath { - unsafe fn as_ptr(&self) -> *mut sys::StorePath { - self.inner.as_ptr() - } -} - -impl StorePath { - /// Parse a store path string into a StorePath. - /// - /// # Arguments - /// - /// * `store` - The store containing the path - /// * `path` - The store path string (e.g., "/nix/store/...") - /// - /// # Errors - /// - /// Returns an error if the path cannot be parsed. - pub fn parse(store: &Store, path: &str) -> NixideResult { - let c_path = CString::new(path).or(Err(new_nixide_error!(StringNulByte)))?; - - let inner = wrap::nix_ptr_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_store_parse_path(ctx.as_ptr(), store.as_ptr(), c_path.as_ptr()) - })?; - - Ok(Self { inner }) - } - - pub fn fake_path(store: &Store) -> NixideResult { - Self::parse(store, "/nix/store/00000000000000000000000000000000-fake") - } - - /// Get the name component of the store path. - /// - /// This returns the name part of the store path (everything after the hash). - /// For example, for "/nix/store/abc123...-hello-1.0", this returns "hello-1.0". - /// - /// # Errors - /// - /// Returns an error if the name cannot be retrieved. - /// - pub fn name(&self) -> NixideResult { - wrap::nix_string_callback!(|callback, userdata: *mut __UserData, _| unsafe { - sys::nix_store_path_name(self.inner.as_ptr(), Some(callback), userdata as *mut c_void); - // NOTE: nix_store_path_name doesn't return nix_err, so we force it to return successfully - // XXX: NOTE: now `nix_string_callback` is a macro this isn't necessary - // sys::nix_err_NIX_OK - }) - } - - /// Get the physical location of a store path - /// - /// A store may reside at a different location than its `storeDir` suggests. - /// This situation is called a relocated store. - /// - /// Relocated stores are used during NixOS installation, as well as in restricted - /// computing environments that don't offer a writable `/nix/store`. - /// - /// Not all types of stores support this operation. - /// - /// # Arguments - /// * `context` [in] - Optional, stores error information - /// * `store` [in] - nix store reference - /// * `path` [in] - the path to get the real path from - /// * `callback` [in] - called with the real path - /// * `user_data` [in] - arbitrary data, passed to the callback when it's called. - /// - /// # Arguments - /// - /// * `store` - The store containing the path - /// - pub fn real_path(&self, store: &Store) -> NixideResult { - wrap::nix_pathbuf_callback!( - |callback, userdata: *mut __UserData, ctx: &ErrorContext| unsafe { - sys::nix_store_real_path( - ctx.as_ptr(), - store.inner.as_ptr(), - self.as_ptr(), - Some(callback), - userdata as *mut c_void, - ) - } - ) - } - - /// Check if a [StorePath] is valid (i.e. that its corresponding store object - /// and its closure of references exists in the store). - /// - /// # Arguments - /// - /// * `store` - The store containing the path - /// - pub fn is_valid(&self, store: &Store) -> bool { - wrap::nix_fn!(|ctx: &ErrorContext| unsafe { - sys::nix_store_is_valid_path(ctx.as_ptr(), store.as_ptr(), self.as_ptr()) - }) - .is_ok() - } -} - -impl Clone for StorePath { - fn clone(&self) -> Self { - let inner = wrap::nix_ptr_fn!(|_| unsafe { sys::nix_store_path_clone(self.as_ptr()) }) - .unwrap_or_else(|_| { - panic_issue_call_failed!("nix_store_path_clone returned None for valid path") - }); - - StorePath { inner } - } -} - -impl Drop for StorePath { - fn drop(&mut self) { - unsafe { - sys::nix_store_path_free(self.as_ptr()); - } - } -} - -// SAFETY: StorePath can be shared between threads -unsafe impl Send for StorePath {} -unsafe impl Sync for StorePath {} diff --git a/nixide/src/store/tests.rs b/nixide/src/store/tests.rs deleted file mode 100644 index da72ae7..0000000 --- a/nixide/src/store/tests.rs +++ /dev/null @@ -1,75 +0,0 @@ -use serial_test::serial; - -use super::{Store, StorePath}; -use crate::errors::ErrorContext; -use crate::sys; -use crate::util::wrappers::AsInnerPtr as _; - -#[test] -#[serial] -fn test_store_opening() { - let mut ctx = ErrorContext::new(); - unsafe { - sys::nix_libutil_init(ctx.as_ptr()); - ctx.pop() - .expect("nix_libutil_init failed with bad ErrorContext"); - sys::nix_libstore_init(ctx.as_ptr()); - ctx.pop() - .expect("nix_libstore_init failed with bad ErrorContext"); - }; - - let _store = Store::open(None).expect("Failed to open store"); -} - -#[test] -#[serial] -fn test_store_path_parse() { - let mut ctx = ErrorContext::new(); - unsafe { - sys::nix_libutil_init(ctx.as_ptr()); - ctx.pop() - .expect("nix_libutil_init failed with bad ErrorContext"); - sys::nix_libstore_init(ctx.as_ptr()); - ctx.pop() - .expect("nix_libstore_init failed with bad ErrorContext"); - }; - - let store = Store::open(None).expect("Failed to open store"); - - // Try parsing a well-formed store path - let result = StorePath::fake_path(&store); - result.expect("idk hopefully this fails"); -} - -#[test] -#[serial] -fn test_store_path_clone() { - let mut ctx = ErrorContext::new(); - unsafe { - sys::nix_libutil_init(ctx.as_ptr()); - ctx.pop() - .expect("nix_libutil_init failed with bad ErrorContext"); - sys::nix_libstore_init(ctx.as_ptr()); - ctx.pop() - .expect("nix_libstore_init failed with bad ErrorContext"); - }; - - let store = Store::open(None).expect("Failed to open store"); - - // Try to get a valid store path by parsing - let path = StorePath::fake_path(&store).expect("Failed to create `StorePath::fake_path`"); - let cloned = path.clone(); - - // Assert that the cloned path has the same name as the original - let original_name = path.name().expect("Failed to get original path name"); - let cloned_name = cloned.name().expect("Failed to get cloned path name"); - - assert_eq!( - original_name, cloned_name, - "Cloned path should have the same name as original" - ); -} - -// Note: test_realize is not included because it requires a valid store path -// to realize, which we can't guarantee in a unit test. Integration tests -// would be more appropriate for testing realize() with actual derivations. diff --git a/nixide/src/util/lazy_array.rs b/nixide/src/util/lazy_array.rs deleted file mode 100644 index 2808314..0000000 --- a/nixide/src/util/lazy_array.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; - -#[derive(Debug)] -pub struct LazyArray -where - F: Fn(usize) -> T, -{ - inner: Rc>>>, - size: usize, - delegate: F, -} - -impl LazyArray -where - F: Fn(usize) -> T, -{ - pub fn new(size: usize, delegate: F) -> LazyArray { - let mut vec = Vec::with_capacity(size); - for _ in 0..size { - vec.push(None); - } - - LazyArray { - inner: Rc::new(RefCell::new(vec)), - size, - delegate, - } - } - - /// Returns `None` if `index < self.size` otherwise always succeeds - /// (unless of course the callback you supply panics). - /// - // pub fn get(&mut self, index: usize) -> Option<&T> { - // // let x = self.inner.get(index).copied().and_then(|value| match value { - // // Some(value) => Some(value), - // // None => { - // // // store the value first - // // let value = (self.delegate)(index); - // // self.inner[index] = Some(value); - - // // // now get a reference to it - // // if let Some(v) = &self.inner[index] { - // // return Some(v); - // // } - // // None - // // } - // // }) - // match self.inner.clone().borrow().get(index) { - // Some(Some(value)) => Some(value), - // Some(None) => { - // let mut inner = self.inner.clone().borrow_mut(); - // // store the value first - // inner[index] = Some((self.delegate)(index)); - - // // now get a reference to it - // inner[index].as_ref() - // } - // None => None, - // } - // } - pub fn get(&mut self, index: usize) -> Option> { - if index >= self.size { - return None; - } - - // let inner = self.inner.borrow(); - if let Some(value) = self.inner.borrow()[index].as_ref() { - return Some(Rc::new(value)); - } - - // drop(inner); // explicitly drop the borrow - - let value = (self.delegate)(index); - self.inner.borrow_mut()[index] = Some(value); - - Some(Rc::new(self.inner.borrow()[index].unwrap())) - } -} diff --git a/nixide/src/util/mod.rs b/nixide/src/util/mod.rs deleted file mode 100644 index f9ec55d..0000000 --- a/nixide/src/util/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[macro_use] -pub mod panic; -mod lazy_array; -pub(crate) mod wrap; -pub mod wrappers; - -pub(crate) use lazy_array::LazyArray; -pub(crate) use panic::{panic_issue, panic_issue_call_failed}; diff --git a/nixide/src/util/panic.rs b/nixide/src/util/panic.rs deleted file mode 100644 index b9e5e95..0000000 --- a/nixide/src/util/panic.rs +++ /dev/null @@ -1,20 +0,0 @@ -macro_rules! panic_issue { - ($($arg:expr),*) => {{ - panic!( - "{}: please open an issue on https://github.com/cry128/nixide", - format!($($arg),*) - ) - }}; -} - -macro_rules! panic_issue_call_failed { - () => {{ - crate::util::panic_issue!("[nixide] call to `{}` failed", $crate::stdext::debug_name!()) - }}; - ($($arg:expr),*) => {{ - crate::util::panic_issue!("[nixide] call to `{}` failed with \"{}\"", $crate::stdext::debug_name!(), format!($($arg),*)) - }}; -} - -pub(crate) use panic_issue; -pub(crate) use panic_issue_call_failed; diff --git a/nixide/src/util/wrap.rs b/nixide/src/util/wrap.rs deleted file mode 100644 index 33790c9..0000000 --- a/nixide/src/util/wrap.rs +++ /dev/null @@ -1,147 +0,0 @@ -#[repr(C)] -#[derive(Debug)] -pub(crate) struct UserData { - pub inner: S, - pub retval: T, - - // XXX: TODO: write impl functions to set and get these values, - // XXX: TODO: and another one to unwrap a `MaybeUninit>` - #[cfg(debug_assertions)] - pub init_inner: bool, - - #[cfg(debug_assertions)] - pub init_retval: bool, -} - -impl AsMut> for UserData { - fn as_mut(&mut self) -> &mut UserData { - self - } -} - -impl UserData { - pub unsafe fn as_mut_ptr(&mut self) -> *mut Self { - self as *mut Self - } - - pub unsafe fn inner_ptr(&mut self) -> *mut S { - unsafe { - let ptr = self.as_mut_ptr(); - &raw mut (*ptr).inner - } - } -} - -macro_rules! nonnull { - ($ptr:expr $(,)? ) => {{ - match ::std::ptr::NonNull::new($ptr) { - ::std::option::Option::Some(p) => ::std::result::Result::Ok(p), - ::std::option::Option::None => { - ::std::result::Result::Err($crate::errors::new_nixide_error!(NullPtr)) - } - } - }}; -} - -pub(crate) use nonnull; - -macro_rules! nix_fn { - ($callback:expr $(,)? ) => {{ - let mut __ctx = $crate::errors::ErrorContext::new(); - let __result = $callback(&__ctx); - __ctx - .pop() - .and_then(|_| ::std::result::Result::Ok(__result)) - }}; -} -pub(crate) use nix_fn; - -macro_rules! nix_ptr_fn { - ($callback:expr $(,)? ) => {{ - $crate::util::wrap::nix_fn!($callback).and_then(|ptr| $crate::util::wrap::nonnull!(ptr)) - }}; -} -pub(crate) use nix_ptr_fn; - -/// `libnix` functions consistently either expect the `userdata`/`user_data` (inconsistently named in the API...) -/// field to be the first or last parameter (differs between function). The `nix_callback!` macro allows the -/// position to be specified by either the following syntax: -/// -/// ```rs -/// nix_callback!(; userdata; ...); // first parameter -/// nix_callback!(...; userdata; ); // last parameter -/// ``` -/// -macro_rules! nix_callback { - ( | $( $($pre:ident : $pre_ty:ty),+ $(,)? )? ; $userdata:ident : $userdata_type:ty ; $( $($post:ident : $post_ty:ty),+ $(,)? )? | -> $ret:ty $body:block, $function:expr $(,)? ) => {{ - type __UserData = $crate::util::wrap::UserData<$userdata_type, $ret>; - // create a function item that wraps the closure body (so it has a concrete type) - // WARNING: this function must have no return type, use the `UserData.inner` - // WARNING: field instead as an `out` pointer. - #[allow(unused_variables)] - unsafe extern "C" fn __captured_fn( - $($( $pre: $pre_ty, )*)? - $userdata: *mut __UserData, - $($( $post: $post_ty, )*)? - ) { $body } - - unsafe extern "C" fn __wrapper_callback( - $($( $pre: $pre_ty, )*)? - $userdata: *mut ::std::ffi::c_void, - $($( $post: $post_ty, )*)? - ) { - unsafe { - __captured_fn( - $($( $pre, )*)? - // userdata_, - $userdata as *mut __UserData, - $($( $post, )*)? - ); - } - } - - let mut __ctx: $crate::errors::ErrorContext = $crate::errors::ErrorContext::new(); - let mut __state: ::std::mem::MaybeUninit<__UserData> = ::std::mem::MaybeUninit::zeroed(); - - $function(__wrapper_callback, __state.as_mut_ptr(), &__ctx); - - // type annotations for compiler - let __return: $crate::NixideResult<$ret> = __ctx.pop().and_then(|_| ::std::result::Result::Ok(unsafe { __state.assume_init().retval })); - __return - }}; -} -pub(crate) use nix_callback; - -macro_rules! nix_string_callback { - ($function:expr $(,)?) => {{ - #[repr(C)] - struct __ReturnType { - start: *const ::std::ffi::c_char, - n: ::std::ffi::c_uint, - } - - let __result = $crate::util::wrap::nix_callback!( - |start: *const ::std::ffi::c_char, n: ::std::ffi::c_uint; userdata: ();| -> (*const ::std::ffi::c_char, ::std::ffi::c_uint) { - unsafe { - let retval = &raw mut (*userdata).retval; - retval.write((start, n)) - } - }, - $function - ); - - __result.and_then(|(start, n)| { - let __return = $crate::stdext::CCharPtrExt::to_utf8_string_n(start, n as usize); - - __return - }) - }}; -} -pub(crate) use nix_string_callback; - -macro_rules! nix_pathbuf_callback { - ($function:expr $(,)?) => {{ - $crate::util::wrap::nix_string_callback!($function).map(::std::path::PathBuf::from) - }}; -} -pub(crate) use nix_pathbuf_callback; diff --git a/nixide/src/util/wrappers.rs b/nixide/src/util/wrappers.rs deleted file mode 100644 index 6fa0378..0000000 --- a/nixide/src/util/wrappers.rs +++ /dev/null @@ -1,32 +0,0 @@ -pub trait AsInnerPtr { - /// Acquires the underlying pointer to the inner `libnix` C struct. - /// - /// # Safety - /// - /// Although this function isn't inherently `unsafe`, it is - /// marked as such intentionally to force calls to be wrapped - /// in `unsafe` blocks for clarity. - unsafe fn as_ptr(&self) -> *mut T; - - /// Returns a shared reference to the inner `libnix` C struct. - /// - /// For the mutable counterpart see [AsInnerPtr::as_mut]. - /// - /// # Safety - /// - /// Although this function isn't inherently `unsafe`, it is - /// marked as such intentionally to force calls to be wrapped - /// in `unsafe` blocks for clarity. - unsafe fn as_ref(&self) -> &T; - - /// Returns a unique reference to the inner `libnix` C struct. - /// - /// For the shared counterpart see [AsInnerPtr::as_ref]. - /// - /// # Safety - /// - /// Although this function isn't inherently `unsafe`, it is - /// marked as such intentionally to force calls to be wrapped - /// in `unsafe` blocks for clarity. - unsafe fn as_mut(&mut self) -> &mut T; -} diff --git a/nixide/src/verbosity.rs b/nixide/src/verbosity.rs deleted file mode 100644 index fb1918b..0000000 --- a/nixide/src/verbosity.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::sys; - -/// Verbosity level -/// -/// # NOTE -/// -/// This should be kept in sync with the C++ implementation (nix::Verbosity) -#[derive(Debug, Clone, Copy)] -pub enum NixVerbosity { - Error, - Warn, - Notice, - Info, - Talkative, - Chatty, - Debug, - Vomit, -} - -impl From for NixVerbosity { - fn from(level: sys::nix_verbosity) -> NixVerbosity { - match level { - sys::nix_verbosity_NIX_LVL_ERROR => NixVerbosity::Error, - sys::nix_verbosity_NIX_LVL_WARN => NixVerbosity::Warn, - sys::nix_verbosity_NIX_LVL_NOTICE => NixVerbosity::Notice, - sys::nix_verbosity_NIX_LVL_INFO => NixVerbosity::Info, - sys::nix_verbosity_NIX_LVL_TALKATIVE => NixVerbosity::Talkative, - sys::nix_verbosity_NIX_LVL_CHATTY => NixVerbosity::Chatty, - sys::nix_verbosity_NIX_LVL_DEBUG => NixVerbosity::Debug, - sys::nix_verbosity_NIX_LVL_VOMIT => NixVerbosity::Vomit, - _ => panic!("nixide encountered unknown `nix_verbosity` value, please submit this as an issue at https://github.com/cry128/nixide"), - } - } -} diff --git a/nixide/src/version.rs b/nixide/src/version.rs deleted file mode 100644 index dfe8890..0000000 --- a/nixide/src/version.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::cmp::Ordering; -use std::ffi::CStr; -use std::num::ParseIntError; - -use crate::sys; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NixVersion { - pub major: u32, - pub minor: u32, - pub patch: u32, - pub is_prerelease: bool, -} - -impl NixVersion { - /// Constructs a new [NixVersion] struct given the raw attributes. - /// - /// # Warning - /// - /// You are most likely interested in using the [NixVersion::current] - /// and [NixVersion::parse] functions instead of this one. - pub fn new(major: u32, minor: u32, patch: u32, is_prerelease: bool) -> Self { - Self { - major, - minor, - patch, - is_prerelease, - } - } - - /// Get the current Nix library version in the comparable [NixVersion] type. - pub fn current() -> Result { - NixVersion::parse(NixVersion::current_string().as_ref()) - } - - /// Get the current Nix library version as an owned [String]. - pub fn current_string() -> String { - unsafe { - let version_ptr = sys::nix_version_get(); - CStr::from_ptr(version_ptr).to_string_lossy().into_owned() - } - } - - /// Parse a Nix version string into the comparable [NixVersion] type. - /// - /// # Examples - /// - /// ``` - /// use nixide::NixVersion; - /// - /// assert_eq!(NixVersion::parse("2.26"), Ok(NixVersion::new(2, 26, 0, false))); - /// assert_eq!(NixVersion::parse("2.33.0pre"), Ok(NixVersion::new(2, 33, 0, true))); - /// assert_eq!(NixVersion::parse("2.33"), Ok(NixVersion::new(2, 33, 0, false))); - /// assert_eq!(NixVersion::parse("2.33.1"), Ok(NixVersion::new(2, 33, 1, false))); - /// - /// // Pre-release versions sort before stable - /// assert!(NixVersion::parse("2.33.0pre").unwrap() < NixVersion::parse("2.33").unwrap()); - /// ``` - pub fn parse(version_str: &str) -> Result { - let parts = version_str.split('.').collect::>(); - let major = parts[0].parse::()?; - let minor = parts[1].parse::()?; - - let (patch, is_prerelease) = match parts.get(2) { - Some(s) => { - let length = s.len(); - let mut offset = length; - if length > 3 { - offset = offset.saturating_sub(3) - } - ( - s[..offset].parse::()?, // patch - length > 3 && s.ends_with("pre"), // is_prerelease - ) - } - None => (0, false), - }; - - Ok(Self { - major, - minor, - patch, - is_prerelease, - }) - } -} - -impl PartialOrd for NixVersion { - fn partial_cmp(&self, other: &Self) -> Option { - if self == other { - Some(Ordering::Equal) - } else if self.major < other.major - || self.minor < other.minor - || (self.patch < other.patch) - || (self.patch == other.patch && self.is_prerelease && !other.is_prerelease) - { - Some(Ordering::Less) - } else { - Some(Ordering::Greater) - } - } -} - -#[cfg(test)] -mod tests { - use super::NixVersion; - - #[test] - fn test_parse_version() { - assert_eq!( - NixVersion::parse("2.26"), - Ok(NixVersion::new(2, 26, 0, false)) - ); - assert_eq!( - NixVersion::parse("2.33.0pre"), - Ok(NixVersion::new(2, 33, 0, true)) - ); - assert_eq!( - NixVersion::parse("2.33"), - Ok(NixVersion::new(2, 33, 0, false)) - ); - assert_eq!( - NixVersion::parse("2.33.1"), - Ok(NixVersion::new(2, 33, 1, false)) - ); - } - - #[test] - fn test_version_ordering() { - // Pre-release versions should sort before stable - assert!(NixVersion::parse("2.33.0pre").unwrap() < NixVersion::parse("2.33").unwrap()); - assert!(NixVersion::parse("2.33.0pre").unwrap() < NixVersion::parse("2.33.0").unwrap()); - - // Normal version ordering - assert!(NixVersion::parse("2.26").unwrap() < NixVersion::parse("2.33").unwrap()); - assert!(NixVersion::parse("2.33").unwrap() < NixVersion::parse("2.33.1").unwrap()); - } -} diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 3a45084..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,5 +0,0 @@ -format_strings = true -group_imports = "Preserve" -imports_granularity = "Module" -reorder_impl_items = true -wrap_comments = true