#!/bin/bash # bos-rescue — live-ISO helper for an installed BOS that will not boot. # # Finds the installed btrfs `@` and the ESP, mounts them, then offers to # arch-chroot and/or reinstall GRUB using the same sequence as # post-install.sh / README Recovery: # UEFI: grub-install NVRAM + --removable, then grub-mkconfig # BIOS: grub-install i386-pc onto the disk hosting / # # Recovery is this script or the GRUB "snapshots" submenu (grub-btrfs). # GRUB pins rootflags=subvol=@ — a snapper-swapped default subvolume is # not what the installed grub.cfg will boot. Never snapper-rollback. # # Safe: prints the devices it will use and requires YES before writing. # Best-effort: do not use `set -e`; a failed probe must not abort the rest. set -uo pipefail MNT="${BOS_RESCUE_MNT:-}" MOUNTED_ROOT=0 MOUNTED_ESP=0 ROOT_DEV="" ESP_DEV="" ROOT_ENCRYPTED=0 bold() { printf '\033[1m%s\033[0m\n' "$1" >&2; } info() { printf ' %s\n' "$1" >&2; } warn() { printf 'WARN: %s\n' "$1" >&2; } usage() { cat <<'EOF' Usage: bos-rescue Live-ISO helper: find the installed BOS btrfs @ and ESP, mount them, then arch-chroot and/or reinstall GRUB. UEFI: grub-install (NVRAM) + grub-install --removable + grub-mkconfig BIOS: grub-install --target=i386-pc onto the disk hosting / Prints the devices it will use and asks YES before writing anything. Do not snapper-rollback. GRUB pins rootflags=subvol=@. Pick a grub-btrfs snapshot entry, or reinstall GRUB with this script. Must be run as root. Intended from the live ISO (SUPER+Return). EOF } need_root() { if [[ "$(id -u)" -ne 0 ]]; then echo "bos-rescue must run as root (sudo bos-rescue)." >&2 exit 1 fi } confirm_yes() { local prompt="$1" local reply="" printf '%s [type YES]: ' "$prompt" >&2 read -r reply || return 1 [[ "$reply" == "YES" ]] } is_live_iso() { [[ -d /run/archiso ]] || [[ -x /usr/local/bin/bos-live-setup ]] } already_on_installed() { # Installed BOS: / is the @ subvolume and this is not the live medium. is_live_iso && return 1 local src opts src="$(findmnt -no SOURCE / 2>/dev/null | sed 's/\[.*\]//')" opts="$(findmnt -no OPTIONS / 2>/dev/null || true)" [[ -n "$src" ]] || return 1 [[ "$opts" == *subvol=/@* || "$opts" == *subvol=@* ]] || return 1 [[ -f /etc/os-release ]] && grep -qE '^ID=bos$' /etc/os-release } pick_mnt() { if [[ -n "$MNT" ]]; then return fi if findmnt -n /mnt >/dev/null 2>&1; then MNT=/mnt/bos-rescue info "/mnt is already a mountpoint — using $MNT" else MNT=/mnt fi } lsblk_line() { lsblk -pnlo NAME,FSTYPE,SIZE,LABEL,UUID,PARTTYPENAME "$1" 2>/dev/null | head -n1 } # Open LUKS containers so a later btrfs scan can see @. offer_luks() { command -v cryptsetup >/dev/null || return 0 local dev name reply while read -r dev; do [[ -n "$dev" ]] || continue [[ -e "$dev" ]] || continue if lsblk -no TYPE "$dev" 2>/dev/null | grep -qx crypt; then continue fi # Skip already-mapped parents. if lsblk -nlo TYPE "$dev" 2>/dev/null | grep -qx crypt; then continue fi printf '\nLUKS container: %s\n %s\n' "$dev" "$(lsblk_line "$dev")" >&2 printf 'Unlock this container? [y/N]: ' >&2 read -r reply || reply="" if [[ "$reply" == [yY] ]]; then name="bos-rescue-$(basename "$dev")" if cryptsetup open "$dev" "$name"; then info "opened $dev as /dev/mapper/$name" else warn "cryptsetup open failed for $dev" fi fi done < <(lsblk -pnlo NAME,FSTYPE | awk '$2 == "crypto_LUKS" { print $1 }') } # Probe a btrfs device for an @ subvolume that looks like BOS (or any @). # Prints: DEVICEKINDPRETTY where KIND is bos|other probe_btrfs_dev() { local dev="$1" local tmp pretty kind id tmp="$(mktemp -d /tmp/bos-rescue.XXXXXX)" || return 1 kind="other" pretty="" if mount -o ro,subvol=@ "$dev" "$tmp" 2>/dev/null; then if [[ -f "$tmp/etc/os-release" ]]; then id="$(grep -E '^ID=' "$tmp/etc/os-release" | head -n1 | cut -d= -f2- | tr -d '"')" pretty="$(grep -E '^PRETTY_NAME=' "$tmp/etc/os-release" | head -n1 | cut -d= -f2- | tr -d '"')" [[ "$id" == "bos" ]] && kind="bos" fi umount "$tmp" 2>/dev/null || umount -l "$tmp" 2>/dev/null || true rmdir "$tmp" 2>/dev/null || true printf '%s\t%s\t%s\n' "$dev" "$kind" "${pretty:-btrfs @}" return 0 fi # Some volumes only accept a top-level probe first. if mount -o ro,subvolid=5 "$dev" "$tmp" 2>/dev/null; then if [[ -d "$tmp/@" ]] || btrfs subvolume show "$tmp/@" &>/dev/null; then umount "$tmp" 2>/dev/null || umount -l "$tmp" 2>/dev/null || true rmdir "$tmp" 2>/dev/null || true printf '%s\t%s\t%s\n' "$dev" "other" "btrfs @ (unreadable os-release)" return 0 fi umount "$tmp" 2>/dev/null || umount -l "$tmp" 2>/dev/null || true fi rmdir "$tmp" 2>/dev/null || true return 1 } find_root_candidates() { local dev while read -r dev; do [[ -n "$dev" ]] || continue probe_btrfs_dev "$dev" || true done < <(lsblk -pnlo NAME,FSTYPE | awk '$2 == "btrfs" { print $1 }') } # Prefer the ESP named in the installed fstab; else EFI type / BOS bits. find_esp_for_root() { local root="$1" local tmp fstab_uuid fstab_dev dev fstype parttype label tmp="$(mktemp -d /tmp/bos-rescue.XXXXXX)" || return 1 if mount -o ro,subvol=@ "$root" "$tmp" 2>/dev/null; then if [[ -f "$tmp/etc/fstab" ]]; then fstab_uuid="$(awk '$2 == "/boot/efi" { if ($1 ~ /^UUID=/) { sub(/^UUID=/, "", $1); print $1; exit } }' "$tmp/etc/fstab")" fi umount "$tmp" 2>/dev/null || umount -l "$tmp" 2>/dev/null || true fi rmdir "$tmp" 2>/dev/null || true if [[ -n "${fstab_uuid:-}" ]]; then fstab_dev="$(blkid -U "$fstab_uuid" 2>/dev/null || true)" if [[ -n "$fstab_dev" ]]; then printf '%s\n' "$fstab_dev" return 0 fi fi local best="" scored=0 score # PARTTYPE is the GPT GUID — no spaces, unlike PARTTYPENAME ("EFI System"). local efi_guid="c12a7328-f81f-11d2-ba4b-00a716dde993" while read -r dev fstype parttype; do [[ -n "$dev" ]] || continue score=0 [[ "$fstype" == "vfat" || "$fstype" == "fat32" || "$fstype" == "FAT-32" ]] && score=$((score + 1)) [[ "${parttype,,}" == "$efi_guid" ]] && score=$((score + 3)) if (( score > scored )); then best="$dev" scored=$score fi done < <(lsblk -pnlo NAME,FSTYPE,PARTTYPE) # Prefer an ESP that already has BOS or removable fallback bits. local probe mp for dev in $best $(lsblk -pnlo NAME,FSTYPE | awk '$2 == "vfat" { print $1 }'); do [[ -n "$dev" ]] || continue mp="$(mktemp -d /tmp/bos-rescue.XXXXXX)" || continue if mount -o ro "$dev" "$mp" 2>/dev/null; then if [[ -f "$mp/EFI/BOS/grubx64.efi" || -f "$mp/EFI/BOOT/BOOTX64.EFI" ]]; then umount "$mp" 2>/dev/null || true rmdir "$mp" 2>/dev/null || true printf '%s\n' "$dev" return 0 fi umount "$mp" 2>/dev/null || true fi rmdir "$mp" 2>/dev/null || true done [[ -n "$best" ]] && printf '%s\n' "$best" } select_from_list() { local title="$1" shift local -a items=("$@") local i choice if (( ${#items[@]} == 0 )); then return 1 fi if (( ${#items[@]} == 1 )); then printf '%s\n' "${items[0]}" return 0 fi bold "$title" for i in "${!items[@]}"; do printf ' %d) %s\n' "$((i + 1))" "${items[$i]}" >&2 done printf 'Select [1-%d]: ' "${#items[@]}" >&2 read -r choice || return 1 if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#items[@]} )); then printf '%s\n' "${items[$((choice - 1))]}" return 0 fi return 1 } discover_and_choose() { bold "Scanning for an installed BOS (btrfs @) …" offer_luks local -a bos_devs=() other_devs=() local dev kind pretty line while IFS=$'\t' read -r dev kind pretty; do [[ -n "$dev" ]] || continue line="$dev (${pretty:-$kind})" if [[ "$kind" == "bos" ]]; then bos_devs+=("$dev") else other_devs+=("$dev") fi info "found $line" done < <(find_root_candidates) if (( ${#bos_devs[@]} == 0 && ${#other_devs[@]} == 0 )); then echo "No btrfs @ subvolume found. Unlock LUKS first if the install is encrypted." >&2 return 1 fi if (( ${#bos_devs[@]} == 1 )); then ROOT_DEV="${bos_devs[0]}" info "Using BOS root $ROOT_DEV" elif (( ${#bos_devs[@]} > 1 )); then ROOT_DEV="$(select_from_list "More than one BOS @ found:" "${bos_devs[@]}")" || return 1 else warn "No ID=bos os-release on @ — offering every btrfs @ found" ROOT_DEV="$(select_from_list "Select the installed root device:" "${other_devs[@]}")" || return 1 fi ESP_DEV="$(find_esp_for_root "$ROOT_DEV" || true)" if [[ -n "$ESP_DEV" ]]; then info "Using ESP $ESP_DEV" fi if [[ -z "$ESP_DEV" ]]; then local -a esps=() while read -r dev; do [[ -n "$dev" ]] && esps+=("$dev") done < <(lsblk -pnlo NAME,FSTYPE,PARTTYPE | awk ' $2 == "vfat" || tolower($3) == "c12a7328-f81f-11d2-ba4b-00a716dde993" { print $1 } ') if (( ${#esps[@]} == 0 )); then warn "No ESP found. GRUB reinstall on UEFI will fail; chroot is still available." else ESP_DEV="$(select_from_list "Select the EFI System Partition:" "${esps[@]}")" || true fi fi } mount_install() { pick_mnt mkdir -p "$MNT" if ! findmnt -n "$MNT" >/dev/null 2>&1; then if ! mount -o subvol=@ "$ROOT_DEV" "$MNT"; then warn "failed to mount $ROOT_DEV subvol=@ at $MNT" return 1 fi MOUNTED_ROOT=1 fi if [[ -n "$ESP_DEV" ]]; then mkdir -p "$MNT/boot/efi" if ! findmnt -n "$MNT/boot/efi" >/dev/null 2>&1; then if mount "$ESP_DEV" "$MNT/boot/efi"; then MOUNTED_ESP=1 else warn "failed to mount ESP $ESP_DEV at $MNT/boot/efi" fi fi fi if [[ "$(lsblk -no TYPE "$ROOT_DEV" 2>/dev/null)" == "crypt" ]]; then ROOT_ENCRYPTED=1 fi } unmount_install() { if [[ "$MOUNTED_ESP" == "1" ]]; then umount "$MNT/boot/efi" 2>/dev/null || umount -l "$MNT/boot/efi" 2>/dev/null || true MOUNTED_ESP=0 fi if [[ "$MOUNTED_ROOT" == "1" ]]; then umount "$MNT" 2>/dev/null || umount -l "$MNT" 2>/dev/null || true MOUNTED_ROOT=0 fi } print_plan() { echo >&2 bold "Devices" info "root: ${ROOT_DEV:-unset} $([[ -n "$ROOT_DEV" ]] && lsblk_line "$ROOT_DEV")" info "ESP: ${ESP_DEV:-none} $([[ -n "$ESP_DEV" ]] && lsblk_line "$ESP_DEV")" info "mount: ${MNT:-unset}" if [[ -d /sys/firmware/efi ]]; then info "firmware: UEFI" else info "firmware: BIOS" fi if [[ "$ROOT_ENCRYPTED" == "1" ]]; then info "root is LUKS (grub-install will include cryptodisk modules)" fi echo >&2 info "Recovery is grub-btrfs (GRUB snapshots submenu) or this GRUB reinstall." info "GRUB pins rootflags=subvol=@ — do not swap the default subvolume." } run_in_target() { local cmd="$1" if command -v arch-chroot >/dev/null; then arch-chroot "$MNT" bash -c "$cmd" return $? fi # arch-install-scripts is not guaranteed on the ISO — bind the API # filesystems the same way arch-chroot would, then chroot. mount --bind /proc "$MNT/proc" 2>/dev/null || mount -t proc proc "$MNT/proc" mount --bind /sys "$MNT/sys" 2>/dev/null || mount -t sysfs sys "$MNT/sys" mount --bind /dev "$MNT/dev" 2>/dev/null || mount -t devtmpfs udev "$MNT/dev" mkdir -p "$MNT/run" mount --bind /run "$MNT/run" 2>/dev/null || mount -t tmpfs tmpfs "$MNT/run" if [[ -d /sys/firmware/efi ]]; then mkdir -p "$MNT/sys/firmware/efi/efivars" mount -t efivarfs efivarfs "$MNT/sys/firmware/efi/efivars" 2>/dev/null || true fi chroot "$MNT" bash -c "$cmd" local rc=$? umount "$MNT/sys/firmware/efi/efivars" 2>/dev/null || true umount "$MNT/run" 2>/dev/null || true umount "$MNT/dev" 2>/dev/null || true umount "$MNT/sys" 2>/dev/null || true umount "$MNT/proc" 2>/dev/null || true return "$rc" } grub_commands_preview() { if [[ -d /sys/firmware/efi ]]; then cat <<'EOF' >&2 grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=BOS --recheck grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck grub-mkconfig -o /boot/grub/grub.cfg EOF else cat <<'EOF' >&2 grub-install --target=i386-pc --recheck grub-mkconfig -o /boot/grub/grub.cfg EOF fi } reinstall_grub() { if [[ ! -d "$MNT/boot" ]]; then warn "target $MNT/boot missing — mount the installed @ first" return 1 fi echo >&2 bold "This will write a bootloader using:" info "root ${ROOT_DEV:-/} ESP ${ESP_DEV:-n/a} chroot $MNT" grub_commands_preview echo >&2 if ! confirm_yes "Reinstall GRUB now?"; then info "skipped" return 0 fi # Same sequence as post-install.sh (UEFI NVRAM + --removable, or BIOS MBR). local script script="$(cat <<'EOS' set -uo pipefail ROOT_SRC="$(findmnt -no SOURCE / | sed 's/\[.*\]//')" if [[ "$(lsblk -no TYPE "$ROOT_SRC" 2>/dev/null)" == "crypt" ]]; then ROOT_ENCRYPTED=1 else ROOT_ENCRYPTED=0 fi if [[ "$ROOT_ENCRYPTED" == "1" ]] && [[ -f /etc/default/grub ]] \ && ! grep -q '^GRUB_ENABLE_CRYPTODISK=' /etc/default/grub; then echo 'GRUB_ENABLE_CRYPTODISK=y' >> /etc/default/grub \ || echo "WARN: adding GRUB_ENABLE_CRYPTODISK failed" fi if ! command -v grub-install >/dev/null; then echo "ERROR: grub-install not found in the installed system" >&2 exit 1 fi CRYPT_MODULES=() [[ "$ROOT_ENCRYPTED" == "1" ]] && CRYPT_MODULES=(--modules="cryptodisk luks luks2") if [[ -d /sys/firmware/efi ]]; then grub-install --target=x86_64-efi --efi-directory=/boot/efi \ --bootloader-id=BOS --recheck "${CRYPT_MODULES[@]}" \ || echo "WARN: grub-install (nvram) failed" grub-install --target=x86_64-efi --efi-directory=/boot/efi \ --removable --recheck "${CRYPT_MODULES[@]}" \ || echo "WARN: grub-install (removable) failed" else ROOT_DEV="$(findmnt -no SOURCE / | sed 's/\[.*\]//')" ROOT_DISK="$(lsblk -no pkname "$ROOT_DEV" 2>/dev/null)" if [[ -n "$ROOT_DISK" ]]; then grub-install --target=i386-pc --recheck "${CRYPT_MODULES[@]}" "/dev/$ROOT_DISK" \ || echo "WARN: grub-install (BIOS) failed" else echo "WARN: could not determine the disk hosting / — BIOS grub-install skipped" fi fi if command -v grub-mkconfig >/dev/null; then grub-mkconfig -o /boot/grub/grub.cfg || echo "WARN: grub-mkconfig failed" else echo "WARN: grub-mkconfig not found" fi EOS )" if run_in_target "$script"; then bold "GRUB reinstall finished." info "Firmware that lost its NVRAM entry can still boot EFI/BOOT/BOOTX64.EFI." else warn "GRUB reinstall returned non-zero — see messages above" return 1 fi } do_chroot() { if [[ ! -d "$MNT/etc" ]]; then warn "target $MNT is not a mounted system" return 1 fi bold "Entering chroot at $MNT (exit to return)." if command -v arch-chroot >/dev/null; then arch-chroot "$MNT" else run_in_target "exec bash -l" fi } menu_live() { local choice while true; do echo bold "bos-rescue" print_plan cat <<'EOF' >&2 1) arch-chroot into the installed system 2) Reinstall GRUB (NVRAM + --removable + grub-mkconfig) 3) Reinstall GRUB, then chroot 4) Unmount and quit q) Quit (leave mounts) EOF printf 'Choice: ' >&2 read -r choice || choice="q" case "$choice" in 1) do_chroot ;; 2) reinstall_grub ;; 3) reinstall_grub; do_chroot ;; 4) unmount_install; bold "Unmounted."; return 0 ;; q|Q) info "Leaving mounts in place at $MNT"; return 0 ;; *) info "unknown choice" ;; esac done } menu_installed() { ROOT_DEV="$(findmnt -no SOURCE / | sed 's/\[.*\]//')" ESP_DEV="$(findmnt -no SOURCE /boot/efi 2>/dev/null || true)" MNT="/" if [[ "$(lsblk -no TYPE "$ROOT_DEV" 2>/dev/null)" == "crypt" ]]; then ROOT_ENCRYPTED=1 fi echo bold "Already running the installed BOS (not the live ISO)." info "Root and ESP are already mounted — chroot is not needed." print_plan if confirm_yes "Reinstall GRUB on this running system?"; then # Running on the installed root: no extra mount/chroot. local old_mnt="$MNT" MNT="/" # run_in_target would chroot into / — just run locally. if [[ -d /sys/firmware/efi && -z "$ESP_DEV" ]]; then warn " /boot/efi is not mounted — refusing to write" return 1 fi bash -c "$(cat <<'EOS' set -uo pipefail ROOT_SRC="$(findmnt -no SOURCE / | sed 's/\[.*\]//')" if [[ "$(lsblk -no TYPE "$ROOT_SRC" 2>/dev/null)" == "crypt" ]]; then ROOT_ENCRYPTED=1 else ROOT_ENCRYPTED=0 fi if [[ "$ROOT_ENCRYPTED" == "1" ]] && [[ -f /etc/default/grub ]] \ && ! grep -q '^GRUB_ENABLE_CRYPTODISK=' /etc/default/grub; then echo 'GRUB_ENABLE_CRYPTODISK=y' >> /etc/default/grub \ || echo "WARN: adding GRUB_ENABLE_CRYPTODISK failed" fi CRYPT_MODULES=() [[ "$ROOT_ENCRYPTED" == "1" ]] && CRYPT_MODULES=(--modules="cryptodisk luks luks2") if [[ -d /sys/firmware/efi ]]; then grub-install --target=x86_64-efi --efi-directory=/boot/efi \ --bootloader-id=BOS --recheck "${CRYPT_MODULES[@]}" \ || echo "WARN: grub-install (nvram) failed" grub-install --target=x86_64-efi --efi-directory=/boot/efi \ --removable --recheck "${CRYPT_MODULES[@]}" \ || echo "WARN: grub-install (removable) failed" else ROOT_DISK="$(lsblk -no pkname "$ROOT_SRC" 2>/dev/null)" if [[ -n "$ROOT_DISK" ]]; then grub-install --target=i386-pc --recheck "${CRYPT_MODULES[@]}" "/dev/$ROOT_DISK" \ || echo "WARN: grub-install (BIOS) failed" fi fi grub-mkconfig -o /boot/grub/grub.cfg || echo "WARN: grub-mkconfig failed" EOS )" MNT="$old_mnt" else info "skipped" fi } main() { if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage exit 0 fi need_root local req for req in mount lsblk blkid findmnt; do if ! command -v "$req" >/dev/null; then echo "bos-rescue: missing required tool '$req'" >&2 exit 1 fi done bold "bos-rescue" info "Live-ISO recovery helper. Prints devices and asks YES before writing." info "Use grub-btrfs (GRUB snapshots submenu) for a bootable snapshot." info "Do not snapper-rollback — GRUB pins rootflags=subvol=@." echo if already_on_installed; then menu_installed return 0 fi if ! is_live_iso; then warn "This does not look like the BOS live ISO (/run/archiso missing)." info "Continuing anyway — will scan disks for a BOS @." fi discover_and_choose || exit 1 print_plan if ! confirm_yes "Mount these devices and continue?"; then info "nothing mounted" exit 0 fi mount_install || exit 1 menu_live } main "$@"