Day 50 — Multiple outputs

Updated

July 30, 2026

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.1

Theory 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 usually

Setup 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.man

Worked 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 libgreet

Lab — 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-dev

Record 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.man

Step 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 against out
  • 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.