Zero flake inputs: npins + haskell-flake standalone#635
Zero flake inputs: npins + haskell-flake standalone#635
Conversation
Drop flake-parts, nixos-unified, fourmolu-nix, git-hooks. Pin nixpkgs, haskell-flake, and the ema/lvar/heist-extra/unionmount/commonmark-*/ emanote-template sources through npins instead. flake.nix now has no inputs; all package graphs live in default.nix, shell.nix, and nix/*.nix, mirroring juspay/kolu. No pre-commit hooks, no fourmolu config — formatting is opt-in (`just fmt`: nixpkgs-fmt + cabal-fmt).
Replaces the omnix-driven `check-closure-size` app with a standard `checks.<system>.closure-size` derivation, so vira (and any other devour-flake consumer) picks it up without omnix in the loop. Uses `exportReferencesGraph` to materialise emanote's runtime closure in the sandbox, then sums `du -sb` of every path. The legacy 600 MB target required `justStaticExecutables` + `removeReferencesTo`, both broken on current nixpkgs. Budget is set to 7.5 GB — ~10% above the observed 6.77 GB baseline — so the check acts as a regression guard.
Lift the per-system `import ./default.nix` into a single `perSystem` thunk that `packages`/`apps`/`devShells`/`checks` each destructure from a shared record. Previously every output attribute re-imported default.nix independently, evaluating the haskell-flake project graph thrice per system.
nix/flake-module/site/outputs.nix no longer duplicates the static site / live-server / link-check derivations; it delegates to nix/emanote-site.nix, the same function default.nix uses for the internal docs site. One htmlproofer flag or build invocation tweak now lands in one file, not two.
After outputs.nix delegated site building to nix/emanote-site.nix, layer.nix's outputs.layer/outputs.layerString became dead — the new single source of truth computes layer specs internally from raw path/pathString/mountPoint fields.
Drops the awk state machine that parsed exportReferencesGraph by hand in favour of pkgs.closureInfo's ready-made store-paths file. Same output (6.76 GB), one-third the code.
default.nix and shell.nix accept an optional project argument; flake.nix evaluates haskell-project.nix once per system and threads the result to both. Previously the haskell-flake project was evaluated twice per system (once for packages/apps/checks, once for devShells). Standalone nix-build/nix-shell entry points still work — the default remains to import haskell-project.nix if project is unset.
Both `layer` and `layerString` diverge by exactly the `@mountPoint` suffix when one is set; compute the suffix once and append.
Drop header comments that restated WHAT the file does when the code and filename already convey it. Kept only the WHY lines (zero-inputs rationale, upstream nixpkgs bugs, shared haskell-flake eval).
Hickey/Lowy Analysis
Hickey rationaleThree separate Lowy rationaleThe volatility axes that matter are well-encapsulated: nixpkgs bumps touch one file, haskell-flake API changes touch one file, new Haskell deps touch one line in Elegance follow-ups (police pass)Four additional commits from the
|
x86_64-linux closure is 6.77 GB; aarch64-darwin is 7.53 GB (~11% larger). Previous 7.5 GB budget fit linux but failed on darwin under vira CI. 8.5 GB gives both systems ~10-15% regression headroom.
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 1s | git fetch ok; forge=github; noGit=false |
| research | ✓ | 15m 18s | Mapped current flake-parts layout, kolu target structure, haskell-flake standalone API, npins flow, and vira's devour-flake-based pipeline. |
| branch | ✓ | 6s | On feature branch npins-haskell-flake-standalone. |
| implement | ✓ | 6m 15s | npins set up with 9 pins, flake.nix rewritten with zero inputs, nix/{nixpkgs,overlay,haskell-project,emanote-site}.nix added, flake-module relocated, pre-commit/fourmolu/fourmolu-nix/git-hooks/flake-parts/nixos-unified removed. |
| check | ✓ | 10s | cabal build all: up to date. |
| docs | ✓ | 22s | nix/flake-module/README.md updated; user-facing docs already reference the external flake-parts module which still ships. |
| fmt | ✓ | 9s | nixpkgs-fmt idempotent on all .nix files; cabal-fmt idempotent on emanote.cabal. |
| commit | ✓ | 2m 33s | Commit 423024d pushed. |
| hickey+lowy | ✓ | 13m 29s | Parallel review. Fixed H#1 (7705932), H#3/L#1 (8455573). Restored check-closure-size (a421000). Other findings deferred or no-op. |
| police | ✓ | 7m 16s | Rules: no-dead-code (a5822d8). Fact-check: clean. Elegance: 4 commits (a2eb788, 78eaebc, 16a4ca5, 7c075e4). |
| test | ✓ | 47s | cabal test all: 23 examples, 0 failures. |
| create-pr | ✓ | 1m 3s | Draft PR #635 opened. |
| ci | ✓ | 2m 26s | vira ci succeeded at HEAD 8a5cfde: 5 packages × 2 systems built, gh-signoff on both aarch64-darwin and x86_64-linux. |
| Total | 52m 33s |
Slowest step: research (15m 18s)
Optimization suggestions
- research was the dominant step (29% of total). The kolu structure mapping + haskell-flake standalone API research can be skipped on a re-run: both are now encoded in the tree. For follow-ups, pass
--from implementso the workflow enters at the code-writing step. - hickey+lowy (13m 29s) was the second-slowest — it had to run twice because the initial run was followed by a
check-closure-sizeescalation from the user. If the closure-size scope had been part of the original prompt, the review wouldn't have needed re-anchoring. Lesson: surface critical preservation items in the initial brief. - ci needed one retry because the 7.5 GB closure budget I set from the x86_64-linux baseline was 26 MB short for aarch64-darwin (7.53 GB). When a budget has to cover multiple systems, measure all targets before picking a number, not one.
- police spent 7m 16s across 3 passes + 4 elegance commits. A lighter pre-check during implement (a quick "would this pattern match
pkgs.closureInfo?" sanity ping) could have landed the final shape in the first commit instead of requiring an elegance follow-up.
Workflow completed at 2026-04-21T00:26Z.
emanote's
flake.nixno longer has any inputs.nixpkgs,haskell-flake, and theema/lvar/heist-extra/unionmount/commonmark-*/emanote-templatesources are pinned throughnpins, and haskell-flake is consumed via its standalonelib.evalHaskellProjectAPI instead of a flake-parts module. Tree structure mirrors juspay/kolu —default.nix+shell.nix+ smallnix/*.nixfiles, so the same package graph drives flake and non-flake consumers.Gone with it:
flake-parts,nixos-unified,fourmolu-nix,git-hooks, the pre-commit plumbing that auto-generated.pre-commit-config.yaml, and the dormantomnixstep hook. Formatting is now opt-in —just fmtrunsnixpkgs-fmtandcabal-fmt; there is no forced hook, no fourmolu config, no pre-commit.check-closure-sizeis preserved but reshaped: it's nowchecks.<system>.closure-size, a standard flake check thatvira/nix flake checkpick up viadevour-flake— no external runner, no omnix. The legacy 600 MB target relied onjustStaticExecutables+removeReferencesTo, both broken on current nixpkgs; until those work again, the budget sits at 8.5 GB to cover both systems (x86_64-linux 6.77 GB baseline, aarch64-darwin 7.53 GB) as a pure regression guard. The check usespkgs.closureInfo+ a summeddu -sb— no hand-rolled graph parser.Downstream users of
inputs.emanote.flakeModulesee no API change — the external flake-parts module (relocated tonix/flake-module/) is byte-equivalent, and internally delegates to the samemkSitefunction that the docs build uses, so a tweak to the static-site pipeline lands in one file not two.Performance
Measured with
hyperfineonx86_64-linux, 5 samples fornix develop, 3 fornix build(eval only), 1 for--rebuild. "Cold eval" means~/.cache/nix/eval-cache-v6/*.sqlitewiped before each sample; fetcher/substitution caches stay warm (so deltas reflect the eval layer, not network).master(13 flake inputs)nix develop -c true— warm evalnix develop -c true— cold evalnix build .#default— warm evalnix build .#default— cold evalnix build --rebuild .#default— forced local rebuild of emanoteBoth branches produce the same
emanote-1.6.0.0.drvderivation hash; rebuild cost is identical within noise.Try it locally