From 68ea39b78441323ea2e54d883657dfd1519ffb2a Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 23 Jul 2026 01:56:02 +0200 Subject: [PATCH] chore(session): harden chroot package installs (PATH + apt-get update) - chroot_bash exports a Debian-safe PATH so /usr/sbin tools (update-initramfs, useradd, chpasswd, ...) resolve inside the chroot instead of failing with code 127. - install_packages runs apt-get update before install (a stock image ships stale lists whose superseded .deb URLs 404) and non-interactive apt-get install -y; pacman gains -Sy for the same index-refresh reason. Co-Authored-By: Claude Opus 4.8 --- lim/image/session.py | 18 ++++++++++++++---- tests/unit/test_image_setup.py | 7 +++++-- tests/unit/test_tor.py | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lim/image/session.py b/lim/image/session.py index cf3981f..4136e83 100644 --- a/lim/image/session.py +++ b/lim/image/session.py @@ -161,11 +161,16 @@ class ImageSession: ui.warning("Failed.") +# The host PATH leaks into the chroot; force one that includes the sbin dirs +# where Debian keeps update-initramfs, useradd, chpasswd, ... (else code 127). +_CHROOT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + + def chroot_bash(root_mount_path: Path | str, script: str, error_msg: str | None = None) -> None: - """Run a bash script inside the image via chroot.""" + """Run a bash script inside the image via chroot (with a Debian-safe PATH).""" runner.run( ["chroot", str(root_mount_path), "/bin/bash"], - input_text=script, + input_text=f"export PATH={_CHROOT_PATH}\n{script}", sudo=True, error_msg=error_msg, ) @@ -175,8 +180,13 @@ def install_packages(distribution: str, root_mount_path: Path, package_names: st """Install packages inside the image with the distribution's package manager.""" ui.info(f"Installing {package_names}...") if distribution in ("arch", "manjaro"): - chroot_bash(root_mount_path, f"pacman --noconfirm -S --needed {package_names}") + chroot_bash(root_mount_path, f"pacman --noconfirm -Sy --needed {package_names}") elif distribution in ("moode", "retropie", "raspios"): - chroot_bash(root_mount_path, f"yes | apt install {package_names}") + # apt-get update first: a stock image ships stale lists whose old .deb + # URLs 404 once the mirror has superseded them. + chroot_bash( + root_mount_path, + f"apt-get update\nDEBIAN_FRONTEND=noninteractive apt-get install -y {package_names}", + ) else: raise LimError("Package manager not supported.") diff --git a/tests/unit/test_image_setup.py b/tests/unit/test_image_setup.py index b47d6e9..f4b48ea 100644 --- a/tests/unit/test_image_setup.py +++ b/tests/unit/test_image_setup.py @@ -101,7 +101,8 @@ class TestInstallPackages: for kind, cmd, input_text in fake_runner.calls if kind == "run" and cmd[0] == "chroot" ] - assert chroot_calls == ["pacman --noconfirm -S --needed btrfs-progs"] + assert len(chroot_calls) == 1 + assert "pacman --noconfirm -Sy --needed btrfs-progs" in chroot_calls[0] def test_apt_for_retropie(self, tmp_path, fake_runner): install_packages("retropie", tmp_path, "btrfs-progs") @@ -110,7 +111,9 @@ class TestInstallPackages: for kind, cmd, input_text in fake_runner.calls if kind == "run" and cmd[0] == "chroot" ] - assert chroot_calls == ["yes | apt install btrfs-progs"] + assert len(chroot_calls) == 1 + assert "apt-get update" in chroot_calls[0] + assert "apt-get install -y btrfs-progs" in chroot_calls[0] def test_unsupported_distribution_raises(self, tmp_path, fake_runner): with pytest.raises(LimError): diff --git a/tests/unit/test_tor.py b/tests/unit/test_tor.py index 8513f86..b8061b1 100644 --- a/tests/unit/test_tor.py +++ b/tests/unit/test_tor.py @@ -189,7 +189,7 @@ class TestInitramfsToolsBackend: apt = [ text for _, _, text in fake_runner.calls - if text and "apt install" in text and "tor busybox" in text + if text and "apt-get install" in text and "tor busybox" in text ] assert len(apt) == 1