Nixpkgs voor de frontend-beperkten
Ondanks mijn bewondering en vreugde voor Nix, de taal en de tool, is dat niet wat er werkelijk magisch aan is. Het store-model is het grote idee. Elke vergelijkbare tool die op hetzelfde model is gebouwd, zal dezelfde magische eigenschappen van reproduceerbaarheid en hermeticiteit hebben.
Ik heb in veel berichten onderzocht wat er voortvloeit uit het Nix-ecosysteem, maar de taal zelf is een kleine, lazy functionele taal met een derivation-primitief. Zo'n taal zou je in een weekend kunnen ontwerpen.
Wat je niet in een weekend kunt ontwerpen, is nixpkgs: twintig jaar aan fixes, elke workaround voor cross-compilatie, elke stdenv-fase, elk gepatcht configure-script, elke "deze bibliotheek heeft deze vlag nodig op aarch64-darwin" die iemand al heeft opgelost. Per oktober 2026 zijn er meer dan 140.000 pakketten die bouwen. Dat is het geheime ingrediënt 🥫, en het is geschreven in Nix; dus als je het wilt hebben, moet je Nix schrijven.
Helaas is dit ook de valkuil. Iedereen die de store-modelruimte vanuit een andere richting wil verkennen — een nieuwe taal, een nieuwe evaluator, een nieuwe editor-ervaring — moet dan vanaf nul pakketten beginnen. Dit is het kerkhof waar andere reimplementaties of forks van Nix sterven.
Als je echter de hele boom van nixpkgs zou kunnen nemen en transformeren naar iets wat een ander ecosysteem al spreekt, zou je het store-model kunnen verkennen zonder het werk van twintig jaar dat in nixpkgs is gestoken, op te geven. 💡
Ik koos voor TypeScript. Waarom? Waarom niet!?
TypeScript heeft een breed scala aan tooling en wordt ondersteund in bijna elke browser. Het heeft een ongelooflijk flexibel typesysteem, wat betekent dat we de vorm van een derivation en alle eigenaardigheden binnen het lazy evaluation-model van Nix kunnen uitdrukken.
Ik ben een frontend-beperkte "noob", maar ik dacht dat het idee het waard was om te onderzoeken.
jinx is het resultaat. Het is een transpiler die een .nix-bestand neemt en een .ts-bestand produceert dat naar dezelfde waarde evalueert, met dezelfde thunks, dezelfde derivation-strings en dezelfde drvPaths.
Vervolgens heb ik alle nixpkgs ermee gecompileerd. Elk attribuut, elk pakket, elke module, elke stdenv- en lib-functie: in totaal 231.777 attributen. De gecompileerde boom staat op fzakaria/jinx-nixpkgs, met één commit per nixpkgs-revisie.
Wat het doet
Elk .nix-bestand wordt één .ts-module met dezelfde naam in een boom die de bron weerspiegelt. Een lambda is een arrow function, een let is een blok van consts, een attribute set is een object literal, import is require. Je kunt het lezen.
$ jinx compile pkgs/by-name/he/hello/package.nix
// Generated by jinx from .../pkgs/by-name/he/hello/package.nix. Do not edit.
import * as nix from "../../../../.jinx/runtime/index.ts";
const here = nix.here(import.meta.url);
const formals$1 = { args: { callPackage: false, fetchurl: false, hello: false, lib: false, nixos: false, stdenv: false, testers: false }, ellipsis: false };
export default nix.lambda((args) => {
const $ = nix.formals(args, formals$1);
const callPackage = nix.arg($, "callPackage");
const fetchurl = nix.arg($, "fetchurl");
...
return nix.app(nix.sel(stdenv, "mkDerivation"), (finalAttrs) => ({
pname: "hello",
version: "2.12.2",
src: nix.thunk(() => nix.app(fetchurl, {
url: nix.thunk(() => nix.str`mirror://gnu/hello/hello-${nix.sel(finalAttrs, "version")}.tar.gz`),
hash: "sha256-...",
})),
...
}));
}, formals$1);
Nix is lazy, dus de gegenereerde code wrapt een expressie in nix.thunk(() => ...) overal waar Nix een thunk zou alloceren. 🤮
De jinx evaluation schrijft niets naar /nix/store. jinx instantiate schrijft de .drv-closure en nix-store --realise bouwt de derivation.
$ jinx eval hello.nix
{ drv = "/nix/store/j8q5j0x495vfrxgha5m6q40yjdi59334-hello-2.12.2.drv";
out = "/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2"; }
$ nix-instantiate --eval --strict hello.nix
{ drv = "/nix/store/j8q5j0x495vfrxgha5m6q40yjdi59334-hello-2.12.2.drv";
out = "/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2"; }
We produceren exact dezelfde .drv en exact dezelfde drvPath als Nix. Dat is de sleutel tot het ontsluiten van nixpkgs in een ander ecosysteem: de store merkt het verschil niet.
Niets verloren in vertaling
We kunnen bewijzen dat onze vertaling getrouw is. We evalueren elk attribuut van de eigen CI-configuratie van nixpkgs, ci/eval/outpaths.nix, met zowel jinx als nix-eval-jobs, en vergelijken alle drvPaths.
Op nixpkgs 6510408d, op x86_64-linux, aarch64-linux en aarch64-darwin:
| Attributen | Identieke drvPath | Falen aan beide zijden | Verschillen | Falen aan één zijde |
|---|---|---|---|---|
| 231.777 | 229.063 | 2.714 | 0 | 0 |
Nul verschillen of fouten aan slechts één zijde. De 2.714 die aan beide zijden falen, zijn de attributen die Nix weigert te evalueren, zoals onondersteunde platforms, kapotte of onveilige pakketten.
Maar serieus, waarom?
Het evalueren van nixpkgs in TypeScript is ongeveer 3 tot 5 keer langzamer. Als dat alles was, zou het niet erg interessant zijn. Het doel is niet om Nix sneller te maken, maar om nixpkgs toegankelijk te maken voor het TypeScript-ecosysteem.
Hier zijn enkele "leuke" dingen die je ermee kunt doen:
- Een evaluatie profileren: Gebruik
node --cpu-profopfirefox.drvPath, gevouwen via de source maps zodat elk frame een regel nixpkgs is. - Volledige nixpkgs-aanvulling in je editor: Automatische Language Server Protocol ondersteuning voor elk attribuut in nixpkgs, met source maps naar het originele
.nix-bestand. Het typesysteem van TypeScript is expressief genoeg om de vorm van een derivation te modelleren, dus krijg je aanvulling en type-checking gratis. - Door een pakket stappen: Source maps plaatsen een breakpoint op een
.nix-regel. Open de gecompileerde boom in VS Code of Chrome DevTools, zet een breakpoint inpkgs/by-name/he/hello/package.nix, en stap instdenv.mkDerivationterwijl deze de attribute set bouwt.
``bash $ node --inspect-brk --enable-source-maps src/cli/main.ts eval hello.nix ``
- Een vraag aan nixpkgs stellen in een loop: Bevraag en exploreer nixpkgs in een REPL of een script, zonder dat je de incantatie hoeft te onthouden om een JSON-dump van de hele boom te krijgen en deze door
jqte pipen.
``typescript import { loadNixpkgs } from "jinx"; const pkgs = loadNixpkgs("/path/to/nixpkgs"); const gpl = []; for (const name of Object.keys(pkgs.lib.licenses)) { if (pkgs.lib.licenses[name].spdxId?.startsWith("GPL")) { gpl.push(name); } } console.log(JSON.stringify(gpl)); ``
- Nix unit-testen met
node:test: Een lib-functie of een pakketdefinitie is een functie in een module, dus deze heeft een unit-test zoals elke andere functie in elke andere taal.
``typescript test("hello builds the derivation Nix would", () => { assert.equal( pkgs.hello.drvPath, "/nix/store/cnraiw0hbdl6jzdhbkn8dah2hqj7xgij-hello-2.12.3.drv", ); }); ``
- Een pakket schrijven in TypeScript: De gecompileerde vorm vanuit jinx is getrouw aan Nix tot in elke thunk, wat het exact en ellendig maakt om met de hand te schrijven. Er is echter een veel fijnere, handgeschreven vorm. Het interopereert met nixpkgs, dus een pakket in TypeScript kan
stdenv.mkDerivationaanroepen en afhankelijk zijn van alles inpkgs.
``typescript const hello = pkgs.stdenv.mkDerivation({ pname: "hello", version: "2.12.2", src: pkgs.fetchurl({ url: "mirror://gnu/hello/hello-2.12.2.tar.gz", hash: "sha256-WpqZbcKSzCTc9BHO6H6S9qrluNE72caBm0x6nc4IGKs=", }), meta: { license: pkgs.lib.licenses.gpl3Plus }, }); console.log(hello.drvPath); ``
- Het draait in een browser: TypeScript wordt ondersteund in elke moderne browser. Je kunt nixpkgs in je tabblad evalueren, waarbij de gecompileerde modules vanaf GitHub worden opgehaald wanneer ze nodig zijn.
Is het traag?
Ja, het is traag, maar cruciaal is dat het niet te traag is. Wanneer het "warm" is (modules gecompileerd en V8-codecache gevuld), zijn de resultaten per attribuut op mijn laptop:
| Probe | Nix | Jinx warm | Warm / Nix | Peak RSS Nix | Peak RSS Jinx |
|---|---|---|---|---|---|
| lib | 0,53 s | 1,31 s | 2,5x | 238 MB | 501 MB |
| hello | 0,37 s | 1,72 s | 4,6x | 88 MB | 376 MB |
| firefox | 3,19 s | 11,44 s | 3,6x | 488 MB | 1608 MB |
| nixos | 4,21 s | 21,35 s | 5,1x | 603 MB | 2377 MB |
Een eerste evaluatie die elk bereikt module moet compileren, is 3 tot 10 keer trager en het geheugengebruik is ongeveer 2 tot 4 keer hoger, omdat thunks erg duur zijn voor de garbage-collector.
Stel dat het vijf keer langzamer is dan een C++-interpreter waar twintig jaar lang aan is geslepen, vanuit een gloednieuwe compiler die draait op een JavaScript-engine. Dat lijkt me een prima startpunt. 🤷
Kan ik het proberen?
De broncode is te vinden op github.com/fzakaria/jinx. Het heeft geen afhankelijkheden, geen native code en geen daemon. Je kunt het draaien met Node 24 of in je browser. De gecompileerde nixpkgs-boom staat op github.com/fzakaria/jinx-nixpkgs.
Ik ben een beetje geërgerd dat ik constant moet melden dat ik "AI" heb gebruikt voor de code. Mensen lijken het leuk om je daarop "te ontmaskeren", alsof het een moreel falen is. Laat me ze de moeite besparen: een AI heeft veel van deze software samen met mij geschreven.
Hier is je stempel. Je kunt hier stoppen met lezen als dat een dealbreaker is; de derivations zullen hun hashes niet voor jou veranderen.
Nixpkgs is nu een TypeScript-programma. Zet een breakpoint in stdenv.
Groetjes,