Skip to content

Commit 702a2ef

Browse files
committed
Add UKI (systemd-boot + Type 2) support
cbootc install to-disk --uki now produces a true Unified Kernel Image: - prepare-boot writes a Type 1 BLS entry to the XBOOTLDR to extract the kernel, initramfs, and the correct install-time composefs= hash - ukify build embeds all three into a single .efi on the ESP (EFI/Linux/) where systemd-boot always scans, avoiding XBOOTLDR discovery issues - Type 1 artifacts (.conf + kernel dir) are removed after the UKI is built This solves the hash circularity problem: build-time directory hashing and install-time OCI hashing produce different values, so the hash must be computed at install time and then embedded — not pre-baked into the image. Other changes: - Containerfile.base: add systemd-ukify to rootfs-uki stage; split into grub/uki named targets with shared rootfs-common base - upgrade.rs: detect UKI system via /boot/efi/EFI/Linux; call build_uki after prepare-boot to keep upgrades in sync - rollback.rs: scan /boot/efi/EFI/Linux/*.efi; use bootctl set-next - container.yml: push fedora-44-uki image alongside fedora-44 - e2e.yml: add UKI build/install/test steps with writable OVMF VARS - tests/e2e.py: fix UKI .efi path (/boot/efi/EFI/Linux), pass VARS in UKI mode - README: document install-time cmdline embedding and VARS requirement
1 parent b9898ad commit 702a2ef

8 files changed

Lines changed: 425 additions & 85 deletions

File tree

.github/workflows/container.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,24 @@ jobs:
2727

2828
# Build the base image and push it. The build is slow (dnf + dracut)
2929
# so GHA layer cache is critical for subsequent runs.
30-
- name: Build and push composefs-os:fedora-44
30+
- name: Build and push composefs-os:fedora-44 (GRUB)
3131
uses: docker/build-push-action@v6
3232
with:
3333
context: .
3434
file: Containerfile.base
35+
target: grub
3536
push: true
3637
tags: ghcr.io/${{ github.repository_owner }}/composefs-os:fedora-44
3738
cache-from: type=gha
3839
cache-to: type=gha,mode=max
40+
41+
- name: Build and push composefs-os:fedora-44-uki (systemd-boot + UKI)
42+
uses: docker/build-push-action@v6
43+
with:
44+
context: .
45+
file: Containerfile.base
46+
target: uki
47+
push: true
48+
tags: ghcr.io/${{ github.repository_owner }}/composefs-os:fedora-44-uki
49+
cache-from: type=gha
50+
cache-to: type=gha,mode=max

.github/workflows/e2e.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,46 @@ jobs:
8484
run: |
8585
python3 tests/e2e.py --secure-boot disk-sb.raw
8686
87+
- name: Build UKI base image
88+
uses: docker/build-push-action@v6
89+
with:
90+
context: .
91+
file: Containerfile.base
92+
target: uki
93+
push: false
94+
load: true
95+
tags: composefs-os:fedora-44-uki
96+
cache-from: type=gha
97+
cache-to: type=gha,mode=max
98+
99+
- name: Load UKI base image into podman
100+
run: |
101+
sudo skopeo copy \
102+
docker-daemon:composefs-os:fedora-44-uki \
103+
containers-storage:localhost/composefs-os:fedora-44-uki
104+
105+
- name: Build UKI test image
106+
run: |
107+
sudo podman build \
108+
--build-arg BASE_IMAGE=localhost/composefs-os:fedora-44-uki \
109+
-t composefs-os-uki-test:latest \
110+
-f examples/fedora/Containerfile .
111+
112+
- name: Install to disk image (UKI)
113+
run: |
114+
sudo podman run --rm --privileged \
115+
-v ${{ github.workspace }}:/output \
116+
-v /var/lib/containers:/var/lib/containers \
117+
-v /var/tmp:/var/tmp \
118+
composefs-os-uki-test:latest \
119+
cbootc install to-disk /output/disk-uki.raw --size 5G --uki
120+
121+
- name: Run e2e tests (UKI)
122+
run: |
123+
OVMF=$(find /usr/share -name 'OVMF_CODE*.fd' ! -name '*VARS*' ! -name '*.secboot*' ! -name '*.ms.*' | head -1)
124+
OVMF_VARS=$(find /usr/share -name 'OVMF_VARS.fd' ! -name '*.secboot*' ! -name '*.ms.*' | head -1)
125+
python3 tests/e2e.py --ovmf "$OVMF" --ovmf-vars "$OVMF_VARS" --uki disk-uki.raw
126+
87127
- name: Upload disk images on failure
88128
if: failure()
89129
uses: actions/upload-artifact@v4
@@ -92,4 +132,5 @@ jobs:
92132
path: |
93133
disk.raw
94134
disk-sb.raw
135+
disk-uki.raw
95136
retention-days: 1

Containerfile.base

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
# composefs-os:fedora-44 — Base image for composefs-rs bootable Fedora 44 systems.
1+
# composefs-os:fedora-44 — Base images for composefs-rs bootable Fedora 44 systems.
22
#
3-
# This image contains the kernel, systemd, grub2, composefs toolchain (cfsctl,
4-
# composefs-setup-root, dracut module), and cbootc. /etc and /var are left
5-
# intact so derived images can use dnf normally.
3+
# Two named targets are available (default: grub):
4+
# --target grub GRUB-based boot (shim → grub2, BLS Type 1)
5+
# --target uki systemd-boot + UKI (BLS Type 2, cmdline embedded at build time)
66
#
7-
# Derived images require no special build steps — just FROM this image, install
8-
# packages, add LABEL containers.bootc=1, and build. The composefs layout
9-
# transformation happens at install time via 'cbootc install to-disk'.
10-
#
11-
# Build: podman build -t composefs-os:fedora-44 -f Containerfile.base .
7+
# Build:
8+
# podman build -t composefs-os:fedora-44 -f Containerfile.base . # GRUB
9+
# podman build -t composefs-os:fedora-44-uki --target uki -f Containerfile.base .
1210

1311
# ---------------------------------------------------------------------------
1412
# Stage 0a: Build cbootc
@@ -30,9 +28,9 @@ RUN cargo build --release -p composefs-ctl && \
3028
cargo build --release -p composefs-setup-root --no-default-features
3129

3230
# ---------------------------------------------------------------------------
33-
# Stage 1: Bootstrap Fedora 44 rootfs via dnf --installroot
31+
# Stage 1: Bootstrap Fedora 44 rootfs (common to both boot styles)
3432
# ---------------------------------------------------------------------------
35-
FROM quay.io/fedora/fedora:44 AS rootfs-builder
33+
FROM quay.io/fedora/fedora:44 AS rootfs-common
3634

3735
# Install dracut on the builder for 'dracut --sysroot /rootfs'
3836
RUN dnf install -y dracut && dnf clean all
@@ -49,8 +47,6 @@ RUN dnf install -y \
4947
NetworkManager iproute iputils hostname \
5048
dnf5 rpm \
5149
e2fsprogs dosfstools btrfs-progs \
52-
grub2-efi-x64 grub2-efi-x64-modules grub2-tools grub2-tools-minimal \
53-
shim-x64 efibootmgr \
5450
fsverity-utils \
5551
dracut \
5652
podman skopeo \
@@ -154,10 +150,7 @@ RUN KVER=$(ls /rootfs/usr/lib/modules | head -1) && \
154150
[ ! -f "/rootfs/usr/lib/modules/$KVER/vmlinuz" ]; then \
155151
cp "/rootfs/boot/vmlinuz-$KVER" \
156152
"/rootfs/usr/lib/modules/$KVER/vmlinuz"; \
157-
fi && \
158-
mkdir -p /rootfs/usr/share/efi && \
159-
cp -r /rootfs/boot/efi/EFI /rootfs/usr/share/efi/ && \
160-
rm -rf /rootfs/boot/*
153+
fi
161154

162155
# ---------------------------------------------------------------------------
163156
# Install cfsctl, cbootc, and update units
@@ -192,17 +185,64 @@ RUN mkdir -p /rootfs/sysroot && \
192185
ln -sf var/srv /rootfs/srv
193186

194187
# ---------------------------------------------------------------------------
195-
# Final stage — /etc and /var intact so derived images can use dnf normally.
196-
# No cfs-layout-apply step is required in derived images; cbootc install
197-
# to-disk handles the composefs layout transformation at install time.
188+
# Stage 2a: GRUB — add bootloader packages, copy EFI binaries, clear /boot
189+
# ---------------------------------------------------------------------------
190+
FROM rootfs-common AS rootfs-grub
191+
RUN dnf install -y \
192+
--installroot /rootfs \
193+
--use-host-config \
194+
--releasever 44 \
195+
--setopt=install_weak_deps=False \
196+
grub2-efi-x64 grub2-efi-x64-modules grub2-tools grub2-tools-minimal \
197+
shim-x64 efibootmgr \
198+
&& dnf clean all --installroot /rootfs --use-host-config
199+
RUN mkdir -p /rootfs/usr/share/efi && \
200+
cp -r /rootfs/boot/efi/EFI /rootfs/usr/share/efi/ && \
201+
rm -rf /rootfs/boot/*
202+
203+
# ---------------------------------------------------------------------------
204+
# Stage 2b: systemd-boot — add systemd-boot-unsigned, clear /boot
205+
# Boot entries (BLS Type 1) are written by cfsctl oci prepare-boot at
206+
# install time, same as the GRUB path. No UKI pre-build is needed here:
207+
# a pre-built UKI would embed a build-time composefs hash, but prepare-boot
208+
# computes the hash from the OCI image (different code path, different hash).
198209
# ---------------------------------------------------------------------------
199-
FROM scratch
200-
COPY --from=rootfs-builder /rootfs /
210+
FROM rootfs-common AS rootfs-uki
211+
RUN dnf install -y \
212+
--installroot /rootfs \
213+
--use-host-config \
214+
--releasever 44 \
215+
--setopt=install_weak_deps=False \
216+
systemd-boot-unsigned \
217+
systemd-ukify \
218+
&& dnf clean all --installroot /rootfs --use-host-config
219+
RUN rm -rf /rootfs/boot/*
220+
221+
# ---------------------------------------------------------------------------
222+
# Final stage: GRUB (default target, backward-compat)
223+
# ---------------------------------------------------------------------------
224+
FROM scratch AS grub
225+
COPY --from=rootfs-grub /rootfs /
201226

202227
LABEL containers.bootc=1
203228
LABEL composefs.backend=cfs-rs
204229
LABEL org.opencontainers.image.title="Fedora CFS Base"
205-
LABEL org.opencontainers.image.description="Base image for composefs-rs bootable Fedora 44 systems"
230+
LABEL org.opencontainers.image.description="Base image for composefs-rs bootable Fedora 44 systems (GRUB)"
231+
LABEL org.opencontainers.image.version="44"
232+
233+
CMD ["/sbin/init"]
234+
235+
# ---------------------------------------------------------------------------
236+
# Final stage: UKI (--target uki)
237+
# ---------------------------------------------------------------------------
238+
FROM scratch AS uki
239+
COPY --from=rootfs-uki /rootfs /
240+
241+
LABEL containers.bootc=1
242+
LABEL composefs.backend=cfs-rs
243+
LABEL cbootc.boot-style=uki
244+
LABEL org.opencontainers.image.title="Fedora CFS Base (UKI)"
245+
LABEL org.opencontainers.image.description="Base image for composefs-rs bootable Fedora 44 systems (systemd-boot + UKI)"
206246
LABEL org.opencontainers.image.version="44"
207247

208248
CMD ["/sbin/init"]

README.md

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ See [DESIGN.md](DESIGN.md) for rationale and architecture.
1919

2020
## Available Images
2121

22-
| Image | Status |
23-
|-------|--------|
24-
| `ghcr.io/henrywang/composefs-os:fedora-44` | Working |
25-
| Ubuntu | Planned |
26-
| Arch Linux | Planned |
22+
| Image | Boot style | Status |
23+
|-------|-----------|--------|
24+
| `ghcr.io/henrywang/composefs-os:fedora-44` | GRUB (BLS Type 1) | Working |
25+
| `ghcr.io/henrywang/composefs-os:fedora-44-uki` | systemd-boot + UKI (BLS Type 2) | Working |
26+
| Ubuntu || Planned |
27+
| Arch Linux || Planned |
2728

2829

2930
## Quick Start
@@ -70,8 +71,37 @@ qemu-system-x86_64 -enable-kvm -m 4096 \
7071
-nographic
7172
```
7273

73-
The same base image works for both modes — the EFI chain difference is handled
74-
entirely at install time.
74+
The same GRUB base image works for both modes — the EFI chain difference is
75+
handled entirely at install time.
76+
77+
### UKI (Unified Kernel Image)
78+
79+
The `-uki` image uses systemd-boot and BLS Type 2 entries: a single `.efi` file
80+
bundles the kernel, initramfs, and `composefs=` cmdline. The cmdline (including
81+
the composefs hash) is embedded at **install time** by `cbootc install to-disk`,
82+
so there is no separate `.conf` file and no writable grubenv.
83+
84+
Pass `--uki` to `cbootc install to-disk` when using the UKI image:
85+
86+
```sh
87+
sudo podman run --rm --privileged \
88+
-v $(pwd):/output \
89+
-v /var/lib/containers:/var/lib/containers \
90+
-v /var/tmp:/var/tmp \
91+
ghcr.io/henrywang/composefs-os:fedora-44-uki \
92+
cbootc install to-disk /output/disk-uki.raw --size 10G --uki
93+
94+
# A writable VARS file is needed for EFI variables (random seed, bootctl set-next)
95+
cp /usr/share/edk2/ovmf/OVMF_VARS.fd /tmp/OVMF_VARS.fd
96+
qemu-system-x86_64 -enable-kvm -m 4096 \
97+
-drive file=disk-uki.raw,if=virtio \
98+
-drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/ovmf/OVMF_CODE.fd \
99+
-drive if=pflash,format=raw,file=/tmp/OVMF_VARS.fd \
100+
-nographic
101+
```
102+
103+
`cbootc rollback` automatically detects the boot style — it uses
104+
`bootctl set-next` on UKI systems and `grub2-editenv` on GRUB systems.
75105

76106
## Building a Custom Image
77107

@@ -120,7 +150,7 @@ survives upgrades. `cbootc-update.timer` (enabled in the base image) runs
120150

121151
```
122152
composefs-os/
123-
Containerfile.base Builds the bootable Fedora 44 base image
153+
Containerfile.base Builds Fedora 44 base images (--target grub | uki)
124154
src/ cbootc source (Rust)
125155
units/
126156
cbootc-update.service Systemd service for automatic upgrades
@@ -162,14 +192,18 @@ the running system.
162192

163193
### Rollback
164194

165-
`cbootc rollback` selects the previous deployment for the next boot by writing
166-
`next_entry` to `/boot/grub2/grubenv`. Run `systemctl reboot` to apply it.
195+
`cbootc rollback` selects the previous deployment for the next boot.
196+
Run `systemctl reboot` to apply it.
167197

168-
If rollback itself fails to boot, use the GRUB menu to select the older BLS
169-
entry manually — each deployment keeps its own entry in `/boot/loader/entries/`.
198+
- **GRUB systems**: writes `next_entry` to `/boot/grub2/grubenv`. If rollback
199+
fails to boot, use the GRUB menu to pick the older BLS entry manually from
200+
`/boot/loader/entries/`.
201+
- **UKI/systemd-boot systems**: calls `bootctl set-next` to set the
202+
`LoaderEntryOneShot` EFI variable. If rollback fails to boot, use the
203+
systemd-boot menu (hold Space at startup) to pick the older `.efi` entry.
170204

171-
Old deployment boot files (`/boot/<digest>/`) accumulate across upgrades and
172-
are not pruned automatically. Remove them manually when disk space is a concern.
205+
Old deployment boot files accumulate across upgrades and are not pruned
206+
automatically. Remove them manually when disk space is a concern.
173207

174208
### x86-64 only
175209

0 commit comments

Comments
 (0)