Navigating nixpkgs
July 30, 2026
Navigating nixpkgs
Goal: Navigate modern nixpkgs package layout—especially pkgs/by-name—and shape a package so it could become an upstream PR (submission optional).
Why it matters
Copy-pasting random default.nix from a blog creates private snowflakes. Knowing where packages live, how they are called, and what reviewers expect makes your flake packages portable and your future contributions reviewable.
Theory 1 — Mental map of nixpkgs (2026)
| Path | Role |
|---|---|
pkgs/by-name/<p>/<pname>/package.nix |
Preferred home for many new single-package expressions |
pkgs/top-level/all-packages.nix |
Historic wiring; less hand-editing for new pkgs |
pkgs/development/… |
Older category tree still holds many packages |
pkgs/applications/… |
Same—legacy category layout |
lib/ |
Nix library functions |
nixos/modules/ |
NixOS modules (not package expressions) |
nixos/tests/ |
nixosTest definitions |
maintainers/maintainer-list.nix |
Maintainer identities |
pkgs/build-support/ |
Fetchers, stdenv pieces, helpers |
by-name rules of thumb
pkgs/by-name/he/hello/package.nix
││ │
││ └─ package directory = pname
└┴─ first two lowercase letters of pname
| Rule | Detail |
|---|---|
| File name | Usually package.nix (not always default.nix) |
| Shard | toLower (substring 0 2 pname) |
| Scope | One package per dir; keep it self-contained |
| Call | Auto-wired for eligible packages—follow current nixpkgs docs if adding upstream |
Your flake can still use callPackage ./pkgs/foo.nix { } without sharding; by-name is the upstream shape.
Theory 2 — Anatomy of a PR-shaped package.nix
{
lib,
stdenv,
fetchFromGitHub,
# only real deps
}:
stdenv.mkDerivation (finalAttrs: {
pname = "cooltool";
version = "1.2.3";
src = fetchFromGitHub {
owner = "acme";
repo = "cooltool";
rev = "v${finalAttrs.version}";
hash = "sha256-…";
};
nativeBuildInputs = [ /* … */ ];
buildInputs = [ /* … */ ];
meta = {
description = "One-line description";
homepage = "https://github.com/acme/cooltool";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ /* your handle if upstream */ ];
platforms = lib.platforms.unix;
mainProgram = "cooltool";
};
})Modern habits
| Habit | Why |
|---|---|
finalAttrs: pattern |
Version/src self-reference without rec pitfalls |
| Explicit function head args | callPackage injects deps |
meta.mainProgram |
nix run nixpkgs#cooltool |
Real license |
Policy and binary caches |
| No unrelated formatting of huge trees | Reviewer happiness |
Tests via passthru / nixosTests |
“Done upstream” bar |
Theory 3 — callPackage and fixed-points
Overlays often do:
Use final.callPackage so deps see other overlay packages.
Why function heads matter
# callPackage binds by argument name
{ stdenv, fetchurl, openssl }: # must match pkgs attributes
stdenv.mkDerivation { … }Unknown args fail; unused args may still be required by callers for override ergonomics—follow local style.
Theory 4 — Reading existing packages as literature
# If you have a nixpkgs checkout:
rg -n 'pname = "ripgrep"' -S pkgs | head
# Query without full clone:
nix edit nixpkgs#ripgrep
# opens the file in $EDITOR when configured
nix eval nixpkgs#ripgrep.meta --json | jq .Read three packages in your primary language:
- Tiny CLI
- One with patches
- One with multiple outputs
Note patterns: hooks, finalAttrs, meta, tests.
What to look for
| Signal | Meaning |
|---|---|
finalAttrs |
Modern self-reference |
passthru.tests |
Linked tests |
updateScript |
nix-update support |
meta.changelog |
Release notes link |
separateDebugInfo |
Debug hygiene |
| Language builder | Ecosystem-native packaging |
Theory 5 — Tests and passthru (awareness)
Many nixpkgs packages expose:
passthru.tests = {
version = testers.testVersion { package = finalAttrs.finalPackage; };
};
passthru.updateScript = nix-update-script { };Your flake can mirror with checks. Upstream PRs often expect some test story for non-trivial packages.
Theory 6 — Category tree vs by-name coexistence
Not everything is migrated:
pkgs/applications/misc/hello/default.nix # classic
pkgs/by-name/ri/ripgrep/package.nix # by-name
| Aspect | Category tree | by-name |
|---|---|---|
| Discovery | Human taxonomy | Automatic shard |
| New packages | Discouraged for many cases | Preferred |
| Review tooling | Mature | Mature + automation |
| Your flake | N/A | Copy the shape |
Do not rewrite half of nixpkgs for fun. New private packages: by-name shape. Reading old packages: know both trees.
Theory 7 — Contribution hygiene (even if you never PR)
| Practice | Why |
|---|---|
| One logical change per commit | Review |
cooltool: init at 1.2.3 message style |
Convention |
| No secrets in expressions | Security |
| Builds on at least one platform claimed | Honesty |
nixpkgs-review awareness |
Community expectation |
| License correct | Legal |
# PR-CHECKLIST.md
- [ ] Builds on my system
- [ ] meta complete (description, license, mainProgram, platforms)
- [ ] Hashes pinned (no fakeHash)
- [ ] Naming matches pname
- [ ] No secrets
- [ ] Commit message style ok
- [ ] Optional: nixpkgs-reviewWorked example — flake package laid out like by-name
packaging-by-name/
flake.nix
pkgs/
by-name/
co/cooltool/package.nix
overlays/default.nix
PR-CHECKLIST.md
NOTES.md
# overlays/default.nix
final: prev: {
cooltool = final.callPackage ../pkgs/by-name/co/cooltool/package.nix { };
}# flake.nix
{
description = "by-name shaped package lab";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ (import ./overlays/default.nix) ];
};
in {
packages.${system}.default = pkgs.cooltool;
packages.${system}.cooltool = pkgs.cooltool;
checks.${system}.meta-license =
assert pkgs.cooltool.meta ? license;
pkgs.runCommand "meta-ok" { } "touch $out";
};
}Reuse your best package from earlier chapters as cooltool—reshape, don’t invent a new product.
Lab — multi-step
Suggested workspace: ~/lab/nixos/packaging/by-name
Step 1 — Locate packages
Record: description, license, platforms, maintainers shape.
Step 2 — Reshape your best packaging package
Move it to:
pkgs/by-name/<shard>/<pname>/package.nix
Use finalAttrs if it fits.
Step 3 — Overlay + flake export
callPackage through overlay; nix build .#cooltool.
Step 4 — Meta completeness pass
Ensure:
description
homepage(if any)
license
mainProgram
platformsor sensible default
Step 5 — PR-shaped checklist
Write PR-CHECKLIST.md even if not submitting.
Step 6 — Optional: clone nixpkgs and dry-place the file
Step 7 — Compare category tree vs by-name
Find one package still under pkgs/applications/… and one under by-name. Note one reviewer-facing difference.
Step 8 — Literature notes
For three packages in your language, fill:
| Package | Builder | Patches? | Multi-out? | Tests? |
|---|---|---|---|---|
Exercises
- Compute the by-name shard for
pname = "ripgrep",pname = "Go",pname = "q".
- Convert a
recderivation tofinalAttrs.
- Add a flake check that fails if
meta.licenseis missing.
- Read
lib.licensesdocs mentally—pick correct license for a real tool you use.
- Sketch what would block an upstream PR for your package (honest list).
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Wrong two-letter shard | Use first two chars of pname, lowercase |
default.nix in by-name |
Prefer package.nix per current convention |
Recursive rec version bugs |
Prefer finalAttrs |
| Giant unrelated deps in head | Only list what the function uses |
Meta missing mainProgram |
Multi-bin packages: set correctly or omit carefully |
| Assuming auto-discovery in your flake | Flakes need explicit callPackage/overlay |
| Editing all-packages by habit | Prefer by-name for new pkgs |
| Fake maintainers list entry | Only add yourself upstream if contributing for real |
Checkpoint
- Explained
by-namesharding
- Package lives in by-name-shaped path
- Overlay +
callPackageworks
- Meta filled to nixpkgs-ish bar
- PR checklist written (submit optional)
- Read three upstream packages as literature
- Know category tree still exists
Commit
Write three personal gotchas before binary caches.