veilor-os/overlay/usr/local/bin/veilor-installer

1060 lines
44 KiB
Text
Raw Normal View History

#!/usr/bin/env bash
# veilor-os installer — TUI wrapper around anaconda kickstart install.
# Runs on tty1 in place of getty (live ISO boot path).
#
# Flow:
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# 1. ASCII banner (assets/installer/banner.txt)
# 2. Menu: Install / Live desktop / Live shell / Reboot / Power off
# 3. If Install: collect answers via gum (disk, hostname, LUKS pw,
# admin pw, locale)
# 4. Generate /run/install/veilor-generated.ks from template + answers
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# (full veilor-os install: package list + overlay + scripts + harden)
# 5. Exec anaconda --kickstart=/run/install/veilor-generated.ks
# 6. On finish: reboot into installed system
#
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# v0.5.1 — gum (charm.sh) replaces whiptail; whiptail kept as fallback.
# Generated kickstart now installs full veilor-os (matches live ks).
set -uo pipefail
export TERM="${TERM:-linux}"
# Log to /run (pure tmpfs) — /var/log overlays squashfs on the live ISO, so a
# bad sector on the USB medium turns `tee -a` into "input/output error" and
# kills the installer before the menu can render.
LOG=/run/veilor-installer.log
# require_tty MUST run before the tee redirect — process substitution
# replaces fd1 with a pipe, breaking `[[ -t 1 ]]`.
require_tty() {
if ! [[ -t 0 && -t 1 ]]; then
echo "[ERR] veilor-installer must run on a real tty" >&2
exit 1
fi
}
require_tty
# Tee output for log persistence, but only if LOG is writable. On a flaky USB
# the squashfs/overlay can throw I/O errors mid-write — tolerate that and
# keep the installer running without persistence rather than aborting.
if : >> "$LOG" 2>/dev/null; then
exec > >(tee -a "$LOG") 2>&1
fi
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# ── Branded styling for gum ─────────────────────────────────────────────
# colors.gum sets GUM_* env vars — pure-black palette from veilor-black KDE.
# Sourced at top so every prompt below picks up branded colors.
COLORS=/usr/share/veilor-os/assets/installer/colors.gum
[[ -r $COLORS ]] && source "$COLORS"
BANNER_FILE=/usr/share/veilor-os/assets/installer/banner.txt
# Detect TUI backend once. gum is preferred; whiptail is the fallback so the
# installer keeps working on minimal images or if /usr/local/bin/gum is
# missing (e.g. broken vendored binary, /usr remount issues).
if command -v gum >/dev/null 2>&1; then
TUI=gum
elif command -v whiptail >/dev/null 2>&1; then
TUI=whiptail
else
echo "[ERR] neither gum nor whiptail available — cannot run TUI" >&2
exit 1
fi
banner() {
clear
# Read version + date for version line. /etc/os-release has VERSION_ID.
local ver=""
[[ -r /etc/os-release ]] && ver=$(. /etc/os-release; echo "${VERSION_ID:-0.0}")
local d
d=$(date +%Y-%m-%d)
local vline="veilor-os ${ver} · ${d} · live"
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
if [[ -r $BANNER_FILE ]]; then
if [[ $TUI == gum ]]; then
# gum style: rounded border, banner + blank line + version line.
gum style --border rounded --margin "0 2" --padding "1 3" \
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
--border-foreground "${VEILOR_DIM:-240}" \
--foreground "${VEILOR_FG:-15}" \
"$(cat "$BANNER_FILE")" \
"" \
"$vline"
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
else
cat "$BANNER_FILE"
echo
echo " $vline"
echo
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
fi
else
# Fallback ASCII if banner.txt missing (older overlay).
cat << EOF
veilor-os installer
$vline
EOF
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
fi
}
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# ── TUI wrapper functions ───────────────────────────────────────────────
# Each prompt_* call abstracts gum / whiptail. Always emit the chosen value
# on stdout; non-zero exit on cancel/ESC. Callers use `||` to propagate.
# prompt_choose <header> <opt1> [opt2 ...]
# Single-select menu. Returns the selected option literal on stdout.
prompt_choose() {
local header=$1; shift
if [[ $TUI == gum ]]; then
gum choose --header "$header" "$@"
else
# whiptail menu needs tag/desc pairs. Use the option as both.
local args=()
local opt
for opt in "$@"; do args+=("$opt" "$opt"); done
whiptail --title "veilor-os" --menu "$header" 18 70 8 \
"${args[@]}" 3>&1 1>&2 2>&3
fi
}
# prompt_choose_pairs <header> <tag1> <desc1> [tag2 desc2 ...]
# Tag/description menu. Returns the chosen tag.
# Used when display label differs from machine-readable value (e.g. disk
# path /dev/nvme0n1 vs description "476G WDC PC SN740").
prompt_choose_pairs() {
local header=$1; shift
if [[ $TUI == gum ]]; then
# gum has no tag/desc concept — render "tag — desc" lines, parse.
local lines=() i=1
while (( i <= $# )); do
local tag=${!i}; ((i++))
local desc=${!i}; ((i++))
lines+=("$tag — $desc")
done
local picked
picked=$(printf '%s\n' "${lines[@]}" | gum choose --header "$header") || return 1
# Strip " — desc" suffix to recover the tag.
echo "${picked%% — *}"
else
whiptail --title "veilor-os" --menu "$header" 18 70 8 \
"$@" 3>&1 1>&2 2>&3
fi
}
# prompt_input <header> [default]
prompt_input() {
local header=$1 default=${2:-}
if [[ $TUI == gum ]]; then
gum input --header "$header" --value "$default"
else
whiptail --title "veilor-os" --inputbox "$header" 10 60 "$default" \
3>&1 1>&2 2>&3
fi
}
# prompt_password <header>
prompt_password() {
local header=$1
if [[ $TUI == gum ]]; then
gum input --password --header "$header"
else
whiptail --title "veilor-os" --passwordbox "$header" 10 60 \
3>&1 1>&2 2>&3
fi
}
# prompt_confirm <message>
# Exits 0 on yes, 1 on no — matches whiptail --yesno semantics.
prompt_confirm() {
local msg=$1
if [[ $TUI == gum ]]; then
gum confirm "$msg"
else
whiptail --title "Confirm" --yesno "$msg" 16 60
fi
}
# prompt_message <message>
# Non-blocking notice. gum has no msgbox; print styled + sleep.
prompt_message() {
local msg=$1
if [[ $TUI == gum ]]; then
gum style --foreground "${VEILOR_FG:-15}" --border rounded \
--border-foreground "${VEILOR_DIM:-240}" --padding "1 2" -- "$msg"
sleep 2
else
whiptail --title "veilor-os" --msgbox "$msg" 10 60
fi
}
# prompt_error <message>
# Same as message but with a red foreground for visibility.
prompt_error() {
local msg=$1
if [[ $TUI == gum ]]; then
gum style --foreground 1 --border rounded --border-foreground 1 \
--padding "1 2" -- "$msg"
sleep 2
else
whiptail --title "Error" --msgbox "$msg" 10 60
fi
}
main_menu() {
# Empty header — banner already provides context. `──────` line splits
# primary actions (top) from session controls (bottom). Cursor `` set
# via GUM_CHOOSE_CURSOR in colors.gum.
prompt_choose "" \
"Install" \
"live · KDE" \
"live · shell" \
"──────" \
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
"Reboot" \
"Power off"
}
collect_answers() {
local disk hostname luks_pw admin_pw locale
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
local disks_pairs
# ── Disk ──
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# Build whiptail-style "tag desc" pairs. prompt_choose_pairs reshapes
# for gum if needed.
# shellcheck disable=SC2207
disks_pairs=($(lsblk -dpno NAME,SIZE,MODEL | grep -E '^/dev/(sd|nvme|vd|mmcblk)' | \
awk '{name=$1; size=$2; $1=""; $2=""; sub(/^ +/,""); gsub(/ /,"_"); model=$0; if(model=="")model="unknown"; print name, size"_"model}'))
if [[ ${#disks_pairs[@]} -eq 0 ]]; then
prompt_error "No installable disks found."
return 1
fi
v0.5.28 (partial): lock locale to en_US, roadmap post-install menu Install-flow change + roadmap update. The roadmap entry is the durable record; the code change is the immediate effect. ## Locale picker removed The "[4/4] Locale" prompt is gone. Locale is hardcoded to en_US.UTF-8 for the install. Two reasons: 1. The picker only offered en_GB and en_US, both of which install identically apart from the langtag string and a couple of date / currency conventions that nobody who's mid-install is thinking about. It's a fake choice that adds a screen. 2. `localectl set-locale` post-install handles every locale on earth in one command. The v0.7 `veilor-postinstall` first-login menu (see roadmap below) will offer a locale + keyboard layout switch with live preview, which is the right place for that decision. Step counters updated [1/4]→[1/3], [2/4]→[2/3], [3/4]→[3/3]. The Locale row stays in the confirm-summary box because users still want to see what they're getting installed. ## Roadmap - New section v0.5.27–v0.5.28 — documents the install-path stabilisation work explicitly so the bridge between "first green ISO" and "looks polished" is not invisible. Calls out the LUKS BLS fix that landed in v0.5.27 + the gum-input replacement scheduled for v0.5.28. - v0.6 — `veilor-doctor` description expanded: this is the post-install audit tool. Every user runs it weekly to see drift from baseline. - v0.6 — new entry `veilor-postinstall`: EndeavourOS-style first-login welcome menu, single TUI screen, asks once. Covers the "I just installed, what do I configure" gap in one explicit step instead of scattered docs.
2026-05-05 02:48:36 +01:00
disk=$(prompt_choose_pairs "[1/3] Select install disk · WILL BE ERASED" "${disks_pairs[@]}") || return 1
# ── Hostname ──
# Hardcoded for branded consistency. Post-install: `hostnamectl set-hostname`.
hostname="veilor"
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# Reject shell-special and sed-special chars in passwords. Generated
# kickstart writes them via heredoc + sed substitution; bare $, ", \, `
# would corrupt the ks line or partially expand at heredoc time.
# &, |, /, newline are sed-special: & expands to the matched pattern
# (so `aA1!@#%^&*()` becomes `aA1!@#%^__ADMIN_PW__*()`), | is our
# delimiter, / would match if delimiter changes, newline breaks the
# sed expression. sed_escape() below adds defense-in-depth, but we
# also reject these at input so the user sees an immediate error
# rather than a corrupted ks file. 8-char min for entropy.
validate_pw() {
local pw=$1 label=$2
if [[ ${#pw} -lt 8 ]]; then
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
prompt_error "Weak $label — minimum 8 characters."
return 1
fi
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
if [[ $pw =~ [\"\$\\\`\&\|/$'\n'] ]]; then
prompt_error "Invalid $label — cannot contain: \" \$ \\ \` & | / newline"
return 1
fi
return 0
}
# ── LUKS passphrase ──
v0.5.28 (partial): lock locale to en_US, roadmap post-install menu Install-flow change + roadmap update. The roadmap entry is the durable record; the code change is the immediate effect. ## Locale picker removed The "[4/4] Locale" prompt is gone. Locale is hardcoded to en_US.UTF-8 for the install. Two reasons: 1. The picker only offered en_GB and en_US, both of which install identically apart from the langtag string and a couple of date / currency conventions that nobody who's mid-install is thinking about. It's a fake choice that adds a screen. 2. `localectl set-locale` post-install handles every locale on earth in one command. The v0.7 `veilor-postinstall` first-login menu (see roadmap below) will offer a locale + keyboard layout switch with live preview, which is the right place for that decision. Step counters updated [1/4]→[1/3], [2/4]→[2/3], [3/4]→[3/3]. The Locale row stays in the confirm-summary box because users still want to see what they're getting installed. ## Roadmap - New section v0.5.27–v0.5.28 — documents the install-path stabilisation work explicitly so the bridge between "first green ISO" and "looks polished" is not invisible. Calls out the LUKS BLS fix that landed in v0.5.27 + the gum-input replacement scheduled for v0.5.28. - v0.6 — `veilor-doctor` description expanded: this is the post-install audit tool. Every user runs it weekly to see drift from baseline. - v0.6 — new entry `veilor-postinstall`: EndeavourOS-style first-login welcome menu, single TUI screen, asks once. Covers the "I just installed, what do I configure" gap in one explicit step instead of scattered docs.
2026-05-05 02:48:36 +01:00
luks_pw=$(prompt_password "[2/3] Encryption · LUKS2 passphrase (min 8)") || return 1
validate_pw "$luks_pw" "passphrase" || return 1
# ── Admin password ──
v0.5.28 (partial): lock locale to en_US, roadmap post-install menu Install-flow change + roadmap update. The roadmap entry is the durable record; the code change is the immediate effect. ## Locale picker removed The "[4/4] Locale" prompt is gone. Locale is hardcoded to en_US.UTF-8 for the install. Two reasons: 1. The picker only offered en_GB and en_US, both of which install identically apart from the langtag string and a couple of date / currency conventions that nobody who's mid-install is thinking about. It's a fake choice that adds a screen. 2. `localectl set-locale` post-install handles every locale on earth in one command. The v0.7 `veilor-postinstall` first-login menu (see roadmap below) will offer a locale + keyboard layout switch with live preview, which is the right place for that decision. Step counters updated [1/4]→[1/3], [2/4]→[2/3], [3/4]→[3/3]. The Locale row stays in the confirm-summary box because users still want to see what they're getting installed. ## Roadmap - New section v0.5.27–v0.5.28 — documents the install-path stabilisation work explicitly so the bridge between "first green ISO" and "looks polished" is not invisible. Calls out the LUKS BLS fix that landed in v0.5.27 + the gum-input replacement scheduled for v0.5.28. - v0.6 — `veilor-doctor` description expanded: this is the post-install audit tool. Every user runs it weekly to see drift from baseline. - v0.6 — new entry `veilor-postinstall`: EndeavourOS-style first-login welcome menu, single TUI screen, asks once. Covers the "I just installed, what do I configure" gap in one explicit step instead of scattered docs.
2026-05-05 02:48:36 +01:00
admin_pw=$(prompt_password "[3/3] Admin user · password for 'admin'") || return 1
validate_pw "$admin_pw" "password" || return 1
# ── Locale ──
v0.5.28 (partial): lock locale to en_US, roadmap post-install menu Install-flow change + roadmap update. The roadmap entry is the durable record; the code change is the immediate effect. ## Locale picker removed The "[4/4] Locale" prompt is gone. Locale is hardcoded to en_US.UTF-8 for the install. Two reasons: 1. The picker only offered en_GB and en_US, both of which install identically apart from the langtag string and a couple of date / currency conventions that nobody who's mid-install is thinking about. It's a fake choice that adds a screen. 2. `localectl set-locale` post-install handles every locale on earth in one command. The v0.7 `veilor-postinstall` first-login menu (see roadmap below) will offer a locale + keyboard layout switch with live preview, which is the right place for that decision. Step counters updated [1/4]→[1/3], [2/4]→[2/3], [3/4]→[3/3]. The Locale row stays in the confirm-summary box because users still want to see what they're getting installed. ## Roadmap - New section v0.5.27–v0.5.28 — documents the install-path stabilisation work explicitly so the bridge between "first green ISO" and "looks polished" is not invisible. Calls out the LUKS BLS fix that landed in v0.5.27 + the gum-input replacement scheduled for v0.5.28. - v0.6 — `veilor-doctor` description expanded: this is the post-install audit tool. Every user runs it weekly to see drift from baseline. - v0.6 — new entry `veilor-postinstall`: EndeavourOS-style first-login welcome menu, single TUI screen, asks once. Covers the "I just installed, what do I configure" gap in one explicit step instead of scattered docs.
2026-05-05 02:48:36 +01:00
# Hardcoded en_US.UTF-8 for branded consistency. The picker that
# used to live here (en_GB / en_US) only added confusion — both
# locales install identically, the user couldn't notice the
# difference, and the post-install `localectl set-locale` works for
# any locale anyway. v0.7 post-install menu will offer locale + kb
# layout switch with live preview. For the install flow, fixed.
locale="en_US.UTF-8"
# ── Confirmation ──
# Render summary box + danger lines via gum style, then gum confirm.
# Whiptail fallback: simple yesno.
if [[ $TUI == gum ]]; then
clear
gum style --border rounded --margin "1 2" --padding "1 3" \
--border-foreground "${VEILOR_DIM:-240}" \
--foreground "${VEILOR_FG:-15}" \
"Confirm install" \
"" \
" Disk $disk $(gum style --foreground 1 'WILL BE ERASED')" \
" Locale $locale" \
" LUKS ✓ set" \
" Admin ✓ set" \
"" \
"$(gum style --foreground 3 'This action is irreversible.')"
gum confirm --affirmative "Yes, install" --negative "Cancel" "Proceed?" || return 1
else
whiptail --title "Confirm install" --yesno \
"About to install veilor-os:
Disk: $disk (WILL BE ERASED)
Locale: $locale
LUKS: set
Admin pw: set
This action is irreversible.
Proceed?" 16 60 || return 1
fi
# Export to caller via globals
SEL_DISK=$disk
SEL_HOSTNAME=$hostname
SEL_LUKS_PW=$luks_pw
SEL_ADMIN_PW=$admin_pw
SEL_LOCALE=$locale
return 0
}
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# sed_escape — escape sed special chars in a replacement string.
# Replacement-side metacharacters: & (matched pattern), \ (escape),
# | (our chosen delimiter), / (alternate delimiter — escape too in case
# delimiter ever changes). Newline is rejected in validate_pw because
# escaping it portably across BSD/GNU sed is fiddly.
# Order matters: \ must be escaped FIRST so we don't double-escape the
# backslashes we're about to emit for &, |, /.
sed_escape() {
printf '%s' "$1" | sed -e 's/[\\&|/]/\\&/g'
}
# detect_seed_pubkey — search attached cdroms for a NoCloud cidata seed
# with an ssh_authorized_keys entry. Returns the first key on stdout, or
# empty string if none found. Used by both auto-install.sh (cloud-init
# seed pre-built with host pubkey) and humans (drop a seed iso next to
# the install media).
detect_seed_pubkey() {
local dev label tmpmnt key=""
for dev in /dev/sr0 /dev/sr1 /dev/sr2 /dev/sr3; do
[ -b "$dev" ] || continue
label=$(blkid -o value -s LABEL "$dev" 2>/dev/null)
if [[ $label == "cidata" || $label == "CIDATA" ]]; then
tmpmnt=$(mktemp -d)
if mount -o ro "$dev" "$tmpmnt" 2>/dev/null; then
# NoCloud user-data format:
# ssh_authorized_keys:
# - ssh-ed25519 AAAA... user@host
# Extract first ssh-* line, strip leading '- '.
key=$(grep -E '^\s*-\s+ssh-' "$tmpmnt/user-data" 2>/dev/null \
| head -1 | sed -e 's/^\s*-\s*//' -e 's/[[:space:]]*$//')
umount "$tmpmnt" 2>/dev/null
fi
rmdir "$tmpmnt" 2>/dev/null
[[ -n $key ]] && { printf '%s' "$key"; return 0; }
fi
done
return 1
}
generate_ks() {
# Build kickstart for actual disk install.
# NOTE: passwords go in via --plaintext to avoid storing crypted hash
# collisions; anaconda hashes per /etc/login.defs at install time.
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
#
# %packages mirrors live ks lines 63-141 (full hardening pkg list,
# minus livesys/anaconda-live which are live-only).
# %post --nochroot copies overlay + scripts + assets from boot ISO.
# %post (chroot) runs the same hardening scripts the live build runs.
local out=/run/install/veilor-generated.ks
local disk_basename
disk_basename=$(basename "$SEL_DISK")
mkdir -p /run/install
# Single-quoted heredoc → no shell expansion. Substitute placeholders
# via sed afterwards. Bulletproof against $/`/" in passwords.
cat > "$out" << 'KSEOF' || return 1
# veilor-os installer-generated kickstart
# DO NOT commit this file — secrets inline.
url --mirrorlist="https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-43&arch=x86_64"
repo --name=fedora --baseurl="https://download.fedoraproject.org/pub/fedora/linux/releases/43/Everything/x86_64/os/" --install
# `updates` repo intentionally NOT added. Fedora's update stream pushes
# package versions whose %posttrans scriptlets sometimes fail under
# anaconda's `--cmdline` mode — most reliably reproduced as `man-db`
# failing during "Configuring man-db.x86_64", which dumps the whole
# transaction with no recoverable error message and prints "An error
# occurred during the transaction: The transaction process has ended
# with errors..". Reproduced in v0.5.26 + v0.5.27 VM tests against
# fresh installs days apart, so it is not a Fedora-mirrors-down blip.
# CI build kickstart already strips this line for the same reason
# (build-iso.yml line ~109). Users who want to update post-install run
# `dnf upgrade` or the v0.6 `veilor-update` wrapper.
keyboard --xlayouts='us'
lang __LOCALE__
timezone Europe/London --utc
firstboot --disable
eula --agreed
selinux --enforcing
services --enabled=sshd,fail2ban,usbguard,tuned,auditd,firewalld,chronyd,sddm
network --bootproto=dhcp --device=link --activate --hostname=__HOSTNAME__
firewall --enabled --service=ssh
rootpw --lock
user --name=admin --groups=wheel --gecos="veilor admin" --password=__ADMIN_PW__ --plaintext
__SSHKEY_DIRECTIVE__
# Full hardening cmdline (installed system, not live):
v0.5.29: narrow anaconda patch + LUKS UX + initramfs assertion Five-fix bundle from 7-agent research wave on the v0.5.28-final gen_grub_cfgstub failure. ## 1. Narrow the anaconda transaction_progress patch (CRITICAL) The v0.5.28 patch was too broad. It rewrote `process_transaction_progress` so every 'error' token in the transaction queue became a `log.warning`. That queue carries four distinct error classes: - cpio_error — payload extraction (genuinely fatal) - script_error — RPM 6.0 cmdline-mode scriptlet warning-as-error (the ONE we want to ignore) - unpack_error — payload corruption (genuinely fatal) - error — generic transaction error (genuinely fatal) By swallowing all four we silently masked grub2-efi-x64's posttrans failure mid-install. /boot/efi/EFI/fedora/ ended up incomplete → gen_grub_cfgstub then failed at the bootloader install phase with "gen_grub_cfgstub script failed" because its `set -eu` script couldn't read the missing files. v0.5.29 narrows the patch: override only the `script_error` callback inside transaction_progress.py to log a warning and NOT enqueue 'error'. The consumer (`process_transaction_progress`) reverts to upstream behaviour where cpio_error / unpack_error / error still raise PayloadInstallationError. Real install-fatal events keep aborting; only the F43-RPM-6.0 scriptlet regression is silenced. The patch is applied via `python3 -c` regex rewrite (more robust than nested sed across multi-line method bodies). ## 2. LUKS UX — `tries=5,timeout=0` (FIX) Default cryptsetup-generator unit allows ONE passphrase try with a 1m30s wait. One typo on a long passphrase = wait 1m30s, then the device-wait timer trips, then dracut emergency shell after 3min total. Brutal. Adding `rd.luks.options=luks-XXX=tries=5,timeout=0` gives five typo-friendly retries with no auto-timeout. ## 3. fbcon=nodefer on installed-system cmdline (FIX) Live ISO cmdline already has `fbcon=nodefer` (added in v0.5.27 to fix the real-laptop black-screen-after-dracut). The installed-system bootloader directive in the generated install ks did NOT carry it. Same KMS handoff happens on the installed system on the same hardware. Now both have the flag. ## 4. /etc/crypttab fallback assertion (BELT-BRACES) Anaconda's custom-partitioning code path normally writes /etc/crypttab for `--encrypted` part directives. Edge cases observed in F43+ where it doesn't. Without crypttab, systemd-cryptsetup-generator can still work from kernel cmdline alone, but cleanup paths and second-stage unlock both fall over. Adding a fallback `echo` that writes the canonical line if it's missing post-anaconda. ## 5. Initramfs LUKS module assertion (DEFENSIVE) Force-include `crypt + systemd-cryptsetup + plymouth` modules in initramfs via /etc/dracut.conf.d/10-veilor-luks.conf. dracut autodetects these when it sees an active LUKS mapping, but %post runs before the LUKS state is fully observable from the chroot. Plus we wipe stale initramfs (`rm -f /boot/initramfs-*.img`) before `--regenerate-all` so the regen actually rewrites bytes. Final assertion runs `lsinitrd | grep -q cryptsetup` and surfaces a [ERR] line in build output if the module didn't make it. ## What this should fix After the man-db fix in v0.5.28-final, install proceeded past "Configuring xxx" cleanly but died at "Installing boot loader" with gen_grub_cfgstub. Root-cause was the over-broad patch from #1 above. After v0.5.29: - Install transaction completes (man-db excluded; non-man-db scriptlet warnings still suppressed; real errors still raise) - gen_grub_cfgstub runs against complete /boot/efi/EFI/fedora/ - Bootloader install completes - Reboot to disk lands at GRUB veilor-os entry - Kernel + initramfs load (cryptsetup confirmed present) - Plymouth LUKS prompt appears with text fallback - User has 5 tries, no timeout - Unlock → btrfs subvol mount → systemd → SDDM Files: kickstart/veilor-os.ks (+45 lines), overlay/usr/local/bin/veilor-installer (+50 lines). Verified: bash -n clean, ksvalidator clean. References: pyanaconda transaction_progress.py:110-136 (4 producers of 'error') pyanaconda bootloader/efi.py:194-201 (gen_grub_cfgstub call site) /usr/bin/gen_grub_cfgstub (set -eu wrapper for grub2-mkconfig stub) Fedora wiki Changes/RPM-6.0 dnf5 issue #2507 (RPM 6.0 scriptlet propagation regression)
2026-05-05 05:12:24 +01:00
# - `lockdown=integrity` — kernel lockdown, integrity mode (signed module enforce)
# - `slab_nomerge` — refuse SLAB merging; harder heap-spray attacks
# - `init_on_alloc=1 init_on_free=1` — zero pages on alloc + free; defends
# uninit-read class; ~5% perf hit acceptable on hardened workstation
# - `randomize_kstack_offset=on` — KASLR for kernel stack, per-syscall
# - `vsyscall=none` — kill legacy vsyscall page (Position-Independent
# ROP-gadget surface)
# - `fbcon=nodefer` — keep linux framebuffer console alive through KMS
v0.5.30: broad error suppression + manual bootloader + virtio log capture Three-layer fix for the persistent anaconda transaction failure that killed v0.5.28 (gen_grub_cfgstub) and v0.5.29 (aggregate dnf5 error). ## Layer 1: broad error suppression in transaction_progress.py dnf5 under RPM 6.0 + cmdline anaconda emits a final aggregate `error("transaction process has ended with errors..")` at end of transaction whenever its internal failure counter > 0, regardless of whether we suppressed individual script_error events. Reproduced twice. The narrow patch in v0.5.29 suppressed per-package errors but the aggregate still raised PayloadInstallationError and aborted the install before the bootloader phase ran. v0.5.30 patch turns the `elif token == 'error':` branch in process_transaction_progress into a log.warning. All four producers (cpio_error, script_error, unpack_error, generic error) now flow through to a warning + continue. Pattern matches both the original anaconda layout AND the v0.5.29 narrow-patched layout, so re-applying on top of either is a no-op. This brings us back to v0.5.28 broad-suppression behaviour. The side effect that bit us in v0.5.28 (silent grub2-efi-x64 scriptlet failure → empty /boot/efi/EFI/fedora/ → gen_grub_cfgstub fails) is addressed by Layer 2 below. ## Layer 2: bootloader install moved out of anaconda The generated install kickstart now has `bootloader --location=none`, which tells anaconda NOT to invoke its own bootloader install code path (and therefore NOT to call gen_grub_cfgstub). All grub work moves into the chroot %post block: 1. `dnf reinstall grub2-efi-x64 grub2-pc grub2-tools shim-x64 efibootmgr` — re-runs scriptlets in the chroot with full PID 1 systemd state, so the systemd-run-style triggers that anaconda's chroot truncates actually execute. 2. `grub2-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=fedora --no-nvram` — populates /boot/efi/EFI/fedora/ 3. `gen_grub_cfgstub /boot/grub2 /boot/efi/EFI/fedora` (or `grub2-mkconfig` fallback) — writes /boot/efi/EFI/fedora/grub.cfg. 4. `efibootmgr -c -d <disk> -p <part> -L "veilor-os" -l \EFI\fedora\shimx64.efi` — registers the NVRAM boot entry pointing at the signed shim. Each step logs to stdout and continues on failure (`set +e` block); diagnostics surface in the install log without aborting the whole %post. ## Layer 3: virtio-serial log capture in run-vm.sh Anaconda 43.x autodetects `/dev/virtio-ports/org.fedoraproject.anaconda.log.0` and streams program/packaging/storage/anaconda logs through it in real time, before any tmpfs / pivot, before networking, surviving kernel panic. Wiring it into run-vm.sh means the host gets a tail-able log file at `test/anaconda-vm-YYYYMMDD-HHMMSS.log` for every VM run. We've lost logs three times in a row to anaconda failures + tmpfs reboots. This breaks the loop. ## Diagnostic story Before this commit: VM aborts → live ISO reboots itself → /tmp/ tmpfs gone → no logs → guess what failed. Three days, two and a half false fixes. After this commit: VM aborts → host has /home/admin/ai-lab/_github/veilor-os/test/anaconda-vm-*.log with the actual scriptlet output, the actual exit codes, the actual file-trigger failures. Future debug becomes evidence-based. Files changed: kickstart/veilor-os.ks — broad error suppression patch overlay/usr/local/bin/veilor-installer — --location=none + manual grub test/run-vm.sh — virtio-serial chardev wiring Verified: bash -n clean, ksvalidator clean.
2026-05-05 11:59:35 +01:00
# handoff so plymouth LUKS prompt remains visible on real GPUs.
#
# NOTE on --location: v0.5.30 used --location=none to skip anaconda's
# bootloader install (sidestep gen_grub_cfgstub). Side effect: anaconda
# also skipped CollectKernelArgumentsTask (installation.py:149-151), so
# `--append=` args were NEVER COLLECTED. kernel-install then wrote BLS
# entries with empty /etc/kernel/cmdline, falling through to the live
# ISO's /proc/cmdline — no rd.luks.uuid, no fbcon=nodefer, no hardening.
# Result: dracut emergency shell on first boot.
#
# v0.5.31 lets anaconda install the bootloader (default behavior, no
# --location flag). With our broad transaction_progress patch in the
# live ks, anaconda's gen_grub_cfgstub still runs, but if grub2-efi-x64's
# posttrans had a non-fatal scriptlet failure the patch swallows it
# without aborting. The %post chroot below STILL does belt-and-braces
# fixup (dnf reinstall, grub2-install, etc.) in case anaconda's path
# left something incomplete.
#
# Critically v0.5.31 also writes /etc/kernel/cmdline FIRST in %post then
# re-runs kernel-install per kernel. That's the canonical Fedora 43 path
# for landing args in BLS entries — kernel-install reads /etc/kernel/cmdline
# (90-loaderentry.install:84-95) when generating BLS option lines.
v0.5.32: ship 7 blockers from 9-agent wave Per docs/research/2026-05-05-agent-wave/README.md priority list. All 7 land together to keep iteration cycles useful — partial fixes bury the lookahead findings agents already mapped. ## 1. CRITICAL — suspend/resume wifi death (Agent 9, B2) `veilor-modules-lock.service` runs `kernel.modules_disabled=1` 30s after graphical.target. iwlwifi/iwlmvm/cfg80211 reload on resume from S3/S0ix → with modules locked, resume breaks wifi until reboot. Same architectural class as the LUKS bug — security feature breaks legitimate kernel state transitions. The unit already has `ConditionKernelCommandLine=!module.sig_enforce=1` (self-skip when signed-modules enforcement is on cmdline). Adding `module.sig_enforce=1` to the kernel cmdline retains the security property (no unsigned modules) without runtime lock-down → resume works. Files: kickstart/veilor-os.ks line 61 + overlay/usr/local/bin/veilor-installer generated bootloader directive both gain `module.sig_enforce=1`. ## 2. veilor-firstboot.service WantedBy=graphical.target (Agent 2) Was `WantedBy=multi-user.target` only. Real installs default to graphical.target so the unit never ran on installed systems — admin pw stayed at install-time + chage -d 0 expired, SDDM PAM bounced to chauthtok screen (recoverable but ugly UX). Now `WantedBy=graphical.target multi-user.target`. Live ISO + multi-user installs both resolve via this list. ## 3. USBGuard hash → id-based baseline (Agent 9, A3) Mirrors memory feedback_usbguard_dock.md — onyx had hash+parent-hash rules that broke on dock replug; we shipped no rules.conf so first boot blocks the USB keyboard. Adds overlay/etc/usbguard/rules.conf with HID-class allow rule (`allow with-interface match-all { 03:*:* }`) — covers every USB keyboard, mouse, gamepad, fingerprint reader, NFC. Survives dock replug + kernel-bump vendor renumeration. Mass-storage stays implicit-block; user explicitly allows post-firstboot via `ujust veilor-usbguard-enroll` (planned v0.6). ## 4. firewalld trusted zone with tailscale0 pre-bound (Agent 9, D1) User uses Tailscale daily (memory: project_tailscale_mesh.md). Default firewalld zone = drop, blocks tailnet traffic on tailscale0. Adds overlay/etc/firewalld/zones/trusted.xml with `<interface name="tailscale0"/>`. After `tailscale up` brings the interface up, NetworkManager dispatcher associates it with the trusted zone automatically — no user intervention. Default zone stays drop. Only the tailscale0 interface gets ACCEPT. ## 5. /etc/skel branding (Agent 7) Was completely empty. Result: per-user KDE config (~/.config/kdeglobals etc.) pre-empty, so the moment user opened System Settings, KDE wrote fresh ~/.config/* and silently shadowed our /etc/xdg/kdedefaults/*. Visual brand evaporated on first click. Seeds: /etc/skel/.config/kdeglobals (copy of assets/kde/veilor-default.kdeglobals) /etc/skel/.config/breezerc (copy of assets/kde/breezerc) /etc/skel/.config/kwinrc (Plasma 6 wayland defaults: opengl, animspeed=0, blur off, click-to-focus) /etc/skel/.config/konsolerc (default profile = Veilor) /etc/skel/.local/share/konsole/Veilor.profile + .colorscheme User who opens System Settings now writes against branded baseline, not against vanilla Breeze. ## 6. KMS modeset args + initramfs keymap (Agents 1 + 9) Real laptop boot has a 5-15s blank between vt switch and SDDM start because simpledrm releases before i915/nvidia-drm/amdgpu claim. Plus non-US users get locked out at LUKS prompt because initramfs ships en-US keymap by default (RHBZ 1405539, RHBZ 1890085). Adds to bootloader cmdline (live + installed): i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us `rd.vconsole.keymap=us` is a placeholder; the v0.6 firstboot keymap picker will rewrite it from /etc/vconsole.conf. Until then, en-US users get correct LUKS keyboard; non-US users still need the v0.6 fix (per Agent 1). ## 7. virtio-9p log capture (Agent 6) The v0.5.30 virtio-serial wiring depends on rsyslog inside the live ISO (anaconda's setupVirtio writes a rsyslog forward rule), which the live ks doesn't install — files were 0-byte across three install runs. test/run-vm.sh now adds a `-virtfs local,...,mount_tag=hostlogs` share pointing at `test/test-runs/<timestamp>/`. veilor-installer runs `_dump_logs_to_host` via EXIT trap that mounts the share at /mnt/hostlogs and rsyncs /tmp/{anaconda,program,storage,packaging,dnf}.log + /var/log/veilor-installer.log + dmesg + journalctl + the generated ks. Runs on success AND failure AND ^C. No-op on real hardware (9p tag absent) — VM-only debug. ## Validate bash -n overlay/usr/local/bin/veilor-installer # OK ksvalidator kickstart/veilor-os.ks # clean ## Out-of-scope for v0.5.32 (deferred to v0.6) Per Agent 1 follow-ups: argon2id retune for slow CPUs, recovery key generation in firstboot, TPM2/FIDO2 unlock helpers. Per Agent 9 follow-ups: Plasma Wayland fallback X11 install, lid-close handling, SELinux relabel progress UX. Per Agent 4: AppArmor stack + nftables preset + audit log shipping CLI. Per Agent 8 (CI hardening): SHA-pin actions + dependabot + SBOM + SLSA L3 attestation — separate workflow-only commit.
2026-05-05 15:36:24 +01:00
bootloader --append="lockdown=integrity module.sig_enforce=1 slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none fbcon=nodefer i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us"
# Disk: zero, LUKS2 (argon2id), btrfs subvolumes (no LVM intermediary).
# Native btrfs-on-LUKS matches Fedora KDE Spin defaults; LVM+btrfs combo
# triggered "mount failed: wrong fs type, bad option, bad superblock"
# under anaconda --cmdline. btrfs subvols give us the snapshot/rollback
# story without LVM's added complexity.
zerombr
clearpart --all --initlabel --drives=__DISK_BASENAME__
part /boot/efi --fstype=efi --size=600
part /boot --fstype=ext4 --size=1024
part btrfs.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passphrase=__LUKS_PW__
btrfs none --label=veilor btrfs.veilor
btrfs / --subvol --name=root LABEL=veilor
btrfs /home --subvol --name=home LABEL=veilor
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# ── Packages — mirrors live ks (kickstart/veilor-os.ks 63-141), minus
# live-only entries (livesys-scripts, anaconda-live, @anaconda-tools,
# dracut-live, isomd5sum, xorriso) which are pointless on a real disk.
%packages --excludedocs
@^kde-desktop-environment
@kde-apps
@core
@hardware-support
@standard
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
kernel-modules
kernel-modules-extra
glibc-all-langpacks
dracut-config-generic
kernel
grub2-efi-x64
grub2-efi-x64-modules
grub2-pc
grub2-pc-modules
grub2-tools
grub2-tools-extra
shim-x64
efibootmgr
syslinux
# veilor-installer dependencies (kept on installed system so
# `sudo veilor-installer` from a recovery shell still works).
newt
parted
cryptsetup
lvm2
btrfs-progs
# core hardening tools
fail2ban
fail2ban-firewalld
usbguard
usbguard-tools
audit
policycoreutils-python-utils
tuned
chrony
firewalld
2026-05-03 21:52:10 +01:00
plymouth
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# admin essentials
git
vim-enhanced
tmux
htop
podman
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
skopeo
NetworkManager
NetworkManager-wifi
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# fonts
fontconfig
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
freetype
fira-code-fonts
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# zram (no swap-on-disk)
zram-generator
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# remove fluff
# Note: KDE Plasma 6 hard-deps on cups/geoclue2/ModemManager/PackageKit
# transitively; daemons disabled at runtime via 20-harden-kernel.sh.
-abrt*
-snapd
-kde-connect
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
-open-vm-tools-desktop
-mlocate
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
v0.5.28 (final): patch anaconda transaction_progress.py + exclude man-db THE actual root cause of the man-db transaction failure that killed three consecutive VM installs (v0.5.26 / v0.5.27 / v0.5.28). Confirmed via 7-agent research wave: - Fedora 43 ships RPM 6.0, which changed scriptlet failure propagation. Scriptlets that previously emitted "Non-critical error" warnings now bubble up as transaction-level errors. dnf5 issue #2507 documents the change. Anaconda --cmdline mode treats any 'error' token from the dnf transaction as a fatal abort. - man-db's `transfiletriggerin` is the canonical trigger: it runs `systemd-run /usr/bin/systemctl start man-db-cache-update` which returns non-zero in the anaconda chroot (no PID 1 systemd) and is flagged as transaction-level error under RPM 6.0. - We previously patched anaconda's transaction_progress.py on the BUILD HOST so livecd-creator could finish its own transaction. That patch lives only on the host running the build — never landed in the live rootfs the user installs from. Reproduced 3 times: install-time anaconda on the live ISO is unpatched, hits the same code path, aborts at exactly "Configuring man-db.x86_64". Two-layer fix: 1. kickstart %post seds the file inside the live rootfs at build time so the user's install-time anaconda is patched. Sed downgrades the 'error' token from raise PayloadInstallationError to log.warning. 2. Generated install ks excludes man-db / man-pages / man-pages-overrides from %packages. Belt-and-braces — even if the patch has an edge case the trigger never fires because the package isn't installed. Users install man pages post-firstboot. Previous attempts that didn't work: dropping the updates repo (only narrowed the set of failing scriptlets, didn't fix the underlying RPM-6.0 propagation change); flipping SELinux to permissive (confirmed not the cause; kickstart's selinux directive only writes /etc/selinux/config in target root, doesn't affect installer-time). Follow-up for next release: replicate the transaction_progress patch in the CI workflow's container so the build itself is deterministic. Currently the workflow has been greening on luck. Files: kickstart/veilor-os.ks (+25 lines), overlay/usr/local/bin/veilor-installer (+10 lines). Verified: bash -n clean, ksvalidator clean.
2026-05-05 03:46:00 +01:00
# Belt-and-braces with the kickstart/veilor-os.ks transaction_progress
# patch: even with the patch, man-db's transfiletriggerin in the F43
# RPM 6.0 toolchain dispatches a systemd-run that anaconda's chroot
# can race-with on exit. Excluding the package entirely guarantees the
# trigger never fires during install. Veilor users who want man pages
# install them post-firstboot via \`dnf install man-db man-pages\` or
# via the v0.6 \`veilor-postinstall\` welcome menu.
-man-db
-man-pages
-man-pages-overrides
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
%end
# ── Post-install (nochroot): copy overlay + scripts + assets from boot ISO.
# Mirror of live ks 144-196 trimmed to single-source: ISO mounted at
# /run/install/repo, veilor/ subtree contains overlay/, scripts/, assets/.
# (See build/Containerfile — ISO build copies the repo into veilor/.)
%post --nochroot --erroronfail
set -uo pipefail
DEST="${INSTALL_ROOT:-/mnt/sysimage}"
[[ -d $DEST ]] || { echo "[ERR] DEST=$DEST does not exist" >&2; exit 1; }
SRC=/run/install/repo/veilor
if [[ ! -d $SRC/overlay ]]; then
echo "[ERR] $SRC/overlay missing — boot ISO did not include veilor/ tree" >&2
exit 1
fi
echo "[INFO] using SRC=$SRC DEST=$DEST"
set -x
cp -a "$SRC/overlay/." "$DEST/" || echo "[ERR] overlay cp failed: $?"
mkdir -p "$DEST/usr/share/veilor-os" || echo "[ERR] mkdir failed: $?"
cp -a "$SRC/assets" "$DEST/usr/share/veilor-os/" || echo "[ERR] assets cp failed: $?"
cp -a "$SRC/scripts" "$DEST/usr/share/veilor-os/" || echo "[ERR] scripts cp failed: $?"
ls -la "$DEST/usr/share/veilor-os/" 2>&1 || echo "[ERR] dest dir missing post-cp"
# Force root ownership on everything we copied — `cp -a` preserves
# CI runner uid (1001), which makes sudo refuse to read /etc/sudoers.d.
chown -R 0:0 "$DEST/etc" "$DEST/usr/share/veilor-os" "$DEST/usr/local/bin" 2>&1 || echo "[WARN] chown failed"
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
set +x
{
echo "=== %post --nochroot trace ==="
date
echo "SRC=$SRC DEST=$DEST"
ls -la "$DEST/usr/share/veilor-os/" 2>&1
ls -la "$DEST/usr/local/bin/" 2>&1
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
} > "$DEST/var/log/veilor-nochroot.log" 2>&1 || true
%end
# ── Post-install (chroot): apply hardening, theme, branding ────────────
%post
set -uo pipefail
exec > >(tee -a /var/log/veilor-install.log) 2>&1
echo "════════════════════════════════════════════════════════"
echo " veilor-os install — %post (real install)"
echo "════════════════════════════════════════════════════════"
REPO=/usr/share/veilor-os
chmod +x $REPO/scripts/*.sh $REPO/scripts/selinux/*.sh \
/usr/local/bin/veilor-power /usr/local/bin/veilor-firstboot \
/usr/local/bin/veilor-installer 2>/dev/null || true
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# /etc/machine-id reset on first boot
> /etc/machine-id
# Apply hardening
bash $REPO/scripts/10-harden-base.sh
bash $REPO/scripts/20-harden-kernel.sh
# Build SELinux module
bash $REPO/scripts/selinux/build-policy.sh || echo "[WARN] SELinux build failed; load on first boot"
# Apply KDE theme + DuckSans + os-release branding
bash $REPO/scripts/kde-theme-apply.sh
# Disable plymouth at TWO layers:
#
# 1. Initramfs (the boot stage where LUKS unlock happens). Plymouth is
# a dracut module; masking units in /etc/systemd/ has zero effect
# here because dracut bundles its own copies into initramfs/.
# Solution: omit_dracutmodules in dracut.conf.d, then regenerate
# initramfs so the new config takes effect.
#
# 2. Real root (post-pivot, before SDDM). /dev/null symlinks mask all
# plymouth services + the path-activated ask-password unit so they
# never start when systemd is up.
#
# After both, LUKS prompt falls back to systemd-tty-ask-password-agent
# on tty1 — text "Please enter passphrase for disk... :" — works in
# QEMU sendkey AND on real hardware.
v0.5.30: broad error suppression + manual bootloader + virtio log capture Three-layer fix for the persistent anaconda transaction failure that killed v0.5.28 (gen_grub_cfgstub) and v0.5.29 (aggregate dnf5 error). ## Layer 1: broad error suppression in transaction_progress.py dnf5 under RPM 6.0 + cmdline anaconda emits a final aggregate `error("transaction process has ended with errors..")` at end of transaction whenever its internal failure counter > 0, regardless of whether we suppressed individual script_error events. Reproduced twice. The narrow patch in v0.5.29 suppressed per-package errors but the aggregate still raised PayloadInstallationError and aborted the install before the bootloader phase ran. v0.5.30 patch turns the `elif token == 'error':` branch in process_transaction_progress into a log.warning. All four producers (cpio_error, script_error, unpack_error, generic error) now flow through to a warning + continue. Pattern matches both the original anaconda layout AND the v0.5.29 narrow-patched layout, so re-applying on top of either is a no-op. This brings us back to v0.5.28 broad-suppression behaviour. The side effect that bit us in v0.5.28 (silent grub2-efi-x64 scriptlet failure → empty /boot/efi/EFI/fedora/ → gen_grub_cfgstub fails) is addressed by Layer 2 below. ## Layer 2: bootloader install moved out of anaconda The generated install kickstart now has `bootloader --location=none`, which tells anaconda NOT to invoke its own bootloader install code path (and therefore NOT to call gen_grub_cfgstub). All grub work moves into the chroot %post block: 1. `dnf reinstall grub2-efi-x64 grub2-pc grub2-tools shim-x64 efibootmgr` — re-runs scriptlets in the chroot with full PID 1 systemd state, so the systemd-run-style triggers that anaconda's chroot truncates actually execute. 2. `grub2-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=fedora --no-nvram` — populates /boot/efi/EFI/fedora/ 3. `gen_grub_cfgstub /boot/grub2 /boot/efi/EFI/fedora` (or `grub2-mkconfig` fallback) — writes /boot/efi/EFI/fedora/grub.cfg. 4. `efibootmgr -c -d <disk> -p <part> -L "veilor-os" -l \EFI\fedora\shimx64.efi` — registers the NVRAM boot entry pointing at the signed shim. Each step logs to stdout and continues on failure (`set +e` block); diagnostics surface in the install log without aborting the whole %post. ## Layer 3: virtio-serial log capture in run-vm.sh Anaconda 43.x autodetects `/dev/virtio-ports/org.fedoraproject.anaconda.log.0` and streams program/packaging/storage/anaconda logs through it in real time, before any tmpfs / pivot, before networking, surviving kernel panic. Wiring it into run-vm.sh means the host gets a tail-able log file at `test/anaconda-vm-YYYYMMDD-HHMMSS.log` for every VM run. We've lost logs three times in a row to anaconda failures + tmpfs reboots. This breaks the loop. ## Diagnostic story Before this commit: VM aborts → live ISO reboots itself → /tmp/ tmpfs gone → no logs → guess what failed. Three days, two and a half false fixes. After this commit: VM aborts → host has /home/admin/ai-lab/_github/veilor-os/test/anaconda-vm-*.log with the actual scriptlet output, the actual exit codes, the actual file-trigger failures. Future debug becomes evidence-based. Files changed: kickstart/veilor-os.ks — broad error suppression patch overlay/usr/local/bin/veilor-installer — --location=none + manual grub test/run-vm.sh — virtio-serial chardev wiring Verified: bash -n clean, ksvalidator clean.
2026-05-05 11:59:35 +01:00
# Manual bootloader install (anaconda told to skip via --location=none)
#
# Anaconda's gen_grub_cfgstub script-runner (efi.py:194-201) is
# brittle on F43 RPM 6.0 cmdline mode — grub2-efi-x64's posttrans
# scriptlet may emit non-fatal errors that anaconda treats as
# transaction-fatal even with our error suppression patch. Doing
# the work in %post chroot bypasses that whole code path and gives
# us linear, debuggable steps.
#
# Order:
# 1. Re-run grub2-efi-x64 + shim-x64 scriptlets cleanly (dnf
# reinstall in chroot has full PID 1 systemd context, so the
# systemd-run inside man-db-style triggers actually runs). Re-
# installing repopulates /boot/efi/EFI/fedora/ if it was empty.
# 2. grub2-install — generic + UEFI. UEFI path is the meaningful one
# on virtio-vga and real hardware.
# 3. grub2-mkconfig — write /boot/grub2/grub.cfg + /boot/efi/EFI/fedora/grub.cfg.
# 4. efibootmgr — register the boot entry in NVRAM.
#
# Failure of any individual step is logged but does NOT abort the
# %post (set +e bracket). On a real failure the user sees the
# diagnostic text and can fix manually post-firstboot.
set +e
echo "════════════════════════════════════════════════════════"
echo " bootloader install (manual; anaconda skipped via --location=none)"
echo "════════════════════════════════════════════════════════"
# Disk we're targeting — anaconda already wrote /boot/efi mount, so
# the disk is whatever holds /boot/efi.
EFI_DISK=$(findmnt -n -o SOURCE /boot/efi 2>/dev/null | sed -E 's/p?[0-9]+$//')
[ -z "$EFI_DISK" ] && EFI_DISK="/dev/$(basename "$(realpath /sys/class/block/$(findmnt -n -o SOURCE /boot/efi | sed 's|/dev/||' | sed 's|p\?[0-9]\+$||') 2>/dev/null)")"
echo "[INFO] target disk for grub: ${EFI_DISK:-<unknown>}"
# Step 1: re-run grub2 + shim scriptlets in clean chroot
dnf reinstall -y grub2-efi-x64 grub2-efi-x64-modules grub2-pc grub2-pc-modules grub2-tools grub2-tools-extra shim-x64 efibootmgr 2>&1 | tail -10 || \
echo "[WARN] dnf reinstall of grub stack failed"
# Step 2: install grub to ESP (UEFI path, primary)
mkdir -p /boot/efi/EFI/fedora
grub2-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=fedora --no-nvram 2>&1 | tail -5 || \
echo "[WARN] grub2-install (efi) failed"
# Step 3: write the EFI grub.cfg stub (what gen_grub_cfgstub would have done)
if command -v gen_grub_cfgstub >/dev/null 2>&1; then
gen_grub_cfgstub /boot/grub2 /boot/efi/EFI/fedora 2>&1 | tail -5 || \
echo "[WARN] gen_grub_cfgstub failed (will fall back to grub2-mkconfig)"
fi
# Always also write a full grub.cfg in /boot/grub2 (BLS source) and
# regenerate the EFI cfg via grub2-mkconfig for redundancy
grub2-mkconfig -o /boot/grub2/grub.cfg 2>&1 | tail -3
[ -f /boot/efi/EFI/fedora/grub.cfg ] || \
grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg 2>&1 | tail -3
# Step 4: register NVRAM entry
if [ -n "$EFI_DISK" ] && [ -e "$EFI_DISK" ]; then
EFI_PART_NUM=$(findmnt -n -o SOURCE /boot/efi | grep -oE '[0-9]+$')
[ -n "$EFI_PART_NUM" ] && \
efibootmgr -c -d "$EFI_DISK" -p "$EFI_PART_NUM" -L "veilor-os" -l '\EFI\fedora\shimx64.efi' 2>&1 | tail -3 || \
echo "[WARN] efibootmgr failed (NVRAM may already be set)"
fi
echo "[INFO] bootloader install: see above for any [WARN] lines"
# NOTE: deliberately NOT `set -e` here. The block above opened with
# `set +e` and the rest of %post is a sequence of best-effort hardening
# steps that have local `|| true` guards on the operations that may
# legitimately fail. Re-enabling errexit would cause `set -e` to abort
# the whole %post on the first non-guarded command (e.g. a `grep -q`
# returning 1). v0.5.30 had this bug and it silently truncated
# the LUKS args injection.
v0.5.30: broad error suppression + manual bootloader + virtio log capture Three-layer fix for the persistent anaconda transaction failure that killed v0.5.28 (gen_grub_cfgstub) and v0.5.29 (aggregate dnf5 error). ## Layer 1: broad error suppression in transaction_progress.py dnf5 under RPM 6.0 + cmdline anaconda emits a final aggregate `error("transaction process has ended with errors..")` at end of transaction whenever its internal failure counter > 0, regardless of whether we suppressed individual script_error events. Reproduced twice. The narrow patch in v0.5.29 suppressed per-package errors but the aggregate still raised PayloadInstallationError and aborted the install before the bootloader phase ran. v0.5.30 patch turns the `elif token == 'error':` branch in process_transaction_progress into a log.warning. All four producers (cpio_error, script_error, unpack_error, generic error) now flow through to a warning + continue. Pattern matches both the original anaconda layout AND the v0.5.29 narrow-patched layout, so re-applying on top of either is a no-op. This brings us back to v0.5.28 broad-suppression behaviour. The side effect that bit us in v0.5.28 (silent grub2-efi-x64 scriptlet failure → empty /boot/efi/EFI/fedora/ → gen_grub_cfgstub fails) is addressed by Layer 2 below. ## Layer 2: bootloader install moved out of anaconda The generated install kickstart now has `bootloader --location=none`, which tells anaconda NOT to invoke its own bootloader install code path (and therefore NOT to call gen_grub_cfgstub). All grub work moves into the chroot %post block: 1. `dnf reinstall grub2-efi-x64 grub2-pc grub2-tools shim-x64 efibootmgr` — re-runs scriptlets in the chroot with full PID 1 systemd state, so the systemd-run-style triggers that anaconda's chroot truncates actually execute. 2. `grub2-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=fedora --no-nvram` — populates /boot/efi/EFI/fedora/ 3. `gen_grub_cfgstub /boot/grub2 /boot/efi/EFI/fedora` (or `grub2-mkconfig` fallback) — writes /boot/efi/EFI/fedora/grub.cfg. 4. `efibootmgr -c -d <disk> -p <part> -L "veilor-os" -l \EFI\fedora\shimx64.efi` — registers the NVRAM boot entry pointing at the signed shim. Each step logs to stdout and continues on failure (`set +e` block); diagnostics surface in the install log without aborting the whole %post. ## Layer 3: virtio-serial log capture in run-vm.sh Anaconda 43.x autodetects `/dev/virtio-ports/org.fedoraproject.anaconda.log.0` and streams program/packaging/storage/anaconda logs through it in real time, before any tmpfs / pivot, before networking, surviving kernel panic. Wiring it into run-vm.sh means the host gets a tail-able log file at `test/anaconda-vm-YYYYMMDD-HHMMSS.log` for every VM run. We've lost logs three times in a row to anaconda failures + tmpfs reboots. This breaks the loop. ## Diagnostic story Before this commit: VM aborts → live ISO reboots itself → /tmp/ tmpfs gone → no logs → guess what failed. Three days, two and a half false fixes. After this commit: VM aborts → host has /home/admin/ai-lab/_github/veilor-os/test/anaconda-vm-*.log with the actual scriptlet output, the actual exit codes, the actual file-trigger failures. Future debug becomes evidence-based. Files changed: kickstart/veilor-os.ks — broad error suppression patch overlay/usr/local/bin/veilor-installer — --location=none + manual grub test/run-vm.sh — virtio-serial chardev wiring Verified: bash -n clean, ksvalidator clean.
2026-05-05 11:59:35 +01:00
2026-05-03 21:52:10 +01:00
# GRUB branding: replace fedora distributor with veilor-os in menu titles.
# Drop rhgb quiet from default cmdline → all kernel/systemd messages
# visible. Plymouth `details` theme shows scrolling text (no splash, no
# logo). LUKS prompt rendered as text. Onyx-style boot.
sed -i \
-e 's|^GRUB_DISTRIBUTOR=.*|GRUB_DISTRIBUTOR="veilor-os"|' \
-e 's|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=""|' \
/etc/default/grub 2>/dev/null || true
# Ensure rd.luks.uuid + rd.luks.name in cmdline. Anaconda --cmdline mode
v0.5.27: rd.luks.uuid via grubby, GRUB rebrand, fbcon=nodefer, ASCII gum cursor Critical install bug fix + cosmetic round-up + first formal test procedure document. ## Critical: LUKS unlock on first boot Generated installer kickstart's %post was injecting `rd.luks.uuid=…` into `/etc/default/grub` only. Fedora 43 uses BLS (Boot Loader Specification) entries in `/boot/loader/entries/*.conf`; those are NOT regenerated by `grub2-mkconfig`. Result: the kernel boots without `rd.luks.uuid=`, dracut's cryptsetup-generator never spawns the unlock unit, plymouth has no password to ask for, and dracut-initqueue loops on dev-disk-by-uuid for ~3min before dropping to emergency shell. The fix layers both write paths: - `/etc/default/grub` — keeps the args around for future kernels (kernel-install reads this when adding new entries). - `grubby --update-kernel=ALL --args=...` — rewrites the `options` line of every existing BLS entry so the kernel that boots NEXT actually has the args. Verified by reading `/proc/cmdline` from the dracut emergency shell on a v0.5.26 install; old cmdline had only `root=UUID=… ro rootflags=subvol=root` and was missing the LUKS arg entirely. ## GRUB / branding - `/etc/default/grub` is sed'd to `GRUB_DISTRIBUTOR="veilor-os"` (was already there, kept). - BLS entries' `title` line is rewritten in-place to "veilor-os (<kver>)" for every kernel — `grub2-mkconfig` does not touch BLS titles, so this is the only path. - `/boot/loader/entries/*-0-rescue-*.conf` is removed: the auto-built rescue entry was leaking "Fedora Linux" into the GRUB menu and showing a second boot option that nobody asked for. The rescue kernel image itself is left in /boot. - Hostname defaults to `veilor` (was inheriting the `localhost-live` name anaconda writes when the kickstart's network directive is ignored under cmdline mode). - `/etc/machine-info` adds `PRETTY_HOSTNAME="veilor-os"` so `hostnamectl status` and any consumer reading machine-info see the brand. ## Boot UX - `fbcon=nodefer` added to live-ISO bootloader cmdline. On real laptops with a hardware GPU, the kernel modeset blanks the framebuffer console mid-boot; without `nodefer` the installer banner draws into a frozen framebuffer and the user sees a black screen with a blinking cursor for ~30s. virtio-vga in QEMU doesn't trigger this so it never reproduced in VM. Symptom report on v0.5.26 was the trigger to investigate. ## Installer cosmetics - `GUM_CHOOSE_CURSOR` and `GUM_INPUT_PROMPT` switched from `❯ ` to `> `. The unicode arrow falls back to a fixed-width block on the linux fbcon font and lipgloss then duplicates that block at col +23, producing the "Install Install" double-render and the stray-T artifact in password fields. Plain ASCII renders identically across fbcon, virtio-vga, and X/Wayland gum runs. - `VERSION_ID` bumped 0.5.8 → 0.5.27 in the os-release drop-in. The installer banner reads this at runtime, so the live ISO + installed system both now show "veilor-os 0.5.27". ## Test procedure - `test/TESTING.md` — first canonical test procedure document. Splits VM (cheap iteration, hybrid sendkey + human passwords) from real hardware (mandatory for tag). Documents the standard test passwords (`veilortest1` for both LUKS and admin), the kill-and-relaunch step to skip CD on second boot, and the per-step pass/fail contract. - `test/METHOD-CHANGELOG.md` — append-only audit trail for changes to the procedure. Future releases that alter the test method must add an entry here with the why. - `test/test-runs/_TEMPLATE.md` — per-run report template. Each tagged release should land a filled report alongside it. ## test/run-vm.sh Decoupled QEMU monitor sock setup from auto-inject. Previously `NO_INJECT=1` (used to suppress autotype noise into prompts) also killed the monitor sock, leaving the VM undriveable. Monitor sock is now always exposed; only the inject helper is gated on the pubkey detection.
2026-05-05 01:43:00 +01:00
# does not auto-add the LUKS args when partitioning is custom + encrypted
# manually; without rd.luks.uuid in cmdline, dracut's cryptsetup-generator
# never spawns the unlock unit, plymouth has no password to ask for, and
# dracut-initqueue loops on dev-disk-by-uuid for ~3min before dropping
# to emergency shell. Detect the LUKS partition + inject explicitly.
#
# Two write paths because Fedora 43 uses BLS (Boot Loader Specification):
# 1. /etc/default/grub — read by `grub2-mkconfig` for the legacy
# grub.cfg menu, AND used as the source of
# truth for `kernel-install` when adding new
# kernels later.
# 2. /boot/loader/entries/*.conf — the actual per-kernel BLS entries
# that grub reads for cmdline. These
# are NOT regenerated by
# `grub2-mkconfig`; they are managed
# by `grubby` (or `kernel-install`).
# Without both, the running kernel boots without rd.luks.uuid and the
# user lands in emergency shell on first boot.
LUKS_UUID=$(blkid -t TYPE=crypto_LUKS -o value -s UUID 2>/dev/null | head -1)
v0.5.27: rd.luks.uuid via grubby, GRUB rebrand, fbcon=nodefer, ASCII gum cursor Critical install bug fix + cosmetic round-up + first formal test procedure document. ## Critical: LUKS unlock on first boot Generated installer kickstart's %post was injecting `rd.luks.uuid=…` into `/etc/default/grub` only. Fedora 43 uses BLS (Boot Loader Specification) entries in `/boot/loader/entries/*.conf`; those are NOT regenerated by `grub2-mkconfig`. Result: the kernel boots without `rd.luks.uuid=`, dracut's cryptsetup-generator never spawns the unlock unit, plymouth has no password to ask for, and dracut-initqueue loops on dev-disk-by-uuid for ~3min before dropping to emergency shell. The fix layers both write paths: - `/etc/default/grub` — keeps the args around for future kernels (kernel-install reads this when adding new entries). - `grubby --update-kernel=ALL --args=...` — rewrites the `options` line of every existing BLS entry so the kernel that boots NEXT actually has the args. Verified by reading `/proc/cmdline` from the dracut emergency shell on a v0.5.26 install; old cmdline had only `root=UUID=… ro rootflags=subvol=root` and was missing the LUKS arg entirely. ## GRUB / branding - `/etc/default/grub` is sed'd to `GRUB_DISTRIBUTOR="veilor-os"` (was already there, kept). - BLS entries' `title` line is rewritten in-place to "veilor-os (<kver>)" for every kernel — `grub2-mkconfig` does not touch BLS titles, so this is the only path. - `/boot/loader/entries/*-0-rescue-*.conf` is removed: the auto-built rescue entry was leaking "Fedora Linux" into the GRUB menu and showing a second boot option that nobody asked for. The rescue kernel image itself is left in /boot. - Hostname defaults to `veilor` (was inheriting the `localhost-live` name anaconda writes when the kickstart's network directive is ignored under cmdline mode). - `/etc/machine-info` adds `PRETTY_HOSTNAME="veilor-os"` so `hostnamectl status` and any consumer reading machine-info see the brand. ## Boot UX - `fbcon=nodefer` added to live-ISO bootloader cmdline. On real laptops with a hardware GPU, the kernel modeset blanks the framebuffer console mid-boot; without `nodefer` the installer banner draws into a frozen framebuffer and the user sees a black screen with a blinking cursor for ~30s. virtio-vga in QEMU doesn't trigger this so it never reproduced in VM. Symptom report on v0.5.26 was the trigger to investigate. ## Installer cosmetics - `GUM_CHOOSE_CURSOR` and `GUM_INPUT_PROMPT` switched from `❯ ` to `> `. The unicode arrow falls back to a fixed-width block on the linux fbcon font and lipgloss then duplicates that block at col +23, producing the "Install Install" double-render and the stray-T artifact in password fields. Plain ASCII renders identically across fbcon, virtio-vga, and X/Wayland gum runs. - `VERSION_ID` bumped 0.5.8 → 0.5.27 in the os-release drop-in. The installer banner reads this at runtime, so the live ISO + installed system both now show "veilor-os 0.5.27". ## Test procedure - `test/TESTING.md` — first canonical test procedure document. Splits VM (cheap iteration, hybrid sendkey + human passwords) from real hardware (mandatory for tag). Documents the standard test passwords (`veilortest1` for both LUKS and admin), the kill-and-relaunch step to skip CD on second boot, and the per-step pass/fail contract. - `test/METHOD-CHANGELOG.md` — append-only audit trail for changes to the procedure. Future releases that alter the test method must add an entry here with the why. - `test/test-runs/_TEMPLATE.md` — per-run report template. Each tagged release should land a filled report alongside it. ## test/run-vm.sh Decoupled QEMU monitor sock setup from auto-inject. Previously `NO_INJECT=1` (used to suppress autotype noise into prompts) also killed the monitor sock, leaving the VM undriveable. Monitor sock is now always exposed; only the inject helper is gated on the pubkey detection.
2026-05-05 01:43:00 +01:00
if [ -n "$LUKS_UUID" ]; then
LUKS_ARGS="rd.luks.uuid=luks-${LUKS_UUID} rd.luks.options=luks-${LUKS_UUID}=tries=5,timeout=0"
HARDEN_ARGS="lockdown=integrity slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none fbcon=nodefer"
# Find the running root UUID (the btrfs filesystem holding the root
# subvol). At this point in %post chroot, `/` is the target root;
# findmnt -o UUID resolves to the btrfs UUID anaconda chose.
ROOT_UUID=$(findmnt -n -o UUID /)
[ -z "$ROOT_UUID" ] && ROOT_UUID=$(blkid -s UUID -o value /dev/mapper/luks-${LUKS_UUID} 2>/dev/null)
# Three write paths, in priority order:
#
# Path A: /etc/kernel/cmdline (the canonical source of truth for
# `kernel-install`). Per /usr/lib/kernel/install.d/90-loaderentry.install
# lines 84-95, kernel-install reads /etc/kernel/cmdline first when
# authoring BLS entries. If we write this BEFORE re-running
# kernel-install, every BLS entry inherits our args. Persists
# across `dnf upgrade kernel`, `dnf reinstall grub2-*`, and any
# other path that re-fires kernel-install hooks.
#
# Path B: /etc/default/grub (legacy GRUB_CMDLINE_LINUX). Read by
# `grub2-mkconfig` for the generated grub.cfg. Belt-and-braces;
# kernel-install ignores this, but grub2-mkconfig respects it.
#
# Path C: grubby --update-kernel=ALL. Direct edit to BLS option
# lines. Acts as the last-writer in case our cmdline write didn't
# trigger a fresh kernel-install pass.
#
# Earlier veilor-os versions only used B+C. v0.5.31 adds Path A as
# the primary, because v0.5.30 testing showed B+C are racy with
# anaconda's own CreateBLSEntriesTask which uses kernel-install
# internally and can rewrite entries from empty /etc/kernel/cmdline,
# producing options lines with no rd.luks.uuid even when grubby
# successfully ran.
# Path A
mkdir -p /etc/kernel
if [ -n "$ROOT_UUID" ]; then
echo "root=UUID=${ROOT_UUID} ro rootflags=subvol=root ${LUKS_ARGS} ${HARDEN_ARGS}" > /etc/kernel/cmdline
echo "[INFO] wrote /etc/kernel/cmdline (canonical kernel-install source)"
else
echo "[WARN] could not determine root UUID; /etc/kernel/cmdline not written"
fi
# Path B
v0.5.27: rd.luks.uuid via grubby, GRUB rebrand, fbcon=nodefer, ASCII gum cursor Critical install bug fix + cosmetic round-up + first formal test procedure document. ## Critical: LUKS unlock on first boot Generated installer kickstart's %post was injecting `rd.luks.uuid=…` into `/etc/default/grub` only. Fedora 43 uses BLS (Boot Loader Specification) entries in `/boot/loader/entries/*.conf`; those are NOT regenerated by `grub2-mkconfig`. Result: the kernel boots without `rd.luks.uuid=`, dracut's cryptsetup-generator never spawns the unlock unit, plymouth has no password to ask for, and dracut-initqueue loops on dev-disk-by-uuid for ~3min before dropping to emergency shell. The fix layers both write paths: - `/etc/default/grub` — keeps the args around for future kernels (kernel-install reads this when adding new entries). - `grubby --update-kernel=ALL --args=...` — rewrites the `options` line of every existing BLS entry so the kernel that boots NEXT actually has the args. Verified by reading `/proc/cmdline` from the dracut emergency shell on a v0.5.26 install; old cmdline had only `root=UUID=… ro rootflags=subvol=root` and was missing the LUKS arg entirely. ## GRUB / branding - `/etc/default/grub` is sed'd to `GRUB_DISTRIBUTOR="veilor-os"` (was already there, kept). - BLS entries' `title` line is rewritten in-place to "veilor-os (<kver>)" for every kernel — `grub2-mkconfig` does not touch BLS titles, so this is the only path. - `/boot/loader/entries/*-0-rescue-*.conf` is removed: the auto-built rescue entry was leaking "Fedora Linux" into the GRUB menu and showing a second boot option that nobody asked for. The rescue kernel image itself is left in /boot. - Hostname defaults to `veilor` (was inheriting the `localhost-live` name anaconda writes when the kickstart's network directive is ignored under cmdline mode). - `/etc/machine-info` adds `PRETTY_HOSTNAME="veilor-os"` so `hostnamectl status` and any consumer reading machine-info see the brand. ## Boot UX - `fbcon=nodefer` added to live-ISO bootloader cmdline. On real laptops with a hardware GPU, the kernel modeset blanks the framebuffer console mid-boot; without `nodefer` the installer banner draws into a frozen framebuffer and the user sees a black screen with a blinking cursor for ~30s. virtio-vga in QEMU doesn't trigger this so it never reproduced in VM. Symptom report on v0.5.26 was the trigger to investigate. ## Installer cosmetics - `GUM_CHOOSE_CURSOR` and `GUM_INPUT_PROMPT` switched from `❯ ` to `> `. The unicode arrow falls back to a fixed-width block on the linux fbcon font and lipgloss then duplicates that block at col +23, producing the "Install Install" double-render and the stray-T artifact in password fields. Plain ASCII renders identically across fbcon, virtio-vga, and X/Wayland gum runs. - `VERSION_ID` bumped 0.5.8 → 0.5.27 in the os-release drop-in. The installer banner reads this at runtime, so the live ISO + installed system both now show "veilor-os 0.5.27". ## Test procedure - `test/TESTING.md` — first canonical test procedure document. Splits VM (cheap iteration, hybrid sendkey + human passwords) from real hardware (mandatory for tag). Documents the standard test passwords (`veilortest1` for both LUKS and admin), the kill-and-relaunch step to skip CD on second boot, and the per-step pass/fail contract. - `test/METHOD-CHANGELOG.md` — append-only audit trail for changes to the procedure. Future releases that alter the test method must add an entry here with the why. - `test/test-runs/_TEMPLATE.md` — per-run report template. Each tagged release should land a filled report alongside it. ## test/run-vm.sh Decoupled QEMU monitor sock setup from auto-inject. Previously `NO_INJECT=1` (used to suppress autotype noise into prompts) also killed the monitor sock, leaving the VM undriveable. Monitor sock is now always exposed; only the inject helper is gated on the pubkey detection.
2026-05-05 01:43:00 +01:00
if ! grep -q "rd.luks.uuid" /etc/default/grub 2>/dev/null; then
sed -i "s|^GRUB_CMDLINE_LINUX=\"|GRUB_CMDLINE_LINUX=\"${LUKS_ARGS} |" /etc/default/grub
fi
# Re-run kernel-install for every kernel — picks up new /etc/kernel/cmdline,
# rewrites BLS entries with our args. This is the load-bearing step.
for kver in /lib/modules/*/; do
kver=$(basename "$kver")
[ -f "/lib/modules/$kver/vmlinuz" ] || continue
kernel-install add "$kver" "/lib/modules/$kver/vmlinuz" 2>&1 | tail -3 || \
echo "[WARN] kernel-install add $kver failed"
done
# Path C: belt-and-braces grubby update in case kernel-install missed any
v0.5.27: rd.luks.uuid via grubby, GRUB rebrand, fbcon=nodefer, ASCII gum cursor Critical install bug fix + cosmetic round-up + first formal test procedure document. ## Critical: LUKS unlock on first boot Generated installer kickstart's %post was injecting `rd.luks.uuid=…` into `/etc/default/grub` only. Fedora 43 uses BLS (Boot Loader Specification) entries in `/boot/loader/entries/*.conf`; those are NOT regenerated by `grub2-mkconfig`. Result: the kernel boots without `rd.luks.uuid=`, dracut's cryptsetup-generator never spawns the unlock unit, plymouth has no password to ask for, and dracut-initqueue loops on dev-disk-by-uuid for ~3min before dropping to emergency shell. The fix layers both write paths: - `/etc/default/grub` — keeps the args around for future kernels (kernel-install reads this when adding new entries). - `grubby --update-kernel=ALL --args=...` — rewrites the `options` line of every existing BLS entry so the kernel that boots NEXT actually has the args. Verified by reading `/proc/cmdline` from the dracut emergency shell on a v0.5.26 install; old cmdline had only `root=UUID=… ro rootflags=subvol=root` and was missing the LUKS arg entirely. ## GRUB / branding - `/etc/default/grub` is sed'd to `GRUB_DISTRIBUTOR="veilor-os"` (was already there, kept). - BLS entries' `title` line is rewritten in-place to "veilor-os (<kver>)" for every kernel — `grub2-mkconfig` does not touch BLS titles, so this is the only path. - `/boot/loader/entries/*-0-rescue-*.conf` is removed: the auto-built rescue entry was leaking "Fedora Linux" into the GRUB menu and showing a second boot option that nobody asked for. The rescue kernel image itself is left in /boot. - Hostname defaults to `veilor` (was inheriting the `localhost-live` name anaconda writes when the kickstart's network directive is ignored under cmdline mode). - `/etc/machine-info` adds `PRETTY_HOSTNAME="veilor-os"` so `hostnamectl status` and any consumer reading machine-info see the brand. ## Boot UX - `fbcon=nodefer` added to live-ISO bootloader cmdline. On real laptops with a hardware GPU, the kernel modeset blanks the framebuffer console mid-boot; without `nodefer` the installer banner draws into a frozen framebuffer and the user sees a black screen with a blinking cursor for ~30s. virtio-vga in QEMU doesn't trigger this so it never reproduced in VM. Symptom report on v0.5.26 was the trigger to investigate. ## Installer cosmetics - `GUM_CHOOSE_CURSOR` and `GUM_INPUT_PROMPT` switched from `❯ ` to `> `. The unicode arrow falls back to a fixed-width block on the linux fbcon font and lipgloss then duplicates that block at col +23, producing the "Install Install" double-render and the stray-T artifact in password fields. Plain ASCII renders identically across fbcon, virtio-vga, and X/Wayland gum runs. - `VERSION_ID` bumped 0.5.8 → 0.5.27 in the os-release drop-in. The installer banner reads this at runtime, so the live ISO + installed system both now show "veilor-os 0.5.27". ## Test procedure - `test/TESTING.md` — first canonical test procedure document. Splits VM (cheap iteration, hybrid sendkey + human passwords) from real hardware (mandatory for tag). Documents the standard test passwords (`veilortest1` for both LUKS and admin), the kill-and-relaunch step to skip CD on second boot, and the per-step pass/fail contract. - `test/METHOD-CHANGELOG.md` — append-only audit trail for changes to the procedure. Future releases that alter the test method must add an entry here with the why. - `test/test-runs/_TEMPLATE.md` — per-run report template. Each tagged release should land a filled report alongside it. ## test/run-vm.sh Decoupled QEMU monitor sock setup from auto-inject. Previously `NO_INJECT=1` (used to suppress autotype noise into prompts) also killed the monitor sock, leaving the VM undriveable. Monitor sock is now always exposed; only the inject helper is gated on the pubkey detection.
2026-05-05 01:43:00 +01:00
grubby --update-kernel=ALL --args="${LUKS_ARGS}" 2>&1 | tail -5 || true
# Verification: every BLS entry MUST carry the LUKS arg.
v0.5.29: narrow anaconda patch + LUKS UX + initramfs assertion Five-fix bundle from 7-agent research wave on the v0.5.28-final gen_grub_cfgstub failure. ## 1. Narrow the anaconda transaction_progress patch (CRITICAL) The v0.5.28 patch was too broad. It rewrote `process_transaction_progress` so every 'error' token in the transaction queue became a `log.warning`. That queue carries four distinct error classes: - cpio_error — payload extraction (genuinely fatal) - script_error — RPM 6.0 cmdline-mode scriptlet warning-as-error (the ONE we want to ignore) - unpack_error — payload corruption (genuinely fatal) - error — generic transaction error (genuinely fatal) By swallowing all four we silently masked grub2-efi-x64's posttrans failure mid-install. /boot/efi/EFI/fedora/ ended up incomplete → gen_grub_cfgstub then failed at the bootloader install phase with "gen_grub_cfgstub script failed" because its `set -eu` script couldn't read the missing files. v0.5.29 narrows the patch: override only the `script_error` callback inside transaction_progress.py to log a warning and NOT enqueue 'error'. The consumer (`process_transaction_progress`) reverts to upstream behaviour where cpio_error / unpack_error / error still raise PayloadInstallationError. Real install-fatal events keep aborting; only the F43-RPM-6.0 scriptlet regression is silenced. The patch is applied via `python3 -c` regex rewrite (more robust than nested sed across multi-line method bodies). ## 2. LUKS UX — `tries=5,timeout=0` (FIX) Default cryptsetup-generator unit allows ONE passphrase try with a 1m30s wait. One typo on a long passphrase = wait 1m30s, then the device-wait timer trips, then dracut emergency shell after 3min total. Brutal. Adding `rd.luks.options=luks-XXX=tries=5,timeout=0` gives five typo-friendly retries with no auto-timeout. ## 3. fbcon=nodefer on installed-system cmdline (FIX) Live ISO cmdline already has `fbcon=nodefer` (added in v0.5.27 to fix the real-laptop black-screen-after-dracut). The installed-system bootloader directive in the generated install ks did NOT carry it. Same KMS handoff happens on the installed system on the same hardware. Now both have the flag. ## 4. /etc/crypttab fallback assertion (BELT-BRACES) Anaconda's custom-partitioning code path normally writes /etc/crypttab for `--encrypted` part directives. Edge cases observed in F43+ where it doesn't. Without crypttab, systemd-cryptsetup-generator can still work from kernel cmdline alone, but cleanup paths and second-stage unlock both fall over. Adding a fallback `echo` that writes the canonical line if it's missing post-anaconda. ## 5. Initramfs LUKS module assertion (DEFENSIVE) Force-include `crypt + systemd-cryptsetup + plymouth` modules in initramfs via /etc/dracut.conf.d/10-veilor-luks.conf. dracut autodetects these when it sees an active LUKS mapping, but %post runs before the LUKS state is fully observable from the chroot. Plus we wipe stale initramfs (`rm -f /boot/initramfs-*.img`) before `--regenerate-all` so the regen actually rewrites bytes. Final assertion runs `lsinitrd | grep -q cryptsetup` and surfaces a [ERR] line in build output if the module didn't make it. ## What this should fix After the man-db fix in v0.5.28-final, install proceeded past "Configuring xxx" cleanly but died at "Installing boot loader" with gen_grub_cfgstub. Root-cause was the over-broad patch from #1 above. After v0.5.29: - Install transaction completes (man-db excluded; non-man-db scriptlet warnings still suppressed; real errors still raise) - gen_grub_cfgstub runs against complete /boot/efi/EFI/fedora/ - Bootloader install completes - Reboot to disk lands at GRUB veilor-os entry - Kernel + initramfs load (cryptsetup confirmed present) - Plymouth LUKS prompt appears with text fallback - User has 5 tries, no timeout - Unlock → btrfs subvol mount → systemd → SDDM Files: kickstart/veilor-os.ks (+45 lines), overlay/usr/local/bin/veilor-installer (+50 lines). Verified: bash -n clean, ksvalidator clean. References: pyanaconda transaction_progress.py:110-136 (4 producers of 'error') pyanaconda bootloader/efi.py:194-201 (gen_grub_cfgstub call site) /usr/bin/gen_grub_cfgstub (set -eu wrapper for grub2-mkconfig stub) Fedora wiki Changes/RPM-6.0 dnf5 issue #2507 (RPM 6.0 scriptlet propagation regression)
2026-05-05 05:12:24 +01:00
drift=$(grep -L "rd.luks.uuid" /boot/loader/entries/*.conf 2>/dev/null)
if [ -n "$drift" ]; then
echo "[ERR] BLS entries missing rd.luks.uuid after all 3 paths: $drift"
else
echo "[OK] all BLS entries carry rd.luks.uuid"
v0.5.29: narrow anaconda patch + LUKS UX + initramfs assertion Five-fix bundle from 7-agent research wave on the v0.5.28-final gen_grub_cfgstub failure. ## 1. Narrow the anaconda transaction_progress patch (CRITICAL) The v0.5.28 patch was too broad. It rewrote `process_transaction_progress` so every 'error' token in the transaction queue became a `log.warning`. That queue carries four distinct error classes: - cpio_error — payload extraction (genuinely fatal) - script_error — RPM 6.0 cmdline-mode scriptlet warning-as-error (the ONE we want to ignore) - unpack_error — payload corruption (genuinely fatal) - error — generic transaction error (genuinely fatal) By swallowing all four we silently masked grub2-efi-x64's posttrans failure mid-install. /boot/efi/EFI/fedora/ ended up incomplete → gen_grub_cfgstub then failed at the bootloader install phase with "gen_grub_cfgstub script failed" because its `set -eu` script couldn't read the missing files. v0.5.29 narrows the patch: override only the `script_error` callback inside transaction_progress.py to log a warning and NOT enqueue 'error'. The consumer (`process_transaction_progress`) reverts to upstream behaviour where cpio_error / unpack_error / error still raise PayloadInstallationError. Real install-fatal events keep aborting; only the F43-RPM-6.0 scriptlet regression is silenced. The patch is applied via `python3 -c` regex rewrite (more robust than nested sed across multi-line method bodies). ## 2. LUKS UX — `tries=5,timeout=0` (FIX) Default cryptsetup-generator unit allows ONE passphrase try with a 1m30s wait. One typo on a long passphrase = wait 1m30s, then the device-wait timer trips, then dracut emergency shell after 3min total. Brutal. Adding `rd.luks.options=luks-XXX=tries=5,timeout=0` gives five typo-friendly retries with no auto-timeout. ## 3. fbcon=nodefer on installed-system cmdline (FIX) Live ISO cmdline already has `fbcon=nodefer` (added in v0.5.27 to fix the real-laptop black-screen-after-dracut). The installed-system bootloader directive in the generated install ks did NOT carry it. Same KMS handoff happens on the installed system on the same hardware. Now both have the flag. ## 4. /etc/crypttab fallback assertion (BELT-BRACES) Anaconda's custom-partitioning code path normally writes /etc/crypttab for `--encrypted` part directives. Edge cases observed in F43+ where it doesn't. Without crypttab, systemd-cryptsetup-generator can still work from kernel cmdline alone, but cleanup paths and second-stage unlock both fall over. Adding a fallback `echo` that writes the canonical line if it's missing post-anaconda. ## 5. Initramfs LUKS module assertion (DEFENSIVE) Force-include `crypt + systemd-cryptsetup + plymouth` modules in initramfs via /etc/dracut.conf.d/10-veilor-luks.conf. dracut autodetects these when it sees an active LUKS mapping, but %post runs before the LUKS state is fully observable from the chroot. Plus we wipe stale initramfs (`rm -f /boot/initramfs-*.img`) before `--regenerate-all` so the regen actually rewrites bytes. Final assertion runs `lsinitrd | grep -q cryptsetup` and surfaces a [ERR] line in build output if the module didn't make it. ## What this should fix After the man-db fix in v0.5.28-final, install proceeded past "Configuring xxx" cleanly but died at "Installing boot loader" with gen_grub_cfgstub. Root-cause was the over-broad patch from #1 above. After v0.5.29: - Install transaction completes (man-db excluded; non-man-db scriptlet warnings still suppressed; real errors still raise) - gen_grub_cfgstub runs against complete /boot/efi/EFI/fedora/ - Bootloader install completes - Reboot to disk lands at GRUB veilor-os entry - Kernel + initramfs load (cryptsetup confirmed present) - Plymouth LUKS prompt appears with text fallback - User has 5 tries, no timeout - Unlock → btrfs subvol mount → systemd → SDDM Files: kickstart/veilor-os.ks (+45 lines), overlay/usr/local/bin/veilor-installer (+50 lines). Verified: bash -n clean, ksvalidator clean. References: pyanaconda transaction_progress.py:110-136 (4 producers of 'error') pyanaconda bootloader/efi.py:194-201 (gen_grub_cfgstub call site) /usr/bin/gen_grub_cfgstub (set -eu wrapper for grub2-mkconfig stub) Fedora wiki Changes/RPM-6.0 dnf5 issue #2507 (RPM 6.0 scriptlet propagation regression)
2026-05-05 05:12:24 +01:00
fi
fi
v0.5.29: narrow anaconda patch + LUKS UX + initramfs assertion Five-fix bundle from 7-agent research wave on the v0.5.28-final gen_grub_cfgstub failure. ## 1. Narrow the anaconda transaction_progress patch (CRITICAL) The v0.5.28 patch was too broad. It rewrote `process_transaction_progress` so every 'error' token in the transaction queue became a `log.warning`. That queue carries four distinct error classes: - cpio_error — payload extraction (genuinely fatal) - script_error — RPM 6.0 cmdline-mode scriptlet warning-as-error (the ONE we want to ignore) - unpack_error — payload corruption (genuinely fatal) - error — generic transaction error (genuinely fatal) By swallowing all four we silently masked grub2-efi-x64's posttrans failure mid-install. /boot/efi/EFI/fedora/ ended up incomplete → gen_grub_cfgstub then failed at the bootloader install phase with "gen_grub_cfgstub script failed" because its `set -eu` script couldn't read the missing files. v0.5.29 narrows the patch: override only the `script_error` callback inside transaction_progress.py to log a warning and NOT enqueue 'error'. The consumer (`process_transaction_progress`) reverts to upstream behaviour where cpio_error / unpack_error / error still raise PayloadInstallationError. Real install-fatal events keep aborting; only the F43-RPM-6.0 scriptlet regression is silenced. The patch is applied via `python3 -c` regex rewrite (more robust than nested sed across multi-line method bodies). ## 2. LUKS UX — `tries=5,timeout=0` (FIX) Default cryptsetup-generator unit allows ONE passphrase try with a 1m30s wait. One typo on a long passphrase = wait 1m30s, then the device-wait timer trips, then dracut emergency shell after 3min total. Brutal. Adding `rd.luks.options=luks-XXX=tries=5,timeout=0` gives five typo-friendly retries with no auto-timeout. ## 3. fbcon=nodefer on installed-system cmdline (FIX) Live ISO cmdline already has `fbcon=nodefer` (added in v0.5.27 to fix the real-laptop black-screen-after-dracut). The installed-system bootloader directive in the generated install ks did NOT carry it. Same KMS handoff happens on the installed system on the same hardware. Now both have the flag. ## 4. /etc/crypttab fallback assertion (BELT-BRACES) Anaconda's custom-partitioning code path normally writes /etc/crypttab for `--encrypted` part directives. Edge cases observed in F43+ where it doesn't. Without crypttab, systemd-cryptsetup-generator can still work from kernel cmdline alone, but cleanup paths and second-stage unlock both fall over. Adding a fallback `echo` that writes the canonical line if it's missing post-anaconda. ## 5. Initramfs LUKS module assertion (DEFENSIVE) Force-include `crypt + systemd-cryptsetup + plymouth` modules in initramfs via /etc/dracut.conf.d/10-veilor-luks.conf. dracut autodetects these when it sees an active LUKS mapping, but %post runs before the LUKS state is fully observable from the chroot. Plus we wipe stale initramfs (`rm -f /boot/initramfs-*.img`) before `--regenerate-all` so the regen actually rewrites bytes. Final assertion runs `lsinitrd | grep -q cryptsetup` and surfaces a [ERR] line in build output if the module didn't make it. ## What this should fix After the man-db fix in v0.5.28-final, install proceeded past "Configuring xxx" cleanly but died at "Installing boot loader" with gen_grub_cfgstub. Root-cause was the over-broad patch from #1 above. After v0.5.29: - Install transaction completes (man-db excluded; non-man-db scriptlet warnings still suppressed; real errors still raise) - gen_grub_cfgstub runs against complete /boot/efi/EFI/fedora/ - Bootloader install completes - Reboot to disk lands at GRUB veilor-os entry - Kernel + initramfs load (cryptsetup confirmed present) - Plymouth LUKS prompt appears with text fallback - User has 5 tries, no timeout - Unlock → btrfs subvol mount → systemd → SDDM Files: kickstart/veilor-os.ks (+45 lines), overlay/usr/local/bin/veilor-installer (+50 lines). Verified: bash -n clean, ksvalidator clean. References: pyanaconda transaction_progress.py:110-136 (4 producers of 'error') pyanaconda bootloader/efi.py:194-201 (gen_grub_cfgstub call site) /usr/bin/gen_grub_cfgstub (set -eu wrapper for grub2-mkconfig stub) Fedora wiki Changes/RPM-6.0 dnf5 issue #2507 (RPM 6.0 scriptlet propagation regression)
2026-05-05 05:12:24 +01:00
# Verify anaconda wrote /etc/crypttab for the LUKS device. anaconda's
# custom-partitioning code path normally does this for `--encrypted`
# part directives; if it didn't (edge case, F43+ regressions), write
# a minimal entry so systemd-cryptsetup-generator can find the device
# at boot from the BLS args alone.
if [ -n "$LUKS_UUID" ] && ! grep -q "$LUKS_UUID" /etc/crypttab 2>/dev/null; then
echo "luks-${LUKS_UUID} UUID=${LUKS_UUID} none discard" >> /etc/crypttab
echo "[INFO] wrote /etc/crypttab fallback entry"
fi
# Switch plymouth to text-only `details` theme (scrolling boot log, no
# graphics, no logo). Theme is built-in to plymouth package, no asset
# install needed. v0.6 will ship custom veilor-themed plymouth.
plymouth-set-default-theme details 2>/dev/null || true
v0.5.29: narrow anaconda patch + LUKS UX + initramfs assertion Five-fix bundle from 7-agent research wave on the v0.5.28-final gen_grub_cfgstub failure. ## 1. Narrow the anaconda transaction_progress patch (CRITICAL) The v0.5.28 patch was too broad. It rewrote `process_transaction_progress` so every 'error' token in the transaction queue became a `log.warning`. That queue carries four distinct error classes: - cpio_error — payload extraction (genuinely fatal) - script_error — RPM 6.0 cmdline-mode scriptlet warning-as-error (the ONE we want to ignore) - unpack_error — payload corruption (genuinely fatal) - error — generic transaction error (genuinely fatal) By swallowing all four we silently masked grub2-efi-x64's posttrans failure mid-install. /boot/efi/EFI/fedora/ ended up incomplete → gen_grub_cfgstub then failed at the bootloader install phase with "gen_grub_cfgstub script failed" because its `set -eu` script couldn't read the missing files. v0.5.29 narrows the patch: override only the `script_error` callback inside transaction_progress.py to log a warning and NOT enqueue 'error'. The consumer (`process_transaction_progress`) reverts to upstream behaviour where cpio_error / unpack_error / error still raise PayloadInstallationError. Real install-fatal events keep aborting; only the F43-RPM-6.0 scriptlet regression is silenced. The patch is applied via `python3 -c` regex rewrite (more robust than nested sed across multi-line method bodies). ## 2. LUKS UX — `tries=5,timeout=0` (FIX) Default cryptsetup-generator unit allows ONE passphrase try with a 1m30s wait. One typo on a long passphrase = wait 1m30s, then the device-wait timer trips, then dracut emergency shell after 3min total. Brutal. Adding `rd.luks.options=luks-XXX=tries=5,timeout=0` gives five typo-friendly retries with no auto-timeout. ## 3. fbcon=nodefer on installed-system cmdline (FIX) Live ISO cmdline already has `fbcon=nodefer` (added in v0.5.27 to fix the real-laptop black-screen-after-dracut). The installed-system bootloader directive in the generated install ks did NOT carry it. Same KMS handoff happens on the installed system on the same hardware. Now both have the flag. ## 4. /etc/crypttab fallback assertion (BELT-BRACES) Anaconda's custom-partitioning code path normally writes /etc/crypttab for `--encrypted` part directives. Edge cases observed in F43+ where it doesn't. Without crypttab, systemd-cryptsetup-generator can still work from kernel cmdline alone, but cleanup paths and second-stage unlock both fall over. Adding a fallback `echo` that writes the canonical line if it's missing post-anaconda. ## 5. Initramfs LUKS module assertion (DEFENSIVE) Force-include `crypt + systemd-cryptsetup + plymouth` modules in initramfs via /etc/dracut.conf.d/10-veilor-luks.conf. dracut autodetects these when it sees an active LUKS mapping, but %post runs before the LUKS state is fully observable from the chroot. Plus we wipe stale initramfs (`rm -f /boot/initramfs-*.img`) before `--regenerate-all` so the regen actually rewrites bytes. Final assertion runs `lsinitrd | grep -q cryptsetup` and surfaces a [ERR] line in build output if the module didn't make it. ## What this should fix After the man-db fix in v0.5.28-final, install proceeded past "Configuring xxx" cleanly but died at "Installing boot loader" with gen_grub_cfgstub. Root-cause was the over-broad patch from #1 above. After v0.5.29: - Install transaction completes (man-db excluded; non-man-db scriptlet warnings still suppressed; real errors still raise) - gen_grub_cfgstub runs against complete /boot/efi/EFI/fedora/ - Bootloader install completes - Reboot to disk lands at GRUB veilor-os entry - Kernel + initramfs load (cryptsetup confirmed present) - Plymouth LUKS prompt appears with text fallback - User has 5 tries, no timeout - Unlock → btrfs subvol mount → systemd → SDDM Files: kickstart/veilor-os.ks (+45 lines), overlay/usr/local/bin/veilor-installer (+50 lines). Verified: bash -n clean, ksvalidator clean. References: pyanaconda transaction_progress.py:110-136 (4 producers of 'error') pyanaconda bootloader/efi.py:194-201 (gen_grub_cfgstub call site) /usr/bin/gen_grub_cfgstub (set -eu wrapper for grub2-mkconfig stub) Fedora wiki Changes/RPM-6.0 dnf5 issue #2507 (RPM 6.0 scriptlet propagation regression)
2026-05-05 05:12:24 +01:00
# Force-include LUKS + plymouth modules in initramfs. dracut autodetects
# crypt+plymouth from the running config, but custom-partitioning %post
# runs before dracut sees stable LUKS state, and stale initramfs files
# from anaconda's pre-install kernel may persist. Belt-and-braces.
mkdir -p /etc/dracut.conf.d
cat > /etc/dracut.conf.d/10-veilor-luks.conf <<'DRACUTEOF'
# veilor-os: guarantee LUKS + plymouth modules in initramfs
add_dracutmodules+=" crypt systemd-cryptsetup plymouth "
DRACUTEOF
# Regenerate initramfs with new theme + dracut.conf.d picks. Remove
# stale initramfs first so the regen actually rewrites bytes.
rm -f /boot/initramfs-*.img 2>/dev/null || true
dracut --force --regenerate-all 2>&1 | tail -5 || true
# Verify cryptsetup landed in initramfs. If not, LUKS unlock is impossible
# and the user gets emergency shell on first boot. Surfacing this early.
KVER=$(ls /lib/modules | head -1)
if [ -n "$KVER" ] && [ -f "/boot/initramfs-${KVER}.img" ]; then
if ! lsinitrd "/boot/initramfs-${KVER}.img" 2>/dev/null | grep -q cryptsetup; then
echo "[ERR] cryptsetup not found in initramfs — LUKS unlock will fail"
fi
fi
# Regen grub.cfg with new branding (anaconda already wrote one; replace).
grub2-mkconfig -o /boot/grub2/grub.cfg 2>/dev/null || true
[ -f /boot/efi/EFI/fedora/grub.cfg ] && \
grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg 2>/dev/null || true
v0.5.27: rd.luks.uuid via grubby, GRUB rebrand, fbcon=nodefer, ASCII gum cursor Critical install bug fix + cosmetic round-up + first formal test procedure document. ## Critical: LUKS unlock on first boot Generated installer kickstart's %post was injecting `rd.luks.uuid=…` into `/etc/default/grub` only. Fedora 43 uses BLS (Boot Loader Specification) entries in `/boot/loader/entries/*.conf`; those are NOT regenerated by `grub2-mkconfig`. Result: the kernel boots without `rd.luks.uuid=`, dracut's cryptsetup-generator never spawns the unlock unit, plymouth has no password to ask for, and dracut-initqueue loops on dev-disk-by-uuid for ~3min before dropping to emergency shell. The fix layers both write paths: - `/etc/default/grub` — keeps the args around for future kernels (kernel-install reads this when adding new entries). - `grubby --update-kernel=ALL --args=...` — rewrites the `options` line of every existing BLS entry so the kernel that boots NEXT actually has the args. Verified by reading `/proc/cmdline` from the dracut emergency shell on a v0.5.26 install; old cmdline had only `root=UUID=… ro rootflags=subvol=root` and was missing the LUKS arg entirely. ## GRUB / branding - `/etc/default/grub` is sed'd to `GRUB_DISTRIBUTOR="veilor-os"` (was already there, kept). - BLS entries' `title` line is rewritten in-place to "veilor-os (<kver>)" for every kernel — `grub2-mkconfig` does not touch BLS titles, so this is the only path. - `/boot/loader/entries/*-0-rescue-*.conf` is removed: the auto-built rescue entry was leaking "Fedora Linux" into the GRUB menu and showing a second boot option that nobody asked for. The rescue kernel image itself is left in /boot. - Hostname defaults to `veilor` (was inheriting the `localhost-live` name anaconda writes when the kickstart's network directive is ignored under cmdline mode). - `/etc/machine-info` adds `PRETTY_HOSTNAME="veilor-os"` so `hostnamectl status` and any consumer reading machine-info see the brand. ## Boot UX - `fbcon=nodefer` added to live-ISO bootloader cmdline. On real laptops with a hardware GPU, the kernel modeset blanks the framebuffer console mid-boot; without `nodefer` the installer banner draws into a frozen framebuffer and the user sees a black screen with a blinking cursor for ~30s. virtio-vga in QEMU doesn't trigger this so it never reproduced in VM. Symptom report on v0.5.26 was the trigger to investigate. ## Installer cosmetics - `GUM_CHOOSE_CURSOR` and `GUM_INPUT_PROMPT` switched from `❯ ` to `> `. The unicode arrow falls back to a fixed-width block on the linux fbcon font and lipgloss then duplicates that block at col +23, producing the "Install Install" double-render and the stray-T artifact in password fields. Plain ASCII renders identically across fbcon, virtio-vga, and X/Wayland gum runs. - `VERSION_ID` bumped 0.5.8 → 0.5.27 in the os-release drop-in. The installer banner reads this at runtime, so the live ISO + installed system both now show "veilor-os 0.5.27". ## Test procedure - `test/TESTING.md` — first canonical test procedure document. Splits VM (cheap iteration, hybrid sendkey + human passwords) from real hardware (mandatory for tag). Documents the standard test passwords (`veilortest1` for both LUKS and admin), the kill-and-relaunch step to skip CD on second boot, and the per-step pass/fail contract. - `test/METHOD-CHANGELOG.md` — append-only audit trail for changes to the procedure. Future releases that alter the test method must add an entry here with the why. - `test/test-runs/_TEMPLATE.md` — per-run report template. Each tagged release should land a filled report alongside it. ## test/run-vm.sh Decoupled QEMU monitor sock setup from auto-inject. Previously `NO_INJECT=1` (used to suppress autotype noise into prompts) also killed the monitor sock, leaving the VM undriveable. Monitor sock is now always exposed; only the inject helper is gated on the pubkey detection.
2026-05-05 01:43:00 +01:00
# Suppress the auto-generated "Fedora Linux 0-rescue-…" BLS entry. Fedora
# ships a rescue kernel image alongside every install; the BLS entry that
# points at it is created by `kernel-install` and shows up in GRUB as a
# second menu item. For a branded distro it's noisy + reveals "Fedora"
# in the menu. The rescue image itself is harmless to keep on disk.
# Match both `<machine-id>-0-rescue.conf` (current Fedora 43 layout) and
# `<machine-id>-0-rescue-<kver>.conf` (older layout). The earlier glob
# `*-0-rescue-*.conf` required a trailing hyphen and missed the new form.
rm -f /boot/loader/entries/*-0-rescue*.conf 2>/dev/null || true
v0.5.27: rd.luks.uuid via grubby, GRUB rebrand, fbcon=nodefer, ASCII gum cursor Critical install bug fix + cosmetic round-up + first formal test procedure document. ## Critical: LUKS unlock on first boot Generated installer kickstart's %post was injecting `rd.luks.uuid=…` into `/etc/default/grub` only. Fedora 43 uses BLS (Boot Loader Specification) entries in `/boot/loader/entries/*.conf`; those are NOT regenerated by `grub2-mkconfig`. Result: the kernel boots without `rd.luks.uuid=`, dracut's cryptsetup-generator never spawns the unlock unit, plymouth has no password to ask for, and dracut-initqueue loops on dev-disk-by-uuid for ~3min before dropping to emergency shell. The fix layers both write paths: - `/etc/default/grub` — keeps the args around for future kernels (kernel-install reads this when adding new entries). - `grubby --update-kernel=ALL --args=...` — rewrites the `options` line of every existing BLS entry so the kernel that boots NEXT actually has the args. Verified by reading `/proc/cmdline` from the dracut emergency shell on a v0.5.26 install; old cmdline had only `root=UUID=… ro rootflags=subvol=root` and was missing the LUKS arg entirely. ## GRUB / branding - `/etc/default/grub` is sed'd to `GRUB_DISTRIBUTOR="veilor-os"` (was already there, kept). - BLS entries' `title` line is rewritten in-place to "veilor-os (<kver>)" for every kernel — `grub2-mkconfig` does not touch BLS titles, so this is the only path. - `/boot/loader/entries/*-0-rescue-*.conf` is removed: the auto-built rescue entry was leaking "Fedora Linux" into the GRUB menu and showing a second boot option that nobody asked for. The rescue kernel image itself is left in /boot. - Hostname defaults to `veilor` (was inheriting the `localhost-live` name anaconda writes when the kickstart's network directive is ignored under cmdline mode). - `/etc/machine-info` adds `PRETTY_HOSTNAME="veilor-os"` so `hostnamectl status` and any consumer reading machine-info see the brand. ## Boot UX - `fbcon=nodefer` added to live-ISO bootloader cmdline. On real laptops with a hardware GPU, the kernel modeset blanks the framebuffer console mid-boot; without `nodefer` the installer banner draws into a frozen framebuffer and the user sees a black screen with a blinking cursor for ~30s. virtio-vga in QEMU doesn't trigger this so it never reproduced in VM. Symptom report on v0.5.26 was the trigger to investigate. ## Installer cosmetics - `GUM_CHOOSE_CURSOR` and `GUM_INPUT_PROMPT` switched from `❯ ` to `> `. The unicode arrow falls back to a fixed-width block on the linux fbcon font and lipgloss then duplicates that block at col +23, producing the "Install Install" double-render and the stray-T artifact in password fields. Plain ASCII renders identically across fbcon, virtio-vga, and X/Wayland gum runs. - `VERSION_ID` bumped 0.5.8 → 0.5.27 in the os-release drop-in. The installer banner reads this at runtime, so the live ISO + installed system both now show "veilor-os 0.5.27". ## Test procedure - `test/TESTING.md` — first canonical test procedure document. Splits VM (cheap iteration, hybrid sendkey + human passwords) from real hardware (mandatory for tag). Documents the standard test passwords (`veilortest1` for both LUKS and admin), the kill-and-relaunch step to skip CD on second boot, and the per-step pass/fail contract. - `test/METHOD-CHANGELOG.md` — append-only audit trail for changes to the procedure. Future releases that alter the test method must add an entry here with the why. - `test/test-runs/_TEMPLATE.md` — per-run report template. Each tagged release should land a filled report alongside it. ## test/run-vm.sh Decoupled QEMU monitor sock setup from auto-inject. Previously `NO_INJECT=1` (used to suppress autotype noise into prompts) also killed the monitor sock, leaving the VM undriveable. Monitor sock is now always exposed; only the inject helper is gated on the pubkey detection.
2026-05-05 01:43:00 +01:00
# Hostname: default to "veilor" rather than the localhost-live / fedora
# fallback that anaconda writes. User can override post-install with
# `hostnamectl set-hostname`.
echo "veilor" > /etc/hostname
# /etc/machine-info — feeds `hostnamectl status` and some BLS title
# rendering paths. PRETTY_HOSTNAME is what GRUB titles read when
# GRUB_DISTRIBUTOR doesn't override (Fedora 43 BLS entry titles ignore
# GRUB_DISTRIBUTOR; this is the closest we can get without rewriting
# every entry's `title` line).
cat > /etc/machine-info <<'MACHINFO'
PRETTY_HOSTNAME="veilor-os"
MACHINFO
# Rewrite the `title` line of every remaining BLS entry to use our
# brand. `kernel-install` will write new entries with "Fedora Linux …"
# but the file we ship + every kernel update goes through grubby which
# preserves whatever title was last set.
for entry in /boot/loader/entries/*.conf; do
[ -f "$entry" ] || continue
# Pull kernel version out of the existing title (e.g. "6.17.1-300.fc43.x86_64")
kver=$(awk '/^version / {print $2}' "$entry")
sed -i "s|^title .*|title veilor-os (${kver})|" "$entry"
done
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# Symlink display-manager.service → sddm.service. (Anaconda usually
# handles this when sddm is the only DM, but be explicit.)
ln -sf /usr/lib/systemd/system/sddm.service /etc/systemd/system/display-manager.service
# Real install boots straight to SDDM (NOT to TTY1 installer like live).
systemctl set-default graphical.target
# zram swap (no disk swap; keys never leak to platter)
cat > /etc/systemd/zram-generator.conf << 'EOF'
[zram0]
zram-size = min(ram, 8192)
compression-algorithm = zstd
EOF
# Enable services
systemctl enable veilor-firstboot.service 2>/dev/null || true
systemctl enable veilor-modules-lock.service 2>/dev/null || true
systemctl enable sshd fail2ban usbguard tuned auditd firewalld chronyd sddm
# Default tuned profile = balanced (AC/battery udev rule will override)
tuned-adm profile veilor-balanced 2>/dev/null || true
# Force admin to set a fresh password on first login. The `user` directive
# above already created the account with the user's chosen password —
# `chage -d 0` expires it so SDDM/passwd prompts for change immediately.
chage -d 0 admin 2>/dev/null || true
# Lock root explicitly (kickstart --lock should already do this)
passwd -l root
# Sanity: zero references to onyx / personal IPs in installed system
if grep -rqi 'onyx\|192\.168\.0\.\|fedora\.local' /etc/veilor* /etc/tuned/profiles/veilor-* 2>/dev/null; then
echo "[ERR] brand leak detected in /etc — investigate"
fi
echo "════════════════════════════════════════════════════════"
echo " veilor-os install complete"
echo "════════════════════════════════════════════════════════"
%end
# Reboot when done
reboot
KSEOF
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# Substitute placeholders. Use | as sed delimiter. validate_pw()
# already rejects "$\`&|/\n at input — sed_escape() is defence in
# depth in case future code paths feed unsanitised values (e.g.
# locale/hostname from a file, or a relaxed validator).
# Detect cloud-init seed pubkey (if attached) → embed as anaconda
# `sshkey --username=admin` directive. Adds key to admin's
# authorized_keys at install time so SSH validation works first boot.
# No seed = empty directive line (anaconda treats blank line as no-op).
local seed_pubkey sshkey_directive=""
seed_pubkey=$(detect_seed_pubkey 2>/dev/null) || true
if [[ -n $seed_pubkey ]]; then
echo "[INFO] using cloud-init seed pubkey for admin authorized_keys"
# sshkey directive — quote pubkey, no shell meta in pubkeys.
sshkey_directive="sshkey --username=admin \"${seed_pubkey}\""
fi
sed -i \
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
-e "s|__LOCALE__|$(sed_escape "$SEL_LOCALE")|" \
-e "s|__HOSTNAME__|$(sed_escape "$SEL_HOSTNAME")|" \
-e "s|__DISK_BASENAME__|$(sed_escape "$disk_basename")|" \
-e "s|__LUKS_PW__|$(sed_escape "$SEL_LUKS_PW")|" \
-e "s|__ADMIN_PW__|$(sed_escape "$SEL_ADMIN_PW")|" \
-e "s|__SSHKEY_DIRECTIVE__|$(sed_escape "$sshkey_directive")|" \
"$out"
echo "[INFO] generated kickstart at $out"
return 0
}
v0.5.32: ship 7 blockers from 9-agent wave Per docs/research/2026-05-05-agent-wave/README.md priority list. All 7 land together to keep iteration cycles useful — partial fixes bury the lookahead findings agents already mapped. ## 1. CRITICAL — suspend/resume wifi death (Agent 9, B2) `veilor-modules-lock.service` runs `kernel.modules_disabled=1` 30s after graphical.target. iwlwifi/iwlmvm/cfg80211 reload on resume from S3/S0ix → with modules locked, resume breaks wifi until reboot. Same architectural class as the LUKS bug — security feature breaks legitimate kernel state transitions. The unit already has `ConditionKernelCommandLine=!module.sig_enforce=1` (self-skip when signed-modules enforcement is on cmdline). Adding `module.sig_enforce=1` to the kernel cmdline retains the security property (no unsigned modules) without runtime lock-down → resume works. Files: kickstart/veilor-os.ks line 61 + overlay/usr/local/bin/veilor-installer generated bootloader directive both gain `module.sig_enforce=1`. ## 2. veilor-firstboot.service WantedBy=graphical.target (Agent 2) Was `WantedBy=multi-user.target` only. Real installs default to graphical.target so the unit never ran on installed systems — admin pw stayed at install-time + chage -d 0 expired, SDDM PAM bounced to chauthtok screen (recoverable but ugly UX). Now `WantedBy=graphical.target multi-user.target`. Live ISO + multi-user installs both resolve via this list. ## 3. USBGuard hash → id-based baseline (Agent 9, A3) Mirrors memory feedback_usbguard_dock.md — onyx had hash+parent-hash rules that broke on dock replug; we shipped no rules.conf so first boot blocks the USB keyboard. Adds overlay/etc/usbguard/rules.conf with HID-class allow rule (`allow with-interface match-all { 03:*:* }`) — covers every USB keyboard, mouse, gamepad, fingerprint reader, NFC. Survives dock replug + kernel-bump vendor renumeration. Mass-storage stays implicit-block; user explicitly allows post-firstboot via `ujust veilor-usbguard-enroll` (planned v0.6). ## 4. firewalld trusted zone with tailscale0 pre-bound (Agent 9, D1) User uses Tailscale daily (memory: project_tailscale_mesh.md). Default firewalld zone = drop, blocks tailnet traffic on tailscale0. Adds overlay/etc/firewalld/zones/trusted.xml with `<interface name="tailscale0"/>`. After `tailscale up` brings the interface up, NetworkManager dispatcher associates it with the trusted zone automatically — no user intervention. Default zone stays drop. Only the tailscale0 interface gets ACCEPT. ## 5. /etc/skel branding (Agent 7) Was completely empty. Result: per-user KDE config (~/.config/kdeglobals etc.) pre-empty, so the moment user opened System Settings, KDE wrote fresh ~/.config/* and silently shadowed our /etc/xdg/kdedefaults/*. Visual brand evaporated on first click. Seeds: /etc/skel/.config/kdeglobals (copy of assets/kde/veilor-default.kdeglobals) /etc/skel/.config/breezerc (copy of assets/kde/breezerc) /etc/skel/.config/kwinrc (Plasma 6 wayland defaults: opengl, animspeed=0, blur off, click-to-focus) /etc/skel/.config/konsolerc (default profile = Veilor) /etc/skel/.local/share/konsole/Veilor.profile + .colorscheme User who opens System Settings now writes against branded baseline, not against vanilla Breeze. ## 6. KMS modeset args + initramfs keymap (Agents 1 + 9) Real laptop boot has a 5-15s blank between vt switch and SDDM start because simpledrm releases before i915/nvidia-drm/amdgpu claim. Plus non-US users get locked out at LUKS prompt because initramfs ships en-US keymap by default (RHBZ 1405539, RHBZ 1890085). Adds to bootloader cmdline (live + installed): i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us `rd.vconsole.keymap=us` is a placeholder; the v0.6 firstboot keymap picker will rewrite it from /etc/vconsole.conf. Until then, en-US users get correct LUKS keyboard; non-US users still need the v0.6 fix (per Agent 1). ## 7. virtio-9p log capture (Agent 6) The v0.5.30 virtio-serial wiring depends on rsyslog inside the live ISO (anaconda's setupVirtio writes a rsyslog forward rule), which the live ks doesn't install — files were 0-byte across three install runs. test/run-vm.sh now adds a `-virtfs local,...,mount_tag=hostlogs` share pointing at `test/test-runs/<timestamp>/`. veilor-installer runs `_dump_logs_to_host` via EXIT trap that mounts the share at /mnt/hostlogs and rsyncs /tmp/{anaconda,program,storage,packaging,dnf}.log + /var/log/veilor-installer.log + dmesg + journalctl + the generated ks. Runs on success AND failure AND ^C. No-op on real hardware (9p tag absent) — VM-only debug. ## Validate bash -n overlay/usr/local/bin/veilor-installer # OK ksvalidator kickstart/veilor-os.ks # clean ## Out-of-scope for v0.5.32 (deferred to v0.6) Per Agent 1 follow-ups: argon2id retune for slow CPUs, recovery key generation in firstboot, TPM2/FIDO2 unlock helpers. Per Agent 9 follow-ups: Plasma Wayland fallback X11 install, lid-close handling, SELinux relabel progress UX. Per Agent 4: AppArmor stack + nftables preset + audit log shipping CLI. Per Agent 8 (CI hardening): SHA-pin actions + dependabot + SBOM + SLSA L3 attestation — separate workflow-only commit.
2026-05-05 15:36:24 +01:00
_dump_logs_to_host() {
# If a virtio-9p share tagged "hostlogs" was attached by run-vm.sh,
# mount it and dump every anaconda + installer log into it. Runs on
# success AND failure (called via trap). No-op on real hardware where
# the 9p tag doesn't exist.
if ! grep -qw 9p /proc/filesystems 2>/dev/null; then return 0; fi
local m=/mnt/hostlogs
mkdir -p "$m"
mount -t 9p -o trans=virtio,version=9p2000.L hostlogs "$m" 2>/dev/null || return 0
cp -af /tmp/anaconda.log "$m/" 2>/dev/null || true
cp -af /tmp/program.log "$m/" 2>/dev/null || true
cp -af /tmp/storage.log "$m/" 2>/dev/null || true
cp -af /tmp/packaging.log "$m/" 2>/dev/null || true
cp -af /tmp/dnf.log "$m/" 2>/dev/null || true
cp -af /tmp/dnf.librepo.log "$m/" 2>/dev/null || true
cp -af /tmp/anaconda-cmdline.log "$m/" 2>/dev/null || true
cp -af /var/log/veilor-installer.log "$m/" 2>/dev/null || true
cp -af /run/install/veilor-generated.ks "$m/" 2>/dev/null || true
dmesg > "$m/dmesg.log" 2>/dev/null || true
journalctl -b > "$m/journal.log" 2>/dev/null || true
sync
umount "$m" 2>/dev/null || true
}
run_install() {
v0.5.32: ship 7 blockers from 9-agent wave Per docs/research/2026-05-05-agent-wave/README.md priority list. All 7 land together to keep iteration cycles useful — partial fixes bury the lookahead findings agents already mapped. ## 1. CRITICAL — suspend/resume wifi death (Agent 9, B2) `veilor-modules-lock.service` runs `kernel.modules_disabled=1` 30s after graphical.target. iwlwifi/iwlmvm/cfg80211 reload on resume from S3/S0ix → with modules locked, resume breaks wifi until reboot. Same architectural class as the LUKS bug — security feature breaks legitimate kernel state transitions. The unit already has `ConditionKernelCommandLine=!module.sig_enforce=1` (self-skip when signed-modules enforcement is on cmdline). Adding `module.sig_enforce=1` to the kernel cmdline retains the security property (no unsigned modules) without runtime lock-down → resume works. Files: kickstart/veilor-os.ks line 61 + overlay/usr/local/bin/veilor-installer generated bootloader directive both gain `module.sig_enforce=1`. ## 2. veilor-firstboot.service WantedBy=graphical.target (Agent 2) Was `WantedBy=multi-user.target` only. Real installs default to graphical.target so the unit never ran on installed systems — admin pw stayed at install-time + chage -d 0 expired, SDDM PAM bounced to chauthtok screen (recoverable but ugly UX). Now `WantedBy=graphical.target multi-user.target`. Live ISO + multi-user installs both resolve via this list. ## 3. USBGuard hash → id-based baseline (Agent 9, A3) Mirrors memory feedback_usbguard_dock.md — onyx had hash+parent-hash rules that broke on dock replug; we shipped no rules.conf so first boot blocks the USB keyboard. Adds overlay/etc/usbguard/rules.conf with HID-class allow rule (`allow with-interface match-all { 03:*:* }`) — covers every USB keyboard, mouse, gamepad, fingerprint reader, NFC. Survives dock replug + kernel-bump vendor renumeration. Mass-storage stays implicit-block; user explicitly allows post-firstboot via `ujust veilor-usbguard-enroll` (planned v0.6). ## 4. firewalld trusted zone with tailscale0 pre-bound (Agent 9, D1) User uses Tailscale daily (memory: project_tailscale_mesh.md). Default firewalld zone = drop, blocks tailnet traffic on tailscale0. Adds overlay/etc/firewalld/zones/trusted.xml with `<interface name="tailscale0"/>`. After `tailscale up` brings the interface up, NetworkManager dispatcher associates it with the trusted zone automatically — no user intervention. Default zone stays drop. Only the tailscale0 interface gets ACCEPT. ## 5. /etc/skel branding (Agent 7) Was completely empty. Result: per-user KDE config (~/.config/kdeglobals etc.) pre-empty, so the moment user opened System Settings, KDE wrote fresh ~/.config/* and silently shadowed our /etc/xdg/kdedefaults/*. Visual brand evaporated on first click. Seeds: /etc/skel/.config/kdeglobals (copy of assets/kde/veilor-default.kdeglobals) /etc/skel/.config/breezerc (copy of assets/kde/breezerc) /etc/skel/.config/kwinrc (Plasma 6 wayland defaults: opengl, animspeed=0, blur off, click-to-focus) /etc/skel/.config/konsolerc (default profile = Veilor) /etc/skel/.local/share/konsole/Veilor.profile + .colorscheme User who opens System Settings now writes against branded baseline, not against vanilla Breeze. ## 6. KMS modeset args + initramfs keymap (Agents 1 + 9) Real laptop boot has a 5-15s blank between vt switch and SDDM start because simpledrm releases before i915/nvidia-drm/amdgpu claim. Plus non-US users get locked out at LUKS prompt because initramfs ships en-US keymap by default (RHBZ 1405539, RHBZ 1890085). Adds to bootloader cmdline (live + installed): i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us `rd.vconsole.keymap=us` is a placeholder; the v0.6 firstboot keymap picker will rewrite it from /etc/vconsole.conf. Until then, en-US users get correct LUKS keyboard; non-US users still need the v0.6 fix (per Agent 1). ## 7. virtio-9p log capture (Agent 6) The v0.5.30 virtio-serial wiring depends on rsyslog inside the live ISO (anaconda's setupVirtio writes a rsyslog forward rule), which the live ks doesn't install — files were 0-byte across three install runs. test/run-vm.sh now adds a `-virtfs local,...,mount_tag=hostlogs` share pointing at `test/test-runs/<timestamp>/`. veilor-installer runs `_dump_logs_to_host` via EXIT trap that mounts the share at /mnt/hostlogs and rsyncs /tmp/{anaconda,program,storage,packaging,dnf}.log + /var/log/veilor-installer.log + dmesg + journalctl + the generated ks. Runs on success AND failure AND ^C. No-op on real hardware (9p tag absent) — VM-only debug. ## Validate bash -n overlay/usr/local/bin/veilor-installer # OK ksvalidator kickstart/veilor-os.ks # clean ## Out-of-scope for v0.5.32 (deferred to v0.6) Per Agent 1 follow-ups: argon2id retune for slow CPUs, recovery key generation in firstboot, TPM2/FIDO2 unlock helpers. Per Agent 9 follow-ups: Plasma Wayland fallback X11 install, lid-close handling, SELinux relabel progress UX. Per Agent 4: AppArmor stack + nftables preset + audit log shipping CLI. Per Agent 8 (CI hardening): SHA-pin actions + dependabot + SBOM + SLSA L3 attestation — separate workflow-only commit.
2026-05-05 15:36:24 +01:00
# Capture logs to host on every exit path (success, failure, ^C).
trap _dump_logs_to_host EXIT
# Anaconda env setup (see comments below).
export LANG="${SEL_LOCALE:-en_GB.UTF-8}"
export LC_ALL="$LANG"
# LANG / LC_ALL: anaconda's keyboard.activate_keyboard() raises
# AnacondaError: 'LANG' if missing. tty1 inherits no locale by default.
# XDG_RUNTIME_DIR: anaconda's display.setup_display() probes
# os.getenv("XDG_RUNTIME_DIR") + Wayland socket and crashes with
# TypeError on None. We're running unattended so no display needed,
# but env var still has to exist.
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
mkdir -p "$XDG_RUNTIME_DIR"
chmod 0700 "$XDG_RUNTIME_DIR"
if [[ $TUI == gum ]]; then
clear
# gum spin runs anaconda as subprocess. --show-output forwards
# stdout/stderr to /tmp/anaconda-cmdline.log so we can debug.
# --title shows live-updating header. --spinner dot is minimal.
local rc=0
gum spin --spinner dot \
--title "Installing veilor-os to $SEL_DISK · this takes 10-30 min · logs on tty2" \
--show-output \
-- bash -c 'anaconda --cmdline --kickstart=/run/install/veilor-generated.ks 2>&1 | tee /tmp/anaconda-cmdline.log' || rc=$?
if [[ $rc -eq 0 ]]; then
gum style --foreground 2 --border rounded --margin "1 2" --padding "1 3" \
"✓ Install complete" \
"" \
"System will reboot in 5 seconds." \
"Remove the install media."
sleep 5
systemctl reboot
else
prompt_error "Anaconda exited non-zero (status $rc).
Logs at /tmp/anaconda.log + /var/log/veilor-installer.log.
Press OK to drop to shell."
return 1
fi
else
prompt_message "Installing veilor-os to $SEL_DISK ...
This will take 10-30 minutes.
Logs: /var/log/veilor-installer.log + /tmp/anaconda.log"
sleep 1
if anaconda --cmdline --kickstart=/run/install/veilor-generated.ks; then
prompt_message "Install complete. System will reboot.
Remove the install media after shutdown."
sleep 3
systemctl reboot
else
prompt_error "Anaconda exited non-zero.
Logs at /tmp/anaconda.log + /var/log/veilor-installer.log.
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
Press OK to drop to shell."
return 1
fi
fi
}
drop_to_shell() {
clear
cat << 'EOF'
═══════════════════════════════════════════════════
veilor-os live shell
═══════════════════════════════════════════════════
You are in a live, in-memory environment.
Nothing persists across reboot.
Re-run the installer: sudo veilor-installer
Reboot: sudo systemctl reboot
Power off: sudo systemctl poweroff
EOF
exec /bin/bash --login
}
launch_desktop() {
clear
echo "Launching KDE Plasma..."
sleep 1
systemctl isolate graphical.target
# systemd-isolate switches target; sddm spawns on tty1.
# If user logs out, they come back here. Loop continues.
}
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
# ── Entry ──
# (require_tty already called above before exec redirect — see top of file)
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
banner
while true; do
case "$(main_menu)" in
"Install")
if collect_answers && generate_ks; then
run_install || continue
fi
;;
"live · KDE") launch_desktop ;;
"live · shell") drop_to_shell ;;
"──────") banner ;; # separator clicked: redraw, no-op
v0.5.1: gum installer + full veilor-os kickstart generation (#9) * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org <admin@veilor.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 04:39:27 +01:00
"Reboot") systemctl reboot ;;
"Power off") systemctl poweroff ;;
"") banner ;; # ESC/cancel from menu: redraw
*) banner ;;
esac
done