diff --git a/.forgejo/workflows/iso.yml b/.forgejo/workflows/iso.yml index 09b4346..9fadb0d 100644 --- a/.forgejo/workflows/iso.yml +++ b/.forgejo/workflows/iso.yml @@ -1,12 +1,12 @@ -name: Image, installer ISO and disk image +name: Image, installer ISO, disk image and live ISO # Builds the quine-linux image on our Aurora build (published by # quine-global-labs/aurora's workflow to this forge's registry), pushes it to # forge.quinefoundation.com/quine-global-labs/quine-linux (`latest`, -# ``, the commit), then builds the installer ISO and a disk image -# (qcow2) from it and uploads them to MinIO as -# /installer//install.iso, /installer/latest/install.iso -# and /disk/latest/disk.qcow2. +# ``, the commit), then builds the installer ISO, a disk image (qcow2) +# and a live ISO from it and uploads them to MinIO as +# /installer//install.iso, /installer/latest/install.iso, +# /disk/latest/disk.qcow2 and /live/latest/live.iso. # # Runs on runner-x86-linux (host mode, root podman; petersweb-infra, # hosts/runner-x86-linux): bootc-image-builder needs an x86_64 host with @@ -86,9 +86,11 @@ jobs: - name: Vendor upstream images # Into the forge's mirror/ (.forgejo/vendor-images.sh), which the # pulls below then read: the installer's two bases - # (installer/Containerfile) and bootc-image-builder (Taskfile.yml). + # (installer/Containerfile), bootc-image-builder (Taskfile.yml) and + # what Titanoboa's ISO step runs in (Taskfile.yml, FEDORA_IMAGE). run: | .forgejo/vendor-images.sh \ + quay.io/fedora/fedora:latest \ quay.io/fedora/fedora-bootc:rawhide \ registry.fedoraproject.org/fedora:rawhide \ ghcr.io/osbuild/bootc-image-builder:latest @@ -103,6 +105,7 @@ jobs: run: | for img in \ "$BASE_IMAGE" \ + quay.io/fedora/fedora:latest \ quay.io/fedora/fedora-bootc:rawhide \ registry.fedoraproject.org/fedora:rawhide \ ghcr.io/osbuild/bootc-image-builder:latest; do @@ -131,6 +134,10 @@ jobs: - name: Build disk image run: task build-disk-image CONTAINER_RUNTIME=podman BASE_IMAGE="$BASE_IMAGE" + - name: Build live ISO + # Installs quine-linux:latest, which "Push image" just updated. + run: task build-live-iso CONTAINER_RUNTIME=podman BASE_IMAGE="$BASE_IMAGE" + - name: Upload to MinIO env: MINIO_ACCESS_KEY: ${{ vars.MINIO_ACCESS_KEY || secrets.MINIO_ACCESS_KEY }} @@ -147,9 +154,11 @@ jobs: dest="store/${MINIO_BUCKET}/installer" mc cp "$iso" "${dest}/${GITHUB_SHA}/install.iso" mc cp "${dest}/${GITHUB_SHA}/install.iso" "${dest}/latest/install.iso" - # The disk image: latest only, it's as big as the ISO. + # The disk image and live ISO: latest only, they're as big as the ISO. ls -lh output/qcow2/disk.qcow2 mc cp output/qcow2/disk.qcow2 "store/${MINIO_BUCKET}/disk/latest/disk.qcow2" + ls -lh output/live/Quine-Linux-Live.iso + mc cp output/live/Quine-Linux-Live.iso "store/${MINIO_BUCKET}/live/latest/live.iso" - name: Publish the release if: github.ref_type == 'tag' @@ -162,6 +171,7 @@ jobs: mkdir -p release ln -f "$(find output -name install.iso -print -quit)" "release/quine-linux-$tag-installer-x86_64.iso" ln -f output/qcow2/disk.qcow2 "release/quine-linux-$tag-x86_64.qcow2" + ln -f output/live/Quine-Linux-Live.iso "release/quine-linux-$tag-live-x86_64.iso" (cd release && sha256sum -- * > SHA256SUMS) cat release/SHA256SUMS diff --git a/README.md b/README.md index e0a9336..de69cd3 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,12 @@ list what's available. Notably: stock Fedora's, landing in `output/bootiso/install.iso`. See [`docs/patched-anaconda-installer.md`](docs/patched-anaconda-installer.md) for forking/patching Anaconda itself. +- `task build-disk-image` — builds a ready-installed disk image (qcow2) + via `bootc-image-builder`, landing in `output/qcow2/disk.qcow2`. It has + no user: plasma-setup creates the first one at first boot. +- `task build-live-iso` — builds a live ISO (`live/`, after Aurora's): + boots into a Quine Linux session with Anaconda WebUI to install it, + landing in `output/live/Quine-Linux-Live.iso`. Podman only. - `task test:stage` / `task test:boot` — stage the locally-built image as this machine's next boot target (`bootc switch`), optionally rebooting into it immediately. The fast way to test a change — especially the diff --git a/Taskfile.yml b/Taskfile.yml index cf4ce64..2b76ad4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -3,6 +3,7 @@ version: '3' vars: IMAGE: quine-linux INSTALLER_IMAGE: quine-installer + LIVE_IMAGE: quine-linux-live # What the Containerfile builds FROM: our Aurora fork # (quine-global-labs/aurora), built and published by that repo's workflow. # Override with a local build of the fork (BASE_IMAGE=localhost/aurora:stable, @@ -25,6 +26,8 @@ vars: PLATFORM: linux/amd64 TARGET_ARCH: amd64 SKOPEO_IMAGE: quay.io/skopeo/stable:latest + # Titanoboa's ISO step runs in this (live/titanoboa-build-iso.sh). + FEDORA_IMAGE: quay.io/fedora/fedora:latest # bootc-image-builder reads images from a containers-storage mounted at # /var/lib/containers/storage. Under podman that's root's real store; docker # has no such store, so images get copied (via skopeo) into this volume. @@ -166,6 +169,47 @@ tasks: generates: - "{{.OUTPUT_DIR}}/qcow2/disk.qcow2" + build-live-image: + desc: >- + Build the live ISO's root filesystem (live/Containerfile): quine-linux + plus the live session and Anaconda WebUI, with PAYLOAD_IMAGE (what the + installer installs) pulled into it. Podman only. + vars: + PAYLOAD_IMAGE: '{{.PAYLOAD_IMAGE | default "forge.quinefoundation.com/quine-global-labs/quine-linux:latest"}}' + preconditions: + - sh: '[ "{{.CONTAINER_RUNTIME}}" = podman ]' + msg: The live ISO tasks need podman (they mount the image into Titanoboa's container). + cmds: + - >- + {{.SUDO}} podman build --platform {{.PLATFORM}} + --cap-add sys_admin --security-opt label=disable + --build-arg BASE_IMAGE=localhost/{{.IMAGE}} + --build-arg PAYLOAD_IMAGE={{.PAYLOAD_IMAGE}} + -t {{.LIVE_IMAGE}} -f live/Containerfile live/ + + build-live-iso: + desc: >- + Build the live ISO: boots into a Quine Linux session, with an installer. + Titanoboa's script (live/titanoboa-build-iso.sh) packs the live image + into it. Output lands in {{.OUTPUT_DIR}}/live/Quine-Linux-Live.iso. + PAYLOAD_IMAGE: see build-live-image. + preconditions: + - sh: '[ "{{.CONTAINER_RUNTIME}}" = podman ]' + msg: The live ISO tasks need podman (they mount the image into Titanoboa's container). + cmds: + - task: build-image + - task: build-live-image + - mkdir -p {{.OUTPUT_DIR}}/live + - >- + {{.SUDO}} podman run --rm {{.PULL_FLAG}} + --cap-add sys_admin --security-opt label=disable + -v {{.ROOT_DIR}}/live/titanoboa-build-iso.sh:/src/build_iso.sh:ro + --mount type=image,source=localhost/{{.LIVE_IMAGE}},dst=/rootfs + -v {{.OUTPUT_DIR}}/live:/output + {{.FEDORA_IMAGE}} /src/build_iso.sh + generates: + - "{{.OUTPUT_DIR}}/live/Quine-Linux-Live.iso" + export-to-storage: internal: true desc: >- diff --git a/live/Containerfile b/live/Containerfile new file mode 100644 index 0000000..6df7a15 --- /dev/null +++ b/live/Containerfile @@ -0,0 +1,17 @@ +# The live ISO's root filesystem: quine-linux plus what booting from an ISO +# and installing from it need (build.sh). Titanoboa's script then packs it +# into the ISO (titanoboa-build-iso.sh, task build-live-iso). +# +# Build with --cap-add sys_admin --security-opt label=disable: build.sh +# remounts /proc/sys and pulls the image the installer installs. +ARG BASE_IMAGE=localhost/quine-linux +# What the installer puts on disk, from the registry, and what installed +# systems then update from. +ARG PAYLOAD_IMAGE=forge.quinefoundation.com/quine-global-labs/quine-linux:latest +FROM ${BASE_IMAGE} +ARG PAYLOAD_IMAGE +ENV PAYLOAD_IMAGE=${PAYLOAD_IMAGE} + +COPY . /src/live/ +RUN --mount=type=cache,dst=/var/cache/libdnf5 \ + bash /src/live/build.sh && rm -rf /src/live diff --git a/live/build.sh b/live/build.sh new file mode 100644 index 0000000..a433213 --- /dev/null +++ b/live/build.sh @@ -0,0 +1,226 @@ +#!/usr/bin/bash +# Turns quine-linux into the live ISO's root filesystem (live/Containerfile): +# boots from the ISO into a KDE session as the live user, with Anaconda WebUI +# to install $PAYLOAD_IMAGE. Adapted from Aurora's live ISO, +# github.com/get-aurora-dev/iso (Apache-2.0), iso_files/*.sh at bee017f. +# +# Installed systems get their first user from plasma-setup at first boot, so +# the installer hides its account pages, as Aurora's does. +set -exo pipefail + +SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Create the directory that /root is symlinked to +mkdir -p "$(realpath /root)" +# bwrap (flatpak) writes /proc/sys/user/max_user_namespaces, read-only here +mount -o remount,rw /proc/sys + +# ── The image to install, and the system Flatpaks to copy over ───────────── +# Both land in this filesystem, so installing needs no network. +podman pull "$PAYLOAD_IMAGE" + +grep -v '#' /usr/share/ublue-os/homebrew/system-flatpaks.Brewfile | grep '^flatpak' | + sed 's/flatpak //' | tr -d '"' >/tmp/flatpaks.list +curl --retry 3 -Lo /etc/flatpak/remotes.d/flathub.flatpakrepo https://dl.flathub.org/repo/flathub.flatpakrepo +xargs -r flatpak install -y --noninteractive /etc/systemd/system/var-tmp.mount <<'EOF' +[Unit] +Description=Larger tmpfs for /var/tmp on live system + +[Mount] +What=tmpfs +Where=/var/tmp +Type=tmpfs +Options=size=50%,nr_inodes=1m,x-systemd.graceful-option=usrquota + +[Install] +WantedBy=local-fs.target +EOF +systemctl enable var-tmp.mount + +rm -f /etc/localtime +systemd-firstboot --timezone UTC + +mkdir -p /usr/lib/bootc-image-builder +cp "$SRC/iso.yaml" /usr/lib/bootc-image-builder/iso.yaml + +# ── What a live session shouldn't do ─────────────────────────────────────── +for unit in tailscaled.service brew-upgrade.timer brew-update.timer brew-setup.service \ + uupd.timer ublue-system-setup.service flatpak-preinstall.service; do + systemctl disable "$unit" || true +done +systemctl --global disable podman-auto-update.timer ublue-user-setup.service || true +rm -f /usr/share/applications/dev.getaurora.system-update.desktop +rm -f /usr/lib/systemd/system/flatpak-add-fedora-repos.service + +# Only the system Flatpaks above get copied to the installed system, not +# what the live user installs. +cat >/usr/lib/systemd/system/var-lib-flatpak.mount <<'EOF' +[Unit] +Description=tmpfs so only the system Flatpaks are transferred to the installed system +Conflicts=umount.target + +[Mount] +Type=overlay +What=overlay +Where=/var/lib/flatpak +Options=lowerdir=/var/lib/flatpak,upperdir=/run/overlay/flatpak,workdir=/run/overlay/flatpak.work +[Install] +WantedBy=local-fs.target +EOF +cat >/usr/lib/tmpfiles.d/quine-live-flatpak.conf <<'EOF' +d /run/overlay/flatpak 0755 - - - +d /run/overlay/flatpak.work 0755 - - - +EOF +systemctl enable var-lib-flatpak.mount + +# ── KDE in the live session ──────────────────────────────────────────────── +# The installer in Kickoff's favourites; no wallet prompts +sed -i '2s/$/;liveinst.desktop/' /usr/share/kde-settings/kde-profile/default/xdg/kicker-extra-favoritesrc +tee -a /etc/xdg/kwalletrc <<'EOF' +[Wallet] +Enabled=false +EOF + +. /etc/os-release +echo "Quine Linux $VERSION_ID" >/etc/system-release + +# plasma-welcome crashes with --live-environment unless given its pages +# (github.com/ublue-os/aurora/issues/2624, bugs.kde.org/523540) +cat >/usr/bin/plasma-welcome <<'EOF' +#!/usr/bin/env bash +exec /usr/bin/plasma-welcome-original --pages Live,Welcome,SimpleByDefault,PowerfulWhenNeeded,Enjoy "$@" +EOF +rm -f /usr/share/plasma/plasma-welcome/intro-customization.desktop + +# ── The installer ────────────────────────────────────────────────────────── +sed -i 's/ANACONDA_PRODUCTVERSION=.*/ANACONDA_PRODUCTVERSION=""/' /usr/{,s}bin/liveinst || true + +# The profile matches os-release's ID (Aurora's, which quine-linux keeps). +mkdir -p /etc/anaconda/profile.d +tee /etc/anaconda/profile.d/quine-linux.conf </dev/null + +dnf install -y squashfs-tools xorriso yq mtools dosfstools + +mkdir -p \ + /work \ + /work/iso-root \ + /work/iso-root/boot/grub2 \ + /work/iso-root/images/pxeboot \ + /work/iso-root/LiveOS +cd /work || exit 1 + +# Create the squashfs image of the container image +mksquashfs /rootfs /work/iso-root/LiveOS/squashfs.img -all-root -noappend -e sysroot -e ostree -comp zstd -Xcompression-level 19 + +iso_config_file=/rootfs/usr/lib/bootc-image-builder/iso.yaml +if [[ ! -f $iso_config_file ]]; then + echo >&2 "ERROR: Missing /usr/lib/bootc-image-builder/iso.yaml file" + exit 1 +fi + +iso_label=$(yq '.label' <$iso_config_file) + +# Copy initrd and kernel +cp -av /rootfs/usr/lib/modules/*/initramfs.img /work/iso-root/images/pxeboot/initrd.img +cp -av /rootfs/usr/lib/modules/*/vmlinuz /work/iso-root/images/pxeboot/vmlinuz + +# Copy GRUB modules +for grub_arch in i386-pc arm64-efi; do + [ -f "/rootfs/usr/lib/grub/$grub_arch" ] || continue + echo >&2 "Found $grub_arch files, copying to /work/iso-root/boot/grub2/$grub_arch ..." + cp -avT /rootfs/usr/lib/grub/$grub_arch /work/iso-root/boot/grub2/$grub_arch +done + +# Copy efi dir +cp -avT /rootfs/boot/efi/EFI /work/EFI + +# Generate grub.cfg +{ grub_cfg="$("$dir/grub.cfg" +done + +# For some reason, fedora also copies EFI into /boot/EFI (?), probably because of hardcoded prefix in grub/shim +cp -avT /work/EFI /work/iso-root/EFI + +# Generate uefi.img +pushd /work || exit 1 +truncate -s 100M /work/uefi.img +mkfs.fat -F32 /work/uefi.img +mcopy -v -i /work/uefi.img -s /work/EFI :: +xorriso -as mkisofs \ + -R \ + -V "$iso_label" \ + -partition_offset 16 \ + -appended_part_as_gpt \ + -append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B ./uefi.img \ + -iso_mbr_part_type EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 \ + -e --interval:appended_partition_2:all:: \ + -no-emul-boot \ + -iso-level 3 \ + -o "/output/$iso_label.iso" \ + iso-root