testsAndMisc/linux_configuration/boot_recovery/lib/repair_kernel.sh
Krzysztof kuhy Rudnicki 0fb3e3eee2 Cap extensionless scripts, and split the one that was hiding
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
2026-08-23 19:13:40 +02:00

155 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Repair steps that operate on the kernel: depmod, the kernel image, the
# initramfs, and the pacman parallel-download check.
#
# Sourced by boot-repair, not executed. Split out of it verbatim to bring the
# entry script under the 250-line cap; it inherits the caller's strict mode,
# its $ROOT/$MODE globals and the output helpers defined there.
#
# shellcheck shell=bash
# shellcheck source=../boot-repair
# ----------------------------------------------------------------------------
# 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
}