testsAndMisc/linux_configuration/boot_recovery/boot-repair
Krzysztof kuhy Rudnicki 4f2b0ccdf7 Dissolve linux_configuration/scripts/ and single_use/
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.

Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.

The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:

  - 52 relative `source` lines for lib/common.sh and lib/android.sh. The
    correct prefix differs per file: 40 were ../../ and 12 were ../../../,
    so a blanket replace would have fixed one set and broken the other.
  - REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
    each walking up four levels from a directory now two levels shallower.
    This caused a real regression -- the fixes suite went red with
    "log_warn: command not found" -- caught by running it and comparing
    against the pre-move baseline worktree.
  - conftest.py's sys.path entries, which took the pytest gate down with
    14 collection errors, and the `# shellcheck source=` directives, which
    took shellcheck down with SC1091.

16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.

Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:53:30 +02:00

757 lines
23 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# boot-repair — offline repair for a stale/unwritten EFI System Partition
# ============================================================================
#
# The failure this fixes (seen 2026-07-30):
# A kernel upgrade replaced /usr/lib/modules/<old> with <new>, but the new
# kernel image never reached the ESP. The firmware kept booting the OLD
# vmlinuz, whose module tree no longer existed — so nothing modular could
# load after pivot-root. In particular `vfat` was gone, /boot itself could
# not be mounted, local-fs.target failed and systemd dropped to emergency
# mode. The upgrade was reported as successful.
#
# Root cause on this machine: pacman-git with ParallelDownloads > 1 makes
# every forked alpm hook child segfault, silently skipping the
# 90-mkinitcpio-install and depmod hooks.
#
# Design constraints (deliberate):
# * SELF-CONTAINED — no sourcing of repo libs. It must run from the systemd
# emergency shell where only the root filesystem is mounted.
# * FULLY OFFLINE — never touches the network. Missing kernel modules are
# recovered from /var/cache/pacman/pkg, nothing else.
# * Repairs by default. Use --dry-run to inspect without changing anything.
#
# Safety invariants:
# * Files under /boot are ONLY deleted while /boot is not a mountpoint AND
# contains no EFI/ or loader/ directory AND the system booted via UEFI.
# Deleting them after mounting, or on a BIOS box that has no ESP at all,
# would destroy the real kernel and leave the machine unbootable.
# * EFI/ and loader/ on the ESP are never written or removed.
# * The running kernel's module tree is never deleted.
# ============================================================================
set -euo pipefail
# Emergency shells can have a threadbare PATH.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
readonly SCRIPT_NAME="${0##*/}"
readonly ESP_MOUNTPOINT="/boot"
# Defaults
ROOT="/"
MODE="fix" # fix | dry-run | preflight | auto
ESP_DEV_OVERRIDE=""
PROBLEMS=0
REPAIRS=0
QUIET=0
# ----------------------------------------------------------------------------
# Output helpers
# ----------------------------------------------------------------------------
msg() { [[ $QUIET -eq 1 ]] || printf '%s\n' "$*"; }
ok() { [[ $QUIET -eq 1 ]] || printf ' [ OK ] %s\n' "$*"; }
info() { [[ $QUIET -eq 1 ]] || printf ' [INFO] %s\n' "$*"; }
# A problem that was found. Counted so the exit status can reflect reality.
problem() {
PROBLEMS=$((PROBLEMS + 1))
printf ' [FAIL] %s\n' "$*" >&2
}
warn() { printf ' [WARN] %s\n' "$*" >&2; }
# A repair that was actually carried out.
repaired() {
REPAIRS=$((REPAIRS + 1))
printf ' [FIXED] %s\n' "$*"
}
die() {
printf '%s: error: %s\n' "$SCRIPT_NAME" "$*" >&2
exit 2
}
# True when we are allowed to modify the system.
acting() { [[ $MODE == "fix" || $MODE == "auto" ]]; }
# Describe an action that dry-run would skip.
would() { msg " [DRY ] would $*"; }
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]
Repairs a stale EFI System Partition after a kernel upgrade failed to write
it. Repairs by default; runs fully offline.
Options:
--dry-run Report problems, change nothing.
--preflight Exit non-zero unless the ESP is mounted. For a pacman
PreTransaction hook that gates kernel upgrades.
--auto Quiet repair for a pacman PostTransaction hook. Prints
only when it acts or fails.
--root DIR Operate on an alternate root (testing, or an Arch ISO
chroot). Live-only steps are skipped.
--esp DEVICE Use DEVICE as the ESP instead of auto-detecting it. Also
overrides the BIOS guard: on a system with no
/sys/firmware/efi, shadow-file cleanup is refused unless
the ESP device is named explicitly here.
-h, --help Show this help.
Exit status:
0 system consistent (or fully repaired)
1 problems remain / preflight gate failed
2 usage or environment error
EOF
exit 0
}
# ----------------------------------------------------------------------------
# Path helpers — everything is expressed relative to \$ROOT so the logic can be
# exercised against a fixture tree without touching the real machine.
# ----------------------------------------------------------------------------
# Join $ROOT with an absolute path, avoiding a doubled leading slash.
rpath() {
local p="$1"
if [[ $ROOT == "/" ]]; then
printf '%s' "$p"
else
printf '%s%s' "${ROOT%/}" "$p"
fi
}
# True when operating on the live system (so mount/modprobe are meaningful).
is_live() { [[ $ROOT == "/" ]]; }
# ----------------------------------------------------------------------------
# Kernel discovery
# ----------------------------------------------------------------------------
# Read the version banner out of a kernel image. Works on a bare file with no
# tooling beyond grep, which matters in a degraded emergency shell.
# Usage: kernel_image_version <path> -> prints e.g. 7.1.5-arch1-2, or nothing
kernel_image_version() {
local img="$1"
[[ -r $img ]] || return 1
grep -aoE '[0-9]+\.[0-9]+\.[0-9]+-arch[0-9]+-[0-9]+' "$img" 2> /dev/null | head -1
}
# A module tree is "complete" only if it has both the kernel image and actual
# modules. The failure mode leaves behind a directory holding just the
# modules.* metadata files, which must not be mistaken for a usable kernel.
kernel_tree_complete() {
local dir="$1"
[[ -f $dir/vmlinuz ]] || return 1
[[ -d $dir/kernel ]] || return 1
return 0
}
# Newest complete module tree under $ROOT/usr/lib/modules, by version sort.
newest_complete_kernel() {
local moddir kver best=""
moddir="$(rpath /usr/lib/modules)"
[[ -d $moddir ]] || return 1
while IFS= read -r kver; do
[[ -n $kver ]] || continue
kernel_tree_complete "$moddir/$kver" || continue
best="$kver"
done < <(find "$moddir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2> /dev/null | sort -V)
[[ -n $best ]] || return 1
printf '%s' "$best"
}
# ----------------------------------------------------------------------------
# ESP discovery
# ----------------------------------------------------------------------------
# The fstab spec for /boot (e.g. "UUID=8A14-F8A7"), if present.
fstab_boot_spec() {
local fstab spec mnt
fstab="$(rpath /etc/fstab)"
[[ -r $fstab ]] || return 1
while read -r spec mnt _; do
[[ $spec == \#* ]] && continue
[[ -z ${spec:-} ]] && continue
if [[ $mnt == "$ESP_MOUNTPOINT" ]]; then
printf '%s' "$spec"
return 0
fi
done < "$fstab"
return 1
}
# The mount options field for /boot in fstab.
fstab_boot_options() {
local fstab spec mnt opts
fstab="$(rpath /etc/fstab)"
[[ -r $fstab ]] || return 1
while read -r spec mnt _ opts _; do
[[ $spec == \#* ]] && continue
[[ -z ${spec:-} ]] && continue
if [[ $mnt == "$ESP_MOUNTPOINT" ]]; then
printf '%s' "$opts"
return 0
fi
done < "$fstab"
return 1
}
# Resolve the fstab spec to a block device. Prefers /dev/disk/by-uuid so it
# works without blkid, which may be unavailable in a minimal shell.
resolve_esp_device() {
local spec uuid dev
if [[ -n $ESP_DEV_OVERRIDE ]]; then
printf '%s' "$ESP_DEV_OVERRIDE"
return 0
fi
spec="$(fstab_boot_spec)" || return 1
case "$spec" in
UUID=*)
uuid="${spec#UUID=}"
if [[ -e /dev/disk/by-uuid/$uuid ]]; then
dev="$(readlink -f "/dev/disk/by-uuid/$uuid")"
printf '%s' "$dev"
return 0
fi
# Fall back to blkid when the by-uuid symlink is absent.
if command -v blkid > /dev/null 2>&1; then
dev="$(blkid -U "$uuid" 2> /dev/null)" && [[ -n $dev ]] && {
printf '%s' "$dev"
return 0
}
fi
return 1
;;
/dev/*)
printf '%s' "$spec"
return 0
;;
*)
return 1
;;
esac
}
# ----------------------------------------------------------------------------
# Mount state
# ----------------------------------------------------------------------------
boot_is_mounted() {
is_live || return 1
findmnt -n "$ESP_MOUNTPOINT" > /dev/null 2>&1
}
# True when the system booted via UEFI, i.e. an ESP can exist at all. The
# kernel only creates /sys/firmware/efi when it was loaded by EFI firmware.
#
# Expressed via rpath so a fixture tree can declare its own firmware type;
# on the live machine this resolves to the real /sys path and is exact.
system_is_uefi() { [[ -d "$(rpath /sys/firmware/efi)" ]]; }
# ----------------------------------------------------------------------------
# Repair steps
# ----------------------------------------------------------------------------
# The root filesystem is sometimes left read-only after a failed boot; every
# later step would fail confusingly without this.
ensure_root_writable() {
is_live || return 0
local testfile
testfile="$(rpath /.boot-repair-write-test)"
if (: > "$testfile") 2> /dev/null; then
rm -f "$testfile"
return 0
fi
problem "root filesystem is read-only"
if acting; then
mount -o remount,rw / || die "could not remount / read-write"
repaired "remounted / read-write"
else
would "remount / read-write"
fi
}
# Restore just enough of the RUNNING kernel's module tree to mount a vfat ESP.
# Without this the repair cannot even reach the ESP: mounting it needs vfat,
# and vfat lives in the module tree the upgrade deleted.
ensure_vfat() {
is_live || return 0
if modprobe -n vfat > /dev/null 2>&1 && modprobe vfat > /dev/null 2>&1; then
ok "vfat available"
return 0
fi
problem "vfat cannot be loaded for the running kernel ($(uname -r))"
if ! acting; then
would "restore fat/nls modules from the pacman cache"
return 1
fi
local kver pkgver cache pkg
kver="$(uname -r)"
# uname 7.0.5-arch1-1 -> package 7.0.5.arch1-1
pkgver="${kver/-arch/.arch}"
cache="$(rpath /var/cache/pacman/pkg)"
pkg=""
if [[ -d $cache ]]; then
# Newest matching cached kernel package for the running version.
pkg="$(find "$cache" -maxdepth 1 -name "linux*-${pkgver}-*.pkg.tar.zst" 2> /dev/null | sort -V | tail -1)"
fi
if [[ -z $pkg ]]; then
problem "no cached package for the running kernel in $cache"
msg ""
msg " Cannot restore vfat offline. Recover with an Arch ISO:"
msg " mount /dev/<root> /mnt -o subvol=@ && arch-chroot /mnt"
msg " pacman -S linux && $SCRIPT_NAME"
return 1
fi
info "restoring fat/nls modules from ${pkg##*/}"
local tmp
tmp="$(mktemp -d)" || die "mktemp failed"
# shellcheck disable=SC2064 # expand $tmp now, on purpose
trap "rm -rf '$tmp'" RETURN
# Extract only the filesystem modules needed to read a FAT32 ESP.
tar --zstd -xf "$pkg" -C "$tmp" --wildcards \
"usr/lib/modules/${kver}/kernel/fs/fat/*" \
"usr/lib/modules/${kver}/kernel/fs/nls/*" 2> /dev/null ||
{
problem "could not extract fat/nls modules from ${pkg##*/}"
return 1
}
local src dest
src="$tmp/usr/lib/modules/${kver}/kernel"
dest="$(rpath "/usr/lib/modules/${kver}")"
[[ -d $src ]] || {
problem "cached package did not contain fat/nls modules"
return 1
}
mkdir -p "$dest"
cp -a "$src" "$dest/"
depmod "$kver" 2> /dev/null || true
if modprobe vfat > /dev/null 2>&1; then
repaired "restored vfat for the running kernel"
return 0
fi
problem "vfat still will not load after restore"
return 1
}
# fstab must GATE on the ESP. `nofail` turns this failure into a silent boot of
# a stale kernel, which is strictly worse: the machine looks fine until the
# module tree disappears, one upgrade later.
check_fstab_options() {
local opts fstab
fstab="$(rpath /etc/fstab)"
opts="$(fstab_boot_options)" || {
problem "no /boot entry in $fstab"
return 1
}
if [[ $opts != *noauto* && $opts != *nofail* ]]; then
ok "fstab gates on /boot ($opts)"
return 0
fi
problem "fstab has noauto/nofail on /boot ($opts) — failures would be silent"
if ! acting; then
would "rewrite the /boot options to 'defaults'"
return 1
fi
cp -a "$fstab" "${fstab}.bak-$(date +%Y%m%d-%H%M%S)"
# Rewrite only the options field of the /boot line.
awk -v mp="$ESP_MOUNTPOINT" '
/^[[:space:]]*#/ { print; next }
NF >= 4 && $2 == mp { $4 = "defaults"; print; next }
{ print }
' OFS='\t' "$fstab" > "${fstab}.new"
mv "${fstab}.new" "$fstab"
repaired "fstab /boot options reset to 'defaults'"
if is_live; then
systemctl daemon-reload 2> /dev/null || true
fi
return 0
}
# Orphaned kernel files written into the ROOT filesystem's /boot directory
# while the ESP was unmounted. They shadow the real ESP and waste ~250 MB.
#
# This is the single most dangerous step in the script: doing it after mounting
# would delete the real kernel. Hence the three hard guards below.
clean_shadow_files() {
local bootdir
bootdir="$(rpath $ESP_MOUNTPOINT)"
[[ -d $bootdir ]] || return 0
# GUARD 1: never touch a mounted /boot.
if boot_is_mounted; then
return 0
fi
# GUARD 2: a real ESP always has EFI/ or loader/. If either is present in
# something we believe is unmounted, our mount detection is wrong — stop.
if [[ -d $bootdir/EFI || -d $bootdir/loader ]]; then
warn "unmounted $ESP_MOUNTPOINT contains EFI/ or loader/ — refusing to delete anything"
return 1
fi
# GUARD 3: on a BIOS/GRUB system there is no ESP at all, so an unmounted
# /boot is not a shadow — it is the only kernel the machine has. Deleting it
# leaves the machine unbootable (measured 2026-08-22: a BIOS guest never
# booted again). --esp is the deliberate override: naming the device by hand
# asserts the firmware assumption that /sys/firmware/efi otherwise proves.
if ! system_is_uefi && [[ -z $ESP_DEV_OVERRIDE ]]; then
warn "no /sys/firmware/efi — this system booted via BIOS and has no ESP;"
warn " $ESP_MOUNTPOINT holds the real kernel. Refusing to delete anything."
warn " If that is wrong, name the ESP explicitly: $SCRIPT_NAME --esp DEVICE"
return 1
fi
local -a shadows=()
local f
while IFS= read -r f; do
[[ -n $f ]] && shadows+=("$f")
done < <(find "$bootdir" -maxdepth 1 -type f \
\( -name 'vmlinuz-*' -o -name 'initramfs-*.img' -o -name '*-ucode.img' \) 2> /dev/null)
[[ ${#shadows[@]} -gt 0 ]] || return 0
problem "${#shadows[@]} orphaned kernel file(s) in the unmounted $ESP_MOUNTPOINT (shadowing the ESP)"
if ! acting; then
would "delete: ${shadows[*]##*/}"
return 1
fi
rm -f "${shadows[@]}"
repaired "removed ${#shadows[@]} shadow file(s) from the root filesystem"
return 0
}
mount_esp() {
is_live || return 0
if boot_is_mounted; then
ok "$ESP_MOUNTPOINT mounted ($(findmnt -n -o SOURCE "$ESP_MOUNTPOINT"))"
return 0
fi
problem "$ESP_MOUNTPOINT is not mounted"
if ! acting; then
would "mount the ESP at $ESP_MOUNTPOINT"
return 1
fi
local dev
dev="$(resolve_esp_device)" || {
problem "could not determine the ESP device"
return 1
}
mkdir -p "$ESP_MOUNTPOINT"
mount -t vfat "$dev" "$ESP_MOUNTPOINT" || {
problem "mounting $dev at $ESP_MOUNTPOINT failed"
return 1
}
repaired "mounted $dev at $ESP_MOUNTPOINT"
return 0
}
# depmod output is what modprobe consults; the same skipped-hook bug that
# leaves the ESP stale also leaves modules.dep missing for the new kernel.
ensure_depmod() {
local kver dep
kver="$1"
dep="$(rpath "/usr/lib/modules/${kver}/modules.dep")"
if [[ -f $dep ]]; then
ok "modules.dep present for $kver"
return 0
fi
problem "modules.dep missing for $kver"
if ! acting; then
would "run depmod $kver"
return 1
fi
if is_live; then
depmod "$kver" || {
problem "depmod $kver failed"
return 1
}
else
depmod -b "${ROOT%/}" "$kver" || {
problem "depmod failed"
return 1
}
fi
repaired "generated modules.dep for $kver"
return 0
}
# The core repair: make the ESP's kernel image match the installed module tree.
sync_kernel_image() {
local kver src dest have
kver="$1"
src="$(rpath "/usr/lib/modules/${kver}/vmlinuz")"
dest="$(rpath "${ESP_MOUNTPOINT}/vmlinuz-linux")"
[[ -f $src ]] || {
problem "no kernel image at $src"
return 1
}
if [[ -f $dest ]]; then
have="$(kernel_image_version "$dest" || true)"
if [[ $have == "$kver" ]]; then
ok "ESP kernel matches installed tree ($kver)"
return 0
fi
problem "ESP kernel is ${have:-unreadable} but installed tree is $kver"
else
problem "no kernel image on the ESP"
fi
if ! acting; then
would "install $kver to $dest"
return 1
fi
install -Dm644 "$src" "$dest"
repaired "installed kernel $kver to the ESP"
return 0
}
# Which kernel an initramfs was built for. Prefers lsinitcpio, but falls back
# to scanning the image directly — lsinitcpio is not guaranteed to be present
# in a degraded emergency shell.
initramfs_version() {
local img="$1" v=""
[[ -r $img ]] || return 1
if command -v lsinitcpio > /dev/null 2>&1; then
v="$(lsinitcpio "$img" 2> /dev/null |
grep -oE 'modules/[0-9]+\.[0-9]+\.[0-9]+-arch[0-9]+-[0-9]+' |
head -1 | cut -d/ -f2 || true)"
fi
if [[ -z $v ]]; then
v="$(grep -aoE 'modules/[0-9]+\.[0-9]+\.[0-9]+-arch[0-9]+-[0-9]+' "$img" 2> /dev/null |
head -1 | cut -d/ -f2 || true)"
fi
[[ -n $v ]] || return 1
printf '%s' "$v"
}
# Rebuild every preset so the initramfs matches the kernel we just installed.
rebuild_initramfs() {
local kver img have
kver="$1"
img="$(rpath "${ESP_MOUNTPOINT}/initramfs-linux.img")"
have="$(initramfs_version "$img" || true)"
if [[ -n $have && $have == "$kver" ]]; then
ok "initramfs matches $kver"
return 0
fi
problem "initramfs is ${have:-missing/unknown}, expected $kver"
if ! acting; then
would "run mkinitcpio -P"
return 1
fi
is_live || {
warn "skipping mkinitcpio (--root given)"
return 1
}
command -v mkinitcpio > /dev/null 2>&1 || {
problem "mkinitcpio is not installed"
return 1
}
mkinitcpio -P || {
problem "mkinitcpio -P failed"
return 1
}
repaired "rebuilt initramfs for $kver"
return 0
}
# The actual root cause on this machine. With ParallelDownloads > 1, pacman-git
# segfaults every forked hook child, so mkinitcpio and depmod never run and the
# transaction still reports success.
check_parallel_downloads() {
local conf value
conf="$(rpath /etc/pacman.conf)"
[[ -r $conf ]] || return 0
value="$(grep -E '^[[:space:]]*ParallelDownloads[[:space:]]*=' "$conf" 2> /dev/null |
tail -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]')"
[[ -n $value ]] || {
ok "ParallelDownloads not set (pacman default is serial)"
return 0
}
if [[ $value == "1" ]]; then
ok "ParallelDownloads = 1"
return 0
fi
problem "ParallelDownloads = $value — forked alpm hooks segfault, silently skipping mkinitcpio"
if ! acting; then
would "set ParallelDownloads = 1 in $conf"
return 1
fi
cp -a "$conf" "${conf}.bak-$(date +%Y%m%d-%H%M%S)"
sed -i -E 's/^([[:space:]]*ParallelDownloads[[:space:]]*=[[:space:]]*).*/\11/' "$conf"
repaired "set ParallelDownloads = 1"
return 0
}
# ----------------------------------------------------------------------------
# Modes
# ----------------------------------------------------------------------------
# PreTransaction gate: refuse a kernel upgrade when the ESP is not mounted,
# because that upgrade would write the kernel into the root filesystem instead.
run_preflight() {
if boot_is_mounted; then
printf 'boot-repair: ESP mounted at %s — proceeding.\n' "$ESP_MOUNTPOINT"
exit 0
fi
cat >&2 << EOF
==> boot-repair: ABORTING TRANSACTION
$ESP_MOUNTPOINT is not mounted. Installing a kernel now would write it into
the root filesystem, leaving the firmware booting a stale image whose
modules this upgrade is about to delete.
Fix it, then retry:
sudo mount $ESP_MOUNTPOINT
sudo $SCRIPT_NAME
EOF
exit 1
}
run_checks() {
local kver
ensure_root_writable
msg "Kernel / ESP consistency"
check_fstab_options || true
clean_shadow_files || true
ensure_vfat || true
mount_esp || true
if ! kver="$(newest_complete_kernel)"; then
problem "no complete kernel module tree found under $(rpath /usr/lib/modules)"
msg ""
msg " Recover the kernel package first (offline, from cache):"
msg " pacman -U /var/cache/pacman/pkg/linux-*.pkg.tar.zst"
return 1
fi
info "target kernel: $kver"
ensure_depmod "$kver" || true
# Only meaningful once the ESP is actually mounted; otherwise these would
# write into the root filesystem — the very bug being repaired.
if boot_is_mounted || ! is_live; then
sync_kernel_image "$kver" || true
rebuild_initramfs "$kver" || true
else
problem "ESP still unmounted — cannot sync the kernel image"
fi
msg ""
msg "Upgrade safety"
check_parallel_downloads || true
return 0
}
# ----------------------------------------------------------------------------
# Argument parsing
# ----------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
MODE="dry-run"
shift
;;
--preflight)
MODE="preflight"
shift
;;
--auto)
MODE="auto"
QUIET=1
shift
;;
--root)
[[ $# -ge 2 ]] || die "--root requires a directory"
ROOT="$2"
shift 2
;;
--esp)
[[ $# -ge 2 ]] || die "--esp requires a device"
ESP_DEV_OVERRIDE="$2"
shift 2
;;
-h | --help) usage ;;
*) die "unknown option: $1 (try --help)" ;;
esac
done
[[ -d $ROOT ]] || die "root directory does not exist: $ROOT"
if [[ $MODE == "preflight" ]]; then
run_preflight
fi
# Root is required to modify the live system, but not to repair a tree handed
# to us via --root (a fixture, or a chroot where we are already root).
if acting && is_live && [[ $EUID -ne 0 ]]; then
die "must run as root (try: sudo $SCRIPT_NAME)"
fi
[[ $QUIET -eq 1 ]] || {
msg "=== boot-repair (${MODE}${ROOT:+, root=$ROOT}) ==="
msg ""
}
run_checks || true
# ----------------------------------------------------------------------------
# Report
# ----------------------------------------------------------------------------
# In --auto (post-transaction hook) stay silent unless something happened.
if [[ $MODE == "auto" ]]; then
if [[ $REPAIRS -gt 0 ]]; then
printf 'boot-repair: repaired %d boot inconsistency/ies after this transaction.\n' "$REPAIRS"
fi
if [[ $PROBLEMS -gt $REPAIRS ]]; then
printf 'boot-repair: UNRESOLVED boot problems remain — do NOT reboot. Run: sudo %s\n' "$SCRIPT_NAME" >&2
exit 1
fi
exit 0
fi
msg ""
if [[ $PROBLEMS -eq 0 ]]; then
msg "=== consistent: nothing to repair ==="
exit 0
fi
if [[ $MODE == "dry-run" ]]; then
msg "=== $PROBLEMS problem(s) found — rerun without --dry-run to repair ==="
exit 1
fi
if [[ $REPAIRS -ge $PROBLEMS ]]; then
msg "=== repaired $REPAIRS problem(s) ==="
msg ""
msg "Verify before rebooting:"
msg " findmnt $ESP_MOUNTPOINT"
msg " sudo $SCRIPT_NAME --dry-run"
exit 0
fi
msg "=== repaired $REPAIRS of $PROBLEMS problem(s) — UNRESOLVED issues remain ==="
exit 1