Skip to content

Building Software with Nix

Building Software with Nix: Derivations and Overlays

Section titled “Building Software with Nix: Derivations and Overlays”

This section focuses on how software actually gets built in Nix.

The goal is not to memorize helpers, but to understand:

  • what a derivation is
  • how sources enter the system
  • how build helpers reduce boilerplate
  • how overlays let you modify builds safely
  • and where common pitfalls live

A derivation is a pure description of a build, not the build itself.

Conceptually:

(inputs) + (build instructions) → derivation → build → /nix/store/…

In Nix code, a derivation is just an attrset produced by mkDerivation (or helpers built on top of it).

Key points:

  • Evaluation creates a derivation (cheap, fast)
  • Building executes it (expensive, sandboxed)
  • Derivations are immutable and content-addressed

Most build helpers ultimately call:

pkgs.stdenv.mkDerivation

A minimal example:

#include <stdio.h>
int main(int argc, char *argv[]) {
const char *name = (argc > 1) ? argv[1] : "world";
printf("hello, %s!\n", name);
return 0;
}
pkgs.stdenv.mkDerivation {
pname = "hello-c";
version = "1.0";
src = ./src;
nativeBuildInputs = [ pkgs.gcc ];
buildPhase = ''
gcc hello.c -o hello
'';
installPhase = ''
mkdir -p $out/bin
cp hello $out/bin/
'';
meta = {
description = "Simple hello world C program";
};
}

Important attributes you’ll see constantly:

  • pname, version
  • src
  • buildInputs
  • nativeBuildInputs
  • phases / installPhase
  • meta

Fetchers are fixed-output derivations: they download something and verify its hash.

  • fetchFromGitHub
  • fetchgit
  • fetchurl
  • fetchzip

Example:

src = pkgs.fetchFromGitHub {
owner = "rakyll";
repo = "hey";
rev = "v0.1.4";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};

Key rules:

  • Hashes are mandatory (reproducibility)
  • Hash mismatch errors are part of the workflow
  • Fetchers run at build time, not eval time

Most real packages do not use mkDerivation directly.

Instead, nixpkgs provides build helpers that encode conventions.

  • buildGoModule
  • buildPythonPackage
  • buildRustPackage
  • buildNpmPackage
  • cmake, meson, autotools helpers

Example (Go):

pkgs.buildGoModule {
pname = "hey";
version = "1.0";
src = pkgs.fetchFromGitHub {
owner = "rakyll";
repo = "hey";
rev = "v0.1.4";
hash = pkgs.lib.fakeHash;
};
vendorHash = pkgs.lib.fakeHash;
}

Helpers:

  • define standard phases
  • set environment variables
  • handle language-specific tooling
  • reduce copy/paste

Not everything is “compile a big project”.

Nix excels at small glue tools.

Best for small CLI tools:

pkgs.writeShellApplication {
name = "hello";
text = ''
echo "hello from nix"
'';
}

Features:

  • wraps runtime dependencies automatically
  • clean, declarative
  • preferred over raw mkDerivation for scripts

For generating config or data:

pkgs.writeText "config.txt" ''
port=8080
''

These return store paths, not strings.


This is a daily source of confusion.

  • tools needed to build
  • run on the build machine
  • libraries or tools needed at runtime

Rule of thumb:

  • compilers, code generators → nativeBuildInputs
  • shared libraries, interpreters → buildInputs

7. Wrapping Programs and Runtime Environment

Section titled “7. Wrapping Programs and Runtime Environment”

Many programs expect:

  • PATH
  • PYTHONPATH
  • SSL_CERT_FILE
  • etc.

Nix does not provide these implicitly.

wrapProgram $out/bin/mytool \
--prefix PATH : ${pkgs.python3}/bin

This creates a wrapper script that:

  • sets environment variables
  • then execs the real program

This is how Nix makes programs self-contained.


Overlays are functions that transform the package set.

Signature:

self: super: { }
  • super → original package set
  • self → package set with your overlay applied

Example:

self: super: {
hello = super.hello.overrideAttrs (_: {
pname = "hello-custom";
});
}

Overlays are used to:

  • patch packages
  • change versions
  • inject custom builds
  • experiment without forking nixpkgs

  • override → change function arguments
  • overrideAttrs → change derivation attributes

Mental model:

(args) → function → derivation
↑ ↑
override overrideAttrs

If you remember nothing else, remember that diagram.


callPackage automatically fills function arguments from pkgs.

Instead of:

import ./pkg.nix { inherit pkgs; }

You write:

pkgs.callPackage ./pkg.nix {}

This:

  • reduces boilerplate
  • makes overrides easier
  • is the standard nixpkgs style

These come up constantly:

  • Using strings where paths are required
  • Forgetting runtime dependencies
  • Assuming /usr/bin/env exists
  • Confusing eval-time vs build-time
  • Overriding the wrong layer (args vs attrs)
  • Not checking meta.platforms

None of these are “advanced” mistakes — they’re normal.


12. How nixpkgs Is Structured (Big Picture)

Section titled “12. How nixpkgs Is Structured (Big Picture)”

Understanding this helps you read code:

  • pkgs/top-level/ → package set assembly
  • pkgs/build-support/ → helpers
  • pkgs/development/ → language ecosystems
  • pkgs/tools/ → CLI tools

Most “magic” is just layered functions.


  • Use fetchFromGitHub with a fake hash
  • Build once
  • Replace the hash from the error

Given three projects:

  • a Go CLI
  • a Python library
  • a shell script

Decide:

  • which build helper to use
  • or whether to avoid mkDerivation entirely

Explain why.


  • Take an existing package
  • Modify one argument via override
  • Modify one derivation attribute via overrideAttrs
  • Verify which changes require rebuilding dependents

  • A derivation is a description, not an action
  • Fetchers define trust boundaries
  • Helpers encode conventions, not magic
  • Overlays are transformations, not patches
  • Evaluation is cheap; builds are expensive

If flakes define what your project is, derivations define how your software exists.

Once you’re comfortable here, you can build almost anything with Nix.