Skip to content

build(nix): build from source instead of fetching per-platform binaries#48

Merged
yusukebe merged 30 commits into
yusukebe:mainfrom
ryoppippi:ryoppippi/fix-nix
Jul 23, 2026
Merged

build(nix): build from source instead of fetching per-platform binaries#48
yusukebe merged 30 commits into
yusukebe:mainfrom
ryoppippi:ryoppippi/fix-nix

Conversation

@ryoppippi

@ryoppippi ryoppippi commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Why

The source is TypeScript and the only runtime dependency (linkedom) is pure JS — nothing about this package is platform-specific except the bun runtime, which nixpkgs already provides per platform. Yet the flake managed four per-platform prebuilt binaries with sha256es that had to be rewritten on every release: scripts/flake-update.ts regex-rewrote flake.nix (reimplementing Nix's base32 hash encoding along the way) and a bot commit was pushed straight to main from the release workflow, which then had to re-dispatch the nix check by hand because bot commits don't trigger workflows.

What

  • Build from source: dependencies are fetched from the hashes already in bun.lock (via bun2nix, which generates nix/bun.nix), so no hash is maintained by hand and nothing varies per platform. A postinstall script keeps nix/bun.nix in sync automatically, and CI fails if it goes stale.
  • The release-time flake bump is gone entirely: no more flake-update.ts, no bot commit to main after each release, and the actions: write permission the release workflow needed to re-dispatch the nix check is dropped too. Releasing now touches nothing in the repo. As a bonus, release tags cut after this lands become valid pins (nix run github:yusukebe/ax/vX.Y.Z) — previously the hash bump landed after the tag, so every existing tag's flake points one release behind (e.g. v0.1.21 serves 0.1.20).
  • The derivation lives in a callPackage-style package.nix at the repo root; flake.nix only wires it up per system. bun2nix is the only argument nixpkgs cannot supply, so the flake passes it explicitly.
  • No compile step: src/index.ts already has a bun shebang, so it is installed as-is with the shebang pointed at the store's bun and symlinked into bin/.
  • Switch the nixpkgs input from nixpkgs-26.05-darwin to nixpkgs-unstable: a source build wants a current bun, and the darwin-stable channel no longer buys us anything now that no binaries are patched.
  • Drop the flake-utils input; derive meta/version from package.json; add a devShell that runs bun install --frozen-lockfile on entry when node_modules is missing or older than the lockfile.
  • x86_64-darwin loses Nix support: bun2nix doesn't ship for it, and nixpkgs is dropping the platform entirely in 26.11 (Intent to Ship) — Homebrew and release binaries still cover it.
  • Update the README's Nix section accordingly (source build, tags as pins).

Nits

  • Smoke-test the installed program with nixpkgs' versionCheckHook instead of a hand-rolled --version | grep pipeline, so the program's own exit code is honoured.
  • Drop the apps output: nix run resolves it from packages + meta.mainProgram automatically.
  • Drop tsconfig.json from the build fileset and the nix.yml path filters: bun install never reads it and it is not copied into $out (no .tsx, no paths aliases in src).
  • Bind ax with let instead of rec in packages to avoid rec's same-name capture and // merge pitfalls.
  • Derive pname from package.json too — it was the last hand-written duplicate of the manifest.
  • The supported-systems list lives once in nix/systems.nix, imported by both the flake's forAllSystems and meta.platforms.
  • Add a formatter output (nixfmt-tree) and enforce formatting in CI with nix fmt -- --ci; statix reports nothing.

Testing

  • nix flake check --all-systems
  • nix build + nix run . -- --version, plus a selector-extraction smoke test exercising linkedom

ryoppippi added 10 commits July 22, 2026 14:04
bun2nix converts bun.lock into per-package fetchurl expressions whose
hashes come straight from the lockfile's sha512 fields, so the Nix
build no longer needs a manually maintained fixed-output hash.

The postinstall script regenerates nix/bun.nix on every bun install,
keeping it in sync with bun.lock without anyone running a tool by
hand — contributors without Nix get this via the WASM CLI.
Replace the hand-rolled fixed-output derivation (bun install inside an
FOD whose outputHash had to be re-pinned by hand on every lockfile
change) with bun2nix: dependencies are fetched per-package from the
hashes already present in bun.lock via the generated nix/bun.nix, so
no hash in the flake ever needs manual maintenance.

The package is writeBunApplication rather than mkDerivation because
the shipped bin is src/index.ts run under bun — there is no compile
step, which also drops the Linux-only autoPatchelf machinery.

Systems shrink to the nix-systems/triplet set bun2nix ships for;
x86_64-darwin loses Nix support (nixpkgs 26.05 is its final release
anyway — Homebrew and the release binaries still cover it).
mkShellNoCC because nothing in this project needs a C compiler; the
shell only provides the bun runtime and the native bun2nix CLI for
regenerating nix/bun.nix.
The flake no longer embeds per-release binary hashes — it builds from
source with hashes taken from bun.lock — so the release-time rewrite
of flake.nix (and the scripts/flake-update.ts helper plus the
actions: write permission used to dispatch nix.yml afterwards) has
nothing left to do.
The package now builds from source, so flake validation must also run
when src, the lockfile, package.json or tsconfig change — not only
when the .nix files do. Also retire the workflow_dispatch note about
the release-time flake bump, which no longer exists.
bun install's postinstall regenerates nix/bun.nix, so after the
frozen-lockfile install a non-empty diff means someone updated
bun.lock without committing the regenerated file.
writeBunApplication generated a bash start script that ran
'bun run src/index.ts'. src/index.ts already carries a
'#!/usr/bin/env bun' shebang, so install the tree as-is, point the
shebang at the store's bun, and symlink it into bin — one less
process and no generated wrapper.

Also restores the --version install check that the wrapper variant
had dropped.
lib.genAttrs over the three supported systems replaces
eachSystem — one less input for a helper the flake barely used.
x86_64-darwin was already dropped when moving to bun2nix, which does
not ship for it.
description, homepage, licence and mainProgram now come from
package.json (homepage is a new field there), so the two files cannot
drift. The licence string is resolved with lib.getLicenseFromSpdxId
and mainProgram is the first bin entry.

The flake-level description stays literal — flake metadata must be a
plain string.
@ryoppippi ryoppippi changed the title refactor(nix): build from source with bun2nix refactor(nix): build from source instead of fetching per-platform binaries Jul 22, 2026
The step name already says everything the comment did.
@ryoppippi ryoppippi changed the title refactor(nix): build from source instead of fetching per-platform binaries build(nix): build from source instead of fetching per-platform binaries Jul 22, 2026
The step-by-step narration restated the step names (and still said
'wrapper' after the wrapper was removed). Keep only the reason the
nix run step must not be dropped.
Run bun install from the shellHook when node_modules is missing or
older than bun.lock, so entering the shell is enough to get a working
tree — and the postinstall keeps nix/bun.nix regenerated as a side
effect.
The bun2nix default check phase only fires through its mkDerivation
wrapper, not when using the hook directly — verified by building
without the flag.
Comment thread package.json
"format": "oxfmt --check .",
"format:fix": "oxfmt --write ."
"format:fix": "oxfmt --write .",
"postinstall": "bun2nix -o nix/bun.nix"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

generate bun.nix when we update dependencies

…kHook

The previous check piped ax --version into grep, so the pipeline's
exit status was grep's alone: ax could exit non-zero after printing
the version and the check would still pass (stdenv phases run without
pipefail). nixpkgs ships versionCheckHook for exactly this smoke test —
it resolves the program from meta.mainProgram, runs it with --version,
honours its exit code, and checks the output for the version.
nix run resolves apps.default and apps.<name> from packages plus
meta.mainProgram automatically, so the apps block duplicated what the
packages output already provides. Both nix run . and nix run .#ax
keep working unchanged.
bun install never reads tsconfig.json and the file is not copied into
$out, so it only widened the rebuild triggers: editing compiler
options rebuilt the package for nothing. src has no .tsx files and no
path aliases, so bun needs nothing from it at runtime either. If JSX
or paths mappings are introduced later, the file must come back and
also be copied into $out/share/ax.

The nix.yml path filters lose the file for the same reason: it no
longer feeds the build, so it should not trigger the workflow.
@ryoppippi
ryoppippi force-pushed the ryoppippi/fix-nix branch from 899e602 to fba5fb9 Compare July 22, 2026 22:46
rec fixes self-references at definition time, so it silently changes
meaning when attributes are added (same-name capture) and loses the
recursion under // merges. A let binding scopes the reference
explicitly and is immune to both.
Reading package.json does not depend on the system, so evaluating it
inside axFor repeated the parse once per system. Binding it in the
outer let makes the system-independence visible and evaluates it once.
nixpkgs.legacyPackages.${system} was spelled out for every package
reference; a let binding names it once, matching how axFor already
does it.
flake.nix carried the whole mkDerivation inline; moving it to a
callPackage-style package.nix at the repo root separates "what the
package is" from "how the flake exposes it", and the file can be
dropped into nixpkgs nearly unchanged if ax is ever upstreamed.
bun2nix is the only argument nixpkgs cannot supply, so the flake
passes it explicitly.

The nix.yml path filters collapse 'flake.nix' + '**/*.nix' into
'**.nix': the old '**/*.nix' pattern needs a path separator, so it
would have missed the new root-level package.nix.
@ryoppippi
ryoppippi force-pushed the ryoppippi/fix-nix branch 2 times, most recently from 904671c to b9290b3 Compare July 22, 2026 23:11
Comment thread nix/systems.nix
@@ -0,0 +1,5 @@
[
"x86_64-linux"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

latest nixpkgs only suppots this

The Nix section still said the flake wraps the prebuilt release
binary and that release tags are not valid pins because the hash
bump landed after the tag. Both stopped being true when the flake
moved to building from source, and the tag caveat's premise is gone
too, so tags now work as pins. Point readers at flake.nix and
package.nix instead of restating the mechanics.
Every other piece of package metadata (version, description,
homepage, license, mainProgram) already comes from package.json;
pname was the last hand-written duplicate of it.
nix fmt now runs nixfmt-tree, so the nixfmt formatting the repo
already follows can be maintained without installing anything, and
the workflow fails on drift via treefmt's --ci mode. bun2nix's
generated nix/bun.nix is already nixfmt-clean, so no excludes are
needed.
meta.platforms briefly inherited bun's list, but bun still claims
x86_64-darwin while this flake dropped it (bun2nix does not ship
there), so the inherited list over-promised. The support declaration
now lives once in nix/systems.nix, imported by both the flake's
forAllSystems and package.nix's meta.platforms — the same pattern
package.nix already uses for package.json and nix/bun.nix.
@ryoppippi
ryoppippi force-pushed the ryoppippi/fix-nix branch from b9290b3 to 4c4882f Compare July 22, 2026 23:19
@ryoppippi
ryoppippi marked this pull request as draft July 23, 2026 00:21
@ryoppippi
ryoppippi marked this pull request as ready for review July 23, 2026 00:33
@ryoppippi

Copy link
Copy Markdown
Contributor Author

READY! PTAL!

@yusukebe yusukebe left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM!

@yusukebe

Copy link
Copy Markdown
Owner

@ryoppippi Thank you for the hard work!

@yusukebe
yusukebe merged commit dbcf8b9 into yusukebe:main Jul 23, 2026
3 checks passed
@ryoppippi

Copy link
Copy Markdown
Contributor Author

no worries!! i'm happy that we can try it with nix

@ryoppippi
ryoppippi deleted the ryoppippi/fix-nix branch July 23, 2026 12:59
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.

2 participants