Skip to content

Instantly share code, notes, and snippets.

@sumodirjo
Last active July 24, 2026 14:45
Show Gist options
  • Select an option

  • Save sumodirjo/4baa45389a653ee0146abc3a97d80447 to your computer and use it in GitHub Desktop.

Select an option

Save sumodirjo/4baa45389a653ee0146abc3a97d80447 to your computer and use it in GitHub Desktop.
Boot Doctor
#!/usr/bin/env bash
#
# boot-doctor.sh — UEFI / GRUB / dual-boot diagnostic and repair helper
#
# Written for an Intel NUC8i7BEH with:
# - SATA SSD holding Windows + the ESP
# - NVMe holding Ubuntu
# - flaky CMOS battery (NVRAM boot entries do not survive power loss)
#
# Default mode is READ-ONLY. Nothing is modified unless you pass --fix,
# and even then every action asks first.
#
# Deliberately NOT automated: creating or resizing partitions, formatting,
# and Windows bootloader repair (bcdboot). Those are reported, not performed.
#
# This is phase 2. Phase 1 is boot-prep.sh, which mounts the target install
# and drops you into a chroot. If you are already booted into Ubuntu normally,
# skip phase 1 and run this directly.
#
# Usage:
# sudo ./boot-doctor.sh # check only, read-only
# sudo ./boot-doctor.sh --clean-esp # free space on a full ESP (backs up first)
# sudo ./boot-doctor.sh --fix # check, then offer repairs
# sudo ./boot-doctor.sh --help
#
# Suggested order when the ESP is short on space:
# --clean-esp then --fix then (no flags, to verify)
#
set -uo pipefail
# ---------------------------------------------------------------- constants --
ESP_GUID="c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
GRUB_DEFAULT="/etc/default/grub"
GRUB_CUSTOM="/etc/grub.d/40_custom"
GRUB_DIR="/boot/grub"
FSTAB="/etc/fstab"
INSPECT_MNT="/run/boot-doctor-esp"
FIX_MODE=0
CLEAN_MODE=0
PROBLEMS=0
WARNINGS=0
# Populated during discovery
declare -a ESP_DEVS=()
declare -a ESP_MOUNTS=()
MOUNTED_ESP="" # device backing /boot/efi, if mounted
WINDOWS_ESP="" # device whose ESP holds bootmgfw.efi
WINDOWS_ESP_MNT=""
WINDOWS_ESP_UUID=""
ROOT_DEV=""
IN_CHROOT=0
EFI_RUNTIME=0
# ------------------------------------------------------------------- output --
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
C_RED=$(tput setaf 1); C_GRN=$(tput setaf 2); C_YEL=$(tput setaf 3)
C_BLU=$(tput setaf 4); C_BLD=$(tput bold); C_RST=$(tput sgr0)
else
C_RED=""; C_GRN=""; C_YEL=""; C_BLU=""; C_BLD=""; C_RST=""
fi
section() { printf '\n%s== %s ==%s\n' "$C_BLD$C_BLU" "$1" "$C_RST"; }
ok() { printf ' %s[ OK ]%s %s\n' "$C_GRN" "$C_RST" "$1"; }
warn() { printf ' %s[WARN]%s %s\n' "$C_YEL" "$C_RST" "$1"; WARNINGS=$((WARNINGS+1)); }
bad() { printf ' %s[FAIL]%s %s\n' "$C_RED" "$C_RST" "$1"; PROBLEMS=$((PROBLEMS+1)); }
info() { printf ' %s[info]%s %s\n' "$C_BLU" "$C_RST" "$1"; }
note() { printf ' %s\n' "$1"; }
ask() {
# ask "prompt" -> returns 0 on yes
local reply
printf '\n %s>>%s %s [y/N] ' "$C_BLD$C_YEL" "$C_RST" "$1"
read -r reply </dev/tty || return 1
[[ "$reply" =~ ^[Yy]$ ]]
}
run() {
printf ' %s$ %s%s\n' "$C_BLD" "$*" "$C_RST"
"$@"
local rc=$?
if [[ $rc -ne 0 ]]; then
printf ' %s(exit %d)%s\n' "$C_RED" "$rc" "$C_RST"
fi
return $rc
}
die() { printf '%serror:%s %s\n' "$C_RED" "$C_RST" "$1" >&2; exit 1; }
# --------------------------------------------------------------------- args --
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
exit 0
}
for arg in "$@"; do
case "$arg" in
--fix) FIX_MODE=1 ;;
--clean-esp) CLEAN_MODE=1 ;;
-h|--help) usage ;;
*) die "unknown argument: $arg (try --help)" ;;
esac
done
[[ $EUID -eq 0 ]] || die "must run as root (use sudo)"
cleanup() {
if mountpoint -q "$INSPECT_MNT" 2>/dev/null; then
umount "$INSPECT_MNT" 2>/dev/null
fi
[[ -d "$INSPECT_MNT" ]] && rmdir "$INSPECT_MNT" 2>/dev/null
return 0
}
trap cleanup EXIT
# ==============================================================================
# 1. Environment
# ==============================================================================
check_environment() {
section "Environment"
if [[ -d /sys/firmware/efi ]]; then
EFI_RUNTIME=1
ok "booted in UEFI mode (/sys/firmware/efi present)"
else
EFI_RUNTIME=0
bad "/sys/firmware/efi missing — this system booted in LEGACY/CSM mode"
note "grub-install --target=x86_64-efi cannot write NVRAM entries from here."
note "Reboot and select a boot entry prefixed 'UEFI:' in the F10 menu."
fi
if [[ $EFI_RUNTIME -eq 1 ]]; then
if mountpoint -q /sys/firmware/efi/efivars 2>/dev/null; then
if grep -qE ' /sys/firmware/efi/efivars .* rw[ ,]' /proc/mounts; then
ok "efivars mounted read-write"
else
warn "efivars mounted read-only — efibootmgr writes will fail"
note "fix: mount -o remount,rw /sys/firmware/efi/efivars"
fi
else
warn "efivars not mounted (normal inside a chroot without --bind /sys)"
fi
fi
# crude chroot detection
if [[ "$(stat -c %d:%i / 2>/dev/null)" != "$(stat -c %d:%i /proc/1/root/. 2>/dev/null)" ]]; then
IN_CHROOT=1
info "running inside a chroot"
note "os-prober is unreliable here; re-run update-grub after a real boot."
fi
ROOT_DEV=$(findmnt -no SOURCE / 2>/dev/null)
[[ -n "$ROOT_DEV" ]] && info "root filesystem: $ROOT_DEV"
local sb
if command -v mokutil >/dev/null 2>&1; then
sb=$(mokutil --sb-state 2>/dev/null | head -1)
if [[ "$sb" == *"enabled"* ]]; then
warn "Secure Boot is ENABLED"
note "The --removable fallback path is happier with it off."
elif [[ -n "$sb" ]]; then
ok "Secure Boot: ${sb#*: }"
fi
fi
}
# ==============================================================================
# 1b. Disk inventory — which physical disk holds what
# ==============================================================================
disk_byid() {
# Prefer a stable, human-meaningful identifier over sda/nvme0n1
local dev="$1" link best=""
for link in /dev/disk/by-id/*; do
[[ -e "$link" ]] || continue
[[ "$(readlink -f "$link")" == "$dev" ]] || continue
case "$(basename "$link")" in
wwn-*|*-part*) continue ;;
nvme-eui.*) continue ;;
esac
best=$(basename "$link")
break
done
echo "$best"
}
disk_inventory() {
section "Disk inventory"
local n_disks=0
while IFS= read -r line; do
eval "$line"
[[ "${TYPE:-}" != "disk" ]] && continue
n_disks=$((n_disks+1))
local dev="/dev/${NAME}"
local byid tag ptype
byid=$(disk_byid "$dev")
ptype=$(blkid -p -s PTTYPE -o value "$dev" 2>/dev/null)
tag=""
[[ "${RM:-0}" == "1" ]] && tag=" ${C_YEL}[REMOVABLE — likely the live USB]${C_RST}"
[[ "${TRAN:-}" == "usb" ]] && tag=" ${C_YEL}[USB — likely the live USB]${C_RST}"
printf '\n %s%s%s %s %s (%s)%b\n' \
"$C_BLD" "$dev" "$C_RST" "${SIZE:-?}" "${MODEL:-unknown model}" \
"${TRAN:-?}${ptype:+, $ptype}" "$tag"
[[ -n "$byid" ]] && printf ' stable id: /dev/disk/by-id/%s\n' "$byid"
# partitions on this disk
local p
while IFS= read -r p; do
eval "$p"
[[ "${TYPE:-}" != "part" ]] && continue
local pdev="/dev/${NAME}"
local role=""
if [[ "${PARTTYPE:-}" == "$ESP_GUID" || "${PARTTYPE:-}" == "0xef" ]]; then
role="${C_GRN}ESP${C_RST}"
# what EFI images does it actually hold?
local pm loaders=""
pm=$(mount_for_inspect "$pdev")
if [[ -n "$pm" ]]; then
[[ -f "$pm/EFI/Microsoft/Boot/bootmgfw.efi" ]] && loaders+="Windows "
[[ -f "$pm/EFI/ubuntu/grubx64.efi" ]] && loaders+="GRUB "
[[ -f "$pm/EFI/ubuntu/shimx64.efi" ]] && loaders+="shim "
local fbk
fbk=$(find "$pm/EFI" -maxdepth 2 -ipath '*BOOT/BOOTX64.EFI' 2>/dev/null | head -1)
[[ -n "$fbk" ]] && loaders+="fallback "
[[ "$pm" == "$INSPECT_MNT" ]] && umount "$INSPECT_MNT" 2>/dev/null
fi
role="$role → ${loaders:-<no loaders>}"
elif [[ "${FSTYPE:-}" == "ntfs" ]]; then
role="Windows data"
elif [[ "${FSTYPE:-}" =~ ^(ext4|ext3|btrfs|xfs)$ ]]; then
role="Linux fs"
[[ "$pdev" == "$ROOT_DEV" ]] && role="${C_GRN}Linux ROOT (mounted at /)${C_RST}"
elif [[ "${FSTYPE:-}" == "crypto_LUKS" ]]; then
role="LUKS container"
elif [[ "${FSTYPE:-}" == "LVM2_member" ]]; then
role="LVM physical volume"
elif [[ "${FSTYPE:-}" == "swap" ]]; then
role="swap"
fi
printf ' %-16s %-7s %-6s %s\n' \
"$pdev" "${SIZE:-?}" "${FSTYPE:-—}" "$role"
done < <(lsblk -Pno NAME,TYPE,SIZE,FSTYPE,PARTTYPE "$dev" 2>/dev/null)
done < <(lsblk -Pdno NAME,TYPE,SIZE,MODEL,TRAN,RM 2>/dev/null)
printf '\n'
if [[ $n_disks -gt 1 ]]; then
note "Kernel names (sda, sdb, nvme0n1) are assigned in probe order and are"
note "NOT stable. A live USB usually takes 'sda', shifting your SATA SSD to"
note "'sdb'. Always confirm against the model string or by-id path above"
note "before passing a device to efibootmgr, grub-install, or mkfs."
fi
}
# ==============================================================================
# 2. Discover EFI System Partitions
# ==============================================================================
mount_for_inspect() {
# mount_for_inspect <device> -> echoes a mountpoint, or empty on failure
local dev="$1" existing
existing=$(findmnt -no TARGET --source "$dev" 2>/dev/null | head -1)
if [[ -n "$existing" ]]; then
echo "$existing"
return 0
fi
mkdir -p "$INSPECT_MNT" 2>/dev/null
if mount -o ro "$dev" "$INSPECT_MNT" 2>/dev/null; then
echo "$INSPECT_MNT"
return 0
fi
return 1
}
discover_esps() {
section "EFI System Partitions"
local name fstype parttype size mnt dev
while IFS= read -r line; do
eval "$line"
[[ -z "${FSTYPE:-}" ]] && continue
[[ "$FSTYPE" != "vfat" ]] && continue
local is_esp=0
[[ "${PARTTYPE:-}" == "$ESP_GUID" ]] && is_esp=1
[[ "${PARTTYPE:-}" == "0xef" ]] && is_esp=1
[[ $is_esp -eq 0 ]] && continue
dev="/dev/${NAME}"
ESP_DEVS+=("$dev")
done < <(lsblk -Pno NAME,FSTYPE,PARTTYPE,SIZE,MOUNTPOINT 2>/dev/null)
if [[ ${#ESP_DEVS[@]} -eq 0 ]]; then
bad "no EFI System Partition found on any disk"
note "Nothing can boot in UEFI mode without one. You will need to create a"
note "512MB FAT32 partition flagged 'esp'. This script will not do that for you."
return
fi
for dev in "${ESP_DEVS[@]}"; do
local disk uuid label
disk=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -1)
size=$(lsblk -no SIZE "$dev" 2>/dev/null | head -1 | tr -d ' ')
uuid=$(blkid -s UUID -o value "$dev" 2>/dev/null)
info "$dev on /dev/${disk:-?} size=${size:-?} UUID=${uuid:-?}"
mnt=$(mount_for_inspect "$dev")
if [[ -z "$mnt" ]]; then
warn " could not mount $dev for inspection"
ESP_MOUNTS+=("")
continue
fi
ESP_MOUNTS+=("$mnt")
# free space
local availk usedpct
availk=$(df -k --output=avail "$mnt" 2>/dev/null | tail -1 | tr -d ' ')
usedpct=$(df -k --output=pcent "$mnt" 2>/dev/null | tail -1 | tr -d ' %')
if [[ -n "$availk" ]]; then
local availm=$(( availk / 1024 ))
if [[ $availm -lt 10 ]]; then
bad " only ${availm}MB free (${usedpct}% used) — FAT32 corrupts files when it fills"
note " This is a very common cause of a Windows loader that 'goes nowhere'."
elif [[ $availm -lt 40 ]]; then
warn " only ${availm}MB free (${usedpct}% used) — tight for shim+grub+fallback"
else
ok " ${availm}MB free (${usedpct}% used)"
fi
fi
# vendor directories
if [[ -d "$mnt/EFI" ]]; then
local dirs
dirs=$(find "$mnt/EFI" -maxdepth 1 -mindepth 1 -type d -printf '%f ' 2>/dev/null)
info " /EFI contains: ${dirs:-<empty>}"
else
warn " no /EFI directory on this partition"
fi
# Windows loader
local bmgr
bmgr=$(find "$mnt/EFI" -maxdepth 3 -iname 'bootmgfw.efi' 2>/dev/null | head -1)
if [[ -n "$bmgr" ]]; then
WINDOWS_ESP="$dev"
WINDOWS_ESP_MNT="$mnt"
WINDOWS_ESP_UUID="$uuid"
local bsz
bsz=$(stat -c %s "$bmgr" 2>/dev/null || echo 0)
if [[ $bsz -lt 102400 ]]; then
bad " bootmgfw.efi present but only ${bsz} bytes — truncated/corrupt"
note " Repair from a Windows install USB (Shift+F10 at the installer):"
note " diskpart -> list vol -> select vol N -> assign letter=S -> exit"
note " bcdboot C:\\Windows /s S: /f UEFI"
else
ok " Windows loader present ($(( bsz / 1024 ))KB)"
fi
fi
# Ubuntu loaders
local ub="$mnt/EFI/ubuntu"
if [[ -d "$ub" ]]; then
[[ -f "$ub/grubx64.efi" ]] && ok " EFI/ubuntu/grubx64.efi present" \
|| warn " EFI/ubuntu/grubx64.efi MISSING"
[[ -f "$ub/shimx64.efi" ]] && ok " EFI/ubuntu/shimx64.efi present" \
|| info " EFI/ubuntu/shimx64.efi absent (fine if Secure Boot is off)"
if [[ -f "$ub/grub.cfg" ]]; then
local pfx
pfx=$(grep -o 'search[^\n]*' "$ub/grub.cfg" 2>/dev/null | head -1)
[[ -n "$pfx" ]] && note " stub cfg: $pfx"
fi
fi
# Removable fallback — the important one for a dead CMOS battery
local fb
fb=$(find "$mnt/EFI" -maxdepth 2 -ipath '*BOOT/BOOTX64.EFI' 2>/dev/null | head -1)
if [[ -n "$fb" ]]; then
local fsz
fsz=$(stat -c %s "$fb" 2>/dev/null || echo 0)
ok " removable fallback BOOTX64.EFI present ($(( fsz / 1024 ))KB)"
else
warn " no EFI/BOOT/BOOTX64.EFI fallback on this ESP"
note " Without it, a CMOS reset leaves this machine unbootable."
fi
if [[ "$mnt" == "$INSPECT_MNT" ]]; then
umount "$INSPECT_MNT" 2>/dev/null
ESP_MOUNTS[-1]=""
fi
done
}
# ==============================================================================
# 3. /boot/efi mount + fstab
# ==============================================================================
check_boot_efi() {
section "/boot/efi mount"
if mountpoint -q /boot/efi; then
MOUNTED_ESP=$(findmnt -no SOURCE /boot/efi)
ok "/boot/efi mounted from $MOUNTED_ESP"
local esp_disk root_disk
esp_disk=$(lsblk -no PKNAME "$MOUNTED_ESP" 2>/dev/null | head -1)
root_disk=$(lsblk -no PKNAME "$ROOT_DEV" 2>/dev/null | head -1)
if [[ -n "$esp_disk" && -n "$root_disk" && "$esp_disk" != "$root_disk" ]]; then
info "ESP is on /dev/$esp_disk but root is on /dev/$root_disk"
note "This is a valid shared-ESP dual-boot layout. It also means the"
note "root disk will never appear as its own UEFI boot device, and"
note "removing /dev/$esp_disk would make this install unbootable."
fi
else
bad "/boot/efi is not mounted"
note "grub-install --efi-directory=/boot/efi will fail or write to the wrong place."
fi
if grep -qE '^[^#].*[[:space:]]/boot/efi[[:space:]]' "$FSTAB" 2>/dev/null; then
ok "/boot/efi has an $FSTAB entry"
local fst_uuid
fst_uuid=$(grep -E '^[^#].*[[:space:]]/boot/efi[[:space:]]' "$FSTAB" | awk '{print $1}' | head -1)
note " $fst_uuid"
if [[ -n "$MOUNTED_ESP" && "$fst_uuid" == UUID=* ]]; then
local want have
want="${fst_uuid#UUID=}"
have=$(blkid -s UUID -o value "$MOUNTED_ESP" 2>/dev/null)
if [[ -n "$have" && "$want" != "$have" ]]; then
bad "fstab UUID ($want) does not match mounted ESP ($have)"
fi
fi
else
bad "no /boot/efi entry in $FSTAB — it will not remount after reboot"
fi
}
# ==============================================================================
# 4. GRUB installation state
# ==============================================================================
check_grub_files() {
section "GRUB installation"
if [[ -d "$GRUB_DIR/x86_64-efi" ]]; then
local n
n=$(find "$GRUB_DIR/x86_64-efi" -name '*.mod' 2>/dev/null | wc -l)
ok "$GRUB_DIR/x86_64-efi present ($n modules)"
else
bad "$GRUB_DIR/x86_64-efi MISSING — GRUB cannot read filesystems in EFI mode"
note "This is what produces 'error: unknown filesystem' at the grub> prompt."
note "Fix: grub-install --target=x86_64-efi --efi-directory=/boot/efi"
fi
if [[ -d "$GRUB_DIR/i386-pc" ]]; then
warn "$GRUB_DIR/i386-pc present — leftover BIOS-mode install"
note "Harmless, just dead weight. Safe to delete once EFI boot works."
fi
if [[ -f "$GRUB_DIR/grub.cfg" ]]; then
ok "grub.cfg present (modified $(date -r "$GRUB_DIR/grub.cfg" '+%Y-%m-%d %H:%M'))"
local nkern
nkern=$(grep -c "^menuentry\|^\s*menuentry" "$GRUB_DIR/grub.cfg" 2>/dev/null)
info " $nkern menu entries"
if grep -qi 'chainloader.*bootmgfw\|Windows Boot Manager' "$GRUB_DIR/grub.cfg" 2>/dev/null; then
ok " a Windows entry exists in the menu"
else
warn " no Windows entry in the GRUB menu"
fi
else
bad "$GRUB_DIR/grub.cfg MISSING — you will land at a grub> prompt with no menu"
note "Fix: update-grub"
fi
# packages
if command -v dpkg-query >/dev/null 2>&1; then
local st
st=$(dpkg-query -W -f='${Status}' grub-efi-amd64 2>/dev/null)
[[ "$st" == *"ok installed"* ]] && ok "grub-efi-amd64 installed" \
|| bad "grub-efi-amd64 NOT installed"
st=$(dpkg-query -W -f='${Status}' grub-pc 2>/dev/null)
[[ "$st" == *"ok installed"* ]] && warn "grub-pc still installed (BIOS variant) — conflicts on upgrade"
fi
}
# ==============================================================================
# 5. NVRAM boot entries
# ==============================================================================
check_nvram() {
section "Firmware boot entries (NVRAM)"
if ! command -v efibootmgr >/dev/null 2>&1; then
warn "efibootmgr not installed — skipping (apt install efibootmgr)"
return
fi
if [[ $EFI_RUNTIME -eq 0 ]]; then
warn "not in UEFI mode; NVRAM cannot be read"
return
fi
local out
out=$(efibootmgr -v 2>/dev/null)
if [[ -z "$out" ]]; then
warn "efibootmgr returned nothing (efivars not available?)"
return
fi
printf '%s\n' "$out" | sed 's/^/ /'
if grep -qi 'ubuntu' <<<"$out"; then
ok "an 'ubuntu' NVRAM entry exists"
else
warn "no 'ubuntu' NVRAM entry"
note "Expected with a failing CMOS battery. The removable fallback covers this."
fi
if grep -qi 'Windows Boot Manager' <<<"$out"; then
ok "a 'Windows Boot Manager' NVRAM entry exists"
if ! grep -i 'Windows Boot Manager' <<<"$out" | grep -qi 'bootmgfw.efi'; then
bad " ...but its path does not reference bootmgfw.efi"
note " The firmware will show the entry and then do nothing. Classic symptom."
fi
else
warn "no 'Windows Boot Manager' NVRAM entry"
fi
}
# ==============================================================================
# 6. /etc/default/grub settings
# ==============================================================================
check_grub_defaults() {
section "GRUB configuration"
[[ -f "$GRUB_DEFAULT" ]] || { bad "$GRUB_DEFAULT missing"; return; }
local v
v=$(grep -E '^GRUB_DISABLE_OS_PROBER=' "$GRUB_DEFAULT" | tail -1 | cut -d= -f2 | tr -d '"')
if [[ "$v" == "false" ]]; then
ok "GRUB_DISABLE_OS_PROBER=false (Windows will be detected)"
else
warn "os-prober disabled — Windows will not appear in the GRUB menu"
note "Since GRUB 2.06 this defaults to true."
fi
v=$(grep -E '^GRUB_GFXMODE=' "$GRUB_DEFAULT" | tail -1 | cut -d= -f2 | tr -d '"')
[[ -n "$v" ]] && info "GRUB_GFXMODE=$v" || info "GRUB_GFXMODE unset (firmware default, 4K on this NUC)"
command -v os-prober >/dev/null 2>&1 && ok "os-prober installed" \
|| warn "os-prober not installed"
}
# ==============================================================================
# 6c. Boot chain summary
# ==============================================================================
boot_chain() {
section "Boot chain"
local esp_disk="" esp_model="" root_disk="" root_model=""
if [[ -n "$MOUNTED_ESP" ]]; then
esp_disk=$(lsblk -no PKNAME "$MOUNTED_ESP" 2>/dev/null | head -1)
esp_model=$(lsblk -dno MODEL "/dev/$esp_disk" 2>/dev/null | sed 's/ *$//')
fi
if [[ -n "$ROOT_DEV" ]]; then
root_disk=$(lsblk -no PKNAME "$ROOT_DEV" 2>/dev/null | head -1)
root_model=$(lsblk -dno MODEL "/dev/$root_disk" 2>/dev/null | sed 's/ *$//')
fi
printf ' firmware (NUC boot menu)\n'
printf ' │\n'
printf ' ▼ picks a device that has an ESP with a bootable image\n'
printf ' %sESP%s %s on /dev/%s (%s)\n' \
"$C_BLD" "$C_RST" "${MOUNTED_ESP:-<not mounted>}" "${esp_disk:-?}" "${esp_model:-?}"
printf ' │ EFI/ubuntu/shimx64.efi or EFI/BOOT/BOOTX64.EFI\n'
printf ' ▼\n'
printf ' %sGRUB%s reads /boot/grub/grub.cfg and the kernel from...\n' "$C_BLD" "$C_RST"
printf ' │\n'
printf ' ▼\n'
printf ' %sroot%s %s on /dev/%s (%s)\n' \
"$C_BLD" "$C_RST" "${ROOT_DEV:-?}" "${root_disk:-?}" "${root_model:-?}"
printf '\n'
if [[ -n "$esp_disk" && -n "$root_disk" && "$esp_disk" != "$root_disk" ]]; then
warn "the ESP and the root filesystem are on DIFFERENT disks"
note "Consequences of this layout:"
note " • /dev/$root_disk will never appear in the F10 boot menu — correct, not a fault."
note " • You must boot the entry for /dev/$esp_disk to reach Ubuntu."
note " • Removing or wiping /dev/$esp_disk makes this Ubuntu install unbootable."
note " • A dedicated ESP on /dev/$root_disk would make it self-sufficient."
elif [[ -n "$esp_disk" ]]; then
ok "ESP and root are on the same disk (/dev/$esp_disk) — self-contained"
fi
}
# ==============================================================================
# 6b. ESP cleanup
# ==============================================================================
esp_backup() {
# Tar the whole ESP somewhere on the root filesystem before deleting anything.
local src="$1" dest="/root/esp-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
printf ' %s$ tar czf %s -C %s .%s\n' "$C_BLD" "$dest" "$src" "$C_RST"
if tar czf "$dest" -C "$src" . 2>/dev/null; then
ok "ESP backed up to $dest ($(du -h "$dest" | cut -f1))"
note "Restore with: tar xzf $dest -C /boot/efi"
return 0
fi
bad "backup FAILED — refusing to delete anything"
return 1
}
esp_usage_report() {
local mnt="$1"
info "largest consumers on the ESP:"
du -sk "$mnt"/EFI/* 2>/dev/null | sort -rn | head -12 | while read -r kb path; do
printf ' %6s MB %s\n' "$(( kb / 1024 ))" "${path#"$mnt"/}"
done
du -sk "$mnt"/EFI/Microsoft/Boot/* 2>/dev/null | sort -rn | head -8 | while read -r kb path; do
printf ' %6s MB %s\n' "$(( kb / 1024 ))" "${path#"$mnt"/}"
done
}
esp_free_mb() {
echo $(( $(df -k --output=avail "$1" 2>/dev/null | tail -1 | tr -d ' ') / 1024 ))
}
clean_esp() {
section "ESP cleanup"
if ! mountpoint -q /boot/efi; then
bad "/boot/efi is not mounted — nothing to clean"
return
fi
local mnt="/boot/efi"
local before after
before=$(esp_free_mb "$mnt")
info "starting free space: ${before}MB"
esp_usage_report "$mnt"
note ""
note "Everything below is opt-in. A full tarball of the ESP is taken first."
esp_backup "$mnt" || return
# -- 1. GRUB and installer backup files -----------------------------------
local junk
junk=$(find "$mnt" -maxdepth 4 \( -iname '*.grb' -o -iname '*.bak' -o -iname '*.old' \
-o -iname '*.orig' -o -iname '*~' \) 2>/dev/null)
if [[ -n "$junk" ]]; then
printf '\n'
printf '%s\n' "$junk" | sed 's/^/ /'
if ask "Delete these backup/leftover files?"; then
printf '%s\n' "$junk" | while read -r f; do rm -f "$f"; done
ok "removed"
fi
else
info "no stray backup files"
fi
# -- 2. Orphaned distro vendor directories --------------------------------
local keep_re='^(BOOT|Boot|boot|Microsoft|ubuntu)$'
local d base
for d in "$mnt"/EFI/*/; do
[[ -d "$d" ]] || continue
base=$(basename "$d")
[[ "$base" =~ $keep_re ]] && continue
local sz
sz=$(du -sk "$d" 2>/dev/null | cut -f1)
printf '\n'
warn "vendor directory /EFI/$base — $(( sz / 1024 ))MB"
note " contents: $(find "$d" -maxdepth 1 -mindepth 1 -printf '%f ' 2>/dev/null)"
note " This is from another distro or a previous install. Only delete it"
note " if you are sure nothing boots from it."
if ask "Delete /EFI/$base?"; then
rm -rf "$d" && ok "removed /EFI/$base"
fi
done
# -- 3. Windows boot locale directories -----------------------------------
local msboot="$mnt/EFI/Microsoft/Boot"
if [[ -d "$msboot" ]]; then
local locales lsz
locales=$(find "$msboot" -maxdepth 1 -mindepth 1 -type d \
\( -regextype posix-extended -regex '.*/[a-z]{2}-[A-Z]{2}$' \
-o -name 'qps-*' \) ! -name 'en-US' 2>/dev/null)
if [[ -n "$locales" ]]; then
lsz=$(printf '%s\n' "$locales" | xargs -r du -sk 2>/dev/null | awk '{s+=$1} END {print int(s/1024)}')
printf '\n'
info "non-English Windows boot locale directories: ~${lsz}MB total"
printf '%s\n' "$locales" | sed "s|$msboot/| |"
note ""
note "These hold translated strings for the Windows boot menu and"
note "recovery screens. Removing them leaves those screens in English."
note "Windows itself is unaffected, and a Windows feature update will"
note "restore them. Skip this if you rely on a non-English boot UI."
if ask "Delete non-en-US locale directories (~${lsz}MB)?"; then
printf '%s\n' "$locales" | while read -r l; do rm -rf "$l"; done
ok "removed"
fi
fi
fi
# -- 4. Report-only items --------------------------------------------------
printf '\n'
local fonts="$msboot/Fonts"
if [[ -d "$fonts" ]]; then
info "$(du -sh "$fonts" 2>/dev/null | cut -f1) in /EFI/Microsoft/Boot/Fonts — NOT offered for deletion"
note "Used by the BitLocker unlock and recovery screens. Deleting it can"
note "leave you staring at an unrenderable recovery prompt. Not worth it."
fi
if [[ -d "$mnt/EFI/Microsoft/Recovery" ]]; then
info "/EFI/Microsoft/Recovery present — leave it alone"
fi
# -- result ----------------------------------------------------------------
after=$(esp_free_mb "$mnt")
section "Cleanup result"
printf ' free before: %sMB\n free after: %sMB (%s+%sMB%s)\n' \
"$before" "$after" "$C_GRN" "$(( after - before ))" "$C_RST"
if [[ $after -lt 25 ]]; then
warn "still under 25MB — GRUB install may still truncate files"
note "The durable fix is a dedicated ESP on the NVMe. See the notes in"
note "boot-prep.sh; partitioning is not automated here."
else
ok "enough headroom for shim + grub + the removable fallback"
fi
}
# ==============================================================================
# 7. Repairs
# ==============================================================================
do_fixes() {
section "Repairs"
if [[ $EFI_RUNTIME -eq 0 ]]; then
bad "refusing to run repairs: not booted in UEFI mode"
note "Reboot via a 'UEFI:' entry in the F10 menu first."
return
fi
if ! mountpoint -q /boot/efi; then
bad "refusing to run repairs: /boot/efi is not mounted"
return
fi
# -- enable os-prober ------------------------------------------------------
local v
v=$(grep -E '^GRUB_DISABLE_OS_PROBER=' "$GRUB_DEFAULT" | tail -1 | cut -d= -f2 | tr -d '"')
if [[ "$v" != "false" ]]; then
if ask "Enable os-prober so Windows appears in the GRUB menu?"; then
cp -a "$GRUB_DEFAULT" "$GRUB_DEFAULT.bak.$(date +%s)"
sed -i '/^GRUB_DISABLE_OS_PROBER=/d' "$GRUB_DEFAULT"
echo 'GRUB_DISABLE_OS_PROBER=false' >> "$GRUB_DEFAULT"
ok "set GRUB_DISABLE_OS_PROBER=false (backup saved)"
fi
fi
# -- set 1080p -------------------------------------------------------------
if ! grep -qE '^GRUB_GFXMODE=' "$GRUB_DEFAULT"; then
if ask "Set GRUB menu resolution to 1920x1080?"; then
cp -a "$GRUB_DEFAULT" "$GRUB_DEFAULT.bak.$(date +%s)"
printf 'GRUB_GFXMODE=1920x1080,1280x1024,auto\nGRUB_GFXPAYLOAD_LINUX=keep\n' >> "$GRUB_DEFAULT"
ok "GRUB_GFXMODE set (falls back automatically if unsupported)"
fi
fi
# -- reinstall GRUB to the ESP --------------------------------------------
if ask "Reinstall GRUB to /boot/efi (both the 'ubuntu' entry and the removable fallback)?"; then
run grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu
run grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable
ok "grub-install done"
note "The --removable copy overwrites EFI/BOOT/BOOTX64.EFI. That is intended:"
note "it is what keeps this machine bootable when NVRAM is wiped."
fi
# -- regenerate menu -------------------------------------------------------
if ask "Regenerate the GRUB menu (update-grub)?"; then
run update-grub
if [[ $IN_CHROOT -eq 1 ]]; then
note "You are in a chroot — os-prober often finds nothing here."
note "Run 'sudo update-grub' again after booting normally."
fi
fi
# -- static Windows chainload entry ---------------------------------------
if [[ -n "$WINDOWS_ESP_UUID" ]]; then
if grep -q 'bootmgfw.efi' "$GRUB_CUSTOM" 2>/dev/null; then
info "40_custom already has a Windows chainload entry"
elif ask "Add a permanent Windows chainload entry to 40_custom (survives os-prober failures)?"; then
cp -a "$GRUB_CUSTOM" "$GRUB_CUSTOM.bak.$(date +%s)" 2>/dev/null
cat >> "$GRUB_CUSTOM" <<EOF
menuentry "Windows Boot Manager (manual)" --class windows {
insmod part_gpt
insmod fat
insmod chain
search --no-floppy --fs-uuid --set=root $WINDOWS_ESP_UUID
chainloader /EFI/Microsoft/Boot/bootmgfw.efi
}
EOF
chmod +x "$GRUB_CUSTOM"
ok "entry added for ESP UUID $WINDOWS_ESP_UUID"
run update-grub
fi
fi
# -- recreate the Windows NVRAM entry -------------------------------------
if [[ -n "$WINDOWS_ESP" ]] && command -v efibootmgr >/dev/null 2>&1; then
local wdisk wpart wmodel wbyid
wdisk="/dev/$(lsblk -no PKNAME "$WINDOWS_ESP" | head -1)"
wpart=$(cat "/sys/class/block/$(basename "$WINDOWS_ESP")/partition" 2>/dev/null)
wmodel=$(lsblk -dno MODEL "$wdisk" 2>/dev/null | sed 's/ *$//')
wbyid=$(disk_byid "$wdisk")
if [[ -n "$wpart" ]] && ! efibootmgr 2>/dev/null | grep -qi 'Windows Boot Manager'; then
printf '\n'
info "target disk for the NVRAM entry:"
note " device: $wdisk partition $wpart"
note " model: ${wmodel:-unknown}"
note " by-id: ${wbyid:-unavailable}"
note " Verify this is your Windows SSD, not the live USB."
if ask "Recreate the 'Windows Boot Manager' NVRAM entry on $wdisk (${wmodel:-?})?"; then
run efibootmgr -c -d "$wdisk" -p "$wpart" -L "Windows Boot Manager" \
-l '\EFI\Microsoft\Boot\bootmgfw.efi'
fi
fi
fi
# -- clean up BIOS leftovers ----------------------------------------------
if [[ -d "$GRUB_DIR/i386-pc" ]]; then
if ask "Delete leftover BIOS modules at $GRUB_DIR/i386-pc?"; then
run rm -rf "$GRUB_DIR/i386-pc"
fi
fi
}
# ==============================================================================
# Main
# ==============================================================================
printf '%sboot-doctor%s — UEFI/GRUB dual-boot check%s\n' \
"$C_BLD" "$C_RST$C_BLD" "$C_RST"
printf 'mode: %s\n' "$([[ $FIX_MODE -eq 1 ]] && echo 'CHECK + FIX (each step confirmed)' || echo 'CHECK ONLY (read-only)')"
check_environment
disk_inventory
discover_esps
check_boot_efi
check_grub_files
check_nvram
check_grub_defaults
boot_chain
section "Summary"
printf ' %d failure(s), %d warning(s)\n' "$PROBLEMS" "$WARNINGS"
[[ $CLEAN_MODE -eq 1 ]] && clean_esp
if [[ $FIX_MODE -eq 1 ]]; then
do_fixes
section "Done"
note "Re-run without --fix to verify, then reboot."
note "BIOS (F2): UEFI on, CSM off, Secure Boot off."
elif [[ $CLEAN_MODE -eq 0 ]] && [[ $PROBLEMS -gt 0 || $WARNINGS -gt 0 ]]; then
printf '\n Re-run with %s--fix%s to be offered repairs for the above.\n' "$C_BLD" "$C_RST"
printf ' If ESP free space is tight, run %s--clean-esp%s first.\n' "$C_BLD" "$C_RST"
fi
printf '\n'
exit 0
#!/usr/bin/env bash
#
# boot-prep.sh — phase 1: mount the target install and enter a chroot
#
# Run this from a live USB booted in UEFI mode. It discovers the Ubuntu root
# filesystem and the EFI System Partition, unlocks LUKS / activates LVM if
# needed, mounts everything in the right order, bind-mounts the kernel
# filesystems (including efivars, so efibootmgr works), and drops you into a
# chroot with boot-doctor.sh ready to run.
#
# Phase 2 is boot-doctor.sh, run from inside that chroot.
#
# Usage:
# sudo ./boot-prep.sh # interactive discovery
# sudo ./boot-prep.sh --root /dev/nvme0n1p2 --esp /dev/sda1
# sudo ./boot-prep.sh --umount # tear everything down
# sudo ./boot-prep.sh --dry-run # show the plan, mount nothing
#
set -uo pipefail
TARGET="/mnt/target"
ESP_GUID="c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
PROBE_MNT="/run/boot-prep-probe"
OPT_ROOT=""
OPT_ESP=""
DO_UMOUNT=0
DRY_RUN=0
MOUNT_NOTE=""
MOUNT_ERR=""
PROBE_PATH=""
PROBE_OWNED=0
# ------------------------------------------------------------------- output --
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
C_RED=$(tput setaf 1); C_GRN=$(tput setaf 2); C_YEL=$(tput setaf 3)
C_BLU=$(tput setaf 4); C_BLD=$(tput bold); C_RST=$(tput sgr0)
else
C_RED=""; C_GRN=""; C_YEL=""; C_BLU=""; C_BLD=""; C_RST=""
fi
section() { printf '\n%s== %s ==%s\n' "$C_BLD$C_BLU" "$1" "$C_RST"; }
ok() { printf ' %s[ OK ]%s %s\n' "$C_GRN" "$C_RST" "$1"; }
warn() { printf ' %s[WARN]%s %s\n' "$C_YEL" "$C_RST" "$1"; }
bad() { printf ' %s[FAIL]%s %s\n' "$C_RED" "$C_RST" "$1"; }
info() { printf ' %s[info]%s %s\n' "$C_BLU" "$C_RST" "$1"; }
note() { printf ' %s\n' "$1"; }
die() { printf '%serror:%s %s\n' "$C_RED" "$C_RST" "$1" >&2; exit 1; }
run() {
printf ' %s$ %s%s\n' "$C_BLD" "$*" "$C_RST"
[[ $DRY_RUN -eq 1 ]] && return 0
"$@"
}
ask() {
local reply
printf '\n %s>>%s %s [y/N] ' "$C_BLD$C_YEL" "$C_RST" "$1"
read -r reply </dev/tty || return 1
[[ "$reply" =~ ^[Yy]$ ]]
}
usage() { sed -n '2,/^$/p' "$0" | sed 's/^# \?//'; exit 0; }
# --------------------------------------------------------------------- args --
while [[ $# -gt 0 ]]; do
case "$1" in
--root) OPT_ROOT="${2:-}"; shift 2 ;;
--esp) OPT_ESP="${2:-}"; shift 2 ;;
--umount) DO_UMOUNT=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
-h|--help) usage ;;
*) die "unknown argument: $1 (try --help)" ;;
esac
done
[[ $EUID -eq 0 ]] || die "must run as root (use sudo)"
# ==============================================================================
# Teardown
# ==============================================================================
teardown() {
section "Unmounting $TARGET"
local m
# deepest first
while IFS= read -r m; do
run umount -l "$m" 2>/dev/null && ok "unmounted $m"
done < <(findmnt -rno TARGET | grep "^$TARGET" | sort -r)
if mountpoint -q "$TARGET" 2>/dev/null; then
warn "$TARGET still mounted — something is holding it"
note "check with: lsof +D $TARGET"
else
ok "all target mounts released"
rmdir "$TARGET" 2>/dev/null
fi
exit 0
}
[[ $DO_UMOUNT -eq 1 ]] && teardown
# ==============================================================================
# 1. Verify we are in UEFI mode
# ==============================================================================
section "Live environment"
if [[ -d /sys/firmware/efi ]]; then
ok "live USB booted in UEFI mode"
else
bad "live USB booted in LEGACY/CSM mode"
note "grub-install --target=x86_64-efi will not be able to write NVRAM entries."
note "Reboot, press F10, and pick the entry prefixed 'UEFI:' instead."
ask "Continue anyway (repairs will be limited)?" || exit 1
fi
for t in cryptsetup lvm blkid findmnt; do
command -v "$t" >/dev/null 2>&1 || warn "$t not found — some discovery may fail"
done
# ==============================================================================
# 2. Unlock encrypted / logical volumes
# ==============================================================================
section "Storage activation"
luks_found=0
while IFS= read -r dev; do
luks_found=1
if cryptsetup status "$(basename "$dev")_crypt" >/dev/null 2>&1; then
ok "$dev already unlocked"
continue
fi
warn "$dev is LUKS-encrypted"
if ask "Unlock $dev now?"; then
run cryptsetup luksOpen "$dev" "$(basename "$dev")_crypt"
fi
done < <(blkid -t TYPE=crypto_LUKS -o device 2>/dev/null)
[[ $luks_found -eq 0 ]] && info "no LUKS volumes detected"
if command -v vgchange >/dev/null 2>&1; then
if vgs --noheadings 2>/dev/null | grep -q .; then
run vgchange -ay >/dev/null 2>&1
ok "LVM volume groups activated"
vgs --noheadings -o vg_name 2>/dev/null | sed 's/^/ vg: /'
else
info "no LVM volume groups detected"
fi
fi
# ==============================================================================
# 3. Find candidate root filesystems
# ==============================================================================
existing_mount() {
# echo where <dev> is currently mounted, if anywhere
findmnt -nro TARGET --source "$1" 2>/dev/null | head -1
}
probe_umount() {
mountpoint -q "$PROBE_MNT" 2>/dev/null && umount "$PROBE_MNT" 2>/dev/null
return 0
}
probe_get() {
# probe_get <dev>
# Sets PROBE_PATH to a readable mountpoint. Reuses an existing mount
# (e.g. a udisks auto-mount from the live desktop) rather than failing.
# Sets PROBE_OWNED=1 if we created the mount ourselves.
local dev="$1" ex
PROBE_PATH=""; PROBE_OWNED=0; MOUNT_NOTE=""; MOUNT_ERR=""
ex=$(existing_mount "$dev")
if [[ -n "$ex" ]]; then
PROBE_PATH="$ex"
MOUNT_NOTE="already mounted at $ex"
return 0
fi
probe_umount
if mount -o ro "$dev" "$PROBE_MNT" 2>/dev/null; then
PROBE_PATH="$PROBE_MNT"; PROBE_OWNED=1; return 0
fi
# ext3/4 with a dirty journal refuses a plain read-only mount
if mount -o ro,noload "$dev" "$PROBE_MNT" 2>/dev/null; then
PROBE_PATH="$PROBE_MNT"; PROBE_OWNED=1
MOUNT_NOTE="dirty journal — mounted with noload"; return 0
fi
if mount -o ro,rescue=all "$dev" "$PROBE_MNT" 2>/dev/null; then
PROBE_PATH="$PROBE_MNT"; PROBE_OWNED=1
MOUNT_NOTE="btrfs rescue mount"; return 0
fi
MOUNT_ERR=$(mount -o ro "$dev" "$PROBE_MNT" 2>&1 | tail -1)
return 1
}
probe_put() {
[[ "${PROBE_OWNED:-0}" == "1" ]] && probe_umount
PROBE_PATH=""; PROBE_OWNED=0
return 0
}
# Undo live-desktop auto-mounts so the real mounts can proceed
release_automounts() {
local dev="$1" ex
ex=$(existing_mount "$dev")
[[ -z "$ex" ]] && return 0
[[ "$ex" == "$TARGET"* ]] && return 0
warn "$dev is already mounted at $ex"
if [[ "$ex" == /run/media/* || "$ex" == /media/* ]]; then
note " This is the live desktop's automatic mount (udisks)."
fi
# 1. the correct way to undo a udisks automount
if command -v udisksctl >/dev/null 2>&1; then
if udisksctl unmount -b "$dev" >/dev/null 2>&1; then
ok " released via udisksctl"
return 0
fi
fi
# 2. plain unmount
if umount "$dev" 2>/dev/null; then
ok " unmounted"
return 0
fi
# 3. relocate the existing mount instead of fighting it
mount --make-private "$ex" 2>/dev/null
if mount --move "$ex" "$TARGET" 2>/dev/null; then
ok " moved $ex -> $TARGET"
return 2 # signals: already at TARGET, do not mount again
fi
# 4. last resort
warn " still busy — showing what is holding it:"
if command -v fuser >/dev/null 2>&1; then
fuser -vm "$ex" 2>&1 | sed 's/^/ /'
elif command -v lsof >/dev/null 2>&1; then
lsof +D "$ex" 2>/dev/null | head -10 | sed 's/^/ /'
fi
if ask " Force a lazy unmount of $ex?"; then
umount -l "$ex" 2>/dev/null && { ok " lazy-unmounted"; return 0; }
fi
return 1
}
trap 'probe_umount; rmdir "$PROBE_MNT" 2>/dev/null' EXIT
section "Candidate root filesystems"
declare -a ROOTS=()
declare -a ROOT_DESC=()
SCANNED=0
mkdir -p "$PROBE_MNT"
while IFS= read -r dev; do
[[ -b "$dev" ]] || continue
SCANNED=$((SCANNED+1))
# blkid reads a cache that can be stale on a live session; fall back to lsblk
fs=$(blkid -s TYPE -o value "$dev" 2>/dev/null)
[[ -z "$fs" ]] && fs=$(lsblk -no FSTYPE "$dev" 2>/dev/null | head -1 | tr -d ' ')
case "$fs" in
ext2|ext3|ext4|btrfs|xfs|f2fs)
;;
crypto_LUKS)
printf ' %-22s %-12s skipped — locked, unlock it above first\n' "$dev" "$fs"
continue ;;
LVM2_member)
printf ' %-22s %-12s skipped — LVM PV, the volumes inside are scanned separately\n' "$dev" "$fs"
continue ;;
vfat|ntfs|swap|"")
printf ' %-22s %-12s skipped — not a Linux root candidate\n' "$dev" "${fs:-no filesystem}"
continue ;;
*)
printf ' %-22s %-12s skipped — unsupported filesystem type\n' "$dev" "$fs"
continue ;;
esac
probe_put
if ! probe_get "$dev"; then
printf ' %-22s %-12s %sMOUNT FAILED%s — %s\n' \
"$dev" "$fs" "$C_RED" "$C_RST" "${MOUNT_ERR:-unknown error}"
note " try manually: mount -o ro,noload $dev /mnt && ls /mnt"
note " if the journal is damaged: fsck -f $dev (unmounted only)"
continue
fi
if [[ -f "$PROBE_PATH/etc/os-release" && -d "$PROBE_PATH/boot" ]]; then
pretty=$(grep -m1 '^PRETTY_NAME=' "$PROBE_PATH/etc/os-release" 2>/dev/null | cut -d= -f2- | tr -d '"')
kern=$(find "$PROBE_PATH/boot" -maxdepth 1 -name 'vmlinuz*' 2>/dev/null | wc -l)
rdisk=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -1)
rmodel=$(lsblk -dno MODEL "/dev/$rdisk" 2>/dev/null | sed 's/ *$//')
rtran=$(lsblk -dno TRAN "/dev/$rdisk" 2>/dev/null | tr -d ' ')
ROOTS+=("$dev")
ROOT_DESC+=("${pretty:-unknown} on /dev/${rdisk:-?} ${rmodel:-?} [${rtran:-?}] — $fs, $kern kernel(s)")
ok "$dev on /dev/${rdisk:-?} — ${pretty:-unknown}"
note " ${rmodel:-unknown model} [${rtran:-?}], $fs, $kern kernel(s)${MOUNT_NOTE:+ ($MOUNT_NOTE)}"
if [[ $kern -eq 0 ]]; then
warn " no kernel in /boot — separate /boot partition? GRUB will have nothing to load."
fi
elif [[ -d "$PROBE_PATH/etc" || -d "$PROBE_PATH/usr" ]]; then
printf ' %-22s %-12s looks Linux-ish but has no /etc/os-release\n' "$dev" "$fs"
else
printf ' %-22s %-12s mounted, but not a root filesystem\n' "$dev" "$fs"
fi
probe_put
done < <(lsblk -pnro NAME,TYPE 2>/dev/null | awk '$2=="part"||$2=="lvm"||$2=="crypt"||$2=="dm"||$2~/^raid/{print $1}')
printf '\n'
info "$SCANNED block device(s) examined, ${#ROOTS[@]} root filesystem(s) found"
if [[ ${#ROOTS[@]} -eq 0 ]]; then
bad "no Linux root filesystem found"
printf '\n'
note "Full block device layout for reference:"
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT 2>/dev/null | sed 's/^/ /'
printf '\n'
note "Common causes, in rough order of likelihood:"
note " 1. Dirty ext4 journal from hard resets. The script now retries with"
note " 'noload', but a badly damaged journal needs: fsck -f <device>"
note " 2. LUKS still locked — answer yes to the unlock prompt above."
note " 3. LVM not activated — run 'vgchange -ay' and re-run this script."
note " 4. The install genuinely failed and there is no root filesystem."
exit 1
fi
ROOT_DEV="$OPT_ROOT"
if [[ -z "$ROOT_DEV" ]]; then
printf '\n'
for i in "${!ROOTS[@]}"; do
printf ' %d) %-18s %s\n' "$((i+1))" "${ROOTS[$i]}" "${ROOT_DESC[$i]}"
done
printf '\n %s>>%s which root filesystem? [1-%d] ' "$C_BLD$C_YEL" "$C_RST" "${#ROOTS[@]}"
read -r sel </dev/tty
[[ "$sel" =~ ^[0-9]+$ ]] && [[ $sel -ge 1 && $sel -le ${#ROOTS[@]} ]] || die "invalid selection"
ROOT_DEV="${ROOTS[$((sel-1))]}"
fi
[[ -b "$ROOT_DEV" ]] || die "$ROOT_DEV is not a block device"
# ==============================================================================
# 4. Find candidate ESPs
# ==============================================================================
section "Candidate EFI System Partitions"
declare -a ESPS=()
declare -a ESP_DESC=()
while IFS= read -r line; do
eval "$line"
[[ "${FSTYPE:-}" == "vfat" ]] || continue
[[ "${PARTTYPE:-}" == "$ESP_GUID" || "${PARTTYPE:-}" == "0xef" ]] || continue
dev="/dev/${NAME}"
pdisk=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -1)
pmodel=$(lsblk -dno MODEL "/dev/$pdisk" 2>/dev/null | sed 's/ *$//')
ptran=$(lsblk -dno TRAN "/dev/$pdisk" 2>/dev/null | tr -d ' ')
prm=$(lsblk -dno RM "/dev/$pdisk" 2>/dev/null | tr -d ' ')
if [[ "$ptran" == "usb" || "$prm" == "1" ]]; then
warn "$dev on /dev/$pdisk (${pmodel:-?}) — USB/REMOVABLE, this is your live installer"
note " Not offered as a choice."
continue
fi
probe_put
if ! probe_get "$dev"; then
printf ' %-22s %-12s %sPROBE FAILED%s — %s\n' \
"$dev" "vfat" "$C_RED" "$C_RST" "${MOUNT_ERR:-unknown error}"
note " Listed anyway so you can still select it."
ESPS+=("$dev")
ESP_DESC+=("/dev/$pdisk ${pmodel:-?} [${ptran:-?}] — ${SIZE:-?}, could not probe contents")
continue
fi
vendors=$(find "$PROBE_PATH/EFI" -maxdepth 1 -mindepth 1 -type d -printf '%f ' 2>/dev/null)
availm=$(( $(df -k --output=avail "$PROBE_PATH" 2>/dev/null | tail -1 | tr -d ' ') / 1024 ))
pct=$(df -k --output=pcent "$PROBE_PATH" 2>/dev/null | tail -1 | tr -d ' ')
haswin=""
[[ -f "$PROBE_PATH/EFI/Microsoft/Boot/bootmgfw.efi" ]] && haswin=", Windows loader present"
ESPS+=("$dev")
ESP_DESC+=("/dev/$pdisk ${pmodel:-?} [${ptran:-?}] — ${SIZE:-?}, ${availm}MB free (${pct} used)${haswin}")
ok "$dev on /dev/$pdisk — ${pmodel:-unknown} [${ptran:-?}]"
note " ${SIZE:-?}, ${availm}MB free (${pct} used)${haswin}"
note " vendors: ${vendors:-<none>}"
[[ -n "$MOUNT_NOTE" ]] && note " $MOUNT_NOTE"
if [[ $availm -lt 25 ]]; then
warn " low free space — run 'boot-doctor.sh --clean-esp' before installing GRUB"
fi
probe_put
done < <(lsblk -Pno NAME,FSTYPE,PARTTYPE,SIZE 2>/dev/null)
ESP_DEV="$OPT_ESP"
if [[ -z "$ESP_DEV" ]]; then
if [[ ${#ESPS[@]} -eq 0 ]]; then
bad "no EFI System Partition found on any disk"
note "You will need to create one (512MB, FAT32, flagged 'esp') before"
note "UEFI boot is possible. This script will not partition for you."
exit 1
fi
printf '\n'
for i in "${!ESPS[@]}"; do
printf ' %d) %-18s %s\n' "$((i+1))" "${ESPS[$i]}" "${ESP_DESC[$i]}"
done
printf '\n'
note "Pick the ESP that already holds the Windows loader, unless you have"
note "deliberately created a dedicated one for Ubuntu."
printf '\n %s>>%s which ESP should /boot/efi use? [1-%d] ' "$C_BLD$C_YEL" "$C_RST" "${#ESPS[@]}"
read -r sel </dev/tty
[[ "$sel" =~ ^[0-9]+$ ]] && [[ $sel -ge 1 && $sel -le ${#ESPS[@]} ]] || die "invalid selection"
ESP_DEV="${ESPS[$((sel-1))]}"
fi
[[ -b "$ESP_DEV" ]] || die "$ESP_DEV is not a block device"
# ==============================================================================
# 5. Separate /boot?
# ==============================================================================
SEP_BOOT=""
probe_umount
if mount -o ro "$ROOT_DEV" "$PROBE_MNT" 2>/dev/null; then
if [[ -z "$(ls -A "$PROBE_MNT/boot" 2>/dev/null)" ]]; then
warn "$ROOT_DEV has an empty /boot — you likely have a separate boot partition"
note "Re-run with the right device, or mount it manually after chroot."
fi
probe_umount
fi
# ==============================================================================
# 6. Mount everything
# ==============================================================================
section "Mount plan"
printf ' root %s -> %s\n' "$ROOT_DEV" "$TARGET"
printf ' esp %s -> %s/boot/efi\n' "$ESP_DEV" "$TARGET"
printf ' bind /dev /dev/pts /proc /sys /run\n'
printf ' bind /sys/firmware/efi/efivars (if present)\n'
if [[ $DRY_RUN -eq 1 ]]; then
printf '\n dry run — nothing mounted\n\n'
exit 0
fi
ask "Proceed with these mounts?" || exit 1
section "Mounting"
run mkdir -p "$TARGET" || die "cannot create $TARGET"
if mountpoint -q "$TARGET"; then
warn "$TARGET already mounted — run '$0 --umount' first"
exit 1
fi
# The live desktop auto-mounts partitions under /run/media. Release them.
release_automounts "$ROOT_DEV"
case $? in
0) run mount "$ROOT_DEV" "$TARGET" || die "failed to mount root" ;;
2) ok "root already relocated to $TARGET" ;;
*) die "could not free $ROOT_DEV — close any file manager windows and retry" ;;
esac
ok "root mounted at $TARGET"
run mkdir -p "$TARGET/boot/efi"
release_automounts "$ESP_DEV"
case $? in
0) run mount "$ESP_DEV" "$TARGET/boot/efi" || die "failed to mount ESP" ;;
2) ok "ESP already relocated" ;;
*) die "could not free $ESP_DEV" ;;
esac
ok "ESP mounted at $TARGET/boot/efi"
# Confirm we mounted what we intended
info "verifying:"
findmnt -no SOURCE,TARGET,FSTYPE "$TARGET" | sed 's/^/ /'
findmnt -no SOURCE,TARGET,FSTYPE "$TARGET/boot/efi" | sed 's/^/ /'
if [[ ! -d "$TARGET/etc" ]]; then
die "$TARGET has no /etc — wrong partition mounted as root"
fi
if [[ ! -d "$TARGET/boot/efi/EFI" ]]; then
warn "$TARGET/boot/efi has no /EFI directory — is this really the ESP?"
fi
for d in dev dev/pts proc sys run; do
run mount --rbind "/$d" "$TARGET/$d" 2>/dev/null || warn "could not bind /$d"
done
ok "kernel filesystems bound"
if [[ -d /sys/firmware/efi/efivars ]]; then
run mount --bind /sys/firmware/efi/efivars "$TARGET/sys/firmware/efi/efivars" 2>/dev/null
ok "efivars bound (efibootmgr will work in the chroot)"
else
warn "no efivars to bind — NVRAM writes will fail inside the chroot"
fi
# network for apt
if [[ -e /etc/resolv.conf ]]; then
cp -L /etc/resolv.conf "$TARGET/etc/resolv.conf" 2>/dev/null && ok "DNS copied for apt"
fi
# carry the fix script in
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOCTOR_SRC=""
DOCTOR_DEST_REL="/root/boot-doctor.sh"
# Look next to this script first, then a few other plausible places
for cand in \
"$SELF_DIR/boot-doctor.sh" \
"$PWD/boot-doctor.sh" \
"${SUDO_USER:+/home/$SUDO_USER/boot-doctor.sh}" \
"/root/boot-doctor.sh" \
"/cdrom/boot-doctor.sh"
do
[[ -n "$cand" && -f "$cand" ]] && { DOCTOR_SRC="$cand"; break; }
done
if [[ -n "$DOCTOR_SRC" ]]; then
mkdir -p "$TARGET/root" 2>/dev/null
if cp "$DOCTOR_SRC" "$TARGET$DOCTOR_DEST_REL" 2>/dev/null \
&& chmod +x "$TARGET$DOCTOR_DEST_REL" 2>/dev/null \
&& [[ -x "$TARGET$DOCTOR_DEST_REL" ]]; then
ok "boot-doctor.sh copied from $DOCTOR_SRC"
note " inside the chroot it is at: $DOCTOR_DEST_REL"
else
DOCTOR_SRC=""
bad "copy to $TARGET$DOCTOR_DEST_REL FAILED"
note " Is the target filesystem full or mounted read-only?"
note " Check with: df -h $TARGET and findmnt $TARGET"
fi
fi
if [[ -z "$DOCTOR_SRC" ]]; then
warn "boot-doctor.sh not available inside the chroot"
note "Searched: $SELF_DIR, $PWD, /root, /cdrom"
note "Copy it in manually from another terminal:"
note " sudo cp /path/to/boot-doctor.sh $TARGET/root/"
note "Or just run the repair commands by hand once inside."
fi
# ==============================================================================
# 7. Hand off
# ==============================================================================
section "Ready"
if [[ -n "$DOCTOR_SRC" ]]; then
note "Entering chroot. Inside, run:"
note ""
note " $DOCTOR_DEST_REL # diagnose (read-only)"
note " $DOCTOR_DEST_REL --clean-esp # free ESP space first if tight"
note " $DOCTOR_DEST_REL --fix # apply repairs"
else
note "Entering chroot without boot-doctor.sh. Minimum manual sequence:"
note ""
note " grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu"
note " grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable"
note " update-grub"
fi
note ""
note "Type 'exit' when done, then run: $0 --umount"
printf '\n'
trap - EXIT
chroot "$TARGET" /bin/bash
section "Chroot exited"
note "Remember to unmount before rebooting: $0 --umount"
printf '\n'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment