Nix-build in under 100 lines

undeveloper1 pts0 comments

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...

path string json derivation store return

Related Articles