Create dummy deb packages

Notes published the
9 - 11 minutes to read, 2154 words

Create an empty deb package

Why should I want to?

There are two main use cases.

The first one is to create a meta-package. A meta-package is a package whose main reason to exist is for defining a set of dependencies. This can be useful for installing, updating, and removing multiple packages.

The second use case is for satisfying a dependency. Some packages have other packages marked as dependencies, and those dependencies cannot be resolved. This happens, for example, if you are installing packages manually, outside of a repository. If the dependency happens to be optional (even if not marked as such), there are four possible ways to circumvent the issue:

  • edit the package and remove the optional dependency (or mark it as optional)

  • force an installation

  • find and install the dependency

  • create a dummy package to satisfy the dependency

The last option is less cumbersome, with no need to search for packages, force an installation, or edit third-party packages.

Create and install an empty package

Suppose there is one program that depends on another package called foo

printf 'Maintainer: Your Name <mail@example.com>
Section: misc
Priority: optional
Package: foo
Version: 1.0
Description: dummy package
' > foo.equivs;
equivs-build foo.equivs;
sudo dpkg -i foo_1.0_all.deb;

The official Debian wiki has an example example for replacing an existing dependency of the gnome desktop (gnome-games):

printf 'Package: gnome-games
Version: 99:99
Maintainer: Your Name <mail@domain.com>
Architecture: all
Description: dummy gnome-games package
 A dummy package with a version number so high that the real gnome packages
 will never reach it.
' > anti-gnome-games.equivs;
equivs-build anti-gnome-games.equivs;
sudo dpkg -i gnome-games_99_all.deb;

Create and install a meta-package

In case you want to create a meta package, be sure to add some dependencies. For example, you might want to use something like that as a "minimal C++ development environment package":

printf 'Maintainer: Your Name <mail@example.com>
Section: misc
Package: c++-dev-env
Version: 1.0
Depends: git, tig, cmake, make, g++, cppcheck, valgrind
Description: A (minimal) C++ development environment
 This is a metapackage for installing the tools I more or less always use
 for development
' > c++-dev-env.equivs;
equivs-build c++-dev-env.equivas;

Then use one of the three equivalent commands for installing the package and all its dependencies

sudo sh -c 'dpkg -i ./c++-dev-env_1.0_all.deb; aptitude install -f;';
sudo apt-get install ./c++-dev-env_1.0_all.deb;
sudo apt install ./c++-dev-env_1.0_all.deb;

Supposing that tig was not already installed on your system, removing c++-dev-env will also remove tig (and its dependencies), but not packages that were already installed.

It also prevents removing packages "by accident" as long as one has c++-dev-env installed.

Create a non-empty package

Creating a good-shaped package might not be easy, but creating a package per se is not difficult.

A deb package is just an archive that contains the files we are interested in, and a couple of files that define some properties. The most important one is the control file, which stores the information about the deb package and the program it installs.

The deb package contains folders that mimic a typical Debian file system, for example, /usr, /opt, and so on. A file put in one of those directories will be copied to the same location in the file system during the installation.

For the sake of simplicity, I am going to assume we want to create a deb package that installs a single file.

NVIMVERSION="0.8.2";
mkdir -p foo/{opt/nvim/bin,DEBIAN}/;
wget https://github.com/neovim/neovim/releases/download/v$NVIMVERSION/nvim.appimage -O foo/opt/nvim/bin/nvim;
printf 'bb0d4599cb506fc6e29bf0e9cef3b52e06dcb4bb930b56d6eb88320f1d46a908 *foo/opt/nvim/bin/nvim' | sha256sum --check;
chmod 755 foo/opt/nvim/bin/nvim;
printf 'Maintainer: Your Name <mail@example.com>
Section: misc
Priority: optional
Package: foo
Version: %s
Architecture: amd64
Description: alternate package for nvim
' "$NVIMVERSION" > foo/DEBIAN/control;
dpkg-deb --build --root-owner-group foo;
sudo dpkg -i foo.deb;

The "Architecture field" is required, it does not have as a default value the current architecture. I have set it to "amd64" as it is the machine I am using and the downloaded binary is compatible with it. A shell script should probably have any as architecture.

Creating such a package seems like a waste of time. Especially because we are downloading a single binary file, and there are no dependencies. Wouldn’t it be much faster to write

