Packaging¶
quadletman is distributed as native OS packages — RPM for Fedora/RHEL-family systems and
DEB for Ubuntu/Debian-family systems. Both build scripts live under packaging/.
Package architecture¶
Both packages are architecture-dependent (Architecture: any in the DEB control file;
BuildArch left unset in the RPM spec so it inherits the build host arch). This is
intentional: the packages bundle a complete Python virtualenv — including compiled C
extensions from dependencies such as psutil and python-pam — directly inside the
package. The .so files compiled during dpkg-buildpackage / rpmbuild are
architecture-specific binaries and cannot be shared across CPU architectures.
Keeping compilation at build time (rather than install time) means:
- The target machine needs no compiler, no Python headers, and no
libpam0g-dev/pam-devel. - Installation is fast and deterministic — no network access or pip invocation at install time.
- The package is self-contained: the bundled venv under
/usr/lib/quadletman/venv/is the sole runtime dependency for Python code.
If you need to support a different CPU architecture, build the package on (or cross-compile
for) that architecture. A separate .deb or .rpm is produced per arch; this is standard
practice for compiled packages.
Building packages¶
The VERSION environment variable controls the package version. If not set, the build
scripts derive it automatically from the nearest annotated git tag (e.g. v0.3.1 →
0.3.1). CI passes VERSION explicitly when building release packages.
RPM (Fedora / RHEL / AlmaLinux / Rocky Linux)¶
Build script: packaging/build-rpm.sh
Spec file: packaging/rpm/quadletman.spec
The spec %build section creates a virtualenv and pip-installs the app with all
dependencies. The resulting venv is copied into %{_libdir}/quadletman/venv/ by the
%install section.
Build dependencies (install once):
Runtime dependencies (declared in the spec Requires: field):
Build and install:
DEB (Ubuntu / Debian)¶
Build script: packaging/build-deb.sh
Packaging files: packaging/debian/
The debian/rules file overrides dh_auto_build to:
- Create a fresh virtualenv under
debian/quadletman-venv/. pip installthe app and all dependencies (including C extensions) into that venv.- Copy the compiled venv into the package staging tree at
usr/lib/quadletman/venv/.
A small wrapper script at /usr/bin/quadletman sets PYTHONPATH explicitly and executes
the bundled Python interpreter, so the service does not rely on pyvenv.cfg symlink
resolution (which can fail in some distro configurations).
Build dependencies (installed automatically by build-deb.sh if missing):
Runtime dependencies (declared in debian/control):
Note that libpam0g-dev is not a runtime dependency — it is only needed at build time
for compiling python-pam. The compiled .so links against libpam.so.0 which is
provided by libpam0g.
Build and install:
Upgrading¶
Build the new package from the updated source tree, then install over the existing package. The service applies any pending database migrations automatically on startup.
RPM-based systems¶
bash packaging/build-rpm.sh
sudo dnf upgrade ~/rpmbuild/RPMS/*/quadletman-*.rpm
sudo systemctl restart quadletman
DEB-based systems¶
CI release builds¶
Pushing an annotated tag to main triggers the release workflow
(.github/workflows/release.yml), which runs the following parallel jobs:
- CI gate — full test suite; all downstream jobs depend on this.
- build-wheel — builds the Python wheel via
uv build --wheel(platform-independent). - build-rpm — builds an RPM inside a Fedora container using
packaging/build-rpm.sh. - build-deb — builds a
.debon Ubuntu usingpackaging/build-deb.sh. - publish — collects all artifacts, extracts the release notes from
CHANGELOG.md, and creates a GitHub Release with the wheel, RPM, and DEB attached.
See docs/ways-of-working.md for the full release step-by-step.
Smoke testing¶
Vagrant-based VMs let you build and install real packages on clean systems and verify the application works end-to-end.
| VM | Base box | Package | Port | Extra checks |
|---|---|---|---|---|
| fedora (primary) | bento/fedora-41 |
RPM | localhost:8081 |
SELinux AVC denials |
| ubuntu | bento/ubuntu-24.04 |
DEB | localhost:8082 |
— |
Both VMs run the same HTTP smoke tests: login via PAM, authenticated GET, unauthenticated redirect. The Fedora VM additionally verifies there are no SELinux AVC denials.
What the smoke tests verify¶
- Package builds cleanly from the current source tree.
- Service starts and reaches
active (running)state within 10 seconds. - Authenticated GET / returns HTTP 200 (PAM auth works, app responds).
- Unauthenticated GET / returns HTTP 302/303 (auth is enforced).
- No SELinux AVC denials attributed to
quadletman(Fedora only).
Prerequisites¶
Linux bare-metal (recommended)¶
Install Vagrant and the libvirt provider:
# Fedora / RHEL
sudo dnf install -y vagrant libvirt libvirt-devel virt-install qemu-kvm
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt $USER # log out and back in afterwards
vagrant plugin install vagrant-libvirt
Windows host (including WSL2)¶
WSL2 does not expose /dev/kvm, so libvirt cannot be used from WSL2. Use VirtualBox on
the Windows host instead, then invoke Vagrant from WSL2 or a Windows terminal.
Install Vagrant and VirtualBox on the Windows host using winget. Open a PowerShell or Command Prompt window (not WSL2) and run:
winget install --id HashiCorp.Vagrant --source winget --silent
winget install --id Oracle.VirtualBox --source winget --silent
Then restart Windows (VirtualBox installs a kernel driver that requires a reboot).
After reboot, verify the installs:
Ensure vagrant.exe is on your PATH — the winget installer adds it automatically, but
open a new terminal after the restart to pick up the updated PATH.
Note: The winget VirtualBox installer does not add
VBoxManagetoPATH. If theVBoxManage --versioncheck above fails, add the VirtualBox install directory manually: open System Properties → Environment Variables and appendC:\Program Files\Oracle\VirtualBoxto the user or systemPath. Vagrant itself locates VirtualBox through its own detection logic and does not rely onPATH.
From WSL2 you can call vagrant.exe directly:
Or open a Windows terminal, cd to the project directory, and run vagrant up fedora there.
Why not libvirt on WSL2? WSL2 runs inside a Hyper-V VM. Microsoft does not expose
/dev/kvmto the WSL2 kernel by default, so KVM-backed virtualisation is not available. VirtualBox runs on the Windows host outside WSL2 and does not have this restriction.
macOS¶
Install Vagrant and VirtualBox via Homebrew:
First-time setup¶
vagrant box add bento/fedora-41 # Fedora VM (~700 MB)
vagrant box add bento/ubuntu-24.04 # Ubuntu VM (~700 MB)
Running the smoke tests¶
The Fedora VM is the primary — vagrant ssh without a name connects to it. The Ubuntu VM
has autostart: false so vagrant up without arguments starts only Fedora.
At the end of a successful run you will see:
============================================================
All smoke tests passed.
UI: http://localhost:8081/ (Fedora)
UI: http://localhost:8082/ (Ubuntu)
Auth: smoketest / smoketest
============================================================
Re-testing after code changes¶
vagrant rsync fedora && vagrant provision fedora # Fedora only
vagrant rsync ubuntu && vagrant provision ubuntu # Ubuntu only
Inspecting the VMs¶
vagrant ssh fedora # shell into Fedora VM
vagrant ssh ubuntu # shell into Ubuntu VM
sudo journalctl -u quadletman -f # follow service logs (either VM)
sudo ausearch -m avc -ts today -c quadletman # SELinux denials (Fedora only)
sudo getenforce # confirm Enforcing mode (Fedora only)
Tearing down¶
vagrant destroy -f # delete all VMs
vagrant destroy fedora -f # delete only Fedora VM
vagrant destroy ubuntu -f # delete only Ubuntu VM
Choosing a provider explicitly¶
Vagrant auto-selects the provider. To force one: