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

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.

Development shells with Nix: four quick examples

Michael Stapelberg

A tutorial demonstrating four ways to set up development shells using Nix, including interactive one-offs, config files, and hermetic Nix Flakes, using GoCV and OpenCV as an example.

How I like to install NixOS (declaratively)

Michael Stapelberg

A guide on declaratively installing NixOS over the network using tools like nixos-anywhere, with an emphasis on managing configuration files under version control.