nix-playgraound/notes/004-packaging.md

4.0 KiB

Packaging

This note covers 02-package/, which builds a shell script into a Nix store path using stdenv.mkDerivation.


1. What nix build Does

When you run nix build inside 02-package/:

  1. Nix evaluates flake.nix and produces a .drv file (the build recipe).
  2. Nix realises the .drv: it runs the install phase inside a sandbox with no network and no access to files outside the declared inputs.
  3. The output lands in /nix/store/<hash>-greet-0.1.0/.
  4. A ./result symlink is created pointing to that store path.

You can then run ./result/bin/greet directly, or use nix run which does step 1 through 3 and then executes the apps.<system>.default.program path.


2. stdenv.mkDerivation in Detail

stdenv.mkDerivation is the main builder in nixpkgs. It wraps the low-level derivation builtin with a phased build system. The default phases, in order:

  1. unpackPhase: extracts src (tarball, git checkout, or copied path).
  2. patchPhase: applies patches from the patches list.
  3. configurePhase: runs ./configure or cmake or similar.
  4. buildPhase: runs make or equivalent.
  5. installPhase: copies outputs into $out.
  6. fixupPhase: strips binaries, patches RPATHs, shrink-wraps references.

For a simple script, most of these are irrelevant. The 02-package example disables unpack and build with dontUnpack = true and dontBuild = true, then writes a custom installPhase.


3. Key Concepts in the Example

$out: every derivation has at least one output. $out is the default output path. The build must place files under $out or the derivation produces nothing.

substituteInPlace: a nixpkgs helper that does in-place string replacement. The example uses it to rewrite the shebang from #!/usr/bin/env bash to a store-path bash. This matters because /usr/bin/env may not exist on a pure NixOS system, and even where it does, it would pick up whichever bash happens to be on $PATH at runtime rather than the pinned one.

src = ./.: copies the entire flake directory into the store before the build starts. Changes to any file in the directory change the hash, which triggers a rebuild. For real projects you'd use lib.fileset or lib.cleanSource to exclude irrelevant files (editor backups, .git/, etc.).

apps.<system>.default: the nix run command looks for this attribute. Its program field must be an absolute store path to an executable.


4. The Build Sandbox

By default, Nix builds run inside a sandbox:

  • No network access (except fixed-output derivations).
  • No access to the host filesystem beyond explicitly declared inputs.
  • No access to environment variables from the host.
  • A restricted /tmp for scratch space.

This is what guarantees reproducibility: the build can only use what it declared.

If a build needs to download something (a source tarball, a Go module cache), it must be a fixed-output derivation. You provide the expected hash, and Nix verifies the output matches. See builtins.fetchurl, pkgs.fetchFromGitHub, and similar.


5. Commands to Try

cd 02-package

nix build                          # build and create ./result symlink
./result/bin/greet                 # run directly
./result/bin/greet "nix learner"   # pass an argument

nix run                            # build + run via apps.default
nix run . -- "nix learner"         # pass arguments after --

nix path-info ./result             # show the store path
nix path-info -rS ./result         # show closure size (all transitive deps)
nix derivation show ./result       # inspect the .drv (inputs, env, builder)

6. Beyond Shell Scripts

The same stdenv.mkDerivation pattern scales to compiled languages. nixpkgs provides wrappers:

  • rustPlatform.buildRustPackage for Rust (Cargo).
  • buildGoModule for Go.
  • python3Packages.buildPythonPackage for Python.
  • mkDerivation directly for C/C++ (autotools, cmake, meson).

Each wrapper pre-configures the phases for its ecosystem. You supply src, a lock file hash, and metadata; the wrapper handles configure/build/install.