1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
|
.Dd September 6, 2026
.Dt GONIX 7
.Os
.Sh NAME
.Nm gonix
.Nd a Nix language evaluator and derivation builder written in Go
.Sh DESCRIPTION
.Nm
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
.Pa gonix/
inside a fork of Lix, whose C++ source makes up the rest of this tree.
.Pp
The C++ tree is here as a
.Em reference implementation ,
not as a dependency.
Nothing in
.Pa 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
.Dq 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.
.Pp
The goal is byte-identical output, not approximate compatibility.
A
.Pa .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.
.Sh STATUS
This is a working evaluator, not a finished one.
.Pp
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.
.Bl -tag -width Ds
.It Parser
A hand-written Go lexer and recursive-descent parser is the only parser.
It was validated by parsing all 41292
.Pa .nix
files in nixpkgs and comparing the resulting AST against
.Ic nix-instantiate --parse ,
via the
.Ic ast-compare
tool in
.Pa 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.
.It 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
.Vt PosTable
and resolved only on error paths, so normal evaluation pays nothing for
them.
.It Derivations
54 of 54 tested nixpkgs packages — including
.Li gcc ,
.Li python3 ,
.Li firefox ,
.Li systemd ,
.Li docker ,
.Li rustc
and
.Li clang
— produce
.Pa .drv
files byte-identical to those from C++ Nix.
This covers input-addressed and fixed-output derivations,
.Li __structuredAttrs ,
the full
.Fn hashDerivationModulo
recursion, and string-context propagation.
.It 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.
.Fn builtins.fetchTarball
and
.Fn builtins.fetchurl
download in Go and register the result with the daemon.
.It Builder
.Pa gonix/builder
runs derivation builders in a Linux sandbox — user, mount, PID, UTS and
IPC namespaces, bind mounts, and
.Xr pivot_root 2
— as a close port of Lix's
.Pa launch-builder-linux.cc .
Seccomp filtering is included; see
.Sx CAVEATS
for how it differs.
.It Test suite
Of the 188 cases in Lix's own
.Pa tests/functional2/lang
suite that are in scope, 185 pass and 3 fail.
The three are named in
.Sx CAVEATS ,
along with the other currently-failing tests.
.El
.Sh SEE ALSO
.Pa gonix/eval/PLAN.md
for the evaluator's implementation plan and a cumulative list of
semantics fixed,
.Pa gonix/eval/PARSER.md
for the parser,
.Pa WASM_PLAN.md
for the abandoned WASM design.
.Pp
Note that
.Pa PLAN.md
records a test tally that no longer matches what the suite actually
reports; the numbers under
.Sx STATUS
above were measured, and should be believed over it.
.Sh HISTORY
The first commit of this repository is a single squashed import of the
Lix tree at upstream commit
.Li 8ab7547a7
.Pq 2026-05-04 ,
with roughly 19000 commits of Lix history deliberately omitted.
That history is complete and authoritative at
.Lk 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.
.Pp
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
.Pa gonix/
commits on top, which is what the fork did in the first place.
.Sh AUTHORS
.An Profpatsch
.Pp
Lix, and therefore the C++ portion of this tree, is the work of the Lix
project and its contributors.
.Sh CAVEATS
.Ss 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.
.Bl -tag -width Ds
.It Li 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
.Li ./${x}
under
.Pa /pwd
yields
.Pa /pwd/pwdfoo
where Nix yields
.Pa /pwd/foo .
A real bug in path coercion, not a missing feature.
.It Li import
Fails with
.Dq undefined variable Sq 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.
.It Li nul_bytes-eval-depr
Expects evaluation to fail on NUL bytes in a string; evaluation
succeeds instead.
The deprecation check is simply not implemented.
.El
.Pp
Two
.Fn import
unit tests
.Pq Li TestImport_RelativeBasePath , Li TestImport_NestedRelativeBasePath
and two
.Ic ast-compare
subtests
.Pq Li concat_strings , Li indented_string
also fail.
.Ic go test ./...
therefore does not come back green, and is not expected to until these
are fixed.
.Ss Build sandbox runs as uid 0
Lix maps the build to an unprivileged uid
.Pq 1000
inside the sandbox namespace.
.Nm
maps it to uid 0 instead.
This is not a shortcut taken for convenience: Lix reaches uid 1000 via a
.Xr setns 2
dance on
.Dv CLONE_NEWUSER
that requires the process to be single-threaded, and a Go program with a
running runtime never is.
.Pp
The divergence is correctness-neutral for build
.Em 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.
.Pa gonix/builder/spawn_linux.go
documents the mechanism and the alternatives at length.
.Ss Seccomp is a hand-written BPF compiler
Lix delegates syscall filtering to
.Sy libseccomp .
.Nm
is built with
.Ev CGO_ENABLED Ns = Ns 0
and cannot link it, so
.Pa gonix/builder/bpf
compiles the classic-BPF program itself, reproducing Lix's allowlist from
.Pa libstore/platform/linux.cc .
.Pp
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
.Xr 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.
.Ss Fetcher limits
.Fn 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.
.Ss Flakes are not supported
.Fn builtins.parseFlakeRef
and
.Fn builtins.flakeRefToString
exist and parse the
.Li github ,
.Li gitlab ,
.Li sourcehut
and indirect schemes, but flake
.Em evaluation
is not implemented and is not planned.
.Ss Vestigial WASM code
.Nm
began by compiling Lix's C++ evaluator to WebAssembly and driving it from
Go through
.Sy 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
.Fn import .
.Pp
What remains of that era is still in the tree and is
.Em not
on any live code path:
.Pa gonix/wasm ,
.Pa gonix/codec ,
.Pa gonix/cmd/eval ,
the
.Li wasm-eval-only=true
Meson cross-build, and the C++ files under
.Pa lix/wasm-shims
and
.Pa 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.
.Pa WASM_PLAN.md
describes the design.
.Ss Stale comments
The doc comment on
.Fn primDerivationStrict
in
.Pa gonix/eval/builtins.go
still claims that
.Li __structuredAttrs
and fixed-output derivations are unsupported.
Both were implemented afterwards and the comment was not updated; the
code below it is authoritative.
.Sh LICENSE
LGPL-2.1-or-later, inherited from Lix.
See
.Pa COPYING .
|