Minimal apt install
I was recently working on a bootstrap script that, among other things, needed to install a fairly large number of apt
packages. The list of packages was pre-existing, and had grown organically over several years. As most apt
users would
know, many packages require a significant number of dependenecies, and so it's pretty likely that any significant list
of apt packages would include at least some transitive redundancy. So, mostly as a mental exercise, I started
pondering how I could go about reducing any set of required apt packages to tha minimal set that would still include all
of the same dependencies.
It actually proved to be pretty easy in the end. But first, I needed to find a reliable way to fetch the list of
dependencies for any given package. The apt-cache
utility does the job, but I had to sort through some misinformation
online re the command's output format. You read about that in the previous Pipes in apt-cache
Output post.
Once I knew how to determine the dependencies for a package, it was pretty easy to wrap it up in a Bash script:
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2024 Paul Colby <git@colby.id.au>
# SPDX-License-Identifier: MIT
set -o errexit -o noclobber -o nounset -o pipefail
shopt -s inherit_errexit
declare -A seen
function getDeps {
echo "Getting deps for $1" >&2
local -a deps
deps=$(apt-cache depends "$1" | gawk -- '/^ *[|]?Depends:/ {if (substr(prev,0,1)!="|")print $2;prev=$1}')
for dep in ${deps}; do
[[ ! "${dep}" =~ ^\<([^:]+)(:[^:]+)?\>$ ]] || dep=${BASH_REMATCH[1]}
if [[ -v seen[${dep}] ]]; then (( seen[${dep}]++ ))
else
echo "Found dep: ${dep}" >&2
seen[${dep}]=1
getDeps "${dep}"
fi
done
}
for pkg in "$@"; do
getDeps "${pkg}"
done
echo -n 'apt install' >&2
for pkg in "$@"; do
[[ -v seen["${pkg}"] ]] || echo "${pkg}"
done | sort | tr '\n' ' '
echo
The script works by defining a getDeps()
function (lines 10 to 23) which recursively finds all dependencies for a
given package, adding those dependencies to a global seen
associative array. Note, if a dependency has already been
seen (line 16), then its dependency are not fetched again, since that would take more time, and not yield anything new.
With getDeps()
available, the script first runs getDeps()
for all packages (lines 25 to 28) specified on command
line:
for pkg in "$@"; do
getDeps "${pkg}"
done
After that, the seen
associative array will contain entries for every package that is a dependant (directly, or
indirectly) of any of the packages provided on the command line. Note, however, that the packages from the command line
themselves will not have been added to seen
unless they are themselve dependencies.
Put another way, getDeps packageX
will add all of packageX
's dependencies to seen
, but not packageX
itself.
Thus, the only way packageX
itself will appear in seen
, is if packageX
was a dependancy for at least one of the
other packages passed to another call to getDeps
.
So finally, all we need to do is loop through all of the packages provided on the command line (lines 29 to 33),
outputting just the ones that are not set in seen
:
for pkg in "$@"; do
[[ -v seen["${pkg}"] ]] || echo "${pkg}"
done | sort | tr '\n' ' '
The script sorts the package list for aesthetics only.
Finally, we can run the script like:
./minimise.sh <package1> ... <packageN>
For example:
$ ./minimise.sh cmake cpp gcc gdb g++ lcov perl qmake6 qt6-base-dev qt6-base-dev-tools qt6-l10n-tools
Getting deps for cmake
Found dep: libarchive13t64
Getting deps for libarchive13t64
Found dep: libacl1
Getting deps for libacl1
Found dep: libc6
Getting deps for libc6
...
Getting deps for libllvm15t64
Found dep: libqt6qml6
Getting deps for libqt6qml6
apt install cmake g++ gdb lcov qt6-base-dev qt6-l10n-tools
So the list:
cmake cpp gcc gdb g++ lcov perl qmake6 qt6-base-dev qt6-base-dev-tools qt6-l10n-tools
Has been reduced to:
cmake g++ gdb lcov qt6-base-dev qt6-l10n-tools
Note, the script's diagnotic output is sent to stderr
, so you can, for example redirect stderr
to /dev/null
(or a
file) for cleaner output. Likewise, you could redirect (or tee
) stdout
to a file have the final apt install
command written to a file. Some examples:
$ # Hide all the diagnostic logging.
$ ./minimise.sh <lots-of-packages> 2> /dev/null
<minimal-packages>
$ # Redirect just the minimal packages list to a text file.
$ ./minimise.sh <lots-of-packages> > min.txt
Getting ...
...
$ cat min.txt
<minimal-packages>
$ # Tee the minimal packages list to a text file, and stdout.
$ ./minimise.sh <lots-of-packages> | tee min2.txt
Getting ...
...
apt install <minimal-packages>
$ cat min2.txt
<minimal-packages>