Day 17 — Python/Rust Packaging, Patching & Outputs
Day 17 — Python/Rust Packaging, Patching & Outputs
Day 48 — Language ecosystem B
Stage V · ~4h
Goal: Package a second language ecosystem tool so the patterns transfer: FODs for language deps, helpers vs raw stdenv, and how overrides differ across builders.
Why this day exists
At work you will not stay monoglot. The point is not fluency in five builders—it is recognizing the same skeleton:
src FOD → language-dep FOD → build/install hooks → $out/bin → meta
Day 47 was depth. Today is transfer with a deliberately smaller scope.
Theory 1 — Pattern map across ecosystems
| Concern | Go | Rust | Python | Node |
|---|---|---|---|---|
| Source pin | src.hash |
src.hash |
src.hash / fetchPypi |
src.hash |
| Dep pin | vendorHash |
cargoHash / lock |
package set / frozen | npmDepsHash |
| Builder | buildGoModule |
buildRustPackage |
buildPython* |
buildNpmPackage |
| Skip tests | doCheck |
doCheck / cargo flags |
pytest hooks |
npm build only |
| Binary name | module main | crate bin | meta.mainProgram |
meta.mainProgram |
| Cross | GOOS/GOARCH care |
target triples | often pure | often pure JS |
When stuck, ask: which hash is wrong—source or language deps?
Theory 2 — Choose B deliberately
| If Day 47 was… | Good Day 48 pick | Why |
|---|---|---|
| Go | Python CLI or Rust | Different dep story |
| Rust | Go small tool | Faster vendor cycle |
| Python | Node or Go | Leaves interpreter wrapping |
| Node | Python or Go | Sees compiled vs interpreted |
Avoid starting a second multi-crate workspace today. One binary, clear version, known license.
Theory 3 — Wrapping and runtime deps
Interpreted ecosystems often need wrappers:
# Python apps usually wrap PYTHONPATH via the builder
# Manual wrap example for a shell/python hybrid:
{ stdenv, lib, python3, makeWrapper }:
stdenv.mkDerivation {
pname = "tool";
version = "0.1.0";
nativeBuildInputs = [ makeWrapper ];
# …
postInstall = ''
wrapProgram $out/bin/tool \
--prefix PATH : ${lib.makeBinPath [ python3 ]}
'';
}| Situation | Prefer |
|---|---|
| Official language builder exists | Use it |
| Need one env var or PATH sibling | wrapProgram / makeWrapper |
| Need whole tree of modules | Builder’s dependency graph |
Theory 4 — Overrides preview (deep dive Day 49)
# Change flags without forking the whole expression
helloGo.overrideAttrs (old: {
ldflags = (old.ldflags or []) ++ [ "-X main.extra=day48" ];
})# Python package override inside python3.pkgs
python3.pkgs.myPkg.overridePythonAttrs (old: {
doCheck = false;
})Today: know these exist. Tomorrow: patch and overlay composition for real.
Worked example — dual packages in one flake
{
description = "Day 48 — second ecosystem";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
systems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
in {
packages = forAllSystems (system:
let pkgs = import nixpkgs { inherit system; }; in {
lang-a = pkgs.callPackage ./pkgs/lang-a.nix { }; # from day47, copy or input
lang-b = pkgs.callPackage ./pkgs/lang-b.nix { };
default = self.packages.${system}.lang-b;
});
checks = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
a = self.packages.${system}.lang-a;
b = self.packages.${system}.lang-b;
in {
both-run = pkgs.runCommand "both-run" {
nativeBuildInputs = [ a b ];
} ''
# invoke both CLIs smoke-test style
${a}/bin/* --help >/dev/null 2>&1 || true
${b}/bin/* --help >/dev/null 2>&1 || true
touch $out
'';
});
};
}Example B — tiny Python app (if A was compiled)
{ lib, python3Packages }:
python3Packages.buildPythonApplication {
pname = "day48-greet";
version = "0.1.0";
pyproject = true;
src = ../src/day48-greet;
build-system = with python3Packages; [ setuptools ];
meta = {
description = "Day 48 greet";
license = lib.licenses.mit;
mainProgram = "day48-greet";
};
}# src/day48-greet/pyproject.toml
[project]
name = "day48-greet"
version = "0.1.0"
requires-python = ">=3.11"
[project.scripts]
day48-greet = "day48_greet:main"
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"# src/day48-greet/day48_greet.py
def main():
print("day48 ecosystem B")
if __name__ == "__main__":
main()Example B — tiny Node CLI (alternative)
{ lib, buildNpmPackage }:
buildNpmPackage {
pname = "day48-node";
version = "0.1.0";
src = ../src/day48-node;
npmDepsHash = "sha256-…";
meta = {
mainProgram = "day48-node";
license = lib.licenses.mit;
};
}Lab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day48
Step 1 — Import or copy Day 47 package
Keep lang-a buildable so you compare logs side by side.
Step 2 — Scaffold smaller B project
Budget: under ~50 lines of application code. No microservices.
Step 3 — Package with the B builder
nix build .#lang-b -L
nix run .#lang-bRecord every hash you had to set (src + language deps).
Step 4 — Comparison table (required)
In NOTES.md:
| Question | Lang A | Lang B |
|---|---|---|
| Builder name | ||
| Source hash field | ||
| Dep hash field | ||
| Time to first success | ||
| Hardest error | ||
| Where binary lands | ||
| Would I use this at work for CLIs? |
Step 5 — Unified check
Add a flake checks.* that runs both tools (smoke). nix flake check.
Step 6 — Optional: same interface
Make both print a comparable line (day47 / day48) so a shell script can assert either.
Step 7 — Closure glance
nix path-info -rS ./result | sort -k2 -n | tail
nix path-info -Sh ./resultNote which ecosystem pulled a heavier closure and why (interpreter? compiler runtime? libc?).
Theory 5 — Cross-ecosystem FOD discipline
Whatever language B you chose, fetchers obey the same rules as Day 46:
| Rule | Why |
|---|---|
| Pin URL + hash | Reproducible |
| Vendor offline where possible | CI without network flakiness |
| Prefer language-native lock → Nix conversion tools carefully | Don’t invent hashes by hand when tooling exists |
# conceptual — language-specific helpers differ
# buildGoModule / buildPythonPackage / rustPlatform.buildRustPackage
# each has its own cargoHash / vendorHash / etc.Document the hash field name for your ecosystem B in notes—future-you will grep for it.
Theory 6 — Runtime vs build-time closure
nativeBuildInputs → tools for build (compilers, make)
buildInputs → linked/used at build; may propagate
propagated* → infect dependents (use sparingly)
A package that “works in nix develop” but fails as nix build often mixed shell conveniences with missing buildInputs.
Worked example — dual-output mental model
Language ecosystems often ship:
- CLI binary
- library crate/wheel/module
On Nix, that may be one derivation or split outputs (Day 50). For today: ensure meta.mainProgram / bin path is clear when wrapping.
Lab — hash bump drill
- Intentionally change a src rev or leave hash empty if your Nix warns
- Run build; capture the got: hash from the error
- Paste correct hash; rebuild green
- Write the exact error class name in notes
Lab — compare A vs B one-pager
Table in notes:
| Dimension | Eco A (Day 47) | Eco B (today) |
|---|---|---|
| Builder helper | ||
| Lockfile | ||
| Hash field | ||
| Common footgun |
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Trying to “finish” a second huge app | Shrink scope; transfer patterns, not product |
| Mixing package sets (python311 vs python312) | Align interpreters |
Assuming vendorHash = null always works |
Only pure module-free trees |
npmDepsHash churn from lock format |
Commit lock; pin npm |
| Comparing wall times unfairly | Same machine, warm vs cold cache noted |
Forgetting meta.license |
Set it; habit for nixpkgs-shaped work |
Checkpoint
- Second ecosystem package builds and runs
- Comparison table filled honestly
- Both A and B reachable from one flake
- At least one combined check
- Closure notes captured
Commit
git add .
git commit -m "day48: language ecosystem B + comparison"Write three personal gotchas before continuing.
Tomorrow
Day 49 — Patching & overrides: overrideAttrs, patches, and overlay composition without forking nixpkgs.
Day 49 — Patching & overrides
Stage V · ~4h (lab-heavy)
Goal: Fix or pin packages with overrideAttrs, patches, and overlays—without maintaining a permanent hard fork of nixpkgs.
Why this day exists
Upstream will be wrong for your window of time: a CVE pin, a broken release, a flag your hardware needs. The professional skill is minimal, reviewable deltas that compose, not copy-pasting half of nixpkgs into your flake.
Theory 1 — Three layers of change
| Mechanism | Scope | Reuse | Maintenance |
|---|---|---|---|
override / overrideAttrs |
One package instance | Call-site | Low–medium |
| Overlay | Package set view for a flake/host | Shared modules | Medium |
Fork / fetch your tree |
Anything | High control | High—avoid until needed |
nixpkgs → overlay(s) → pkgs.yourTool
↑
overrideAttrs / patches
Overlays you already touched lightly in Stage III; today they become packaging tools.
Theory 2 — overrideAttrs
pkgs.hello.overrideAttrs (old: {
patches = (old.patches or []) ++ [ ./patches/hello-msg.patch ];
postPatch = (old.postPatch or "") + ''
echo "day49" >> README
'';
})| Form | Notes |
|---|---|
overrideAttrs (old: { … }) |
Preferred; see previous attrs |
overrideAttrs { … } |
OK for total replace of listed keys; easy to drop old patches by mistake |
package.override { stdenv = …; } |
Change callPackage args (dependencies), not phases |
override vs overrideAttrs
# Change dependencies / function args
pkgs.ffmpeg.override { withXcb = true; }
# Change derivation attributes / phases
pkgs.ffmpeg.overrideAttrs (old: {
configureFlags = (old.configureFlags or []) ++ [ "--enable-something" ];
})Language builders often expose both (e.g. overridePythonAttrs).
Theory 3 — Patches as first-class inputs
stdenv.mkDerivation {
# …
patches = [
./patches/0001-fix-build-on-musl.patch
(fetchpatch {
url = "https://github.com/org/proj/commit/abc123.patch";
hash = "sha256-…";
})
];
}| Practice | Why |
|---|---|
| Numbered filenames | Apply order is list order |
| Keep patches small | Rebase pain scales with hunk size |
| Prefer upstream PRs | Local patches rot on every release |
| Test without patch once | Confirm the patch is still required |
patchPhase applies these automatically; fail logs show hunk rejects.
Theory 4 — Overlay composition
# overlays/hello-patch.nix
final: prev: {
hello = prev.hello.overrideAttrs (old: {
patches = (old.patches or []) ++ [ ../patches/hello-msg.patch ];
});
}# flake.nix fragment
pkgs = import nixpkgs {
inherit system;
overlays = [
(import ./overlays/hello-patch.nix)
(import ./overlays/my-tool.nix)
];
};Order matters
overlay1 final:prev → overlay2 final:prev → …
Later overlays see earlier results via prev/final fixed-point.
Use final when depending on other packages you also overlay; use prev to modify the incoming package without recursion traps.
final: prev: {
myApp = prev.myApp.overrideAttrs (_: { /* … */ });
# depend on overlaid hello:
myAppWrapped = final.hello;
}NixOS / HM consumption
nixpkgs.overlays = [
(import ./overlays/hello-patch.nix)
];
# or per-flake pkgs passed into modulesTheory 5 — Pinning and “don’t rebuild the world”
| Goal | Technique |
|---|---|
| One package newer than channel | Overlay with fetchFromGitHub src bump + hashes |
| One package older (pin) | Overlay src to known-good rev; note security debt |
| Only your app sees the change | Don’t put overlay in global NixOS; inject via callPackage scope |
| Temporary CI fix | Overlay in flake only; ticket to drop it |
final: prev: {
curl = prev.curl.overrideAttrs (old: rec {
version = "8.11.1";
src = prev.fetchurl {
url = "https://curl.se/download/curl-${version}.tar.bz2";
hash = "sha256-…";
};
});
}Bumping shared libraries can rebuild huge closures—check nix why-depends before celebrating.
Worked examples
Example A — local patch on hello
# patches/hello-msg.patch
--- a/src/hello.c
+++ b/src/hello.c
@@ -1,5 +1,5 @@
/* … */
- printf ("Hello, world!\n");
+ printf ("Hello, Stage V!\n");# overlays/hello-stagev.nix
final: prev: {
hello = prev.hello.overrideAttrs (old: {
pname = old.pname;
patches = (old.patches or []) ++ [ ../patches/hello-msg.patch ];
});
}Example B — overlay adding your Day 47 package
final: prev: {
hello-go = prev.callPackage ../pkgs/hello-go.nix { };
}Example C — packageOverrides legacy note
Older blogs use packageOverrides. Prefer overlays in modern flakes; recognize the old name when reading.
Lab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day49
Step 1 — Baseline
nix build nixpkgs#hello -o baseline-hello
./baseline-hello/bin/helloStep 2 — Write a patch
Create a minimal patch (message change or README touch). If patching C is painful on your platform, patch a script package you own from Day 45 instead.
Step 3 — overrideAttrs only (no overlay)
In a default.nix / flake package:
pkgs.hello.overrideAttrs (old: {
patches = (old.patches or []) ++ [ ./patches/hello-msg.patch ];
})nix build .#hello-patched -L
./result/bin/helloStep 4 — Promote to overlay
Move the same change to overlays/…. Import overlay into flake pkgs. Confirm:
nix eval .#packages.x86_64-linux.hello.drvPath
# differs from unpatched when patch appliedStep 5 — Stack two overlays
- Overlay patches
hello
- Overlay adds
myTooldepending onfinal.hello
Prove myTool sees the patched hello if it references it (even a trivial wrap).
Step 6 — Dependency override
Pick a package with a boolean flag (ffmpeg, qemu, etc.—choose something not enormous if you can):
pkgs.ffmpeg.override { /* one flag */ }Build or evaluate and document why you stopped short if closure is huge.
Step 7 — Maintenance note
In NOTES.md: expiry conditions for the patch (“remove when nixpkgs ≥ rev …” / “upstream PR #”).
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Patch rejects | Refresh patch against actual src version |
| Overlay silently not applied | Ensure same pkgs instance reaches the host/package |
| Infinite recursion in overlay | Use prev for the package you modify; avoid final.pkg = final.pkg… |
| Dropped upstream patches | Always (old.patches or []) ++ [ ours ] |
| Rebuilt half of nixpkgs | You overrode a low-level lib; narrow scope |
| Unmaintainable mega-patch | Split; upstream; or wait for release |
| Darwin vs Linux patch drift | Conditional patches with lib.optionals stdenv.isLinux |
Checkpoint
overrideAttrspatch builds and changes behavior
- Overlay form works from flake
- Two overlays composed with known order
- Difference
overridevsoverrideAttrswritten down
- Patch expiry / upstream plan noted
Commit
git add .
git commit -m "day49: patches, overrideAttrs, overlays"Write three personal gotchas before continuing.
Tomorrow
Day 50 — Multiple outputs: split out/dev/doc and shrink closures on purpose.
Day 50 — Multiple outputs
Stage V · ~4h
Goal: Use multi-output derivations (out, dev, man, doc, …) to keep runtime closures small while preserving headers and debug assets for dependents.
Why this day exists
A single $out that ships headers, man pages, static libs, and a 2 MB binary forces every dependent to download and GC the world. Multi-output packaging is how nixpkgs stays usable on laptops and CI runners.
Theory 1 — Outputs are separate store paths
/nix/store/hash-foo-1.0 ← out (runtime)
/nix/store/hash-foo-1.0-dev ← dev (headers, pkg-config)
/nix/store/hash-foo-1.0-man ← man
/nix/store/hash-foo-1.0-doc ← html docs
Each is a different path. References between them are tracked so GC stays correct: runtime out should not reference dev if you did it right.
stdenv.mkDerivation {
pname = "foo";
version = "1.0";
outputs = [ "out" "dev" "man" ];
# …
}Default if omitted: outputs = [ "out" ];.
Theory 2 — Conventional output roles
| Output | Typical content | Who needs it |
|---|---|---|
out |
Binaries, shared libs, runtime data | Everyone running the tool |
dev |
Headers, .pc, static libs, cmake files |
Compiling dependents |
lib |
Sometimes shared libs split from bins | Rare patterns / legacy |
man |
Man pages | Humans; optional on servers |
doc |
HTML/PDF docs | Optional |
info |
Texinfo | Optional |
devdoc |
Heavy API docs | Dev machines |
debug |
Separate debuginfo (with separateDebugInfo) |
Debuggers |
Environment variables in the build
| Var | Points at |
|---|---|
$out |
First output (usually runtime) |
$dev |
Dev output path |
$man |
Man output |
$outputs |
List of outputs |
Install into the correct root:
install -Dm755 foo $out/bin/foo
install -Dm644 foo.h $dev/include/foo.h
install -Dm644 foo.pc $dev/lib/pkgconfig/foo.pc
install -Dm644 foo.1 $man/share/man/man1/foo.1Theory 3 — Why dependents get smaller
app depends on libfoo
→ needs libfoo.out (and its runtime closure)
→ does NOT need libfoo.dev unless building against headers
nix path-info -r on an application should not pull *-dev for every library if packaging is correct.
nix path-info -r ./result | rg -- '-dev$' || echo "no dev outputs in closure (good for runtime)"Theory 4 — propagatedBuildInputs and multi-out hazards
| Hazard | Result | Mitigation |
|---|---|---|
Headers installed to $out |
Runtime closure grows | Move to $dev |
out references $dev |
Forces dev into runtime closure |
Fix install paths / use moveToOutput |
Forgetting outputs but installing to $dev |
Empty/wrong paths | Declare outputs first |
| pkg-config file wrong prefix | Consumers cannot find libs | PKG_CONFIG paths; check .pc |
stdenv provides helpers like moveToOutput used heavily in nixpkgs.
moveToOutput share/man "$man"
moveToOutput include "$dev"Theory 5 — Consuming multi-output packages
# As a build dependency of another package
nativeBuildInputs = [ pkg-config ];
buildInputs = [ foo ]; # pulls foo.dev for compilation via setup hooks usuallySetup hooks from dev outputs typically put headers on the search path when foo is in buildInputs.
Selecting outputs explicitly (advanced / rare in modern nixpkgs):
foo.dev # the dev store path attribute
foo.out
foo.manWorked example — toy multi-output C library + app
/* src/libgreet/greet.c */
#include "greet.h"
const char *greet_msg(void) { return "hello multi-out"; }/* src/libgreet/greet.h */
#pragma once
const char *greet_msg(void);/* src/greetapp/main.c */
#include <stdio.h>
#include "greet.h"
int main(void) { puts(greet_msg()); return 0; }# pkgs/libgreet.nix
{ stdenv, lib }:
stdenv.mkDerivation {
pname = "libgreet";
version = "0.1.0";
outputs = [ "out" "dev" "man" ];
src = ../src/libgreet;
buildPhase = ''
$CC -shared -fPIC greet.c -o libgreet.so
'';
installPhase = ''
runHook preInstall
install -Dm755 libgreet.so $out/lib/libgreet.so
install -Dm644 greet.h $dev/include/greet.h
mkdir -p $dev/lib/pkgconfig
cat > $dev/lib/pkgconfig/libgreet.pc <<EOF
prefix=$out
libdir=$out/lib
includedir=$dev/include
Name: libgreet
Version: 0.1.0
Libs: -L$out/lib -lgreet
Cflags: -I$dev/include
EOF
mkdir -p $man/share/man/man3
echo ".TH GREET 3" > $man/share/man/man3/greet.3
runHook postInstall
'';
meta = {
description = "Tiny greet library (multi-output lab)";
license = lib.licenses.mit;
};
}# pkgs/greetapp.nix
{ stdenv, lib, libgreet, pkg-config }:
stdenv.mkDerivation {
pname = "greetapp";
version = "0.1.0";
src = ../src/greetapp;
nativeBuildInputs = [ pkg-config ];
buildInputs = [ libgreet ];
buildPhase = ''
$CC main.c -o greetapp $(pkg-config --cflags --libs libgreet)
'';
installPhase = ''
install -Dm755 greetapp $out/bin/greetapp
'';
meta = {
mainProgram = "greetapp";
license = lib.licenses.mit;
};
}nix build .#libgreet
ls result* # multiple outputs may appear as result, result-dev, …
nix build .#libgreet.dev -o result-dev
nix build .#greetapp
nix path-info -r ./result | rg libgreetLab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day50
Step 1 — Inspect an existing multi-out package
nix eval nixpkgs#openssl.outputs
nix build nixpkgs#openssl -o o-out
nix build nixpkgs#openssl.dev -o o-dev
du -sh o-out o-devRecord sizes.
Step 2 — Build libgreet with three outputs
Implement the toy library (or split your Day 45 tool into lib + bin).
Step 3 — Verify separate paths
nix build .#libgreet
nix build .#libgreet.dev
nix build .#libgreet.man
nix path-info .#libgreet .#libgreet.dev .#libgreet.manStep 4 — Build greetapp against libgreet
Confirm the app runs and its runtime closure includes libgreet out but ideally not manpages.
nix path-info -r ./result | rg 'libgreet'Step 5 — Deliberate mistake
Install a header into $out/include and rebuild app. Compare closure size / path-info. Fix it.
Step 6 — Document policy
In NOTES.md: when you will use outputs = [ "out" "dev" ] for internal packages vs single-output scripts.
Step 7 — Optional separateDebugInfo
separateDebugInfo = true;Build and note the extra debug output if supported on your platform.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
dev empty |
Not installing into $dev; check outputs list |
Runtime needs dev |
Cross-reference leak; fix files that embed $dev paths |
pkg-config cannot find package |
Ensure .pc in dev and package in buildInputs |
Only result symlink |
Multi-out: use nix build .#pkg.dev or -o names |
| Broke library for dependents | Test a dependent package in the same flake |
Docs in out |
moveToOutput or install directly to $doc/$man |
Checkpoint
- Can explain multi-out as multiple store paths
- Toy (or real) package produces
out+dev
- Dependent app builds against
dev, runs againstout
- Closure inspection done with
nix path-info
- One deliberate mis-install fixed
Commit
git add .
git commit -m "day50: multiple outputs libgreet + greetapp"Write three personal gotchas before continuing.
Tomorrow
Day 51 — Debug builds: nix log, --keep-failed, breakpointHook, and recovering from sandbox failures.