From 59760335a1fe7ec692a2a613d6565507de8f9502 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 25 Sep 2026 13:05:29 -0400 Subject: fix: declare libreoffice-fresh, imv and git-lfs Each of the three backs something the dotfiles already ship, and each fails quietly rather than loudly when it's missing. libreoffice-fresh is what the mimeapps.list defaults for presentations, documents and spreadsheets resolve to. With the package absent, xdg-mime doesn't error. It falls through to the next application claiming the type, which on a machine carrying the winvm dotfiles is powerpoint.desktop. Every .pptx opened a Windows VM for a month and the only symptom was that it felt slow. imv is what gui-open --image execs. git-lfs is required by any repo tracking globs in LFS, which fails every checkout without it. All three were installed before the 2026-08-13 rebuild and gone after it, because nothing in the installer declared them. I made the test run supplemental_software with pacman_install stubbed as a recorder, asserting each package is invoked exactly once under both desktop environments. Running the function beats matching its source because only a run resolves the conditionals that could make "every machine installs this" false. A fourth assertion pins ranger present under dwm and absent under hyprland, so a harness that stopped resolving conditionals fails loudly instead of passing the other three vacuously. --- archsetup | 3 + .../test_dotfiles_dependency_packages.py | 119 +++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 tests/installer-steps/test_dotfiles_dependency_packages.py diff --git a/archsetup b/archsetup index ed5426d..a8eabd9 100755 --- a/archsetup +++ b/archsetup @@ -3320,6 +3320,7 @@ supplemental_software() { pacman_install fdupes # identify binary duplicates pacman_install filezilla # ftp gui pacman_install gimp # image editor + pacman_install git-lfs # large-file storage; repos tracking LFS globs fail checkout without it pacman_install gparted # disk partition utility pacman_install gst-plugin-pipewire # gstreamer audio plugin for pipewire pacman_install gst-plugins-base # gstreamer base audio plugins @@ -3331,9 +3332,11 @@ supplemental_software() { pacman_install gucharmap # gui display of character maps pacman_install gzip # compression tool pacman_install handbrake # video transcoder + pacman_install imv # wayland image viewer (gui-open --image execs it) pacman_install libconfig # library for processing structured config files pacman_install libmad # mpeg audio decoder pacman_install libmpeg2 # library for decoding mpeg video streams + pacman_install libreoffice-fresh # office suite; the mimeapps.list defaults resolve to its .desktop files pacman_install maim # screenshot utility pacman_install mosh # alt SSH terminal with roaming and responsiveness support pacman_install odt2txt # converts from open document to text diff --git a/tests/installer-steps/test_dotfiles_dependency_packages.py b/tests/installer-steps/test_dotfiles_dependency_packages.py new file mode 100644 index 0000000..58b7275 --- /dev/null +++ b/tests/installer-steps/test_dotfiles_dependency_packages.py @@ -0,0 +1,119 @@ +"""Pin the packages the dotfiles assume are present. + +Three packages are not optional extras: something the dotfiles ship depends +on each one, and when the package is missing the dependent silently does the +wrong thing rather than failing. + +- libreoffice-fresh :: common/.config/mimeapps.list maps presentations, + documents and spreadsheets to libreoffice-impress/-writer/-calc. With the + package absent those .desktop files don't exist, so xdg-mime falls through + to the next application claiming the type — on a machine with the winvm + dotfiles that is powerpoint.desktop, which boots a Windows VM to open a + deck (2026-09-18). +- imv :: gui-open --image execs imv, and without it every agent-side image + render fails with "required application is unavailable" (2026-09-18). +- git-lfs :: a repo tracking globs in LFS fails every checkout and merge + with "smudge filter lfs failed" (2026-09-20). + +All three were installed before the 2026-08-13 rebuild and absent after it, +which is the regression this pins: they are dependencies of shipped defaults, +so the installer has to declare them rather than leave them to whatever a +machine happens to carry. + +Method mirrors test_required_software: sed-extract supplemental_software from +the real `archsetup`, stub pacman_install as a recorder, run it, and assert +against what it actually invoked. Running the function beats matching its +source text, because the property under test is "every machine installs this" +and only a run resolves the conditionals that could make that false. The +function is run once per desktop environment for the same reason. + +Run from repo root: + python3 -m unittest tests.installer-steps.test_dotfiles_dependency_packages +""" + +import os +import subprocess +import textwrap +import unittest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") + +DEPENDENCY_PACKAGES = ("libreoffice-fresh", "imv", "git-lfs") + +# Every desktop environment the installer branches on inside this function. +DESKTOP_ENVS = ("dwm", "hyprland") + + +def declared_packages(desktop_env): + """Return (exit_code, [pacman package, ...]) for one desktop environment. + + Everything the function calls besides pacman_install is stubbed to a no-op, + so the run records package declarations and nothing else. aur_install is + deliberately separate: these three are pacman packages, and folding the two + recorders together would let an AUR declaration satisfy the pin. + """ + script = textwrap.dedent(f"""\ + desktop_env={desktop_env} + display() {{ :; }} + aur_install() {{ :; }} + mask_fwupd_passim() {{ :; }} + run_task() {{ :; }} + error_warn() {{ :; }} + pacman_install() {{ echo "$1"; }} + source <(sed -n '/^supplemental_software() {{/,/^}}/p' "{ARCHSETUP}") + supplemental_software + """) + result = subprocess.run( + ["bash", "-c", script], capture_output=True, text=True, timeout=30, + ) + return result.returncode, result.stdout.split() + + +class DotfilesDependencyPackages(unittest.TestCase): + # ------------------------------------------------------------ normal ---- + def test_each_dependency_package_is_installed(self): + rc, pkgs = declared_packages("hyprland") + self.assertEqual(rc, 0) + for package in DEPENDENCY_PACKAGES: + with self.subTest(package=package): + self.assertIn( + package, pkgs, + f"{package} is a dependency of a shipped dotfiles default " + "and must be declared", + ) + + # ---------------------------------------------------------- boundary ---- + def test_each_is_declared_exactly_once(self): + # A second declaration is dead weight and drifts out of sync with the + # first when one of them is edited. + rc, pkgs = declared_packages("hyprland") + self.assertEqual(rc, 0) + for package in DEPENDENCY_PACKAGES: + with self.subTest(package=package): + self.assertEqual(pkgs.count(package), 1) + + # ------------------------------------------------------------- error ---- + def test_none_is_gated_behind_a_desktop_environment(self): + # The dotfiles defaults that need these apply on every DE, so a + # declaration reachable under only one of them would leave the same + # hole on the other. + for env in DESKTOP_ENVS: + rc, pkgs = declared_packages(env) + self.assertEqual(rc, 0) + for package in DEPENDENCY_PACKAGES: + with self.subTest(desktop_env=env, package=package): + self.assertIn(package, pkgs) + + def test_harness_observes_desktop_environment_gating(self): + # The control for the test above: ranger IS gated to dwm on purpose, so + # if this run can't see that, the DE-independence assertion is vacuous + # and would pass against a gated package too. + _, dwm = declared_packages("dwm") + _, hyprland = declared_packages("hyprland") + self.assertIn("ranger", dwm) + self.assertNotIn("ranger", hyprland) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3