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.
“ (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:
$ # remove files
$ cd / && tar -jtf base.txz | grep -v '/$' | xargs rm
$ # remove empty directories
$ cd / && tar -jtf base.txz | grep '/$' | sort -r | xargs rmdir
This is more or less how the (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 (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 (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 (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:
Text representation of the graph.
python-numpycblasblasglibc
lapacblasgcc-libsglibc
pythonbzip2expatgdbmlibffilibnsllibxcryptopensslzlibtzdatampdecimal
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.
(Blas stands for Basic Linear Algebra Subprograms.)
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!
(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, (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 (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
...
(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 (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 (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 (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 (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 (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!