From 5cea6cd2f924d929dcfbdc63675b79e6f29670c7 Mon Sep 17 00:00:00 2001 From: Leandro Rodrigues Date: Tue, 21 Jul 2026 13:55:44 -0300 Subject: [PATCH 1/5] feat(iso): embed the installer payload inside the live root image Instead of image-builder's containers-storage payload embed (which forces bootc install to re-tar ~2.5 GiB into a RAM-backed /var/tmp), the -iso container now carries the payload as an OCI layout at /usr/lib/luminusos/payload.oci, exported into the build context with skopeo beforehand (Justfile locally, a workflow step in CI) and COPYed into the live root. bootc install streams the blobs straight to disk, so the RAM floor drops to what the live GNOME session needs; a full install was verified in a 4 GB RAM VM. This also removes the osbuild manifest patching entirely: packaging is a plain image-builder bootc-generic-iso run again, with no --bootc-installer-payload-ref, so it keeps working even if upstream drops payload embedding from the generic ISO (osbuild/image-builder#2414). The final container verification asserts the embedded index.json carries the :latest ref name annotation that bootc resolves against. Redundant comments in the touched scripts and Containerfile were trimmed. --- .github/scripts/ci-package.sh | 27 ++------ .github/workflows/publish.yml | 17 ++++- ARCHITECTURE.md | 14 ++-- Justfile | 69 ++++--------------- editions/workstation/Containerfile.installer | 19 ++--- .../workstation/files/etc/sirius/distro.toml | 6 +- tests/run.sh | 15 ---- tools/patch-iso-payload-to-oci.sh | 35 ---------- 8 files changed, 52 insertions(+), 150 deletions(-) delete mode 100755 tools/patch-iso-payload-to-oci.sh diff --git a/.github/scripts/ci-package.sh b/.github/scripts/ci-package.sh index 6b0ab62..de3c0a4 100755 --- a/.github/scripts/ci-package.sh +++ b/.github/scripts/ci-package.sh @@ -3,7 +3,7 @@ # Runs inside a privileged Fedora container; never builds containers itself. # # Usage: ci-package.sh iso|qcow2 -# Env: WORKSTATION_IMAGE payload image ref (ghcr) +# Env: WORKSTATION_IMAGE payload image ref (ghcr, qcow2 only) # WORKSTATION_ISO_IMAGE live-root image ref (ghcr, iso only) # OUTPUT_NAME artifact file name # GHCR_USER / GHCR_TOKEN registry credentials (packages may be private) @@ -11,7 +11,7 @@ set -euxo pipefail format="$1" -dnf -y install image-builder podman jq osbuild +dnf -y install image-builder podman if [ -n "${GHCR_TOKEN:-}" ]; then podman login ghcr.io -u "${GHCR_USER}" -p "${GHCR_TOKEN}" @@ -19,8 +19,6 @@ else echo "WARNING: GHCR_TOKEN is empty — pulls from private GHCR packages will fail" >&2 fi -podman pull "${WORKSTATION_IMAGE}" - # The osbuild cache must NOT live on the container's overlayfs root: the # ISO build deploys a containers-storage tree inside it, and the overlay # graph driver refuses to run on top of overlayfs. /work is a bind mount @@ -29,33 +27,18 @@ cache_dir="${PWD}/.osbuild-cache" case "${format}" in iso) + # The installer payload is already embedded in the live root as an OCI + # layout (see Containerfile.installer), so only the live root is needed. podman pull "${WORKSTATION_ISO_IMAGE}" - manifest_json="${OUTPUT_NAME%.iso}.osbuild-manifest.json" - patched_manifest="${OUTPUT_NAME%.iso}.osbuild-manifest.oci.json" image-builder build \ --cache "${cache_dir}" \ --output-dir . \ --output-name "${OUTPUT_NAME}" \ - --with-manifest \ --bootc-ref "${WORKSTATION_ISO_IMAGE}" \ - --bootc-installer-payload-ref "${WORKSTATION_IMAGE}" \ bootc-generic-iso - test -f "${manifest_json}" - # Embed the payload as an OCI layout (ready-made layer blobs) instead of - # a containers-storage blob, so bootc install streams it straight to - # disk instead of re-tarring each layer into RAM. Same manifest patch - # the local Justfile flow applies. - bash tools/patch-iso-payload-to-oci.sh "${manifest_json}" "${patched_manifest}" - rm -rf bootiso - osbuild \ - --store "${cache_dir}" \ - --output-directory . \ - --export bootiso \ - "${patched_manifest}" - mv -f bootiso/install.iso "${OUTPUT_NAME}" - rm -rf bootiso ;; qcow2) + podman pull "${WORKSTATION_IMAGE}" image-builder build \ --cache "${cache_dir}" \ --output-dir . \ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 254a918..da5d507 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -118,8 +118,22 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + # The live root embeds the installer payload as an OCI layout copied + # from the build context (see Containerfile.installer); export it from + # the payload image pushed above. skopeo reuses the GHCR credentials + # written by docker/login-action. + - name: Export installer payload as OCI layout + run: | + sudo apt-get update + sudo apt-get install -y skopeo + mkdir -p .test + rm -rf .test/payload.oci + skopeo copy \ + "docker://${WORKSTATION_IMAGE}" \ + "oci:.test/payload.oci:latest" + # The live root's distro.toml must reference the same GHCR ref that - # the ISO job embeds as the installer payload. + # the installed system will track for future `bootc upgrade`. - name: Build and push workstation ISO live root uses: docker/build-push-action@v7 with: @@ -170,7 +184,6 @@ jobs: sudo podman run --rm --privileged \ --security-opt label=type:unconfined_t \ -v "$PWD:/work" -w /work \ - -e WORKSTATION_IMAGE="ghcr.io/${NAMESPACE}/luminusos-workstation:${VERSION_TAG}" \ -e WORKSTATION_ISO_IMAGE="ghcr.io/${NAMESPACE}/luminusos-workstation:${VERSION_TAG}-iso" \ -e OUTPUT_NAME="luminusos-workstation-${VERSION_TAG}.iso" \ -e GHCR_USER="${GHCR_USER}" -e GHCR_TOKEN="${GHCR_TOKEN}" \ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5d6fb79..c2de953 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -471,11 +471,11 @@ flowchart TD ## Install Memory Staging -`bootc-generic-iso` normally embeds the installer payload as a `containers-storage` blob. `bootc install` can't stream that directly: containers/storage keeps layers already unpacked on disk, so install has to re-diff and re-tar each layer into a large `/var/tmp` staging area (~2.5 GiB compressed) before it can deploy them. On a live ISO, `/var/tmp` has nowhere to go but RAM, which is why installs used to need a dedicated tmpfs and a ~5 GiB RAM gate. +`bootc-generic-iso` can embed the installer payload as a `containers-storage` blob. `bootc install` can't stream that directly: containers/storage keeps layers already unpacked on disk, so install has to re-diff and re-tar each layer into a large `/var/tmp` staging area (~2.5 GiB compressed) before it can deploy them. On a live ISO, `/var/tmp` has nowhere to go but RAM, which is why installs used to need a dedicated tmpfs and a ~5 GiB RAM gate. -Instead, `just package iso` generates the `bootc-generic-iso` osbuild manifest, patches the one `org.osbuild.skopeo` stage in the `os-tree` pipeline to embed the payload as an **OCI layout** (`destination: {type: oci, path: /usr/lib/luminusos/payload.oci:latest}`) instead of `containers-storage`, and runs `osbuild` directly against the patched manifest (see `tools/patch-iso-payload-to-oci.sh`). The `:latest` suffix in the destination path is load-bearing: `skopeo copy oci:...` strips it from the on-disk directory name and records it as the `org.opencontainers.image.ref.name` annotation in `index.json`, which is what makes the `oci:/usr/lib/luminusos/payload.oci:latest` reference resolvable. OCI layout blobs are already ready-made layer tarballs, so `bootc install --source-imgref oci:/usr/lib/luminusos/payload.oci:latest` (set in `distro.toml`) streams them straight to the target disk — no re-tar, no large staging area. This mirrors how Anaconda embeds ostree-native container payloads. +Instead, the payload is embedded in the `-iso` container image itself as an **OCI layout** at `/usr/lib/luminusos/payload.oci`. The build exports it into the build context beforehand — `skopeo copy oci:.test/payload.oci:latest` (Justfile locally, a workflow step in CI) — and `Containerfile.installer` copies it into the live root. OCI layout blobs are already ready-made layer tarballs, so `bootc install --source-imgref oci:/usr/lib/luminusos/payload.oci:latest` (set in `distro.toml`) streams them straight to the target disk — no re-tar, no large staging area, and no image-builder payload-embedding support required at all. -`image-builder-cli` has no flag for the OCI destination, so the manifest patch is a bridge until `osbuild/images` grows one upstream; the jq patch fails loudly if the manifest shape it depends on changes. +The `:latest` suffix is load-bearing: `skopeo copy oci:...` strips it from the on-disk directory name and records it as the `org.opencontainers.image.ref.name` annotation in `index.json`, which is what makes the `:latest` reference resolvable. The container build fails its final verification if the annotation is missing. The Sirius diagnostics gate (`min_ram_gib = 2`) only needs to cover the live GNOME session now. @@ -492,11 +492,10 @@ sudo image-builder build \ --output-dir . \ --output-name luminusos-workstation-.iso \ --bootc-ref \ - --bootc-installer-payload-ref \ bootc-generic-iso ``` -`--bootc-ref` defines the live root and points at `luminusos-workstation:-iso`. `--bootc-installer-payload-ref` embeds the normal `luminusos-workstation:` image as the installer payload. +`--bootc-ref` defines the live root and points at `luminusos-workstation:-iso`. The installer payload is already embedded in that image as an OCI layout (see [Install Memory Staging](#install-memory-staging)), so no payload flag is passed to image-builder. ### qcow2 @@ -515,18 +514,17 @@ The direct qcow2 artifact uses `/usr/lib/image-builder/bootc/disk.yaml`. It keep ```mermaid flowchart TD Workstation["workstation image"] + PayloadOCI["payload OCI layout
.test/payload.oci"] ISOBuilder["image-builder
bootc-generic-iso"] QCOW2Builder["image-builder
qcow2"] BootcRef["--bootc-ref"] - PayloadRef["--bootc-installer-payload-ref"] WorkstationISO["workstation ISO root
luminusos-workstation:<tag>-iso"] ISO["luminusos-workstation-.iso"] QCOW2["luminusos-workstation-.qcow2"] LastISO[".test/last-iso"] LastQCOW2[".test/last-qcow2"] - Workstation --> WorkstationISO --> BootcRef --> ISOBuilder - Workstation --> PayloadRef --> ISOBuilder + Workstation --> PayloadOCI --> WorkstationISO --> BootcRef --> ISOBuilder ISOBuilder --> ISO --> LastISO Workstation --> QCOW2Builder --> QCOW2 --> LastQCOW2 ``` diff --git a/Justfile b/Justfile index b6e76aa..67f4221 100644 --- a/Justfile +++ b/Justfile @@ -106,6 +106,11 @@ build edition="workstation": echo "Run 'just build workstation' first." exit 1 fi + echo "Exporting installer payload as OCI layout to .test/payload.oci" + mkdir -p .test + sudo rm -rf .test/payload.oci + sudo skopeo copy "containers-storage:{{ workstation_image }}" "oci:.test/payload.oci:latest" + sudo chown -R "$(id -u):$(id -g)" .test/payload.oci sudo buildah bud \ --layers \ --build-arg fedora_version={{ fedora_ver }} \ @@ -260,66 +265,16 @@ package edition="workstation" format="all": echo "Wrote artifact pointer: $pointer -> $path" } - # bootc-generic-iso embeds the installer payload as a containers-storage - # blob, which forces `bootc install` to re-diff/tar each layer into - # /var/tmp at install time (~2.5 GiB, hence the tmpfs var-tmp.mount and - # the 5 GiB RAM gate). osbuild's skopeo stage also supports an "oci" - # destination, which stores ready-made layer blobs — no re-tar, no - # large staging area — matching how Anaconda embeds ostree-native - # container payloads. image-builder-cli has no flag for this, so we - # generate the manifest, patch that one stage, and run osbuild directly. - patch_iso_payload_to_oci() { - ./tools/patch-iso-payload-to-oci.sh "$1" "$2" - } - package_iso() { build_iso_image - local out_name="luminusos-workstation-${package_tag}.iso" - local ib_cache="$(pwd)/.test/image-builder-cache" - local manifest_json=".test/${package_tag}.osbuild-manifest.json" - local patched_manifest=".test/${package_tag}.osbuild-manifest.oci.json" - mkdir -p "$ib_cache" - - echo "Generating osbuild manifest for bootc-generic-iso (payload: $image_ref)" - sudo image-builder build \ - --output-dir . \ - --output-name "$out_name" \ - --cache "$ib_cache" \ - --with-manifest \ - --bootc-ref "$iso_image_ref" \ - --bootc-installer-payload-ref "$image_ref" \ - bootc-generic-iso - - local generated_manifest="${out_name%.iso}.osbuild-manifest.json" - if [ ! -f "$generated_manifest" ]; then - echo "Expected osbuild manifest was not generated: $generated_manifest" - exit 1 - fi - mv -f "$generated_manifest" "$manifest_json" - - echo "Patching installer payload embed: containers-storage -> oci" - patch_iso_payload_to_oci "$manifest_json" "$patched_manifest" - - echo "Rebuilding ISO from the patched manifest (payload embedded as OCI layout)" - sudo rm -rf bootiso - sudo osbuild \ - --store "$ib_cache" \ - --output-directory . \ - --export bootiso \ - "$patched_manifest" - - if [ ! -f bootiso/install.iso ]; then - echo "Expected osbuild export not found: bootiso/install.iso" - exit 1 - fi - sudo mv -f bootiso/install.iso "$out_name" - sudo rm -rf bootiso - sudo chown "$(id -u):$(id -g)" "$out_name" - - local iso_path="$(pwd)/$out_name" - printf '%s\n' "$iso_path" > .test/last-iso - echo "Wrote artifact pointer: .test/last-iso -> $iso_path" + # The installer payload is already embedded in the live root as an OCI + # layout (see Containerfile.installer), so no --bootc-installer-payload-ref + # here: image-builder only needs the live root image. + echo "Building workstation ISO from $iso_image_ref" + run_image_builder bootc-generic-iso \ + "luminusos-workstation-${package_tag}.iso" .test/last-iso \ + --bootc-ref "$iso_image_ref" } package_qcow2() { diff --git a/editions/workstation/Containerfile.installer b/editions/workstation/Containerfile.installer index 68d8aa8..bc25481 100644 --- a/editions/workstation/Containerfile.installer +++ b/editions/workstation/Containerfile.installer @@ -124,15 +124,14 @@ RUN --mount=type=bind,from=ctx-files,source=/workstation/files,target=/wfiles \ install -m 0644 /wfiles/boot/efi/EFI/fedora/grub.cfg /boot/efi/EFI/fedora/grub.cfg && \ install -m 0644 /boot/efi/EFI/fedora/grub.cfg /boot/efi/EFI/BOOT/grub.cfg +# -- Installer payload as OCI layout ------------------------------------------ +# Generated into the build context by the caller: +# skopeo copy oci:.test/payload.oci:latest +COPY .test/payload.oci /usr/lib/luminusos/payload.oci + # -- Squashed final image ---------------------------------------------------- -# image-builder/osbuild deploys the ISO live root without applying OCI -# whiteout semantics, so any ".wh.*" entries from lower layers land in the -# live rootfs as real, empty files. When the rpm-ostree removal in the base -# images left such whiteouts behind, the empty -# /usr/share/dbus-1/system.d/.wh.org.projectatomic.rpmostree1.conf made -# dbus-broker fail on "Invalid XML", which took down D-Bus and GDM and left -# the live boot on a black screen. Copy the merged filesystem into a single -# layer so no whiteouts can reach the ISO. +# osbuild deploys the ISO live root without applying OCI whiteout semantics, +# so the merged filesystem is copied into a single whiteout-free layer. FROM scratch COPY --from=live / / @@ -157,6 +156,10 @@ RUN command -v sirius && \ command -v bootupctl && \ # The squashed single layer must not contain materialized OCI whiteouts. test -z "$(find / -xdev -name '.wh.*' -print -quit)" && \ + # The installer payload is embedded as an OCI layout tagged "latest". + test -f /usr/lib/luminusos/payload.oci/index.json && \ + jq -e '.manifests[0].annotations."org.opencontainers.image.ref.name" == "latest"' \ + /usr/lib/luminusos/payload.oci/index.json && \ test -f /etc/sirius/distro.toml && \ test -f /etc/sirius/sirius.toml && \ # Exactly the three LuminusOS repart definitions: the RPM's generic diff --git a/editions/workstation/files/etc/sirius/distro.toml b/editions/workstation/files/etc/sirius/distro.toml index 4bd4e75..fb77d08 100644 --- a/editions/workstation/files/etc/sirius/distro.toml +++ b/editions/workstation/files/etc/sirius/distro.toml @@ -1,8 +1,8 @@ [bootc] # The ISO embeds the payload as an OCI layout at this fixed path (see -# `package_iso()` in the top-level Justfile) instead of containers-storage, -# so `bootc install` streams ready-made layer blobs straight to disk -# instead of re-tarring each layer into a large /var/tmp staging area. +# Containerfile.installer) instead of containers-storage, so `bootc install` +# streams ready-made layer blobs straight to disk instead of re-tarring each +# layer into a large /var/tmp staging area. image = "oci:/usr/lib/luminusos/payload.oci:latest" target_imgref = "@WORKSTATION_TARGET_IMAGE@" # TODO: sign the published images (cosign/sigstore) and flip this to true — diff --git a/tests/run.sh b/tests/run.sh index f041dfc..5a8af13 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -98,21 +98,6 @@ expect "repart.d ships exactly 3 definitions" \ expect "repart root is btrfs" grep -q 'Format=btrfs' "${repart_dir}/10-root.conf" expect "repart esp exists" test -f "${repart_dir}/30-esp.conf" -# ── patch-iso-payload-to-oci.sh ───────────────────────────────────────── -cat >"${tmp}/manifest.json" <<'EOF' -{"pipelines":[{"name":"os-tree","stages":[{"type":"org.osbuild.skopeo","options":{"destination":{"type":"containers-storage"}}}]}]} -EOF -bash "${ROOT}/tools/patch-iso-payload-to-oci.sh" "${tmp}/manifest.json" "${tmp}/patched.json" -expect "payload patch writes oci destination with :latest tag" \ - jq -e '.pipelines[0].stages[0].options.destination == {"type":"oci","path":"/usr/lib/luminusos/payload.oci:latest"}' \ - "${tmp}/patched.json" -echo '{"pipelines":[{"name":"os-tree","stages":[]}]}' >"${tmp}/bad-manifest.json" -if bash "${ROOT}/tools/patch-iso-payload-to-oci.sh" "${tmp}/bad-manifest.json" "${tmp}/x.json" >/dev/null 2>&1; then - fail "payload patch rejects manifest without the skopeo stage" -else - pass "payload patch rejects manifest without the skopeo stage" -fi - # ── Justfile parses and lists recipes ───────────────────────────────── if command -v just >/dev/null 2>&1; then expect "Justfile parses" just --justfile "${ROOT}/Justfile" --list diff --git a/tools/patch-iso-payload-to-oci.sh b/tools/patch-iso-payload-to-oci.sh deleted file mode 100755 index bd267dc..0000000 --- a/tools/patch-iso-payload-to-oci.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# Patch the bootc-generic-iso osbuild manifest so the installer payload is -# embedded as an OCI layout instead of a containers-storage blob. -# -# bootc-generic-iso embeds the payload as a containers-storage blob, which -# forces `bootc install` to re-diff/re-tar each layer into /var/tmp at -# install time (~2.5 GiB). On a live ISO that has nowhere to go but RAM. -# osbuild's skopeo stage also supports an "oci" destination, which stores -# ready-made layer blobs — no re-tar, no large staging area — matching how -# Anaconda embeds ostree-native container payloads. -# -# The destination path carries the ":latest" tag on purpose: the skopeo -# stage passes it straight to `skopeo copy oci:...`, which strips the tag -# from the on-disk directory name and records it as the -# org.opencontainers.image.ref.name annotation in index.json. Without it -# the layout has no ref name and bootc fails with 'no descriptor found for -# reference "latest"' (distro.toml points at the :latest reference). -# -# Usage: patch-iso-payload-to-oci.sh -set -euo pipefail - -manifest_json="$1" -patched_manifest="$2" - -skopeo_count="$(jq '[.pipelines[] | select(.name == "os-tree") | .stages[] | select(.type == "org.osbuild.skopeo")] | length' "$manifest_json")" -if [ "$skopeo_count" != "1" ]; then - echo "Expected exactly 1 org.osbuild.skopeo stage in the 'os-tree' pipeline, found $skopeo_count" >&2 - echo "osbuild/images changed the bootc-generic-iso manifest shape; update tools/patch-iso-payload-to-oci.sh" >&2 - exit 1 -fi - -jq ' - (.pipelines[] | select(.name == "os-tree") | .stages[] | select(.type == "org.osbuild.skopeo") | .options.destination) - = {"type": "oci", "path": "/usr/lib/luminusos/payload.oci:latest"} -' "$manifest_json" >"$patched_manifest" From 1662350959fab490afc34be86c777e6f37a78937 Mon Sep 17 00:00:00 2001 From: Leandro Rodrigues Date: Tue, 21 Jul 2026 14:04:28 -0300 Subject: [PATCH 2/5] chore: drop decorative section banners from Containerfiles and scripts --- Justfile | 2 -- editions/core/Containerfile | 2 -- editions/workstation/Containerfile | 6 ------ editions/workstation/Containerfile.installer | 5 ----- tests/run.sh | 9 --------- 5 files changed, 24 deletions(-) diff --git a/Justfile b/Justfile index 67f4221..ea633c7 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,5 @@ set dotenv-load -# ── Configuration ──────────────────────────────────────────────────────── base := env("LOS_BASE", "quay.io/fedora/fedora-bootc:44") name := env("LOS_NAME", "LuminusOS") pretty := env("LOS_PRETTY_NAME", "Luminus OS") @@ -33,7 +32,6 @@ keep_sudo_alive() { # QEMU settings qemu_disk_size := env("QEMU_INSTALL_DISK_SIZE", "64G") -# ── Recipes ────────────────────────────────────────────────────────────── # Default recipe: show available commands default: diff --git a/editions/core/Containerfile b/editions/core/Containerfile index 6754d59..a34fdd2 100644 --- a/editions/core/Containerfile +++ b/editions/core/Containerfile @@ -1,11 +1,9 @@ ARG base=quay.io/fedora/fedora-bootc:44 -# ── Context stage: shared build scripts ────────────────────────────────── FROM scratch AS ctx-scripts COPY shared/scripts /scripts -# ── Core image (container-only, no desktop) ─────────────────────────────── FROM ${base} ARG fedora_version=44 diff --git a/editions/workstation/Containerfile b/editions/workstation/Containerfile index cf16fdf..a06392b 100644 --- a/editions/workstation/Containerfile +++ b/editions/workstation/Containerfile @@ -2,27 +2,22 @@ ARG core_image=localhost/luminusos:latest ARG fedora_version=44 ARG aurora_shell_version=v50.3 -# ── Context stage: shared build scripts ────────────────────────────────── FROM scratch AS ctx-scripts COPY shared/scripts /scripts -# ── Context stage: workstation script (stable — changes rarely) ─────────── FROM scratch AS ctx-workstation-script COPY editions/workstation/build.sh /workstation/build.sh -# ── Context stage: shared Flatpak refs ──────────────────────────────────── FROM scratch AS ctx-flatpaks COPY shared/flatpaks /flatpaks -# ── Context stage: static files (volatile — changes often) ──────────────── FROM scratch AS ctx-files COPY editions/workstation/files /workstation/files -# ── Aurora-shell release artifact ───────────────────────────────────────── FROM quay.io/fedora/fedora-bootc:${fedora_version} AS aurora-extension ARG fedora_version=44 @@ -47,7 +42,6 @@ RUN --mount=type=cache,target=/var/cache/dnf \ jq -e --arg uuid "${aurora_shell_uuid}" '.uuid == $uuid' \ "/out/${aurora_shell_uuid}/metadata.json" -# ── Workstation image (installed desktop payload) ──────────────────────── FROM ${core_image} ARG fedora_version=44 diff --git a/editions/workstation/Containerfile.installer b/editions/workstation/Containerfile.installer index bc25481..b235ab5 100644 --- a/editions/workstation/Containerfile.installer +++ b/editions/workstation/Containerfile.installer @@ -1,17 +1,14 @@ ARG fedora_version=44 ARG workstation_image=localhost/luminusos-workstation:latest -# -- Context stage: shared build scripts ------------------------------------- FROM scratch AS ctx-scripts COPY shared/scripts /scripts -# -- Context stage: workstation static files -------------------------------- FROM scratch AS ctx-files COPY editions/workstation/files /workstation/files -# -- Workstation live ISO root using Sirius --------------------------------- FROM ${workstation_image} AS live ARG fedora_version=44 @@ -124,12 +121,10 @@ RUN --mount=type=bind,from=ctx-files,source=/workstation/files,target=/wfiles \ install -m 0644 /wfiles/boot/efi/EFI/fedora/grub.cfg /boot/efi/EFI/fedora/grub.cfg && \ install -m 0644 /boot/efi/EFI/fedora/grub.cfg /boot/efi/EFI/BOOT/grub.cfg -# -- Installer payload as OCI layout ------------------------------------------ # Generated into the build context by the caller: # skopeo copy oci:.test/payload.oci:latest COPY .test/payload.oci /usr/lib/luminusos/payload.oci -# -- Squashed final image ---------------------------------------------------- # osbuild deploys the ISO live root without applying OCI whiteout semantics, # so the merged filesystem is copied into a single whiteout-free layer. FROM scratch diff --git a/tests/run.sh b/tests/run.sh index 5a8af13..cf5c5b0 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -22,13 +22,11 @@ expect() { # expect tmp="$(mktemp -d)" trap 'rm -rf "${tmp}"' EXIT -# ── shared script syntax ────────────────────────────────────────────── for script in "${ROOT}"/shared/scripts/*.sh "${ROOT}"/tools/*.sh \ "${ROOT}"/.github/scripts/*.sh "${ROOT}"/editions/workstation/build.sh; do expect "bash -n $(basename "${script}")" bash -n "${script}" done -# ── os-release-set.sh ───────────────────────────────────────────────── printf 'NAME="Fedora"\nVERSION_ID=44\n' >"${tmp}/os-release" OS_RELEASE_FILE="${tmp}/os-release" \ bash "${ROOT}/shared/scripts/os-release-set.sh" \ @@ -40,7 +38,6 @@ expect "os-release-set appends missing keys" \ expect "os-release-set leaves other keys alone" \ grep -qx 'VERSION_ID=44' "${tmp}/os-release" -# ── aurora-session-modes.sh ─────────────────────────────────────────── echo '{"uuid":"x","session-modes":["user"]}' >"${tmp}/metadata.json" bash "${ROOT}/shared/scripts/aurora-session-modes.sh" \ "${tmp}/metadata.json" live-installer initial-setup @@ -54,7 +51,6 @@ bash "${ROOT}/shared/scripts/aurora-session-modes.sh" \ expect "aurora-session-modes defaults missing list to user" \ jq -e '."session-modes" == ["initial-setup","user"]' "${tmp}/metadata2.json" -# ── TOML configs parse ──────────────────────────────────────────────── for toml in "${WORKSTATION_FILES}/etc/sirius/distro.toml" \ "${WORKSTATION_FILES}/etc/sirius/sirius.toml" \ "${ROOT}/shared/bootc-image-builder.toml.example"; do @@ -62,19 +58,16 @@ for toml in "${WORKSTATION_FILES}/etc/sirius/distro.toml" \ python3 -c 'import sys, tomllib; tomllib.load(open(sys.argv[1], "rb"))' "${toml}" done -# ── distro.toml still carries the build-time placeholder ────────────── expect "distro.toml has @WORKSTATION_TARGET_IMAGE@ placeholder" \ grep -q '@WORKSTATION_TARGET_IMAGE@' "${WORKSTATION_FILES}/etc/sirius/distro.toml" expect "distro.toml points bootc install at the embedded OCI payload" \ grep -q 'image = "oci:/usr/lib/luminusos/payload.oci:latest"' "${WORKSTATION_FILES}/etc/sirius/distro.toml" -# ── JSON files are valid ────────────────────────────────────────────── while IFS= read -r json; do expect "JSON parses: ${json#"${ROOT}"/}" \ python3 -c 'import sys, json; json.load(open(sys.argv[1]))' "${json}" done < <(find "${WORKSTATION_FILES}" -name '*.json') -# ── ini-style files parse (systemd units, repart, desktop, gdm) ─────── while IFS= read -r ini; do expect "INI parses: ${ini#"${ROOT}"/}" \ python3 -c ' @@ -91,14 +84,12 @@ done < <(find "${WORKSTATION_FILES}" \ -name '*.desktop' -o \ -path '*/gdm/custom.conf') -# ── repart layout invariants ────────────────────────────────────────── repart_dir="${WORKSTATION_FILES}/usr/share/sirius/repart.d" expect "repart.d ships exactly 3 definitions" \ test "$(find "${repart_dir}" -name '*.conf' | wc -l)" = 3 expect "repart root is btrfs" grep -q 'Format=btrfs' "${repart_dir}/10-root.conf" expect "repart esp exists" test -f "${repart_dir}/30-esp.conf" -# ── Justfile parses and lists recipes ───────────────────────────────── if command -v just >/dev/null 2>&1; then expect "Justfile parses" just --justfile "${ROOT}/Justfile" --list else From beaab78814e3a0ed33fe9fd5f976cb4649858dad Mon Sep 17 00:00:00 2001 From: Leandro Rodrigues Date: Tue, 21 Jul 2026 14:09:36 -0300 Subject: [PATCH 3/5] chore: remove comments that only narrate the code --- Justfile | 1 - editions/workstation/Containerfile.installer | 1 - editions/workstation/build.sh | 2 -- tools/install-qemu.sh | 3 --- tools/qemu.sh | 12 ------------ 5 files changed, 19 deletions(-) diff --git a/Justfile b/Justfile index ea633c7..e7d01f4 100644 --- a/Justfile +++ b/Justfile @@ -29,7 +29,6 @@ keep_sudo_alive() { ''' -# QEMU settings qemu_disk_size := env("QEMU_INSTALL_DISK_SIZE", "64G") diff --git a/editions/workstation/Containerfile.installer b/editions/workstation/Containerfile.installer index b235ab5..e00cca0 100644 --- a/editions/workstation/Containerfile.installer +++ b/editions/workstation/Containerfile.installer @@ -95,7 +95,6 @@ RUN --mount=type=bind,from=ctx-files,source=/workstation/files,target=/wfiles \ install -Dm0644 /wfiles/etc/gdm/custom.conf /etc/gdm/custom.conf && \ install -Dm0644 /wfiles/var/lib/AccountsService/users/liveuser /var/lib/AccountsService/users/liveuser -# Rebuild initramfs for live ISO boot. RUN --mount=type=bind,from=ctx-scripts,source=/scripts,target=/scripts \ bash /scripts/rebuild-initramfs.sh --add "plymouth dmsquash-live" diff --git a/editions/workstation/build.sh b/editions/workstation/build.sh index 70f2ae7..7899e54 100755 --- a/editions/workstation/build.sh +++ b/editions/workstation/build.sh @@ -10,7 +10,6 @@ cat >/usr/share/gnome-shell/search-providers/org.gnome.Software-search-provider. DefaultDisabled=true EOF -# Compile GNOME schema overrides glib-compile-schemas /usr/share/glib-2.0/schemas/ if command -v dconf >/dev/null 2>&1; then dconf update @@ -24,7 +23,6 @@ if [ -f /usr/share/gnome-shell/modes/initial-setup.json ]; then rm -f "${tmp}" fi -# Ensure graphical boot and GDM as display manager ln -sf /usr/lib/systemd/system/graphical.target /etc/systemd/system/default.target ln -sf /usr/lib/systemd/system/gdm.service /etc/systemd/system/display-manager.service diff --git a/tools/install-qemu.sh b/tools/install-qemu.sh index 2edf336..4319fc8 100755 --- a/tools/install-qemu.sh +++ b/tools/install-qemu.sh @@ -25,7 +25,6 @@ if [ ! -f "$BIB_CONFIG" ]; then exit 1 fi -# load .env if it exists if [ -f "$PROJECT_DIR/.env" ]; then # shellcheck disable=SC1091 source "$PROJECT_DIR/.env" @@ -46,8 +45,6 @@ fi echo "==> Installing $IMAGE_REF into disk via bootc-image-builder..." -# Use bootc-image-builder (via podman) to write the image to a qcow2 disk. -# This handles partitioning, bootloader setup, and writing the ostree commit. # --rootfs btrfs: required to specify the filesystem type for partitions. # The output disk lands at $TEST_DIR/qcow2/disk.qcow2 sudo podman run --rm -it --privileged \ diff --git a/tools/qemu.sh b/tools/qemu.sh index f32d8ff..053986b 100755 --- a/tools/qemu.sh +++ b/tools/qemu.sh @@ -9,14 +9,10 @@ set -x # ./qemu.sh [disk|iso] # If no argument is provided, disk is used by default -# Variables for test environment - -# load .env if it exists if [ -f .env ]; then source .env fi -# default values for QEMU # NOTE: the installer payload is embedded as an OCI layout, so `bootc # install` streams layer blobs straight to disk instead of staging a # re-tarred copy in RAM (see distro.toml). 4 GiB comfortably covers the @@ -45,15 +41,12 @@ if [ "$QEMU_DISPLAY" = "auto" ]; then fi # Install disk for ISO boots (set QEMU_INSTALL_DISK_SIZE like "64G") -# Defaults are set after TEST_HOME is known : ${QEMU_INSTALL_DISK_SIZE:="64G"} : ${QEMU_INSTALL_DISK_PATH:=""} : ${QEMU_OVMF_CODE:="/usr/share/OVMF/OVMF_CODE.fd"} : ${QEMU_OVMF_VARS_TEMPLATE:="/usr/share/OVMF/OVMF_VARS.fd"} : ${QEMU_OVMF_VARS:="$TEST_HOME/OVMF_VARS.fd"} -# Get testing mode from first argument - if [ -z "${1:-}" ]; then SCRIPT_MODE="disk" else @@ -160,7 +153,6 @@ else exit 1 fi -# if QEMU_BOOT = "uefi" then add UEFI firmware if [ "$QEMU_BOOT" = "uefi" ]; then if [ "$QEMU_RESET_NVRAM" != "0" ]; then rm -f "$QEMU_OVMF_VARS" @@ -172,14 +164,10 @@ if [ "$QEMU_BOOT" = "uefi" ]; then QEMU_ARGS+=" -drive if=pflash,format=raw,file=$QEMU_OVMF_VARS" fi -# Run QEMU - -# Now that TEST_HOME exists, set a default path for the optional install disk if [ -z "$QEMU_INSTALL_DISK_PATH" ]; then QEMU_INSTALL_DISK_PATH="$TEST_HOME/install-disk.qcow2" fi -# If requested and we are booting an ISO, create/attach an empty install disk function ensure_install_disk { if [ -n "$QEMU_INSTALL_DISK_SIZE" ]; then if [ "$QEMU_RESET_DISK" != "0" ]; then From b688f0b3bfc82bedc9591c3cfd4d94983153f6a0 Mon Sep 17 00:00:00 2001 From: Leandro Rodrigues Date: Tue, 21 Jul 2026 14:17:18 -0300 Subject: [PATCH 4/5] refactor(just): single buildah invocation site in the build recipe --- Justfile | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/Justfile b/Justfile index e7d01f4..0ebe3b4 100644 --- a/Justfile +++ b/Justfile @@ -47,19 +47,20 @@ build edition="workstation": find editions/core -type f -print0 | sort -z | xargs -0 sha256sum } | sha256sum | awk '{print $1}' } + build_image() { # build_image [extra buildah bud args...] + local image_tag="$1" file="$2" + shift 2 + sudo buildah bud --layers "$@" --tag "$image_tag" --file "$file" . + } keep_sudo_alive case "{{ edition }}" in core) - sudo buildah bud \ - --layers \ + build_image {{ core_image }} editions/core/Containerfile \ --build-arg base={{ base }} \ --build-arg fedora_version={{ fedora_ver }} \ --build-arg distro_version={{ tag }} \ --build-arg distro_name="{{ name }}" \ - --build-arg distro_pretty_name="{{ pretty }}" \ - --tag {{ core_image }} \ - --file editions/core/Containerfile \ - . + --build-arg distro_pretty_name="{{ pretty }}" ./tools/squash-image.sh {{ core_image }} mkdir -p .test printf '%s\n' "{{ tag }}" > .test/last-core-tag @@ -78,8 +79,7 @@ build edition="workstation": else echo "Using existing core image: {{ core_image }}" fi - sudo buildah bud \ - --layers \ + build_image {{ workstation_image }} editions/workstation/Containerfile \ --cap-add sys_admin \ --security-opt label=disable \ --build-arg core_image={{ core_image }} \ @@ -89,10 +89,7 @@ build edition="workstation": --build-arg edition_id="workstation" \ --build-arg aurora_shell_version={{ aurora_shell_version }} \ --build-arg aurora_shell_sha256={{ aurora_shell_sha256 }} \ - --build-arg skip_flatpaks={{ skip_flatpaks }} \ - --tag {{ workstation_image }} \ - --file editions/workstation/Containerfile \ - . + --build-arg skip_flatpaks={{ skip_flatpaks }} ./tools/squash-image.sh {{ workstation_image }} mkdir -p .test printf '%s\n' "{{ tag }}" > .test/last-workstation-tag @@ -108,16 +105,12 @@ build edition="workstation": sudo rm -rf .test/payload.oci sudo skopeo copy "containers-storage:{{ workstation_image }}" "oci:.test/payload.oci:latest" sudo chown -R "$(id -u):$(id -g)" .test/payload.oci - sudo buildah bud \ - --layers \ + build_image {{ workstation_iso_image }} editions/workstation/Containerfile.installer \ --build-arg fedora_version={{ fedora_ver }} \ --build-arg sirius_version={{ sirius_version }} \ --build-arg workstation_image={{ workstation_image }} \ --build-arg workstation_target_image={{ workstation_target_image }} \ - --build-arg image_version={{ tag }} \ - --tag {{ workstation_iso_image }} \ - --file editions/workstation/Containerfile.installer \ - . + --build-arg image_version={{ tag }} echo "No post-build squash needed for {{ workstation_iso_image }}: Containerfile.installer squashes itself into a single layer" ;; *) From 354c89480f610c10ead07db4f04a93fdc86ee7a5 Mon Sep 17 00:00:00 2001 From: Leandro Rodrigues Date: Tue, 21 Jul 2026 14:21:06 -0300 Subject: [PATCH 5/5] refactor(just): rewrite the qcow2 disk layout check for readability --- Justfile | 64 ++++++++++++-------------------------------------------- 1 file changed, 13 insertions(+), 51 deletions(-) diff --git a/Justfile b/Justfile index 0ebe3b4..e05e2fa 100644 --- a/Justfile +++ b/Justfile @@ -161,67 +161,29 @@ package edition="workstation" format="all": } check_qcow2_disk_layout() { - local image="$1" - local ctr="" - local rootfs="" + local image="$1" ctr rootfs boot_fstype local disk_yaml="/usr/lib/image-builder/bootc/disk.yaml" - local status=0 [[ "$image" == localhost/* ]] || return 0 ctr="$(sudo buildah from --pull=never "${image}")" rootfs="$(sudo buildah mount "${ctr}")" - if [ ! -f "${rootfs}${disk_yaml}" ]; then - echo "Missing ${disk_yaml} inside ${image}" >&2 - status=1 - else - set +e - sudo awk ' - BEGIN { - code = 12 - } - /payload:/ { - in_payload = 1 - fs = "" - } - in_payload && /^[[:space:]]+type:/ { - fs = $2 - gsub(/"/, "", fs) - } - in_payload && /^[[:space:]]+mountpoint:[[:space:]]+"\/boot"$/ { - if (fs == "ext4") { - code = 0 - exit 0 - } - if (fs == "btrfs") { - code = 10 - exit 10 - } - code = 11 - exit 11 - } - END { - exit code - } - ' "${rootfs}${disk_yaml}" - status=$? - set -e - fi - - if [ "${status}" != "0" ]; then - if [ "${status}" = "10" ]; then - echo "${image} still has /boot as btrfs in ${disk_yaml}." >&2 - echo "Rebuild the workstation image before packaging qcow2:" >&2 - echo " LOS_TAG=${package_tag} just build workstation" >&2 - else - echo "Unable to validate /boot ext4 in ${image}:${disk_yaml}" >&2 - fi - fi + # /boot must stay ext4: image-builder qcow2 generation rejects btrfs there. + boot_fstype="$(sudo awk ' + /^[[:space:]]+type: "(ext4|btrfs)"$/ { t = $2; gsub(/"/, "", t) } + /mountpoint:[[:space:]]+"\/boot"/ { print t; exit } + ' "${rootfs}${disk_yaml}" 2>/dev/null || true)" sudo buildah umount "${ctr}" >/dev/null 2>&1 || true sudo buildah rm "${ctr}" >/dev/null 2>&1 || true - return "${status}" + + if [ "${boot_fstype}" != "ext4" ]; then + echo "${image} does not have /boot as ext4 in ${disk_yaml} (found: '${boot_fstype:-missing}')." >&2 + echo "Rebuild the workstation image before packaging qcow2:" >&2 + echo " LOS_TAG=${package_tag} just build workstation" >&2 + return 1 + fi } case "{{ format }}" in