Files
felhom.eu/scripts/iso/build-felhom-iso.sh
T
admin 317037f8eb scripts v1.22.0 — ISO boot screen + single-entry GRUB menu (R-38 GRUB slice)
Two jobs, one repack pass.

BRANDING. Every ISO now carries a Felhom boot screen built from the website's
og-image_2.png at repack time (ImageMagick in the assistant container), so the
boot card has ONE source and not a second pre-rendered copy in the repo to
drift. The card is scaled onto a 1024x768 gfxterm canvas, top-centered, and the
card's own subtle background grid is continued across the letterbox fill
PHASE-LOCKED to where the card's grid lands — the fill is seamless rather than a
square of grid floating in flat navy. Menu positioning needs a gfxmenu theme
(plain background_image cannot move the menu off the wordmark), so the stock
pvetheme is replaced by felhomtheme, which puts the menu in the lower third the
layout deliberately leaves empty.

SAFETY — the half that matters. The stock PVE menu offers Graphical, Terminal
UI and serial installers plus an Advanced Options submenu (nomodeset x2, three
debug variants, Rescue Boot, memtest, UEFI settings). Every one of them reaches
the MANUAL installer, whose first question is which disk to wipe. A customer, or
their helpful nephew, must not be able to get there from a boot menu. They are
not hidden and not password-gated: they are NOT EMITTED. What ships is one
entry, 'Felhom telepítés', default, 5s.

Boot behavior is unchanged. The kernel/append and initrd lines are lifted
VERBATIM from the ISO's own 'Install Proxmox VE (Automated)' entry rather than
frozen into a copy here, so a PVE bump tracks automatically; the build fails if
they cannot be found, if the append line has lost proxmox-start-auto-installer,
or if auto-installer-mode.toml is absent (which would mean the one Felhom-
labelled entry boots a manual installer). The rendered menu is then gated for
exactly 1 entry, 0 submenus, and zero references to proxtui/proxdebug/nomodeset/
Rescue Boot/memtest/fwsetup — and re-verified by reading the menu back OUT of
the finished ISO, not merely out of the extract tree.

mkimage-surgery.sh -> iso-repack.sh: branding and the slice-B loader swap need
the same extract -> modify -> re-master cycle, so they share one pass instead of
re-mastering twice. The mkimage recipe is untouched. The embedded module list is
still derived from the STOCK grub.cfg (snapshotted before branding rewrites it),
plus gfxmenu's bitmap/bitmap_scale/trig renderer deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 08:45:45 +02:00

378 lines
23 KiB
Bash

