iso v1.26.0: the PUBLIC release image — no answer file, interactive install, day-0 by .deb

Design inputs: SPIKE-universal-iso-{1,2,3,4}-2026-07-31.md. Every choice below is a measurement.

NEW: scripts/iso/pkg/ — the felhom-bootstrap .deb, built from committed source.
  Two files only (script + unit), NOT three: felhom-bootstrap.sh:91 reads /etc/felhom/bootstrap.env
  only 'if [[ -r ]]', and its defaults at :95-96 are EXACTLY what the pairing env set
  (build-felhom-iso.sh:257-258) — so shipping it would add a 0600 file to a public package to express
  values the script already defaults to. NO dependencies: the binaries it calls run at FIRST BOOT,
  not at postinst time, so SPIKE 4's open 'dpkg --configure -a' ordering question does not arise.
  The postinst is structurally incapable of failing (no 'set -e', every statement guarded, ends
  'exit 0'); build-deb.sh self-asserts G8/G9 and REFUSES to emit a package that violates them.

iso-repack.sh — two changes, both narrowing rather than deleting:
  - R-155 guard: now applies to FELHOM_MENU=single ONLY. It protected the single-entry mode's promise
    (one button labelled 'install' must not drop into a disk-picker); a release image carries no
    auto-installer-mode.toml BY DESIGN (gate G1), so refusing it would be the guard firing on the
    shape it describes rather than the one it prevents.
  - the menu collapse now has a release mode: two INTERACTIVE entries, Graphical default, timeout 15.
    Entry-count and banned-token gates are per-mode; the six-token list is UNCHANGED for single mode.
  - .deb injection into /proxmox/packages/, with a skip-list collision check (a colliding name would
    be dropped silently — the inert-payload class) and a post-remaster assertion that it landed in
    final.iso, not merely in the extract tree.

build-felhom-iso.sh — --release: no profile, no root hash, no answer.toml, no prepare-iso at all.
  Skipping prepare-iso is what removes the Automated entry by construction, since the stock grub.cfg
  emits it only inside 'if [ -f auto-installer-mode.toml ]'.

R-128 RULING — FIXED, by correcting the claim rather than inventing an assertion for it. The comment
  said ISO_VERSION 'aligns with SCRIPT_VERSION'; nothing evaluated it and the two had drifted. The
  coupling does not exist: the ISO is frozen, felhom-host-install.sh is fetched at run time from main
  (R-94/R-110), so an assertion would invent a constraint. Comment corrected, ISO_VERSION -> 1.26.0.

Release gate G6 AMENDED before the build, with its reasoning recorded in the runbook: the six-token
  ban existed to keep users away from the manual installer, which the ruling makes the product.
  'proxtui' (the TUI installer we ship) and 'nomodeset' (its graphics fallback) are dropped for
  release images; proxdebug/Rescue Boot/memtest/fwsetup stay banned in both modes.
