sparkles:nix — Delivery Plan
Audience: contributors implementing the library. This document is execution-only — milestones, verification, and risks. For the desired-state specification read SPEC.md; for the binding workflow read Integrating C Libraries.
The library is an ImportC raw layer (sparkles.nix.c) plus a hand-written high-level wrapper (RAII handles + Expected-based errors), built bottom-up in the C-library dependency order (util → store → expr → flake). Each milestone ends green: it compiles, dub test :nix passes, and the consumer (dub build :nix-eval) builds.
1. Milestone overview
| # | Deliverable | Depends on |
|---|---|---|
| M0 | Nix flake input + devshell wiring + libs/nix skeleton + ImportC shim that compiles & links | — |
| M1 | Error/context/string foundation: NixError, NixResult, NixContext, checkCall, string sink | M0 |
| M2 | Lifecycle + store + evaluation + value extraction/creation + NixSession (the core) | M1 |
| M3 | Flake support (FlakeSettings/FetchersSettings/FlakeReference/lock/outputs) | M2 |
| M4 | apps/nix-eval example + README runnable example + docs | M2 (M3 opt) |
M0 de-risks the whole effort (does ImportC parse the headers? does pkg-config feed the include path?). M2 is the bulk. M3 layers on M2. M4 is the user-facing demo.
2. Per-milestone detail
M0 — Build plumbing (the de-risking milestone)
Ship in this order, each step verified before the next:
- Flake input. Add to
flake.nixinputs:nixnix = { url = "github:NixOS/nix"; inputs.nixpkgs.follows = "nixpkgs"; };nix flake lockto pin it. - Devshell. Add to
nix/shells/default.nixpackagestheout+.devof all six C libs (inputs'.nix.packages.nix-{util,store,expr,fetchers,flake,main}-cand each.dev).pkg-configis already present. Confirm in the shell:pkg-config --cflags --libs nix-expr-cprints include + link flags. libs/nixskeleton. Createlibs/nix/src/sparkles/nix/c.c(the#pragma-wrapped includes from SPEC §2) anddub.sdl(targetType "sourceLibrary",libs "nix-util-c" … "nix-main-c",dflags "-preview=in" "-preview=dip1000"). Addpackage.d=public import sparkles.nix.c;for now. RegistersubPackage "libs/nix"in the rootdub.sdl(sorted).git addall new files.- Prove it. A trivial
@system unittestthat callsnix_version_get()and asserts the returned C string is non-empty.dub test :nixmust pass. - Verify pkg-config does the work: rebuild with
env -u NIX_CFLAGS_COMPILE -u CPATH -u C_INCLUDE_PATH dub test :nix --force— still finds<nix_api_*.h>⇒ pkg-config (sourceLibrary) is correct.
Risk —
[[deprecated]]innix_api_value.h. The raw C23 attribute may trip ImportC. Ifdub test :nixfails parsing it: (a) try LDC vs the gcc14dmd; (b) if still failing, strip the attribute by post-patching the header in a tiny Nix derivation that wrapsnix-expr-c.dev, orsed-patch into a local include dir put first on the ImportC path. Record the resolution in [[nix-c-api-binding-facts]].
Risk — GCC 15
stddef.h/nullptr. Known ImportC issue (the toolchain overridesdmd→gcc14; LDC is unaffected). If it bites, build with LDC.
M1 — Error / context / string foundation
sparkles.nix.error then sparkles.nix.strings (SPEC §4–§5):
NixErrCodeenum (mirror ofnix_err),NixErrorstruct (code + GC message +toString),NixResult!Talias,nixOk/nixErrhelpers.NixContextmove-only RAII (create/free/ptr/check).checkreadsnix_err_code; on non-OK copiesnix_err_msginto a GC string and clears. Unit-test the error path by triggering aNIX_ERR_KEY(e.g.nix_setting_geton a bogus key).checkCall!fntemplate (splice ctx ptr → call → check;voidfornix_err-returning,Rfor value-returning) andcheckCallOptKey!fn(NIX_ERR_KEY→Nullable.null). Confine pointer/FFI work to@trustedblocks.stringSinkCallback(W)extern(C)trampoline +collectStringInto/collectString. Test by round-trippingnix_version-style strings and a setting value.
M2 — Core: lifecycle, store, eval, values, session
Build the evaluator surface (SPEC §6–§9, §11):
sparkles.nix.library—initNix(once-guarded, value-type cached error),nixVersion,getSetting/setSetting(mutex-guarded),GcThreadGuardno-op stub + the threading-model doc comment.sparkles.nix.value—ValueTypeenum +Valuehandle (incref postblit / decref dtor,adopt/retainpackage factories,type/isNull). No state-dependent ops here.sparkles.nix.store—Store(refcounted;openwith dummy:// default,uri/storeDir/version_,parsePath,isValidPath),StorePath(name/dup/free). Realise/derivations behind a version guard.sparkles.nix.eval—EvalStateBuilder+EvalState(create/build,eval), then the value operations asEvalStatemethods:force/forceDeep/valueType;requireInt/Float/Bool/String /Path(+requireStringInto);listSize/requireList/listAt;attrCount/requireAttr/requireAttrOpt/attrNames;mkInt/Float/Bool/Null/String/Path/List/Attrs;call/callMulti/applyLazy;realiseString.NixSessionfacade inpackage.d(init + store + eval bundled) + re-exports of the public surface.
Tests (all on one thread, dummy store): scalars (1+2, 3.14, true, "hi"), a list ([1 2 3] → requireList → requireInt each), an attrset ({x=1; y="hi";} → attrNames/requireAttr/requireAttrOpt incl. a missing key → null), type-mismatch → NixError, a thrown eval (builtins.throw "boom") → NixError with the message, round-trip construction (mkAttrs/mkList → extract). Prefer the project's check helpers where they fit; otherwise assert on NixResult directly.
M3 — Flakes
sparkles.nix.flake (SPEC §10): the six handles + EvalStateBuilder.flakes wiring. Integration test mirroring the Rust flake_lock_load_flake: write a tiny flake.nix to a temp dir (use sparkles:test-utils tmpfs helpers), setSetting("experimental-features","flakes"), build a flakes-enabled eval state, parse path:<dir>#frag, lock (virtual mode), get outputs, select + requireString an output, assert. Guard on Nix ≥ 2.26.
M4 — Example app + docs
apps/nix-eval(targetType "executable", depssparkles:nix+sparkles:core-cli): parse args, open aNixSession, evaluate, and recursively render theValuebyValueType. Optional--flakepath. RegistersubPackage "apps/nix-eval"in the rootdub.sdl(sorted).- A runnable
README.mdexample per the[Output]convention (usesversion="*"), verified withnix run .#ci -- --verify. - Per-library docs stub under
docs/libs/nix/(Diátaxis) is encouraged but may follow.
3. Verification checklist
- [ ]
dub test :nixgreen; the unset-NIX_CFLAGS_COMPILErebuild still finds the headers (pkg-config proven). - [ ]
dub build :nix-evalbuilds; the demo evaluates int/list/attrs. - [ ]
nix run .#ci -- --test --fail-fastpasses for the new package. - [ ] README example verifies (
nix run .#ci -- --verify --files README.md). - [ ] New files
git add-ed before anynix develop/flake build. - [ ] Atomic commits per AGENTS.md (
build(nix)flake wiring first, thenfeat(nix)per layer, thenfeat(terminal)-stylefeat(nix-eval)).
4. Workflow orchestration
M0 and M1 are mostly sequential and small — do them inline. M2's value operations (the many require*/mk*) are independent once the Value handle and checkCall exist and can be fanned out across agents (one per operation group) with a final integration-test pass; M3 likewise (one agent per flake handle). Use a workflow for those fan-outs when implementing, reviewing each layer's tests before moving up the stack.
5. Risks & open decisions
[[deprecated]]/ ImportC C23 attributes — see M0 risk. Resolved empirically in M0; this gates everything.github:NixOS/nixversion drift — the flake input may resolve to a Nix whose C API differs slightly from the 2.35-dev headers read during design. Pin viaflake.lock; gate newer calls withversion (Nix_2_x).- Single-thread constraint — accepted for v1 (SPEC §6.3). A real
GcThreadGuard(binding<gc/gc.h>, pkg-configbdw-gc,-lgc) is the first post-v1 extension if multi-threaded eval is needed. @nogclean vs. reality — the wrapper layer is GC-using by design (SPEC §12);@nogcsink overloads are provided where they matter.