#!/usr/bin/env bash # Read-only checks that a builder home (and optionally a staged image) has # everything build-local.sh needs before mkarchiso. Exit non-zero on failure. # # Builder home stays user-layout (~/.local). The image is system-prefix # /usr/local; pass SKEL and/or AIROOTFS to check those destinations. # # LAPTOP_HOME=/build-home ./scripts/ci-verify-bake.sh # SKEL=/tmp/bos-iso-stage/airootfs/etc/skel ./scripts/ci-verify-bake.sh # AIROOTFS=/tmp/bos-iso-stage/airootfs ./scripts/ci-verify-bake.sh set -euo pipefail REPO="$(cd "$(dirname "$0")/.." && pwd)" LOCKFILE="${LOCKFILE:-$REPO/iso/bread-lockfile.toml}" LAPTOP_HOME="${LAPTOP_HOME:-/build-home}" SKEL="${SKEL:-}" AIROOTFS="${AIROOTFS:-}" pass=0 fail=0 ok() { printf ' PASS %s\n' "$1"; pass=$((pass + 1)); } bad() { printf ' FAIL %s\n' "$1" >&2; fail=$((fail + 1)); } if [[ ! -f "$LOCKFILE" ]]; then echo "ERROR: lockfile missing: $LOCKFILE" >&2 exit 1 fi eval "$(python3 - "$LOCKFILE" <<'PY' import sys, tomllib path = sys.argv[1] with open(path, "rb") as f: data = tomllib.load(f) required = data.get("required_bins") optional = data.get("optional_bins") or [] if required is None: required = data.get("bins") or data.get("binaries") or [] def emit(name, values): print(f"{name}=(") for v in values: print(f" {v!r}") print(")") emit("REQUIRED_BINS", required) emit("OPTIONAL_BINS", optional) PY )" echo "== lockfile $LOCKFILE ==" echo " ${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional" echo "== host tools ==" if command -v grub-install >/dev/null 2>&1; then ok "grub-install (uefi.grub bootmode)" else bad "grub-install missing — mkarchiso uefi.grub will abort (install grub on the builder)" fi echo "== builder home $LAPTOP_HOME ==" check_exec() { local path="$1" label="$2" if [[ -x "$path" && -f "$path" ]]; then ok "$label executable: $path" else bad "$label missing or not executable: $path" fi } check_dir() { local path="$1" label="$2" if [[ -d "$path" ]]; then ok "$label: $path" else bad "$label missing: $path" fi } check_file() { local path="$1" label="$2" if [[ -f "$path" ]]; then ok "$label: $path" else bad "$label missing: $path" fi } for b in "${REQUIRED_BINS[@]}"; do check_exec "$LAPTOP_HOME/.local/bin/$b" "required bin $b" done for b in "${OPTIONAL_BINS[@]}"; do if [[ -x "$LAPTOP_HOME/.local/bin/$b" ]]; then ok "optional bin $b present" else printf ' ---- optional bin %s not staged (ok until bread ships it)\n' "$b" fi done check_dir "$LAPTOP_HOME/.local/share/breadhelp/content" "breadhelp content" check_file "$LAPTOP_HOME/.cache/bakery/index.json" "bakery index cache" check_file "$LAPTOP_HOME/.local/state/bakery/installed.json" "bakery installed.json" mapfile -t UNITS < <(python3 - "$LAPTOP_HOME/.local/state/bakery/installed.json" <<'PY' import json, sys path = sys.argv[1] with open(path) as f: data = json.load(f) pkgs = data.get("packages", data) for pkg in pkgs.values(): for s in pkg.get("services", []): print(s["unit"] if isinstance(s, dict) else s) PY ) if [[ ${#UNITS[@]} -eq 0 ]]; then bad "installed.json lists no service units" else for unit in "${UNITS[@]}"; do [[ -n "$unit" ]] || continue check_file "$LAPTOP_HOME/.config/systemd/user/$unit" "unit $unit" done fi if [[ -n "$SKEL" && -z "$AIROOTFS" ]]; then if [[ -d "$SKEL/usr/local/bin" ]]; then AIROOTFS="$SKEL" SKEL="$AIROOTFS/etc/skel" elif [[ -d "$SKEL/../../usr/local" ]]; then AIROOTFS="$(cd "$SKEL/../.." && pwd)" fi elif [[ -n "$AIROOTFS" && -z "$SKEL" ]]; then SKEL="$AIROOTFS/etc/skel" fi if [[ -n "$AIROOTFS" || -n "$SKEL" ]]; then if [[ -n "$AIROOTFS" ]]; then echo "== staged image $AIROOTFS ==" check_file "$AIROOTFS/etc/bakery/config.toml" "bakery prefix config" if [[ -f "$AIROOTFS/etc/bakery/config.toml" ]] && grep -q 'prefix[[:space:]]*=[[:space:]]*"/usr/local"' "$AIROOTFS/etc/bakery/config.toml"; then ok "bakery prefix = /usr/local" else bad "bakery prefix is not /usr/local in $AIROOTFS/etc/bakery/config.toml" fi for b in "${REQUIRED_BINS[@]}"; do check_exec "$AIROOTFS/usr/local/bin/$b" "image required bin $b" done check_dir "$AIROOTFS/usr/local/share/breadhelp/content" "image breadhelp content" fi if [[ -n "$SKEL" ]]; then echo "== staged skel $SKEL ==" check_file "$SKEL/.cache/bakery/index.json" "skel bakery index cache" check_file "$SKEL/.local/state/bakery/installed.json" "skel bakery installed.json" for b in "${REQUIRED_BINS[@]}"; do if [[ -e "$SKEL/.local/bin/$b" ]]; then bad "skel still has bakery bin $b (belongs in /usr/local/bin)" fi done fi image_units_json="" if [[ -n "$SKEL" && -f "$SKEL/.local/state/bakery/installed.json" ]]; then image_units_json="$SKEL/.local/state/bakery/installed.json" fi if [[ -n "$image_units_json" ]]; then mapfile -t IMAGE_UNITS < <(python3 - "$image_units_json" <<'PY' import json, sys path = sys.argv[1] with open(path) as f: data = json.load(f) pkgs = data.get("packages", data) for pkg in pkgs.values(): for s in pkg.get("services", []): print(s["unit"] if isinstance(s, dict) else s) PY ) else IMAGE_UNITS=("${UNITS[@]}") fi if [[ -n "$AIROOTFS" ]]; then for unit in "${IMAGE_UNITS[@]}"; do [[ -n "$unit" ]] || continue check_file "$AIROOTFS/usr/lib/systemd/user/$unit" "image unit $unit" if [[ -f "$AIROOTFS/usr/lib/systemd/user/$unit" ]]; then if grep -q '^ExecStart=/usr/local/bin/' "$AIROOTFS/usr/lib/systemd/user/$unit"; then ok "image unit $unit ExecStart uses /usr/local/bin" elif grep -q '^ExecStart=' "$AIROOTFS/usr/lib/systemd/user/$unit"; then bad "image unit $unit ExecStart is not /usr/local/bin: $(grep '^ExecStart=' "$AIROOTFS/usr/lib/systemd/user/$unit")" fi fi done fi fi echo printf 'Result: %d passed, %d failed\n' "$pass" "$fail" [[ "$fail" -eq 0 ]]