iso: add bos-rescue, first-boot probe, optional Calamares refresh
Live-ISO bos-rescue finds the installed btrfs @ and ESP, then offers arch-chroot and/or the same GRUB NVRAM + --removable sequence as post-install.sh. Recovery is grub-btrfs or this reinstall — GRUB pins rootflags=subvol=@. bos-first-boot runs once after the first graphical login: NVIDIA offer file + notify (no driver install), VM-without-GL notify, HiDPI hint file (never rewrites monitors.json). Re-enable the Calamares packages module as a refresh-only step with skip_if_no_internet and ignore_update_db_error so offline installs cannot abort on pacman -Sy.
This commit is contained in:
parent
fd385bafae
commit
744f18cd90
11 changed files with 838 additions and 18 deletions
|
|
@ -360,14 +360,15 @@ not shipped.
|
||||||
**The system won't boot (broken GRUB / lost EFI entry):**
|
**The system won't boot (broken GRUB / lost EFI entry):**
|
||||||
|
|
||||||
1. Boot the BOS ISO and open a terminal (`SUPER+Return`).
|
1. Boot the BOS ISO and open a terminal (`SUPER+Return`).
|
||||||
2. Mount the installed root and EFI, then chroot:
|
2. Run `sudo bos-rescue`. It finds the installed btrfs `@` and the ESP,
|
||||||
|
prints the devices it will use, and asks `YES` before writing. It can
|
||||||
|
`arch-chroot` and/or reinstall GRUB with the same sequence the
|
||||||
|
installer uses (NVRAM + `--removable` + `grub-mkconfig`).
|
||||||
|
3. Manual equivalent, if you would rather type it:
|
||||||
```sh
|
```sh
|
||||||
mount -o subvol=@ /dev/sdXN /mnt
|
mount -o subvol=@ /dev/sdXN /mnt
|
||||||
mount /dev/sdXP /mnt/boot/efi # the EFI partition
|
mount /dev/sdXP /mnt/boot/efi # the EFI partition
|
||||||
arch-chroot /mnt
|
arch-chroot /mnt
|
||||||
```
|
|
||||||
3. Reinstall the bootloader (the same sequence the installer uses):
|
|
||||||
```sh
|
|
||||||
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=BOS --recheck
|
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-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
|
||||||
grub-mkconfig -o /boot/grub/grub.cfg
|
grub-mkconfig -o /boot/grub/grub.cfg
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,15 @@ BOS ships the generic **Mesa** stack. AMD and Intel work out of the box.
|
||||||
firmware is not on the image, and there is no Hyprland NVIDIA env wiring.
|
firmware is not on the image, and there is no Hyprland NVIDIA env wiring.
|
||||||
Installing `nvidia` / `nvidia-utils` after the fact is not a product path.
|
Installing `nvidia` / `nvidia-utils` after the fact is not a product path.
|
||||||
|
|
||||||
|
On first graphical login, `bos-first-boot` probes `lspci` / `/proc` and,
|
||||||
|
if an NVIDIA GPU is present, writes `~/.local/state/bos/nvidia-offer.json`
|
||||||
|
and notifies that the proprietary driver is not on the ISO. It does **not**
|
||||||
|
install anything. bos-settings can grow a panel that reads that file later.
|
||||||
|
|
||||||
|
The same probe leaves a HiDPI hint at `~/.local/state/bos/hidpi-hint.json`
|
||||||
|
when scale > 1 or the panel is dense; it never rewrites `monitors.json`.
|
||||||
|
A VM without `/dev/dri` gets a notification only.
|
||||||
|
|
||||||
## Recovery
|
## Recovery
|
||||||
|
|
||||||
An update that breaks the system is recovered from the **GRUB "snapshots"
|
An update that breaks the system is recovered from the **GRUB "snapshots"
|
||||||
|
|
@ -17,5 +26,8 @@ BOS GRUB pins `rootflags=subvol=@`. `snapper rollback` swaps the default
|
||||||
subvolume; the installed `grub.cfg` will still boot `@`. Pick the grub-btrfs
|
subvolume; the installed `grub.cfg` will still boot `@`. Pick the grub-btrfs
|
||||||
entry so the kernel command line matches the snapshot you want.
|
entry so the kernel command line matches the snapshot you want.
|
||||||
|
|
||||||
A/B root swapping is not implemented. See the README Recovery section for
|
If the system will not boot (lost EFI entry / broken GRUB), boot the live
|
||||||
the "system will not boot" GRUB/EFI repair path.
|
ISO and run `sudo bos-rescue`. It mounts `@` + the ESP and offers the same
|
||||||
|
`grub-install` NVRAM + `--removable` sequence as `post-install.sh`.
|
||||||
|
|
||||||
|
A/B root swapping is not implemented. See the README Recovery section.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,24 @@
|
||||||
---
|
---
|
||||||
|
# Optional online pacman refresh. The previous packages step used
|
||||||
|
# update_db:true with no skip/ignore, so `pacman -Sy` aborted offline
|
||||||
|
# installs (the case bos-netcheck exists for). skip_if_no_internet
|
||||||
|
# skips the whole module when Calamares sees no network;
|
||||||
|
# ignore_update_db_error keeps a flake-mirror -Sy from failing the
|
||||||
|
# install. update_system stays false — this is not a -Syu.
|
||||||
|
#
|
||||||
|
# try_install is empty: pipewire-pulse / pipewire-alsa already come
|
||||||
|
# from packages.x86_64 via unpackfs. No extra packages (and no
|
||||||
|
# nvidia) are pulled here.
|
||||||
backend: pacman
|
backend: pacman
|
||||||
|
|
||||||
options:
|
skip_if_no_internet: true
|
||||||
- update_db: true
|
update_db: true
|
||||||
|
ignore_update_db_error: true
|
||||||
|
update_system: false
|
||||||
|
|
||||||
operations:
|
pacman:
|
||||||
- try_install:
|
num_retries: 1
|
||||||
- pipewire-pulse
|
disable_download_timeout: false
|
||||||
- pipewire-alsa
|
needed_only: true
|
||||||
|
|
||||||
|
operations: []
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,6 @@ sequence:
|
||||||
- users
|
- users
|
||||||
- networkcfg
|
- networkcfg
|
||||||
- hwclock
|
- hwclock
|
||||||
# packages module removed: it set update_db:true with no
|
|
||||||
# skip_if_no_internet/ignore_update_db_error, so an offline install (the
|
|
||||||
# exact case bos-welcome's nmtui step exists for) aborted here with a
|
|
||||||
# fatal pacman -Sy failure. Its only try_install packages (pipewire-pulse,
|
|
||||||
# pipewire-alsa) are already in packages.x86_64 and installed by
|
|
||||||
# unpackfs, so the step did nothing useful even when it succeeded.
|
|
||||||
# archiso strips the kernel from the squashfs; stage it, drop the archiso
|
# archiso strips the kernel from the squashfs; stage it, drop the archiso
|
||||||
# initramfs config, and write a stock mkinitcpio preset before initcpio runs.
|
# initramfs config, and write a stock mkinitcpio preset before initcpio runs.
|
||||||
- shellprocess@kernel
|
- shellprocess@kernel
|
||||||
|
|
@ -57,6 +51,12 @@ sequence:
|
||||||
# BOS finalization: GRUB install + cleanup + snapper + services + dotfiles.
|
# BOS finalization: GRUB install + cleanup + snapper + services + dotfiles.
|
||||||
# All fast, and runs after initcpio so /boot has the kernel + initramfs.
|
# All fast, and runs after initcpio so /boot has the kernel + initramfs.
|
||||||
- shellprocess
|
- shellprocess
|
||||||
|
# Optional online pacman -Sy. After post-install so the target keyring
|
||||||
|
# exists. skip_if_no_internet + ignore_update_db_error: an offline
|
||||||
|
# install (or a flake-mirror -Sy) must not abort. operations is empty —
|
||||||
|
# pipewire-pulse/alsa already come from unpackfs; nothing extra (and
|
||||||
|
# no nvidia) is installed here.
|
||||||
|
- packages
|
||||||
- umount
|
- umount
|
||||||
- show:
|
- show:
|
||||||
- finished
|
- finished
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
{ "command": "breadbar", "label": "Bar (breadbar)", "enabled": true },
|
{ "command": "breadbar", "label": "Bar (breadbar)", "enabled": true },
|
||||||
{ "command": "hypridle", "label": "Idle / lock daemon (hypridle)", "enabled": true },
|
{ "command": "hypridle", "label": "Idle / lock daemon (hypridle)", "enabled": true },
|
||||||
{ "command": "bos-netcheck", "label": "Network connectivity check", "enabled": true },
|
{ "command": "bos-netcheck", "label": "Network connectivity check", "enabled": true },
|
||||||
|
{ "command": "bash -c 'command -v bos-first-boot >/dev/null && exec bos-first-boot'", "label": "First-boot hardware probe", "enabled": true },
|
||||||
{ "command": "breadhelp --autostart", "label": "BOS Help (first-run onboarding)", "enabled": true },
|
{ "command": "breadhelp --autostart", "label": "BOS Help (first-run onboarding)", "enabled": true },
|
||||||
{ "command": "bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'", "label": "Wallpaper command bus (breadpaper listen)", "enabled": true },
|
{ "command": "bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'", "label": "Wallpaper command bus (breadpaper listen)", "enabled": true },
|
||||||
{ "command": "bash -c 'command -v breadshot >/dev/null && exec breadshot listen'", "label": "Screenshot command bus (breadshot listen)", "enabled": true },
|
{ "command": "bash -c 'command -v breadshot >/dev/null && exec breadshot listen'", "label": "Screenshot command bus (breadshot listen)", "enabled": true },
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ hl.on("hyprland.start", function()
|
||||||
"breadbar",
|
"breadbar",
|
||||||
"hypridle",
|
"hypridle",
|
||||||
"bos-netcheck",
|
"bos-netcheck",
|
||||||
|
"bash -c 'command -v bos-first-boot >/dev/null && exec bos-first-boot'",
|
||||||
"breadhelp --autostart",
|
"breadhelp --autostart",
|
||||||
"bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'",
|
"bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'",
|
||||||
"bash -c 'command -v breadshot >/dev/null && exec breadshot listen'",
|
"bash -c 'command -v breadshot >/dev/null && exec breadshot listen'",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ local DEFAULT_EXTRA = {
|
||||||
{ command = "breadbar", enabled = true },
|
{ command = "breadbar", enabled = true },
|
||||||
{ command = "hypridle", enabled = true },
|
{ command = "hypridle", enabled = true },
|
||||||
{ command = "bos-netcheck", enabled = true },
|
{ command = "bos-netcheck", enabled = true },
|
||||||
|
{ command = "bash -c 'command -v bos-first-boot >/dev/null && exec bos-first-boot'", enabled = true },
|
||||||
{ command = "breadhelp --autostart", enabled = true },
|
{ command = "breadhelp --autostart", enabled = true },
|
||||||
{ command = "bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'", enabled = true },
|
{ command = "bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'", enabled = true },
|
||||||
{ command = "bash -c 'command -v breadshot >/dev/null && exec breadshot listen'", enabled = true },
|
{ command = "bash -c 'command -v breadshot >/dev/null && exec breadshot listen'", enabled = true },
|
||||||
|
|
|
||||||
187
iso/airootfs/usr/local/bin/bos-first-boot
Executable file
187
iso/airootfs/usr/local/bin/bos-first-boot
Executable file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# bos-first-boot — one-shot hardware probe after the first graphical login.
|
||||||
|
#
|
||||||
|
# Detects NVIDIA (offer file + notify; never auto-installs a driver), a VM
|
||||||
|
# without GL, and HiDPI (hint file only — never rewrites monitors.json).
|
||||||
|
#
|
||||||
|
# Non-fatal: missing tools, notify-send, or hyprctl must not block login.
|
||||||
|
# Guarded with `command -v`. Flag: ~/.local/state/bos/first-boot-done.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/bos"
|
||||||
|
FLAG="$STATE_DIR/first-boot-done"
|
||||||
|
NVIDIA_OFFER="$STATE_DIR/nvidia-offer.json"
|
||||||
|
HIDPI_HINT="$STATE_DIR/hidpi-hint.json"
|
||||||
|
VM_HINT="$STATE_DIR/vm-gl-hint.json"
|
||||||
|
|
||||||
|
# Never run on the live/installer session — only on an installed system.
|
||||||
|
[[ "$(id -un)" == "liveuser" ]] && exit 0
|
||||||
|
|
||||||
|
# Already probed this home.
|
||||||
|
[[ -f "$FLAG" ]] && exit 0
|
||||||
|
|
||||||
|
notify() {
|
||||||
|
local msg="$1"
|
||||||
|
local urgency="${2:-normal}"
|
||||||
|
command -v notify-send >/dev/null 2>&1 || return 0
|
||||||
|
notify-send -u "$urgency" "BOS" "$msg" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
iso_now() {
|
||||||
|
date -Iseconds 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ
|
||||||
|
}
|
||||||
|
|
||||||
|
# Best-effort: hyprland.start can beat the notification daemon by a beat.
|
||||||
|
if [[ -z "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR" 2>/dev/null || exit 0
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# NVIDIA — hardware only. Do not install nvidia / nvidia-utils.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
nvidia_present=0
|
||||||
|
nvidia_pci=""
|
||||||
|
if command -v lspci >/dev/null 2>&1; then
|
||||||
|
nvidia_pci="$(lspci -d 10de: -nn 2>/dev/null | grep -iE 'VGA|3D|Display' || true)"
|
||||||
|
[[ -n "$nvidia_pci" ]] && nvidia_present=1
|
||||||
|
fi
|
||||||
|
if [[ "$nvidia_present" != "1" ]]; then
|
||||||
|
if [[ -d /proc/driver/nvidia || -d /sys/module/nvidia ]]; then
|
||||||
|
nvidia_present=1
|
||||||
|
nvidia_pci="${nvidia_pci:-module}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "$nvidia_present" == "1" ]]; then
|
||||||
|
cat >"$NVIDIA_OFFER" <<EOF
|
||||||
|
{
|
||||||
|
"detected": true,
|
||||||
|
"pci": "$(json_escape "$nvidia_pci")",
|
||||||
|
"driver_on_iso": false,
|
||||||
|
"auto_install": false,
|
||||||
|
"message": "NVIDIA GPU detected. The proprietary driver is not on the ISO.",
|
||||||
|
"offered_at": "$(iso_now)"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
notify "NVIDIA GPU detected. The proprietary driver is not on the ISO — open BOS Settings later. Nothing was installed." normal
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# VM without GL (no /dev/dri). Notify only when both are true.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
virt="none"
|
||||||
|
if command -v systemd-detect-virt >/dev/null 2>&1; then
|
||||||
|
virt="$(systemd-detect-virt 2>/dev/null || true)"
|
||||||
|
[[ -n "$virt" ]] || virt="none"
|
||||||
|
fi
|
||||||
|
has_gl=0
|
||||||
|
shopt -s nullglob
|
||||||
|
dri_nodes=(/dev/dri/card* /dev/dri/renderD*)
|
||||||
|
(( ${#dri_nodes[@]} > 0 )) && has_gl=1
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
if [[ "$virt" != "none" && "$has_gl" != "1" ]]; then
|
||||||
|
cat >"$VM_HINT" <<EOF
|
||||||
|
{
|
||||||
|
"virt": "$(json_escape "$virt")",
|
||||||
|
"gl": false,
|
||||||
|
"dri": false,
|
||||||
|
"noted_at": "$(iso_now)"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
notify "This looks like a virtual machine without hardware GL. Hyprland may use software rendering." normal
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HiDPI — hint file for bos-settings. Do not rewrite monitors.json.
|
||||||
|
# scale > 1 from hyprctl, or computed DPI >= 140.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if command -v hyprctl >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then
|
||||||
|
# Compositor may still be settling when autostart fires.
|
||||||
|
mon_json=""
|
||||||
|
tries=0
|
||||||
|
while [[ -z "$mon_json" && "$tries" -lt 5 ]]; do
|
||||||
|
mon_json="$(hyprctl -j monitors 2>/dev/null || true)"
|
||||||
|
if [[ -z "$mon_json" || "$mon_json" == "[]" ]]; then
|
||||||
|
mon_json=""
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
tries=$((tries + 1))
|
||||||
|
done
|
||||||
|
if [[ -n "$mon_json" ]]; then
|
||||||
|
BOS_HYPR_MONITORS="$mon_json" python3 - "$HIDPI_HINT" "$(iso_now)" <<'PY' || true
|
||||||
|
import json, os, sys
|
||||||
|
hint_path, noted_at = sys.argv[1], sys.argv[2]
|
||||||
|
try:
|
||||||
|
monitors = json.loads(os.environ.get("BOS_HYPR_MONITORS") or "")
|
||||||
|
except Exception:
|
||||||
|
sys.exit(0)
|
||||||
|
if not isinstance(monitors, list):
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
hits = []
|
||||||
|
for m in monitors:
|
||||||
|
if not isinstance(m, dict):
|
||||||
|
continue
|
||||||
|
name = m.get("name") or m.get("output") or ""
|
||||||
|
try:
|
||||||
|
scale = float(m.get("scale") or 1)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
scale = 1.0
|
||||||
|
try:
|
||||||
|
w = int(m.get("width") or 0)
|
||||||
|
h = int(m.get("height") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
w = h = 0
|
||||||
|
mm_w = mm_h = 0
|
||||||
|
phys = m.get("physicalSize")
|
||||||
|
if isinstance(phys, dict):
|
||||||
|
mm_w = phys.get("x") or phys.get("width") or 0
|
||||||
|
mm_h = phys.get("y") or phys.get("height") or 0
|
||||||
|
elif isinstance(phys, (list, tuple)) and len(phys) >= 2:
|
||||||
|
mm_w, mm_h = phys[0], phys[1]
|
||||||
|
else:
|
||||||
|
mm_w = m.get("physicalWidth") or 0
|
||||||
|
mm_h = m.get("physicalHeight") or 0
|
||||||
|
try:
|
||||||
|
mm_w = float(mm_w or 0)
|
||||||
|
mm_h = float(mm_h or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
mm_w = mm_h = 0.0
|
||||||
|
dpi = round(w / (mm_w / 25.4), 1) if mm_w and w else 0.0
|
||||||
|
px_per_mm = round(w / mm_w, 3) if mm_w and w else 0.0
|
||||||
|
# High px/mm (dense panel) or Hyprland already chose scale > 1.
|
||||||
|
hidpi = scale > 1.01 or dpi >= 140
|
||||||
|
if hidpi:
|
||||||
|
hits.append({
|
||||||
|
"name": name,
|
||||||
|
"width": w,
|
||||||
|
"height": h,
|
||||||
|
"scale": scale,
|
||||||
|
"dpi": dpi,
|
||||||
|
"px_per_mm": px_per_mm,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not hits:
|
||||||
|
sys.exit(0)
|
||||||
|
with open(hint_path, "w") as f:
|
||||||
|
json.dump({
|
||||||
|
"suggested": True,
|
||||||
|
"rewrote_monitors_json": False,
|
||||||
|
"reason": "scale > 1 or DPI >= 140",
|
||||||
|
"monitors": hits,
|
||||||
|
"noted_at": noted_at,
|
||||||
|
}, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mark done even if every probe was a no-op — do not nag next login.
|
||||||
|
printf '%s\n' "$(iso_now)" >"$FLAG" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
598
iso/airootfs/usr/local/bin/bos-rescue
Executable file
598
iso/airootfs/usr/local/bin/bos-rescue
Executable file
|
|
@ -0,0 +1,598 @@
|
||||||
|
#!/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: DEVICE<TAB>KIND<TAB>PRETTY 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 <disk-hosting-root>
|
||||||
|
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 "$@"
|
||||||
|
|
@ -29,4 +29,6 @@ file_permissions=(
|
||||||
["/usr/local/bin/bos-session"]="0:0:755"
|
["/usr/local/bin/bos-session"]="0:0:755"
|
||||||
["/usr/local/bin/bos-netcheck"]="0:0:755"
|
["/usr/local/bin/bos-netcheck"]="0:0:755"
|
||||||
["/usr/local/bin/bos-update"]="0:0:755"
|
["/usr/local/bin/bos-update"]="0:0:755"
|
||||||
|
["/usr/local/bin/bos-rescue"]="0:0:755"
|
||||||
|
["/usr/local/bin/bos-first-boot"]="0:0:755"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ check "breadhelp installed" "command -v breadhelp"
|
||||||
check "breadhelp content installed" \
|
check "breadhelp content installed" \
|
||||||
"[ -d \"\$HOME/.local/share/breadhelp/content\" ] || [ -d /etc/skel/.local/share/breadhelp/content ]"
|
"[ -d \"\$HOME/.local/share/breadhelp/content\" ] || [ -d /etc/skel/.local/share/breadhelp/content ]"
|
||||||
check "bos-netcheck present" "command -v bos-netcheck"
|
check "bos-netcheck present" "command -v bos-netcheck"
|
||||||
|
check "bos-rescue present" "command -v bos-rescue"
|
||||||
|
check "bos-first-boot present" "command -v bos-first-boot"
|
||||||
|
|
||||||
echo "== default dotfiles =="
|
echo "== default dotfiles =="
|
||||||
check "hyprland.lua present" "[ -f \"\$HOME/.config/hypr/hyprland.lua\" ]"
|
check "hyprland.lua present" "[ -f \"\$HOME/.config/hypr/hyprland.lua\" ]"
|
||||||
|
|
@ -72,6 +74,7 @@ check "binds.json present" "[ -f \"\$HOME/.config/hypr/binds.json\" ]"
|
||||||
check "monitors.json present" "[ -f \"\$HOME/.config/hypr/monitors.json\" ]"
|
check "monitors.json present" "[ -f \"\$HOME/.config/hypr/monitors.json\" ]"
|
||||||
check "settings.json present" "[ -f \"\$HOME/.config/hypr/settings.json\" ]"
|
check "settings.json present" "[ -f \"\$HOME/.config/hypr/settings.json\" ]"
|
||||||
check "autostart.json present" "[ -f \"\$HOME/.config/hypr/autostart.json\" ]"
|
check "autostart.json present" "[ -f \"\$HOME/.config/hypr/autostart.json\" ]"
|
||||||
|
check "autostart includes first-boot probe" "grep -q bos-first-boot \"\$HOME/.config/hypr/autostart.json\""
|
||||||
check "hypr scripts/lib present" "[ -f \"\$HOME/.config/hypr/scripts/lib/json.lua\" ]"
|
check "hypr scripts/lib present" "[ -f \"\$HOME/.config/hypr/scripts/lib/json.lua\" ]"
|
||||||
check "mimeapps.list present" "[ -f \"\$HOME/.config/mimeapps.list\" ]"
|
check "mimeapps.list present" "[ -f \"\$HOME/.config/mimeapps.list\" ]"
|
||||||
check "kitty config present" "[ -f \"\$HOME/.config/kitty/kitty.conf\" ]"
|
check "kitty config present" "[ -f \"\$HOME/.config/kitty/kitty.conf\" ]"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue