#!/usr/bin/env bash
#
# build-image.sh — build the Alloy bootc image and a disk image
# (raw by default) with bootc-image-builder.
#
# Alloy re-brands os-release (ID=alloy, with its own VERSION_ID). Two
# consequences are handled in the Containerfile (pinned $releasever, disabled
# build-only repos) and one here: bootc-image-builder has no distro definition
# named "alloy-<VERSION_ID>", so build/bib-defs.yaml is bind-mounted into its
# defs dir under that name. The name is derived from usr/lib/os-release rather
# than hardcoded, so a product version bump does not rename a file whose
# contents have no version in them.
#
# Everything runs rootful on purpose: the image build and bib share one
# container store, so bib finds the image at /var/lib/containers/storage
# without a rootless->rootful copy.
#
# This script builds disk images only. It does not build ISOs, and asking it
# for one is an error rather than a surprise: every ISO type bib offers ends in
# Anaconda (verified by generating the manifests; see GO task a1d037f8), and an
# install from such an ISO leaves root locked and no account, because the
# Anaconda flow has no user-creation spoke. The installer ISO that boots into
# `alloy install` is built outside bib by build/build-iso.sh.
#
# Usage:
#   build/build-image.sh                     # build image + raw disk image
#   build/build-image.sh --type qcow2        # build image + qcow2 disk image
#   build/build-image.sh --host fw13         # mint with fw13's recipe
#   build/build-image.sh --no-preflight      # mint a knowingly incomplete image
#   build/build-image.sh --skip-build        # reuse the current image, just run bib
#   build/build-image.sh --write-only --write /dev/sdX  # write what is already built
#   build/build-image.sh --write /dev/sdX    # also dd the artifact to a device
#
# Writing is build/write-device.sh's job, shared with build/build-iso.sh: it
# requires an explicit device path and an interactive confirmation, refuses
# partitions and anything with a mounted filesystem, and verifies the result
# with cmp before claiming success.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# priv / privc. run0 where it exists, sudo where it does not; see the header
# of build/privilege.sh for which of the two a call site wants.
# shellcheck source=build/privilege.sh
. "$REPO_ROOT/build/privilege.sh"

# stamp_build_args. The build number the Containerfile bakes into os-release;
# see the header of build/build-stamp.sh.
# shellcheck source=build/build-stamp.sh
. "$REPO_ROOT/build/build-stamp.sh"

# host_recipe_args. The per-machine dials, so a build host is asked for by name
# rather than by remembering four flags; see the header of build/host-recipe.sh.
# shellcheck source=build/host-recipe.sh
. "$REPO_ROOT/build/host-recipe.sh"

# preflight_gate. Runs build/preflight.sh ahead of the mint and decides what a
# finding means; see the header of build/preflight-gate.sh.
# shellcheck source=build/preflight-gate.sh
. "$REPO_ROOT/build/preflight-gate.sh"

IMAGE="localhost/alloy:local"
BIB_IMAGE="quay.io/centos-bootc/bootc-image-builder:latest"
DEF="$REPO_ROOT/build/bib-defs.yaml"
OUTPUT="$REPO_ROOT/output"

# The name bib will look the def up under. It resolves a distro def as
# <ID>-<VERSION_ID>, both read from the image's os-release, so this has to
# track usr/lib/os-release and not a constant here. Read with `sed` rather
# than sourced: os-release is shell-shaped but this file is not a place to
# execute it.
DEF_VERSION="$(sed -n 's/^VERSION_ID="\{0,1\}\([^"]*\)"\{0,1\}$/\1/p' \
  "$REPO_ROOT/usr/lib/os-release")"
[ -n "$DEF_VERSION" ] || { echo "error: no VERSION_ID in usr/lib/os-release" >&2; exit 1; }
DEF_NAME="alloy-${DEF_VERSION}.yaml"

TYPE="raw"
WRITE_DEV=""
# Empty means no per-machine recipe: the Containerfile's own defaults, which
# carry no compiler and no database. See build/host-recipe.sh.
HOST=""
SKIP_BUILD=0
# The preflight runs ahead of every mint that has a recipe. See --no-preflight.
NO_PREFLIGHT=0
SKIP_BIB=0

BUILD_ARGS=()

die() { printf 'error: %s\n' "$*" >&2; exit 1; }

# The header block is the help text, so it stops where the comments stop.
# A hardcoded last line was wrong by seven lines and printed `set -euo
# pipefail` and REPO_ROOT= at people; any edit to the header would have
# rotted it again.
usage() {
  awk 'NR==1 {next} !/^#/ {exit} {sub(/^# ?/, ""); print}' "${BASH_SOURCE[0]}"
  exit "${1:-0}"
}

while [ $# -gt 0 ]; do
  case "$1" in
    --type)       TYPE="${2:?--type needs a value}"; shift 2 ;;
    --write)      WRITE_DEV="${2:?--write needs a device path}"; shift 2 ;;
    --skip-build) SKIP_BUILD=1; shift ;;
    # Mint without running build/preflight.sh first, for deliberately building a
    # known-incomplete image. Not a way past a finding you would rather not read.
    --no-preflight) NO_PREFLIGHT=1; shift ;;
    # --skip-bib is the older spelling, kept working. --write-only is the
    # name both scripts answer to, because "bib" means nothing in the ISO
    # path and the console emits one flag for both artifacts.
    --write-only|--skip-bib) SKIP_BIB=1; SKIP_BUILD=1; shift ;;
    # Passed straight to `podman build`. The Containerfile validates every
    # one of them against its own curated sets (PROFILE, BROWSER, LANGS), so
    # the gate lives there rather than here: a bad value has to fail the
    # build whether it came from `alloy image` or from a hand-typed flag.
    --build-arg)  BUILD_ARGS+=(--build-arg "${2:?--build-arg needs KEY=VALUE}"); shift 2 ;;
    # The machine this image is for, read from build/hosts/<name>.env. Its
    # dials go in ahead of anything typed here, so an explicit --build-arg is
    # the last value podman sees and overrides the recipe.
    --host)       HOST="${2:?--host needs a machine name}"; shift 2 ;;
    -h|--help)    usage 0 ;;
    *)            die "unknown argument: $1 (see --help)" ;;
  esac
