#!/bin/bash # Build a raw (or qcow2) disk image from a dish, without libvirt/virt-install. # # Runs `livemedia-creator --no-virt --make-disk` directly on the host, so # the result is a disk image you can hand to another VM manager directly # (e.g. `virtpull local ./build/.img --wonder-vm NAME`) instead of a # bootable ISO. # # Usage: ./build-image.sh [--as qcow2] [--root-size MiB] [--extra-ks FILE] # # Requires: lorax-lmc-novirt (livemedia-creator), qemu-img, and root (the # --no-virt disk install needs real loop-device access via # /dev/loop-control; individual commands below run under sudo — passwordless # sudo, or run this whole script as root). # # Earlier iteration wrapped this in `mock` (mirroring # .gitea/workflows/build-iso.yaml's ISO build) to pin the target release's # toolchain. That does not work for --make-disk: mock's chroot has no live # systemd-udevd, so udev never populates properties for the loop device # livemedia-creator creates for the image, and blivet's device scan # crashes (blivet/udev.py device_get_name: "TypeError: argument of type # 'NoneType' is not a container or iterable") — reproduced identically on # Fedora 44 and 43 mock chroots, and confirmed absent when run directly on # the host instead. --make-iso never hits this: ISO building doesn't touch # block devices at all, only --make-disk (and other disk/appliance modes) # does. The consequence: this script now builds using whatever # anaconda/lorax the host has installed — the kickstart's own repo/url # lines still pin the actual package set to the target Fedora release # regardless, same as any other direct (non-mock) livemedia-creator use. set -euo pipefail AS_FORMAT="" ROOT_SIZE=8192 EXTRA_KS="" while [[ $# -gt 0 ]]; do case "$1" in --as) AS_FORMAT="$2" shift 2 ;; --root-size) ROOT_SIZE="$2" shift 2 ;; --extra-ks) EXTRA_KS="$2" shift 2 ;; *) echo "Unknown argument: $1" echo "Usage: $0 [--as qcow2] [--root-size MiB] [--extra-ks FILE]" exit 1 ;; esac done if [[ -n "$AS_FORMAT" && "$AS_FORMAT" != "qcow2" ]]; then echo "--as only supports 'qcow2' (the build itself always produces raw)" exit 1 fi if ! [[ "$ROOT_SIZE" =~ ^[0-9]+$ ]]; then echo "--root-size must be a whole number of MiB, got '$ROOT_SIZE'" exit 1 fi if [[ -n "$EXTRA_KS" && ! -f "$EXTRA_KS" ]]; then echo "--extra-ks file not found: $EXTRA_KS" exit 1 fi for tool in livemedia-creator qemu-img; do if ! command -v "$tool" &> /dev/null; then echo "Missing required tool: $tool" echo "Fedora: sudo dnf install lorax-lmc-novirt qemu-img" exit 1 fi done if [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then echo "This needs root (loop-device access for the --no-virt disk install)." echo "Re-run as root, or ensure passwordless sudo is available." exit 1 fi # Regenerate recipes and dishes before building (they are build products) if ! (cd cook && make all); then echo "Failed to generate dishes in cook/ (see 'make all' in cook/)" exit 1 fi # Pick a dish (shared with deploy/deploy-distro.sh) # shellcheck source=./deploy/select-dish.sh source "$(dirname "$0")/deploy/select-dish.sh" dish="$selected_dish" # Derive the target release, same convention as # deploy/deploy-distro.sh's find_fedora_iso() (used for --releasever, which # substitutes @VERSION@ in bootloader/template text — the kickstart's own # repo/url lines are what actually pin the package set). if [[ "$dish" == *"rawhide"* ]]; then releasever="rawhide" elif [[ "$dish" =~ _([0-9]+) ]]; then releasever="${BASH_REMATCH[1]}" else echo "Could not determine a Fedora release version from dish name '$dish'." echo "Expected a '_' segment (e.g. desktop_44_standard...) or 'rawhide'." exit 1 fi out_dir="./build" mkdir -p "$out_dir" out_dir="$(cd "$out_dir" && pwd)" # Adjustments needed only for this --no-virt disk-image path, applied to a # scratch copy rather than the shared dish: # # 1. cook/ingredients/core/base.ks sets `text` for deploy/deploy-distro.sh's # virt-install netinstall console; livemedia-creator manages the display # mode itself and refuses a kickstart that also sets one ("The kickstart # must not set a display mode"). # # 2. cook/ingredients' storage layout uses `part / --grow` with no explicit # --size: fine for virt-install, which pre-creates the disk at a known # size for anaconda to grow into. livemedia-creator instead computes the # total image size up front by summing partition sizes, and crashes # (TypeError: unsupported operand type(s) for +: 'int' and 'NoneType') # on a --grow partition with no size to add. Give it one via --root-size # (default 8192 MiB; the desktop dishes likely need more). # # 3. --extra-ks appends arbitrary local content (e.g. a bespoke `user`/ # `rootpw`/`security` override) after the above. Kickstart's singular # commands take the last occurrence, so this can override anything the # dish already set without touching the shared ingredients — see the # README's "bespoke dish not part of the matrix" pattern. Never commit # a file used here if it carries real credentials. disk_ks="$(mktemp "${TMPDIR:-/tmp}/${dish}.disk-build.XXXXXX.cfg")" trap 'rm -f "$disk_ks"' EXIT grep -v -E '^(text|cmdline|graphical)([[:space:]]|$)' "cook/dishes/${dish}.cfg" \ | awk -v size="$ROOT_SIZE" ' /^part / && /--grow/ && !/--size=/ { $0 = $0 " --size=" size } { print } ' > "$disk_ks" if [[ -n "$EXTRA_KS" ]]; then { echo cat "$EXTRA_KS" } >> "$disk_ks" fi if command -v ksvalidator &> /dev/null; then echo "Validating kickstart (informational; a build failure is the real test)..." ksvalidator "$disk_ks" || true fi resultdir="$(mktemp -u "${TMPDIR:-/tmp}/lmc-result.XXXXXX")" echo "Building disk image for '$dish' (target release $releasever)..." sudo livemedia-creator --ks "$disk_ks" --no-virt --resultdir "$resultdir" \ --make-disk --image-name "${dish}.img" --releasever "$releasever" # livemedia-creator's exact output filename depends on the installed lorax # version; don't assume --image-name is honored verbatim, take whatever # single disk image landed in the resultdir. produced=$(sudo find "$resultdir" -maxdepth 1 -type f \( -iname '*.img' -o -iname '*disk*' \) | head -n 1) if [[ -z "$produced" ]]; then echo "livemedia-creator did not produce a recognizable disk image under $resultdir" exit 1 fi raw_out="${out_dir}/${dish}.img" sudo cp --reflink=auto "$produced" "$raw_out" sudo chown "$(id -u):$(id -g)" "$raw_out" sudo rm -rf "$resultdir" echo "Built: $raw_out" if [[ "$AS_FORMAT" == "qcow2" ]]; then qcow2_out="${out_dir}/${dish}.qcow2" qemu-img convert -O qcow2 "$raw_out" "$qcow2_out" echo "Converted: $qcow2_out" final="$qcow2_out" else final="$raw_out" fi echo echo "Next: virtpull local $final --wonder-vm "