Patches the Omarchy ISO to install alongside Windows with a firmware-proof Limine-first boot (bootmgfw spoof), LUKS2 encryption, and btrfs snapshots. Includes repair mode for re-applying Limine after a Windows feature update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1371 lines
52 KiB
Bash
Executable file
1371 lines
52 KiB
Bash
Executable file
#!/bin/bash
|
|
#===============================================================================
|
|
# PATCH OMARCHY ISO FOR DUAL-BOOT
|
|
#
|
|
# This version hooks into the original Omarchy installer (archinstall)
|
|
# We just handle partitioning, then let archinstall do everything else
|
|
#
|
|
# Usage: sudo ./patch-omarchy-dualboot.sh omarchy-3.x.x.iso
|
|
#===============================================================================
|
|
|
|
set -e
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${GREEN}[✓]${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
|
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
[[ $EUID -ne 0 ]] && error "Run as root: sudo $0"
|
|
|
|
# Auto-detect ISO if not provided
|
|
if [[ -z "$1" ]]; then
|
|
SOURCE_ISO=$(find "$SCRIPT_DIR" -maxdepth 1 -name "omarchy-*.iso" ! -name "omarchy-dualboot-*" ! -name "omarchy-installer-*" ! -name "win-omarchy-*" 2>/dev/null | head -1)
|
|
if [[ -z "$SOURCE_ISO" ]]; then
|
|
error "No Omarchy ISO found in $SCRIPT_DIR
|
|
|
|
Please either:
|
|
1. Place omarchy-*.iso in the same directory as this script
|
|
2. Or specify the path: sudo $0 /path/to/omarchy.iso"
|
|
fi
|
|
log "Auto-detected ISO: $(basename "$SOURCE_ISO")"
|
|
else
|
|
SOURCE_ISO="$1"
|
|
fi
|
|
|
|
[[ ! -f "$SOURCE_ISO" ]] && error "ISO not found: $SOURCE_ISO"
|
|
WORK_DIR="$SCRIPT_DIR/.omarchy-patch-$$"
|
|
OUTPUT_ISO="$SCRIPT_DIR/win-omarchy-$(date +%Y.%m.%d).iso"
|
|
|
|
# Check and install dependencies
|
|
MISSING_PKGS=""
|
|
command -v xorriso &>/dev/null || MISSING_PKGS="$MISSING_PKGS xorriso"
|
|
command -v unsquashfs &>/dev/null || MISSING_PKGS="$MISSING_PKGS squashfs-tools"
|
|
command -v isoinfo &>/dev/null || MISSING_PKGS="$MISSING_PKGS cdrtools"
|
|
|
|
if [[ -n "$MISSING_PKGS" ]]; then
|
|
log "Installing missing dependencies:$MISSING_PKGS"
|
|
pacman -S --noconfirm --needed $MISSING_PKGS || error "Failed to install dependencies"
|
|
log "Dependencies installed"
|
|
fi
|
|
|
|
log "Patching: $SOURCE_ISO"
|
|
|
|
# Cleanup on exit
|
|
cleanup() {
|
|
umount "$WORK_DIR/iso" 2>/dev/null || true
|
|
rm -rf "$WORK_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
mkdir -p "$WORK_DIR"/{iso,extracted,newiso}
|
|
|
|
#===============================================================================
|
|
# EXTRACT ISO
|
|
#===============================================================================
|
|
|
|
log "Extracting ISO..."
|
|
mount -o loop,ro "$SOURCE_ISO" "$WORK_DIR/iso"
|
|
cp -a "$WORK_DIR/iso/"* "$WORK_DIR/newiso/"
|
|
umount "$WORK_DIR/iso"
|
|
|
|
# Find squashfs
|
|
SQUASHFS="$WORK_DIR/newiso/arch/x86_64/airootfs.sfs"
|
|
[[ ! -f "$SQUASHFS" ]] && error "Squashfs not found at expected path"
|
|
|
|
log "Extracting squashfs (this takes a few minutes)..."
|
|
unsquashfs -d "$WORK_DIR/extracted" "$SQUASHFS"
|
|
|
|
# Verify squashfs structure
|
|
if [[ ! -d "$WORK_DIR/extracted/root" ]]; then
|
|
error "Unexpected squashfs structure: /root directory not found!"
|
|
fi
|
|
|
|
log "Original /root contents:"
|
|
ls -la "$WORK_DIR/extracted/root/" | grep -E '\.(zlogin|zprofile|automated|bash)' || echo " (no matching files)"
|
|
|
|
#===============================================================================
|
|
# CREATE DUAL-BOOT WRAPPER SCRIPT
|
|
#===============================================================================
|
|
|
|
log "Creating dual-boot wrapper..."
|
|
|
|
# This script runs BEFORE the original installer
|
|
# It handles partitioning, then lets archinstall do the rest
|
|
cat > "$WORK_DIR/extracted/root/dualboot-setup.sh" << 'DUALBOOT_SCRIPT'
|
|
#!/bin/bash
|
|
#===============================================================================
|
|
# DUAL-BOOT PARTITION SETUP
|
|
# Creates partitions in free space, then hands off to archinstall
|
|
#===============================================================================
|
|
|
|
set -e
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
|
|
|
log() { echo -e "${GREEN}[✓]${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
|
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
|
info() { echo -e "${CYAN}[i]${NC} $1"; }
|
|
|
|
header() {
|
|
echo ""
|
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
echo -e "${BOLD} $1${NC}"
|
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
echo ""
|
|
}
|
|
|
|
cleanup_and_exit() {
|
|
warn "Setup cancelled or failed. Cleaning up..."
|
|
umount -R /mnt 2>/dev/null || true
|
|
cryptsetup close cryptroot 2>/dev/null || true
|
|
exit 1
|
|
}
|
|
|
|
#===============================================================================
|
|
# CLEANUP FUNCTION FOR FAILED INSTALLATIONS
|
|
#===============================================================================
|
|
|
|
cleanup_failed_install() {
|
|
header "CLEANUP FAILED INSTALLATION"
|
|
|
|
echo "This will remove Linux partitions from a failed installation attempt."
|
|
echo "Windows partitions will NOT be touched."
|
|
echo ""
|
|
|
|
echo "Available drives:"
|
|
lsblk -d -o NAME,SIZE,MODEL | grep -E "^(nvme|sd|vd)"
|
|
echo ""
|
|
|
|
read -p "Enter drive to clean (e.g., nvme0n1): " drive_input
|
|
DRIVE="/dev/$drive_input"
|
|
|
|
[[ ! -b "$DRIVE" ]] && error "Drive not found: $DRIVE"
|
|
[[ "$DRIVE" == *"nvme"* ]] && PART_PREFIX="${DRIVE}p" || PART_PREFIX="$DRIVE"
|
|
|
|
echo ""
|
|
echo "Current partitions:"
|
|
lsblk -o NAME,SIZE,FSTYPE,LABEL "$DRIVE"
|
|
echo ""
|
|
|
|
cryptsetup close cryptroot 2>/dev/null || true
|
|
umount -R /mnt 2>/dev/null || true
|
|
|
|
LINUX_EFI=""
|
|
LINUX_ROOT=""
|
|
|
|
for part in ${PART_PREFIX}*; do
|
|
[[ "$part" == "$DRIVE" || "$part" == "${PART_PREFIX}" ]] && continue
|
|
[[ ! -b "$part" ]] && continue
|
|
|
|
LABEL=$(lsblk -n -o LABEL "$part" 2>/dev/null | tr -d ' ')
|
|
FSTYPE=$(lsblk -n -o FSTYPE "$part" 2>/dev/null | tr -d ' ')
|
|
|
|
if [[ "$LABEL" == "LINUXEFI" ]]; then
|
|
LINUX_EFI="$part"
|
|
echo -e "${YELLOW}Found Linux EFI partition: $part${NC}"
|
|
elif [[ "$FSTYPE" == "crypto_LUKS" ]]; then
|
|
LINUX_ROOT="$part"
|
|
echo -e "${YELLOW}Found LUKS partition: $part${NC}"
|
|
fi
|
|
done
|
|
|
|
if [[ -z "$LINUX_EFI" && -z "$LINUX_ROOT" ]]; then
|
|
echo ""
|
|
info "No Linux partitions found from failed installation."
|
|
read -p "Press Enter to continue..." _
|
|
return 1
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${RED}WARNING: The following partitions will be DELETED:${NC}"
|
|
[[ -n "$LINUX_EFI" ]] && echo " - $LINUX_EFI (Linux EFI)"
|
|
[[ -n "$LINUX_ROOT" ]] && echo " - $LINUX_ROOT (Linux Root/LUKS)"
|
|
echo ""
|
|
|
|
read -p "Type 'DELETE' to confirm removal: " confirm
|
|
if [[ "$confirm" != "DELETE" ]]; then
|
|
echo "Cancelled."
|
|
return 1
|
|
fi
|
|
|
|
if [[ -n "$LINUX_ROOT" ]]; then
|
|
PART_NUM=$(echo "$LINUX_ROOT" | grep -oE '[0-9]+$')
|
|
log "Deleting partition $PART_NUM..."
|
|
sgdisk -d "$PART_NUM" "$DRIVE"
|
|
fi
|
|
|
|
if [[ -n "$LINUX_EFI" ]]; then
|
|
PART_NUM=$(echo "$LINUX_EFI" | grep -oE '[0-9]+$')
|
|
log "Deleting partition $PART_NUM..."
|
|
sgdisk -d "$PART_NUM" "$DRIVE"
|
|
fi
|
|
|
|
partprobe "$DRIVE"
|
|
|
|
for entry in $(efibootmgr 2>/dev/null | grep -i "Omarchy" | sed -n 's/^Boot\([0-9A-Fa-f]*\).*/\1/p'); do
|
|
efibootmgr -b "$entry" -B 2>/dev/null || true
|
|
done
|
|
|
|
# Restore the genuine Windows bootloader if the install spoofed it. The
|
|
# dual-boot install replaces \EFI\Microsoft\Boot\bootmgfw.efi with Limine
|
|
# and saves the real loader as bootmgfwbackup.efi. Without restoring it,
|
|
# removing the Linux partitions leaves a now-configless Limine as Windows'
|
|
# bootloader and Windows won't boot. The Windows ESP is never deleted, so
|
|
# the backup is still there.
|
|
log "Restoring genuine Windows bootloader if it was spoofed..."
|
|
for part in ${PART_PREFIX}*; do
|
|
[[ "$part" == "$DRIVE" || "$part" == "${PART_PREFIX}" ]] && continue
|
|
[[ ! -b "$part" ]] && continue
|
|
[[ "$(lsblk -no FSTYPE "$part" 2>/dev/null)" != "vfat" ]] && continue
|
|
RMNT=$(mktemp -d)
|
|
if mount "$part" "$RMNT" 2>/dev/null; then
|
|
WB="$RMNT/EFI/Microsoft/Boot"
|
|
if [[ -f "$WB/bootmgfwbackup.efi" ]]; then
|
|
if cp "$WB/bootmgfwbackup.efi" "$WB/bootmgfw.efi"; then
|
|
rm -f "$WB/bootmgfwbackup.efi"
|
|
sync
|
|
log "Restored genuine Windows bootloader on $part"
|
|
else
|
|
warn "Failed to restore bootmgfw.efi on $part — Windows may not boot!"
|
|
fi
|
|
fi
|
|
umount "$RMNT"
|
|
fi
|
|
rmdir "$RMNT"
|
|
done
|
|
|
|
log "Cleanup complete!"
|
|
echo ""
|
|
lsblk -o NAME,SIZE,FSTYPE,LABEL "$DRIVE"
|
|
echo ""
|
|
read -p "Press Enter to continue..." _
|
|
return 0
|
|
}
|
|
|
|
#===============================================================================
|
|
# REPAIR DUAL-BOOT (re-apply the bootmgfw spoof after a Windows update)
|
|
#===============================================================================
|
|
#
|
|
# A Windows feature update can reinstall \EFI\Microsoft\Boot\bootmgfw.efi,
|
|
# overwriting our Limine spoof. The machine then boots straight to Windows and
|
|
# the Limine menu disappears. This repair re-installs Limine over bootmgfw.efi
|
|
# (refreshing the backup with the now-current Windows loader) so Limine loads
|
|
# first again. Neither Windows nor the Omarchy install is otherwise touched.
|
|
|
|
repair_dualboot() {
|
|
header "REPAIR DUAL-BOOT — RE-APPLY LIMINE"
|
|
|
|
echo "Use this if the machine started booting straight to Windows and the"
|
|
echo "Limine menu disappeared (usually after a Windows feature update"
|
|
echo "overwrote the boot file)."
|
|
echo ""
|
|
echo "This re-installs Limine as the Windows boot file. Windows and your"
|
|
echo "Omarchy install are NOT modified."
|
|
echo ""
|
|
|
|
echo "Available drives:"
|
|
lsblk -d -o NAME,SIZE,MODEL | grep -E "^(nvme|sd|vd)"
|
|
echo ""
|
|
read -p "Enter the drive with the dual-boot install (e.g., nvme0n1): " drive_input
|
|
DRIVE="/dev/$drive_input"
|
|
[[ ! -b "$DRIVE" ]] && error "Drive not found: $DRIVE"
|
|
[[ "$DRIVE" == *"nvme"* ]] && PART_PREFIX="${DRIVE}p" || PART_PREFIX="$DRIVE"
|
|
|
|
# Locate the LINUXEFI partition (holds Limine + limine.conf) and the Windows
|
|
# ESP (holds bootmgfw.efi).
|
|
LINUXEFI_PART=""
|
|
WIN_ESP_PART=""
|
|
WIN_ESP_PARTUUID=""
|
|
for part in ${PART_PREFIX}*; do
|
|
[[ "$part" == "$DRIVE" || "$part" == "${PART_PREFIX}" ]] && continue
|
|
[[ ! -b "$part" ]] && continue
|
|
[[ "$(lsblk -no FSTYPE "$part" 2>/dev/null)" != "vfat" ]] && continue
|
|
if [[ "$(lsblk -no LABEL "$part" 2>/dev/null | tr -d ' ')" == "LINUXEFI" ]]; then
|
|
LINUXEFI_PART="$part"
|
|
continue
|
|
fi
|
|
TMP=$(mktemp -d)
|
|
if mount -o ro "$part" "$TMP" 2>/dev/null; then
|
|
[[ -f "$TMP/EFI/Microsoft/Boot/bootmgfw.efi" ]] && {
|
|
WIN_ESP_PART="$part"
|
|
WIN_ESP_PARTUUID=$(lsblk -no PARTUUID "$part")
|
|
}
|
|
umount "$TMP"
|
|
fi
|
|
rmdir "$TMP"
|
|
done
|
|
|
|
[[ -z "$LINUXEFI_PART" ]] && error "LINUXEFI partition not found on $DRIVE — is this the right drive?"
|
|
[[ -z "$WIN_ESP_PART" ]] && error "Windows ESP (with bootmgfw.efi) not found on $DRIVE"
|
|
log "LINUXEFI: $LINUXEFI_PART"
|
|
log "Windows ESP: $WIN_ESP_PART (PARTUUID=$WIN_ESP_PARTUUID)"
|
|
|
|
# Pull the exact Limine binary from the install's LINUXEFI partition.
|
|
LMNT=$(mktemp -d)
|
|
mount -o ro "$LINUXEFI_PART" "$LMNT" || error "Could not mount $LINUXEFI_PART"
|
|
LIMINE_SRC="$LMNT/EFI/BOOT/BOOTX64.EFI"
|
|
if [[ ! -f "$LIMINE_SRC" ]]; then
|
|
umount "$LMNT"; rmdir "$LMNT"
|
|
error "Limine EFI not found at LINUXEFI:/EFI/BOOT/BOOTX64.EFI"
|
|
fi
|
|
cp "$LIMINE_SRC" /tmp/limine-repair.efi
|
|
umount "$LMNT"; rmdir "$LMNT"
|
|
|
|
# Re-apply the spoof on the Windows ESP.
|
|
WMNT=$(mktemp -d)
|
|
mount "$WIN_ESP_PART" "$WMNT" || error "Could not mount $WIN_ESP_PART"
|
|
WB="$WMNT/EFI/Microsoft/Boot"
|
|
|
|
if cmp -s "$WB/bootmgfw.efi" /tmp/limine-repair.efi; then
|
|
log "bootmgfw.efi is already Limine — spoof is intact, no repair needed"
|
|
echo ""
|
|
info "If the Limine menu still doesn't appear, the cause is the firmware"
|
|
info "boot order, not the spoof. Use the firmware boot menu (F8/F11/F12)"
|
|
info "to select 'Windows Boot Manager' once — it will load Limine."
|
|
else
|
|
log "bootmgfw.efi is the Windows loader — re-applying the spoof"
|
|
# Refresh the backup with the CURRENT (post-update) Windows loader so the
|
|
# Windows menu entry chainloads the newest genuine loader.
|
|
if cp "$WB/bootmgfw.efi" "$WB/bootmgfwbackup.efi"; then
|
|
log "Saved current Windows loader -> bootmgfwbackup.efi"
|
|
else
|
|
warn "Could not refresh bootmgfwbackup.efi — keeping any existing backup"
|
|
fi
|
|
if cp /tmp/limine-repair.efi "$WB/bootmgfw.efi"; then
|
|
sync
|
|
log "Installed Limine as bootmgfw.efi — Limine will load first on next boot"
|
|
else
|
|
umount "$WMNT"; rmdir "$WMNT"; rm -f /tmp/limine-repair.efi
|
|
error "Failed to install Limine over bootmgfw.efi"
|
|
fi
|
|
fi
|
|
umount "$WMNT"; rmdir "$WMNT"
|
|
rm -f /tmp/limine-repair.efi
|
|
|
|
# Make sure limine.conf still has a Windows entry pointing at the backup.
|
|
# limine.conf lives on LINUXEFI and isn't touched by Windows updates, so this
|
|
# is just a safety net for an edge case.
|
|
CMNT=$(mktemp -d)
|
|
if mount "$LINUXEFI_PART" "$CMNT" 2>/dev/null; then
|
|
if [[ -f "$CMNT/limine.conf" ]] && ! grep -q "bootmgfwbackup.efi" "$CMNT/limine.conf"; then
|
|
warn "limine.conf has no Windows entry — adding one"
|
|
cat >> "$CMNT/limine.conf" <<WINREPAIR
|
|
|
|
/Windows Boot Manager
|
|
comment: Chainload the genuine Windows bootloader from the Windows ESP
|
|
protocol: efi_chainload
|
|
image_path: guid($WIN_ESP_PARTUUID):/EFI/Microsoft/Boot/bootmgfwbackup.efi
|
|
WINREPAIR
|
|
sync
|
|
fi
|
|
umount "$CMNT"
|
|
fi
|
|
rmdir "$CMNT"
|
|
|
|
echo ""
|
|
log "Repair finished — reboot to get the Limine menu back"
|
|
echo ""
|
|
read -p "Press Enter to continue..." _
|
|
return 0
|
|
}
|
|
|
|
#===============================================================================
|
|
# MAIN MENU
|
|
#===============================================================================
|
|
|
|
header "OMARCHY DUAL-BOOT INSTALLER"
|
|
|
|
echo "This installer will set up Omarchy alongside Windows."
|
|
echo "Windows partitions will NOT be touched."
|
|
echo ""
|
|
echo "Options:"
|
|
echo " 1) Dual Boot (Heaven and Hell mode) — install Omarchy alongside Windows"
|
|
echo " 2) Consider that a divorce (remove a failed install)"
|
|
echo " 3) Nuke the site from orbit — it's the only way to be sure (wipe drive, install Omarchy)"
|
|
echo " 4) Exit to Ghost in the... (drop to a terminal)"
|
|
echo " 5) I'll be back (re-apply Limine after a Windows update)"
|
|
echo ""
|
|
read -p "Select option [1]: " MENU_CHOICE
|
|
MENU_CHOICE=${MENU_CHOICE:-1}
|
|
|
|
case $MENU_CHOICE in
|
|
2)
|
|
cleanup_failed_install || true
|
|
exec /root/dualboot-setup.sh
|
|
;;
|
|
3)
|
|
# Run original installer
|
|
info "Starting standard Omarchy installer..."
|
|
exec /root/.automated_script.sh.orig
|
|
;;
|
|
4)
|
|
echo "Type '/root/dualboot-setup.sh' to restart installer."
|
|
exit 0
|
|
;;
|
|
5)
|
|
repair_dualboot || true
|
|
exec /root/dualboot-setup.sh
|
|
;;
|
|
1)
|
|
# Continue with dual-boot setup
|
|
;;
|
|
*)
|
|
exec /root/dualboot-setup.sh
|
|
;;
|
|
esac
|
|
|
|
#===============================================================================
|
|
# PRE-FLIGHT CHECKS
|
|
#===============================================================================
|
|
|
|
[[ ! -d /sys/firmware/efi ]] && error "UEFI mode required for dual-boot"
|
|
|
|
# Secure Boot check — Limine BOOTX64.EFI is unsigned, so firmware with
|
|
# Secure Boot enabled will silently reject it and fall through to the
|
|
# Windows Boot Manager, defeating the whole "Limine is the bootloader"
|
|
# design. Detect it now so the user can disable it in firmware first.
|
|
SB_STATE="unknown"
|
|
if command -v mokutil &>/dev/null; then
|
|
SB_STATE=$(mokutil --sb-state 2>/dev/null | head -1)
|
|
elif [[ -d /sys/firmware/efi/efivars ]]; then
|
|
SB_VAR=$(find /sys/firmware/efi/efivars -maxdepth 1 -name 'SecureBoot-*' 2>/dev/null | head -1)
|
|
if [[ -n "$SB_VAR" ]]; then
|
|
# 4-byte attributes header, then 1 byte: 0x00 = disabled, 0x01 = enabled
|
|
SB_BYTE=$(od -An -tu1 -j4 -N1 "$SB_VAR" 2>/dev/null | tr -d ' ')
|
|
case "$SB_BYTE" in
|
|
1) SB_STATE="SecureBoot enabled" ;;
|
|
0) SB_STATE="SecureBoot disabled" ;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
if echo "$SB_STATE" | grep -qi "enabled"; then
|
|
warn "Secure Boot is ENABLED in firmware."
|
|
echo ""
|
|
echo " Limine is unsigned. With Secure Boot on, the firmware will"
|
|
echo " silently reject Limine on every boot and fall through to"
|
|
echo " Windows Boot Manager — defeating this installer's whole point."
|
|
echo ""
|
|
echo " Reboot, enter firmware setup (F2/Del), DISABLE Secure Boot,"
|
|
echo " then re-run this installer."
|
|
echo ""
|
|
read -p "Continue anyway (not recommended)? (y/N): " sb_cont
|
|
[[ ! "$sb_cont" =~ ^[Yy]$ ]] && error "Cancelled — disable Secure Boot first"
|
|
else
|
|
log "Secure Boot: $SB_STATE"
|
|
fi
|
|
|
|
header "SELECT DRIVE"
|
|
|
|
echo "Available drives:"
|
|
echo ""
|
|
lsblk -d -o NAME,SIZE,MODEL | grep -E "^(nvme|sd|vd)"
|
|
echo ""
|
|
|
|
mapfile -t DRIVES < <(lsblk -d -n -o NAME,TRAN | awk '$2!="usb" {print $1}' | grep -E "^(nvme|sd|vd)")
|
|
|
|
if [[ ${#DRIVES[@]} -eq 1 ]]; then
|
|
DRIVE="/dev/${DRIVES[0]}"
|
|
info "Found: $DRIVE"
|
|
read -p "Use this drive? (Y/n): " confirm
|
|
[[ "$confirm" =~ ^[Nn]$ ]] && error "Cancelled"
|
|
else
|
|
read -p "Enter drive (e.g., nvme0n1): " drive_input
|
|
DRIVE="/dev/$drive_input"
|
|
fi
|
|
|
|
[[ ! -b "$DRIVE" ]] && error "Drive not found: $DRIVE"
|
|
[[ "$DRIVE" == *"nvme"* ]] && PART_PREFIX="${DRIVE}p" || PART_PREFIX="$DRIVE"
|
|
|
|
echo ""
|
|
echo "Current partitions on $DRIVE:"
|
|
lsblk -o NAME,SIZE,FSTYPE,LABEL "$DRIVE"
|
|
echo ""
|
|
|
|
# Check for Windows
|
|
if lsblk -o FSTYPE "$DRIVE" | grep -q ntfs; then
|
|
log "Windows detected - it will be preserved"
|
|
|
|
# BitLocker + TPM/PCR warning. Changing the boot path (and what the
|
|
# firmware loads first) changes the TPM PCR measurements Windows
|
|
# uses to unseal the BitLocker key. On next Windows boot this can
|
|
# demand the 48-digit recovery key. Same chain of consequences
|
|
# applies if Windows uses TPM-backed device encryption (default on
|
|
# most Win 11 OEM installs) even without explicit BitLocker setup.
|
|
header "BITLOCKER / DEVICE ENCRYPTION WARNING"
|
|
|
|
echo "Windows 11 typically has BitLocker or 'Device Encryption' on"
|
|
echo "by default, sealed to the TPM and current boot configuration."
|
|
echo ""
|
|
echo "After this install:"
|
|
echo " • Limine becomes the first thing the firmware loads"
|
|
echo " • TPM PCR measurements (PCR 4/7) will differ"
|
|
echo " • Next Windows boot can demand the 48-digit recovery key"
|
|
echo ""
|
|
echo -e "${BOLD}Before continuing, in Windows:${NC}"
|
|
echo -e " 1. Sign in and open ${BOLD}Settings → Privacy → Find My Device${NC}"
|
|
echo " and confirm your BitLocker recovery key is saved to your"
|
|
echo " Microsoft account (or print/save it locally)."
|
|
echo " 2. Open an admin PowerShell and run:"
|
|
echo -e " ${CYAN}manage-bde -protectors -disable C: -RebootCount 0${NC}"
|
|
echo " This suspends BitLocker until you re-enable it from Windows."
|
|
echo " 3. Shut down Windows fully (not 'restart' — Fast Startup will"
|
|
echo " leave the NTFS dirty and Windows will run chkdsk on next boot)."
|
|
echo ""
|
|
echo "If you skip this and don't have the recovery key, you may be"
|
|
echo "permanently locked out of your Windows installation."
|
|
echo ""
|
|
read -p "Type 'I HAVE MY RECOVERY KEY' to continue: " bl_confirm
|
|
if [[ "$bl_confirm" != "I HAVE MY RECOVERY KEY" ]]; then
|
|
error "Cancelled — back up your BitLocker recovery key first"
|
|
fi
|
|
else
|
|
warn "Windows not detected on this drive"
|
|
read -p "Continue anyway? (y/N): " cont
|
|
[[ ! "$cont" =~ ^[Yy]$ ]] && error "Cancelled"
|
|
fi
|
|
|
|
# Check free space
|
|
FREE_SECTORS=$(sgdisk -p "$DRIVE" 2>/dev/null | grep "Total free space" | awk '{print $5}')
|
|
if [[ "$FREE_SECTORS" =~ ^[0-9]+$ ]]; then
|
|
FREE_GB=$((FREE_SECTORS * 512 / 1024 / 1024 / 1024))
|
|
[[ $FREE_GB -lt 20 ]] && error "Need 20GB+ free space, found ${FREE_GB}GB"
|
|
log "Free space: ${FREE_GB}GB"
|
|
else
|
|
warn "Could not determine free space"
|
|
fi
|
|
|
|
#===============================================================================
|
|
# ENCRYPTION PASSWORD
|
|
#===============================================================================
|
|
|
|
header "DISK ENCRYPTION"
|
|
|
|
echo "Your Linux partition will be encrypted with LUKS2."
|
|
echo "You'll enter your user password in the next step (configurator)."
|
|
echo ""
|
|
|
|
while true; do
|
|
read -s -p "Enter LUKS encryption password: " LUKS_PASS; echo ""
|
|
read -s -p "Confirm password: " LUKS_PASS2; echo ""
|
|
[[ "$LUKS_PASS" == "$LUKS_PASS2" ]] && break
|
|
warn "Passwords don't match, try again"
|
|
done
|
|
|
|
#===============================================================================
|
|
# CONFIRMATION
|
|
#===============================================================================
|
|
|
|
header "CONFIRM PARTITIONING"
|
|
|
|
echo "The following will be created in FREE SPACE on $DRIVE:"
|
|
echo ""
|
|
echo " • 1GB EFI partition (for Linux bootloader)"
|
|
echo " • Rest: LUKS2 encrypted partition (for Linux root)"
|
|
echo ""
|
|
echo -e "${GREEN}Windows partitions will NOT be touched.${NC}"
|
|
echo ""
|
|
read -p "Type 'yes' to continue: " confirm
|
|
[[ "$confirm" != "yes" ]] && error "Cancelled"
|
|
|
|
#===============================================================================
|
|
# CREATE PARTITIONS
|
|
#===============================================================================
|
|
|
|
header "CREATING PARTITIONS"
|
|
|
|
trap cleanup_and_exit ERR
|
|
|
|
DRIVE_NAME=$(basename "$DRIVE")
|
|
LAST_PART=$(lsblk -n -o NAME "$DRIVE" | grep -v "^${DRIVE_NAME}$" | grep -oE '[0-9]+$' | sort -n | tail -1)
|
|
LAST_PART=${LAST_PART:-0}
|
|
|
|
# Create EFI partition
|
|
log "Creating Linux EFI partition (1GB)..."
|
|
NEXT=$((LAST_PART + 1))
|
|
sgdisk -n "${NEXT}:0:+1G" -t "${NEXT}:ef00" "$DRIVE"
|
|
EFI_PART="${PART_PREFIX}${NEXT}"
|
|
partprobe "$DRIVE"; udevadm settle; sleep 2
|
|
mkfs.fat -F32 -n "LINUXEFI" "$EFI_PART"
|
|
log "Created: $EFI_PART"
|
|
|
|
# Create root partition
|
|
log "Creating Linux root partition..."
|
|
NEXT=$((NEXT + 1))
|
|
sgdisk -n "${NEXT}:0:0" -t "${NEXT}:8300" "$DRIVE"
|
|
ROOT_PART="${PART_PREFIX}${NEXT}"
|
|
partprobe "$DRIVE"; udevadm settle; sleep 2
|
|
log "Created: $ROOT_PART"
|
|
|
|
#===============================================================================
|
|
# SETUP ENCRYPTION & FILESYSTEM
|
|
#===============================================================================
|
|
|
|
header "SETTING UP ENCRYPTION"
|
|
|
|
log "Formatting LUKS2..."
|
|
echo -n "$LUKS_PASS" | cryptsetup luksFormat --type luks2 --key-file=- "$ROOT_PART"
|
|
|
|
log "Opening encrypted volume..."
|
|
echo -n "$LUKS_PASS" | cryptsetup open --key-file=- "$ROOT_PART" cryptroot
|
|
LUKS_UUID=$(blkid -s UUID -o value "$ROOT_PART")
|
|
|
|
log "Creating btrfs filesystem..."
|
|
mkfs.btrfs -f /dev/mapper/cryptroot
|
|
|
|
log "Creating subvolumes..."
|
|
mount /dev/mapper/cryptroot /mnt
|
|
btrfs subvolume create /mnt/@
|
|
btrfs subvolume create /mnt/@home
|
|
btrfs subvolume create /mnt/@log
|
|
btrfs subvolume create /mnt/@pkg
|
|
# NOTE: Do NOT create @snapshots - snapper will create .snapshots as nested subvolume
|
|
umount /mnt
|
|
|
|
log "Mounting filesystems..."
|
|
mount -o compress=zstd,subvol=@ /dev/mapper/cryptroot /mnt
|
|
mkdir -p /mnt/{home,var/log,var/cache/pacman/pkg,boot}
|
|
mount -o compress=zstd,subvol=@home /dev/mapper/cryptroot /mnt/home
|
|
mount -o compress=zstd,subvol=@log /dev/mapper/cryptroot /mnt/var/log
|
|
mount -o compress=zstd,subvol=@pkg /dev/mapper/cryptroot /mnt/var/cache/pacman/pkg
|
|
# NOTE: Do NOT mount .snapshots - snapper creates it as nested subvolume in @
|
|
mount "$EFI_PART" /mnt/boot
|
|
|
|
log "Partitions ready!"
|
|
|
|
# Save info for later
|
|
echo "$LUKS_UUID" > /tmp/luks_uuid
|
|
echo "$EFI_PART" > /tmp/efi_part
|
|
echo "$ROOT_PART" > /tmp/root_part
|
|
echo "$DRIVE" > /tmp/install_drive
|
|
|
|
trap - ERR
|
|
|
|
#===============================================================================
|
|
# HAND OFF TO ARCHINSTALL
|
|
#===============================================================================
|
|
|
|
header "STARTING OMARCHY INSTALLER"
|
|
|
|
echo "Partitions are ready. Now the Omarchy installer will collect your"
|
|
echo "user information and install the system."
|
|
echo ""
|
|
info "The disk is already partitioned - just enter your user details."
|
|
echo ""
|
|
read -p "Press Enter to continue to Omarchy setup..." _
|
|
|
|
# Run the configurator to get user info.
|
|
# The configurator is a separate child process that re-sources its helpers via
|
|
# $OMARCHY_INSTALL and calls helper functions like clear_logo. Without these
|
|
# exported it prints "/helpers/all.sh: No such file or directory" and repeated
|
|
# "clear_logo: command not found" noise. Export them using Omarchy's own
|
|
# convention (OMARCHY_INSTALL=$OMARCHY_PATH/install) so the configurator runs
|
|
# clean.
|
|
cd /root
|
|
export OMARCHY_PATH=/root/omarchy
|
|
export OMARCHY_INSTALL=/root/omarchy/install
|
|
source "$OMARCHY_INSTALL/helpers/all.sh" 2>/dev/null || true
|
|
|
|
# Set colors
|
|
if [[ $(tty) == "/dev/tty"* ]]; then
|
|
echo -en "\e]P01a1b26"
|
|
echo -en "\e]P7a9b1d6"
|
|
echo -en "\e]PFc0caf5"
|
|
echo -en "\033[0m"
|
|
clear
|
|
fi
|
|
|
|
# Drain any buffered keystrokes / stray terminal bytes left over from the
|
|
# "Press Enter" prompt and the logo animation, so they don't leak into the
|
|
# configurator's first input field (was showing up as a phantom "A" in the
|
|
# Username prompt before the user typed anything).
|
|
read -r -t 0.2 -N 100000 _drain 2>/dev/null || true
|
|
|
|
# Run configurator
|
|
./configurator
|
|
|
|
# Get username from generated config
|
|
OMARCHY_USER=$(jq -r '.users[0].username' user_credentials.json 2>/dev/null)
|
|
|
|
if [[ -z "$OMARCHY_USER" || "$OMARCHY_USER" == "null" ]]; then
|
|
error "Failed to get username from configurator"
|
|
fi
|
|
|
|
log "User: $OMARCHY_USER"
|
|
|
|
#===============================================================================
|
|
# MODIFY CONFIG FOR PRE-MOUNTED PARTITIONS
|
|
#===============================================================================
|
|
|
|
header "CONFIGURING INSTALLATION"
|
|
|
|
log "Updating configuration for dual-boot..."
|
|
|
|
# Only modify disk_config and remove bootloader, preserve everything else from generated config
|
|
# This keeps the correct kernel, locale, packages, profile, etc.
|
|
# We MUST remove bootloader because archinstall crashes when trying to
|
|
# detect root device with pre_mounted_config - it fails BEFORE installing packages
|
|
# We install limine manually after archinstall completes
|
|
jq '.disk_config = {"config_type": "pre_mounted_config", "mountpoint": "/mnt"} | del(.bootloader)' \
|
|
user_configuration.json > user_configuration.json.tmp
|
|
mv user_configuration.json.tmp user_configuration.json
|
|
|
|
# Extract kernel name for later use in bootloader config
|
|
KERNEL_NAME=$(jq -r '.kernels[0] // "linux"' user_configuration.json)
|
|
echo "$KERNEL_NAME" > /tmp/kernel_name
|
|
log "Using kernel: $KERNEL_NAME"
|
|
|
|
log "Configuration updated for pre-mounted partitions"
|
|
|
|
#===============================================================================
|
|
# RUN ARCHINSTALL
|
|
#===============================================================================
|
|
|
|
header "INSTALLING SYSTEM"
|
|
|
|
log "Running archinstall..."
|
|
info "This will take several minutes..."
|
|
|
|
# Initialize keyring
|
|
pacman-key --init
|
|
pacman-key --populate archlinux
|
|
pacman-key --populate omarchy 2>/dev/null || true
|
|
|
|
# For offline install, ensure we use the offline-only pacman config
|
|
# and skip database sync since the database is already on the ISO
|
|
cat > /tmp/pacman-offline.conf << 'PACCONF'
|
|
[options]
|
|
HoldPkg = pacman glibc
|
|
Architecture = auto
|
|
SigLevel = Required DatabaseOptional
|
|
LocalFileSigLevel = Optional
|
|
|
|
[offline]
|
|
SigLevel = Optional TrustAll
|
|
Server = file:///var/cache/omarchy/mirror/offline/
|
|
PACCONF
|
|
|
|
# Overwrite system pacman.conf with offline-only version
|
|
cp /tmp/pacman-offline.conf /etc/pacman.conf
|
|
|
|
# Debug: verify config
|
|
log "Pacman config set to offline-only:"
|
|
grep -E "^\[|^Server" /etc/pacman.conf
|
|
|
|
# Skip pacman -Sy for offline install - database already exists on ISO
|
|
# Check for either offline.db or offline.db.tar.gz
|
|
if [[ -f /var/cache/omarchy/mirror/offline/offline.db ]] || [[ -f /var/cache/omarchy/mirror/offline/offline.db.tar.gz ]]; then
|
|
log "Offline package database found - skipping sync"
|
|
else
|
|
warn "Offline database not found - attempting sync..."
|
|
pacman --config /tmp/pacman-offline.conf -Sy --noconfirm
|
|
fi
|
|
|
|
# Run archinstall with our config (same flags as original Omarchy installer)
|
|
# bootloader=none prevents archinstall from crashing on pre_mounted_config
|
|
archinstall \
|
|
--config user_configuration.json \
|
|
--creds user_credentials.json \
|
|
--silent \
|
|
--skip-ntp \
|
|
--skip-wkd \
|
|
--skip-wifi-check
|
|
|
|
# Verify archinstall created the user
|
|
if [[ ! -d /mnt/home/$OMARCHY_USER ]]; then
|
|
error "archinstall failed - user $OMARCHY_USER not created"
|
|
fi
|
|
|
|
log "Base system installed!"
|
|
|
|
# Install limine bootloader package (archinstall didn't because we removed bootloader config)
|
|
log "Installing limine bootloader..."
|
|
# Copy offline pacman config to chroot and mount offline mirror first
|
|
cp /tmp/pacman-offline.conf /mnt/etc/pacman.conf
|
|
mkdir -p /mnt/var/cache/omarchy/mirror/offline
|
|
mount --bind /var/cache/omarchy/mirror/offline /mnt/var/cache/omarchy/mirror/offline
|
|
# Install limine using offline repo (no -Sy needed, packages are local)
|
|
arch-chroot /mnt pacman --noconfirm --needed -S limine
|
|
|
|
#===============================================================================
|
|
# POST-INSTALL CONFIGURATION
|
|
#===============================================================================
|
|
|
|
header "CONFIGURING BOOTLOADER"
|
|
|
|
LUKS_UUID=$(cat /tmp/luks_uuid)
|
|
KERNEL_NAME=$(cat /tmp/kernel_name)
|
|
|
|
# Configure mkinitcpio for encryption using Omarchy's drop-in approach
|
|
log "Configuring initramfs for encryption..."
|
|
|
|
# Install required packages
|
|
log "Installing plymouth and limine tools..."
|
|
arch-chroot /mnt pacman --noconfirm --needed -S plymouth limine-snapper-sync limine-mkinitcpio-hook || warn "Some packages may already be installed"
|
|
|
|
# Create Omarchy-style mkinitcpio drop-in config with encryption support
|
|
# This uses the same hooks as Omarchy but includes encrypt for LUKS
|
|
mkdir -p /mnt/etc/mkinitcpio.conf.d
|
|
cat > /mnt/etc/mkinitcpio.conf.d/omarchy_hooks.conf << 'MKINIT'
|
|
HOOKS=(base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs)
|
|
MKINIT
|
|
|
|
cat > /mnt/etc/mkinitcpio.conf.d/thunderbolt_module.conf << 'MKINIT'
|
|
MODULES+=(thunderbolt)
|
|
MKINIT
|
|
|
|
# Rebuild initramfs (warnings about missing firmware are OK)
|
|
log "Building initramfs (warnings about missing firmware are normal)..."
|
|
if ! arch-chroot /mnt mkinitcpio -P; then
|
|
warn "mkinitcpio reported warnings - checking if initramfs was created..."
|
|
fi
|
|
|
|
# Verify the initramfs was actually created for our kernel
|
|
if [[ -f /mnt/boot/initramfs-${KERNEL_NAME}.img ]]; then
|
|
log "initramfs-${KERNEL_NAME}.img created successfully"
|
|
elif [[ -f /mnt/boot/initramfs-linux.img ]]; then
|
|
log "initramfs-linux.img created successfully"
|
|
else
|
|
error "initramfs was not created! Check mkinitcpio output above."
|
|
fi
|
|
|
|
# Verify kernel was installed
|
|
if [[ ! -f /mnt/boot/vmlinuz-$KERNEL_NAME ]]; then
|
|
error "Kernel vmlinuz-$KERNEL_NAME not found in /mnt/boot/"
|
|
fi
|
|
log "Found kernel: vmlinuz-$KERNEL_NAME"
|
|
|
|
# Configure Limine bootloader using Omarchy's approach
|
|
log "Configuring Limine bootloader..."
|
|
mkdir -p /mnt/boot/EFI/BOOT /mnt/boot/EFI/limine
|
|
|
|
# Copy Limine EFI files
|
|
cp /mnt/usr/share/limine/BOOTX64.EFI /mnt/boot/EFI/BOOT/
|
|
cp /mnt/usr/share/limine/BOOTX64.EFI /mnt/boot/EFI/limine/
|
|
|
|
# Create /etc/default/limine for limine-update (Omarchy style)
|
|
CRYPT_CMDLINE="cryptdevice=UUID=$LUKS_UUID:cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@"
|
|
cat > /mnt/etc/default/limine << LIMINEDEF
|
|
TARGET_OS_NAME="Omarchy"
|
|
|
|
ESP_PATH="/boot"
|
|
|
|
KERNEL_CMDLINE[default]="$CRYPT_CMDLINE rw quiet splash"
|
|
|
|
ENABLE_UKI=yes
|
|
CUSTOM_UKI_NAME="omarchy"
|
|
|
|
ENABLE_LIMINE_FALLBACK=yes
|
|
|
|
# Find and add other bootloaders (Windows)
|
|
FIND_BOOTLOADERS=yes
|
|
|
|
BOOT_ORDER="*, *fallback, Snapshots"
|
|
|
|
MAX_SNAPSHOT_ENTRIES=5
|
|
|
|
SNAPSHOT_FORMAT_CHOICE=5
|
|
LIMINEDEF
|
|
|
|
# Create base limine.conf (limine-update will add entries)
|
|
cat > /mnt/boot/limine.conf << 'LIMINE'
|
|
default_entry: 2
|
|
interface_branding: Omarchy Bootloader
|
|
interface_branding_color: 2
|
|
hash_mismatch_panic: no
|
|
|
|
term_background: 1a1b26
|
|
backdrop: 1a1b26
|
|
|
|
term_palette: 15161e;f7768e;9ece6a;e0af68;7aa2f7;bb9af7;7dcfff;a9b1d6
|
|
term_palette_bright: 414868;f7768e;9ece6a;e0af68;7aa2f7;bb9af7;7dcfff;c0caf5
|
|
|
|
term_foreground: c0caf5
|
|
term_foreground_bright: c0caf5
|
|
term_background_bright: 24283b
|
|
LIMINE
|
|
|
|
# Run limine-update to generate boot entries (including snapshots)
|
|
log "Running limine-update to generate boot entries..."
|
|
arch-chroot /mnt limine-update || warn "limine-update had issues"
|
|
|
|
# Enable btrfs quota for space-aware snapshot cleanup
|
|
log "Enabling btrfs quota..."
|
|
btrfs quota enable /mnt || warn "Could not enable btrfs quota"
|
|
|
|
#===============================================================================
|
|
# INSTALL OMARCHY
|
|
#===============================================================================
|
|
|
|
header "INSTALLING OMARCHY PACKAGES"
|
|
|
|
# Mount offline mirror if available
|
|
if [[ -d /var/cache/omarchy/mirror/offline ]]; then
|
|
mkdir -p /mnt/var/cache/omarchy/mirror/offline
|
|
mount --bind /var/cache/omarchy/mirror/offline /mnt/var/cache/omarchy/mirror/offline
|
|
fi
|
|
|
|
if [[ -d /opt/packages ]]; then
|
|
mkdir -p /mnt/opt/packages
|
|
mount --bind /opt/packages /mnt/opt/packages
|
|
fi
|
|
|
|
# Backup system pacman.conf and use ISO's version temporarily for offline install
|
|
cp /mnt/etc/pacman.conf /mnt/etc/pacman.conf.bak
|
|
cp /etc/pacman.conf /mnt/etc/pacman.conf
|
|
|
|
# Copy omarchy to user's home
|
|
mkdir -p /mnt/home/$OMARCHY_USER/.local/share/
|
|
cp -a /root/omarchy /mnt/home/$OMARCHY_USER/.local/share/
|
|
# Ensure all scripts are executable
|
|
chmod -R +x /mnt/home/$OMARCHY_USER/.local/share/omarchy/bin/
|
|
chmod -R +x /mnt/home/$OMARCHY_USER/.local/share/omarchy/install/
|
|
find /mnt/home/$OMARCHY_USER/.local/share/omarchy -name "*.sh" -exec chmod +x {} \;
|
|
arch-chroot /mnt chown -R $OMARCHY_USER:$OMARCHY_USER /home/$OMARCHY_USER/.local/
|
|
|
|
# Get user info from configurator-generated files
|
|
USER_FULL_NAME=""
|
|
USER_EMAIL=""
|
|
[[ -f /root/user_full_name.txt ]] && USER_FULL_NAME=$(<user_full_name.txt)
|
|
[[ -f /root/user_email_address.txt ]] && USER_EMAIL=$(<user_email_address.txt)
|
|
OMARCHY_MIRROR=""
|
|
[[ -f /root/omarchy_mirror ]] && OMARCHY_MIRROR=$(<omarchy_mirror)
|
|
|
|
# Run omarchy installer in chroot (with proper environment like original installer).
|
|
# Pass OMARCHY_CHROOT_INSTALL=1 and the rest on the sudo command line — `sudo -E`
|
|
# alone isn't enough because sudo's default env_reset strips unknown vars regardless.
|
|
# Without OMARCHY_CHROOT_INSTALL=1 the install/post-install/finished.sh script
|
|
# prompts the user with a gum-confirm "Reboot Now" dialog and then runs
|
|
# `sudo reboot`, which kills the archiso live env before our wrapper can finish
|
|
# (no plymouth/snapper/Windows entry/BootOrder lock would ever run).
|
|
log "Installing Omarchy packages and configuration..."
|
|
arch-chroot /mnt /bin/bash -c "
|
|
export HOME=/home/$OMARCHY_USER
|
|
export USER=$OMARCHY_USER
|
|
cd /home/$OMARCHY_USER/.local/share/omarchy
|
|
sudo -u $OMARCHY_USER \
|
|
OMARCHY_CHROOT_INSTALL=1 \
|
|
OMARCHY_USER_NAME='$USER_FULL_NAME' \
|
|
OMARCHY_USER_EMAIL='$USER_EMAIL' \
|
|
OMARCHY_MIRROR='$OMARCHY_MIRROR' \
|
|
bash -c 'source install.sh' || true
|
|
" || warn "Omarchy install script had some issues (this may be OK)"
|
|
|
|
# Set up proper pacman.conf for the installed system
|
|
log "Configuring pacman for installed system..."
|
|
cat > /mnt/etc/pacman.conf << 'PACMANCONF'
|
|
[options]
|
|
Color
|
|
ILoveCandy
|
|
VerbosePkgLists
|
|
HoldPkg = pacman glibc
|
|
Architecture = auto
|
|
CheckSpace
|
|
ParallelDownloads = 5
|
|
DownloadUser = alpm
|
|
|
|
SigLevel = Required DatabaseOptional
|
|
LocalFileSigLevel = Optional
|
|
|
|
[core]
|
|
Include = /etc/pacman.d/mirrorlist
|
|
|
|
[extra]
|
|
Include = /etc/pacman.d/mirrorlist
|
|
|
|
[multilib]
|
|
Include = /etc/pacman.d/mirrorlist
|
|
|
|
[omarchy]
|
|
SigLevel = Optional TrustAll
|
|
Server = https://pkgs.omarchy.org/stable/$arch
|
|
PACMANCONF
|
|
|
|
# Set up mirrorlist
|
|
cat > /mnt/etc/pacman.d/mirrorlist << 'MIRRORLIST'
|
|
Server = https://stable-mirror.omarchy.org/$repo/os/$arch
|
|
MIRRORLIST
|
|
|
|
log "Pacman configured with Omarchy stable repos"
|
|
|
|
# Now that Omarchy is installed, set Plymouth theme and rebuild initramfs
|
|
log "Setting Omarchy Plymouth theme..."
|
|
if [[ -d /mnt/usr/share/plymouth/themes/omarchy ]]; then
|
|
arch-chroot /mnt plymouth-set-default-theme omarchy
|
|
log "Rebuilding initramfs with Omarchy Plymouth theme..."
|
|
arch-chroot /mnt mkinitcpio -P
|
|
else
|
|
warn "Omarchy Plymouth theme not found"
|
|
fi
|
|
|
|
#===============================================================================
|
|
# FINAL SETUP
|
|
#===============================================================================
|
|
|
|
header "FINAL CONFIGURATION"
|
|
|
|
# Enable services
|
|
log "Enabling services..."
|
|
arch-chroot /mnt systemctl enable sddm 2>/dev/null || true
|
|
arch-chroot /mnt systemctl enable NetworkManager 2>/dev/null || true
|
|
arch-chroot /mnt systemctl enable iwd 2>/dev/null || true
|
|
arch-chroot /mnt systemctl enable bluetooth 2>/dev/null || true
|
|
|
|
# Snapper configuration
|
|
log "Installing and configuring snapper..."
|
|
arch-chroot /mnt pacman --noconfirm --needed -S snapper 2>/dev/null || true
|
|
|
|
# Create snapper config for root - this creates .snapshots as nested subvolume in @
|
|
# --no-dbus is required: the arch-iso environment has no running DBus daemon,
|
|
# so snapper's default IPC path fails with org.freedesktop.DBus.Error.ServiceUnknown.
|
|
log "Creating snapper root config..."
|
|
arch-chroot /mnt snapper --no-dbus -c root create-config / || warn "snapper create-config had issues"
|
|
|
|
# Enable snapper services
|
|
arch-chroot /mnt systemctl enable snapper-cleanup.timer 2>/dev/null || true
|
|
arch-chroot /mnt systemctl enable snapper-timeline.timer 2>/dev/null || true
|
|
arch-chroot /mnt systemctl enable limine-snapper-sync.service 2>/dev/null || true
|
|
|
|
# Create initial snapshot
|
|
log "Creating initial snapshot..."
|
|
arch-chroot /mnt snapper --no-dbus -c root create -d "Fresh install" || warn "Could not create initial snapshot"
|
|
|
|
# Sync snapshots to limine boot menu
|
|
log "Syncing snapshots to limine..."
|
|
arch-chroot /mnt limine-snapper-sync 2>/dev/null || warn "limine-snapper-sync had issues (may need to run after first boot)"
|
|
|
|
log "Snapper configured with bootable snapshots"
|
|
|
|
# Add Windows Boot Manager entry to limine.conf.
|
|
# This MUST happen after every tool that auto-rewrites limine.conf:
|
|
# - limine-update / limine-mkinitcpio-hook (regenerates kernel entries)
|
|
# - limine-snapper-sync (regenerates snapshot entries)
|
|
# Both wipe unrecognised entries. Appending here is the only place the
|
|
# entry survives to first boot. limine-entry-tool --add-efi only supports
|
|
# entries on the same ESP, so we hand-write the entry to chainload the
|
|
# Windows ESP.
|
|
#
|
|
# IMPORTANT: reference the partition by its GPT PARTUUID via guid(), NOT the
|
|
# FAT filesystem serial via uuid(). Limine's guid()/uuid() resolves GPT
|
|
# partition GUIDs; it does NOT match FAT32 volume serials (e.g. uuid(XXXX-XXXX)),
|
|
# which panics at boot with "Failed to open image". lsblk -no PARTUUID gives the
|
|
# GPT GUID we want.
|
|
log "Looking for a Windows ESP on $DRIVE..."
|
|
WIN_ESP_PARTUUID=""
|
|
WIN_ESP_PART=""
|
|
for p in $(lsblk -ln -o NAME "$DRIVE" | tail -n +2); do
|
|
PART="/dev/$p"
|
|
[[ "$PART" == "$EFI_PART" ]] && continue
|
|
[[ "$(lsblk -no FSTYPE "$PART" 2>/dev/null)" != "vfat" ]] && continue
|
|
TMPMNT=$(mktemp -d)
|
|
if mount -o ro "$PART" "$TMPMNT" 2>/dev/null; then
|
|
if [[ -f "$TMPMNT/EFI/Microsoft/Boot/bootmgfw.efi" ]]; then
|
|
WIN_ESP_PARTUUID=$(lsblk -no PARTUUID "$PART")
|
|
WIN_ESP_PART="$PART"
|
|
log "Found Windows ESP: $PART (PARTUUID=$WIN_ESP_PARTUUID)"
|
|
fi
|
|
umount "$TMPMNT"
|
|
fi
|
|
rmdir "$TMPMNT"
|
|
[[ -n "$WIN_ESP_PARTUUID" ]] && break
|
|
done
|
|
|
|
if [[ -n "$WIN_ESP_PARTUUID" ]]; then
|
|
# bootmgfw spoof. Many consumer firmwares (confirmed on AMI-based mini-PCs)
|
|
# ignore the efibootmgr BootOrder lock entirely and always boot
|
|
# \EFI\Microsoft\Boot\bootmgfw.efi, landing straight in Windows and never
|
|
# showing Limine. To guarantee "Limine first, no F12" we replace bootmgfw.efi
|
|
# with Limine and stash the genuine Windows loader as bootmgfwbackup.efi,
|
|
# which the Windows menu entry chainloads.
|
|
#
|
|
# CAVEAT: a Windows *feature* update can reinstall bootmgfw.efi, overwriting
|
|
# Limine and reverting to straight-to-Windows. Re-run option 1 (or just the
|
|
# spoof) to restore it.
|
|
log "Applying bootmgfw spoof (firmware-proof Limine-first boot)..."
|
|
SPOOF_OK=0
|
|
SPOOFMNT=$(mktemp -d)
|
|
if mount "$WIN_ESP_PART" "$SPOOFMNT" 2>/dev/null; then
|
|
WINBOOT="$SPOOFMNT/EFI/Microsoft/Boot"
|
|
LIMINE_EFI="/mnt/boot/EFI/BOOT/BOOTX64.EFI"
|
|
if [[ -f "$WINBOOT/bootmgfw.efi" && -f "$LIMINE_EFI" ]]; then
|
|
# Back up the genuine Windows loader only once — never clobber a real
|
|
# backup with Limine on a re-run.
|
|
if [[ ! -f "$WINBOOT/bootmgfwbackup.efi" ]]; then
|
|
cp "$WINBOOT/bootmgfw.efi" "$WINBOOT/bootmgfwbackup.efi" &&
|
|
log "Backed up Windows loader -> bootmgfwbackup.efi"
|
|
else
|
|
warn "bootmgfwbackup.efi already exists — keeping existing backup"
|
|
fi
|
|
if cp "$LIMINE_EFI" "$WINBOOT/bootmgfw.efi"; then
|
|
sync
|
|
log "Spoof applied: bootmgfw.efi is now Limine"
|
|
SPOOF_OK=1
|
|
else
|
|
warn "Could not copy Limine over bootmgfw.efi — spoof skipped"
|
|
fi
|
|
else
|
|
warn "bootmgfw.efi or Limine EFI missing — spoof skipped"
|
|
fi
|
|
umount "$SPOOFMNT"
|
|
else
|
|
warn "Could not mount Windows ESP for spoof — skipped"
|
|
fi
|
|
rmdir "$SPOOFMNT"
|
|
|
|
# If the spoof succeeded, the Windows menu entry must chainload the
|
|
# backed-up real loader (bootmgfw.efi is now Limine — chainloading it would
|
|
# loop). If it didn't, fall back to the original bootmgfw.efi.
|
|
if [[ "$SPOOF_OK" == "1" ]]; then
|
|
WIN_LOADER="bootmgfwbackup.efi"
|
|
else
|
|
WIN_LOADER="bootmgfw.efi"
|
|
fi
|
|
|
|
# Reference the Windows ESP by GPT PARTUUID via guid(), NOT the FAT serial
|
|
# via uuid() — Limine resolves GPT GUIDs, not FAT32 volume serials.
|
|
log "Adding Windows entry to limine.conf (chainloading $WIN_LOADER)"
|
|
cat >> /mnt/boot/limine.conf <<WINENTRY
|
|
|
|
/Windows Boot Manager
|
|
comment: Chainload the genuine Windows bootloader from the Windows ESP
|
|
protocol: efi_chainload
|
|
image_path: guid($WIN_ESP_PARTUUID):/EFI/Microsoft/Boot/$WIN_LOADER
|
|
WINENTRY
|
|
else
|
|
warn "No Windows ESP detected — Limine menu will not include a Windows entry"
|
|
fi
|
|
|
|
# Configure SDDM auto-login (Omarchy uses hyprland-uwsm session)
|
|
log "Configuring auto-login for $OMARCHY_USER..."
|
|
mkdir -p /mnt/etc/sddm.conf.d
|
|
cat > /mnt/etc/sddm.conf.d/autologin.conf << SDDMCONF
|
|
[Autologin]
|
|
User=$OMARCHY_USER
|
|
Session=hyprland-uwsm
|
|
[Theme]
|
|
Current=breeze
|
|
SDDMCONF
|
|
|
|
# Create UEFI boot entry
|
|
log "Creating UEFI boot entry..."
|
|
DRIVE=$(cat /tmp/install_drive)
|
|
EFI_PART=$(cat /tmp/efi_part)
|
|
EFI_PART_NUM=$(echo "$EFI_PART" | grep -oE '[0-9]+$')
|
|
|
|
# Remove any existing Omarchy or Limine entries to avoid duplicates
|
|
for entry in $(efibootmgr 2>/dev/null | grep -iE "Omarchy|Limine" | sed -n 's/^Boot\([0-9A-Fa-f]*\).*/\1/p'); do
|
|
log "Removing old boot entry: $entry"
|
|
efibootmgr -b "$entry" -B >/dev/null 2>&1 || true
|
|
done
|
|
|
|
# Create single Omarchy entry
|
|
efibootmgr --create --disk "$DRIVE" --part "$EFI_PART_NUM" \
|
|
--loader '\\EFI\\BOOT\\BOOTX64.EFI' --label 'Omarchy' >/dev/null 2>&1 || \
|
|
warn "Could not create UEFI entry - use F12 boot menu"
|
|
|
|
# Force Limine to the top of BootOrder so firmware lands on Limine first on
|
|
# well-behaved firmware. NOTE: this is best-effort only — many consumer
|
|
# firmwares ignore BootOrder and always boot bootmgfw.efi, which is why the
|
|
# bootmgfw spoof above is the real guarantee. The Windows menu entry is added
|
|
# by us explicitly (FIND_BOOTLOADERS does NOT detect Windows — it only scans
|
|
# for systemd-boot/rEFInd/EFI-fallback on the same ESP).
|
|
log "Locking BootOrder so Limine is the default bootloader..."
|
|
NEW_ID=$(efibootmgr 2>/dev/null | grep -i "Omarchy" | head -1 | sed -n 's/^Boot\([0-9A-Fa-f]*\)\*\?.*/\1/p')
|
|
BOOTORDER_LOCKED=0
|
|
if [[ -n "$NEW_ID" ]]; then
|
|
CURRENT=$(efibootmgr 2>/dev/null | sed -n 's/^BootOrder: //p')
|
|
if [[ -n "$CURRENT" ]]; then
|
|
REST=$(echo "$CURRENT" | tr ',' '\n' | grep -v "^${NEW_ID}$" | paste -sd, -)
|
|
if [[ -n "$REST" ]]; then
|
|
if efibootmgr -o "${NEW_ID},${REST}" >/dev/null 2>&1; then
|
|
log "BootOrder set: ${NEW_ID} (Omarchy) first, then ${REST}"
|
|
BOOTORDER_LOCKED=1
|
|
else
|
|
warn "Could not set BootOrder - firmware may still default to Windows"
|
|
fi
|
|
else
|
|
efibootmgr -o "${NEW_ID}" >/dev/null 2>&1 && BOOTORDER_LOCKED=1
|
|
fi
|
|
else
|
|
warn "No existing BootOrder found - firmware may auto-rebuild it"
|
|
fi
|
|
else
|
|
warn "New Omarchy entry not visible to efibootmgr - cannot lock BootOrder"
|
|
fi
|
|
|
|
# Verify the change actually stuck — some buggy firmware (notably Insyde
|
|
# on Acer) silently ignores efibootmgr -o and rebuilds BootOrder pointing
|
|
# at Windows. If verification fails, surface it loudly so the user knows
|
|
# they may need approach #3 (bootmgfw spoof) rather than discovering it
|
|
# on first reboot.
|
|
if [[ "$BOOTORDER_LOCKED" == "1" && -n "$NEW_ID" ]]; then
|
|
sleep 1
|
|
VERIFY=$(efibootmgr 2>/dev/null | sed -n 's/^BootOrder: //p' | cut -d, -f1)
|
|
if [[ "$VERIFY" == "$NEW_ID" ]]; then
|
|
log "BootOrder verified — Limine is first (${VERIFY})"
|
|
else
|
|
warn "BootOrder did NOT stick! Firmware reset it to: ${VERIFY:-<empty>}"
|
|
echo ""
|
|
echo " Your firmware (likely Insyde / Acer / HP) is ignoring efibootmgr."
|
|
echo " After first boot, if the machine goes straight to Windows:"
|
|
echo " 1. Use the firmware boot menu (F12/F8/Esc) once to pick Omarchy"
|
|
echo " 2. Inside Omarchy, consider the 'bootmgfw spoof' workaround"
|
|
echo " (install Limine at \\EFI\\Microsoft\\Boot\\bootmgfw.efi)"
|
|
echo ""
|
|
fi
|
|
fi
|
|
|
|
#===============================================================================
|
|
# CLEANUP
|
|
#===============================================================================
|
|
|
|
header "CLEANING UP"
|
|
|
|
umount /mnt/var/cache/omarchy/mirror/offline 2>/dev/null || true
|
|
umount /mnt/opt/packages 2>/dev/null || true
|
|
|
|
# Recursive unmount of /mnt. If something inside is still busy (lingering
|
|
# chroot pid, snapshot mount, etc), fall back to a lazy unmount so we still
|
|
# reach the success banner — the kernel will release the mount once the
|
|
# holder exits, and the user is about to reboot anyway.
|
|
if ! umount -R /mnt 2>/dev/null; then
|
|
warn "/mnt busy — attempting lazy unmount"
|
|
fuser -km /mnt 2>/dev/null || true
|
|
sleep 1
|
|
umount -R -l /mnt 2>/dev/null || warn "Could not fully unmount /mnt — reboot will clear"
|
|
fi
|
|
cryptsetup close cryptroot 2>/dev/null || warn "cryptsetup close deferred until reboot"
|
|
|
|
rm -f /tmp/luks_uuid /tmp/efi_part /tmp/root_part /tmp/install_drive /tmp/kernel_name
|
|
|
|
#===============================================================================
|
|
# DONE
|
|
#===============================================================================
|
|
|
|
header "INSTALLATION COMPLETE!"
|
|
|
|
echo ""
|
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ Omarchy has been installed alongside Windows! ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo "To boot:"
|
|
echo " 1. Reboot your computer"
|
|
echo " 2. Press F12/F8/DEL at startup for boot menu"
|
|
echo " 3. Select 'Omarchy' for Linux"
|
|
echo " 4. Select 'Windows Boot Manager' for Windows"
|
|
echo ""
|
|
echo " Username: $OMARCHY_USER"
|
|
echo ""
|
|
read -p "Press Enter to reboot..." _
|
|
reboot
|
|
DUALBOOT_SCRIPT
|
|
|
|
chmod +x "$WORK_DIR/extracted/root/dualboot-setup.sh"
|
|
|
|
#===============================================================================
|
|
# MODIFY BOOT SEQUENCE
|
|
#===============================================================================
|
|
|
|
log "Configuring boot sequence..."
|
|
|
|
# Backup original automated script
|
|
if [[ -f "$WORK_DIR/extracted/root/.automated_script.sh" ]]; then
|
|
mv "$WORK_DIR/extracted/root/.automated_script.sh" "$WORK_DIR/extracted/root/.automated_script.sh.orig"
|
|
fi
|
|
|
|
# Replace .zlogin to run our dual-boot setup
|
|
# When a serial port exists (e.g. running under QEMU with -serial), tee the
|
|
# installer's full stdout+stderr to it so the host can capture a forensic log
|
|
# of the install run without scraping the QEMU window.
|
|
cat > "$WORK_DIR/extracted/root/.zlogin" << 'EOF'
|
|
# Omarchy Dual-Boot Installer
|
|
if [[ $(tty) == "/dev/tty1" ]]; then
|
|
if [[ -w /dev/ttyS0 ]]; then
|
|
/root/dualboot-setup.sh 2>&1 | tee /dev/ttyS0
|
|
else
|
|
/root/dualboot-setup.sh
|
|
fi
|
|
fi
|
|
EOF
|
|
|
|
log "Boot sequence configured"
|
|
|
|
#===============================================================================
|
|
# REPACK SQUASHFS
|
|
#===============================================================================
|
|
|
|
log "Repacking squashfs (this takes several minutes)..."
|
|
rm "$SQUASHFS"
|
|
mksquashfs "$WORK_DIR/extracted" "$SQUASHFS" -comp zstd -Xcompression-level 3
|
|
|
|
if [[ ! -f "$SQUASHFS" ]]; then
|
|
error "Failed to create squashfs!"
|
|
fi
|
|
log "New squashfs created: $(du -h "$SQUASHFS" | cut -f1)"
|
|
|
|
#===============================================================================
|
|
# UPDATE AIROOTFS CHECKSUM
|
|
#===============================================================================
|
|
|
|
# Recompute the airootfs.sha512 to match the new squashfs. archiso checks
|
|
# this only when the kernel cmdline includes verify=y (3.6 doesn't), but
|
|
# keeping it in sync is cheap insurance against future versions enabling it.
|
|
log "Updating airootfs.sha512..."
|
|
NEW_HASH=$(sha512sum "$SQUASHFS" | awk '{print $1}')
|
|
echo "$NEW_HASH airootfs.sfs" > "$WORK_DIR/newiso/arch/x86_64/airootfs.sha512"
|
|
|
|
#===============================================================================
|
|
# CREATE ISO (faithful boot-layout replay)
|
|
#===============================================================================
|
|
# Replay the original ISO's boot setup byte-for-byte and overlay only the
|
|
# files we changed (patched squashfs + its checksum). This preserves MBR,
|
|
# GPT, El Torito, the appended EFI partition, archisosearchuuid, and the
|
|
# archiso layout flags (--mbr-force-bootable, -partition_offset 16,
|
|
# -iso_mbr_part_type 0x00, -partition_cyl_align off) that mkarchiso emits.
|
|
# Recreating from scratch via -as mkisofs tends to drop those and breaks
|
|
# boot on stricter UEFI firmware (Acer Insyde, Lenovo, MSI). Works for any
|
|
# Omarchy/archiso version (3.4, 3.5, 3.6, future) without per-version tweaks.
|
|
|
|
log "Building new ISO via boot-image replay..."
|
|
rm -f "$OUTPUT_ISO"
|
|
|
|
xorriso \
|
|
-indev "$SOURCE_ISO" \
|
|
-outdev "$OUTPUT_ISO" \
|
|
-boot_image any replay \
|
|
-map "$SQUASHFS" /arch/x86_64/airootfs.sfs \
|
|
-map "$WORK_DIR/newiso/arch/x86_64/airootfs.sha512" /arch/x86_64/airootfs.sha512 \
|
|
-commit
|
|
|
|
#===============================================================================
|
|
# DONE
|
|
#===============================================================================
|
|
|
|
echo ""
|
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
echo -e "${GREEN} SUCCESS!${NC}"
|
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
echo ""
|
|
echo " Created: $OUTPUT_ISO"
|
|
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
|
|
echo ""
|
|
echo " This ISO provides:"
|
|
echo " • Option 1: Dual-boot install (preserves Windows)"
|
|
echo " • Option 3: Standard install (original Omarchy installer)"
|
|
echo ""
|
|
echo " Next steps:"
|
|
echo " 1. Copy ISO to Ventoy USB drive"
|
|
echo " 2. Boot from Ventoy"
|
|
echo " 3. Select option 1 for dual-boot"
|
|
echo ""
|