done

[ -z "$HOST" ] || host_recipe_args "$HOST" BUILD_ARGS

# Refuse the ISO types outright. Both of bib's spellings compose Anaconda, and
# an artifact from either one installs a machine nobody can log into, so the
# failure has to land here at the argument rather than an hour later at a boot.
case "$TYPE" in
  iso|bootc-installer)
    die "$TYPE builds an Anaconda ISO, which installs an unloginable machine; use build/build-iso.sh for the Alloy installer ISO" ;;
esac

command -v podman >/dev/null || die "podman not found"
[ -f "$DEF" ] || die "missing distro def: $DEF"
[ -f "$REPO_ROOT/Containerfile" ] || die "no Containerfile at $REPO_ROOT"

# --write-only exists to make `--write` usable on its own. Without it the only
# way to write an artifact that already exists was to rebuild it first, so
# the documented workaround was to bypass this script and run dd by hand,
# which is exactly where the guards and the verify live.
if [ "$SKIP_BIB" -eq 1 ]; then
  echo "==> Skipping image build and bib; using the artifact already in $OUTPUT"
  [ -n "$WRITE_DEV" ] || die "--write-only only makes sense with --write"
fi

# 1. Build the bootc image (rootful, so bib sees it in the same store).
if [ "$SKIP_BIB" -eq 1 ]; then
  :
elif [ "$SKIP_BUILD" -eq 0 ]; then
  # Here rather than at the top: --write-only and --skip-build both reach this
  # point without minting anything, and a gate that refused to write a disk
  # because a recipe had a finding would be gating the wrong act. build-image.sh
  # builds disk images for this machine and reads no ARCH, so the gate always
  # sees the host's own architecture and runs in full.
  preflight_gate "$HOST" "$HOST_ARCH" "$NO_PREFLIGHT"
  echo "==> Building $IMAGE (rootful)"
  # --jobs 2 to overlap the two stages; see the same call in build/build-iso.sh
  # for why two and not more.
  stamp_build_args BUILD_ARGS
  priv podman build --jobs 2 "${BUILD_ARGS[@]}" -t "$IMAGE" "$REPO_ROOT"
else
  echo "==> Skipping image build; reusing $IMAGE"
  privc podman image exists "$IMAGE" || die "$IMAGE not in the root store; drop --skip-build"
fi

if [ "$SKIP_BIB" -eq 0 ]; then
  # 2. Make sure the image builder is present.
  privc podman image exists "$BIB_IMAGE" || {
    echo "==> Pulling $BIB_IMAGE"
    priv podman pull "$BIB_IMAGE"
  }

  # 3. Build the artifact. The def is mounted read-only into bib's defs
  #    directory under the name it looks up ($DEF_NAME); librepo (the default)
  #    resolves repos from the image.
  echo "==> Building --type $TYPE into $OUTPUT"

  # bib wants a clean output directory, but clearing it up front means a
  # build that fails half way has already destroyed the artifact that was
  # working. That happened on 2026-07-19: the previous ISO was gone before
  # anyone thought to keep it. Rotate one generation aside instead of
  # deleting, so a failed build leaves something to fall back to.
  if [ -d "$OUTPUT" ] && [ -n "$(privc ls -A "$OUTPUT" 2>/dev/null)" ]; then
    echo "==> Rotating previous output to ${OUTPUT}.prev"
    privc rm -rf "${OUTPUT:?}.prev"
    privc mv "$OUTPUT" "${OUTPUT}.prev"
  fi
  mkdir -p "$OUTPUT"

  priv podman run --rm --privileged \
    --security-opt label=type:unconfined_t \
    -v /var/lib/containers/storage:/var/lib/containers/storage \
    -v "$OUTPUT":/output \
    -v "$DEF":"/usr/share/bootc-image-builder/defs/$DEF_NAME":ro \
    "$BIB_IMAGE" \
    --type "$TYPE" \
    --log-level info \
    "$IMAGE"
fi

# 4. Locate the produced artifact.
case "$TYPE" in
  raw)          ARTIFACT="$OUTPUT/image/disk.raw" ;;
  qcow2)        ARTIFACT="$OUTPUT/qcow2/disk.qcow2" ;;
  *)            ARTIFACT="$(privc find "$OUTPUT" -type f ! -name '*.json' | head -1)" ;;
esac
[ -n "$ARTIFACT" ] && privc test -f "$ARTIFACT" || die "expected artifact not found for type $TYPE"
echo "==> Built: $ARTIFACT ($(privc du -h "$ARTIFACT" | cut -f1))"

# 5. Optionally write to a device. The guards, the confirmation and the
#    verify live in build/write-device.sh, which build/build-iso.sh calls
#    too — one implementation of the dd path, per wiki alloy-distribution.
if [ -n "$WRITE_DEV" ]; then
  "$REPO_ROOT/build/write-device.sh" "$ARTIFACT" "$WRITE_DEV" "$TYPE"
else
  echo "==> To write it to a USB stick:"
  echo "    $PRIV_NAME dd if=$ARTIFACT of=/dev/sdX bs=4M oflag=direct conv=fsync status=progress"
  echo "    (or re-run with --write /dev/sdX to rebuild and write in one step)"
fi
