gonix

A Nix language evaluator and derivation builder written in Go, inside a fork of Lix.

Clone the full history (gonix.bundle, a git bundle, 2.6 MiB):
curl -O https://sources.profpatsch.de/gonix.bundle
git clone gonix.bundle gonix

The clone keeps the bundle as its origin; replace the file and git fetch to update.

Run a package with Nix (gonix.tar.gz, a source tarball, 2.1 MiB):
nix run https://sources.profpatsch.de/gonix.tar.gz#<package>

The archive is a Nix flake; nix flake show https://sources.profpatsch.de/gonix.tar.gz lists its packages.

gonix(7)

gonix - a Nix language evaluator and derivation builder written in Go

gonix is a from-scratch implementation of the Nix language evaluator, its store path algorithms, and its sandboxed derivation builder, written in Go. It lives in gonix/ inside a fork of Lix, whose C++ source makes up the rest of this tree.

The C++ tree is here as a reference implementation, not as a dependency. Nothing in gonix/ links against it or calls into it. It is kept because the whole project is an exercise in matching an existing implementation exactly, and the answer to "what does Nix actually do here" is far more often found in the C++ source than in any specification. Nearly every commit in this repository cites the specific C++ function whose behaviour it reproduces.

The goal is byte-identical output, not approximate compatibility. A .drv file that differs from the C++ one in a single byte hashes differently, produces a different store path, and is therefore wrong even if it looks plausible. That standard is what makes the project tractable to work on: every question has an observable right answer, and the C++ implementation is sitting right there to be asked.

This is a working evaluator, not a finished one.

The test-suite tally below was measured by running the suite on this tree. The nixpkgs parser and derivation figures were reported by the commits that achieved them and have not been re-measured since; treat them as the high-water mark they were, not as a claim about today.

Parser

A hand-written Go lexer and recursive-descent parser is the only parser. It was validated by parsing all 41292 .nix files in nixpkgs and comparing the resulting AST against nix-instantiate --parse, via the ast-compare tool in gonix/cmd/ast-compare. 41259 files (99.9%) match exactly; the 33 that differ do so only because the Go parser collapses adjacent string literals where the C++ one does not, which is semantically invisible.

Evaluator

Lazy, with thunks, blackholing, and the same displacement-based environment lookup the C++ evaluator uses. Around 108 builtins are implemented. Positions are kept out-of-band in a PosTable and resolved only on error paths, so normal evaluation pays nothing for them.

Derivations

54 of 54 tested nixpkgs packages — including gcc, python3, firefox, systemd, docker, rustc and clang — produce .drv files byte-identical to those from C++ Nix. This covers input-addressed and fixed-output derivations, __structuredAttrs, the full hashDerivationModulo() recursion, and string-context propagation.

Store and IFD

Import-from-derivation works by speaking the Nix daemon's worker protocol over its Unix socket, so builds and substitution are delegated to the real daemon rather than reimplemented. builtins.fetchTarball() and builtins.fetchurl() download in Go and register the result with the daemon.

Builder

gonix/builder runs derivation builders in a Linux sandbox — user, mount, PID, UTS and IPC namespaces, bind mounts, and pivot_root(2) — as a close port of Lix's launch-builder-linux.cc. Seccomp filtering is included; see CAVEATS for how it differs.

Test suite

Of the 188 cases in Lix's own tests/functional2/lang suite that are in scope, 185 pass and 3 fail. The three are named in CAVEATS, along with the other currently-failing tests.

gonix/eval/PLAN.md for the evaluator's implementation plan and a cumulative list of semantics fixed, gonix/eval/PARSER.md for the parser, WASM_PLAN.md for the abandoned WASM design.

Note that PLAN.md records a test tally that no longer matches what the suite actually reports; the numbers under STATUS above were measured, and should be believed over it.

