mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:07:03 +01:00
"Pre-commit checks" has failed on every push since b83ce5d removed
shellcheck's --severity filter. The findings never appeared locally: CI
installs an older shellcheck than this machine, and 0.11.0 no longer emits
SC2002/SC2015/SC2119 at all. Reproduced by fetching shellcheck 0.9.0, which
matches CI's output exactly, and verified against both versions.
SC2015 x6 -- rewrote `A && B || C` as an explicit if. Each keeps its trailing
`|| true`, so a failing side effect still cannot abort the caller under set -e.
The first draft dropped it in run_phone.sh; a review pass caught that and it
was restored, since that call is the last statement of a function.
SC2119 x3 -- require_imagemagick and adb_select_device take an OPTIONAL first
argument, so the bare calls were already correct. Following shellcheck's
literal advice and passing "$@" would forward the script's own arguments as a
tool preference, which would be a real defect. Passing an explicit "" states
the same intent without changing behaviour, confirmed by running both forms.
SC2002 x2 -- `cat FILE | tr ...` becomes `tr ... < FILE`, keeping 2>/dev/null
so a missing file still yields an empty string.
No suppressions, no --severity filter, no exclusions added. Verified: 0
findings under both shellcheck versions, `pre-commit run shellcheck
--all-files` passes, test_adb_common 13/13, test_boot_repair 31/31, 1193
Python tests at 100% coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7
735 lines
22 KiB
Bash
Executable File
735 lines
22 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. Deleting them after mounting
|
|
# 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.
|
|
-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
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 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 two 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
|
|
|
|
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
|