25 December 2025 tags: freebsd, linux, package-managers, tools
This article considers OS package managers, but much of it can also apply to programming language package managers, like npm and Cargo.
In the pursuit of getting out of my tech comfort zone, I have been working on my “zen garden”: a small, cheap virtual private server running FreeBSD that runs some services I self-host. I am mainly a Linux person, so being confronted with all the things that I take for granted on Linux which work differently — or not at all! — on FreeBSD lets me learn more about what happens under the hood on both platforms.
One highlight of the newly released FreeBSD 15.0 is that [you can now let the package manager manage the operating system](https://www.freebsd.org/releases/15.0R/announce/#_packag…
25 December 2025 tags: freebsd, linux, package-managers, tools
This article considers OS package managers, but much of it can also apply to programming language package managers, like npm and Cargo.
In the pursuit of getting out of my tech comfort zone, I have been working on my “zen garden”: a small, cheap virtual private server running FreeBSD that runs some services I self-host. I am mainly a Linux person, so being confronted with all the things that I take for granted on Linux which work differently — or not at all! — on FreeBSD lets me learn more about what happens under the hood on both platforms.
One highlight of the newly released FreeBSD 15.0 is that you can now let the package manager manage the operating system.
“What? That’s been the case on Linux since forever! (side note: “Look at this kid, I used to install Slackware from floppies!” )” says the Linux user.
Even Windows has its own OS package manager, what does FreeBSD do? Well, it uses, or used to use, the simplest package manager: tar.
Tar, the simplest package manager
Well, maybe not just tar, but any archive file tool.
We want to be able to install packages on top of a “thing”, and maybe to remove them at some point. An archive file bundles files together and stores some attributes, like file name, access bits, directory structure etc. It’s no wonder that more developed package managers like rpm and dpkg are based on archives, as this is effectively the starting point.
To install a “package”, we can just extract an archive in / with the -x flag (eXtract):
$ tar -C / -xjvf base.txz
(The -j flag tells tar that the archive is compressed with xz.)
Uninstalling is interesting. You can get a list of all the files in an archive like so, with the -t flag (lisT?):
$ tar -tjf base.txz
./
./COPYRIGHT
./bin/
./bin/[
./bin/cat
./bin/chflags
./bin/chio
./bin/chmod
./bin/cp
./bin/cpuset
...
Remember the file attributes I just mentioned? You can see them with the -v flag:
$ tar -tjvf base.txz
drwxr-xr-x 0 root wheel 0 Nov 28 03:51 ./
-r--r--r-- 0 root wheel 6070 Nov 28 03:51 ./COPYRIGHT
drwxr-xr-x 0 root wheel 0 Nov 28 03:43 ./bin/
-r-xr-xr-x 0 root wheel 11736 Nov 28 03:42 ./bin/[
-r-xr-xr-x 0 root wheel 13808 Nov 28 03:42 ./bin/cat
...
lrwxr-xr-x 0 root wheel 0 Nov 28 03:43 ./sbin/nologin -> ../usr/sbin/nologin
-r-sr-xr-x 0 root wheel 26312 Nov 28 03:43 ./usr/bin/login
...
hr-xr-xr-x 0 root wheel 0 Nov 28 03:45 ./usr/bin/ssh link to ./usr/bin/slogin
...
Besides the access flags and owner information of each file, there are also some interesting attributes. ./sbin/nologin is a symbolic link, ./usr/bin/login has the SETUID flag, and ./usr/bin/ssh is a hard link to ./usr/bin/slogin. Depending on the archive file format, there can also be extended attributes and flags, but that’s a discussion for another time…
So, we have something we can pipe to xargs rm to remove the files from the system. We can see that the output from tar -t also contains directories. We need to filter them out before passing them to rm, because removing a directory with plain rm would lead to an error. And we cannot use xargs rm -r because that could delete more things than we want! We get:
$
$ cd / && tar -jtf base.txz | grep -v '/$' | xargs rm
$
$ cd / && tar -jtf base.txz | grep '/$' | sort -r | xargs rmdir
This is more or less how the three main BSD operating systems (side note: FreeBSD, OpenBSD, NetBSD. ) are distributed — in a couple tar archives. More or less — we will get to that later.
A couple of questions immediately come to mind:
-
How do you handle package updates?
-
How do we know what package each file comes from?
-
What if, when installing the package, we overwrite some existing files?
-
What if, when removing the package, we remove a file that was installed from a different package?
These are table stakes for the package managers most people are used to, like rpm, dpkg, apk, pacman etc. The first question can be answered easily as long as we have the file list of the old archive. You remove the old files, and then you extract the new archive.
And if we have the file list, then we can also answer the other questions. If we keep a database of files installed by each package, we can manage some packages.
Tar + package file database
It is time for some disruptive innovation. Let’s store a database with all the files that make up each package. Given the name of an installed package, we should get a list of all files that are part of that package.
There are many ways of storing such a database. For example, RPM uses the Berkeley DB format, and pkg (from FreeBSD) keeps tables in an SQLite 3 database. I will use Arch Linux’s pacman (side note: I don’t use Arch, btw.) as an example, owing to its text-based simplicity.
Pacman creates a directory for each installed package in /var/lib/pacman/local:
$ ls /var/lib/pacman/local/
acl-2.3.2-1
ALPM_DB_VERSION
archlinux-keyring-20251116-1
attr-2.5.2-1
audit-4.1.2-1
autoconf-2.72-1
automake-1.18.1-1
avahi-1:0.9rc2-1
base-3-2
base-devel-1-2
...
There is also a regular text file named ALPM_DB_VERSION (side note: Alpm stands for “Arch Linux Package Management”, according to pacman(8). ) that holds the database format version. On my system, this is equal to 9.
Let’s see what’s inside one of these directories:
$ ls /var/lib/pacman/local/pacman-7.1.0.r7.gb9f7d4a-1/
desc
files
mtree
Three files: desc, files, and mtree. In the aptly named text file files each line is a path to a file originating from this package:
$ head /var/lib/pacman/local/pacman-7.1.0.r7.gb9f7d4a-1/files
%FILES%
etc/
etc/makepkg.conf
etc/makepkg.conf.d/
etc/makepkg.conf.d/fortran.conf
etc/makepkg.conf.d/rust.conf
etc/makepkg.d/
etc/pacman.conf
usr/
usr/bin/
...
Now we have a clear idea of which package contains what, which allows us to answer a multitude of questions. We can now avoid conflicts, correctly upgrade or downgrade packages, and check which package manages a file. Which package offers the file /usr/bin/bash?
$ grep -lr usr/bin/bash /var/lib/pacman/local/ | cut -d'/' -f1
bash-5.3.9-1
...
On FreeBSD, we can leverage pkg’s SQLite 3 database:
# pkg shell
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> select packages.name from files
...> left join packages on files.package_id = packages.id
...> where files.path = '/usr/local/etc/rsync/rsyncd.conf.sample';
rsync
We can also check whether a package has been incorrectly applied, or whether files from the package are for some reason missing on the host. You might remember from a couple paragraphs up that pacman also stores an mtree file in its database. But what is it?
$ file mtree
mtree: gzip compressed data, from Unix, original size modulo 2^32 29540
Hmm, let’s use zcat to uncompress it and pipe the result to cat:
$ zcat mtree | head
#mtree
/set type=file uid=0 gid=0 mode=644
./.BUILDINFO time=1765404175.0 size=5292 sha256digest=c1e0872d1c7200038790e6e90c54ea58be4f9321eda478dde48af7f367c6c926
./.INSTALL time=1765404175.0 size=454 sha256digest=a041703891b00fc7c2109d004ac548de8e3f684a910887d7d463adcc4984548d
./.PKGINFO time=1765404175.0 size=633 sha256digest=766e91bb5b8d47f9b93d1f6367002e9bb28de43280f6ffa5b56814bd42566a14
./etc time=1765404175.0 mode=755 type=dir
./etc/bash.bash_logout time=1765404175.0 size=28 sha256digest=025bccfb374a3edce0ff8154d990689f30976b78f7a932dc9a6fcef81821811e
./etc/bash.bashrc time=1765404175.0 size=733 sha256digest=563e03eb4b40edfcc778f04efa2b4913bcdc6929c73d2bd4c07e1f799b98721a
./etc/skel time=1765404175.0 mode=755 type=dir
./etc/skel/.bash_logout time=1765404175.0 size=21 sha256digest=4330edf340394d0dae50afb04ac2a621f106fe67fb634ec81c4bfb98be2a1eb5
Oh, this is also a file list, but enhanced with hashes, access flags and ownership information. In fact, this listing is to be used by mtree, a tool which can either create such a file from a file hierarchy or compare a file hierarchy against a file listing of this kind. Perfect for checking that our system is consistent after installing packages.
In theory, with only archive files and mtree, we now have enough tools to install independent packages that don’t depend on each other. Or packages that have such simple dependency relations that the user can handle them, like installation sets on BSDs (side note: On FreeBSD, the only required set is base.txz. You can throw on top other optional sets that don’t have any dependency other than base, such as base-dbg for debugging symbols and src for the OS source code. ). But if we want to be able to automatically install and uninstall dependencies, we need to add another puzzle piece: requirements.
Packages requiring packages
Here is what happens when I try to install NumPy with pacman:
$ sudo pacman -S python-numpy
resolving dependencies...
looking for conflicting packages...
Package (6) New Version Net Change
extra/blas 3.12.1-2 0.74 MiB
extra/cblas 3.12.1-2 0.34 MiB
extra/lapack 3.12.1-2 15.06 MiB
core/mpdecimal 4.0.1-1 0.33 MiB
core/python 3.13.11-1 67.66 MiB
extra/python-numpy 2.4.0-1 46.42 MiB
(-S stands for sync. Pacman works with the sync database when running with -S, i.e. information about all remote packages that is synchronised on your system. This database can be found in /var/lib/pacman/sync/.)
The dependency tree of python-numpy up to depth 2 looks something like this:
Dependency graph for python-numpy up to depth = 2. Generated with pacgraph and graphviz. Text representation of the graph.
-
python-numpy -
cblas -
blas -
glibc -
lapac -
blas -
gcc-libs -
glibc -
python -
bzip2 -
expat -
gdbm -
libffi -
libnsl -
libxcrypt -
openssl -
zlib -
tzdata -
mpdecimal
In the pacman run from above, pacman computed the complete dependency graph, and kept only the nodes corresponding to packages that are not already installed. python-numpy obviously needs Python because it’s a Python library. It also needs Blas, the de-facto standard in matrix computations.
This dependency resolution works very well when installing packages! But what happens if I want to uninstall a package that depends on other packages?
$ sudo pacman -R python-numpy
checking dependencies...
Package (1) Old Version Net Change
python-numpy 2.4.0-1 -46.42 MiB
Wait, where did Python and Blas go? Why doesn’t pacman remove, them, too? I don’t need them any more!
With pacman (side note: But also with DNF, Zypper, APT, pkg…) you need to explicitly mention that you want to remove packages recursively using the -s flag:
$ sudo pacman -Rs python-numpy
Package (6) Old Version Net Change
blas 3.12.1-2 -0.74 MiB
cblas 3.12.1-2 -0.34 MiB
lapack 3.12.1-2 -15.06 MiB
mpdecimal 4.0.1-1 -0.33 MiB
python 3.13.11-1 -67.66 MiB
python-numpy 2.4.0-1 -46.42 MiB
...
Ah, just what I wanted! And it figured out that I don’t need Python and those weird sciency libraries.
Well, how does pacman know that you need a package or not? The answer lies in the local package database. You remember for each package whether it was installed explicitly by the user, or implicitly. Explicit means that I spelled the name of the package when I ran pacman, like I did with python-numpy in the example above. Implicit means that I didn’t spell it, but I agreed to install it after pacman figured out that I need it. Some package managers call implicit installation automatic installation. Pacman sets an internal %REASON% flag to 1 if the package was installed implicitly.
$ cat /var/lib/pacman/local/cblas-3.12.1-2/desc
%NAME%
cblas
%VERSION%
3.12.1-2
...
%REASON%
1
...
Say I want to keep cblas because I happen to use it for some scientific programming project of mine, but I still want to remove Numpy and whatever else it needs that I don’t. I can first tell pacman to mark cblas as an explicitly-installed package — even though I initially installed it implicitly — and then I can remove Numpy with the command from above.
$ sudo pacman -D --asexplicit cblas
cblas: install reason has been set to 'explicitly installed'
$ sudo pacman -Rs python-numpy
Package (4) Old Version Net Change
lapack 3.12.1-2 -15.06 MiB
mpdecimal 4.0.1-1 -0.33 MiB
python 3.13.11-1 -67.66 MiB
python-numpy 2.4.0-1 -46.42 MiB
...
(pacman -D is a special mode for modifying fields in the local package database.)
Works as intended!
OK, so now we can install and remove packages with dependencies, and we can determine whether packages are needed or not when we want to free up some disk space. But what about the following: What if a package needs something that is available in more than one package? Say I want to install Steam to play video games. Steam for some reason needs a Vulkan driver to be present, maybe it uses it for rendering. But there are multiple Vulkan drivers out there, made by different graphics card manufacturers. What dependencies does Steam have?
$ sudo pacman -Si steam
Repository : multilib
Name : steam
Version : 1.0.0.85-1
Description : Valve's digital software delivery system
Architecture : x86_64
URL : https://steampowered.com/
Licenses : LicenseRef-steam-subscriber-agreement
Groups : None
Provides : None
Depends On : bash coreutils curl dbus desktop-file-utils diffutils freetype2 gcc-libs gdk-pixbuf2 glibc hicolor-icon-theme libxcrypt
libxcrypt-compat libxkbcommon-x11 lsb-release lsof nss python ttf-font usbutils vulkan-driver vulkan-icd-loader xdg-user-dirs
xorg-xrandr xz zenity lib32-alsa-plugins lib32-fontconfig lib32-gcc-libs lib32-glibc lib32-libgl lib32-libgpg-error lib32-libnm
lib32-libva lib32-libx11 lib32-libxcrypt lib32-libxcrypt-compat lib32-libxinerama lib32-libxss lib32-nss lib32-pipewire
lib32-systemd lib32-vulkan-driver lib32-vulkan-icd-loader
...
(-i stands for “info”. You can find information about an already-installed package with -Qi. pacman -Q queries the local database.)
That’s a lot of dependencies! Can you spot the Vulkan driver that Steam wants?
... ttf-font usbutils vulkan-driver vulkan-icd-loader xdg-user-dirs ...
Ah, there it is! It’s just that, vulkan-driver? But I thought there are multiple of them?
There are, indeed, multiple. The vulkan-driver package is not real!
Fake packages, virtual packages, capabilities
We solve this problem by introducing fake packages that do not contain anything: they are either installed, or not installed, but otherwise they don’t do anything. Other names include “virtual packages” and “capabilities”. They act kind of like general-purpose booleans in the package manager’s local database. Other packages can then assert these booleans to be true for their dependencies to be satisfied. “Real” packages can claim that they provide the fake package: the package upholds a guarantee; if you install it, the boolean will be set, the requirement will be satisfied.
Let’s continue with the Vulkan driver example. As of December 2025, there are ten packages providing a Vulkan driver in the Arch Linux repositories. There is one for Intel GPUs, two for Nvidia (side note: One provided by Nvidia and one made by the Mesa project, which is called Nouveau. Clever.), one for use in virtual machines etc. The package containing the Steam client lists vulkan-driver as a requirement. This is the fake package: there is no package named vulkan-driver you can download from the Arch Linux repos! But if your computer for example has an Intel GPU, and you already have working graphics, chances are that you installed the right Vulkan driver together with the rest of the graphics stack — likely as an implicit dependency.
But if you did not, and you want to install Steam, pacman will ask you nicely: which of the following Vulkan drivers do you need?
Let’s ask pacman some information about the vulkan-driver package I have on my system:
$ sudo pacman -Q vulkan-driver
vulkan-intel 1:25.3.2-1
So I asked for vulkan-driver and I got vulkan-intel back! Let’s ask for more information:
$ sudo pacman -Qi vulkan-driver
Name : vulkan-intel
Version : 1:25.3.2-1
Description : Open-source Vulkan driver for Intel GPUs
Architecture : x86_64
URL : https://www.mesa3d.org/
Licenses : MIT AND BSD-3-Clause AND SGI-B-2.0
Groups : None
Provides : vulkan-driver
...
A-ha! Provides: vulkan-driver, that’s the line! You can think of this line as telling pacman “mark in the local database that there is a Vulkan driver installed”.
But wait, it can get trickier: programs are often linked against dynamic libraries. They need files ending in .so to work. We can find out which using ldd:
$ ldd $(which bash)
linux-vdso.so.1 (0x00007f167b946000)
libreadline.so.8 => /usr/lib/libreadline.so.8 (0x00007f167b7ad000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f167b59b000)
libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x00007f167b52c000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f167b948000)
So, bash needs every single one (side note: With the exception of linux-vdso.so.1, that one is special. It doesn’t even exist on disk. You can read more about it from its dedicated man page, vdso(7).) of these files to be present in a known directory in the system — /usr/lib — or it will not work at all. Sounds like the Vulkan driver case! I know for example that libreadline.so.8 comes from the readline package, but the name of the exact file the program requires is libreadline.so.8. Maybe there are packages that provide multiple library files at once, do I then need to remember the name of the package providing each .so file if I want to make my own package? What if the name changes, or if the package is split, or if some packages merge into one? All my program cares about is having the right .so present.
Package capabilities to the rescue, again! On many platforms — Arch Linux, dpkg-based distros, rpm-based distros, Alpine Linux etc. — packages containing .so libraries advertise them as capabilities.
Say I need to use some binary I downloaded off the internet, but it complains that it needs libasound.so.2. Let’s find out whether pacman has it for us:
$ sudo pacman -Ss libasound.so
extra/alsa-lib 1.2.14-2
An alternative implementation of Linux sound support
Great! What does its Provides line look like?
$ sudo pacman -Si alsa-lib | grep Provides
Provides : libasound.so=2-64 libatopology.so=2-64
We can see that it has a version attached. That is important, because an older version of libasound.so may not be compatible with the binary I downloaded off the internet. This can be the case when a function’s prototype changes, for example when its return type is different in a later version. Such incompatibilities are marked by incrementing the number in the filename. This is called a “soname bump”.
Coming back to my random binary example, we want to make sure that we install a compatible libasound.so. Thanks to the version information, we can be specific when asking pacman to install it:
$ sudo pacman -S libasound.so=2
resolving dependencies...
looking for conflicting packages...
Package (3) New Version Net Change Download Size
extra/alsa-topology-conf 1.2.5.1-4 0.33 MiB 0.01 MiB
extra/alsa-ucm-conf 1.2.14-2 0.54 MiB 0.11 MiB
extra/alsa-lib 1.2.14-2 1.68 MiB 0.48 MiB
...
Red Hat-family Linux distros (side note: Red Hat Enterprise Linux, Fedora, CentOS, CentOS Stream, Alma Linux, Rocky Linux etc.) make heavy use of package capabilities. These distros use DNF and RPM to manage packages. I will show some examples from Alma Linux 9.
Example 1, a Python library:
$ sudo dnf repoquery --provides python3-numpy
libnpymath-static = 1:1.23.5-1.el9
libnpymath-static(x86-64) = 1:1.23.5-1.el9
numpy = 1:1.23.5-1.el9
numpy(x86-64) = 1:1.23.5-1.el9
python-numpy = 1:1.23.5-1.el9
python3-numpy = 1:1.23.5-1.el9
python3-numpy(x86-64) = 1:1.23.5-1.el9
python3.9-numpy = 1:1.23.5-1.el9
python3.9dist(numpy) = 1.23.5
python3dist(numpy) = 1.23.5
Here we have capabilities for the python library itself in different forms, with and without the CPU architecture, and capabilities with the Python interpreter’s version.
Example 2, a C library, without headers:
$ sudo dnf repoquery --provides readline
libhistory.so.8
libhistory.so.8()(64bit)
libreadline.so.8
libreadline.so.8()(64bit)
readline = 8.1-4.el9
readline(x86-32) = 8.1-4.el9
readline(x86-64) = 8.1-4.el9
Here we have two .so files with architecture information. The package also provides files for running 32-bit executables.
Example 3, the development files of the same C library:
$ sudo dnf repoquery --provides readline-devel
pkgconfig(readline) = 8.1
readline-devel = 8.1-4.el9
readline-devel(x86-32) = 8.1-4.el9
readline-devel(x86-64) = 8.1-4.el9
This package provides database files for pkg-config, a tool which finds the right directories for the header (.h) and library files (.so) when building C and C++ code and generates the right compiler flags to use them.
Example 4, DNF itself:
$ sudo dnf repoquery --provides dnf
dnf = 4.14.0-31.el9.alma.1
dnf-command(alias)
dnf-command(autoremove)
dnf-command(check-update)
dnf-command(clean)
dnf-command(distro-sync)
dnf-command(downgrade)
...
DNF can be extended with plug-ins, which can, among other things, extend DNF with new subcommands. Say you found a command on a forum to fix the problem you’re having, and it uses dnf diff:
$ dnf diff
No such command: diff. Please use /usr/bin/dnf --help
It could be a DNF plugin command, try: "dnf install 'dnf-command(diff)'"
You run the suggested command, and:
$ sudo dnf install 'dnf-command(diff)'
...
Installing:
dnf-plugin-diff noarch 2.0-1.el9 epel 21 k
...
So easy, and you don’t need to know the name of the package at all! It could be named something like dnf-plugin-magic-diff-command or something (it is not).
Example 5, Perl modules:
$ sudo dnf repoquery --provides perl-IO
perl(IO) = 1.43
perl(IO::Dir) = 1.41
perl(IO::File) = 1.41
perl(IO::Handle) = 1.42
perl(IO::Pipe) = 1.41
perl(IO::Pipe::End)
perl(IO::Poll) = 1.41
perl(IO::Seekable) = 1.41
perl(IO::Select) = 1.42
perl(IO::Socket) = 1.43
perl(IO::Socket::INET) = 1.41
perl(IO::Socket::UNIX) = 1.42
perl-IO = 1.43-481.1.el9_6
perl-IO(x86-64) = 1.43-481.1.el9_6
Just like with .so files, you don’t have to know the name of the package providing the modules.
And the list can go on… You can read more about the various capabilities used in these distros in the Fedora Packaging Guidelines.
OK, now, one more feature that would be very convenient in a package manager… You know how some programs also provide shell completion definitions? I love shell completion; I want them for as many programs as possible. But there are also multiple shells around. It would be nice if the shell completions would just install themselves automatically. It would also be nice if, when I install a different shell, all the completion definitions for the myriads of command line programs I use would also be installed automatically for that shell.
Auto-install rules
The only package manager I know of that supports this is apk, from Alpine Linux (side note: Probably one of the most freeloaded Linux distros. I wonder how much critical infrastructure at big name companies runs on it…). Apk is also used on Chimera Linux.
An apk package can have a field named “install-if”, in order to define auto-install rules. An example from helix-fish-completion:
$ sudo apk info --install-if helix-fish-completion
helix-fish-completion-25.07.1-r2 has auto-install rule:
helix=25.07.1-r2
fish
When all the packages in that list will be installed, helix-fish-completion will also be installed automatically.
If I now install Helix, helix-bash-completion will also be installed, because I use bash:
$ sudo apk add helix
(1/2) Installing helix (25.07.1-r2)
(2/3) Installing helix-bash-completion (25.07.1-r2)
...
Later, if I want to install the Fish shell, helix-fish-completion will be installed automatically.
$ sudo apk add fish
(1/4) Installing fish (4.0.2-r0)
(2/4) Installing curl-fish-completion (8.17.0-r1)
(3/4) Installing fish-doc (4.0.2-r0)
(4/4) Installing helix-fish-completion (25.07.1-r2)
...
The same thing can happen in reverse (side note: This stems from the way apk works. In apk, the add and del commands modify the “world file” at /etc/apk/world. This is a regular text file where each line contains the name of a package that was installed explicitly. apk add adds a line, apk del removes a line. After the file is modified, apk computes the dependency graph of all the files mentioned in that file — taking into account the auto-install rules — compares it against the graph of all packages that are actually installed, and installs and uninstalls packages accordingly to make the second graph match the first. ): if a package was installed implicitly, and the auto-install rule doesn’t hold any more, then the package will be removed:
$ sudo apk del helix
(1/3) Purging helix-bash-completion (25.07.1-r2)
(2/3) Purging helix-fish-completion (25.07.1-r2)
(3/3) Purging helix (25.07.1-r2)
Giving shell completion as an example was a bit silly, I must admit. Shell completion files are very small files; it’s not the end of the world if you have them around. Maybe it is the end of the world for some people, so it’s good to have this feature!
This feature shines when you want an environment where you do not wish to have man pages and documentation. On Alpine Linux, documentation is split into separate packages with names ending with -doc. These packages have auto-install rules:
$ apk info --install-if zfs-doc
zfs-doc-2.4.0-r0 has auto-install rule:
docs
zfs=2.4.0-r0
So, if you want to get rid of all documentation, you just uninstall the docs package! Conversely, if you want documentation, you install the docs package and the -doc packages corresponding to your installed packages will be magically installed. And whenever you will install a new package that has documentation available, its corresponding -doc package will be installed with it. So neat and elegant!
OK, but I started this post with FreeBSD not being managed with a package manager before. How does that work?
If it looks like a duck, swims like a duck…
So FreeBSD has only a handful of packages: base, kernel-dbg, base-dbg, ports etc. Only base is required, and all the other packages depend on base only. So dependency resolution is really simple, it fits in your head.
But what about package upgrades? And, what about configuration files? Your OS comes with some default configs. What happens when you modify a config, and a new version of the OS comes with new defaults?
That’s so 1997, I hear you say. Haven’t you heard about having a default config file and a new, different file that contains your overrides (side note: Like /etc/ssh/sshd_config and /etc/ssh/sshd_config.d/*. )? And in 2025 we have these things called “container images”, we upgrade our OSes atomically. OK, a package installs a config and I override it. So what? When there’s a new package version, I don’t apply it on top, I just reinstall the whole OS! Why would I even care…
There used to be a time when sysadmins used to care about bandwidth, storage and electricity and they didn’t reinstall their entire OS for each package update — figuratively, that is pretty much what happens when you rebuild a container image. As for why config files weren’t split in defaults and overrides, beats me. I am too young to know that.
But OK, this is an actual problem. You have the old default, the new default, and whatever the user modified in, say, /etc/resolv.conf, or /etc/passwd. What do you do?
RPM and pacman (side note: See Pacnew and Pacsave. ), for example, just save the new version with a new extension and keep your changes untouched. Hopefully you will see the warning on the screen next to the other hundreds of scrolling lines and you will check whether the file needs any manual intervention.
On FreeBSD, there is a tool named etcupdate that you are supposed to run after OS upgrades. But you usually don’t run it manually, unless you installed the OS from source or something weird like that. The conventional route is to run freebsd-update which will take care of downloading the new archives, extracting them in root, removing old files and so on. Hey, that looks like a package manager!
Indeed it does, albeit a purpose-built one. It was made to install new versions of that couple of OS archives, rollback from an upgrade, and nothing else, basically. And it does some fancy magic to only download binary diff files to save space and bandwidth, neat. But I assume that it must be pretty hard to maintain it, because the effort that goes into this tool doesn’t translate into improvements for pkg, which was used — until now — only to install third-party software. And, you have a problem if you want a slim OS install. What if you don’t need everything that comes in the base package? On Linux distros you can install a small set of packages and be done with it. On FreeBSD, you had to recompile the OS from source for that! That’s a bit of a waste, and it requires you to understand how the build system works before you even try to obtain a smaller install. That’s quite the overhead. With the new pkg-based installation workflow, if I don’t need the ZFS filesystem for example, I can just do pkg delete FreeBSD-zfs and it’s gone.
How did we end up here? I didn’t live through the good old days when Linux just became a thing and the BSD world split into three, and I did not read any UNIX history book on this subject either — if it exists. But I can speculate that the Linux world, being broken into multiple, seemingly independent projects that compete with each other (side note: Not literally, I hope that there is no KPI reporting with the conversion rate of Plasma users to Gnome.), felt the need for a program that allows users to mix and match software before the BSD world needed. Unlike Linux distros, BSD operating systems are one whole product, developed by one team per operating system, so they have easier control of the software developed under their umbrella. They built their own custom tools — etcupdate, freebsd-update etc. — and those worked well, then they created improved tools like pkg. To me it seems like a good, natural progression.
If there is anything to be learned from this, is that package managers started from the basic requirement of installing (and removing!) software and slowly added smart features on the go. There you have it: an overview of how package managers can be helpful!