wget https://github.com/neovim/neovim/releases/download/stable/nvim.appimage -O nvim;
printf 'bb0d4599cb506fc6e29bf0e9cef3b52e06dcb4bb930b56d6eb88320f1d46a908 *nvim' | sha256sum --check;
sudo sh -c 'mkdir -p /opt/nvim/bin && mv nvim /opt/nvim/bin/nvim && chmod 755 /opt/nvim/bin/nvim';

and avoid creating the package at all?

It would be, but the main benefit of the first approach is that we need to create the package only once (per update), and then reuse it in multiple situations.

Especially when dealing with a testing environment, installing a package can be simpler than executing a custom script that copies files in different locations.

It seems common practice to have Dockerfiles installing packages and execute custom commands, like downloading files and copying them over the file system. This is literally what package formats like deb, rpm, and others have been designed for!

One could theoretically replace a whole Dockerfile with apt-get install my-special-environment.

Having deb files is especially practical outside of an ephemeral environment, as it gives you for free an uninstaller!

aptitude remove foo removes automatically all files and (empty) directories added with the package foo.

This can also be done with a custom script, but it does not surprise me that most setup scripts do not have a corresponding cleanup script.

Creating such packages can streamline setting up and maintaining environments a lot.

It is possible to create a separate package for every GCC and Clang version one wants to test, instead of relying on one docker container per compiler.

It is still possible to use those packages inside docker containers, but you are not forced to and can configure your work environment where you prefer.

In particular, since normally you are not only interested in using a specific Compiler version, but also one (or more) build systems, dynamic and static analyzers, and test suites, …​ this generates a lot of images you have to create and maintain. Being able to execute those binaries locally has also some advantages.

For providing a more concrete example, here is how one could package a specific CMake version (documentation excluded)

CMAKE_VERSION=3.25.2;
mkdir -p foo/{opt/,DEBIAN}/;
(
  cd /foo/opt/;
  wget "https://github.com/Kitware/CMake/releases/download/v$CMAKE_VERSION/cmake-$CMAKE_VERSION-linux-x86_64.tar.gz";
  printf '783da74f132fd1fea91b8236d267efa4df5b91c5eec1dea0a87f0cf233748d99 *cmake-%s-linux-x86_64.tar.gz' "$CMAKE_VERSION" | sha256sum --check;
  tar xf "cmake-$CMAKE_VERSION-linux-x86_64.tar.gz" "cmake-$CMAKE_VERSION-linux-x86_64"/{bin,share};
  rm "cmake-$CMAKE_VERSION-linux-x86_64.tar.gz";
)
printf 'Maintainer: Your Name <mail@example.com>
Section: devel
Recommends: gcc, make
Suggests: ninja-build
Priority: optional
Package: foo
Version: %s
Architecture: amd64
Description: alternate package for cmake
' "$NVIMVERSION" > foo/DEBIAN/control;
dpkg-deb --build --root-owner-group foo;
sudo dpkg -i foo.deb;

And one for creating a package of a specific JDK (documentation excluded) from AdoptOpenJDK (now Adoptium)

mkdir -p foo/{opt,DEBIAN}/;
OPENJDK_VERSION=8u352;
OPENJDK_BUILD=b08;
(
  cd foo/opt/;
  FILE="OpenJDK8U-jdk_x64_linux_hotspot_$OPENJDK_VERSION$OPENJDK_BUILD.tar.gz"
  wget "https://github.com/adoptium/temurin8-binaries/releases/download/jdk$OPENJDK_VERSION-$OPENJDK_BUILD/$FILE";
  printf '1633bd7590cb1cd72f5a1378ae8294451028b274d798e2a4ac672059a2f00fee *%s' "$FILE" | sha256sum --check;
  tar xf "$FILE" --exclude="jdk$OPENJDK_VERSION-$OPENJDK_BUILD"/{man,sample};
  rm "$FILE";
)
printf 'Maintainer: Your Name <mail@example.com>
Section: java
Priority: optional
Package: foo
Version: %s
Architecture: amd64
Description: alternate package for jdk8
' "8.352.8" > foo/DEBIAN/control;
dpkg-deb --build --root-owner-group foo;
sudo dpkg -i foo.deb;

postinstall and prerm

While putting some files on the disk might be good enough, sometimes we might want to make other changes.

The main issue of the previous examples is that the program is not in PATH, so the user needs, for example, to type /opt/nvim/bin/nvim, or add manually /opt/nvim/bin/ to PATH.

The easiest solution would be to create a symlink between foo/opt/nvim/bin/nvim and something under usr/bin/ (or just copy the binary to /usr/bin in this case), as /usr/bin is part of the PATH. Calling the file nvim would cause a conflict with the official package neovim, so better to provide an alternate name, like /usr/bin/foo-nvim.

