From 8fd2e53c53382447e2e4c91575c6bf852fb133da Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 25 Jul 2026 22:28:10 -0400 Subject: [PATCH] build: package the kitchen CLI as a Nix flake Add a flake-parts flake whose default package Bun-compiles the kitchen CLI into a self-contained native binary, built in the sandbox with dependencies vendored by bun2nix. Bun is pinned via a dedicated nixpkgs-bun input (Bun 1.3.13) that bun2nix follows, so nixpkgs can track unstable without moving the compile toolchain onto a release that emits empty binaries. bun2nix is both a flake input and an npm devDependency, and a postinstall hook keeps the checked-in bun.nix in sync with the lockfile. --- .claude/tasks/0001-flake-build.md | 53 ++++++++++ .gitignore | 4 + bun.lock | 7 ++ bun.nix | 78 ++++++++++++++ flake.lock | 163 ++++++++++++++++++++++++++++++ flake.nix | 41 ++++++++ package.json | 6 +- package.nix | 19 ++++ 8 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 .claude/tasks/0001-flake-build.md create mode 100644 bun.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 package.nix diff --git a/.claude/tasks/0001-flake-build.md b/.claude/tasks/0001-flake-build.md new file mode 100644 index 0000000..a780625 --- /dev/null +++ b/.claude/tasks/0001-flake-build.md @@ -0,0 +1,53 @@ +--- +spec: nix-flake-packaging +--- + +## What to build + +The tracer bullet for packaging the repository as a Nix flake: a flake-parts flake whose default package Bun-compiles the `kitchen` CLI into a self-contained native binary, built reproducibly in the sandbox with vendored dependencies. + +This is the whole risk seam — a Nix build is sandboxed with no network, while `bun install` fetches from npm, so the dependency closure must be vendored before anything compiles. +Bun dependencies are vendored via bun2nix (a flake input) reading the workspace lockfile into a checked-in generated expression, kept in sync with the lockfile by a postinstall hook so a dependency change needs no separate manual regeneration. +A single pinned Bun version is shared by the build derivation (and later the dev shell), avoiding point releases known to produce empty binaries under sandboxed native compilation. + +Once this compiles, the remaining outputs (`nix run`, `nix develop`, checks) hang off the working flake in the next slice. + +## Acceptance criteria + +- [x] The flake is structured with flake-parts, with `x86_64-linux` in the systems list and nixpkgs tracking `nixpkgs-unstable`. +- [x] bun2nix is a flake input, and its generated vendoring expression is checked in and regenerated by a postinstall hook on lockfile changes. +- [x] A single Bun version is pinned in one place and consumed by the build derivation. +- [x] `nix build` produces a runnable `kitchen` binary reproducibly with no network access during the build. +- [x] The compiled binary embeds its dependencies and runs on a machine with no Node.js or Bun runtime present. +- [x] The default package is consumable as a flake input from another configuration. + +## Implementation Notes + +The tracer bullet builds and all six criteria were verified end-to-end against a real `nix build`, not just by inspection. + +### The Bun pin + +bun2nix compiles the binary with the `bun` from *its own* nixpkgs, baked into its setup hook at bun2nix build time. +So pinning Bun means controlling the nixpkgs that bun2nix follows, not overlaying `bun` in this flake's package set. +The pin is a dedicated `nixpkgs-bun` input fixed to one revision (Bun 1.3.13), with `bun2nix.inputs.nixpkgs.follows = "nixpkgs-bun"`. +That revision is the one bun2nix 2.1.2 itself locks, so the compile toolchain matches what bun2nix was tested against. +`nixpkgs` still tracks `nixpkgs-unstable` for everything else, and updating it cannot move Bun. +The dev shell in task 0002 consumes the same `nixpkgs-bun`, keeping "develop with the version that compiles" true from one place. + +### Vendoring and the postinstall hook + +bun2nix is both a flake input (native builder) and an npm devDependency, so the `postinstall: bun2nix -o bun.nix` hook regenerates the checked-in `bun.nix` on any `bun install` even outside the Nix dev shell. +The build passes `dontRunLifecycleScripts = true` so that same postinstall does not fire redundantly inside the sandbox. + +### Verification performed + +`nix build` produced a 101 MB native ELF that runs to exit 0 under `env -i` (empty environment, no Node or Bun, `ldd` shows only glibc), confirming the embedded-runtime and no-network claims — the compile derivation is a normal sandboxed derivation with networking disabled. +A throwaway consumer flake built the package through `inputs.kitchen.packages.x86_64-linux.default`, confirming criterion 6. + +### Deviations from the plan + +- The nix-community cachix `nixConfig` block that the bun2nix templates ship was dropped, because the spec lists "any binary cache or substituter setup" as out of scope. + Consequence: a first build with a cold store compiles bun2nix from source. + A developer who wants the prebuilt bun2nix can add the substituter to their own Nix configuration. +- `packages/bin/src/index.ts` is still the placeholder entry point. + The CLI's behaviour belongs to the core-parser and cli-view specs, so the compiled binary is a no-op that exits 0 — enough to prove the packaging seam. diff --git a/.gitignore b/.gitignore index 93588c1..09295f6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ node_modules/ # compiled CLI binary packages/bin/kitchen +# nix build result symlinks +result +result-* + # local environment overrides .env .env.local diff --git a/bun.lock b/bun.lock index 9142b64..d2af586 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "devDependencies": { "@biomejs/biome": "^2.5.3", "bun-types": "latest", + "bun2nix": "^2.1.2", }, }, "packages/bin": { @@ -51,6 +52,12 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun2nix": ["bun2nix@2.1.2", "", { "dependencies": { "sade": "^1.8.1" }, "bin": { "bun2nix": "index.ts" } }, "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], } } diff --git a/bun.nix b/bun.nix new file mode 100644 index 0000000..43a395f --- /dev/null +++ b/bun.nix @@ -0,0 +1,78 @@ +# Autogenerated by `bun2nix`, editing manually is not recommended +# +# Set of Bun packages to install +# +# Consume this with `fetchBunDeps` (recommended) +# or `pkgs.callPackage` if you wish to handle +# it manually. +{ + copyPathToStore, + fetchFromGitHub, + fetchgit, + fetchurl, + ... +}: +{ + "@biomejs/biome@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.3.tgz"; + hash = "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A=="; + }; + "@biomejs/cli-darwin-arm64@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.3.tgz"; + hash = "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g=="; + }; + "@biomejs/cli-darwin-x64@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.3.tgz"; + hash = "sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ=="; + }; + "@biomejs/cli-linux-arm64-musl@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.3.tgz"; + hash = "sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg=="; + }; + "@biomejs/cli-linux-arm64@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.3.tgz"; + hash = "sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA=="; + }; + "@biomejs/cli-linux-x64-musl@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.3.tgz"; + hash = "sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ=="; + }; + "@biomejs/cli-linux-x64@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.3.tgz"; + hash = "sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA=="; + }; + "@biomejs/cli-win32-arm64@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.3.tgz"; + hash = "sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw=="; + }; + "@biomejs/cli-win32-x64@2.5.3" = fetchurl { + url = "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.3.tgz"; + hash = "sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q=="; + }; + "@kitchen-md/bin" = copyPathToStore ./packages/bin; + "@kitchen-md/core" = copyPathToStore ./packages/core; + "@types/node@26.1.1" = fetchurl { + url = "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz"; + hash = "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="; + }; + "bun-types@1.3.14" = fetchurl { + url = "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz"; + hash = "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="; + }; + "bun2nix@2.1.2" = fetchurl { + url = "https://registry.npmjs.org/bun2nix/-/bun2nix-2.1.2.tgz"; + hash = "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg=="; + }; + "mri@1.2.0" = fetchurl { + url = "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz"; + hash = "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="; + }; + "sade@1.8.1" = fetchurl { + url = "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"; + hash = "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="; + }; + "undici-types@8.3.0" = fetchurl { + url = "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz"; + hash = "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="; + }; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3d8bda3 --- /dev/null +++ b/flake.lock @@ -0,0 +1,163 @@ +{ + "nodes": { + "bun2nix": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs-bun" + ], + "systems": [ + "systems" + ], + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1784665499, + "narHash": "sha256-9BMxlTxCCDAeoNLtb1a/st7udtTIJep+wpUzquA29VU=", + "owner": "nix-community", + "repo": "bun2nix", + "rev": "0f2a1f0b6f42cebe3b149bf62d38754c5e0e9729", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "2.1.2", + "repo": "bun2nix", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "bun2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1784872115, + "narHash": "sha256-THPEF2po0fsoH8gNtp+Ae0XFDJH3N/ol7xO3v6VMTJU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "335f0738cb2fa9708f3f428e39d2eae975d1338d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-bun": { + "locked": { + "lastModified": 1784497964, + "narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1782614948, + "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "bun2nix": "bun2nix", + "flake-parts": "flake-parts_2", + "nixpkgs": "nixpkgs", + "nixpkgs-bun": "nixpkgs-bun", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1680978846, + "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=", + "owner": "nix-systems", + "repo": "x86_64-linux", + "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "x86_64-linux", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "bun2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784369104, + "narHash": "sha256-47cxbcZODibHv3rELFQ9vZly0vUNkND/atn/U7HLeb0=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "df3c0640565d04a0261253cdd89fce78ec50168a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..69691bb --- /dev/null +++ b/flake.nix @@ -0,0 +1,41 @@ +{ + description = "kitchen — the KitchenMD CLI, packaged as a Nix flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + + # Provides the single pinned Bun used to compile the binary, held apart + # from `nixpkgs` so a `nixpkgs` update cannot drag Bun to a release that + # emits empty binaries under sandboxed native compilation. + nixpkgs-bun.url = "github:nixos/nixpkgs/241313f4e8e508cb9b13278c2b0fa25b9ca27163"; + + flake-parts.url = "github:hercules-ci/flake-parts"; + + systems.url = "github:nix-systems/x86_64-linux"; + + bun2nix = { + url = "github:nix-community/bun2nix?ref=2.1.2"; + # Compile against the pinned Bun rather than `nixpkgs`'s. + inputs.nixpkgs.follows = "nixpkgs-bun"; + inputs.systems.follows = "systems"; + }; + }; + + outputs = + inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; + + perSystem = + { system, ... }: + let + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ inputs.bun2nix.overlays.default ]; + }; + in + { + packages.default = pkgs.callPackage ./package.nix { }; + }; + }; +} diff --git a/package.json b/package.json index 7efc269..af0041e 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,12 @@ "scripts": { "test": "bun test", "lint": "biome check .", - "format": "biome check --write ." + "format": "biome check --write .", + "postinstall": "bun2nix -o bun.nix" }, "devDependencies": { "@biomejs/biome": "^2.5.3", - "bun-types": "latest" + "bun-types": "latest", + "bun2nix": "^2.1.2" } } diff --git a/package.nix b/package.nix new file mode 100644 index 0000000..8f7dc37 --- /dev/null +++ b/package.nix @@ -0,0 +1,19 @@ +# The `kitchen` CLI compiled to a self-contained native binary. +{ lib, bun2nix }: +bun2nix.mkDerivation { + pname = "kitchen"; + version = (lib.importJSON ./packages/bin/package.json).version; + + src = ./.; + + module = "packages/bin/src/index.ts"; + + bunDeps = bun2nix.fetchBunDeps { + bunNix = ./bun.nix; + }; + + # No vendored dependency needs install-time lifecycle scripts, and running + # them would fire this repo's own `postinstall` (bun2nix) inside the sandbox, + # where it is redundant. + dontRunLifecycleScripts = true; +}