nix-build in under 100 lines | Farid Zakaria’s Blog
nix-build in under 100 lines
Published 2026-06-21 on<br>Farid Zakaria's Blog
I’ve said before that Nix is a lie, and that underneath the ceremony Nix is really just an Input Output Machine.
The nix daemon feels like a black box. You type nix build and somewhere behind a Unix socket a privileged process does inscrutable things, and out the other end pops a path in /nix/store. 🪄
What if I told you the part everyone thinks is magic and that turning a derivation into a store path is nearly an exec ?
Let’s reimplement nix-build in under 100 lines of Go.
First off, What is a derivation, really?
A derivation (.drv) is just a build plan. Let’s instantiate the most boring one imaginable.
# hello.nix<br>derivation {<br>name = "hello";<br>system = builtins.currentSystem;<br>builder = "/bin/sh";<br>args = [ "-c" "echo 'Hello World' > $out" ];
$ nix derivation show $(nix-instantiate hello.nix)
"derivations": {<br>"gifgxsqfsjg8pxna1kv0nbzz1zvivs0b-hello.drv": {<br>"args": [<br>"-c",<br>"echo 'Hello World' > $out"<br>],<br>"builder": "/bin/sh",<br>"env": {<br>"builder": "/bin/sh",<br>"name": "hello",<br>"out": "/nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello",<br>"system": "x86_64-linux"<br>},<br>"inputs": { "drvs": {}, "srcs": [] },<br>"name": "hello",<br>"outputs": {<br>"out": {<br>"path": "ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello"<br>},<br>"system": "x86_64-linux",<br>"version": 4<br>},<br>"version": 4
That’s the whole thing. A program to run (builder + args), an environment (env), the outputs it must produce, and the other derivations it depends on (inputDrvs), which in this case is empty. No magic 🪄.
So “realising” a derivation is just four steps:
Realise its inputDrvs first recursively. This is the build graph.
Scrub the environment down to a known set of variables.
Set $out to the store path the build must create.
exec the builder and check it produced $out.
Here is the whole program in Go, in less than 100 lines (excluding comments 😉).<br>You can find the source here.
Note<br>I cheated a tiny bit and rather than writing a parser for Nix’s ATerm format, I leveraged nix show derivation to get the JSON equivalent.
build.go<br>package main
import (<br>"encoding/json"<br>"fmt"<br>"os"<br>"os/exec"<br>"strings"
const store = "/nix/store"
type drv struct {<br>Args []string `json:"args"`<br>Builder string `json:"builder"`<br>Env map[string]string `json:"env"`<br>Inputs struct {<br>Drvs map[string]any `json:"drvs"`<br>} `json:"inputs"`<br>Outputs map[string]struct {<br>Path string `json:"path"`<br>} `json:"outputs"`
func exists(path string) bool { _, err := os.Stat(path); return err == nil }
// storePath makes a store path absolute; Nix's JSON uses bare basenames.<br>func storePath(p string) string {<br>if strings.HasPrefix(p, "/") {<br>return p<br>return store + "/" + p
// loadDrv shells out to Nix to turn a .drv into JSON, then decodes it.<br>func loadDrv(path string) (error, drv) {<br>data, err := exec.Command("nix", "--extra-experimental-features", "nix-command",<br>"derivation", "show", path).Output()<br>if err != nil {<br>return err, drv{}<br>var doc struct {<br>Derivations map[string]drv `json:"derivations"`<br>if err := json.Unmarshal(data, &doc); err != nil {<br>return err, drv{}<br>for _, d := range doc.Derivations {<br>return nil, d // exactly one entry: the derivation we asked for<br>panic("no derivation found for " + path)
// realise ensures the derivation's output exists, building its inputs first,<br>// and returns the default output's store path.<br>func realise(path string) (error, string) {<br>err, d := loadDrv(path)<br>if err != nil {<br>return err, ""<br>out := storePath(d.Outputs["out"].Path)<br>if exists(out) {<br>return nil, out // already built (this also memoises shared dependencies)<br>for dep := range d.Inputs.Drvs {<br>realise(storePath(dep)) // recurse: dependencies before dependents
fmt.Fprintln(os.Stderr, "building", out)<br>tmp, err := os.MkdirTemp("", "simple-nix-")<br>if (err != nil) {<br>return err, ""<br>defer os.RemoveAll(tmp)
// The build's entire environment: a few fixed vars, the derivation's own<br>// attributes, and one var per output (this is where $out comes from).<br>// These fixed variables and their values are specified by the Nix manual:<br>// https://github.com/NixOS/nix/blob/f8bb823a23bf6d62f4c8feb792a77702d7a49fe1/doc/manual/source/store/building.md?plain=1#L154<br>env := map[string]string{<br>"PATH": "/path-not-set", "HOME": "/homeless-shelter",<br>"NIX_STORE": store, "NIX_BUILD_TOP": tmp,<br>"TMPDIR": tmp, "TEMPDIR": tmp, "TMP": tmp, "TEMP": tmp,<br>for k, v := range d.Env {<br>env[k] = v<br>for name, o := range d.Outputs {<br>env[name] = storePath(o.Path)
cmd := exec.Command(d.Builder, d.Args...)<br>cmd.Dir, cmd.Stdout, cmd.Stderr = tmp, os.Stderr, os.Stderr<br>for k, v := range env {<br>cmd.Env = append(cmd.Env, k+"="+v)
if err := cmd.Run(); err != nil {<br>return err, ""
if !exists(out) {<br>panic(fmt.Sprintf("builder did not produce %s", out))<br>return nil, out
func main() {<br>if len(os.Args) 2 {<br>fmt.Fprintln(os.Stderr, "usage: simple-nix ...")<br>os.Exit(2)<br>for _, arg := range os.Args[1:] {<br>fmt.Println(realise(arg))
That’s it. Does it work?
$ go...