From 680c94e29b5bc8d2365404142fcd2feb41b76c4b Mon Sep 17 00:00:00 2001 From: Leandro Rodrigues Date: Mon, 20 Jul 2026 08:35:33 -0300 Subject: [PATCH 1/3] feat(iso): embed installer payload as OCI layout, drop RAM-tmpfs staging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (~2.5 GiB) at install time. On a live ISO that has nowhere to go but RAM, hence the var-tmp.mount tmpfs and the 5 GiB min_ram_gib gate. osbuild's skopeo stage also supports an "oci" destination (ready-made layer blobs, no re-tar) — the same model Anaconda uses for ostree-native container payloads. image-builder-cli has no flag to pick it, so package_iso() now generates the bootc-generic-iso manifest, patches the one org.osbuild.skopeo stage in the os-tree pipeline from containers-storage to oci, and runs osbuild directly against the patched manifest. The jq patch fails loudly if that stage isn't found, so an upstream manifest shape change breaks the build instead of silently reverting to the old embed. distro.toml now points bootc at the fixed oci:/usr/lib/luminusos/ payload.oci:latest path; the var-tmp.mount unit and the image_copy_tmp_dir containers.conf.d drop-in are gone (unneeded); min_ram_gib drops from 5 to 2, matching the live GNOME session's own floor. --- ARCHITECTURE.md | 14 ++-- Justfile | 72 +++++++++++++++++-- editions/workstation/Containerfile.installer | 7 +- .../99-luminusos-bootc-install-tmp.conf | 2 - .../workstation/files/etc/sirius/distro.toml | 6 +- .../workstation/files/etc/sirius/sirius.toml | 12 ++-- .../usr/lib/systemd/system/var-tmp.mount | 26 ------- tests/run.sh | 6 +- tools/qemu.sh | 10 +-- 9 files changed, 94 insertions(+), 61 deletions(-) delete mode 100644 editions/workstation/files/etc/containers/containers.conf.d/99-luminusos-bootc-install-tmp.conf delete mode 100644 editions/workstation/files/usr/lib/systemd/system/var-tmp.mount diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 92d3ee1..6b847b9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -311,7 +311,6 @@ The live ISO is not itself a bootc deployment. It is generated from the workstat | `/usr/lib/systemd/user/luminusos-sirius.service` | Starts Sirius in the live user session. | | `/etc/dconf/db/local.d/00-iso-live-mode` | Live ISO GNOME Shell and Aurora Shell module defaults. | | `/usr/share/polkit-1/rules.d/50-sirius-live.rules` | Lets `liveuser` run the privileged Sirius install action without authentication. | -| `/usr/lib/systemd/system/var-tmp.mount` | Live-only tmpfs on `/var/tmp` (75% of RAM) so `bootc install` can stage the compressed payload. | ```mermaid flowchart TD @@ -371,7 +370,7 @@ args = ["--skip-fetch-check"] repart_dir = "/usr/share/sirius/repart.d" ``` -`image` points at the payload embedded in the live ISO; `target_imgref` is the reference the installed system uses for future `bootc upgrade`. `/etc/sirius/sirius.toml` gates the wizard: keyboard/timezone/user pages are disabled (GNOME Initial Setup owns user creation) and diagnostics require UEFI, ≥5 GiB usable RAM, and enough disk space — the RAM gate exists because `bootc install` stages the ~2.5 GiB compressed payload in the live tmpfs `/var/tmp` (see `var-tmp.mount`). +`image` points at the payload embedded in the live ISO as an OCI layout (`oci:/usr/lib/luminusos/payload.oci:latest`, see [Install Memory Staging](#install-memory-staging)); `target_imgref` is the reference the installed system uses for future `bootc upgrade`. `/etc/sirius/sirius.toml` gates the wizard: keyboard/timezone/user pages are disabled (GNOME Initial Setup owns user creation) and diagnostics require UEFI, ≥2 GiB usable RAM, and enough disk space. ## Installation Flow @@ -443,7 +442,7 @@ flowchart TD ### Live-Only Isolation -The workstation payload never contains live-only installer artifacts: Sirius, its configs (`/etc/sirius/`), the live polkit rule, the `live-installer` session files, `var-tmp.mount`, and the `liveuser` autologin setup are added only in `Containerfile.installer`, on top of the payload image. The bootc install path deploys the clean payload image, so no exclusion list is needed — live files simply never existed in the payload. +The workstation payload never contains live-only installer artifacts: Sirius, its configs (`/etc/sirius/`), the live polkit rule, the `live-installer` session files, and the `liveuser` autologin setup are added only in `Containerfile.installer`, on top of the payload image. The bootc install path deploys the clean payload image, so no exclusion list is needed — live files simply never existed in the payload. Sirius itself belongs to the installer image, not to the installed workstation payload. @@ -472,12 +471,13 @@ flowchart TD ## Install Memory Staging -`bootc install` stages the compressed payload copy (~2.5 GiB) under `/var/tmp` while the live session itself runs from RAM. Two live-only pieces make that fit: +`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. -- `/usr/lib/systemd/system/var-tmp.mount` mounts a dedicated tmpfs at `/var/tmp` sized at 75% of RAM, only when booted with `rd.live.image`. -- `/etc/containers/containers.conf.d/99-luminusos-bootc-install-tmp.conf` points podman/bootc staging (`image_copy_tmp_dir`) at that tmpfs. +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}`) instead of `containers-storage`, and runs `osbuild` directly against the patched manifest (see `patch_iso_payload_to_oci()` in the Justfile). 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. -The Sirius diagnostics gate (`min_ram_gib = 5`) blocks machines that cannot hold the staging. +`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 Sirius diagnostics gate (`min_ram_gib = 2`) only needs to cover the live GNOME session now. ## Workstation Artifact Packaging diff --git a/Justfile b/Justfile index 417d162..ba00511 100644 --- a/Justfile +++ b/Justfile @@ -261,17 +261,81 @@ 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() { + local manifest_json="$1" patched_manifest="$2" + local skopeo_count + 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 patch_iso_payload_to_oci() in the Justfile" >&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"} + ' "$manifest_json" > "$patched_manifest" + } + package_iso() { build_iso_image if [[ "$iso_image_ref" == localhost/* ]]; then echo "No post-build squash needed for the workstation ISO image: Containerfile.installer squashes itself into a single layer" fi - echo "Building workstation ISO from $iso_image_ref with payload $image_ref" - run_image_builder bootc-generic-iso \ - "luminusos-workstation-${package_tag}.iso" .test/last-iso \ + 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 \ + --bootc-default-fs btrfs \ + --output-dir . \ + --output-name "$out_name" \ + --cache "$ib_cache" \ + --with-manifest \ --bootc-ref "$iso_image_ref" \ - --bootc-installer-payload-ref "$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 "$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 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" } package_qcow2() { diff --git a/editions/workstation/Containerfile.installer b/editions/workstation/Containerfile.installer index d24ae45..68d8aa8 100644 --- a/editions/workstation/Containerfile.installer +++ b/editions/workstation/Containerfile.installer @@ -47,9 +47,6 @@ RUN --mount=type=cache,target=/var/cache/dnf \ RUN --mount=type=bind,from=ctx-files,source=/workstation/files,target=/wfiles \ install -Dm0644 /wfiles/etc/dconf/db/local.d/00-iso-live-mode /etc/dconf/db/local.d/00-iso-live-mode && \ install -Dm0644 /wfiles/etc/dconf/profile/user /etc/dconf/profile/user && \ - install -Dm0644 /wfiles/etc/containers/containers.conf.d/99-luminusos-bootc-install-tmp.conf /etc/containers/containers.conf.d/99-luminusos-bootc-install-tmp.conf && \ - install -Dm0644 /wfiles/usr/lib/systemd/system/var-tmp.mount /usr/lib/systemd/system/var-tmp.mount && \ - systemctl enable var-tmp.mount && \ install -Dm0644 /wfiles/usr/lib/image-builder/bootc/iso.yaml /usr/lib/image-builder/bootc/iso.yaml && \ install -Dm0644 /wfiles/usr/share/wayland-sessions/live-installer.desktop /usr/share/wayland-sessions/live-installer.desktop && \ install -Dm0644 /wfiles/usr/share/gnome-session/sessions/live-installer.session /usr/share/gnome-session/sessions/live-installer.session && \ @@ -58,10 +55,9 @@ RUN --mount=type=bind,from=ctx-files,source=/workstation/files,target=/wfiles \ RUN --mount=type=bind,from=ctx-files,source=/workstation/files,target=/wfiles \ install -Dm0644 /wfiles/etc/sirius/distro.toml /etc/sirius/distro.toml && \ sed -i \ - -e "s|@WORKSTATION_IMAGE@|${workstation_image}|g" \ -e "s|@WORKSTATION_TARGET_IMAGE@|${workstation_target_image}|g" \ /etc/sirius/distro.toml && \ - grep -q 'image = "containers-storage:'"${workstation_image}"'"' /etc/sirius/distro.toml && \ + grep -q 'image = "oci:/usr/lib/luminusos/payload.oci:latest"' /etc/sirius/distro.toml && \ grep -q 'target_imgref = "'"${workstation_target_image}"'"' /etc/sirius/distro.toml && \ install -Dm0644 /wfiles/etc/sirius/sirius.toml /etc/sirius/sirius.toml && \ install -Dm0644 /wfiles/usr/share/polkit-1/rules.d/50-sirius-live.rules /usr/share/polkit-1/rules.d/50-sirius-live.rules && \ @@ -163,7 +159,6 @@ RUN command -v sirius && \ test -z "$(find / -xdev -name '.wh.*' -print -quit)" && \ test -f /etc/sirius/distro.toml && \ test -f /etc/sirius/sirius.toml && \ - test -f /etc/containers/containers.conf.d/99-luminusos-bootc-install-tmp.conf && \ # Exactly the three LuminusOS repart definitions: the RPM's generic # 10-esp/20-root pair must be gone or systemd-repart would see five. test ! -e /usr/share/sirius/repart.d/10-esp.conf && \ diff --git a/editions/workstation/files/etc/containers/containers.conf.d/99-luminusos-bootc-install-tmp.conf b/editions/workstation/files/etc/containers/containers.conf.d/99-luminusos-bootc-install-tmp.conf deleted file mode 100644 index e5c1236..0000000 --- a/editions/workstation/files/etc/containers/containers.conf.d/99-luminusos-bootc-install-tmp.conf +++ /dev/null @@ -1,2 +0,0 @@ -[engine] -image_copy_tmp_dir = "/var/tmp" diff --git a/editions/workstation/files/etc/sirius/distro.toml b/editions/workstation/files/etc/sirius/distro.toml index a89ec81..4bd4e75 100644 --- a/editions/workstation/files/etc/sirius/distro.toml +++ b/editions/workstation/files/etc/sirius/distro.toml @@ -1,5 +1,9 @@ [bootc] -image = "containers-storage:@WORKSTATION_IMAGE@" +# 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. +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 — # today the ghcr.io images are unsigned, so verification has nothing to check. diff --git a/editions/workstation/files/etc/sirius/sirius.toml b/editions/workstation/files/etc/sirius/sirius.toml index 852b157..db38951 100644 --- a/editions/workstation/files/etc/sirius/sirius.toml +++ b/editions/workstation/files/etc/sirius/sirius.toml @@ -4,10 +4,8 @@ disabled = ["keyboard", "timezone", "user"] [diagnostics] require = ["uefi", "ram", "disk_space"] warn = ["secure_boot", "network", "virt"] -# bootc stages the compressed image copy (~2.5 GiB) in /var/tmp, a dedicated -# tmpfs capped at 75% of RAM (see var-tmp.mount), while the live session -# itself also runs from RAM. Installs need roughly 6 GB of memory. The probe -# measures USABLE RAM (a "6 GB" machine reports ~5.7 GiB after -# kernel/firmware reservations), so the gate sits at 5 GiB: 6 GB machines -# pass, machines that cannot hold the staging are blocked. -min_ram_gib = 5 +# The installer payload is embedded as an OCI layout (see distro.toml), so +# `bootc install` streams layer blobs straight to disk instead of staging a +# re-tarred copy in RAM. The gate only needs to cover the live GNOME +# session itself. +min_ram_gib = 2 diff --git a/editions/workstation/files/usr/lib/systemd/system/var-tmp.mount b/editions/workstation/files/usr/lib/systemd/system/var-tmp.mount deleted file mode 100644 index a3e6dcf..0000000 --- a/editions/workstation/files/usr/lib/systemd/system/var-tmp.mount +++ /dev/null @@ -1,26 +0,0 @@ -# Large RAM-backed /var/tmp for the live installer session. -# -# The live root's writable overlay keeps its upperdir in /run, a tmpfs the -# kernel caps at 20% of RAM (1.6 GiB on an 8 GiB machine). `bootc install` -# stages the compressed container image (~2.5 GiB) in /var/tmp before -# deploying, which can never fit there. A dedicated tmpfs on /var/tmp lifts -# that ceiling to 75% of RAM without touching /run or the overlay. -# -# Modeled after systemd's stock tmp.mount. - -[Unit] -Description=Large temporary directory for bootc image staging (/var/tmp) -ConditionKernelCommandLine=rd.live.image -DefaultDependencies=no -Conflicts=umount.target -Before=local-fs.target umount.target -After=swap.target - -[Mount] -What=tmpfs -Where=/var/tmp -Type=tmpfs -Options=mode=1777,strictatime,nosuid,nodev,size=75% - -[Install] -WantedBy=local-fs.target diff --git a/tests/run.sh b/tests/run.sh index c602107..5a8af13 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -62,11 +62,11 @@ 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 placeholders ───────────── -expect "distro.toml has @WORKSTATION_IMAGE@ placeholder" \ - grep -q '@WORKSTATION_IMAGE@' "${WORKSTATION_FILES}/etc/sirius/distro.toml" +# ── 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 diff --git a/tools/qemu.sh b/tools/qemu.sh index ba3778c..f32d8ff 100755 --- a/tools/qemu.sh +++ b/tools/qemu.sh @@ -17,11 +17,11 @@ if [ -f .env ]; then fi # default values for QEMU -# NOTE: the live session stages the bootc image copy in RAM (/var/tmp is a -# dedicated tmpfs capped at 75% of RAM, see var-tmp.mount in the installer -# image). The compressed payload is ~2.5 GiB, so the installer needs at -# least ~6 GiB of guest memory; 8 GiB is safe. -: ${QEMU_MEM:="8G"} +# 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 +# live GNOME session plus headroom. +: ${QEMU_MEM:="4G"} : ${QEMU_CPU:="4"} : ${QEMU_BOOT:="uefi"} : ${QEMU_DISPLAY:="auto"} From dc02529e0cf8a08d0afb62cced27101ef39fceaa Mon Sep 17 00:00:00 2001 From: Leandro Rodrigues Date: Mon, 20 Jul 2026 16:48:38 -0300 Subject: [PATCH 2/3] fix(iso): tag the embedded OCI payload as :latest, share the manifest patch with CI skopeo copy to an oci: destination without a tag writes no org.opencontainers.image.ref.name annotation into index.json, so bootc install failed with 'no descriptor found for reference "latest"' even though the payload was embedded. Append :latest to the destination path: skopeo strips it from the on-disk directory name (still payload.oci) and records it as the ref name annotation. The OCI embed patch also only existed in the local Justfile flow; the CI packaging script kept building the ISO with a plain image-builder run, embedding containers-storage while distro.toml already pointed at the oci: path. Extract the patch into tools/patch-iso-payload-to-oci.sh and use it from both package_iso() and .github/scripts/ci-package.sh (which now installs jq/osbuild, generates the manifest with --with-manifest, patches it, and runs osbuild directly). --- .github/scripts/ci-package.sh | 19 ++++++++++++++++- ARCHITECTURE.md | 2 +- Justfile | 13 +----------- tests/run.sh | 15 +++++++++++++ tools/patch-iso-payload-to-oci.sh | 35 +++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 14 deletions(-) create mode 100755 tools/patch-iso-payload-to-oci.sh diff --git a/.github/scripts/ci-package.sh b/.github/scripts/ci-package.sh index cb6b0bc..160d43c 100755 --- a/.github/scripts/ci-package.sh +++ b/.github/scripts/ci-package.sh @@ -11,7 +11,7 @@ set -euxo pipefail format="$1" -dnf -y install image-builder podman +dnf -y install image-builder podman jq osbuild if [ -n "${GHCR_TOKEN:-}" ]; then podman login ghcr.io -u "${GHCR_USER}" -p "${GHCR_TOKEN}" @@ -30,14 +30,31 @@ cache_dir="${PWD}/.osbuild-cache" case "${format}" in iso) 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}" \ --bootc-default-fs btrfs \ --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 bootiso/install.iso "${OUTPUT_NAME}" + rm -rf bootiso ;; qcow2) image-builder build \ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6b847b9..3b5e79f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -473,7 +473,7 @@ flowchart TD `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. -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}`) instead of `containers-storage`, and runs `osbuild` directly against the patched manifest (see `patch_iso_payload_to_oci()` in the Justfile). 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, `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. `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. diff --git a/Justfile b/Justfile index ba00511..8b541e1 100644 --- a/Justfile +++ b/Justfile @@ -270,18 +270,7 @@ package edition="workstation" format="all": # 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() { - local manifest_json="$1" patched_manifest="$2" - local skopeo_count - 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 patch_iso_payload_to_oci() in the Justfile" >&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"} - ' "$manifest_json" > "$patched_manifest" + ./tools/patch-iso-payload-to-oci.sh "$1" "$2" } package_iso() { diff --git a/tests/run.sh b/tests/run.sh index 5a8af13..f041dfc 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -98,6 +98,21 @@ 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 new file mode 100755 index 0000000..bd267dc --- /dev/null +++ b/tools/patch-iso-payload-to-oci.sh @@ -0,0 +1,35 @@ +#!/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 dc545ca6fd31ea39b6b334edbb4a10fa85fbac76 Mon Sep 17 00:00:00 2001 From: Leandro Rodrigues Date: Mon, 20 Jul 2026 16:49:12 -0300 Subject: [PATCH 3/3] chore(iso): silence packaging warnings and make reruns non-interactive - drop --bootc-default-fs btrfs: the image's disk.yaml already defines the btrfs root and takes precedence, so the flag only produced an 'ignoring --bootc-default-fs' warning - remove the duplicated 'no post-build squash needed' echo in package_iso() - mv -f when moving the generated manifest and ISO so reruns do not prompt interactively when artifacts from a previous run exist - docs: disk.yaml is now described as the source of truth for the install storage layout (AGENTS.md, ARCHITECTURE.md) --- .github/scripts/ci-package.sh | 4 +--- AGENTS.md | 2 +- ARCHITECTURE.md | 7 ++----- Justfile | 9 ++------- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/scripts/ci-package.sh b/.github/scripts/ci-package.sh index 160d43c..6b0ab62 100755 --- a/.github/scripts/ci-package.sh +++ b/.github/scripts/ci-package.sh @@ -34,7 +34,6 @@ case "${format}" in patched_manifest="${OUTPUT_NAME%.iso}.osbuild-manifest.oci.json" image-builder build \ --cache "${cache_dir}" \ - --bootc-default-fs btrfs \ --output-dir . \ --output-name "${OUTPUT_NAME}" \ --with-manifest \ @@ -53,13 +52,12 @@ case "${format}" in --output-directory . \ --export bootiso \ "${patched_manifest}" - mv bootiso/install.iso "${OUTPUT_NAME}" + mv -f bootiso/install.iso "${OUTPUT_NAME}" rm -rf bootiso ;; qcow2) image-builder build \ --cache "${cache_dir}" \ - --bootc-default-fs btrfs \ --output-dir . \ --output-name "${OUTPUT_NAME}" \ --bootc-ref "${WORKSTATION_IMAGE}" \ diff --git a/AGENTS.md b/AGENTS.md index e0736e4..2f5d82a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Use `QEMU_RESET_DISK=1 just qemu iso` when the installer test needs a fresh disk The workstation ISO uses Sirius (the LuminusOS installer, consumed as a prebuilt RPM from its GitHub release) and installs the embedded workstation image from container storage. ISO packaging uses `luminusos-workstation:-iso` as the live root and `luminusos-workstation:` as the installed payload. The live ISO itself is not booted as a bootc deployment. -Installed Linux filesystems should be Btrfs in the Sirius ISO install path. Keep `--bootc-default-fs btrfs` and the Sirius repart templates (`files/usr/share/sirius/repart.d/`) aligned when changing that storage layout. The direct qcow2 `image-builder` path uses `/usr/lib/image-builder/bootc/disk.yaml`; keep root/home/var Btrfs there, but `/boot` must remain ext4 because image-builder qcow2 generation does not support Btrfs for `/boot`. The ISO boot menu is configured through `/usr/lib/image-builder/bootc/iso.yaml`. +Installed Linux filesystems should be Btrfs in the Sirius ISO install path. Keep `/usr/lib/image-builder/bootc/disk.yaml` (which image-builder uses for the filesystem layout, overriding `--bootc-default-fs`) and the Sirius repart templates (`files/usr/share/sirius/repart.d/`) aligned when changing that storage layout: root/home/var stay Btrfs, but `/boot` must remain ext4 because image-builder qcow2 generation does not support Btrfs for `/boot`. The ISO boot menu is configured through `/usr/lib/image-builder/bootc/iso.yaml`. Sirius is configured through `/etc/sirius/distro.toml` and `/etc/sirius/sirius.toml`, installed only into the ISO live root by `editions/workstation/Containerfile.installer`. The installed workstation payload should not ship Sirius. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3b5e79f..5d6fb79 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -397,7 +397,7 @@ flowchart TD The Sirius ISO install storage model must remain aligned across: -- `--bootc-default-fs btrfs` in the `Justfile`. +- `/usr/lib/image-builder/bootc/disk.yaml` in the workstation image (btrfs root), which image-builder uses for both the ISO install path and direct qcow2 artifacts — it overrides `--bootc-default-fs`. - Sirius repart templates under `/usr/share/sirius/repart.d/`. Expected layout: @@ -489,7 +489,6 @@ Conceptual command: ```bash sudo image-builder build \ - --bootc-default-fs btrfs \ --output-dir . \ --output-name luminusos-workstation-.iso \ --bootc-ref \ @@ -505,7 +504,6 @@ Conceptual command: ```bash sudo image-builder build \ - --bootc-default-fs btrfs \ --output-dir . \ --output-name luminusos-workstation-.qcow2 \ --bootc-ref \ @@ -630,8 +628,7 @@ flowchart TD When changing this repository, keep these points aligned: - `Justfile`, `README.md`, `ARCHITECTURE.md`, and `AGENTS.md` must agree on active commands and supported editions. -- `--bootc-default-fs btrfs` and the Sirius repart templates must describe the same ISO install storage layout. -- `disk.yaml` must keep root/home/var Btrfs for direct qcow2 artifacts, with `/boot` ext4 for image-builder compatibility. +- `disk.yaml` must keep root/home/var Btrfs with `/boot` ext4 (image-builder compatibility) and describe the same storage layout as the Sirius repart templates; image-builder takes the filesystem from `disk.yaml`, not from `--bootc-default-fs`. - Any live-only file added under `editions/workstation/files/` must be installed only by `Containerfile.installer` so it stays out of the workstation payload. - Changes to `live-installer` must consider the Wayland session file, GNOME session file, GNOME Shell mode JSON, systemd user drop-in, and Aurora Shell metadata. - If the install flow changes, update `/etc/sirius/distro.toml` / `sirius.toml` and this document. diff --git a/Justfile b/Justfile index 8b541e1..b6e76aa 100644 --- a/Justfile +++ b/Justfile @@ -246,7 +246,6 @@ package edition="workstation" format="all": shift 3 sudo image-builder build \ - --bootc-default-fs btrfs \ --output-dir . \ --output-name "$output" \ "$@" \ @@ -275,9 +274,6 @@ package edition="workstation" format="all": package_iso() { build_iso_image - if [[ "$iso_image_ref" == localhost/* ]]; then - echo "No post-build squash needed for the workstation ISO image: Containerfile.installer squashes itself into a single layer" - fi local out_name="luminusos-workstation-${package_tag}.iso" local ib_cache="$(pwd)/.test/image-builder-cache" @@ -287,7 +283,6 @@ package edition="workstation" format="all": echo "Generating osbuild manifest for bootc-generic-iso (payload: $image_ref)" sudo image-builder build \ - --bootc-default-fs btrfs \ --output-dir . \ --output-name "$out_name" \ --cache "$ib_cache" \ @@ -301,7 +296,7 @@ package edition="workstation" format="all": echo "Expected osbuild manifest was not generated: $generated_manifest" exit 1 fi - mv "$generated_manifest" "$manifest_json" + mv -f "$generated_manifest" "$manifest_json" echo "Patching installer payload embed: containers-storage -> oci" patch_iso_payload_to_oci "$manifest_json" "$patched_manifest" @@ -318,7 +313,7 @@ package edition="workstation" format="all": echo "Expected osbuild export not found: bootiso/install.iso" exit 1 fi - sudo mv bootiso/install.iso "$out_name" + sudo mv -f bootiso/install.iso "$out_name" sudo rm -rf bootiso sudo chown "$(id -u):$(id -g)" "$out_name"