Day 16 — Packages & programs
Day 16 — Packages & programs
Stage II · ~4h
Goal: Install software declaratively on the host—know when to use environment.systemPackages versus programs.* modules, and keep ad-hoc profiles from becoming the real SoT.
Why this day exists
New NixOS users nix-env / nix profile install a zoo, then wonder why rebuilds do not reproduce the machine. Declarative packages are how the host stays honest.
Theory 1 — environment.systemPackages
environment.systemPackages = with pkgs; [
vim
git
curl
htop
jq
];| Property | Meaning |
|---|---|
| On PATH system-wide | For all users (typical) |
| Closure in system gen | Rolled back with system |
| No service config | Just binaries/libs |
This is the blunt instrument—and it is correct for many CLI tools.
Theory 2 — programs.* modules
Many packages have first-class modules:
programs.git.enable = true;
programs.zsh.enable = true;
programs.tmux.enable = true;
# programs.neovim.enable = true; # if presentModules may:
- Install the package
- Drop config snippets
- Set shells in
/etc/shells
- Enable related services
Prefer programs.* when you need integration, not only a binary.
Discovery
# search options (host with nixos, or online search.nixos.org)
man configuration.nix # huge
# or
nixos-option programs.git 2>/dev/nullOnline: https://search.nixos.org/options (select 26.05 when available).
Theory 3 — Decision table
| Need | Prefer |
|---|---|
| Just a CLI on PATH | environment.systemPackages |
| Shell as login shell + vendor config | programs.zsh / programs.fish / programs.bash |
| Git system-wide with config hooks | programs.git and/or HM later |
| Language toolchains for one project | devShell (Day 8/28), not systemPackages |
| Desktop app | packages or specialized modules |
| Always-on service | services.* (Day 17) |
Theory 4 — Closure discipline (light)
Every package on the system increases:
- Download size
- Eval/build time
- Attack surface
- Cognitive load
| Smell | Fix |
|---|---|
| Entire language toolchains system-wide | devShell / HM |
| Three editors “just in case” | Pick one for system |
Random nix profile duplicates |
Remove; declare in flake |
Theory 5 — Overlays and pins (awareness only)
You might see:
nixpkgs.overlays = [ … ];or pinned packages. Stage III/V cover overlays properly. Today: stick to nixpkgs 26.05 attribute names from your lock.
Theory 6 — Unfree and allow flags
Some packages need:
nixpkgs.config.allowUnfree = true;Or per-package predicates. Only enable when you understand licensing implications for your lab.
Theory 7 — System vs user packages
| Layer | Mechanism |
|---|---|
| System | environment.systemPackages, programs.* |
| User (later) | Home Manager home.packages |
| Project | nix develop shells |
| Imperative | nix profile — minimize |
Stage III moves dotfiles and user CLIs to HM; today system layer only.
Worked examples bank
Example A — Baseline admin tools
{ pkgs, ... }: {
environment.systemPackages = with pkgs; [
vim
git
curl
wget
htop
tmux
jq
ripgrep
tree
];
}Example B — programs integration
{
programs.zsh.enable = true;
users.users.alice.shell = pkgs.zsh;
}Example C — Search package attr
nix search nixpkgs ripgrepLab 1 — Declare your daily CLI set
List 8–15 tools you truly want on the lab host. Add them via environment.systemPackages. Rebuild switch. Verify each:
command -v rg
command -v jq
# …Lab 2 — Convert one package to programs.*
Pick something with a module (git, zsh, tmux, …):
- Remove from
systemPackagesif redundant
- Enable
programs.<name>
- Rebuild
- Note extra behavior (completions, config files)
Lab 3 — Anti-profile audit
nix profile list 2>/dev/null || true
# nix-env -q 2>/dev/null || trueIf tools exist only imperatively, either:
- migrate into flake, or
- remove them
Document the end state: flake owns system tools.
Lab 4 — Size awareness
nix path-info -Sh /run/current-system
# optional deeper:
nix path-info -rS /run/current-system | sort -n | tailWrite one sentence on the cost of your package list.
Lab 5 — Split comments in config
Organize packages with comments:
environment.systemPackages = with pkgs; [
# editors
vim
# net
curl
# observability
htop
];Commit as readable policy, not a dump.
Theory 8 — environment.defaultPackages trap
NixOS may install a small default package set. Know it exists so you do not double-declare or fight removals:
# advanced / intentional only
# environment.defaultPackages = [ ];Prefer adding what you need over emptying defaults on day one unless you understand the fallout (e.g. nano/perl expectations in scripts).
Theory 9 — nix-shell vs system packages vs flakes
| Want | Tool |
|---|---|
| Permanent host CLI | environment.systemPackages / programs.* |
| Project toolchain | nix develop / direnv (later) |
| One-off try | nix shell nixpkgs#foo / nix run |
Do not promote every experiment into the system closure.
Worked example D — Conditional unfree single package
{
nixpkgs.config.allowUnfreePredicate = pkg:
builtins.elem (pkgs.lib.getName pkg) [ "vscode" ]; # example name only
}Narrower than global allowUnfree = true when you must pull one unfree tool onto a mostly free lab.
Lab 6 — programs.* option archaeology
# on the lab host
nixos-option programs.zsh 2>/dev/null | head -40
# or use search.nixos.org with channel 26.05List three options under a programs.* module that are not just “install the binary.”
Lab 7 — Remove one redundant package
Find a package present both via systemPackages and a programs.* enable (or profile). Collapse to one path. Rebuild. Confirm still on PATH.
Common gotchas
| Symptom | Theory |
|---|---|
| Package name ≠ binary name | Check pkgs attr vs meta.mainProgram |
| Unfree errors | allowUnfree policy |
| Tool missing after rebuild | Not in config; was profile-only |
| Giant system closure | Too many packages / wrong layer |
| Shell not changing | users.users.*.shell not set |
Checkpoint
- Daily tools declared in flake
- At least one
programs.*used
- Imperative profile not required for lab tools
- Rough system path size noted
- Config comments organized
Commit
cd ~/nixos-config
git add .
git commit -m "day16: systemPackages and programs modules"Tomorrow
Day 17 — systemd via Nix. One custom service + timer generated from modules—units as code.