gonix
A Nix language evaluator and derivation builder written in Go, inside a fork of Lix.
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.
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.
- .github/ 2.9 KiB · 1 dirs, 5 files
- bench/ 12.0 KiB · 4 files
- contrib/ 4.7 KiB · 1 dirs, 5 files
- doc/ 885.7 KiB · 20 dirs, 300 files
- gonix/ 944.9 KiB · 18 dirs, 101 filesThe Go implementation: evaluator, store, sandboxed builder. Everything else in this repository is Lix's C++.
- gonix/eval/ 594.1 KiB · 5 dirs, 39 filesLexer, parser, lazy evaluator, ~108 builtins, and the store-path and derivation hashing that must match C++ Nix byte for byte.
- gonix/builder/ 187.7 KiB · 4 dirs, 35 filesRuns derivation builders in a Linux namespace sandbox, with a pure-Go seccomp
cBPFcompiler inbpf/. - lix/ 3.7 MiB · 42 dirs, 963 files
- maintainers/ 10.7 KiB · 9 files
- meson/ 16.3 KiB · 1 dirs, 7 files
- misc/ 37.3 KiB · 7 dirs, 24 files
- nix-support/ 940 B · 2 files
- perl/ 32.1 KiB · 2 dirs, 12 files
- releng/ 62.3 KiB · 2 dirs, 20 files
- scripts/ 11.8 KiB · 6 files
- subprojects/ 144.0 KiB · 6 dirs, 51 files
- tests/ 1.5 MiB · 214 dirs, 1373 files
- .clang-format 1.3 KiB
- .clang-tidy 1.2 KiB
- .dir-locals.el 598 B
- .editorconfig 765 B
- .envrc 230 B
- .gitignore 1.0 KiB
- .mailmap 219 B
- .source-forge 610 B
- .this-is-lix 163 B
- CONTRIBUTING.md 2.6 KiB
- COPYING 25.8 KiB
- CPP_EXCEPTION_TEST_PLAN.md 6.2 KiB
- Cargo.lock 2.1 KiB
- Cargo.toml 106 B
- OWNERS 458 B
- WASM_PLAN.md 23.3 KiB
- default.nix 374 B
- docker.nix 11.3 KiB
- flake.lock 4.1 KiB
- flake.nix 22.7 KiB
- gonix.7 8.8 KiB
- justfile 2.0 KiB
- meson.build 29.2 KiB
- meson.options 4.1 KiB
- package.nix 27.6 KiB
- pyproject.toml 6.5 KiB
- shell.nix 372 B
- treefmt.toml 404 B
- version.json 90 B
- wasm-cross.ini 2.2 KiB
gonix(7)
NAME
gonix - a Nix language evaluator and derivation builder written in Go
DESCRIPTION
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.
STATUS
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,rustcandclang— 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.
SEE ALSO
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.
HISTORY
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.
AUTHORS
Profpatsch
Lix, and therefore the C++ portion of this tree, is the work of the Lix project and its contributors.
CAVEATS
Known test failures
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.
Build sandbox runs as uid 0
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.
Seccomp is a hand-written BPF compiler
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.
Fetcher limits
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.
Flakes are not supported
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.
Vestigial WASM code
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.
Stale comments
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.
LICENSE
LGPL-2.1-or-later, inherited from Lix. See COPYING.