Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 5 additions & 22 deletions .github/scripts/ci-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@
# 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)
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}"
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
Expand All @@ -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 . \
Expand Down
17 changes: 15 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}" \
Expand Down
14 changes: 6 additions & 8 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <payload-image> 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.

Expand All @@ -492,11 +492,10 @@ sudo image-builder build \
--output-dir . \
--output-name luminusos-workstation-<tag>.iso \
--bootc-ref <workstation-iso-image> \
--bootc-installer-payload-ref <workstation-image> \
bootc-generic-iso
```

`--bootc-ref` defines the live root and points at `luminusos-workstation:<tag>-iso`. `--bootc-installer-payload-ref` embeds the normal `luminusos-workstation:<tag>` image as the installer payload.
`--bootc-ref` defines the live root and points at `luminusos-workstation:<tag>-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

Expand All @@ -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<br/>.test/payload.oci"]
ISOBuilder["image-builder<br/>bootc-generic-iso"]
QCOW2Builder["image-builder<br/>qcow2"]
BootcRef["--bootc-ref"]
PayloadRef["--bootc-installer-payload-ref"]
WorkstationISO["workstation ISO root<br/>luminusos-workstation:&lt;tag&gt;-iso"]
ISO["luminusos-workstation-<tag>.iso"]
QCOW2["luminusos-workstation-<tag>.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
```
Expand Down
165 changes: 36 additions & 129 deletions Justfile
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -30,10 +29,8 @@ keep_sudo_alive() {
'''


# QEMU settings
qemu_disk_size := env("QEMU_INSTALL_DISK_SIZE", "64G")

# ── Recipes ──────────────────────────────────────────────────────────────

# Default recipe: show available commands
default:
Expand All @@ -50,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 <tag> <containerfile> [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
Expand All @@ -81,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 }} \
Expand All @@ -92,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
Expand All @@ -106,16 +100,17 @@ build edition="workstation":
echo "Run 'just build workstation' first."
exit 1
fi
sudo buildah bud \
--layers \
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
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"
;;
*)
Expand Down Expand Up @@ -166,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
Expand Down Expand Up @@ -260,66 +217,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() {
Expand Down
Loading