mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 12:07:52 +01:00
check_file_length.sh required a dot in the filename, so every
extensionless executable was silently uncapped. is_source_file() now
reads the first two bytes and accepts a shebang, which catches real
scripts while still skipping data files like i3/config.
That immediately surfaced boot_recovery/boot-repair at 756 lines --
three times the largest capped shell file in the repo, uncapped purely
because its name has no dot in it. Split along its own section markers
into lib/{discovery,repair_fs,repair_kernel,modes}.sh, leaving the entry
script at 218.
The report block stays inline on purpose. It is top-level code with exit
calls rather than function definitions, and sourcing it changed control
flow: 30 of 42 tests failed. That is recorded in the lib headers so the
next reader does not retry it.
No vmbox pass for this one, deliberately. boot-repair is UEFI/ESP tooling
and the guest is BIOS/GRUB, so a sandbox green would be actively
misleading -- the vmbox skill says so itself. The evidence is the fixture
suite, which drives the real script against synthetic root trees: 42
passed before, 42 passed after.
verify_shell_split.sh reports 31 of 32 functions byte-identical. The one
difference is clean_shadow_files, where quoting $ESP_MOUNTPOINT fixed a
genuine SC2086 that only became visible once the function left the
monolith -- shellcheck could no longer see the readonly declaration and
stopped assuming the expansion was safe.
lib/tests/run_all.sh delegates to that same suite rather than standing up
a second, weaker harness: these libraries need the entry script's strict
mode and globals, so testing them in isolation would test a fiction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
219 lines
7.0 KiB
Bash
Executable File
219 lines
7.0 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
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Libraries. Sourced, not executed: they need this script's strict mode, its
|
|
# $ROOT/$MODE globals and the output helpers defined above.
|
|
# ----------------------------------------------------------------------------
|
|
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/lib" && pwd)"
|
|
# shellcheck source=lib/discovery.sh
|
|
source "$LIB_DIR/discovery.sh"
|
|
# shellcheck source=lib/repair_fs.sh
|
|
source "$LIB_DIR/repair_fs.sh"
|
|
# shellcheck source=lib/repair_kernel.sh
|
|
source "$LIB_DIR/repair_kernel.sh"
|
|
# shellcheck source=lib/modes.sh
|
|
source "$LIB_DIR/modes.sh"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 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
|