The first commit of this repository is a single squashed import of the Lix tree at upstream commit 8ab7547a7 (2026-05-04), with roughly 19000 commits of Lix history deliberately omitted. That history is complete and authoritative at https://git.lix.systems/lix-project/lix, and carrying a second copy of it here cost 70 MB in the published git bundle against 3 MB without it.

The practical consequence is that this repository shares no commit with upstream and cannot be merged with it. Moving to a newer Lix means redoing the import and replaying the gonix/ commits on top, which is what the fork did in the first place.

Profpatsch

Lix, and therefore the C++ portion of this tree, is the work of the Lix project and its contributors.

Three lang-suite cases fail as of this writing. They are named here rather than summarised because a count alone invites the assumption that they are cosmetic.

path-string-interpolation

Path interpolation concatenates in the wrong place: the base path is appended to the interpolated segment instead of the segment being appended to the base, so ./${x} under /pwd yields /pwd/pwdfoo where Nix yields /pwd/foo. A real bug in path coercion, not a missing feature.

import

Fails with "undefined variable 'range'", because the case needs the test harness to supply extra files that it does not currently supply. A harness gap rather than an evaluator gap.

nul_bytes-eval-depr

Expects evaluation to fail on NUL bytes in a string; evaluation succeeds instead. The deprecation check is simply not implemented.

Two import() unit tests (TestImport_RelativeBasePath, TestImport_NestedRelativeBasePath) and two ast-compare subtests (concat_strings, indented_string) also fail. go test ./... therefore does not come back green, and is not expected to until these are fixed.

Lix maps the build to an unprivileged uid (1000) inside the sandbox namespace. gonix maps it to uid 0 instead. This is not a shortcut taken for convenience: Lix reaches uid 1000 via a setns(2) dance on CLONE_NEWUSER that requires the process to be single-threaded, and a Go program with a running runtime never is.

The divergence is correctness-neutral for build outputs, since Nix canonicalises ownership on the result, but it does mean a builder that inspects its own uid, or that relies on some operation failing for an unprivileged user, will behave differently here. gonix/builder/spawn_linux.go documents the mechanism and the alternatives at length.

Lix delegates syscall filtering to libseccomp. gonix is built with CGO_ENABLED=0 and cannot link it, so gonix/builder/bpf compiles the classic-BPF program itself, reproducing Lix's allowlist from libstore/platform/linux.cc.

It is checked against a reference evaluator by structural routing tests, exhaustive per-syscall comparison, and a differential fuzz target run for millions of executions with no divergence, plus a live-kernel test confirming that a setuid chmod(2) is denied while a benign one succeeds. That is a good deal of evidence, but it remains a reimplementation of a security-relevant component, and should be read as one.

fetchTarball() handles gzip and bzip2 only; xz is not supported. Decompressed output is capped at 1 GiB. PAX global header entries in tar streams are skipped.

builtins.parseFlakeRef() and builtins.flakeRefToString() exist and parse the github, gitlab, sourcehut and indirect schemes, but flake evaluation is not implemented and is not planned.

gonix began by compiling Lix's C++ evaluator to WebAssembly and driving it from Go through wazero, with the Go side owning only the store. That worked, and then the native Go parser replaced it entirely, removing a 7.5-second startup cost and a codec round-trip on every import().

What remains of that era is still in the tree and is not on any live code path: gonix/wasm, gonix/codec, gonix/cmd/eval, the wasm-eval-only=true Meson cross-build, and the C++ files under lix/wasm-shims and lix/lib*/*-wasm.cc. It is kept because it is a working demonstration that the C++ evaluator can be run under wazero at all, including a full Go implementation of the Emscripten exception-handling ABI, which was the hard part and is not written down anywhere else. Do not mistake it for infrastructure. WASM_PLAN.md describes the design.

The doc comment on primDerivationStrict() in gonix/eval/builtins.go still claims that __structuredAttrs and fixed-output derivations are unsupported. Both were implemented afterwards and the comment was not updated; the code below it is authoritative.

LGPL-2.1-or-later, inherited from Lix. See COPYING.