#!/bin/bash
#===============================================================================
# build-felhom-iso.sh — R-21 slice A+B+C: turn the official PVE ISO into a Felhom auto-install ISO.
#
# SLICE C — --pairing builds the GENERIC, SECRET-FREE universal ISO: no customer-id / passphrase is
# baked in. The box registers itself at the hub as an UNCLAIMED APPLIANCE, the operator binds it to a
# customer, and the hub delivers the credentials ONCE — then the box completes day-0 exactly like a
# direct-mode box. Direct mode (--bootstrap-env, secret-bearing, operator-prepped) is unchanged.
#
# Renders answer.toml (from answer.toml.tmpl + a profile), mints a fresh THROWAWAY root hash,
# gates the answer through validate-answer by PARSING ITS OUTPUT (never $? — validate-answer returns
# exit 0 even on failure, spike S1 trap), renders the first-boot stub (injecting the bootstrap
# script/unit/env), and runs prepare-iso --fetch-from iso --on-first-boot. Emits the ISO + sha256 +
# a build manifest.
#
# R-38 GRUB SLICE (v1.22.0) — every ISO is now REPACKED after prepare-iso to carry the Felhom boot
# screen and, more importantly, a menu with exactly ONE entry ("Felhom telepítés", default, 5s). The
# stock PVE menu offers Graphical/Terminal-UI/serial installers plus an Advanced Options submenu with
# debug/nomodeset/rescue entries — every one of them a route into the MANUAL installer, whose first
# question is which disk to wipe. A customer, or their helpful nephew, must not be able to reach it
# from a boot menu; those entries are not hidden, they are not emitted. The kernel/append line is
# lifted verbatim from the ISO's own automated entry, so boot behavior is unchanged. See iso-repack.sh.
# (--no-brand exists for debugging the stock menu; it is not a shipping mode.)
#
# SLICE B — --loader shim|mkimage (default shim). shim = the stock output (MS-signed shim→GRUB chain,
# keeps Secure Boot working on compliant firmware, spike S2b). mkimage = replace the ISO's UEFI boot
# path with a monolithic grub-mkimage-built BOOTX64.EFI built from the ISO's OWN GRUB modules — the
# workaround PROVEN LIVE during the N100 run (VALIDATION-n100-baremetal F1: cheap AMI AN3PLUS-class
# firmware can't relocate the ISO's signed GRUB from USB, `relocation 0x0`). The mkimage loader is
# UNSIGNED → the target board MUST have Secure Boot OFF (documented in the n100 profile's prep). The
# loader surgery runs AFTER prepare-iso (the assistant's answer/first-boot payload is provably
# untouched except the loader path).
#
# SECRET-BEARING: if the bootstrap-env carries a retrieval passphrase (it must, for an unattended
# install — see README "secret-bearing"), the produced ISO embeds it. Supervised/single-use only;
# never distributed; delete after the run. The build log says so loudly.
#
# Runs on DooPlex; delegates validate-answer + prepare-iso + the mkimage surgery to the
# felhom-iso-assistant container (which carries proxmox-auto-install-assistant, xorriso, grub-mkimage,
# and mtools).
#===============================================================================
set -euo pipefail
ISO_VERSION="1.22.0" # Felhom release the ISO is tagged to (aligns with felhom-host-install SCRIPT_VERSION).
IMAGE="${FELHOM_ISO_ASSISTANT_IMAGE:-felhom-iso-assistant:trixie}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- logging (host-install idiom) -----------------------------------------------------------------
if [[ -t 1 ]]; then RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; BLUE=$'\033[0;34m'; CYAN=$'\033[0;36m'; NC=$'\033[0m'
else RED=""; GREEN=""; YELLOW=""; BLUE=""; CYAN=""; NC=""; fi
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_dry() { echo -e "${CYAN}[DRY-RUN]${NC} $1"; }
die() { log_error "$1"; exit 1; }
PVE_ISO=""; ISO_SHA256=""; PROFILE=""; BOOTSTRAP_ENV=""; OUT_DIR="${FELHOM_ISO_OUT:-/mnt/5_hdd/felhom.eu/felhom-iso/out}"; PVE_VERSION=""; DRY_RUN=false
LOADER_CLI="" # --loader override; empty = fall back to the profile, then the shim default.
PAIRING=false # --pairing: build the GENERIC secret-free ISO (slice C); no --bootstrap-env.
BRAND=true # R-38: Felhom boot screen + single-entry menu. --no-brand is a debugging escape only.
BRAND_IMAGE="" # --brand-image override; empty = the website's og-image_2.png (the single source).
usage() {
cat <<EOF
Usage (direct): build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --bootstrap-env FILE [options]
Usage (generic): build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --pairing [options]
Required:
--pve-iso PATH pre-downloaded official PVE ISO (not fetched here)
--iso-sha256 SHA expected sha256 of --pve-iso (verified before build; abort on mismatch)
--profile FILE build profile (fqdn + [disk-setup]); see profiles/ and README
Mode (exactly one):
--bootstrap-env FILE DIRECT mode: the in-ISO /etc/felhom/bootstrap.env (SECRET-BEARING: retrieval
passphrase). Must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE.
--pairing PAIRING mode (slice C): the GENERIC, SECRET-FREE universal ISO. The box registers
itself as an unclaimed appliance at the hub; the operator binds it; the hub
delivers the customer-id + passphrase ONCE. No customer secret is baked in. The
hub URL comes from the profile (FELHOM_HUB_URL) or the default.
Options:
--loader shim|mkimage UEFI boot loader (default: shim, or the profile's FELHOM_LOADER; --loader wins).
shim = stock MS-signed chain (Secure Boot OK on compliant firmware).
mkimage = monolithic grub-mkimage loader for cheap AMI boards that can't boot
the ISO's GRUB from USB (F1). UNSIGNED -> the target board needs Secure Boot OFF.
--no-brand DEBUG ONLY: keep the stock PVE boot menu (all installer entries reachable).
Shipping ISOs are always branded: Felhom boot screen + exactly ONE entry.
--brand-image FILE override the boot-screen card (default: website/assets/og-image_2.png)
--out DIR output directory (default: the DooPlex build root
/mnt/5_hdd/felhom.eu/felhom-iso/out; override via \$FELHOM_ISO_OUT for other hosts)
--pve-version VER override PVE version tag (default: parsed from the ISO filename)
--dry-run print the steps without producing an ISO
-h, --help this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--pve-iso) PVE_ISO="$2"; shift 2 ;;
--iso-sha256) ISO_SHA256="$2"; shift 2 ;;
--profile) PROFILE="$2"; shift 2 ;;
--bootstrap-env) BOOTSTRAP_ENV="$2"; shift 2 ;;
--pairing) PAIRING=true; shift ;;
--loader) LOADER_CLI="$2"; shift 2 ;;
--no-brand) BRAND=false; shift ;;
--brand-image) BRAND_IMAGE="$2"; shift 2 ;;
--out) OUT_DIR="$2"; shift 2 ;;
--pve-version) PVE_VERSION="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage; exit 0 ;;
*) die "unknown argument: $1 (see --help)" ;;
esac
done
[[ -z "$LOADER_CLI" || "$LOADER_CLI" == "shim" || "$LOADER_CLI" == "mkimage" ]] \
|| die "--loader must be 'shim' or 'mkimage' (got '$LOADER_CLI')"
[[ -n "$PVE_ISO" ]] || die "--pve-iso is required"
[[ -n "$ISO_SHA256" ]] || die "--iso-sha256 is required"
[[ -n "$PROFILE" ]] || die "--profile is required"
[[ -f "$PVE_ISO" ]] || die "--pve-iso not found: $PVE_ISO"
[[ -f "$PROFILE" ]] || die "--profile not found: $PROFILE"
# Mode: exactly one of --bootstrap-env (direct, secret-bearing) or --pairing (generic, secret-free).
if $PAIRING; then
[[ -z "$BOOTSTRAP_ENV" ]] || die "--pairing and --bootstrap-env are mutually exclusive"
else
[[ -n "$BOOTSTRAP_ENV" ]] || die "one of --bootstrap-env (direct) or --pairing (generic) is required"
[[ -f "$BOOTSTRAP_ENV" ]] || die "--bootstrap-env not found: $BOOTSTRAP_ENV"
fi
command -v docker >/dev/null || die "docker not found (needed for the assistant container)"
docker image inspect "$IMAGE" >/dev/null 2>&1 || die "assistant image '$IMAGE' not found — build it: docker build -f $HERE/Dockerfile.assistant -t $IMAGE $HERE"
# --- verify source ISO ----------------------------------------------------------------------------
log_step "verifying source ISO sha256"
actual_sha=$(sha256sum "$PVE_ISO" | awk '{print $1}')
[[ "$actual_sha" == "$ISO_SHA256" ]] || die "ISO sha256 MISMATCH: expected $ISO_SHA256, got $actual_sha"
log_success "source ISO sha256 OK ($actual_sha)"
if [[ -z "$PVE_VERSION" ]]; then
PVE_VERSION=$(basename "$PVE_ISO" | sed -E 's/^proxmox-ve_(.+)\.iso$/\1/')
[[ "$PVE_VERSION" != "$(basename "$PVE_ISO")" ]] || die "cannot parse PVE version from '$(basename "$PVE_ISO")' — pass --pve-version"
fi
PROFILE_NAME="$(basename "$PROFILE")"; PROFILE_NAME="${PROFILE_NAME%.profile}"
# --- load + validate profile ----------------------------------------------------------------------
log_step "loading profile: $PROFILE"
FELHOM_FQDN=""; FELHOM_DISK_SETUP=""; FELHOM_ROOT_SSH_KEY=""; FELHOM_LOADER=""; FELHOM_HUB_URL=""; FELHOM_INSTALL_URL=""
# shellcheck disable=SC1090
source "$PROFILE"
[[ -n "$FELHOM_FQDN" ]] || die "profile missing FELHOM_FQDN"
[[ -n "$FELHOM_DISK_SETUP" ]] || die "profile missing FELHOM_DISK_SETUP"
# Optional emergency/validation SSH key baked into the installed root account.
ROOT_SSH_LINE=""
[[ -n "$FELHOM_ROOT_SSH_KEY" ]] && ROOT_SSH_LINE="root-ssh-keys = [\"${FELHOM_ROOT_SSH_KEY}\"]"
# --- resolve the loader mode: --loader wins over the profile's FELHOM_LOADER over the shim default ---
LOADER="${LOADER_CLI:-${FELHOM_LOADER:-shim}}"
[[ "$LOADER" == "shim" || "$LOADER" == "mkimage" ]] \
|| die "profile FELHOM_LOADER must be 'shim' or 'mkimage' (got '$FELHOM_LOADER')"
if [[ "$LOADER" == "mkimage" ]]; then
echo -e "${YELLOW}==================================================================================${NC}"
log_warn "LOADER MODE = mkimage — the UEFI boot path is a monolithic grub-mkimage loader (F1 fix)."
log_warn "This loader is UNSIGNED: the target board MUST have Secure Boot OFF. shim/SB is bypassed."
log_warn "The ISO filename gains '-mkimage'; never confuse it with a shim (SB-capable) build."
echo -e "${YELLOW}==================================================================================${NC}"
else
log_info "loader mode = shim (stock MS-signed chain; Secure Boot works on compliant firmware)"
fi
# --- resolve the boot-screen card (R-38) ------------------------------------------------------------
# Single source of truth: the website asset. NOT copied into scripts/iso/ — a second copy of a brand
# asset is a second thing to forget to update.
if $BRAND; then
[[ -n "$BRAND_IMAGE" ]] || BRAND_IMAGE="$HERE/../../website/assets/og-image_2.png"
[[ -f "$BRAND_IMAGE" ]] || die "brand image not found: $BRAND_IMAGE (pass --brand-image, or run from the repo checkout)"
log_info "boot-screen card = $BRAND_IMAGE (menu: exactly ONE entry, 'Felhom telepítés')"
else
echo -e "${YELLOW}==================================================================================${NC}"
log_warn "--no-brand — the STOCK PVE boot menu ships: Graphical/Terminal-UI/serial installers and"
log_warn "the Advanced Options submenu are all reachable, i.e. a manual 'which disk do I wipe'"
log_warn "installer is one keypress away. DEBUGGING ONLY — never hand this ISO to a customer."
echo -e "${YELLOW}==================================================================================${NC}"
fi
# --- mode: DIRECT validates the secret-bearing env; PAIRING is secret-free (env generated below) -----
if $PAIRING; then
SECRET_BEARING="no"
PAIR_HUB_URL="${FELHOM_HUB_URL:-https://hub.felhom.eu}"
PAIR_INSTALL_URL="${FELHOM_INSTALL_URL:-https://felhom.eu/scripts/felhom-host-install.sh}"
echo -e "${YELLOW}==================================================================================${NC}"
log_info "PAIRING MODE — building the GENERIC, SECRET-FREE universal ISO (slice C)."
log_info "The box registers as an unclaimed appliance; the operator binds it; the hub delivers the"
log_info "customer-id + passphrase ONCE. Baked env carries only the hub URL ($PAIR_HUB_URL) — no secret."
echo -e "${YELLOW}==================================================================================${NC}"
else
log_step "checking bootstrap-env (secret-bearing detection)"
( set +e
FELHOM_CUSTOMER_ID=""; FELHOM_MODE=""; FELHOM_RETRIEVAL_PASSPHRASE=""
# shellcheck disable=SC1090
source "$BOOTSTRAP_ENV"
[[ -n "$FELHOM_CUSTOMER_ID" ]] || { echo "MISSING FELHOM_CUSTOMER_ID"; exit 3; }
[[ -n "$FELHOM_MODE" ]] || { echo "MISSING FELHOM_MODE"; exit 3; }
[[ -n "$FELHOM_RETRIEVAL_PASSPHRASE" ]] || { echo "MISSING FELHOM_RETRIEVAL_PASSPHRASE"; exit 3; }
) || die "bootstrap-env invalid ($BOOTSTRAP_ENV) — must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE"
SECRET_BEARING="yes" # a valid bootstrap-env always carries the retrieval passphrase
log_warn "this ISO will be SECRET-BEARING (embeds the customer retrieval passphrase) — supervised/single-use only"
fi
# --- workspace ------------------------------------------------------------------------------------
WORK="$(mktemp -d "${TMPDIR:-/tmp}/felhom-iso.XXXXXX")"
# chmod first: the mkimage surgery's osirrox extract leaves read-only file modes that rm can't clear.
cleanup() { chmod -R u+w "$WORK" 2>/dev/null || true; rm -rf "$WORK"; }
trap cleanup EXIT
mkdir -p "$OUT_DIR" "$WORK/tmp"
ISO_DIR="$(cd "$(dirname "$PVE_ISO")" && pwd)"; ISO_BASE="$(basename "$PVE_ISO")"
# PAIRING: generate the SECRET-FREE env the stub bakes — only the hub URL, no customer/passphrase.
# (The bootstrap detects the absent customer-id/passphrase and enters pairing mode.)
if $PAIRING; then
BOOTSTRAP_ENV="$WORK/pairing.env"
cat > "$BOOTSTRAP_ENV" <<EOF
# GENERIC secret-free pairing env (R-21 slice C). NO customer-id, NO passphrase — the box registers
# as an unclaimed appliance and the hub delivers the credentials once, after the operator binds it.
FELHOM_HUB_URL=$PAIR_HUB_URL
FELHOM_INSTALL_URL=$PAIR_INSTALL_URL
EOF
log_info "generated secret-free pairing env (hub=$PAIR_HUB_URL)"
fi
# --- mint fresh THROWAWAY root hash ---------------------------------------------------------------
log_step "minting fresh throwaway root password hash"
ROOT_PLAIN="felhom-throwaway-$(head -c12 /dev/urandom | base64 | tr -dc 'A-Za-z0-9')"
ROOT_HASH="$(openssl passwd -6 "$ROOT_PLAIN")"
unset ROOT_PLAIN
[[ -n "$ROOT_HASH" ]] || die "failed to mint root hash"
log_info "throwaway root hash written to the answer file (value stored out-of-band, not logged)"
# --- render answer.toml (pure bash param-expansion; no metachar hazards) ---------------------------
log_step "rendering answer.toml"
ANSWER="$WORK/answer.toml"
: > "$ANSWER"
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" == "__DISK_SETUP__" ]]; then
printf '%s\n' "$FELHOM_DISK_SETUP" >> "$ANSWER"
elif [[ "$line" == "__ROOT_SSH_KEYS__" ]]; then
[[ -n "$ROOT_SSH_LINE" ]] && printf '%s\n' "$ROOT_SSH_LINE" >> "$ANSWER" # blank -> omit line
else
line="${line//__FQDN__/$FELHOM_FQDN}"
line="${line//__ROOT_HASH__/$ROOT_HASH}"
printf '%s\n' "$line" >> "$ANSWER"
fi
done < "$HERE/answer.toml.tmpl"
# --- validate-answer OUTPUT-PARSE gate (never $? — spike S1) --------------------------------------
gate_validate_answer() {
local out
out=$(docker run --rm -v "$WORK":/work "$IMAGE" \
proxmox-auto-install-assistant validate-answer /work/answer.toml 2>&1) || true
echo "----- validate-answer output -----"; echo "$out"; echo "----------------------------------"
# LOAD-BEARING: validate-answer exits 0 even on failure; decide on the MESSAGE TEXT, not $?.
if echo "$out" | grep -q "parsed successfully" && ! echo "$out" | grep -qi "Found issues"; then
return 0
fi
return 1
}
log_step "validating rendered answer (output-parse gate)"
if $DRY_RUN; then
log_dry "docker run … validate-answer /work/answer.toml (output-parse gate)"
else
gate_validate_answer || die "answer validation FAILED — NO ISO produced (fix the answer/profile)"
log_success "answer validated"
fi
# --- render the first-boot stub (inject bootstrap script/unit/env as base64) ----------------------
log_step "rendering first-boot stub"
STUB="$WORK/stub-first-boot.sh"
sh_b64="$(base64 -w0 < "$HERE/felhom-bootstrap.sh")"
unit_b64="$(base64 -w0 < "$HERE/felhom-bootstrap.service")"
env_b64="$(base64 -w0 < "$BOOTSTRAP_ENV")"
awk -v sh="$sh_b64" -v unit="$unit_b64" -v env="$env_b64" '
{ gsub(/@@BOOTSTRAP_SH_B64@@/, sh); gsub(/@@BOOTSTRAP_UNIT_B64@@/, unit); gsub(/@@BOOTSTRAP_ENV_B64@@/, env); print }
' "$HERE/stub-first-boot.sh" > "$STUB"
chmod 0755 "$STUB"
grep -q '@@BOOTSTRAP_.*_B64@@' "$STUB" && die "stub still has unfilled markers — injection failed"
# --- prepare-iso ----------------------------------------------------------------------------------
# Rule 4: the loader mode is loud in the filename — a '-mkimage' ISO implies Secure-Boot-off prep.
LOADER_SUFFIX=""; [[ "$LOADER" != "shim" ]] && LOADER_SUFFIX="-${LOADER}"
MODE_SUFFIX=""; $PAIRING && MODE_SUFFIX="-generic" # the secret-free universal ISO is unmistakable
OUT_ISO="$OUT_DIR/felhom-pve-${PVE_VERSION}-v${ISO_VERSION}-${PROFILE_NAME}${MODE_SUFFIX}${LOADER_SUFFIX}.iso"
GRUB_VERSION="" # populated by the mkimage surgery (the grub-mkimage build used)
log_step "building ISO: $(basename "$OUT_ISO")"
if $DRY_RUN; then
log_dry "docker run … prepare-iso /iso/$ISO_BASE --fetch-from iso --answer-file /work/answer.toml --on-first-boot /work/stub-first-boot.sh --output /work/out.iso"
$BRAND && log_dry "docker run … (repack/brand) generate 1024x768 boot card from $(basename "$BRAND_IMAGE") → install felhomtheme → rewrite grub.cfg to ONE entry ('Felhom telepítés', 5s), kernel line lifted from the stock automated entry"
[[ "$LOADER" == "mkimage" ]] && log_dry "docker run … (repack/mkimage) grub-mkimage from the ISO's own modules → swap BOOTX64.EFI in the EFI tree + efi.img"
( $BRAND || [[ "$LOADER" == "mkimage" ]] ) && log_dry "docker run … (repack) xorriso re-master → /work/final.iso"
log_info "DRY-RUN: no ISO produced"
exit 0
fi
docker run --rm -v "$ISO_DIR":/iso:ro -v "$WORK":/work "$IMAGE" \
proxmox-auto-install-assistant prepare-iso "/iso/$ISO_BASE" \
--fetch-from iso --answer-file /work/answer.toml \
--on-first-boot /work/stub-first-boot.sh \
--tmp /work/tmp --output /work/out.iso
[[ -f "$WORK/out.iso" ]] || die "prepare-iso produced no output"
# --- REPACK: R-38 GRUB branding + single-entry menu, and (slice B) the mkimage loader surgery. Both
# need the same extract -> modify -> re-master cycle, so iso-repack.sh does them in ONE pass. The
# assistant's answer/first-boot payload is untouched; only the GRUB menu/theme and (mkimage) the
# EFI boot path change. The mkimage recipe is the N100 run's proven workaround (VALIDATION F1) —
# do NOT re-derive it. See iso-repack.sh for the full rationale. -----------------------------------
if $BRAND || [[ "$LOADER" == "mkimage" ]]; then
REPACK_WHAT=""
$BRAND && REPACK_WHAT="branding"
[[ "$LOADER" == "mkimage" ]] && REPACK_WHAT="${REPACK_WHAT:+$REPACK_WHAT + }mkimage loader"
log_step "repacking ISO ($REPACK_WHAT)"
[[ -f "$HERE/iso-repack.sh" ]] || die "iso-repack.sh not found next to build-felhom-iso.sh"
cp "$HERE/iso-repack.sh" "$WORK/iso-repack.sh"
if $BRAND; then
mkdir -p "$WORK/brand"
cp "$HERE/grub/grub.cfg.tmpl" "$HERE/grub/felhom-theme.txt" \
"$HERE/grub/generate-grub-background.sh" "$WORK/brand/"
cp "$BRAND_IMAGE" "$WORK/brand/card.png"
fi
docker run --rm -v "$WORK":/work \
-e FELHOM_LOADER="$LOADER" -e FELHOM_BRAND="$($BRAND && echo 1 || echo 0)" \
"$IMAGE" bash /work/iso-repack.sh 2>&1 | sed 's/^/ [repack] /'
[[ -f "$WORK/final.iso" ]] || die "repack produced no output (see [repack] log above)"
GRUB_VERSION="$(cat "$WORK/grub-version.txt" 2>/dev/null || echo unknown)"
cp "$WORK/final.iso" "$OUT_ISO"
log_success "repack applied ($REPACK_WHAT)"
else
cp "$WORK/out.iso" "$OUT_ISO"
fi
# --- sha256 + manifest ----------------------------------------------------------------------------
OUT_SHA="$(sha256sum "$OUT_ISO" | awk '{print $1}')"
OUT_SIZE="$(stat -c '%s' "$OUT_ISO")"
ASSISTANT_VER="$(docker run --rm "$IMAGE" proxmox-auto-install-assistant --version 2>&1 | head -1)"
echo "$OUT_SHA $(basename "$OUT_ISO")" > "$OUT_ISO.sha256"
LOADER_NOTE="shim (stock MS-signed chain; Secure Boot OK on compliant firmware)"
[[ "$LOADER" == "mkimage" ]] && LOADER_NOTE="mkimage (monolithic grub-mkimage UEFI loader, F1 fix — UNSIGNED; target board MUST have Secure Boot OFF)"
BRAND_NOTE="FELHOM (single entry 'Felhom telepítés', default, 5s; stock installer entries not emitted)"
$BRAND || BRAND_NOTE="STOCK PVE menu (--no-brand; DEBUG BUILD — manual installer reachable, do not ship)"
MODE_NOTE="direct (env-baked customer-id + retrieval passphrase; secret-bearing)"
$PAIRING && MODE_NOTE="pairing (GENERIC secret-free universal ISO — box self-registers, operator binds, hub delivers once)"
cat > "$OUT_ISO.manifest.txt" <<EOF
Felhom bare-metal ISO build manifest (R-21 slice A+B+C)
built : $(date -Is)
iso-version-tag : v${ISO_VERSION}
pve-version : ${PVE_VERSION}
source-iso : ${ISO_BASE}
source-iso-sha256 : ${ISO_SHA256}
assistant-version : ${ASSISTANT_VER}
profile : ${PROFILE_NAME}
fqdn : ${FELHOM_FQDN}
mode : ${MODE_NOTE}
loader : ${LOADER_NOTE}
grub-mkimage : ${GRUB_VERSION:-n/a (shim mode; loader unchanged)}
boot-menu : ${BRAND_NOTE}
$( $BRAND && cat "$WORK/brand-report.txt" 2>/dev/null || true )
host-install-url : $(grep -oE 'FELHOM_INSTALL_URL=[^ ]*' "$BOOTSTRAP_ENV" 2>/dev/null || echo 'https://felhom.eu/scripts/felhom-host-install.sh (default)')
secret-bearing : ${SECRET_BEARING}$( $PAIRING && echo ' (GENERIC ISO — carries NO customer secret)' || echo ' (embeds the customer retrieval passphrase — supervised/single-use, delete after the run)')
output : $(basename "$OUT_ISO")
output-sha256 : ${OUT_SHA}
output-size-bytes : ${OUT_SIZE}
EOF
log_success "ISO built: $OUT_ISO"
log_info "sha256 : $OUT_SHA"
log_info "size : $OUT_SIZE bytes"
log_info "manifest : $OUT_ISO.manifest.txt"
if $PAIRING; then
log_success "GENERIC secret-free ISO — carries NO customer secret. Bind the box on the hub after it registers."
else
log_warn "SECRET-BEARING ISO (embeds the retrieval passphrase). Supervised/single-use; never distribute; delete after the run."
fi