This commit is contained in:
2026-07-31 16:39:47 +02:00
parent e787391c0a
commit 01a8155c5a
7 changed files with 439 additions and 38 deletions
+116 -22
View File
@@ -15,6 +15,12 @@
# Env:
# FELHOM_LOADER = shim | mkimage (default shim) — mkimage swaps BOOTX64.EFI (F1 firmware fix)
# FELHOM_BRAND = 1 | 0 (default 1) — 0 leaves the stock PVE menu completely alone
# FELHOM_MENU = single | release (default single) — single: ONE automated entry (appliance ISO,
# requires a prepared auto-install ISO). release: TWO INTERACTIVE
# entries for the PUBLIC image, which carries no answer.toml.
# FELHOM_DEB = <path> (optional) — a .deb to inject into /proxmox/packages/, which
# the PVE installer unpacks into the target on EVERY install path
# including the interactive one (Install.pm:1343-1372, :1378).
#
# Expects in /work (placed by build-felhom-iso.sh) when FELHOM_BRAND=1:
# brand/grub.cfg.tmpl, brand/felhom-theme.txt, brand/generate-grub-background.sh, brand/card.png
@@ -39,6 +45,10 @@ ORIG_CFG=/work/grub.cfg.orig
LOADER="${FELHOM_LOADER:-shim}"
BRAND="${FELHOM_BRAND:-1}"
MENU="${FELHOM_MENU:-single}"
DEB="${FELHOM_DEB:-}"
[[ "$MENU" == "single" || "$MENU" == "release" ]] \
|| { echo "iso-repack: FELHOM_MENU must be single|release (got '$MENU')" >&2; exit 2; }
say() { echo "iso-repack: $*"; }
@@ -95,15 +105,27 @@ cp "$GCFG" "$ORIG_CFG"
if [[ "$BRAND" == "1" ]]; then
say "branding GRUB (background + single-entry menu)"
# 2a. The prepared ISO MUST be an auto-install ISO. Our one menu entry boots the automated
# installer, which reads this file; without it the entry would drop into the manual installer
# — the exact outcome the single-entry menu exists to prevent. Fail loudly, never silently.
AIM="$(find "$EX" -maxdepth 2 -iname 'auto-installer-mode.toml' | head -1)"
[[ -n "$AIM" ]] || {
echo "iso-repack: auto-installer-mode.toml not found in the ISO — this is not a prepared" >&2
echo " auto-install ISO, so the single Felhom entry would boot the MANUAL installer. Refusing." >&2
exit 10
}
# 2a. R-155 — the guard, NARROWED rather than removed.
#
# What it protected: in SINGLE-entry mode the menu shows exactly one item, "Felhom telepítés",
# and that item boots the AUTOMATED installer, which reads auto-installer-mode.toml. Without
# that file the very same label would drop the user into the manual disk-picker — a button
# promising an unattended install that silently does the opposite. That promise is real and
# the guard still enforces it, unchanged, for FELHOM_MENU=single.
#
# Why it must not apply to FELHOM_MENU=release: the public image deliberately offers the
# interactive installer (see grub-release.cfg.tmpl for the measurements behind that ruling),
# so it carries NO answer.toml and NO auto-installer-mode.toml by design — that absence is a
# release-gate criterion (G1), not a defect. Refusing it would be the guard firing on the
# shape it was written to describe rather than the shape it was written to prevent.
if [[ "$MENU" == "single" ]]; then
AIM="$(find "$EX" -maxdepth 2 -iname 'auto-installer-mode.toml' | head -1)"
[[ -n "$AIM" ]] || {
echo "iso-repack: auto-installer-mode.toml not found in the ISO — this is not a prepared" >&2
echo " auto-install ISO, so the single Felhom entry would boot the MANUAL installer. Refusing." >&2
exit 10
}
fi
# 2b. Lift the kernel + initrd lines VERBATIM from the stock 'Install Proxmox VE (Automated)'
# entry, so a PVE bump that changes the kernel path or append line tracks automatically.
@@ -118,14 +140,36 @@ if [[ "$BRAND" == "1" ]]; then
inblk && /^[[:space:]]*}/ { inblk=0 }
' "$ORIG_CFG")"
[[ -n "$LINUX_LINE" ]] || { echo "iso-repack: could not lift the 'linux' line from the stock automated entry" >&2; exit 11; }
# release mode lifts the GRAPHICAL and TERMINAL-UI kernel lines instead of the automated one.
LINUX_GFX="$(awk '
/menuentry .Install Proxmox VE \(Graphical\)./ { inblk=1; next }
inblk && /^[[:space:]]*linux[[:space:]]/ { print; exit }
inblk && /^[[:space:]]*}/ { inblk=0 }
' "$ORIG_CFG")"
LINUX_TUI="$(awk '
/menuentry .Install Proxmox VE \(Terminal UI\)./ { inblk=1; next }
inblk && /^[[:space:]]*linux[[:space:]]/ { print; exit }
inblk && /^[[:space:]]*}/ { inblk=0 }
' "$ORIG_CFG")"
if [[ "$MENU" == "release" ]]; then
[[ -n "$LINUX_GFX" ]] || { echo "iso-repack: could not lift the Graphical 'linux' line" >&2; exit 11; }
[[ -n "$LINUX_TUI" ]] || { echo "iso-repack: could not lift the Terminal-UI 'linux' line" >&2; exit 11; }
# The release menu must NOT carry the unattended flag — that is the whole point of the ruling.
grep -q 'proxmox-start-auto-installer' <<<"$LINUX_GFX$LINUX_TUI" && {
echo "iso-repack: a release menu kernel line carries proxmox-start-auto-installer — refusing" >&2
exit 12
}
fi
[[ -n "$LINUX_LINE" || "$MENU" == "release" ]] || { echo "iso-repack: could not lift the 'linux' line from the stock automated entry" >&2; exit 11; }
[[ -n "$INITRD_LINE" ]] || { echo "iso-repack: could not lift the 'initrd' line from the stock automated entry" >&2; exit 11; }
# The append flag that MAKES it unattended. If PVE ever renames it, we must not ship an ISO that
# boots a manual installer behind a button labelled "Felhom telepítés".
grep -q 'proxmox-start-auto-installer' <<<"$LINUX_LINE" || {
echo "iso-repack: the lifted kernel line has no 'proxmox-start-auto-installer' flag:" >&2
echo " $LINUX_LINE" >&2; exit 12
}
if [[ "$MENU" == "single" ]]; then
grep -q 'proxmox-start-auto-installer' <<<"$LINUX_LINE" || {
echo "iso-repack: the lifted kernel line has no 'proxmox-start-auto-installer' flag:" >&2
echo " $LINUX_LINE" >&2; exit 12
}
fi
grep -q '/boot/initrd.img' <<<"$INITRD_LINE" || {
echo "iso-repack: the lifted initrd line looks wrong: $INITRD_LINE" >&2; exit 12
}
@@ -143,21 +187,39 @@ if [[ "$BRAND" == "1" ]]; then
# 2d. Render the new grub.cfg. Use awk (not sed) so the lifted lines are inserted literally —
# the append line is full of `/` and `=` that sed would need escaped.
awk -v lx="$LINUX_NORM" -v ird="$INITRD_NORM" '
{ gsub(/@@LINUX@@/, lx); gsub(/@@INITRD@@/, ird); print }
' "$BRANDDIR/grub.cfg.tmpl" > "$GCFG"
grep -q '@@LINUX@@\|@@INITRD@@' "$GCFG" && { echo "iso-repack: grub.cfg still has unfilled markers" >&2; exit 13; }
if [[ "$MENU" == "release" ]]; then
GFX_NORM=" $(sed -E 's/^[[:space:]]+//' <<<"$LINUX_GFX")"
TUI_NORM=" $(sed -E 's/^[[:space:]]+//' <<<"$LINUX_TUI")"
awk -v gfx="$GFX_NORM" -v tui="$TUI_NORM" -v ird="$INITRD_NORM" '
{ gsub(/@@LINUX_GFX@@/, gfx); gsub(/@@LINUX_TUI@@/, tui); gsub(/@@INITRD@@/, ird); print }
' "$BRANDDIR/grub-release.cfg.tmpl" > "$GCFG"
else
awk -v lx="$LINUX_NORM" -v ird="$INITRD_NORM" '
{ gsub(/@@LINUX@@/, lx); gsub(/@@INITRD@@/, ird); print }
' "$BRANDDIR/grub.cfg.tmpl" > "$GCFG"
fi
grep -q '@@LINUX@@\|@@LINUX_GFX@@\|@@LINUX_TUI@@\|@@INITRD@@' "$GCFG" && { echo "iso-repack: grub.cfg still has unfilled markers" >&2; exit 13; }
# 2e. GATES — the safety half is the whole point, so assert it on the rendered file rather than
# trusting the template. Exactly one entry, zero submenus, no path back to a manual installer.
N_ENTRY="$(grep -c '^[[:space:]]*menuentry ' "$GCFG" || true)"
N_SUB="$(grep -c '^[[:space:]]*submenu ' "$GCFG" || true)"
[[ "$N_ENTRY" == "1" ]] || { echo "iso-repack: rendered grub.cfg has $N_ENTRY menuentries, want exactly 1" >&2; exit 14; }
WANT_ENTRIES=1; [[ "$MENU" == "release" ]] && WANT_ENTRIES=2
[[ "$N_ENTRY" == "$WANT_ENTRIES" ]] || { echo "iso-repack: rendered grub.cfg has $N_ENTRY menuentries, want exactly $WANT_ENTRIES ($MENU mode)" >&2; exit 14; }
[[ "$N_SUB" == "0" ]] || { echo "iso-repack: rendered grub.cfg has $N_SUB submenus, want 0" >&2; exit 14; }
# Strip comments first: the template's header EXPLAINS which stock entries were dropped, and
# naming them there must not trip the gate. What matters is that no live directive uses them.
LIVE="$(grep -v '^[[:space:]]*#' "$GCFG")"
for banned in proxtui proxdebug nomodeset 'Rescue Boot' memtest fwsetup; do
# The banned set differs by mode, and the difference is the whole ruling (release-gate G6 amendment):
# single — six tokens. The one entry promises an unattended install, so ANY route to the manual
# installer breaks that promise. Unchanged.
# release — four tokens. The manual installer IS the product here, so `proxtui` (the Terminal-UI
# installer, one of the two entries we ship) and `nomodeset` (a graphics fallback for
# the same installer) are legitimate. What stays banned is what never installs anything:
# a debug shell, a rescue boot of an existing system, memtest and firmware settings.
BANNED=(proxtui proxdebug nomodeset 'Rescue Boot' memtest fwsetup)
[[ "$MENU" == "release" ]] && BANNED=(proxdebug 'Rescue Boot' memtest fwsetup)
for banned in "${BANNED[@]}"; do
if grep -q "$banned" <<<"$LIVE"; then
echo "iso-repack: rendered grub.cfg still has a live reference to '$banned'" >&2; exit 14
fi
@@ -267,6 +329,30 @@ CFG
say "swapped bootx64.efi in the ISO tree ($TREE_HITS) and inside $(basename "$EFIIMG")"
fi
# --- 5b. INJECT the Felhom package into /proxmox/packages/ (release images).
#
# Install.pm:1343-1372 unpacks EVERY .deb in that directory into the target, with a fixed
# skip-list of known package-name patterns, then :1378 runs `dpkg --configure -a` which executes
# the postinsts. This happens on EVERY install path — measured on the interactive one in
# SPIKE-universal-iso-4 (package installed, postinst run, unit enabled, unit fired at 7.98 s
# uptime, with proxmox-first-boot absent on the same machine). It is the ONLY delivery mechanism
# that survives an interactive install; the answer file's [first-boot] hook does not.
if [[ -n "$DEB" ]]; then
[[ -f "$DEB" ]] || { echo "iso-repack: FELHOM_DEB not found: $DEB" >&2; exit 16; }
PKGDIR="$EX/proxmox/packages"
[[ -d "$PKGDIR" ]] || { echo "iso-repack: $PKGDIR missing — not a PVE ISO?" >&2; exit 16; }
cp "$DEB" "$PKGDIR/$(basename "$DEB")"
say "injected $(basename "$DEB") into /proxmox/packages/ ($(ls "$PKGDIR"/*.deb | wc -l) debs total)"
# The skip-list at Install.pm:1352-1362 matches known package-name prefixes. A Felhom package must
# not collide with one, or it would be silently skipped on some hardware and the whole delivery
# would fail invisibly — exactly the inert-payload class this project keeps hitting.
case "$(basename "$DEB")" in
grub-pc_*|grub-efi-*|proxmox-grub*|proxmox-secure-boot-support_*|proxmox-first-boot*|amd64-microcode_*|intel-microcode_*)
echo "iso-repack: package name collides with the installer's skip-list — it would be dropped" >&2
exit 16 ;;
esac
fi
# --- 6. re-master from the (modified) tree, reproducing the source ISO's boot geometry from its OWN
# as_mkisofs report so we track PVE minor versions. We drop ONLY the Apple APM/HFS+ boot map
# (-hfsplus / -apm-block-size): re-emitting it trips xorriso 1.5.6's "Overlapping MBR partition
@@ -314,10 +400,18 @@ if [[ "$BRAND" == "1" ]]; then
xorriso -osirrox on -indev "$FINAL" -extract /boot/grub/grub.cfg "$VER/grub.cfg" >/dev/null 2>&1
xorriso -osirrox on -indev "$FINAL" -extract /boot/grub/felhomtheme "$VER/felhomtheme" >/dev/null 2>&1
N="$(grep -c '^[[:space:]]*menuentry ' "$VER/grub.cfg" 2>/dev/null || echo 0)"
[[ "$N" == "1" ]] || { echo "iso-repack: final.iso menu has $N entries, want 1" >&2; exit 15; }
[[ "$N" == "$WANT_ENTRIES" ]] || { echo "iso-repack: final.iso menu has $N entries, want $WANT_ENTRIES" >&2; exit 15; }
[[ -s "$VER/felhomtheme/background.png" ]] || { echo "iso-repack: final.iso carries no theme background" >&2; exit 15; }
grep -q "Felhom telepítés" "$VER/grub.cfg" || { echo "iso-repack: final.iso menu entry is not the Felhom one" >&2; exit 15; }
rm -rf "$VER"
say "verified in final.iso: 1 entry ('Felhom telepítés') + theme background present"
say "verified in final.iso: $N entr(y/ies) + theme background present"
fi
# --- 8b. and prove the PACKAGE landed in final.iso too (seam-wiring: assert on what ships, not on the
# tree we built it from). --------------------------------------------------------------------
if [[ -n "$DEB" ]]; then
B="$(basename "$DEB")"
xorriso -indev "$FINAL" -find /proxmox/packages -maxdepth 1 2>/dev/null | grep -q "$B" \
|| { echo "iso-repack: $B is NOT in final.iso/proxmox/packages — delivery would be inert" >&2; exit 17; }
say "verified in final.iso: $B present in /proxmox/packages/"
fi
say "done"