Packaging Anti-Patterns
Packaging Anti-Patterns
Most “Nix is slow / undeterministic” reports are the same five habits. The boring default is: fix these before adding Crane, uv2nix, or another overlay file.
Mental model
| Smell | Usual cause |
|---|---|
| CI ≠ laptop | <nixpkgs> or unlocked nixpkgs# |
| Every commit rebuilds gcc | src = ./. includes docs / .git / target/ |
| Eval takes minutes | import-from-derivation |
go on PATH ≠ CI |
nix-env / Home Manager compiler |
| Infinite recursion | final.pkg.overrideAttrs |
Password in nix why-depends |
secret in a derivation |
dream2nix / flake-parts are not the default. They paper over these if you let them. Delete the anti-pattern first.
Worked examples
Case 1: <nixpkgs> in production
Bad:
# configuration.nix — do not
{ pkgs ? import <nixpkgs> { }, ... }: {
environment.systemPackages = [ pkgs.hello ];
}Fix — pin nixos-26.05:
# flake.nix
{
description = "Desk";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }: {
nixosConfigurations.desk = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./configuration.nix ];
};
};
}nix flake metadataOne nixpkgs node. Two nodes means a follows is missing.
Case 2: Fat src
Bad: src = ./.; at the monorepo root.
Fix:
# desk-api.nix
{ pkgs, lib }:
pkgs.buildGoModule {
pname = "desk-api";
version = "1.0.0";
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.unions [ ../apps/desk-api ../go.mod ../go.sum ];
};
vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}nix-build
echo x >> README.md
nix-build --dry-runIf gcc is in “will be built,” the filter failed.
Case 3: IFD
Bad:
# do not
let
src = pkgs.fetchFromGitHub {
owner = "desk";
repo = "mod";
rev = "abc123";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
in
import "${src}/default.nix"Eval builds src before it can parse default.nix. Flakes + IFD is a foot-gun.
Fix: copy default.nix into this repo, or add a flake input and callPackage.
Case 4: Compiler on the profile
Bad:
nix-env -iA nixpkgs.go
nix profile install nixpkgs#rustcFix: devShells.default with the same go / rustPlatform as buildGoModule / buildRustPackage.
# flake.nix fragment
{
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.go pkgs.gopls ];
};
}nix profile for jq, not compilers. which go outside nix develop should fail (or be a distro go you must not use).
Case 5: Overlay recursion and secrets
Bad: hello = final.hello.overrideAttrs (_: { }) → infinite recursion.
Fix: prev.hello.overrideAttrs.
Bad:
# do not
stdenv.mkDerivation {
pname = "desk-secret";
version = "1.0";
src = ./.;
password = "hunter2";
}or writeText "env" "TOKEN=…".
Fix: sops-nix path at runtime. Check:
nix path-info -r ./result | xargs -I{} grep -l hunter2 {} 2>/dev/null || echo 'token not in closure'nix path-info -r must not grep the token.
The trap
The trap is papering over these with dream2nix / flake-parts / a mega-overlay. The new tool then IFDs, too. Delete the anti-pattern first. The other trap is fakeHash left on main — every CI run is a FOD mismatch.
The boring rule
- Pin nixos-26.05. Commit
flake.lock. - Filter
src. No IFD on the hot path. - No
nix-env. No compiler inhome.packages. - Override this package from
prev. - No secret bytes in
$out.
Try this
git grep -n '<nixpkgs>'andgit grep 'src = \./\.'.git grep nix-env.nix flake metadata— a single nixpkgs lock node.--dry-runafter a README-only edit on a filtered package.git grep -nE 'password = "|TOKEN=' -- '*.nix'.