diff --git a/scripts/iso/Dockerfile.assistant b/scripts/iso/Dockerfile.assistant index 656b49e..bacd4df 100644 --- a/scripts/iso/Dockerfile.assistant +++ b/scripts/iso/Dockerfile.assistant @@ -12,10 +12,13 @@ FROM debian:trixie # xorriso: ISO (re-)mastering. shellcheck: lint. grub-mkimage (+ x86_64-efi module dir) and mtools: # the slice-B mkimage loader surgery — build a monolithic UEFI loader from the ISO's own GRUB modules # and inject it into the efi.img ESP (FAT, via mtools). grub 2.12 on trixie == the PVE 9.x ISO's -# 2.12-9+pmx2 generation, so the tool matches the modules it embeds. +# 2.12-9+pmx2 generation, so the tool matches the modules it embeds. imagemagick: the R-38 GRUB boot +# card — grub/generate-grub-background.sh letterboxes the website's brand asset onto a 1024x768 +# gfxterm canvas at repack time, so the boot screen has ONE source (the website asset) and not a +# second pre-rendered copy checked into the repo to drift. RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates wget gnupg xorriso shellcheck \ - grub-common grub-efi-amd64-bin mtools dosfstools \ + grub-common grub-efi-amd64-bin mtools dosfstools imagemagick \ && rm -rf /var/lib/apt/lists/* # PVE 9.x (trixie) no-subscription repo — pairs the assistant to the 9.2 ISO generation. diff --git a/scripts/iso/README.md b/scripts/iso/README.md index 83257f0..c80a9fa 100644 --- a/scripts/iso/README.md +++ b/scripts/iso/README.md @@ -19,7 +19,10 @@ channel). |---|---| | `Dockerfile.assistant` | build host: Debian trixie + `proxmox-auto-install-assistant` (paired to the ISO's PVE generation by Debian codename) + `xorriso` + `shellcheck` | | `build-felhom-iso.sh` | the pipeline: render answer, mint throwaway root hash, **validate-answer output-parse gate**, render stub, `prepare-iso`, **optional `--loader mkimage` surgery**, emit ISO + sha256 + manifest | -| `mkimage-surgery.sh` | slice-B loader surgery (runs in the container, post-`prepare-iso`): builds a monolithic `grub-mkimage` UEFI loader and swaps it into the ISO's EFI boot path (F1 firmware fix) | +| `iso-repack.sh` | the post-`prepare-iso` repack (runs in the container). **v1.22.0/R-38:** Felhom GRUB branding + **single-entry menu surgery**; plus, in `--loader mkimage`, the slice-B monolithic `grub-mkimage` UEFI loader swap (F1 firmware fix). One extract→modify→re-master pass for both. *(was `mkimage-surgery.sh` ≤ v1.21.0)* | +| `grub/grub.cfg.tmpl` | the replacement GRUB menu: exactly **one** entry („Felhom telepítés", default, 5 s). The `linux`/`initrd` lines are lifted verbatim at repack time from the ISO's own *Install Proxmox VE (Automated)* entry, so a PVE bump tracks automatically | +| `grub/felhom-theme.txt` | gfxmenu theme — positions the menu in the clean lower third of the boot card (plain `background_image` cannot position the menu, so the text would land on the wordmark) | +| `grub/generate-grub-background.sh` | letterboxes `website/assets/og-image_2.png` onto a 1024×768 gfxterm canvas, phase-locking a continuation of the card's own background grid across the fill | | `answer.toml.tmpl` | Proxmox answer template (`__FQDN__` / `__ROOT_HASH__` / `__ROOT_SSH_KEYS__` / `__DISK_SETUP__` placeholders; `[first-boot]` from-iso/fully-up) | | `profiles/*.profile` | disk-selection + fqdn fragments (sourceable, no secret) | | `stub-first-boot.sh` | the ONE first-boot executable (skeleton; build injects the bootstrap script/unit/env as base64) | diff --git a/scripts/iso/build-felhom-iso.sh b/scripts/iso/build-felhom-iso.sh index 3085626..8d688d5 100644 --- a/scripts/iso/build-felhom-iso.sh +++ b/scripts/iso/build-felhom-iso.sh @@ -13,6 +13,15 @@ # 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 @@ -32,7 +41,7 @@ #=============================================================================== set -euo pipefail -ISO_VERSION="1.21.0" # Felhom release the ISO is tagged to (aligns with felhom-host-install SCRIPT_VERSION). +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)" @@ -50,6 +59,8 @@ 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 < 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) @@ -87,6 +101,8 @@ while [[ $# -gt 0 ]]; do --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 ;; @@ -150,6 +166,21 @@ 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" @@ -260,7 +291,9 @@ GRUB_VERSION="" # populated by the mkimage surgery (the grub-mkimage build use 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" - [[ "$LOADER" == "mkimage" ]] && log_dry "docker run … (mkimage surgery) grub-mkimage from the ISO's own modules → swap BOOTX64.EFI in the EFI tree + efi.img → xorriso re-master → /work/final.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 @@ -271,21 +304,31 @@ docker run --rm -v "$ISO_DIR":/iso:ro -v "$WORK":/work "$IMAGE" \ --tmp /work/tmp --output /work/out.iso [[ -f "$WORK/out.iso" ]] || die "prepare-iso produced no output" -# --- SLICE B: mkimage loader surgery (post-prepare; the assistant payload is untouched but the EFI -# boot path). The recipe is the N100 run's proven workaround (VALIDATION F1) — do NOT re-derive it: -# grub-mkimage from the ISO's OWN x86_64-efi modules (2.12-9+pmx2), embedding the module set the -# ISO's grub.cfg needs + an embedded config that `search --fs-uuid`es the ISO and `configfile`s its -# real menu; then swap BOOTX64.EFI in the ISO9660 EFI/BOOT tree AND inside efi.img, and re-master -# with xorriso preserving BOTH the hybrid BIOS boot and the injected answer/first-boot payload. --- -if [[ "$LOADER" == "mkimage" ]]; then - log_step "applying mkimage UEFI loader (F1 firmware fix; recipe from the N100 run evidence)" - [[ -f "$HERE/mkimage-surgery.sh" ]] || die "mkimage-surgery.sh not found next to build-felhom-iso.sh" - cp "$HERE/mkimage-surgery.sh" "$WORK/mkimage-surgery.sh" - docker run --rm -v "$WORK":/work "$IMAGE" bash /work/mkimage-surgery.sh 2>&1 | sed 's/^/ [surgery] /' - [[ -f "$WORK/final.iso" ]] || die "mkimage surgery produced no output (see [surgery] log above)" +# --- 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 "mkimage loader applied (grub-mkimage: ${GRUB_VERSION})" + log_success "repack applied ($REPACK_WHAT)" else cp "$WORK/out.iso" "$OUT_ISO" fi @@ -297,6 +340,8 @@ ASSISTANT_VER="$(docker run --rm "$IMAGE" proxmox-auto-install-assistant --versi 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" </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") diff --git a/scripts/iso/grub/felhom-theme.txt b/scripts/iso/grub/felhom-theme.txt new file mode 100644 index 0000000..f7fa859 --- /dev/null +++ b/scripts/iso/grub/felhom-theme.txt @@ -0,0 +1,46 @@ +# Felhom ISO — GRUB gfxmenu theme (scripts v1.22.0, R-38 GRUB slice). +# +# WHY A THEME AND NOT `background_image`: plain gfxterm draws the menu in the terminal's own region +# (top-left), which on our card lands the text straight across the cloud/wordmark. A gfxmenu theme is +# the only way to POSITION the menu, so the menu sits in the clean lower third that +# generate-grub-background.sh deliberately leaves empty (the card is composited into the TOP 516px of +# a 1024x768 canvas). Keep this file and the background generator's CARD/CARD_Y constants in sync — +# if the card grows, `boot_menu.top` must move down with it. +# +# Colors are design-system v2: --blue #0083D8 for the selected item, light text on the near-black +# navy (#0D1117) the card itself uses. + +title-text: "" + +desktop-image: "background.png" +desktop-color: "#0D1117" + +# The single entry ("Felhom telepítés"). Below the card, horizontally centered. ++ boot_menu { + left = 25% + top = 70% + width = 50% + height = 14% + + item_color = "#E6EDF3" + selected_item_color = "#0083D8" + item_height = 32 + item_padding = 0 + item_icon_space = 0 + item_spacing = 2 +} + +# Honest countdown — the customer sees that something IS about to happen, not a frozen screen. +# NOTE: no "e: edit entry / esc: back" hint label (the stock PVE theme has one). Advertising the +# editor works against the point of the single-entry menu. ++ label { + id = "__timeout__" + text = "Indítás %d másodperc múlva..." + left = 25% + top = 84% + width = 50% + height = 30 + + color = "#8B949E" + align = "center" +} diff --git a/scripts/iso/grub/generate-grub-background.sh b/scripts/iso/grub/generate-grub-background.sh new file mode 100644 index 0000000..73d739b --- /dev/null +++ b/scripts/iso/grub/generate-grub-background.sh @@ -0,0 +1,96 @@ +#!/bin/bash +#=============================================================================== +# generate-grub-background.sh — build the Felhom GRUB boot background (scripts v1.22.0, R-38). +# +# Turns Viktor's square brand card (website/assets/og-image_2.png, 1017x1016) into a GRUB-friendly +# 1024x768 PNG. Runs INSIDE the felhom-iso-assistant container (needs ImageMagick); called by +# iso-repack.sh. +# +# THE LAYOUT, AND WHY: +# +# * 1024x768 canvas — gfxterm-safe. The stock PVE grub.cfg already asks for `gfxmode=1024x768`, so +# this is the mode the ISO's GRUB is known to get on real firmware. Anything larger risks a +# fallback to 800x600 and a scaled/cropped background. +# +# * The card is scaled to 500x500 and composited TOP-CENTERED (y=16). That deliberately leaves the +# bottom ~250px empty: it is where felhom-theme.txt positions the boot menu (top=70%) and the +# countdown label (top=84%). Menu text therefore never lands on the cloud/wordmark/tagline. If +# you change CARD or CARD_Y here, move `boot_menu.top` in felhom-theme.txt to match. +# +# * THE GRID. The card carries a subtle background grid (4px lines of #0D131A on #0D1117, every +# 131px) and Viktor asked for it to stay visible. Two things could destroy it: the downscale +# (handled — at 500/1017 the lines land at ~2px, still legible), and the letterbox fill. A flat +# navy fill would leave an obvious 500px square of grid floating in a plain field. +# +# So the grid is drawn across the WHOLE canvas first, PHASE-LOCKED to where the card's own grid +# will land, in the card's own line color — then the card is composited on top. Inside the card +# region the card's real grid shows; outside, the drawn continuation lines up exactly. The seam +# is invisible because the card's background (#0D1117) is also the canvas fill. +# +# The phase-lock math: source line centers are x=81.5+n*131, y=21.5+n*131 (measured, not guessed +# — sampled from og-image_2.png). Scale s=CARD/SRC_W maps them onto the canvas, offset by the +# card's origin. Line width scales the same way. +# +# Usage: generate-grub-background.sh +#=============================================================================== +set -euo pipefail + +SRC="${1:?source card PNG required}" +OUT="${2:?output PNG path required}" + +# --- canvas --------------------------------------------------------------------------------------- +CW=1024; CH=768 +BG="#0D1117" # the card's own background — the letterbox must be seamless, not merely "navy" +GRID="#0D131A" # the card's own grid line color (sampled) + +# --- card placement (keep in sync with felhom-theme.txt boot_menu.top) ------------------------------ +CARD=500; CARD_X=262; CARD_Y=16 + +# --- measured properties of og-image_2.png ---------------------------------------------------------- +SRC_W=1017 # source width; the card is square (1017x1016) +PITCH=131 # grid pitch in source px +V0=81.5 # center of the first vertical grid line, source px +H0=21.5 # center of the first horizontal grid line, source px +LINE=4 # grid line width in source px + +command -v magick >/dev/null && IM=magick || IM=convert +[[ -f "$SRC" ]] || { echo "generate-grub-background: source card not found: $SRC" >&2; exit 2; } + +# Refuse a source whose geometry no longer matches the measured constants above — a silently swapped +# asset would misplace every grid line, and the result only shows up on a boot screen nobody re-checks. +GEOM="$($IM identify -format '%wx%h' "$SRC")" +[[ "$GEOM" == "1017x1016" ]] || { + echo "generate-grub-background: source card is ${GEOM}, expected 1017x1016." >&2 + echo " The grid phase-lock constants (PITCH/V0/H0/LINE) were measured against that geometry." >&2 + echo " Re-measure them for the new asset before changing this check." >&2 + exit 3 +} + +S=$(awk -v c="$CARD" -v w="$SRC_W" 'BEGIN{printf "%.6f", c/w}') # card scale factor +P=$(awk -v p="$PITCH" -v s="$S" 'BEGIN{printf "%.4f", p*s}') # canvas grid pitch +LW=$(awk -v l="$LINE" -v s="$S" 'BEGIN{printf "%.2f", l*s}') # canvas grid line width + +# Walk n outward far enough to cover the canvas in both axes from the card-anchored origin. +draw="" +for n in $(seq -20 40); do + x=$(awk -v a="$CARD_X" -v v="$V0" -v s="$S" -v p="$P" -v n="$n" 'BEGIN{printf "%.2f", a+v*s+n*p}') + if awk -v x="$x" -v w="$CW" 'BEGIN{exit !(x>-2 && x-2 && y&2; exit 4; } + +# PNG24 (no palette, no alpha): GRUB's png module reads plain 8-bit RGB reliably; an indexed or +# 16-bit PNG is where "the background silently doesn't render" comes from. +$IM -size "${CW}x${CH}" xc:"$BG" \ + -stroke "$GRID" -strokewidth "$LW" -draw "$draw" -stroke none \ + \( "$SRC" -resize "${CARD}x${CARD}!" \) -geometry "+${CARD_X}+${CARD_Y}" -composite \ + -depth 8 "PNG24:$OUT" + +[[ -s "$OUT" ]] || { echo "generate-grub-background: produced no output" >&2; exit 5; } +OGEOM="$($IM identify -format '%wx%h %[channels] %[depth]' "$OUT")" +echo "generate-grub-background: $OUT ($OGEOM, $(stat -c%s "$OUT") bytes)" diff --git a/scripts/iso/grub/grub.cfg.tmpl b/scripts/iso/grub/grub.cfg.tmpl new file mode 100644 index 0000000..195b739 --- /dev/null +++ b/scripts/iso/grub/grub.cfg.tmpl @@ -0,0 +1,76 @@ +# Felhom ISO — GRUB menu (scripts v1.22.0, R-38 GRUB slice). GENERATED by iso-repack.sh; the stock +# PVE grub.cfg is REPLACED by this file at repack time. +# +# TWO jobs, one file: +# +# 1. BRANDING — a Felhom gfxmenu theme (felhomtheme/) over a 1024x768 card built from the website's +# og-image_2.png. The stock `set theme=/boot/grub/pvetheme/theme.txt` is gone. +# +# 2. SAFETY — exactly ONE entry is exposed. The stock menu offers Graphical / Terminal UI / serial, +# plus an "Advanced Options" submenu with nomodeset, three debug variants, Rescue Boot, memtest +# and UEFI firmware settings. Every one of those 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, they are NOT EMITTED. +# +# The `linux` / `initrd` lines below are lifted VERBATIM by iso-repack.sh from the stock +# 'Install Proxmox VE (Automated)' entry of the ISO being repacked, so a PVE version bump that +# changes the kernel path or the append line is tracked automatically instead of silently diverging +# from a copy frozen here. iso-repack.sh fails the build if it cannot find them, and asserts the +# append line still carries `proxmox-start-auto-installer` — the flag that makes the install +# unattended. Boot behavior is therefore byte-identical to v1.21.0; only the menu around it changed. + +insmod gzio +insmod iso9660 +insmod png +insmod gfxmenu + +if [ x$feature_default_font_path = xy ] ; then + font=unicode +else + font=$prefix/unicode.pf2 +fi + +# Matches the background canvas exactly (1024x768). 800x600/640x480 are fallbacks for firmware that +# refuses the preferred mode — the theme is percentage-positioned, so it degrades cleanly. +set gfxmode=1024x768,800x600,640x480 +set gfxpayload=1024x768 + +if loadfont $font; then + if test "${grub_platform}" = "efi"; then + insmod efi_gop + insmod efi_uga + fi + insmod video_bochs + insmod video_cirrus + insmod all_video + insmod gfxterm + set theme=/boot/grub/felhomtheme/theme.txt + export theme + terminal_input console + terminal_output gfxterm +fi + +# Serial stays available for operator/nested-canary debugging (the stock cfg does this too). What we +# do NOT do is the stock's `set show_serial_entry=y` — that would add a fourth menu entry. +insmod serial +insmod usbserial_common +insmod usbserial_ftdi +insmod usbserial_pl2303 +insmod usbserial_usbdebug +if serial --unit=0 --speed=115200; then + terminal_input --append serial + terminal_output --append serial +fi + +# Short and decisive: one entry, default, 5s. Note `timeout_style` (underscore) — the stock cfg +# writes `timeout-style`, which GRUB does not recognise as a variable name at all. +set timeout_style=menu +set timeout=5 +set default=0 + +menuentry 'Felhom telepítés' --class felhom --class os { + echo 'A Felhom telepítése indul...' +@@LINUX@@ + echo 'Rendszerbetöltő betöltése...' +@@INITRD@@ +} diff --git a/scripts/iso/iso-repack.sh b/scripts/iso/iso-repack.sh new file mode 100644 index 0000000..5948721 --- /dev/null +++ b/scripts/iso/iso-repack.sh @@ -0,0 +1,320 @@ +#!/bin/bash +#=============================================================================== +# iso-repack.sh — the post-prepare-iso repack stage: GRUB branding + single-entry menu surgery +# (scripts v1.22.0, R-38) and, when asked, the slice-B mkimage UEFI loader swap. +# +# WAS mkimage-surgery.sh (slice B, v1.18.0). v1.22.0 generalised it because BOTH jobs need the same +# expensive extract -> modify -> re-master cycle, and doing them as two separate repacks would double +# the runtime and re-master the image twice for no reason. The mkimage recipe below is UNCHANGED and +# still the N100 run's proven-live one — do NOT re-derive it. +# +# RUNS INSIDE the felhom-iso-assistant container; operates on /work/out.iso (the prepare-iso output) +# and writes /work/final.iso. NEVER touches the source ISO or the assistant's answer/first-boot +# payload — only the GRUB menu, the theme, and (in mkimage mode) the EFI boot path. +# +# 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 +# +# 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 +# +# Writes: /work/final.iso, /work/grub-version.txt (mkimage), /work/brand-report.txt (branding) +# +# --- the mkimage recipe (unchanged from v1.18.0) --------------------------------------------------- +# Build BOOTX64.EFI from the ISO's OWN x86_64-efi GRUB modules (the box's working 2.12-9+pmx2 build), +# embedding the module set the ISO's grub.cfg needs + an embedded config that `search --fs-uuid`es the +# ISO and `configfile`s its real menu; swap it into the ISO9660 EFI/BOOT tree AND inside efi.img. The +# image is UNSIGNED (Secure Boot must be OFF on the target) — that is the documented mkimage contract. +#=============================================================================== +set -euo pipefail + +OUT=/work/out.iso +FINAL=/work/final.iso +EX=/work/ex +EMB=/work/embedded.cfg +LDR=/work/BOOTX64.EFI +BRANDDIR=/work/brand +ORIG_CFG=/work/grub.cfg.orig + +LOADER="${FELHOM_LOADER:-shim}" +BRAND="${FELHOM_BRAND:-1}" + +say() { echo "iso-repack: $*"; } + +# The osirrox extract tree is written by container-root; the host-side build cleanup (a non-root user) +# cannot remove it. Remove it here (we ARE root in the container) on every exit path so no /tmp litter +# survives the run. +cleanup_ex() { if [[ -n "${EX:-}" && -e "$EX" ]]; then rm -rf "$EX" 2>/dev/null || true; fi; } +trap cleanup_ex EXIT + +[[ -f "$OUT" ]] || { echo "iso-repack: /work/out.iso missing" >&2; exit 2; } +[[ "$LOADER" == "shim" || "$LOADER" == "mkimage" ]] || { echo "iso-repack: bad FELHOM_LOADER '$LOADER'" >&2; exit 2; } + +# Nothing to do at all -> pass the prepared ISO through byte-for-byte rather than re-mastering it. +if [[ "$BRAND" != "1" && "$LOADER" == "shim" ]]; then + say "no branding, shim loader — passing the prepared ISO through unmodified" + cp "$OUT" "$FINAL"; exit 0 +fi + +command -v xorriso >/dev/null || { echo "iso-repack: missing tool: xorriso" >&2; exit 2; } +if [[ "$LOADER" == "mkimage" ]]; then + for t in grub-mkimage mcopy mdir; do + command -v "$t" >/dev/null || { echo "iso-repack: missing tool: $t" >&2; exit 2; } + done +fi +if [[ "$BRAND" == "1" ]]; then + command -v magick >/dev/null || command -v convert >/dev/null \ + || { echo "iso-repack: ImageMagick missing (needed for the GRUB background) — rebuild the assistant image" >&2; exit 2; } + for f in grub.cfg.tmpl felhom-theme.txt generate-grub-background.sh card.png; do + [[ -f "$BRANDDIR/$f" ]] || { echo "iso-repack: brand asset missing: $BRANDDIR/$f" >&2; exit 2; } + done +fi + +# --- 1. extract the full prepared ISO tree (osirrox) — preserves the answer + first-boot payload. +# osirrox reproduces the ISO's (read-only) file modes, so make the tree writable afterwards or +# the loader swap and the workspace cleanup can't overwrite/remove the files. ------------------ +[[ -e "$EX" ]] && { chmod -R u+w "$EX" 2>/dev/null || true; rm -rf "$EX"; } +mkdir -p "$EX" +xorriso -osirrox on -indev "$OUT" -extract / "$EX" >/dev/null 2>&1 +chmod -R u+w "$EX" +say "extracted prepared ISO tree" + +# Locate the ISO's real grub.cfg and snapshot it BEFORE branding rewrites it: the mkimage module list +# below is derived from the STOCK cfg's insmods, and branding must not be able to shrink that set. +GCFG="" +for c in "$EX/boot/grub/grub.cfg" "$EX/boot/grub/x86_64-efi/grub.cfg"; do + [[ -f "$c" ]] && { GCFG="$c"; break; } +done +[[ -n "$GCFG" ]] || { echo "iso-repack: no grub.cfg found in the extracted tree" >&2; exit 3; } +cp "$GCFG" "$ORIG_CFG" + +#==================================================================================================== +# --- 2. BRANDING + SINGLE-ENTRY MENU SURGERY (R-38) ------------------------------------------------ +#==================================================================================================== +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 + } + + # 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. + LINUX_LINE="$(awk ' + /menuentry .Install Proxmox VE \(Automated\)./ { inblk=1; next } + inblk && /^[[:space:]]*linux[[:space:]]/ { print; exit } + inblk && /^[[:space:]]*}/ { inblk=0 } + ' "$ORIG_CFG")" + INITRD_LINE="$(awk ' + /menuentry .Install Proxmox VE \(Automated\)./ { inblk=1; next } + inblk && /^[[:space:]]*initrd[[:space:]]/ { print; exit } + 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; } + [[ -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 + } + grep -q '/boot/initrd.img' <<<"$INITRD_LINE" || { + echo "iso-repack: the lifted initrd line looks wrong: $INITRD_LINE" >&2; exit 12 + } + # Normalise indentation only — the command and its arguments are untouched. + LINUX_NORM=" $(sed -E 's/^[[:space:]]+//' <<<"$LINUX_LINE")" + INITRD_NORM=" $(sed -E 's/^[[:space:]]+//' <<<"$INITRD_LINE")" + say "lifted kernel line: $(sed -E 's/^[[:space:]]+//' <<<"$LINUX_LINE")" + + # 2c. Build the background and install the theme. + THEMEDIR="$EX/boot/grub/felhomtheme" + mkdir -p "$THEMEDIR" + bash "$BRANDDIR/generate-grub-background.sh" "$BRANDDIR/card.png" "$THEMEDIR/background.png" \ + | sed 's/^/ /' + cp "$BRANDDIR/felhom-theme.txt" "$THEMEDIR/theme.txt" + + # 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; } + + # 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; } + [[ "$N_SUB" == "0" ]] || { echo "iso-repack: rendered grub.cfg has $N_SUB submenus, want 0" >&2; exit 14; } + for banned in proxtui proxdebug nomodeset 'Rescue Boot' memtest fwsetup; do + if grep -q "$banned" "$GCFG"; then + echo "iso-repack: rendered grub.cfg still references '$banned'" >&2; exit 14 + fi + done + grep -q "set theme=/boot/grub/felhomtheme/theme.txt" "$GCFG" \ + || { echo "iso-repack: rendered grub.cfg does not point at the Felhom theme" >&2; exit 14; } + [[ -s "$THEMEDIR/background.png" && -s "$THEMEDIR/theme.txt" ]] \ + || { echo "iso-repack: theme assets missing after install" >&2; exit 14; } + + # The stock PVE theme is now unreferenced. Remove it so the ISO carries one theme, not two. + rm -rf "$EX/boot/grub/pvetheme" + + cat > /work/brand-report.txt < /work/grub-version.txt + say "grub: $(cat /work/grub-version.txt)" + + # 3a. GRUB build to assemble the loader from. The N100 fix used the box's OWN INSTALLED 2.12 GRUB + # (a DIFFERENT, working build than the ISO's problem one — which is the whole point). The ISO + # ships modules but NOT kernel.img, so grub-mkimage cannot use the ISO's module dir directly; + # the box used its /usr/lib/grub/x86_64-efi. The container mirrors that: grub 2.12 == the PVE + # 9.x ISO's 2.12-9+pmx2 generation. We take the module BINARIES from here and the module LIST + # from the ISO's own grub.cfg (so we embed exactly what the ISO menu needs). ------------------- + GDIR="" + for d in /usr/lib/grub/x86_64-efi /usr/lib/grub/x86_64-efi-signed; do + [[ -f "$d/kernel.img" ]] && { GDIR="$d"; break; } + done + [[ -n "$GDIR" ]] || { echo "iso-repack: no usable GRUB x86_64-efi build (kernel.img) in the container" >&2; exit 3; } + say "grub module source: $GDIR" + + # 3b. module list: the base set the search/configfile-from-USB chain needs, PLUS every module the + # STOCK grub.cfg insmod's (read from the pre-branding snapshot — branding must not be able to + # shrink the embedded set). bitmap/bitmap_scale/trig are gfxmenu's renderer dependencies: the + # Felhom theme needs them and the stock cfg does not insmod them explicitly. + BASE="part_gpt part_msdos msdospart fat exfat iso9660 udf search search_fs_uuid search_fs_file search_label \ +configfile normal boot linux linuxefi chain loadenv loopback echo test true cat ls help \ +all_video efi_gop efi_uga video video_fb font gfxterm gfxterm_background gfxmenu bitmap bitmap_scale trig \ +png jpeg terminal serial gzio xzio lzopio minicmd reboot halt probe regexp sleep keystatus read" + CFGMODS="$(grep -hoE 'insmod[[:space:]]+[a-zA-Z0-9_]+' "$ORIG_CFG" | awk '{print $2}' | sort -u)" + MODS="" + for m in $BASE $CFGMODS; do + [[ -f "$GDIR/$m.mod" ]] && MODS="$MODS $m" + done + MODS="$(echo "$MODS" | tr ' ' '\n' | awk 'NF' | sort -u | tr '\n' ' ')" + say "embedding $(echo "$MODS" | wc -w) modules from the ISO's own x86_64-efi build" +fi + +# --- 4. pin the volume modification-date so the ISO's GRUB fs-uuid is DETERMINISTIC and KNOWN before +# we build the loader (GRUB's iso9660 fs_uuid is derived from the PVD modification timestamp). +# Reuse the prepared ISO's own timestamp verbatim -> the embedded search matches the re-mastered +# image (we pin the same value on re-master in step 6). ----------------------------------------- +MDATE="$(xorriso -indev "$OUT" -report_el_torito as_mkisofs 2>/dev/null \ + | grep -oE "modification-date='[0-9]+'" | grep -oE '[0-9]+' | head -1)" +[[ -n "$MDATE" && ${#MDATE} -ge 14 ]] || { echo "iso-repack: could not read the ISO modification-date" >&2; exit 4; } +ISO_UUID="$(echo "${MDATE:0:16}" | sed -E 's/^(.{4})(.{2})(.{2})(.{2})(.{2})(.{2})(.{2})$/\1-\2-\3-\4-\5-\6-\7/')" +say "ISO fs-uuid (from modification-date $MDATE): $ISO_UUID" + +if [[ "$LOADER" == "mkimage" ]]; then + # --- 5a. embedded config: find the ISO by fs-uuid, then chain its real menu (the recorded recipe) -- + cat > "$EMB" <&2; exit 5; } + say "built BOOTX64.EFI ($(stat -c%s "$LDR") bytes)" + + # --- 5c. swap the loader into BOTH the ISO9660 EFI/BOOT tree AND inside the efi.img ESP. The ISO + # tree uses Rock Ridge (LOWERCASE) names — `/efi/boot/bootx64.efi` — so overwrite the + # EXISTING files in place (case-insensitive find), NEVER mkdir a spurious uppercase path. + # The efi.img ESP is FAT (case-insensitive), the authoritative loader UEFI firmware actually + # runs from USB. ------------------------------------------------------------------------- + TREE_HITS=0 + while IFS= read -r f; do cp "$LDR" "$f"; TREE_HITS=$((TREE_HITS+1)); done \ + < <(find "$EX" -ipath '*/efi/boot/bootx64.efi') + while IFS= read -r f; do cp "$LDR" "$f"; done \ + < <(find "$EX" -ipath '*/efi/boot/grubx64.efi') + [[ "$TREE_HITS" -ge 1 ]] || { echo "iso-repack: no bootx64.efi found in the ISO9660 tree to replace" >&2; exit 6; } + + EFIIMG="$EX/efi.img" + [[ -f "$EFIIMG" ]] || EFIIMG="$(find "$EX" -maxdepth 3 -iname 'efi*.img' | head -1)" + [[ -f "$EFIIMG" ]] || { echo "iso-repack: efi.img ESP not found in the ISO tree" >&2; exit 6; } + # FAT is case-insensitive: ::/EFI/BOOT/BOOTX64.EFI resolves the real loader regardless of stored case. + mcopy -i "$EFIIMG" -o "$LDR" ::/EFI/BOOT/BOOTX64.EFI + if mdir -i "$EFIIMG" ::/EFI/BOOT 2>/dev/null | grep -qi grubx64; then + mcopy -i "$EFIIMG" -o "$LDR" ::/EFI/BOOT/grubx64.efi + fi + say "swapped bootx64.efi in the ISO tree ($TREE_HITS) and inside $(basename "$EFIIMG")" +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 +# entries" on THIS layout, and Mac boot is irrelevant for N100/PC hardware. We KEEP the hybrid +# BIOS grub2-mbr + El Torito (BIOS eltorito.img + UEFI /efi.img) + the GPT EFI System Partition +# (-efi-boot-part) that USB UEFI firmware boots from — the whole point of this fix. The volume +# id + modification-date are pinned explicitly so the embedded fs-uuid stays valid. ------------- +RPT="$(xorriso -indev "$OUT" -report_el_torito as_mkisofs 2>/dev/null)" +VOLID="$(printf '%s\n' "$RPT" | sed -nE "s/^-V '(.*)'\$/\\1/p" | head -1)"; [[ -n "$VOLID" ]] || VOLID="PVE" +# Drop, then re-add explicitly: the volume id + modification-date. Drop entirely: the Apple APM/HFS+ +# map (-hfsplus / -apm-block-size) AND the isohybrid GPT-basdat marking (-part_like_isohybrid / +# -isohybrid-gpt-basdat) — prepare-iso re-masters with these, and re-emitting them alongside +# -efi-boot-part + the protective MBR trips xorriso 1.5.6's "Overlapping MBR partition entries". The +# resulting image keeps the protective MBR + grub2-mbr (BIOS) + El Torito (BIOS+UEFI) + the GPT EFI +# System Partition (verified). Repoint the grub2-mbr template at the in-container out.iso. +FILTERED="$(printf '%s\n' "$RPT" \ + | grep -vE "^-V '|^--modification-date=|^-apm-block-size |^-hfsplus\$|^-part_like_isohybrid\$|^-isohybrid-gpt-basdat\$" \ + | sed -E "s#(--interval:[^']*:)'[^']*'#\\1'$OUT'#")" +rm -f "$FINAL" +# shellcheck disable=SC2086 +eval xorriso -as mkisofs -V "'$VOLID'" --modification-date="'$MDATE'" \ + $FILTERED -o "$FINAL" "$EX" >/work/xorriso.log 2>&1 \ + || { echo "iso-repack: xorriso re-master FAILED"; tail -25 /work/xorriso.log >&2; exit 7; } + +[[ -f "$FINAL" ]] || { echo "iso-repack: no final.iso produced" >&2; exit 7; } +# assert both boot images survived (BIOS eltorito.img + UEFI efi.img) — a silent loss would fail-safe +# to an unbootable stick, so gate it here. +ETIMG="$(xorriso -indev "$FINAL" -report_el_torito plain 2>/dev/null | grep -cE 'El Torito boot img')" +[[ "$ETIMG" -ge 2 ]] || { echo "iso-repack: re-master lost a boot image (El Torito entries=$ETIMG, want >=2)" >&2; exit 8; } +say "re-mastered final.iso ($(stat -c%s "$FINAL") bytes; El Torito boot images=$ETIMG)" + +# --- 7. verify the re-mastered image kept the modification-date (so the embedded fs-uuid matches) --- +FINAL_MDATE="$(xorriso -indev "$FINAL" -report_el_torito as_mkisofs 2>/dev/null \ + | grep -oE "modification-date='[0-9]+'" | grep -oE '[0-9]+' | head -1)" +if [[ "${FINAL_MDATE:0:14}" != "${MDATE:0:14}" ]]; then + echo "iso-repack: WARN final modification-date ($FINAL_MDATE) != source ($MDATE) — the search fs-uuid may not match; re-check" >&2 +else + say "fs-uuid preserved ($ISO_UUID)" +fi + +# --- 8. post-re-master proof that the branding actually LANDED in the image we ship (not merely in +# the extract tree) — read the menu back out of final.iso. -------------------------------------- +if [[ "$BRAND" == "1" ]]; then + VER=/work/verify; rm -rf "$VER"; mkdir -p "$VER" + 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; } + [[ -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" +fi +say "done" diff --git a/scripts/iso/mkimage-surgery.sh b/scripts/iso/mkimage-surgery.sh deleted file mode 100644 index f9e19f5..0000000 --- a/scripts/iso/mkimage-surgery.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/bin/bash -#=============================================================================== -# mkimage-surgery.sh — R-21 slice B: replace a prepared Felhom ISO's UEFI boot loader with a -# monolithic grub-mkimage image, so cheap AMI (AN3PLUS-class) firmware that can't relocate the ISO's -# signed GRUB from USB (VALIDATION-n100-baremetal F1, `relocation 0x0`) boots. -# -# RUNS INSIDE the felhom-iso-assistant container; operates on /work/out.iso (the prepare-iso output) -# and writes /work/final.iso + /work/grub-version.txt. NEVER touches the source ISO or the assistant's -# answer/first-boot payload — only the EFI boot path. -# -# The recipe is the N100 run's PROVEN-LIVE workaround (do NOT re-derive it): build BOOTX64.EFI from -# the ISO's OWN x86_64-efi GRUB modules (the box's working 2.12-9+pmx2 build), embedding the module -# set the ISO's grub.cfg needs + an embedded config that `search --fs-uuid`es the ISO and -# `configfile`s its real menu; swap it into the ISO9660 EFI/BOOT tree AND inside efi.img; re-master -# with xorriso preserving BOTH the hybrid BIOS boot and the payload. The image is UNSIGNED (Secure -# Boot must be OFF on the target) — that is the documented mkimage contract. -#=============================================================================== -set -euo pipefail - -OUT=/work/out.iso -FINAL=/work/final.iso -EX=/work/ex -EMB=/work/embedded.cfg -LDR=/work/BOOTX64.EFI - -say() { echo "mkimage-surgery: $*"; } - -# The osirrox extract tree is written by container-root; the host-side build cleanup (a non-root user) -# cannot remove it. Remove it here (we ARE root in the container) on every exit path so no /tmp litter -# survives the run. -cleanup_ex() { if [[ -n "${EX:-}" && -e "$EX" ]]; then rm -rf "$EX" 2>/dev/null || true; fi; } -trap cleanup_ex EXIT - -[[ -f "$OUT" ]] || { echo "mkimage-surgery: /work/out.iso missing" >&2; exit 2; } -for t in xorriso grub-mkimage mcopy mdir; do - command -v "$t" >/dev/null || { echo "mkimage-surgery: missing tool: $t" >&2; exit 2; } -done - -grub-mkimage --version | head -1 > /work/grub-version.txt -say "grub: $(cat /work/grub-version.txt)" - -# --- 1. extract the full prepared ISO tree (osirrox) — preserves the answer + first-boot payload. -# osirrox reproduces the ISO's (read-only) file modes, so make the tree writable afterwards or -# the loader swap (step 7) and the workspace cleanup can't overwrite/remove the files. --------- -[[ -e "$EX" ]] && { chmod -R u+w "$EX" 2>/dev/null || true; rm -rf "$EX"; } -mkdir -p "$EX" -xorriso -osirrox on -indev "$OUT" -extract / "$EX" >/dev/null 2>&1 -chmod -R u+w "$EX" -say "extracted prepared ISO tree" - -# --- 2. GRUB build to assemble the loader from. The N100 fix used the box's OWN INSTALLED 2.12 GRUB -# (a DIFFERENT, working build than the ISO's problem one — which is the whole point). The ISO -# ships modules but NOT kernel.img, so grub-mkimage cannot use the ISO's module dir directly; -# the box used its /usr/lib/grub/x86_64-efi. The container mirrors that: grub 2.12 == the PVE -# 9.x ISO's 2.12-9+pmx2 generation. We take the module BINARIES from here and the module LIST -# from the ISO's own grub.cfg (so we embed exactly what the ISO menu needs). -------------------- -GDIR="" -for d in /usr/lib/grub/x86_64-efi /usr/lib/grub/x86_64-efi-signed; do - [[ -f "$d/kernel.img" ]] && { GDIR="$d"; break; } -done -[[ -n "$GDIR" ]] || { echo "mkimage-surgery: no usable GRUB x86_64-efi build (kernel.img) in the container" >&2; exit 3; } -say "grub module source: $GDIR" -GCFG="" -for c in "$EX/boot/grub/grub.cfg" "$EX/boot/grub/x86_64-efi/grub.cfg"; do - [[ -f "$c" ]] && { GCFG="$c"; break; } -done - -# --- 3. module list: the base set the search/configfile-from-USB chain needs, PLUS every module the -# ISO's grub.cfg insmod's. Keep only modules present in the GRUB build. This IS "every module -# the grub.cfg needs". -BASE="part_gpt part_msdos msdospart fat exfat iso9660 udf search search_fs_uuid search_fs_file search_label \ -configfile normal boot linux linuxefi chain loadenv loopback echo test true cat ls help \ -all_video efi_gop efi_uga video video_fb font gfxterm gfxterm_background gfxmenu png jpeg \ -terminal serial gzio xzio lzopio minicmd reboot halt probe regexp sleep keystatus read" -CFGMODS="" -[[ -n "$GCFG" ]] && CFGMODS=$(grep -hoE 'insmod[[:space:]]+[a-zA-Z0-9_]+' "$GCFG" | awk '{print $2}' | sort -u) -MODS="" -for m in $BASE $CFGMODS; do - [[ -f "$GDIR/$m.mod" ]] && MODS="$MODS $m" -done -MODS="$(echo "$MODS" | tr ' ' '\n' | awk 'NF' | sort -u | tr '\n' ' ')" -say "embedding $(echo "$MODS" | wc -w) modules from the ISO's own x86_64-efi build" - -# --- 4. pin the volume modification-date so the ISO's GRUB fs-uuid is DETERMINISTIC and KNOWN before -# we build the loader (GRUB's iso9660 fs_uuid is derived from the PVD modification timestamp). -# Reuse the prepared ISO's own timestamp verbatim -> the embedded search matches the re-mastered -# image (we pin the same value on re-master in step 8). -MDATE="$(xorriso -indev "$OUT" -report_el_torito as_mkisofs 2>/dev/null \ - | grep -oE "modification-date='[0-9]+'" | grep -oE '[0-9]+' | head -1)" -[[ -n "$MDATE" && ${#MDATE} -ge 14 ]] || { echo "mkimage-surgery: could not read the ISO modification-date" >&2; exit 4; } -ISO_UUID="$(echo "${MDATE:0:16}" | sed -E 's/^(.{4})(.{2})(.{2})(.{2})(.{2})(.{2})(.{2})$/\1-\2-\3-\4-\5-\6-\7/')" -say "ISO fs-uuid (from modification-date $MDATE): $ISO_UUID" - -# --- 5. embedded config: find the ISO by fs-uuid, then chain its real menu (the recorded recipe) --- -cat > "$EMB" <&2; exit 5; } -say "built BOOTX64.EFI ($(stat -c%s "$LDR") bytes)" - -# --- 7. swap the loader into BOTH the ISO9660 EFI/BOOT tree AND inside the efi.img ESP. The ISO tree -# uses Rock Ridge (LOWERCASE) names — `/efi/boot/bootx64.efi` — so overwrite the EXISTING files -# in place (case-insensitive find), NEVER mkdir a spurious uppercase path. The efi.img ESP is -# FAT (case-insensitive), the authoritative loader UEFI firmware actually runs from USB. -------- -TREE_HITS=0 -while IFS= read -r f; do cp "$LDR" "$f"; TREE_HITS=$((TREE_HITS+1)); done \ - < <(find "$EX" -ipath '*/efi/boot/bootx64.efi') -while IFS= read -r f; do cp "$LDR" "$f"; done \ - < <(find "$EX" -ipath '*/efi/boot/grubx64.efi') -[[ "$TREE_HITS" -ge 1 ]] || { echo "mkimage-surgery: no bootx64.efi found in the ISO9660 tree to replace" >&2; exit 6; } - -EFIIMG="$EX/efi.img" -[[ -f "$EFIIMG" ]] || EFIIMG="$(find "$EX" -maxdepth 3 -iname 'efi*.img' | head -1)" -[[ -f "$EFIIMG" ]] || { echo "mkimage-surgery: efi.img ESP not found in the ISO tree" >&2; exit 6; } -# FAT is case-insensitive: ::/EFI/BOOT/BOOTX64.EFI resolves the real loader regardless of stored case. -mcopy -i "$EFIIMG" -o "$LDR" ::/EFI/BOOT/BOOTX64.EFI -if mdir -i "$EFIIMG" ::/EFI/BOOT 2>/dev/null | grep -qi grubx64; then - mcopy -i "$EFIIMG" -o "$LDR" ::/EFI/BOOT/grubx64.efi -fi -say "swapped bootx64.efi in the ISO tree ($TREE_HITS) and inside $(basename "$EFIIMG")" - -# --- 8. 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 -# entries" on THIS layout, and Mac boot is irrelevant for N100/PC hardware. We KEEP the hybrid -# BIOS grub2-mbr + El Torito (BIOS eltorito.img + UEFI /efi.img) + the GPT EFI System Partition -# (-efi-boot-part) that USB UEFI firmware boots from — the whole point of this fix. The volume -# id + modification-date are pinned explicitly so the embedded fs-uuid stays valid. ------------- -RPT="$(xorriso -indev "$OUT" -report_el_torito as_mkisofs 2>/dev/null)" -VOLID="$(printf '%s\n' "$RPT" | sed -nE "s/^-V '(.*)'\$/\\1/p" | head -1)"; [[ -n "$VOLID" ]] || VOLID="PVE" -# Drop, then re-add explicitly: the volume id + modification-date. Drop entirely: the Apple APM/HFS+ -# map (-hfsplus / -apm-block-size) AND the isohybrid GPT-basdat marking (-part_like_isohybrid / -# -isohybrid-gpt-basdat) — prepare-iso re-masters with these, and re-emitting them alongside -# -efi-boot-part + the protective MBR trips xorriso 1.5.6's "Overlapping MBR partition entries". The -# resulting image keeps the protective MBR + grub2-mbr (BIOS) + El Torito (BIOS+UEFI) + the GPT EFI -# System Partition (verified). Repoint the grub2-mbr template at the in-container out.iso. -FILTERED="$(printf '%s\n' "$RPT" \ - | grep -vE "^-V '|^--modification-date=|^-apm-block-size |^-hfsplus\$|^-part_like_isohybrid\$|^-isohybrid-gpt-basdat\$" \ - | sed -E "s#(--interval:[^']*:)'[^']*'#\\1'$OUT'#")" -rm -f "$FINAL" -# shellcheck disable=SC2086 -eval xorriso -as mkisofs -V "'$VOLID'" --modification-date="'$MDATE'" \ - $FILTERED -o "$FINAL" "$EX" >/work/xorriso.log 2>&1 \ - || { echo "mkimage-surgery: xorriso re-master FAILED"; tail -25 /work/xorriso.log >&2; exit 7; } - -[[ -f "$FINAL" ]] || { echo "mkimage-surgery: no final.iso produced" >&2; exit 7; } -# assert both boot images survived (BIOS eltorito.img + UEFI efi.img) — a silent loss would fail-safe -# to an unbootable stick, so gate it here. -ETIMG="$(xorriso -indev "$FINAL" -report_el_torito plain 2>/dev/null | grep -cE 'El Torito boot img')" -[[ "$ETIMG" -ge 2 ]] || { echo "mkimage-surgery: re-master lost a boot image (El Torito entries=$ETIMG, want >=2)" >&2; exit 8; } -say "re-mastered final.iso ($(stat -c%s "$FINAL") bytes; El Torito boot images=$ETIMG)" - -# --- 9. verify the re-mastered image kept the modification-date (so the embedded fs-uuid matches) --- -FINAL_MDATE="$(xorriso -indev "$FINAL" -report_el_torito as_mkisofs 2>/dev/null \ - | grep -oE "modification-date='[0-9]+'" | grep -oE '[0-9]+' | head -1)" -if [[ "${FINAL_MDATE:0:14}" != "${MDATE:0:14}" ]]; then - echo "mkimage-surgery: WARN final modification-date ($FINAL_MDATE) != source ($MDATE) — the search fs-uuid may not match; re-check" >&2 -else - say "fs-uuid preserved ($ISO_UUID)" -fi -say "done" diff --git a/website/assets/og-image_2.png b/website/assets/og-image_2.png new file mode 100644 index 0000000..733b3af Binary files /dev/null and b/website/assets/og-image_2.png differ