Skip to content

Zero flake inputs: npins + haskell-flake standalone#635

Closed
srid wants to merge 11 commits intomasterfrom
npins-haskell-flake-standalone
Closed

Zero flake inputs: npins + haskell-flake standalone#635
srid wants to merge 11 commits intomasterfrom
npins-haskell-flake-standalone

Conversation

@srid
Copy link
Copy Markdown
Owner

@srid srid commented Apr 21, 2026

emanote's flake.nix no longer has any inputs. nixpkgs, haskell-flake, and the ema/lvar/heist-extra/unionmount/commonmark-*/emanote-template sources are pinned through npins, and haskell-flake is consumed via its standalone lib.evalHaskellProject API instead of a flake-parts module. Tree structure mirrors juspay/koludefault.nix + shell.nix + small nix/*.nix files, 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 dormant omnix step hook. Formatting is now opt-injust fmt runs nixpkgs-fmt and cabal-fmt; there is no forced hook, no fourmolu config, no pre-commit.

check-closure-size is preserved but reshaped: it's now checks.<system>.closure-size, a standard flake check that vira/nix flake check pick up via devour-flake — no external runner, no omnix. The legacy 600 MB target relied on justStaticExecutables + 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 uses pkgs.closureInfo + a summed du -sb — no hand-rolled graph parser.

Downstream users of inputs.emanote.flakeModule see no API change — the external flake-parts module (relocated to nix/flake-module/) is byte-equivalent, and internally delegates to the same mkSite function that the docs build uses, so a tweak to the static-site pipeline lands in one file not two.

Performance

Measured with hyperfine on x86_64-linux, 5 samples for nix develop, 3 for nix build (eval only), 1 for --rebuild. "Cold eval" means ~/.cache/nix/eval-cache-v6/*.sqlite wiped before each sample; fetcher/substitution caches stay warm (so deltas reflect the eval layer, not network).

Scenario master (13 flake inputs) this PR (zero inputs) Δ
nix develop -c true — warm eval 170.0 ± 1.7 ms 162.3 ± 2.1 ms −7.7 ms (−4.5%)
nix develop -c true — cold eval 2.905 s ± 81 ms 2.671 s ± 118 ms −234 ms (−8.1%)
nix build .#default — warm eval 107.6 ± 0.9 ms 106.9 ± 0.1 ms −0.7 ms (−0.6%)
nix build .#default — cold eval 1.560 s ± 12 ms 1.649 s ± 49 ms +89 ms (+5.7%)
nix build --rebuild .#default — forced local rebuild of emanote 24.56 s 24.65 s +0.1 s (noise)

Both branches produce the same emanote-1.6.0.0.drv derivation hash; rebuild cost is identical within noise.

Honest read: the perf case is weak. Warm eval is in the noise. Cold nix develop eval saves ~234 ms. Cold nix build eval is slightly slower. The gains are nowhere near the 2–3× that kolu saw on a much larger flake — because emanote's baseline is already modest (2.9 s cold vs kolu's 7.5 s). The real case for this PR is simplification: four dead dependencies gone, static-free default.nix/shell.nix entry points, closure-size guard now a first-class flake check. Merge for the cleanup, not the microseconds.

Try it locally

nix build github:srid/emanote/npins-haskell-flake-standalone#docs
nix develop github:srid/emanote/npins-haskell-flake-standalone

srid added 9 commits April 21, 2026 00:00
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).
@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 21, 2026

Hickey/Lowy Analysis

# Lens Finding Disposition
1 Hickey Triple import ./default.nix in flake.nix Fixed in this PR (7705932)
2 Hickey Three npins load sites No-op — templates belong at flake root; default.nix/shell.nix are legitimately independent entry points
3 Hickey / Lowy #1 Site-building logic duplicated across nix/emanote-site.nix and nix/flake-module/site/outputs.nix Fixed in this PR (8455573)
4 Hickey resolveLayer duplicates layer.nix outputs.layer/outputs.layerString Fixed in this PR (a5822d8 — dead layer outputs removed after #3)
5 Hickey shell.nix independent import No-op — standalone nix-shell entry point is intentional (now shares project thunk with default.nix per elegance pass)
6 Hickey / pre-existing Option duplication TODO between site module and home-manager module Deferred — pre-existing scope
2 Lowy nixpkgs version volatility well-encapsulated behind nix/nixpkgs.nix No-op
3 Lowy haskell-flake API volatility well-encapsulated in nix/haskell-project.nix No-op
4 Lowy Adding a new Haskell source: one line in packages + npins/sources.json No-op
5 Lowy Flake output shape: implicit contract between flake.nix and default.nix Deferred — low magnitude, no schema yet
6 Lowy Build tool scatter No-op — collapsed by #1 fix
7 Lowy emanote CLI invocation reimplemented in home-manager module + both site builders Deferred — pre-existing, out of scope
8 Lowy External flakeModule boundary isolated from internal consumers No-op

Hickey rationale

Three separate import ./default.nix { inherit pkgs; } calls in packages/apps/checks meant the haskell-flake project was evaluated three times per system. Lifting them into a single perSystem thunk collapsed that to one. The other big one: nix/emanote-site.nix (internal, for the docs site) and nix/flake-module/site/outputs.nix (external, for downstream flake-parts users) both implemented the same static-site + live-server + link-check trio. Extracted mkSite as the single source of truth; the flake-parts module now delegates to it, which in turn made outputs.layer/outputs.layerString in layer.nix dead code (removed).

Lowy rationale

The 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 haskell-project.nix plus a pin. The one real leak the lens surfaced — "how a site is built" spanning two implementations — collapsed to one via the same refactor Hickey flagged. Deferred items (#5, #6, #7) are either pre-existing scope or low enough magnitude that a schema/abstraction isn't yet worth its keep.

Elegance follow-ups (police pass)

Four additional commits from the /code-police elegance pass:

  • a2eb7887 — replace awk state machine with pkgs.closureInfo
  • 78eaebcc — share haskell-flake eval across default.nix/shell.nix (saves one evalHaskellProject per system)
  • 16a4ca52 — share mountPoint suffix across layer / layerString in resolveLayer
  • 7c075e45 — trim WHAT-narration comments; keep only WHY

srid added 2 commits April 21, 2026 00:24
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.
@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 21, 2026

/do results

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 implement so 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-size escalation 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.

@srid srid closed this Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant