nix-build in under 100 lines

Lobsters Hottest Tools

Summary

The article demystifies the Nix build process by reimplementing nix-build in under 100 lines of Go, showing that turning a derivation into a store path is essentially an exec.

<p><a href="https://lobste.rs/s/gig3cr/nix_build_under_100_lines">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 06/22/26, 01:29 AM

# nix-build in under 100 lines Source: [https://fzakaria.com/2026/06/21/nix-build-in-under-100-lines](https://fzakaria.com/2026/06/21/nix-build-in-under-100-lines) Published 2026\-06\-21 on[Farid Zakaria's Blog](https://fzakaria.com/) I’ve said before that[Nix is a lie](https://fzakaria.com/2026/03/07/nix-is-a-lie-and-that-s-ok), and that underneath the ceremony Nix is really just an[Input Output Machine](https://fzakaria.com/2026/06/05/the-guix-nix-abomination-leveraging-guix-derivations-in-nix)\. 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 derivation { name = "hello"; system = builtins.currentSystem; builder = "/bin/sh"; args = [ "-c" "echo 'Hello World' > $out" ]; } ``` ``` $ nix derivation show $(nix-instantiate hello.nix) ``` ``` { "derivations": { "gifgxsqfsjg8pxna1kv0nbzz1zvivs0b-hello.drv": { "args": [ "-c", "echo 'Hello World' > $out" ], "builder": "/bin/sh", "env": { "builder": "/bin/sh", "name": "hello", "out": "/nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello", "system": "x86_64-linux" }, "inputs": { "drvs": {}, "srcs": [] }, "name": "hello", "outputs": { "out": { "path": "ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello" } }, "system": "x86_64-linux", "version": 4 } }, "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: 1. Realise its`inputDrvs`first recursively\. This*is*the build graph\. 2. Scrub the environment down to a known set of variables\. 3. Set`$out`to the store path the build must create\. 4. `exec`the builder and check it produced`$out`\. Here is the whole program in Go, in less than 100 lines \(excluding comments 😉\)\. You can find the source[here](https://gist.github.com/fzakaria/1be0657cc5f10df5e45d4ff1574b0273)\. > **Note**I*cheated*a tiny bit and rather than writing a parser for Nix’s[ATerm format](https://nix.dev/manual/nix/2.25/protocols/derivation-aterm), I leveraged`nix show derivation`to get the JSON equivalent\. build\.go``` package main import ( "encoding/json" "fmt" "os" "os/exec" "strings" ) const store = "/nix/store" type drv struct { Args []string `json:"args"` Builder string `json:"builder"` Env map[string]string `json:"env"` Inputs struct { Drvs map[string]any `json:"drvs"` } `json:"inputs"` Outputs map[string]struct { Path string `json:"path"` } `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. func storePath(p string) string { if strings.HasPrefix(p, "/") { return p } return store + "/" + p } // loadDrv shells out to Nix to turn a .drv into JSON, then decodes it. func loadDrv(path string) (error, drv) { data, err := exec.Command("nix", "--extra-experimental-features", "nix-command", "derivation", "show", path).Output() if err != nil { return err, drv{} } var doc struct { Derivations map[string]drv `json:"derivations"` } if err := json.Unmarshal(data, &doc); err != nil { return err, drv{} } for _, d := range doc.Derivations { return nil, d // exactly one entry: the derivation we asked for } panic("no derivation found for " + path) } // realise ensures the derivation's output exists, building its inputs first, // and returns the default output's store path. func realise(path string) (error, string) { err, d := loadDrv(path) if err != nil { return err, "" } out := storePath(d.Outputs["out"].Path) if exists(out) { return nil, out // already built (this also memoises shared dependencies) } for dep := range d.Inputs.Drvs { realise(storePath(dep)) // recurse: dependencies before dependents } fmt.Fprintln(os.Stderr, "building", out) tmp, err := os.MkdirTemp("", "simple-nix-") if (err != nil) { return err, "" } defer os.RemoveAll(tmp) // The build's entire environment: a few fixed vars, the derivation's own // attributes, and one var per output (this is where $out comes from). // These fixed variables and their values are specified by the Nix manual: // https://github.com/NixOS/nix/blob/f8bb823a23bf6d62f4c8feb792a77702d7a49fe1/doc/manual/source/store/building.md?plain=1#L154 env := map[string]string{ "PATH": "/path-not-set", "HOME": "/homeless-shelter", "NIX_STORE": store, "NIX_BUILD_TOP": tmp, "TMPDIR": tmp, "TEMPDIR": tmp, "TMP": tmp, "TEMP": tmp, } for k, v := range d.Env { env[k] = v } for name, o := range d.Outputs { env[name] = storePath(o.Path) } cmd := exec.Command(d.Builder, d.Args...) cmd.Dir, cmd.Stdout, cmd.Stderr = tmp, os.Stderr, os.Stderr for k, v := range env { cmd.Env = append(cmd.Env, k+"="+v) } if err := cmd.Run(); err != nil { return err, "" } if !exists(out) { panic(fmt.Sprintf("builder did not produce %s", out)) } return nil, out } func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "usage: simple-nix <file.drv> ...") os.Exit(2) } for _, arg := range os.Args[1:] { fmt.Println(realise(arg)) } } ``` That’s it\. Does it work? ``` $ go build -o simple-nix . $ ./simple-nix $(nix-instantiate hello.nix) building /nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello /nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello $ cat /nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello Hello World ``` We can even build a real\-world derivation\. ``` $ ./simple-nix $(nix eval nixpkgs#hello --raw) Using versionCheckHook Running phase: unpackPhase unpacking source archive /nix/store/wj7phsmi7ncidl8k00p489krqss7n9sd-hello-2.12.3.tar.gz source root is hello-2.12.3 setting SOURCE_DATE_EPOCH to timestamp 1773804383 of file "hello-2.12.3/ChangeLog" Running phase: patchPhase Running phase: updateAutotoolsGnuConfigScriptsPhase Updating Autotools / GNU config script to a newer upstream version: ./build-aux/config.sub Updating Autotools / GNU config script to a newer upstream version: ./build-aux/config.guess ... ``` So what*is*missing? Quite a lot, honestly**but**none of it is the part that turns a derivation into a path\. - **Sandboxing**: Nix runs the builder in a mount/network/PID namespaces for security and hermiticity\. - **The database**: Nix records every valid path and its references in a SQLite db\. We just check if the file exists\. - **Substitution**: Nix asks a binary cache if the derivation was alreay built\. - **Everything else**: Multiple output paths, support for fixed\-output derivations \(`fetchurl`\), garbage collection, etc\. The beauty of Nix is the derivation is a pure function\. Getting the store path is not magic\. It’s`exec`with a clean environment\. Everything else,*mostly*is bookkeeping and security\. --- [Improve this page @ 212fbd5](https://github.com/fzakaria/fzakaria.com/tree/212fbd51833fe59017e627293f89671335b0e76e/_posts/2026-06-21-nix-build-in-under-100-lines.md) The content for this site is[CC\-BY\-SA](https://creativecommons.org/licenses/by-sa/2.0/)\.

Similar Articles

A Nix store is three functions

Lobsters Hottest

The article explains that a Nix store can be implemented using only three simple HTTP functions, enabling static file servers like GitHub Pages to act as Nix binary caches.

Building (systems) software with Nix

Lobsters Hottest

A blog post discussing how Nix helps solve dependency and reproducibility issues in building systems software, particularly for fast-evolving subsystems like BPF and io_uring.

Nix for Haskell: Static Builds

Lobsters Hottest

This tutorial explains how to create statically-linked executables for Haskell projects using Nix, covering configuration of GHC for static builds and integration with Docker.

The postmodern build system

Lobsters Hottest

A blog post exploring the design of an ideal 'postmodern' build system that prioritizes trustworthy incremental builds, maximized computation reuse, and distributed builds, using Nix as a reference point.