-
Notifications
You must be signed in to change notification settings - Fork 0
Backend Internals
This document is intended for developers and contributors looking to understand how pkgwrap abstracts package managers and how to implement new ones.
When a user runs a command like pkgwrap install vim, the execution follows a specific layered flow:
-
CLI Layer (
cli.py): Parses the user's arguments, detects if the user is already running as root (viaos.geteuid()), and captures flags like-y(auto-yes). -
Detection Layer (
detector.py): Identifies the host operating system's primary package manager and returns its string identifier (e.g.,"apt"or"pacman"). -
Registry & Instantiation (
backends/__init__.py): Maps the string identifier to the correspondingBackendsubclass and instantiates it. -
Backend Implementation (
base.py& Subclasses): The specific subclass formats the exact shell command needed (e.g.,["apt", "install", "-y", "vim"]) and passes it back up to the base class's_run_command()method. -
Execution (
base.py): The_run_command()method handles any necessary privilege escalation (sudo), user confirmations, and error handling before safely invoking the native command viasubprocess.run().
All package managers must implement the Backend abstract base class located in src/pkgwrap/backends/base.py.
Subclasses must define their name property and implement the following abstract methods:
install(self, package: str, already_root: bool = False, auto_yes: bool = False)remove(self, package: str, already_root: bool = False, auto_yes: bool = False)update(self, already_root: bool = False, auto_yes: bool = False)search(self, query: str, already_root: bool = False, auto_yes: bool = False)
Each method is responsible for constructing the native command as a list of strings and passing it to self._run_command(). They must return a subprocess.CompletedProcess object.
When a subclass calls self._run_command(), or when cli.py calls a backend method, three critical boolean parameters control the execution context:
-
require_sudo: Hardcoded by the backend implementation. Dictates whether the native package manager requires root privileges for a specific operation. For example,AptBackend.install()sets this toTrue, butBrewBackend.install()sets it toFalse. -
already_root: Passed down fromcli.py. IfTrue, the user executedpkgwrapas root (e.g.,sudo pkgwrap install). This tells the base class to skip prependingsudoand to skip confirmation prompts. -
auto_yes: Passed down fromcli.pyif the user provided the-yflag. This tells the base class to skip the "Do you want to proceed with sudo?" prompt, and it is usually passed directly into the native command (e.g., adding-yto theaptcommand).
The detect_backend() function is the brain of the wrapper. To ensure speed, it first checks a local cache (a temporary file storing the last detected backend). If there is a cache miss, it runs a detection sequence.
Because both Termux (Android) and FreeBSD use a package manager named pkg, we cannot rely purely on shutil.which("pkg"). Furthermore, Termux includes an apt wrapper by default.
-
Termux (Highest Priority): The detector explicitly checks
os.environforPREFIXcontainingcom.termuxor the presence ofTERMUX_VERSION. If found, it immediately returns thepkgbackend for Termux, ignoring anyaptbinaries on the system. -
FreeBSD: The detector checks if
platform.system().lower() == "freebsd". If true andpkgis available, it routes to thefreebsd_backend.
If neither Termux nor FreeBSD is detected, detector.py iterates through a prioritized list of generic package managers using shutil.which(). The priority order is carefully chosen to favor standard system managers over secondary ones:
-
apt(Debian/Ubuntu) -
pacman(Arch) -
dnf(Fedora/RHEL) -
apk(Alpine) -
zypper(openSUSE) -
xbps-install(Void Linux) -
nix-env(NixOS) -
eopkg(Solus) -
brew(macOS / Linuxbrew)
The first available binary triggers a cache write and returns the corresponding backend identifier.