Skip to content

refactor(nix): Managed Reproducibility. Managed Cache.#1601

Closed
lietblue wants to merge 25 commits intomoeru-ai:mainfrom
lietblue:nix-build-redesign-merge
Closed

refactor(nix): Managed Reproducibility. Managed Cache.#1601
lietblue wants to merge 25 commits intomoeru-ai:mainfrom
lietblue:nix-build-redesign-merge

Conversation

@lietblue
Copy link
Copy Markdown
Collaborator

@lietblue lietblue commented Apr 7, 2026

Super Nix. Our build system

REPRODUCIBILITY — IMMUTABILITY — INCREMENTAL BUILDS.

Our righteous path to build.


but build doesn't come free.

image

LOOKS FAMILIAR?

Full-store rebuilds. Multi‑gigabyte pnpm trees. CI nodes burning hours because one line in the lockfile moved. Things like these were happening all over the galaxy right now. Your monorepo could be next. Unless you can make the most important decision of your life.

Join the Nix Diver.

FREEDOM IS NOT FREE. DISK IS NOT EITHER.

The old world asked you to pay for the whole stack every time. We asked a different question: what if each package were its own verdict?

Prove to yourself that you have the strength and courage to be free — free from all-or-nothing rebuilds, free from shipping the same tarball work twice, free from watching Nix re-materialize civilization because someone bumped a patch version.

  • Per-package derivations — ~3,000 independent fragments, each cached on its own terms
  • Content-addressed merge — same hash, same file; no debates, no duplicates
  • Parallelism that scales — cold builds can fan out the way democracy intended
  • Hermetic CI — no network at store assembly time; only the decision to build what changed

image

CI/CD changes

.github/workflows/update-nix-pnpm-deps-hash.yaml

@lietblue lietblue changed the title refactor(NIX): Managed Reproducibility. Managed Cache. refactor(nix): Managed Reproducibility. Managed Cache. Apr 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

⏳ Approval required for deploying to Cloudflare Workers (Preview) for stage-web.

Name Link
🔭 Waiting for approval For maintainers, approve here

Hey, maintainers, kindly take some time to review and approve this deployment when you are available. Thank you! 🙏

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new, more granular Nix-based pnpm dependency management system that generates individual derivations for each package to improve caching. It includes a documentation file explaining the pnpm CAFS store merging process and scripts to automate the generation of these Nix expressions. Several improvements were identified in the review: the openssl dependency needs to be explicitly added to the Nix environment for tarball hashing, the find command in the merge phase should be made more robust using -exec to avoid potential failures with xargs, and the cafs-add.mjs script should use idiomatic Node.js writeFileSync instead of spawning shell processes for file operations.

Comment thread nix/pnpm-store.nix
Comment thread nix/pnpm-store.nix
Comment thread nix/pnpm-store.nix Outdated
Comment thread nix/scripts/cafs-add.mjs Outdated
Comment thread nix/scripts/cafs-add.mjs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a0f35679f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread nix/scripts/gen-pnpm-packages.ts Outdated
Comment thread .github/workflows/update-nix-pnpm-deps-hash.yaml Outdated
@LemonNekoGH
Copy link
Copy Markdown
Member

My life for super nix

Copy link
Copy Markdown
Collaborator

@Weathercold Weathercold left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just call pnpm in each derivation to fetch the package, then copy the store to out? This custom logic is prone to breakage every time pnpm changes its internal storage format

Comment thread nix/scripts/gen-pnpm-packages.ts Outdated
Comment thread nix/scripts/gen-pnpm-packages.ts Outdated
Comment thread nix/scripts/cafs-add.mjs Outdated
lietblue added a commit to lietblue/airi that referenced this pull request Apr 8, 2026
Address PR moeru-ai#1601 review feedback from Weathercold:

- Replace custom cafs-add.mjs with pnpm-cafs-add.ts using @pnpm/store.cafs
  (pnpm's own CAFS library), eliminating fragile reimplementation of pnpm
  internals that would break on store format changes
- Generator now outputs pnpm-packages.json instead of generated .nix files;
  pnpm-store.nix is hand-written Nix that reads JSON via builtins.fromJSON
- Use `find -exec {} +` instead of `xargs` in merge phase (Gemini review)
- CI workflow: add --ignore-scripts to pnpm install (Codex review)
- Bundle pnpm-cafs-add.ts with tsdown for self-contained Nix builds

Deleted: nix/scripts/cafs-add.mjs, nix/pnpm-packages.nix
Added: nix/scripts/pnpm-cafs-add.{ts,mjs}, nix/pnpm-packages.json
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42ecc54aa9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread nix/scripts/pnpm-cafs-add.ts
@lietblue
Copy link
Copy Markdown
Collaborator Author

lietblue commented Apr 8, 2026

I reworked the PR per your feedback:

1. Replaced custom CAFS logic with @pnpm/store.cafs

The hand-rolled cafs-add.mjs (tarball extraction, SHA-512 hashing, -exec suffix logic, CAFS file placement) is replaced by a ~60-line script that delegates everything to @pnpm/store.cafs — pnpm's own CAFS implementation. This means the store format automatically tracks pnpm's internal layout across version bumps. The script is bundled as a self-contained .mjs via tsdown so Nix derivations only need nodejs at build time.

2. pnpm-store.nix is now hand-written, not generated

The generator no longer emits Nix code. pnpm-store.nix is a static, human-readable Nix file that uses lib.mapAttrs to create per-package derivations from data. Addresses the "code inside code is unmaintainable" concern.

3. Generator outputs JSON instead of Nix

gen-pnpm-packages.ts now produces a simple pnpm-packages.json instead of two .nix files. Nix reads it via builtins.fromJSON (builtins.readFile ./pnpm-packages.json).

4. Other fixes

  • find -exec {} + instead of find | xargs in merge phase (handles empty results gracefully)
  • --ignore-scripts in CI workflow (prevents unrelated build:packages during lockfile update)
  • writeFileSync instead of execSync('cat > ...') (handled by the library now)

I have a question: eliminate the JSON file entirely via IFD?

The JSON is purely derived from pnpm-lock.yaml. We could remove it (along with gen-pnpm-packages.ts and the CI regeneration workflow) by parsing the lockfile directly in Nix:

let
  # IFD: convert pnpm-lock.yaml to JSON (tiny derivation, cached after first eval)
  lockfileJson = runCommand "pnpm-lock-json" {
    nativeBuildInputs = [ yj ];
  } ''
    yj < ${../pnpm-lock.yaml} > $out
  '';
  lockfile = builtins.fromJSON (builtins.readFile lockfileJson);
  # Extract packages directly in Nix — no gen script, no committed JSON
in ...

The IFD derivation is minimal (yj < file > $out, sub-second) and gets cached by the Nix store after first evaluation. nix build allows IFD by default; only --pure-eval (used by nix flake check) disables it, which can be overridden with --allow-import-from-derivation or allow-import-from-derivation = true in nix.conf.

@Weathercold What's your preference?

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83b7684410

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread nix/scripts/gen-pnpm-packages.ts Outdated
@Weathercold
Copy link
Copy Markdown
Collaborator

Weathercold commented Apr 12, 2026

I prefer with IFD. Indeed, the overhead should be minimal (we currently have IFD as well)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b12b95d82

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread nix/pnpm-store.nix
@nekomeowww
Copy link
Copy Markdown
Member

Rebase.

lietblue added a commit to lietblue/airi that referenced this pull request Apr 17, 2026
Address PR moeru-ai#1601 review feedback from Weathercold:

- Replace custom cafs-add.mjs with pnpm-cafs-add.ts using @pnpm/store.cafs
  (pnpm's own CAFS library), eliminating fragile reimplementation of pnpm
  internals that would break on store format changes
- Generator now outputs pnpm-packages.json instead of generated .nix files;
  pnpm-store.nix is hand-written Nix that reads JSON via builtins.fromJSON
- Use `find -exec {} +` instead of `xargs` in merge phase (Gemini review)
- CI workflow: add --ignore-scripts to pnpm install (Codex review)
- Bundle pnpm-cafs-add.ts with tsdown for self-contained Nix builds

Deleted: nix/scripts/cafs-add.mjs, nix/pnpm-packages.nix
Added: nix/scripts/pnpm-cafs-add.{ts,mjs}, nix/pnpm-packages.json
@lietblue lietblue force-pushed the nix-build-redesign-merge branch from 7b12b95 to b0b52de Compare April 17, 2026 07:13
@lietblue
Copy link
Copy Markdown
Collaborator Author

Rebase.

done

lietblue added 14 commits April 25, 2026 00:19
…yers

Previously, `pnpm.fetchDeps` was a single fixed-output derivation that
downloaded all ~2990 packages in one shot. Any change to pnpm-lock.yaml
invalidated the entire layer, forcing a full re-download.

New approach: each package gets two independent Nix derivations —
one `fetchurl` for the tarball download and one `makePkgStore` for
assembling it into a pnpm CAFS v10 store fragment. The merge derivation
combines all ~2990 fragments with `cp -rn` (safe since CAFS is
content-addressed). Only changed packages need to be rebuilt.

New files:
- nix/scripts/cafs-add.mjs: pure Node.js script that extracts a tarball
  and writes pnpm CAFS v10 files/ and index/ entries
- nix/scripts/gen-pnpm-packages.ts: parses pnpm-lock.yaml and generates
  nix/pnpm-packages.nix (~2990 fetchurl entries) and nix/pnpm-store.nix
- nix/pnpm-packages.nix: auto-generated fetchurl derivations (DO NOT EDIT)
- nix/pnpm-store.nix: auto-generated CAFS assembly + merge (DO NOT EDIT)

Developer workflow after pnpm-lock.yaml changes:
  pnpm run nix:gen
  git add nix/pnpm-packages.nix nix/pnpm-store.nix
  nix build .#airi.pnpmDeps

Removes nix/pnpm-deps-hash.txt and nix/update-pnpm-deps-hash.sh.
Updates CI workflow to run pnpm run nix:gen instead.
- cafs-add: chmod -R u+rwX unconditionally after tar extraction so that
  tarballs whose top-level dir is stored with a non-executable mode (e.g.
  0600) don't cause EACCES in statSync/readdirSync during the walk phase.
  The extraction loop only called chmod on failure; tar can exit 0 while
  leaving dirs non-traversable because it applies final permissions after
  populating children.

- cafs-add: chmod -R u+rwX in the finally block before rm -rf so that
  cleanup never hits EPERM on non-writable extracted directories.

- pnpm-store: use passAsFile for the store-paths list to avoid E2BIG.
  Interpolating ~3000 store paths directly into buildPhase produces a
  ~150 KB env var that exceeds ARG_MAX when bash is exec'd. passAsFile
  writes the content to a temp file and exposes $pkgStoresListPath.

- pnpm-store: add --no-preserve=mode to cp -rn so that Nix store dirs
  (mode 0555) are not copied verbatim into $out; the first package to
  create a shared subdir would otherwise lock out all subsequent packages
  with "Permission denied".
Two bugs caused pnpm to fail with ERR_PNPM_NO_OFFLINE_TARBALL even
though packages were present in the store:

1. Missing -exec suffix on executable CAFS files.
   pnpm v10 CAFS requires executable files to be stored with a "-exec"
   filename suffix (e.g. {hash}-exec). Without it, pnpm cannot locate
   bin scripts and falls back to downloading, which fails offline.
   Fix: append "-exec" to cafsName when isExec is true.

2. Stale checkedAt timestamp in package index JSON.
   Setting checkedAt: 1 (epoch ms) caused pnpm to treat the entry as
   never-verified and trigger integrity re-checks that could fail in the
   sandbox. nixpkgs pnpm.fetchDeps removes this field entirely via
   `jq "del(.. | .checkedAt?)"`. Fix: omit checkedAt from index entries.

3. Dynamic pnpm CAFS version subdirectory in merge phase.
   Previously hardcoded "files/" at root; pnpm actually stores CAFS
   under a versioned subdir (e.g. "v10/"). The merge derivation now adds
   pnpm as a nativeBuildInput, runs `pnpm store path` once to detect the
   version, and writes output under $out/$storeVer/. Per-package drv
   hashes are unaffected (pnpm is not in makePkgStore's nativeBuildInputs).
…e is processed

The while IFS= read -r loop skips the last line if it has no trailing newline.
This caused the last package (zwitch@2.0.4) to not be copied into the merged
pnpm store, resulting in only 2989/2990 packages being available.

Adding a trailing newline to lib.concatStringsSep output ensures all packages
are correctly merged into the final CAFS store.
CAFS v10 files with -exec suffix denote executable binaries (turbo, esbuild, etc).
These need execute permission (mode 555) so pnpm can run them during install.

Without this, pnpm install fails with EACCES when turbo or other native binaries
are executed in the build sandbox.
lietblue added 10 commits April 25, 2026 00:19
- Generate passAsFile pkgStoresList loop (avoid ARG_MAX/E2BIG)
- Detect $storeVer via pnpm store path; merge under $out/$storeVer
- cp -rn --no-preserve=mode; chmod *-exec for native bins
- Regenerate pnpm-packages.nix from lockfile (3014 packages)
- Bump nix/assets-hash.txt
Made-with: Cursor
Address PR moeru-ai#1601 review feedback from Weathercold:

- Replace custom cafs-add.mjs with pnpm-cafs-add.ts using @pnpm/store.cafs
  (pnpm's own CAFS library), eliminating fragile reimplementation of pnpm
  internals that would break on store format changes
- Generator now outputs pnpm-packages.json instead of generated .nix files;
  pnpm-store.nix is hand-written Nix that reads JSON via builtins.fromJSON
- Use `find -exec {} +` instead of `xargs` in merge phase (Gemini review)
- CI workflow: add --ignore-scripts to pnpm install (Codex review)
- Bundle pnpm-cafs-add.ts with tsdown for self-contained Nix builds

Deleted: nix/scripts/cafs-add.mjs, nix/pnpm-packages.nix
Added: nix/scripts/pnpm-cafs-add.{ts,mjs}, nix/pnpm-packages.json
…m merge

Move all pnpm internal format knowledge into pnpm-cafs-add.ts:
- Import STORE_VERSION from @pnpm/constants (currently "v10")
- Write store version prefix and .fetcher-version inside the script
- Merge derivation is now completely format-agnostic (just cp -rn)
- Remove pnpm dependency and runtime detection from merge derivation
- Remove storeVersion from pnpm-packages.json (no longer needed)
- Build node_modules from fetchurl tarballs in a Nix derivation (cafsScript)
- Run pnpm-cafs-add.ts directly via Node's --experimental-strip-types
- Delete the 142KB unauditable bundled pnpm-cafs-add.mjs
- Generator now emits cafsScriptDeps list in JSON for Nix to consume
- Remove nix:bundle-cafs script and eslint ignore for bundle
Add pnpm to nativeBuildInputs in common and package derivations so pnpmConfigHook can execute in the Nix sandbox without missing binary errors.

Made-with: Cursor
Parse pnpm-lock.yaml directly in Nix via yj (Import From Derivation)
instead of maintaining a committed 18K-line JSON file with a TypeScript
generator and CI regeneration workflow.

- Rewrite pnpm-store.nix to use IFD + pure Nix package extraction
- Delete nix/pnpm-packages.json (~18,200 lines)
- Delete nix/scripts/gen-pnpm-packages.ts (generator script)
- Delete .github/workflows/update-nix-pnpm-deps-hash.yaml (CI workflow)
- Remove nix:gen script from package.json

pnpm-lock.yaml is now the single source of truth. The IFD derivation
(yj < lockfile > $out) is sub-second and cached after first evaluation.
…oded list

Replace the manually maintained cafsScriptDeps version list with a pure
Nix lockfile graph walk (depClosure). The nix/scripts workspace declares
@pnpm/constants and @pnpm/store.cafs; the Nix evaluator reads the
lockfile importers + snapshots sections to compute the full transitive
closure automatically. No manual updates needed when deps change.
@lietblue lietblue force-pushed the nix-build-redesign-merge branch from b0b52de to 3364ab3 Compare April 24, 2026 16:21
@lietblue
Copy link
Copy Markdown
Collaborator Author

Why do we need such a complicated nix build to set up a pnpm project? We could just grab the binary directly, then use elfpatch to make nix happy, and that's it. This PR might remain just a PoC for now. Even though modifying a dependency only requires fetching a single package, still — why should we use nix to manage such a complex pnpm project? In a few days, I'll just use nix patch to package the binary — no need for assets, and no need for this huge blob of pnpmDeps.

@lietblue lietblue closed this Apr 24, 2026
@lietblue
Copy link
Copy Markdown
Collaborator Author

my bad

@Weathercold
Copy link
Copy Markdown
Collaborator

Why do we need such a complicated nix build to set up a pnpm project? We could just grab the binary directly, then use elfpatch to make nix happy, and that's it. This PR might remain just a PoC for now. Even though modifying a dependency only requires fetching a single package, still — why should we use nix to manage such a complex pnpm project? In a few days, I'll just use nix patch to package the binary — no need for assets, and no need for this huge blob of pnpmDeps.

One of the points of using nix is reproducible builds. Packaging binary build defeats that point

You can add a -bin package if you want

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.

4 participants