NVIMVERSION="0.8.2";
mkdir -p foo/{opt/nvim/bin,DEBIAN,usr/bin}/;
wget https://github.com/neovim/neovim/releases/download/v$NVIMVERSION/nvim.appimage -O foo/opt/nvim/bin/nvim;
printf 'bb0d4599cb506fc6e29bf0e9cef3b52e06dcb4bb930b56d6eb88320f1d46a908 *foo/opt/nvim/bin/nvim' | sha256sum --check;
chmod 755 foo/opt/nvim/bin/nvim;
ln -fs /opt/nvim/bin/nvim foo/usr/bin/foo-nvim;
printf 'Maintainer: Your Name <mail@example.com>
Section: misc
Priority: optional
Package: foo
Version: %s
Architecture: amd64
Description: alternate package for nvim
' "$NVIMVERSION" > foo/DEBIAN/control;
dpkg-deb --build --root-owner-group foo;
sudo dpkg -i foo.deb;

But generally, we might want to make other changes, like adding Neovim as a possible alternative for a command-line editor

This can be done with a separate script, executed after extracting all files: DEBIAN/postinst.

NVIMVERSION="0.8.2";
mkdir -p foo/{opt/nvim/bin,DEBIAN,usr/bin}/;
wget https://github.com/neovim/neovim/releases/download/v$NVIMVERSION/nvim.appimage -O foo/opt/nvim/bin/nvim;
printf 'bb0d4599cb506fc6e29bf0e9cef3b52e06dcb4bb930b56d6eb88320f1d46a908 *foo/opt/nvim/bin/nvim' | sha256sum --check;
chmod 755 foo/opt/nvim/bin/nvim;
ln -fs /opt/nvim/bin/nvim foo/usr/bin/foo-nvim
printf 'Maintainer: Your Name <mail@example.com>
Section: misc
Priority: optional
Package: foo
Version: %s
Architecture: amd64
Description: alternate package for nvim
' "$NVIMVERSION" > foo/DEBIAN/control;
printf '#!/bin/sh

set -e

update-alternatives --install /usr/bin/editor editor /opt/nvim/bin/nvim 50
' > foo/DEBIAN/postinst;
chmod +x foo/DEBIAN/postinst;
dpkg-deb --build --root-owner-group foo;
sudo dpkg -i foo.deb;

And after installing foo, /opt/nvim/bin/nvim can be configured as the default editor with sudo update-alternatives --set editor /opt/nvim/bin/nvim, but if we remove foo, then update-alternatives will have, as an option, an invalid entry, and on some system editor might be broken!

In this case, we do not have an uninstaller for free anymore, and we need to provide the appropriate logic in a script that is executed before the files are removed: DEBIAN/prerm.

NVIMVERSION="0.8.2";
mkdir -p foo/{opt/nvim/bin,DEBIAN,usr/bin}/;
wget https://github.com/neovim/neovim/releases/download/v$NVIMVERSION/nvim.appimage -O foo/opt/nvim/bin/nvim;
printf 'bb0d4599cb506fc6e29bf0e9cef3b52e06dcb4bb930b56d6eb88320f1d46a908 *foo/opt/nvim/bin/nvim' | sha256sum --check;
chmod 755 foo/opt/nvim/bin/nvim;
ln -fs /opt/nvim/bin/nvim foo/usr/bin/foo-nvim
printf 'Maintainer: Your Name <mail@example.com>
Section: misc
Priority: optional
Package: foo
Version: %s
Architecture: amd64
Description: alternate package for nvim
' "$NVIMVERSION" > foo/DEBIAN/control;
printf '#!/bin/sh

set -e

update-alternatives --install /usr/bin/editor editor /opt/nvim/bin/nvim 50
' > foo/DEBIAN/postinst;
printf '#!/bin/sh

set -e

update-alternatives --remove editor /opt/nvim/bin/nvim
' > foo/DEBIAN/prerm;
chmod +x foo/DEBIAN/{prerm,postinst};
dpkg-deb --build --root-owner-group foo;
sudo dpkg -i foo.deb;

A final note

Until now, all examples were about installing a locally created deb file.

When working with multiple environments and machines and with packages that can depend on each other, this approach might not work as well.

Creating a repository should be the preferred way.

Also, the created packages are not good for redistribution, at least because the licenses are missing.

Also writing the control scripts manually is not very efficient.

Build systems like cmake 🗄️ are also able to create deb packages, otherwise, tools like dh-make 🗄️ can help to automate this task.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.