nixpkgs
the world's largest package collection and how to navigate it.
the world's largest package collection and how to navigate it.
nixpkgs looks like a package repository. nix evaluates it as a nix expression.
that expression returns an attribute set. some attributes are packages, some are helper functions, some are build systems, some are package sets inside package sets. there are old patterns, new patterns, compatibility layers, and enough special cases to make the tree feel larger than the idea behind it.
most package access looks like this:
pkgs.hello
pkgs.curl
pkgs.python313
pkgs.stdenv
pkgs.fetchurl
attribute access. no registry lookup, no hidden package table outside nix. pkgs.hello is a derivation value. evaluating it gives nix enough information to write a .drv file.
on the pinned nixpkgs used by this site:
$ nix eval --raw --inputs-from . nixpkgs#legacyPackages.aarch64-darwin.hello.name
hello-2.12.1
$ nix eval --raw --inputs-from . nixpkgs#legacyPackages.aarch64-darwin.hello.drvPath
/nix/store/5mh0djicybhh26h0m8qciaizc1iwsy83-hello-2.12.1.drv
$ nix eval --raw --inputs-from . nixpkgs#legacyPackages.aarch64-darwin.hello.outPath
/nix/store/39856ps7lpawggvbnph7i3iwmy74dd3h-hello-2.12.1
same derivation model as before. nixpkgs is where most of the package definitions live.
flakes expose the package set through legacyPackages.${system}:
pkgs = nixpkgs.legacyPackages.aarch64-darwin;
that gives you a package set for one platform. pkgs.hello on aarch64-darwin is not the same derivation as pkgs.hello on x86_64-linux. different compiler, different stdenv, different platform hooks.
for this machine:
$ nix eval --raw --inputs-from . nixpkgs#stdenv.hostPlatform.system
aarch64-darwin
the hello derivation records the platform and store paths directly:
{
"system": "aarch64-darwin",
"builder": "/nix/store/5c8hb299k0acbypqw6j9m4znyd6b97cz-bash-5.2p37/bin/bash",
"env": {
"name": "hello-2.12.1",
"pname": "hello",
"version": "2.12.1",
"src": "/nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz",
"stdenv": "/nix/store/bq6j4f1qpdycxviy53fyh2ic39mplwhk-stdenv-darwin",
"out": "/nix/store/39856ps7lpawggvbnph7i3iwmy74dd3h-hello-2.12.1"
}
}
trimmed output from nix derivation show, using this repo's pinned nixpkgs.
legacyPackages does not mean old packages. in the nixpkgs flake, it is the full package set:
nixpkgs.legacyPackages.aarch64-darwin.hello
why not packages?
because packages has special meaning to the flake CLI. take this command:
nix flake show nixpkgs
nix tries to inspect the packages tree so it can print what is inside. that is fine for a small project with ten packages. nixpkgs has far more than that, and many attributes are package sets that contain more package sets. figuring out what to print means forcing too much evaluation.
legacyPackages is treated differently. the CLI can omit the tree instead of walking it. package access still works:
nix build nixpkgs#hello
and in flakes:
pkgs = nixpkgs.legacyPackages.${system};
so when you see legacyPackages, read it as the normal nixpkgs package set exposed through the flake interface. not deprecated packages.
open the package file:
$ nix eval --json --inputs-from . nixpkgs#legacyPackages.aarch64-darwin.hello.meta.position
"/nix/store/6bwyhcqsr8pj2gyvdzsr742v9jf2zn52-source/pkgs/by-name/he/hello/package.nix:47"
the file starts like this:
{
callPackage,
lib,
stdenv,
fetchurl,
nixos,
testers,
versionCheckHook,
hello,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "hello";
version = "2.12.1";
src = fetchurl {
url = "mirror://gnu/hello/hello-${finalAttrs.version}.tar.gz";
hash = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA=";
};
doCheck = true;
})
it is a function with one argument, and that argument is an attribute set.
same function shape as functions. nixpkgs package files do not usually import their dependencies directly. they ask for them by name.
hello asks for stdenv, fetchurl, versionCheckHook, and a few helpers. nixpkgs supplies matching attributes from the package set.
callPackage wires those names together.
roughly:
callPackage ./package.nix { }
it means:
./package.nixpkgsif the package asks for stdenv, callPackage passes pkgs.stdenv. if it asks for fetchurl, it passes pkgs.fetchurl. if it asks for lib, it passes pkgs.lib.
the dependency list stays at the top. the body describes the build.
you can still override an argument:
callPackage ./package.nix {
stdenv = clangStdenv;
}
now the same package function gets a different stdenv. the input changed, so the derivation changes too.
pkgs is self-referential.
pkgs contains hello. hello is produced by calling a function with values from pkgs. some of those values are packages, and those packages were produced from the same pkgs.
a simplified version:
pkgs = fix (self: {
hello = self.callPackage ./hello/package.nix { };
curl = self.callPackage ./curl/package.nix { };
fetchurl = self.callPackage ./build-support/fetchurl { };
})
not real nixpkgs code. self is the final package set. package definitions can refer to other packages in that set.
lazy evaluation makes this usable. nix does not evaluate every package when the set is created. asking for pkgs.hello evaluates the path needed to produce hello; the rest can stay as thunks.
the real top-level file even warns you:
AAAAAASomeThingsFailToEvaluate = throw ''
This pseudo-package is likely not the only part of Nixpkgs that fails to evaluate.
You should not evaluate entire Nixpkgs without measures to handle failing packages.
'';
nixpkgs is too large and too platform-dependent to force naively. you ask for the attribute you need.
the pinned nixpkgs revision here is:
5b5be50345d4113d04ba58c444348849f5585b4a
from the nixos-25.05 branch. nix stores the source under this path:
/nix/store/6bwyhcqsr8pj2gyvdzsr742v9jf2zn52-source
the main directories:
| path | what it is |
|---|---|
pkgs/by-name/ | newer package layout, two-letter prefix directories like he/hello |
pkgs/top-level/all-packages.nix | the old central wiring file for many packages and helpers |
pkgs/build-support/ | fetchers, wrappers, setup helpers, build infrastructure |
pkgs/development/ | compilers, interpreters, libraries, language ecosystems |
pkgs/tools/ | command-line tools grouped by rough purpose |
pkgs/os-specific/ | linux, darwin, bsd, windows-specific packages |
lib/ | pure nix helper functions |
hello lives in the by-name layout:
pkgs/by-name/he/hello/package.nix
older packages still live under category directories and get wired through all-packages.nix. both styles exist. nixpkgs moves slowly because the tree is large and compatibility matters.
nixpkgs also contains NixOS modules:
nixos/modules/
these are not packages. they are configuration modules. a package says "build this program". a NixOS module says "add users, files, services, timers, sockets, kernel modules, firewall rules, and activation scripts to a system configuration".
systemd shows up here.
packages sometimes install upstream unit files under paths like these:
$out/lib/systemd/system/
$out/lib/systemd/user/
installing the package does not enable the service. a NixOS module normally does the integration:
services.postgresql.enable = true;
which eventually contributes to options under these names:
systemd.services
systemd.timers
systemd.sockets
NixOS uses systemd as its init system, and many modules expose systemd concepts directly. wantedBy, after, serviceConfig, socket activation, timers, tmpfiles, credentials, sandboxing. these are part of how the service is described.
there have been discussions about a thinner service abstraction layer, so service definitions could be reused for other backends like launchd, containers, or a different init system. the hard part is deciding what the shared layer can say. ExecStart and environment variables are easy. ordering, readiness, cgroups, credentials, socket activation, user services, and sandboxing are not.
for this chapter, the boundary is enough. nixpkgs packages are mostly derivations. NixOS modules are where services become system configuration. the systemd-heavy parts belong in the NixOS chapter.
packages carry metadata.
meta = {
description = "Program that produces a familiar, friendly greeting";
homepage = "https://www.gnu.org/software/hello/manual/";
license = lib.licenses.gpl3Plus;
maintainers = with lib.maintainers; [ stv0g ];
mainProgram = "hello";
platforms = lib.platforms.all;
};
metadata does not usually affect the build output directly. it affects search, documentation, Hydra jobs, availability checks, and whether nix considers a package installable on a platform.
meta.platforms = lib.platforms.all is why hello is available on this darwin machine. a linux-only package can still exist in the package set, but trying to build it on darwin fails early.
nixpkgs is a package set for a platform. most packages are functions. callPackage fills those function arguments from the package set. lazy evaluation lets that package set refer to itself.
pkgs.hello still ends as a derivation value. instantiate it and you get a .drv. build it and you get an output path in /nix/store.
the callPackage pattern goes deeper into the argument matching and override behavior. overlays and overrides shows how to change the package set without editing nixpkgs itself.