// linux fundamentals — module 09
Module 9 — Package Management and Shared Libraries (The Bridge to NixOS)
Thesis: The final foundation is the one that makes NixOS's existence obvious. Learn how software actually gets onto a Linux system — packages, shared libraries, dynamic linking — and then look hard at the structural weakness of the whole scheme: global mutable state. Every pain this module names is a pain Nix was designed to remove.
Prerequisite: Module 8 — Networking on Linux, and especially solid Module 3 — Everything Is a File — this module is the FHS and symlinks bearing full load.
9.1 What a package is
You've been installing packages by recipe since Lab 2. Here's what one actually is: an archive of files + metadata + scripts.
- The files — compiled binaries, libraries, man pages, default config — each tagged with its destination path in the FHS: this binary →
/usr/bin/, that library →/usr/lib/, that config →/etc/. Installing = unpacking the archive into the live system's shared directories. Hold onto that phrasing; it is the crux of everything below. - The metadata — name, version, description, and above all dependency declarations: "needs
libssl >= 3.0," "conflicts with X." Dependencies are what turn installation from copying into constraint-solving. - Maintainer scripts — arbitrary shell run at (un)install time: create a system user (§4.1), reload a systemd unit (§7.5), rebuild a cache. Powerful, and — note for later — unrecorded mutations of system state.
Two great packaging families, same design, different dialects: Debian (.deb; low-level tool dpkg, resolver apt) and Red Hat (.rpm; rpm, resolver dnf). Arch's pacman is its own dialect of the same idea. The layer split matters: dpkg/rpm install one archive you already have; apt/dnf are the brains on top — they read repositories (distro-hosted indexes of thousands of packages), resolve the full dependency graph, download, verify signatures, and drive the low-level tool. The database of "what's installed, which files belong to whom" lives in /var/lib/dpkg / /var/lib/rpm (§3.2's /var/lib, as promised).
Interrogating the system — genuinely useful daily, and the lab drills them:
dpkg -L curl / rpm -ql curl # which files did this package install, and where?
dpkg -S /usr/bin/curl / rpm -qf /usr/bin/curl # reverse: which package owns this file?
apt show curl / dnf info curl # metadata, dependencies
apt list --installed / dnf list installed
9.2 Shared libraries and dynamic linking
Why do packages depend on each other at all? Because programs don't carry their own copies of everything.
Vast amounts of code are common to all programs — libc above all (§2.2), plus crypto, compression, GUI toolkits. Compiling a private copy into every binary (static linking) costs disk, RAM (§2.4's page-sharing would be defeated — one libc in memory serves every process only because they share the file), and worst, security: a hole in one library would need every program on the system rebuilt. So Unix chose dynamic linking: common code lives in shared object files — libssl.so.3, .so = shared object, the /usr/lib population from §3.2 — and programs load them at launch.
Mechanics, in course vocabulary: a dynamically linked binary contains a list of library names it needs and an interpreter path — the dynamic linker (ld-linux-…so). At execve (§5.1), the kernel sees that and runs the linker first; the linker searches configured paths (/etc/ld.so.conf + a cache, ldconfig; overridable with LD_LIBRARY_PATH — an env var, §1.8) for each library, maps them into memory (mmap, §2.2), wires up the symbols, then jumps to the program. Those mysterious openat(… "lib….so.…") calls at the top of every strace since Lab 2 — that was the dynamic linker, every time. Now you know.
The inspection tool: ldd — print a binary's library needs and where each resolves:
$ ldd $(which curl)
libcurl.so.4 => /usr/lib/libcurl.so.4
libssl.so.3 => /usr/lib/libssl.so.3
libc.so.6 => /usr/lib/libc.so.6
…
=> not found on any line = a program that cannot start — the infamous "error while loading shared libraries." And the versioning scheme that tries to keep everything compatible: library filenames carry an ABI version (the soname, libssl.so.3), under which the real file sits with symlinks (§3.5) layering the names: libssl.so.3 → libssl.so.3.2.1. Bump the minor version and replace the file, every program transparently gets the fix — that's the good side. The soname major version is a promise of compatibility. Everything that follows is about what happens when promises meet reality.
9.3 The core problem: global mutable state
Now stand back and look at the design as a whole, with Module 3 eyes:
- There is one
/usr/bin, one/usr/lib, one/etc— shared, global namespaces. - Every package install/upgrade/removal mutates them in place.
- Dependency declarations are constraints over one global set: at most one version of
libssl.so.3can be the file/usr/lib/libssl.so.3.
The system's software state is therefore the cumulative residue of every operation ever performed on it — a giant, shared, mutable variable. If you've programmed, that phrase should raise your hackles; global mutable state is where bugs live. Concretely, four structural pains:
1 — Dependency hell. App A needs libfoo.so.1, App B needs libfoo.so.2 — incompatible majors. One filename slot… actually two here (different sonames coexist) — but A needs libfoo ≥1.4 and <2 while B needs exactly 1.2: now genuinely unsatisfiable within one global set. Distros mitigate by curating one blessed version of everything and patching all packages against it — an enormous, ongoing labor that works well inside the repo and shatters at its edges (third-party software, "I need a newer version than the distro ships"). Every workaround you may have met — PPAs and pinning, containers/Docker "just to run one app," AppImage/Flatpak/Snap bundling — is the ecosystem paying tax on this one design decision.
2 — Upgrades can break unrelated software. Upgrading B pulls a new libfoo; A — untouched by you — now loads a library it was never tested with. This is spooky action at a distance, and it's inherent: the library is a global variable, and B's upgrade wrote to it.
3 — Rollback is nearly impossible. Undo an upgrade? The package manager can install the old version forward (another mutation), but the maintainer scripts (§9.1) ran, configs migrated, and the exact prior state of the global pile is gone — nobody recorded it. Compare what does roll back on your machine: NixOS generations aside, think of git — snapshots, not mutations. Foreshadowing.
4 — Reproducibility is fragile. "Same distro, same package list" does not yield the same system: final state depends on install order and history (which maintainer scripts ran when, what was upgraded vs. fresh-installed, what config prompts answered). Two "identical" servers drift apart; "works on my machine" is this fact wearing a costume. Configuration management (Ansible etc.) fights the symptom — by scripting the mutations more carefully — not the cause.
Name the common root once more, because it's the sentence the whole course has been walking toward: installation is mutation of shared global state, and nothing records or isolates it.
9.4 The bridge: how Nix dissolves each pain
Nix's founding move (Eelco Dolstra's 2006 PhD thesis) is to treat package management like a pure function: a build takes explicit inputs (source + exact dependencies) and produces an output that is never modified afterward — stored at a path derived from a hash of all its inputs:
/nix/store/9an9ijxk2r51…-openssl-3.2.1/
/nix/store/7c2m0qvxk8ra…-openssl-1.1.1w/ ← coexisting, colliding with nothing
Walk the four pains into it — every resolution is a foundation concept you already own:
- Dependency hell → there is no shared slot to fight over. Every version has its own store path; A is wired (via absolute paths baked in at build time —
lddon a NixOS binary shows/nix/store/…resolutions) to exactly thelibfooit was built against, B to its own. §9.2's linker mechanics, pointed at isolated paths instead of a global namespace. - Spooky upgrades → upgrading B builds new store paths and repoints B; A's wiring is untouched by construction. No global variable was written, so no action at a distance.
- Rollback → old store paths still exist; a "system" is just a tree of symlinks (§3.5) into the store; rolling back = repointing symlinks — instant, atomic, and surfaced in your boot menu as generations (§6.8, mechanism now complete).
- Reproducibility → the system is built from a declarative description, and same inputs ⇒ same hash ⇒ same output. History and order stop mattering; your
configuration.nixis the system, the way source code is the program.
Costs, honestly: disk space (multiple versions live simultaneously; garbage collection exists), a learning curve (the Nix language, and unlearning FHS reflexes), and friction with software that hard-assumes FHS paths (NixOS has standard answers — patching, steam-run, FHS-emulating environments). You'll judge the trade yourself — but now from understanding, not marketing.
And the curriculum's full-circle sentence: Module 0 defined a distribution as an opinionated pile of files at standard paths, maintained by mutation. You are now equipped to hear NixOS's counter-proposal precisely: a distribution as the output of a pure function.
9.5 → NixOS
This whole module was the NixOS note. One practical addition: when you begin the Nix curriculum (next file), you'll find every concept has a foundation anchor — the store (§3.4 inodes/immutability + §4.9 read-only enforcement), wiring (§9.2 linking + §1.8 $PATH), activation (§3.5 symlinks), generations (§6.8 boot entries), services (§7.7 generated units), users (§4.9 declared accounts). Nothing in NixOS is new mechanism — it is your foundations, recomposed under one discipline.
Lab 9
Use your distro's dialect (dpkg/apt or rpm/dnf — or pacman -Ql/-Qo/-Si on Arch).
Anatomize an installed package. Pick
curl. List every file it installed (dpkg -L/rpm -ql) and sort each into its FHS territory out loud (§3.2): binaries, libraries, man pages, configs. Then reverse-lookup:dpkg -S /usr/bin/curl/rpm -qf— and try the reverse-lookup on a file you created in your home directory (no owner — the database only knows package-delivered files; your edits are off the books, which is §9.3's point 4 in miniature).Read a dependency graph.
apt show curl/dnf repoquery --requires curl(orapt-cache depends curl). Find libcurl, an SSL library, and libc in the tree. Then go up: what depends on the SSL library (apt-cache rdepends libssl3/dnf repoquery --whatrequires openssl-libs)? The size of that reverse list is the blast radius of §9.3's pain 2 — count it and say the sentence.Watch the dynamic linker work. Run
ldd $(which bash)and read every line, noting how few libraries a shell needs vs.ldd $(which curl). Then catch it live:strace -e openat bash -c true 2>&1 | grep '\.so'— the Lab 2 mystery lines, now fully attributed. Finally, prove the soname symlink layering:ls -l /usr/lib/libssl*(or wherever your distro keeps it) and trace name → symlink → real versioned file.Break a program with one env var (safe, instructive). Run
LD_LIBRARY_PATH=/tmp ldd $(which curl)— harmless, still resolves (search order includes the standard paths). Now simulate real damage in a scratch dir: copy some binary's needed lib name as an empty file into/tmp/broken-lib/, run the binary withLD_LIBRARY_PATH=/tmp/broken-lib, and read the loader's error. That message — "error while loading shared libraries" — is dependency hell's face; you've now caused and can explain it. (Nothing was mutated: unset the var and all is well. Notice that isolation-by-environment is a tiny taste of Nix's approach.)Snapshot the global state, mutate it, diff it. Record
ls /usr/bin | wc -landapt list --installed 2>/dev/null | wc -l(or dnf equivalent). Install something small (slorcowsay), re-run both counts, then list the package's delivered files and find them in the live FHS. Remove it; re-count. You just watched the global variable being written and un-written — and consider: what did the maintainer scripts do that the counts don't show? Where would you even look? (Exactly. Pain 3.)Write the essay — this one is mandatory. In your own words, one page in this vault: describe a concrete dependency-hell scenario end-to-end (two apps, one soname slot, an upgrade), then explain how per-package immutable store paths dissolve it, tracing the exact mechanism with
ldd-level specificity. If you can write this page fluently, the entire curriculum has landed; it is the capstone's centerpiece question.(Optional preview.) Install standalone Nix on your current distro (
nix profile install nixpkgs#hello), thenldd $(which hello)andls -l $(which hello)— watch every §9.4 claim be literally true on your own disk: symlink chain into/nix/store, dependencies resolving to hashed store paths.
✅ Mastery Check — do not proceed until true
Answer out loud, without notes:
- What three things are in a package? What's the division of labor between
dpkgandapt(orrpmanddnf)? Where does the installed-files database live? - Why did Unix choose dynamic over static linking — three reasons, including the §2.4 memory argument? What happens at
execveof a dynamically linked binary, step by step, ending at the program's first instruction? - What is a
.so, a soname, and the symlink layering under it? What doeslddshow, and what does=> not foundmean for the program? - State the core structural problem of traditional package management in one sentence, then derive all four pains from it: dependency hell, spooky upgrades, no rollback, irreproducibility.
- Why can't two incompatible versions of a library both satisfy the global namespace? Name two real-world ecosystem workarounds and what tax they pay.
- For each of the four pains, state Nix's dissolution and the foundation concept it's built from (hashing/store paths, baked linker paths, symlink repointing, declarative builds).
- What does it mean that a NixOS system is "the output of a pure function," and what are the honest costs?
And perform cold:
- For any installed program: which package owns it, what does that package depend on, which libraries does the binary load, and which package owns those.
- Diagnose an "error while loading shared libraries" from first principles.
- Give the five-minute "why NixOS exists" explanation to an imaginary colleague, using only concepts from this course.
When all of that is effortless: Capstone — Ready for NixOS