Day 50 — Multiple outputs
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.