""" Installer for Debian/Ubuntu packages defined via debian/control. This installer: 1. Installs build dependencies via `apt-get build-dep ./` 2. Uses dpkg-buildpackage to build .deb packages from debian/* 3. Installs the resulting .deb files via `dpkg -i` It is intended for Debian-based systems where dpkg-buildpackage and apt/dpkg tooling are available. """ from __future__ import annotations import glob import os import shutil from pkgmgr.actions.install.context import RepoContext from pkgmgr.actions.install.installers.base import BaseInstaller from pkgmgr.core.command.run import run_command class DebianControlInstaller(BaseInstaller): """ Build and install a Debian/Ubuntu package from debian/control. This installer is responsible for the full build + install of the application on Debian-like systems. """ # Logical layer name, used by capability matchers. layer = "os-packages" CONTROL_DIR = "debian" CONTROL_FILE = "control" def _is_debian_like(self) -> bool: """Return True if this looks like a Debian-based system.""" return shutil.which("dpkg-buildpackage") is not None def _control_path(self, ctx: RepoContext) -> str: return os.path.join(ctx.repo_dir, self.CONTROL_DIR, self.CONTROL_FILE) def supports(self, ctx: RepoContext) -> bool: """ This installer is supported if: - we are on a Debian-like system (dpkg-buildpackage available), and - debian/control exists. """ if not self._is_debian_like(): return False return os.path.exists(self._control_path(ctx)) def _find_built_debs(self, repo_dir: str) -> list[str]: """ Find .deb files built by dpkg-buildpackage. By default, dpkg-buildpackage creates .deb files in the parent directory of the source tree. """ parent = os.path.dirname(repo_dir) pattern = os.path.join(parent, "*.deb") return sorted(glob.glob(pattern)) def _privileged_prefix(self) -> str | None: """ Determine how to run privileged commands: - If 'sudo' is available, return 'sudo '. - If we are running as root (e.g. inside CI/container), return ''. - Otherwise, return None, meaning we cannot safely elevate. Callers are responsible for handling the None case (usually by warning and skipping automatic installation). """ sudo_path = shutil.which("sudo") is_root = False try: is_root = os.geteuid() == 0 except AttributeError: # pragma: no cover - non-POSIX platforms # On non-POSIX systems, fall back to assuming "not root". is_root = False if sudo_path is not None: return "sudo " if is_root: return "" return None def _install_build_dependencies(self, ctx: RepoContext) -> None: """ Install build dependencies using `apt-get build-dep ./`. This is a best-effort implementation that assumes: - deb-src entries are configured in /etc/apt/sources.list*, - apt-get is available on PATH. Any failure is treated as fatal (SystemExit), just like other installer steps. """ if shutil.which("apt-get") is None: print( "[Warning] apt-get not found on PATH. " "Skipping automatic build-dep installation for Debian." ) return prefix = self._privileged_prefix() if prefix is None: print( "[Warning] Neither 'sudo' is available nor running as root. " "Skipping automatic build-dep installation for Debian. " "Please install build dependencies from debian/control manually." ) return # Update package lists first for reliable build-dep resolution. run_command( f"{prefix}apt-get update", cwd=ctx.repo_dir, preview=ctx.preview, ) # Install build dependencies based on debian/control in the current tree. # `apt-get build-dep ./` uses the source in the current directory. builddep_cmd = f"{prefix}apt-get build-dep -y ./" run_command(builddep_cmd, cwd=ctx.repo_dir, preview=ctx.preview) def run(self, ctx: RepoContext) -> None: """ Build and install Debian/Ubuntu packages from debian/*. Steps: 1. apt-get build-dep ./ (automatic build dependency installation) 2. dpkg-buildpackage -b -us -uc 3. sudo dpkg -i ../*.deb (or plain dpkg -i when running as root) """ control_path = self._control_path(ctx) if not os.path.exists(control_path): return # 1) Install build dependencies self._install_build_dependencies(ctx) # 2) Build the package build_cmd = "dpkg-buildpackage -b -us -uc" run_command(build_cmd, cwd=ctx.repo_dir, preview=ctx.preview) # 3) Locate built .deb files debs = self._find_built_debs(ctx.repo_dir) if not debs: print( "[Warning] No .deb files found after dpkg-buildpackage. " "Skipping Debian package installation." ) return prefix = self._privileged_prefix() if prefix is None: print( "[Warning] Neither 'sudo' is available nor running as root. " "Skipping automatic .deb installation. " "You can manually install the following files with dpkg -i:\n " + "\n ".join(debs) ) return # 4) Install .deb files install_cmd = prefix + "dpkg -i " + " ".join(os.path.basename(d) for d in debs) parent = os.path.dirname(ctx.repo_dir) run_command(install_cmd, cwd=parent, preview=ctx.preview)