feat(image): auto-enable cross-arch chroot via qemu binfmt
Building a foreign-arch image (e.g. arm64 Raspberry Pi OS on an x86 host) needs a qemu binfmt handler, or the chroot fails with "Exec format error". crossarch detects this before the target is erased and, on confirmation, installs qemu-user-static via the host package manager (apt-get/dnf/zypper/pacman) and registers binfmt; otherwise it aborts with per-distro instructions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
71
lim/image/crossarch.py
Normal file
71
lim/image/crossarch.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""Ensure the host can chroot into a foreign-architecture image (qemu binfmt)."""
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from lim import runner, ui
|
||||||
|
from lim.errors import LimError
|
||||||
|
|
||||||
|
# Building natively needs no qemu; on these the chroot runs directly.
|
||||||
|
_NATIVE_ARM = ("aarch64", "armv7l", "armv6l", "arm64")
|
||||||
|
|
||||||
|
# Repo-based installs only; AUR helpers refuse to run from this root process.
|
||||||
|
_BINFMT_INSTALLERS = {
|
||||||
|
"apt-get": ["apt-get", "install", "-y", "qemu-user-static", "binfmt-support"],
|
||||||
|
"dnf": ["dnf", "install", "-y", "qemu-user-static"],
|
||||||
|
"zypper": ["zypper", "--non-interactive", "install", "qemu-linux-user"],
|
||||||
|
"pacman": ["pacman", "-S", "--needed", "--noconfirm", "qemu-user-static-binfmt"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_enabled(entry: Path) -> bool:
|
||||||
|
try:
|
||||||
|
return "enabled" in entry.read_text()
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def qemu_binfmt_enabled() -> bool:
|
||||||
|
binfmt = Path("/proc/sys/fs/binfmt_misc")
|
||||||
|
return binfmt.is_dir() and any(_entry_enabled(e) for e in binfmt.glob("qemu-*"))
|
||||||
|
|
||||||
|
|
||||||
|
def auto_enable() -> bool:
|
||||||
|
"""Install qemu-user-static via the host package manager and register binfmt."""
|
||||||
|
command = next((cmd for tool, cmd in _BINFMT_INSTALLERS.items() if shutil.which(tool)), None)
|
||||||
|
if command is None:
|
||||||
|
ui.warning("No supported host package manager found (apt-get/dnf/zypper/pacman).")
|
||||||
|
return False
|
||||||
|
runner.run(command, sudo=True, check=False)
|
||||||
|
runner.run(["systemctl", "restart", "systemd-binfmt"], sudo=True, check=False)
|
||||||
|
return qemu_binfmt_enabled()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_ready() -> None:
|
||||||
|
"""Make foreign-arch chroot work, or fail with actionable guidance.
|
||||||
|
|
||||||
|
Called before the target is erased, so a host that cannot run the image's
|
||||||
|
binaries aborts early instead of mid-build with a cryptic "Exec format error".
|
||||||
|
"""
|
||||||
|
if platform.machine() in _NATIVE_ARM:
|
||||||
|
return
|
||||||
|
if qemu_binfmt_enabled():
|
||||||
|
ui.info("Cross-architecture build: qemu binfmt handlers are registered.")
|
||||||
|
return
|
||||||
|
ui.warning(
|
||||||
|
f"Cross-architecture build: this host ({platform.machine()}) cannot run the "
|
||||||
|
"image's ARM binaries in chroot yet."
|
||||||
|
)
|
||||||
|
if ui.confirm("Install qemu-user-static and register binfmt now?"):
|
||||||
|
if auto_enable():
|
||||||
|
ui.success("qemu binfmt handlers registered.")
|
||||||
|
return
|
||||||
|
ui.warning("Automatic setup did not register a handler.")
|
||||||
|
raise LimError(
|
||||||
|
"Cross-architecture chroot is not available. Install qemu-user-static + binfmt "
|
||||||
|
"manually, or build on the target itself (e.g. the Pi booted from USB).\n"
|
||||||
|
" Arch/Manjaro : qemu-user-static + qemu-user-static-binfmt (AUR), "
|
||||||
|
"then: sudo systemctl restart systemd-binfmt\n"
|
||||||
|
" Debian/Ubuntu: sudo apt install qemu-user-static binfmt-support"
|
||||||
|
)
|
||||||
50
tests/unit/test_cross_arch.py
Normal file
50
tests/unit/test_cross_arch.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from lim.errors import LimError
|
||||||
|
from lim.image import crossarch
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrossArch:
|
||||||
|
def test_native_arm_is_ok(self, monkeypatch):
|
||||||
|
monkeypatch.setattr(crossarch.platform, "machine", lambda: "aarch64")
|
||||||
|
crossarch.ensure_ready()
|
||||||
|
|
||||||
|
def test_cross_arch_with_binfmt_ok(self, monkeypatch):
|
||||||
|
monkeypatch.setattr(crossarch.platform, "machine", lambda: "x86_64")
|
||||||
|
monkeypatch.setattr(crossarch, "qemu_binfmt_enabled", lambda: True)
|
||||||
|
crossarch.ensure_ready()
|
||||||
|
|
||||||
|
def test_declined_auto_install_raises(self, monkeypatch, answers):
|
||||||
|
monkeypatch.setattr(crossarch.platform, "machine", lambda: "x86_64")
|
||||||
|
monkeypatch.setattr(crossarch, "qemu_binfmt_enabled", lambda: False)
|
||||||
|
answers("n")
|
||||||
|
with pytest.raises(LimError, match="Cross-architecture"):
|
||||||
|
crossarch.ensure_ready()
|
||||||
|
|
||||||
|
def test_auto_install_success_proceeds(self, monkeypatch, answers):
|
||||||
|
monkeypatch.setattr(crossarch.platform, "machine", lambda: "x86_64")
|
||||||
|
monkeypatch.setattr(crossarch, "qemu_binfmt_enabled", lambda: False)
|
||||||
|
monkeypatch.setattr(crossarch, "auto_enable", lambda: True)
|
||||||
|
answers("y")
|
||||||
|
crossarch.ensure_ready()
|
||||||
|
|
||||||
|
def test_auto_install_failure_raises(self, monkeypatch, answers):
|
||||||
|
monkeypatch.setattr(crossarch.platform, "machine", lambda: "x86_64")
|
||||||
|
monkeypatch.setattr(crossarch, "qemu_binfmt_enabled", lambda: False)
|
||||||
|
monkeypatch.setattr(crossarch, "auto_enable", lambda: False)
|
||||||
|
answers("y")
|
||||||
|
with pytest.raises(LimError, match="Cross-architecture"):
|
||||||
|
crossarch.ensure_ready()
|
||||||
|
|
||||||
|
def test_auto_enable_uses_detected_manager_and_registers(self, monkeypatch, fake_runner):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
crossarch.shutil, "which", lambda tool: "/bin/pacman" if tool == "pacman" else None
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(crossarch, "qemu_binfmt_enabled", lambda: True)
|
||||||
|
assert crossarch.auto_enable() is True
|
||||||
|
assert fake_runner.find("pacman", "-S", "qemu-user-static-binfmt")
|
||||||
|
assert fake_runner.find("systemctl", "restart", "systemd-binfmt")
|
||||||
|
|
||||||
|
def test_auto_enable_without_manager_returns_false(self, monkeypatch):
|
||||||
|
monkeypatch.setattr(crossarch.shutil, "which", lambda _tool: None)
|
||||||
|
assert crossarch.auto_enable() is False
|
||||||
Reference in New Issue
Block a user