Add GRUB migration and status commands to blob_boot
This commit is contained in:
+631
-19
@@ -1,28 +1,640 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# blob_boot.sh - boot splash and bootloader management.
|
||||||
|
#
|
||||||
|
# blob_boot.sh [image] Apply a Plymouth boot splash image (default action)
|
||||||
|
# blob_boot.sh grub Migrate from Limine back to GRUB, detecting every OS
|
||||||
|
# --purge removes Limine in the same run instead of
|
||||||
|
# keeping it for one fallback boot
|
||||||
|
# blob_boot.sh detect Rescan for other operating systems, rebuild the menu
|
||||||
|
# blob_boot.sh cleanup Retire Limine once GRUB has been confirmed working
|
||||||
|
# blob_boot.sh status Show what this machine currently boots with
|
||||||
|
#
|
||||||
|
# This machine has a 256 MB EFI System Partition, which is why Omarchy switched
|
||||||
|
# it to a single unified kernel image. Going back to GRUB means going back to a
|
||||||
|
# separate kernel and initramfs, so every step here is careful about space.
|
||||||
|
|
||||||
# Default to the branding image if no argument is provided
|
set -euo pipefail
|
||||||
if [ -z "$1" ]; then
|
|
||||||
IMAGE_PATH="$HOME/Documents/dotfiles/branding/boot_flash.png"
|
|
||||||
else
|
|
||||||
IMAGE_PATH=$(realpath "$1")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$IMAGE_PATH" ]; then
|
DEFAULT_IMAGE="$HOME/Documents/dotfiles/branding/boot_flash.png"
|
||||||
echo "Error: File '$IMAGE_PATH' does not exist."
|
PLYMOUTH_LOGO="/usr/share/plymouth/themes/omarchy/logo.png"
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Applying boot splash image: $IMAGE_PATH"
|
log() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
|
||||||
|
info() { printf ' %s\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m %s\033[0m\n' "$*"; }
|
||||||
|
ok() { printf '\033[1;32m==> %s\033[0m\n' "$*"; }
|
||||||
|
die() { printf '\n\033[1;31m!!! %s\033[0m\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
require_sudo() {
|
||||||
echo "This requires sudo privileges."
|
echo "This requires sudo privileges."
|
||||||
|
sudo -v || die "Could not obtain sudo."
|
||||||
|
}
|
||||||
|
|
||||||
# Copy the image to the Plymouth theme directory
|
# --------------------------------------------------------------------------
|
||||||
sudo cp "$IMAGE_PATH" /usr/share/plymouth/themes/omarchy/logo.png
|
# Boot splash
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
# Ensure the correct permissions
|
apply_splash() {
|
||||||
sudo chmod 644 /usr/share/plymouth/themes/omarchy/logo.png
|
local image_path
|
||||||
|
if [ -z "${1:-}" ]; then
|
||||||
|
image_path="$DEFAULT_IMAGE"
|
||||||
|
else
|
||||||
|
image_path=$(realpath "$1")
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Rebuilding initramfs..."
|
[ -f "$image_path" ] || die "File '$image_path' does not exist."
|
||||||
# Exclusively use mkinitcpio for GRUB compatibility
|
[ -d "$(dirname "$PLYMOUTH_LOGO")" ] || die "Plymouth omarchy theme is not installed."
|
||||||
sudo mkinitcpio -P
|
|
||||||
|
|
||||||
echo "Boot splash successfully updated!"
|
log "Applying boot splash image: $image_path"
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
sudo cp "$image_path" "$PLYMOUTH_LOGO"
|
||||||
|
sudo chmod 644 "$PLYMOUTH_LOGO"
|
||||||
|
|
||||||
|
log "Rebuilding initramfs"
|
||||||
|
rebuild_initramfs
|
||||||
|
|
||||||
|
# Under GRUB the menu references the initramfs by path, so it only needs
|
||||||
|
# regenerating if the file set changed - but it is cheap and keeps the menu
|
||||||
|
# honest after a kernel or os-prober change.
|
||||||
|
if using_grub; then
|
||||||
|
log "Refreshing the GRUB menu"
|
||||||
|
sudo grub-mkconfig -o /boot/grub/grub.cfg
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "Boot splash successfully updated!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call mkinitcpio directly: limine-mkinitcpio-hook installs a wrapper at
|
||||||
|
# /usr/local/bin/mkinitcpio that stops to ask whether to rebuild Limine
|
||||||
|
# entries, which is useless in a script. While Limine is still the active
|
||||||
|
# bootloader its own tool is the one that actually updates what boots.
|
||||||
|
rebuild_initramfs() {
|
||||||
|
if command -v limine-mkinitcpio >/dev/null && ! using_grub; then
|
||||||
|
info "Limine is still the bootloader; rebuilding through limine-mkinitcpio"
|
||||||
|
sudo limine-mkinitcpio
|
||||||
|
else
|
||||||
|
sudo /usr/bin/mkinitcpio -P
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
using_grub() {
|
||||||
|
[ -f /boot/grub/grub.cfg ] && [ -f /boot/EFI/GRUB/grubx64.efi ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Limine -> GRUB migration
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
migrate_to_grub() {
|
||||||
|
local purge=0
|
||||||
|
[ "${1:-}" = "--purge" ] && purge=1
|
||||||
|
|
||||||
|
[ -d /sys/firmware/efi ] || die "Not booted in UEFI mode; this script assumes UEFI."
|
||||||
|
mountpoint -q /boot || die "/boot is not mounted."
|
||||||
|
command -v grub-install >/dev/null || die "The 'grub' package is not installed."
|
||||||
|
command -v os-prober >/dev/null || die "The 'os-prober' package is not installed."
|
||||||
|
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
local machine_id esp_uuid backup
|
||||||
|
machine_id=$(</etc/machine-id)
|
||||||
|
esp_uuid=$(findmnt -no UUID /boot)
|
||||||
|
backup=/root/limine-to-grub-$(date +%Y%m%d-%H%M%S)
|
||||||
|
|
||||||
|
log "Backing up the current boot configuration to $backup"
|
||||||
|
sudo mkdir -p "$backup"
|
||||||
|
for f in /etc/default/grub /etc/mkinitcpio.d/linux.preset /etc/mkinitcpio.conf \
|
||||||
|
/boot/limine.conf /etc/mkinitcpio.conf.d /etc/grub.d; do
|
||||||
|
[ -e "$f" ] && sudo cp -a "$f" "$backup/" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
sudo efibootmgr -v | sudo tee "$backup/efibootmgr-before.txt" >/dev/null 2>&1 || true
|
||||||
|
info "saved."
|
||||||
|
|
||||||
|
reclaim_esp_space "$machine_id" "$backup"
|
||||||
|
restore_standard_initramfs
|
||||||
|
configure_grub_scripts
|
||||||
|
install_grub "$esp_uuid"
|
||||||
|
fix_failing_boot_units
|
||||||
|
|
||||||
|
if (( purge )); then
|
||||||
|
purge_limine
|
||||||
|
set_boot_order
|
||||||
|
enable_fallback_initramfs
|
||||||
|
generate_menu
|
||||||
|
else
|
||||||
|
# Test GRUB with a one-shot BootNext rather than reordering BootOrder.
|
||||||
|
# If GRUB fails to boot, a power cycle falls straight back to Limine on
|
||||||
|
# its own - no boot-menu keypress, no timing, nothing to get right while
|
||||||
|
# staring at a broken screen.
|
||||||
|
set_boot_next
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Result"
|
||||||
|
df -h /boot | tail -1 | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
info "GRUB menu entries:"
|
||||||
|
list_menu_entries
|
||||||
|
echo
|
||||||
|
if (( purge )); then
|
||||||
|
ok "GRUB is the only bootloader. Limine is gone and its space is reclaimed."
|
||||||
|
else
|
||||||
|
ok "GRUB is installed and set to boot ONCE on the next restart."
|
||||||
|
info "If it works: run '$0 cleanup' to delete Limine and reclaim its 51 MB."
|
||||||
|
info "If it fails: hold the power button, then power on - the firmware falls"
|
||||||
|
info " back to Limine by itself. Nothing to press, nothing lost."
|
||||||
|
fi
|
||||||
|
info "Backups: $backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The 'omarchy' metapackage hard-depends on limine, limine-mkinitcpio-hook and
|
||||||
|
# limine-snapper-sync, so they can only be forced out with -Rdd - and the next
|
||||||
|
# omarchy upgrade will resolve those dependencies and pull them straight back in.
|
||||||
|
#
|
||||||
|
# NoExtract makes that harmless: pacman may reinstall the packages, but it will
|
||||||
|
# never write the files that actually do anything. Only the active parts are
|
||||||
|
# listed - the pacman hooks that rebuild unified kernel images and redeploy
|
||||||
|
# Limine onto the ESP, plus the /usr/local/bin/mkinitcpio wrapper that shadows
|
||||||
|
# the real one. Delete these lines from /etc/pacman.conf to undo it.
|
||||||
|
guard_limine_files() {
|
||||||
|
local marker="# Limine neutralised by blob_boot.sh"
|
||||||
|
if grep -q "$marker" /etc/pacman.conf; then
|
||||||
|
info "pacman.conf guard already present"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo cp /etc/pacman.conf /etc/pacman.conf.bak
|
||||||
|
|
||||||
|
# These are [options] directives, so they have to go inside that section.
|
||||||
|
# Appending to the end of the file would land them in the last repo block,
|
||||||
|
# where pacman would ignore them.
|
||||||
|
sudo awk -v marker="$marker" '
|
||||||
|
/^\[options\]/ && !done {
|
||||||
|
print
|
||||||
|
print marker
|
||||||
|
print "NoExtract = etc/pacman.d/hooks/90-mkinitcpio-install.hook"
|
||||||
|
print "NoExtract = usr/local/bin/mkinitcpio"
|
||||||
|
print "NoExtract = usr/share/libalpm/hooks/60-limine-mkinitcpio-remove-pre.hook"
|
||||||
|
print "NoExtract = usr/share/libalpm/hooks/80-limine-efi-deploy.hook"
|
||||||
|
print "NoExtract = usr/share/libalpm/hooks/90-limine-mkinitcpio-remove-post.hook"
|
||||||
|
done = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' /etc/pacman.conf.bak | sudo tee /etc/pacman.conf >/dev/null
|
||||||
|
|
||||||
|
grep -q "$marker" /etc/pacman.conf \
|
||||||
|
|| die "Failed to write the NoExtract guard. Your original is at /etc/pacman.conf.bak - restore it before doing anything else."
|
||||||
|
info "added NoExtract guard to /etc/pacman.conf (backup: /etc/pacman.conf.bak)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# pacman refuses a plain -R because omarchy depends on these. Force it, but only
|
||||||
|
# once the guard is in place, so a later reinstall cannot resurrect the hooks.
|
||||||
|
drop_limine_pkgs() {
|
||||||
|
local present=() pkg
|
||||||
|
for pkg in "$@"; do
|
||||||
|
pacman -Qq "$pkg" &>/dev/null && present+=("$pkg")
|
||||||
|
done
|
||||||
|
if (( ${#present[@]} == 0 )); then
|
||||||
|
info "nothing to remove"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
guard_limine_files
|
||||||
|
|
||||||
|
if sudo pacman -Rn --noconfirm "${present[@]}" 2>/dev/null; then
|
||||||
|
info "removed: ${present[*]}"
|
||||||
|
else
|
||||||
|
warn "the 'omarchy' metapackage depends on these; forcing removal with -Rdd"
|
||||||
|
sudo pacman -Rddn --noconfirm "${present[@]}"
|
||||||
|
info "removed: ${present[*]}"
|
||||||
|
warn "'omarchy' will now report unsatisfied dependencies. That is expected and"
|
||||||
|
warn "harmless - nothing checks them outside of a pacman transaction. If a"
|
||||||
|
warn "future omarchy upgrade reinstalls them, the guard keeps them inert."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# The ESP is 95% full. Reclaim only files that are provably unreachable, and
|
||||||
|
# leave Limine's own UKI alone so the machine keeps a working fallback.
|
||||||
|
reclaim_esp_space() {
|
||||||
|
local machine_id=$1 backup=$2
|
||||||
|
log "Reclaiming space on the ESP ($(df -h --output=avail /boot | tail -1 | tr -d ' ') free)"
|
||||||
|
|
||||||
|
# Limine's entry tool writes one kernel directory per machine-id. A
|
||||||
|
# directory keyed by a different machine-id is left over from a previous
|
||||||
|
# install and nothing in NVRAM or limine.conf can reach it.
|
||||||
|
local dir name
|
||||||
|
shopt -s nullglob
|
||||||
|
for dir in /boot/[0-9a-f]*; do
|
||||||
|
name=${dir##*/}
|
||||||
|
[[ ${#name} -eq 32 && $name =~ ^[0-9a-f]+$ ]] || continue
|
||||||
|
[[ $name == "$machine_id" ]] && { info "keeping $name (this machine)"; continue; }
|
||||||
|
info "removing stale kernel dir $name ($(sudo du -sh "$dir" | cut -f1))"
|
||||||
|
sudo rm -rf "$dir"
|
||||||
|
done
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
# arch-linux.efi is what the mkinitcpio preset wrote; limine.conf boots
|
||||||
|
# omarchy_linux.efi and never reads it. Only drop it while the UKI that
|
||||||
|
# Limine actually boots is present, so a fallback always survives.
|
||||||
|
if [ -f /boot/EFI/Linux/arch-linux.efi ]; then
|
||||||
|
if [ -f /boot/EFI/Linux/omarchy_linux.efi ] && ! grep -q "arch-linux.efi" /boot/limine.conf 2>/dev/null; then
|
||||||
|
info "removing unreferenced UKI arch-linux.efi ($(sudo du -sh /boot/EFI/Linux/arch-linux.efi | cut -f1))"
|
||||||
|
sudo rm -f /boot/EFI/Linux/arch-linux.efi
|
||||||
|
else
|
||||||
|
warn "arch-linux.efi is still referenced; keeping it"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local avail_kb
|
||||||
|
avail_kb=$(df --output=avail -k /boot | tail -1 | tr -d ' ')
|
||||||
|
info "ESP now has $((avail_kb / 1024)) MB free"
|
||||||
|
(( avail_kb > 61440 )) || die "Only $((avail_kb / 1024)) MB free; an initramfs needs ~60 MB. Nothing further has been changed."
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_standard_initramfs() {
|
||||||
|
log "Restoring a standard kernel + initramfs layout"
|
||||||
|
|
||||||
|
# btrfs-overlayfs ships with limine-mkinitcpio-hook and is about to vanish.
|
||||||
|
# omarchy_hooks.conf assigns HOOKS wholesale, so filter the hook out from a
|
||||||
|
# drop-in that sorts after it instead of editing an Omarchy-managed file.
|
||||||
|
sudo tee /etc/mkinitcpio.conf.d/zz-no-limine.conf >/dev/null <<'EOF'
|
||||||
|
# Written when this machine was migrated from Limine back to GRUB.
|
||||||
|
# btrfs-overlayfs comes from limine-mkinitcpio-hook, which is no longer
|
||||||
|
# installed, and this root filesystem is ext4 - so drop the hook rather than
|
||||||
|
# let mkinitcpio fail on a missing one.
|
||||||
|
_hooks=()
|
||||||
|
for _hook in "${HOOKS[@]}"; do
|
||||||
|
[[ $_hook == "btrfs-overlayfs" ]] || _hooks+=("$_hook")
|
||||||
|
done
|
||||||
|
HOOKS=("${_hooks[@]}")
|
||||||
|
unset _hooks _hook
|
||||||
|
EOF
|
||||||
|
info "wrote /etc/mkinitcpio.conf.d/zz-no-limine.conf"
|
||||||
|
|
||||||
|
# Only the 'default' preset: Limine's 51 MB UKI is still on the ESP as a
|
||||||
|
# fallback and a fallback initramfs will not fit beside it. The cleanup
|
||||||
|
# step turns the fallback on once that space comes back.
|
||||||
|
sudo tee /etc/mkinitcpio.d/linux.preset >/dev/null <<'EOF'
|
||||||
|
# mkinitcpio preset file for the 'linux' package
|
||||||
|
|
||||||
|
#ALL_config="/etc/mkinitcpio.conf"
|
||||||
|
ALL_kver="/boot/vmlinuz-linux"
|
||||||
|
|
||||||
|
PRESETS=('default')
|
||||||
|
|
||||||
|
#default_config="/etc/mkinitcpio.conf"
|
||||||
|
default_image="/boot/initramfs-linux.img"
|
||||||
|
#default_uki="/boot/EFI/Linux/arch-linux.efi"
|
||||||
|
default_options=""
|
||||||
|
|
||||||
|
#fallback_config="/etc/mkinitcpio.conf"
|
||||||
|
fallback_image="/boot/initramfs-linux-fallback.img"
|
||||||
|
#fallback_uki="/boot/EFI/Linux/arch-linux-fallback.efi"
|
||||||
|
fallback_options="-S autodetect"
|
||||||
|
EOF
|
||||||
|
info "wrote /etc/mkinitcpio.d/linux.preset (image, not UKI)"
|
||||||
|
|
||||||
|
# The 'limine' package itself stays for now - EFI/limine plus limine.conf
|
||||||
|
# remain a working fallback. Only the pieces that hijack mkinitcpio go.
|
||||||
|
log "Removing Limine's mkinitcpio integration"
|
||||||
|
drop_limine_pkgs limine-mkinitcpio-hook limine-snapper-sync
|
||||||
|
[ -e /usr/local/bin/mkinitcpio ] && warn "/usr/local/bin/mkinitcpio still shadows /usr/bin/mkinitcpio"
|
||||||
|
|
||||||
|
log "Building the initramfs"
|
||||||
|
rebuild_initramfs
|
||||||
|
[ -f /boot/initramfs-linux.img ] || die "mkinitcpio produced no /boot/initramfs-linux.img. Limine is still bootable - do NOT reboot into GRUB."
|
||||||
|
[ -f /boot/vmlinuz-linux ] || die "/boot/vmlinuz-linux is missing."
|
||||||
|
info "initramfs: $(du -h /boot/initramfs-linux.img | cut -f1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The previous GRUB setup booted a UKI: 10_linux had been made non-executable
|
||||||
|
# and a custom 15_uki emitted a bare 'uki' command in its place. That is why the
|
||||||
|
# old menu listed Windows and Ubuntu but no Arch entry at all. Back on a normal
|
||||||
|
# kernel + initramfs, that has to be undone or the menu cannot boot this system.
|
||||||
|
configure_grub_scripts() {
|
||||||
|
log "Fixing the GRUB menu generators"
|
||||||
|
|
||||||
|
if [ -f /etc/grub.d/10_linux ] && [ ! -x /etc/grub.d/10_linux ]; then
|
||||||
|
sudo chmod +x /etc/grub.d/10_linux
|
||||||
|
info "enabled 10_linux (generates the Arch entries)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x /etc/grub.d/15_uki ]; then
|
||||||
|
sudo chmod -x /etc/grub.d/15_uki
|
||||||
|
info "disabled 15_uki (no unified kernel image any more)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -x /etc/grub.d/30_os-prober ] || { sudo chmod +x /etc/grub.d/30_os-prober; info "enabled 30_os-prober"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
install_grub() {
|
||||||
|
local esp_uuid=$1
|
||||||
|
log "Configuring GRUB"
|
||||||
|
|
||||||
|
set_grub_key() {
|
||||||
|
local key=$1 val=$2
|
||||||
|
if grep -q "^${key}=" /etc/default/grub; then
|
||||||
|
sudo sed -i "s|^${key}=.*|${key}=${val}|" /etc/default/grub
|
||||||
|
elif grep -qE "^#\s*${key}=" /etc/default/grub; then
|
||||||
|
sudo sed -i "0,/^#\s*${key}=.*/s||${key}=${val}|" /etc/default/grub
|
||||||
|
else
|
||||||
|
echo "${key}=${val}" | sudo tee -a /etc/default/grub >/dev/null
|
||||||
|
fi
|
||||||
|
info "${key}=${val}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Carry over the exact kernel command line Limine was booting, so Plymouth
|
||||||
|
# and the quiet splash behave the way they do today.
|
||||||
|
set_grub_key GRUB_CMDLINE_LINUX_DEFAULT '"rtc_cmos.use_acpi_alarm=1 initramfs_async=0 quiet splash loglevel=0 systemd.show_status=false rd.udev.log_level=0 vt.global_cursor_default=0"'
|
||||||
|
set_grub_key GRUB_DISABLE_OS_PROBER 'false'
|
||||||
|
set_grub_key GRUB_TIMEOUT '5'
|
||||||
|
set_grub_key GRUB_TIMEOUT_STYLE 'menu'
|
||||||
|
set_grub_key GRUB_GFXPAYLOAD_LINUX 'keep'
|
||||||
|
|
||||||
|
log "Installing GRUB to the ESP"
|
||||||
|
sudo grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --recheck
|
||||||
|
[ -f /boot/EFI/GRUB/grubx64.efi ] || die "grub-install produced no /boot/EFI/GRUB/grubx64.efi."
|
||||||
|
|
||||||
|
generate_menu "$esp_uuid"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# OS detection
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
generate_menu() {
|
||||||
|
local esp_uuid=${1:-$(findmnt -no UUID /boot)}
|
||||||
|
|
||||||
|
log "Generating the GRUB menu (os-prober scans for other systems)"
|
||||||
|
sudo grub-mkconfig -o /boot/grub/grub.cfg
|
||||||
|
|
||||||
|
# os-prober can miss Windows when its EFI System Partition is the very one
|
||||||
|
# mounted at /boot, as it is here. Chainload it explicitly rather than ship
|
||||||
|
# a menu with no Windows in it.
|
||||||
|
if grep -qi "bootmgfw.efi" /boot/grub/grub.cfg; then
|
||||||
|
info "os-prober found Windows"
|
||||||
|
elif [ -f /boot/EFI/Microsoft/Boot/bootmgfw.efi ]; then
|
||||||
|
warn "os-prober missed Windows; adding an explicit chainload entry"
|
||||||
|
sudo tee /etc/grub.d/40_custom >/dev/null <<EOF
|
||||||
|
#!/bin/sh
|
||||||
|
exec tail -n +3 \$0
|
||||||
|
# Entries below are added to the end of the GRUB menu.
|
||||||
|
|
||||||
|
# os-prober does not reliably detect Windows when its EFI System Partition is
|
||||||
|
# the same one mounted at /boot, so chainload the Windows boot manager directly.
|
||||||
|
menuentry "Windows Boot Manager" --class windows --class os {
|
||||||
|
insmod part_gpt
|
||||||
|
insmod fat
|
||||||
|
insmod chain
|
||||||
|
search --no-floppy --fs-uuid --set=root ${esp_uuid}
|
||||||
|
chainloader /EFI/Microsoft/Boot/bootmgfw.efi
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
sudo chmod +x /etc/grub.d/40_custom
|
||||||
|
sudo grub-mkconfig -o /boot/grub/grub.cfg
|
||||||
|
fi
|
||||||
|
|
||||||
|
verify_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# Refuse to leave the machine with a menu that cannot boot it.
|
||||||
|
verify_menu() {
|
||||||
|
local entries
|
||||||
|
entries=$(grep -cE "^\s*menuentry " /boot/grub/grub.cfg || true)
|
||||||
|
|
||||||
|
grep -qE "^\s*menuentry .*(Arch|Linux)" /boot/grub/grub.cfg \
|
||||||
|
|| die "grub.cfg contains no Arch entry. Check that /etc/grub.d/10_linux is executable and that /boot/vmlinuz-linux and /boot/initramfs-linux.img both exist. Do NOT reboot into GRUB until this is fixed."
|
||||||
|
|
||||||
|
grep -q "initramfs-linux.img" /boot/grub/grub.cfg \
|
||||||
|
|| warn "no initramfs referenced in grub.cfg - check the Arch entry by hand"
|
||||||
|
|
||||||
|
info "$entries menu entries generated"
|
||||||
|
}
|
||||||
|
|
||||||
|
list_menu_entries() {
|
||||||
|
grep -E "^\s*(menuentry|submenu) '" /boot/grub/grub.cfg \
|
||||||
|
| sed -E "s/^[[:space:]]*(menuentry|submenu) '([^']*)'.*/ - \2/"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_os() {
|
||||||
|
using_grub || die "GRUB is not installed yet. Run: $0 grub"
|
||||||
|
require_sudo
|
||||||
|
generate_menu
|
||||||
|
echo
|
||||||
|
info "GRUB menu entries:"
|
||||||
|
list_menu_entries
|
||||||
|
ok "OS detection complete."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Boot-time failures
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Two units fail on every boot on this machine, both left over from a btrfs
|
||||||
|
# layout that no longer exists - the root filesystem is ext4.
|
||||||
|
fix_failing_boot_units() {
|
||||||
|
log "Clearing boot-time unit failures"
|
||||||
|
|
||||||
|
# fstab still lists a btrfs hibernation swapfile that was never created.
|
||||||
|
# zram provides this machine's swap.
|
||||||
|
if grep -q "^/swap/swapfile" /etc/fstab && [ ! -f /swap/swapfile ]; then
|
||||||
|
sudo cp /etc/fstab /etc/fstab.bak
|
||||||
|
sudo sed -i '/^# Btrfs swapfile for system hibernation$/d; /^\/swap\/swapfile/d' /etc/fstab
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
info "removed the missing /swap/swapfile entry from fstab (backup: /etc/fstab.bak)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# snapper only manages btrfs subvolumes; on ext4 its timers fail nightly.
|
||||||
|
if systemctl list-unit-files snapper-cleanup.timer &>/dev/null \
|
||||||
|
&& [ "$(findmnt -no FSTYPE /)" != "btrfs" ]; then
|
||||||
|
sudo systemctl disable --now snapper-cleanup.timer snapper-timeline.timer &>/dev/null || true
|
||||||
|
sudo systemctl reset-failed snapper-cleanup.service &>/dev/null || true
|
||||||
|
info "disabled snapper timers (root is $(findmnt -no FSTYPE /), not btrfs)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Boot order
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# efibootmgr prints "Boot0000* GRUB<TAB>HD(1,GPT,...)" on this machine - the
|
||||||
|
# device path is there even without -v - so match the label field exactly
|
||||||
|
# rather than anchoring on end of line.
|
||||||
|
efi_entry_num() {
|
||||||
|
sudo efibootmgr | awk -F'\t' -v want="$1" '
|
||||||
|
$1 ~ /^Boot[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]/ {
|
||||||
|
num = substr($1, 5, 4)
|
||||||
|
label = $1
|
||||||
|
sub(/^Boot[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]\*? +/, "", label)
|
||||||
|
if (label == want) { print num; exit }
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Boot GRUB exactly once, leaving Limine as the standing default. A failed
|
||||||
|
# GRUB boot then needs no user intervention at all to recover.
|
||||||
|
set_boot_next() {
|
||||||
|
log "Arming GRUB for a one-shot test boot"
|
||||||
|
local grub_num limine_num
|
||||||
|
grub_num=$(efi_entry_num GRUB)
|
||||||
|
[ -n "$grub_num" ] || die "No GRUB entry in NVRAM after grub-install. BootOrder is untouched, so this machine still boots Limine."
|
||||||
|
|
||||||
|
# grub-install prepends its own entry to BootOrder, which would make GRUB the
|
||||||
|
# standing default and defeat the whole point of a one-shot test. Put Limine
|
||||||
|
# back in front so a failed GRUB boot recovers on a plain power cycle.
|
||||||
|
limine_num=$(efi_entry_num Limine)
|
||||||
|
if [ -n "$limine_num" ]; then
|
||||||
|
local current_order new_order n
|
||||||
|
current_order=$(sudo efibootmgr | sed -n 's/^BootOrder: //p')
|
||||||
|
new_order=$limine_num
|
||||||
|
for n in ${current_order//,/ }; do
|
||||||
|
[ "$n" = "$limine_num" ] || new_order+=",$n"
|
||||||
|
done
|
||||||
|
sudo efibootmgr -o "$new_order" >/dev/null
|
||||||
|
info "BootOrder: $new_order (Limine first - the standing default)"
|
||||||
|
else
|
||||||
|
warn "no Limine entry in NVRAM; GRUB will be the standing default with no automatic fallback"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set BootNext last: it must survive the BootOrder rewrite above.
|
||||||
|
sudo efibootmgr -n "$grub_num" >/dev/null
|
||||||
|
info "BootNext=$grub_num (GRUB) - next restart only"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_boot_order() {
|
||||||
|
log "Setting the firmware boot order"
|
||||||
|
|
||||||
|
local grub_num limine_num current_order new_order n
|
||||||
|
grub_num=$(efi_entry_num GRUB)
|
||||||
|
limine_num=$(efi_entry_num Limine)
|
||||||
|
|
||||||
|
[ -n "$grub_num" ] || die "No GRUB entry in NVRAM after grub-install. Limine is still first in the boot order, so the machine remains bootable."
|
||||||
|
|
||||||
|
current_order=$(sudo efibootmgr | sed -n 's/^BootOrder: //p')
|
||||||
|
new_order=$grub_num
|
||||||
|
[ -n "$limine_num" ] && new_order+=",$limine_num"
|
||||||
|
for n in ${current_order//,/ }; do
|
||||||
|
[ "$n" = "$grub_num" ] && continue
|
||||||
|
[ "$n" = "${limine_num:-}" ] && continue
|
||||||
|
new_order+=",$n"
|
||||||
|
done
|
||||||
|
|
||||||
|
sudo efibootmgr -o "$new_order" >/dev/null
|
||||||
|
info "BootOrder: $new_order (GRUB=$grub_num, Limine=${limine_num:-none})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Retire Limine
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Delete every trace of Limine and hand its EFI fallback path to GRUB.
|
||||||
|
purge_limine() {
|
||||||
|
log "Removing Limine"
|
||||||
|
|
||||||
|
drop_limine_pkgs limine limine-mkinitcpio-hook limine-snapper-sync
|
||||||
|
|
||||||
|
# limine-snapper-sync leaves units behind that would fail on every boot once
|
||||||
|
# there is no limine.conf for them to write into.
|
||||||
|
sudo systemctl disable --now limine-snapper-sync.service limine-snapper-sync.timer &>/dev/null || true
|
||||||
|
|
||||||
|
# /boot/EFI/Linux holds only the unified kernel images Limine booted; with
|
||||||
|
# GRUB on a normal kernel + initramfs nothing reads them any more. This is
|
||||||
|
# where the 51 MB comes back.
|
||||||
|
sudo rm -rf /boot/EFI/limine /boot/EFI/Linux
|
||||||
|
sudo rm -f /boot/limine.conf /boot/limine.conf.bak /boot/limine.conf.old
|
||||||
|
sudo rm -rf /etc/limine-entry-tool.d /etc/limine-entry-tool.conf /var/lib/limine
|
||||||
|
info "removed Limine's files from the ESP"
|
||||||
|
|
||||||
|
local limine_num
|
||||||
|
limine_num=$(efi_entry_num Limine)
|
||||||
|
[ -n "$limine_num" ] && { sudo efibootmgr -b "$limine_num" -B >/dev/null; info "removed the Limine NVRAM entry"; }
|
||||||
|
|
||||||
|
# EFI/BOOT/BOOTX64.EFI is still Limine's copy. Overwrite it with GRUB so the
|
||||||
|
# firmware's default fallback path keeps working if the NVRAM entry is ever
|
||||||
|
# lost - otherwise deleting Limine leaves a dead pointer there.
|
||||||
|
log "Claiming the removable EFI fallback path for GRUB"
|
||||||
|
sudo grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable --recheck
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only worth attempting once Limine's UKI has freed up room on the ESP.
|
||||||
|
enable_fallback_initramfs() {
|
||||||
|
log "Enabling the fallback initramfs"
|
||||||
|
sudo sed -i "s|^PRESETS=.*|PRESETS=('default' 'fallback')|" /etc/mkinitcpio.d/linux.preset
|
||||||
|
if ! rebuild_initramfs; then
|
||||||
|
warn "the fallback initramfs did not build - reverting to the default preset only"
|
||||||
|
sudo sed -i "s|^PRESETS=.*|PRESETS=('default')|" /etc/mkinitcpio.d/linux.preset
|
||||||
|
sudo rm -f /boot/initramfs-linux-fallback.img
|
||||||
|
rebuild_initramfs
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_limine() {
|
||||||
|
using_grub || die "GRUB is not installed. Run: $0 grub"
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
# Only safe once the machine has actually come up through GRUB.
|
||||||
|
local current
|
||||||
|
current=$(sudo efibootmgr | sed -n 's/^BootCurrent: //p')
|
||||||
|
[ -n "$current" ] || die "Cannot determine the current boot entry."
|
||||||
|
sudo efibootmgr | grep -qE "^Boot${current}\*?[[:space:]]+GRUB" \
|
||||||
|
|| die "This session did not boot through GRUB (BootCurrent=$current). Reboot first - GRUB is armed for the next restart - then run this again."
|
||||||
|
|
||||||
|
purge_limine
|
||||||
|
set_boot_order
|
||||||
|
enable_fallback_initramfs
|
||||||
|
generate_menu
|
||||||
|
|
||||||
|
echo
|
||||||
|
df -h /boot | tail -1 | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
info "GRUB menu entries:"
|
||||||
|
list_menu_entries
|
||||||
|
ok "Limine is gone. GRUB is now the only bootloader."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Status
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
show_status() {
|
||||||
|
log "Bootloader"
|
||||||
|
if using_grub; then
|
||||||
|
info "GRUB installed at /boot/EFI/GRUB/grubx64.efi"
|
||||||
|
else
|
||||||
|
warn "GRUB is not installed"
|
||||||
|
fi
|
||||||
|
pacman -Qq limine &>/dev/null && warn "limine is still installed"
|
||||||
|
[ -e /usr/local/bin/mkinitcpio ] && warn "/usr/local/bin/mkinitcpio shadows /usr/bin/mkinitcpio"
|
||||||
|
|
||||||
|
log "Kernel images"
|
||||||
|
ls -lh /boot/vmlinuz-linux /boot/initramfs-linux*.img /boot/EFI/Linux/*.efi 2>/dev/null \
|
||||||
|
| awk '{print " " $5 "\t" $9}'
|
||||||
|
|
||||||
|
log "ESP usage"
|
||||||
|
df -h /boot | tail -1 | sed 's/^/ /'
|
||||||
|
|
||||||
|
if [ -f /boot/grub/grub.cfg ]; then
|
||||||
|
log "GRUB menu entries"
|
||||||
|
list_menu_entries
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Firmware boot order"
|
||||||
|
sudo efibootmgr 2>/dev/null | grep -E "^(BootCurrent|BootOrder|Boot[0-9A-F]{4})" \
|
||||||
|
| grep -viE "USB|Setup|Boot Menu|Diagnostics|NVMe:" | sed 's/^/ /'
|
||||||
|
|
||||||
|
log "Failed units"
|
||||||
|
systemctl --failed --no-pager --no-legend | sed 's/^/ /' || info "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
grub|migrate) migrate_to_grub "${2:-}" ;;
|
||||||
|
detect|osprobe) detect_os ;;
|
||||||
|
cleanup) cleanup_limine ;;
|
||||||
|
status) show_status ;;
|
||||||
|
-h|--help|help)
|
||||||
|
sed -n '3,11p' "$0" | sed 's/^# \?//'
|
||||||
|
;;
|
||||||
|
*) apply_splash "${1:-}" ;;
|
||||||
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user