feat: zero-mount bootloader installation
Metadata
Current evaluation
Merged to main (commit 0ca6f6e). Replaces privileged loop-device+chroot GRUB install (pack/grubutil.py) with a zero-mount bootloader package staging files into prime dirs; +3415/-1275 across 31 files.
Resolution: closed resolved
Reason: PR #437 by maintainer lengau was approved and merged into main as commit 0ca6f6e, deleting pack/grubutil.py and adding imagecraft/pack/bootloader/. The related earlier attempt #416 was closed unmerged and superseded by this merge.
Evaluated by: mr-cal-bot
No scores available.
Issue body
So this ended up having way more than I intended but it sort of fit all the puzzle pieces together. AI-generated summary is below, including an AI-generated walkthrough of the code and suggestion of how to review.
IMAGECRAFT-176
## Summary
Replaces imagecraft's privileged loop-device + chroot GRUB installation (`imagecraft.pack.grubutil`) with a new pre-format-staging bootloader installer under `imagecraft/pack/bootloader/`.
Instead of attaching loop devices and mounting partitions, this:
1. Stages bootloader files (`/etc/fstab`, `/boot/grub/grub.cfg`, EFI binaries) directly into partition **prime directories** before formatting, so `mke2fs -d`/`mcopy` embed them automatically.
2. Pre-generates filesystem IDs and passes them to the formatters (`mke2fs -U` for ext, `mkfs.fat -i` for FAT — FAT volume IDs use the `XXXX-XXXX` serial form GRUB probes) so they match what's baked into `fstab`/`grub.cfg`.
3. Generates `grub.cfg` by running the guest rootfs's own `grub-mkconfig` in a prime-dir chroot, with a `grub-probe` shim bind-mounted over the guest binary (the chroot root is a plain directory that the real `grub-probe` cannot resolve to a device in unprivileged containers) answering with the pre-generated IDs.
4. For BIOS targets, mounts the formatted image's root partition read-write via `fuse2fs` (using the existing `imagecraft.utils.mount.ExtFuseMount`) and runs the guest's own `grub-mkimage` + `grub-bios-setup` chrooted into that mount, with the image bind-mounted at its host path and a generated `device.map`. Inside the chroot, `/proc/self/mountinfo` shows the fuse mount as `/`, so `grub-bios-setup`'s mountinfo-based device resolution works unmodified; `grub-bios-setup` writes Sector 0 and embeds `core.img` (post-MBR gap or BIOS Boot partition) itself. No loop devices, no byte-level patching.
For native-arch builds this removes the old approach's loop-device requirements (no `losetup`, no partition mounts). Cross-architecture builds still need binfmt/QEMU for the chrooted guest tools — unchanged from the previous `grubutil` flow, which also chrooted and executed guest-architecture binaries.
This is intended to be **non-breaking**: treating `imagecraft` as a CLI black box, any `imagecraft.yaml` that packed successfully before this change should still pack successfully after it.
## Reviewing this PR
Start with the execution flow rather than reading the new package alphabetically. The key boundary is **before versus after filesystem formatting**: files written to prime directories must be staged before formatting; BIOS boot sectors are installed into the finalized image afterwards.
**Scope clarification:** the bootloader no longer needs kernel-mounted image partitions, but the wider `ImagecraftPackService.pack()` flow still calls `ImageService.attach_images()` and formats loop-device partitions. This PR does not make the entire pack operation loopless. Chrooted guest tools still require binfmt/QEMU for foreign-architecture execution.
1. **Read the integration first:** `imagecraft/services/pack.py`, then `bootloader/installer.py`. Follow one EFI image and one BIOS image through staging, formatting, detachment, and final installation.
2. **Trace a separate `/boot` layout:** follow its prime directory and filesystem ID through `installer.py`, `mkconfig.py`, `efi.py` or `bios.py`, and `diskutil.py`. The formatter, GRUB search configuration, and fstab must agree. Kernel `root=UUID=` must still identify the root filesystem, not `/boot`.
3. **Inspect the environment boundaries:** `mkconfig.py`, `bios.py`, `chrootenv.py`, and `pack/chroot.py`. Pay particular attention to temporary files, bind mounts, cleanup on failure, and the distinction between missing tooling (a reported skip) and command failures (errors).
4. **Read the regression cases alongside the implementation:** `tests/unit/pack/bootloader/`, `tests/unit/pack/test_diskutil.py`, and `tests/unit/services/test_pack.py`. The four GRUB pack scenarios under `tests/spread/pack/` cover EFI/BIOS with shared or separate `/boot`; their log expectations change with the new flow. Unit-level command assertions are not a substitute for checking that an image actually boots.
## Code walkthrough
### 1. Coordinate the two installation phases
`ImagecraftPackService.pack()` creates and attaches the image, constructs `BootloaderInstaller`, and obtains its `partition_uuids` mapping. It passes the lifecycle's prime-directory resolver and the project's filesystem mount mapping into `prepare_rootfs()`.
After staging, it formats each partition with that partition's assigned ID, verifies the images, and detaches loop devices in a `finally` block. It then finalizes the image paths and calls `install_image_boot_code()`; that last phase only acts on a successfully prepared BIOS target.
### 2. Select partitions, IDs, and boot method
`imagecraft/models/volume.py` provides the root, ESP, dedicated boot, and BIOS Boot partition lookups. Hybrid type strings are normalized for GPT-type comparisons; ESP and raw BIOS Boot partitions are excluded from the dedicated boot lookup.
`bootloader/installer.py` uses those lookups and the architecture specifications in `const.py` to choose EFI, BIOS, or no installation. An ESP selects EFI; supported PC architectures can use BIOS on MBR or with a GPT BIOS Boot partition.
The installer assigns conventional UUIDs to ext filesystems and `XXXX-XXXX` volume IDs to FAT filesystems. `configure_fstab()` updates the root entry and, when the mount mapping associates `/boot` with the selected boot partition, its boot entry. Existing entry fields are retained while the source is updated. `diskutil.format_device()` applies the same IDs through `mke2fs -U` or `mkfs.fat -i`.
### 3. Generate the main GRUB configuration
`bootloader/mkconfig.py` runs the guest's `grub-mkconfig` in a chroot rooted at the root prime directory. A dedicated boot prime is bind-mounted at `/boot`. Because the real `grub-probe` cannot discover the future image filesystem from these ordinary directories, a shell shim supplies device, UUID, and partition-map answers.
A transient defaults snippet and device placeholders support configuration generation; the snippet's previous content is restored when present. For separate `/boot`, `_fs_internal_path()` and `_strip_boot_prefix()` translate generated file operands to boot-filesystem-relative paths without rewriting kernel arguments. These helpers deserve close attention because build-host paths must not leak into boot-time paths.
### 4. Stage the EFI bootloader
`bootloader/efi.py` tries signed shim/GRUB binaries first, then an unsigned prebuilt GRUB binary, then an in-chroot `grub-mkimage` build. It places the selected files in the ESP's `EFI/BOOT` and `EFI/ubuntu` directories.
The early configuration rendered by `const.py` searches for the filesystem containing the main GRUB configuration. Its prefix is `/boot/grub` for shared root/boot, or `/grub` for a dedicated boot partition. `chrootenv.stage_grub_modules()` copies runtime modules into the appropriate prime directory where needed, before formatting embeds them.
### 5. Install BIOS boot code
BIOS preparation stages runtime modules before formatting. Afterwards, `PCBiosInstaller` in `bootloader/bios.py` locates the root partition's byte offset, including the extended-container numbering adjustment for MBR layouts with more than four entries.
It mounts that filesystem through `ExtFuseMount`, builds a chroot on the mount, and bind-mounts the image at its host pathname inside the chroot. The guest's `grub-mkimage` builds `core.img`, and `grub-bios-setup` performs the boot-sector and embedding writes using a generated device map. This leaves GRUB responsible for the disk-level installation rather than introducing Python sector-patching code. FAT boot layouts require `fat.mod` in the core image.
### 6. Check supporting helpers and removed paths
`bootloader/chrootenv.py` centralizes guest-tool discovery, the restricted command PATH, error conversion, and module staging. `pack/chroot.py` retains the existing chroot machinery and adds `build_prime_chroot()` to assemble the required mounts.
Finally, compare the deleted `pack/grubutil.py` and `pack/image.py` with the coordinator and service flow. Their responsibilities move into staging, the existing image service, and the BIOS installer; the old unit tests are replaced by coverage at those boundaries.
Evaluation history
| Date | Model | Scores | Action | Summary |
|---|---|---|---|---|
| stealth/space-bunny-alpha | — | closed resolved | Merged to main (commit 0ca6f6e). Replaces privileged loop-device+chroot GRUB install (pack/grubutil.py) with a zero-mount bootloader package staging files into prime dirs; +3415/-1275 across 31 files. | |
| qwen/qwen3.8-27b | — | closed resolved | Merged into main. Replaces imagecraft's privileged loop-device + chroot GRUB install with a zero-mount bootloader installer (imagecraft/pack/bootloader/) that stages files into prime dirs, pre-generates fs IDs, and uses fuse2fs for BIOS. +3415/-1275, 31 files. | |
| qwen/qwen3.8-27b |
Impact:
75
Quick Win:
4
Complexity:
90
Confidence:
75
Actionability:
60
|
needs review | Rewrites imagecraft's GRUB installation from loop-device+chroot to a zero-mount bootloader package (pre-format staging, pre-generated fs IDs, fuse2fs-based BIOS setup). Approved by 2 reviewers; 1 unresolved comment, OSV-scanner failing, spread boot checks pending; author just merged main and fixed lint. | |
| qwen/qwen3.8-27b |
Impact:
75
Quick Win:
4
Complexity:
90
Confidence:
70
Actionability:
60
|
needs review | Replaces loop-device+chroot GRUB installation with a zero-mount bootloader package (prime-dir staging, pre-generated fs IDs, fuse2fs-based BIOS setup). Approved; conflicts resolved and lint fixed in d1860e2; OSV-scanner failing, snap builds pending. | |
| qwen/qwen3.8-27b |
Impact:
72
Quick Win:
5
Complexity:
85
Confidence:
75
Actionability:
45
|
keep open | Replaces imagecraft's loop-device GRUB installer with an unprivileged zero-mount bootloader package (staging, pre-generated fs IDs, chrooted grub-mkconfig, fuse-based BIOS setup). +3412/-1269, 31 files. Approved; 4 CI checks failing, 3 pending. | |
| qwen/qwen3.8-27b |
Impact:
70
Quick Win:
6
Complexity:
85
Confidence:
75
Actionability:
55
|
needs review | Replaces loop-device+chroot GRUB installation with a pre-format staging bootloader package (EFI/BIOS, shared/dedicated /boot). Approved, 2 unresolved comments; OSV-scanner failing, boot spread pending; conflicts fixed in d1860e2, awaiting re-review. | |
| qwen/qwen3.8-27b |
Impact:
65
Quick Win:
4
Complexity:
85
Confidence:
70
Actionability:
40
|
keep open | Replaces privileged loop-device/chroot GRUB install (grubutil) with a zero-mount bootloader package staging files into prime dirs, pre-generating fs IDs, and running guest grub-mkconfig/mkimage via chroot/fuse. Approved, but 8 unresolved comments and spread boot CI suites failing; author just pushed conflict/lint fix. | |
| stealth/space-bunny-alpha |
Impact:
68
Quick Win:
5
Complexity:
85
Confidence:
64
Actionability:
45
|
needs review | Replaces loop-device+chroot GRUB install with a zero-mount bootloader package that stages files into prime dirs pre-format. Approved by 1 reviewer, 5 unresolved comments, amd64 spread suite failing. | |
| stealth/space-bunny-alpha |
Impact:
82
Quick Win:
4
Complexity:
93
Confidence:
78
Actionability:
78
|
needs review | Replaces Imagecraft’s privileged loop-device/chroot GRUB installation with pre-format staging and post-format BIOS setup using FUSE; 31 files and substantial unit/spread coverage are updated. All checks pass, but no reviewer has evaluated the large architectural change. | |
| qwen/qwen3.6-35b-a3b |
Impact:
75
Quick Win:
11
Staleness:
5
Complexity:
85
Confidence:
75
|
needs review | Replaces privileged loop-device GRUB installation with pre-format-staging bootloader installer, removing loop device requirements for EFI/BIOS boot code installation. All CI passing, 0 reviewers. | |
| qwen/qwen3.6-35b-a3b |
Impact:
65
Quick Win:
14
Staleness:
5
Complexity:
78
Confidence:
75
|
needs review | Replaces privileged loop-device grubutil with new bootloader installer staging files in prime dirs before formatting. Fresh PR, all CI passing, no reviewers yet. | |
| qwen/qwen3.6-35b-a3b |
Impact:
75
Quick Win:
8
Staleness:
0
Complexity:
90
Confidence:
75
|
needs review | Large refactoring replacing privileged loop-device GRUB installation with a new zero-mount bootloader installer. Brand new PR, 0 days old, 0 reviewers, 6 unresolved review comments, CI passing. | |
| qwen/qwen3.6-35b-a3b |
Impact:
75
Quick Win:
11
Staleness:
0
Complexity:
85
Confidence:
75
|
needs review | Large architectural change replacing privileged loop-device GRUB installation with a new zero-mount bootloader installer. Fresh PR (0 days old) with 6 unresolved review comments and mostly passing CI. | |
| qwen/qwen3.6-35b-a3b |
Impact:
70
Quick Win:
14
Staleness:
0
Complexity:
80
Confidence:
75
|
needs review | Replaces privileged loop-device + chroot GRUB installation with a new zero-mount bootloader installer. Fresh PR with 6 unresolved review comments, all CI checks passing. | |
| qwen/qwen3.6-35b-a3b |
Impact:
75
Quick Win:
9
Staleness:
5
Complexity:
88
Confidence:
72
|
needs review | Major refactor replacing privileged loop-device + chroot GRUB installation with a zero-mount bootloader installer. Fresh PR from maintainer, no reviews yet, CI partially pending with one failing build check. | |
| qwen/qwen3.6-35b-a3b |
Impact:
65
Quick Win:
12
Staleness:
0
Complexity:
82
Confidence:
65
|
needs review | Large refactoring replacing privileged loop-device + chroot GRUB installation with a zero-mount bootloader installer. Brand new PR from maintainer, 0 reviewers, CI passing, no comments yet. | |
| qwen/qwen3.6-35b-a3b |
Impact:
75
Quick Win:
11
Staleness:
0
Complexity:
85
Confidence:
75
|
needs review | Replaces privileged loop-device + chroot GRUB installation with zero-mount bootloader installer. Brand new PR under review with 3 unresolved comments and pending CI checks. | |
| qwen/qwen3.6-35b-a3b |
Impact:
75
Quick Win:
11
Staleness:
10
Complexity:
85
Confidence:
65
|
needs review | Replaces privileged loop-device + chroot GRUB installation with a new zero-mount bootloader installer. Large refactoring (+1725/-1383 lines, 27 files) adding imagecraft/pack/bootloader/ package. Fresh PR with 5 unresolved review comments, 0 reviewers, passing CI. | |
| qwen/qwen3.6-35b-a3b |
Impact:
70
Quick Win:
18
Staleness:
5
Complexity:
75
Confidence:
55
|
needs review | Replaces privileged loop-device + chroot GRUB installation with a new zero-mount bootloader installer under imagecraft/pack/bootloader/, removing host-privilege requirements and adding riscv64 EFI support. | |
| qwen/qwen3.6-35b-a3b |
Impact:
75
Quick Win:
11
Staleness:
0
Complexity:
85
Confidence:
70
|
needs review | Replaces privileged loop-device + chroot GRUB installation with a new zero-mount bootloader installer that stages files before formatting, eliminating host-privilege requirements. Fresh PR awaiting review. | |
| qwen/qwen3.6-35b-a3b |
Impact:
75
Quick Win:
11
Staleness:
0
Complexity:
85
Confidence:
70
|
needs review | Major refactor replacing privileged loop-device + chroot GRUB installation with a zero-mount bootloader installer. New package structure, breaking change. Fresh PR with 0 reviewers and 2 unresolved review comments, CI passing. | |
| qwen/qwen3.6-35b-a3b |
Impact:
65
Quick Win:
10
Staleness:
0
Complexity:
85
Confidence:
75
|
needs review | Replaces privileged loop-device + chroot GRUB installation with a new zero-mount bootloader installer. Fresh PR from maintainer, all CI passing, awaiting review. |
Update history
| Date | Change |
|---|---|
| merged | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| closed | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| updated | |
| created |
Related work
-
Superseded By:
canonical/imagecraft#416
(confidence 78%)
Earlier PR 'feat(grub): install BIOS GRUB without loop devices or chroot' closed unmerged after inactivity; #437 delivered the merged zero-mount approach.
Related issues
| Issue | Project | State | Summary | Similarity |
|---|---|---|---|---|
| #434 feat(grub): loop-device-free EFI GRUB setup | imagecraft | closed | Closed unmerged and superseded by #437, which merged the broader zero-mount bootloader installer in commit 0ca6f6e. | |
| #416 feat(grub): install BIOS GRUB without loop devices or chroot | imagecraft | closed | Closed unmerged after inactivity; its zero-mount GRUB goal was superseded by merged #437 (commit 0ca6f6e), which replaced loop-device and chroot installation. |