Fix partition issue
This commit is contained in:
1 parent
7c50d72f40
commit
7d3b01eae9
5 files changed
+424
-64
No files matched your search
@@ -5,11 +5,10 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
TEST_DISK="${OMARCHY_ISO_TEST_WINDOWS_DISK:-/tmp/omarchy-iso-test-windows.qcow2}"
|
|
||||||
TEST_OVMF_VARS="${OMARCHY_ISO_TEST_WINDOWS_OVMF_VARS:-/tmp/OVMF_VARS.omarchy-iso-test-windows.fd}"
|
|
||||||
DISK_SIZE_GIB=96
|
DISK_SIZE_GIB=96
|
||||||
MIN_FREE_GIB=64
|
MIN_FREE_GIB=64
|
||||||
RECREATE=0
|
RECREATE=0
|
||||||
|
GAP=0
|
||||||
ISO=""
|
ISO=""
|
||||||
|
|
||||||
ACTIVE_NBD=""
|
ACTIVE_NBD=""
|
||||||
@@ -19,7 +18,7 @@ TEMPORARY_DISK=""
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<USAGE
|
cat <<USAGE
|
||||||
Usage: omarchy-iso-test-windows-disk [--recreate] [release/omarchy.iso]
|
Usage: omarchy-iso-test-windows-disk [--recreate] [--gap] [release/omarchy.iso]
|
||||||
|
|
||||||
Creates or validates a synthetic Windows-style test disk with:
|
Creates or validates a synthetic Windows-style test disk with:
|
||||||
- a 512MiB EFI partition containing EFI/Microsoft
|
- a 512MiB EFI partition containing EFI/Microsoft
|
||||||
@@ -29,8 +28,16 @@ Creates or validates a synthetic Windows-style test disk with:
|
|||||||
This exercises Windows partition detection and preservation; it is not a
|
This exercises Windows partition detection and preservation; it is not a
|
||||||
bootable Windows installation.
|
bootable Windows installation.
|
||||||
|
|
||||||
|
--gap builds the same fixture with a hole in the partition numbering: a third
|
||||||
|
partition is created and then deleted, leaving slot 3 free while slot 4 stays
|
||||||
|
in use. parted fills the lowest free slot, so the installer must create its
|
||||||
|
partitions as 3 and 5 and read those numbers back. The contiguous fixture
|
||||||
|
cannot catch that class of bug, because there "lowest free" and "highest + 1"
|
||||||
|
are the same number.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--recreate Replace the existing test fixture
|
--recreate Replace the existing test fixture
|
||||||
|
--gap Build the numbering-hole variant (separate disk image)
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
|
|
||||||
Paths can be overridden with:
|
Paths can be overridden with:
|
||||||
@@ -45,6 +52,10 @@ while (( $# > 0 )); do
|
|||||||
RECREATE=1
|
RECREATE=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--gap)
|
||||||
|
GAP=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
@@ -66,6 +77,18 @@ while (( $# > 0 )); do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Separate images per layout so the two fixtures can coexist and neither has
|
||||||
|
# to be rebuilt to run the other.
|
||||||
|
if (( GAP )); then
|
||||||
|
TEST_DISK="${OMARCHY_ISO_TEST_WINDOWS_DISK:-/tmp/omarchy-iso-test-windows-gap.qcow2}"
|
||||||
|
TEST_OVMF_VARS="${OMARCHY_ISO_TEST_WINDOWS_OVMF_VARS:-/tmp/OVMF_VARS.omarchy-iso-test-windows-gap.fd}"
|
||||||
|
LAYOUT_LABEL="512MiB Windows ESP + data + deleted slot 3 + data + 64GiB+ free"
|
||||||
|
else
|
||||||
|
TEST_DISK="${OMARCHY_ISO_TEST_WINDOWS_DISK:-/tmp/omarchy-iso-test-windows.qcow2}"
|
||||||
|
TEST_OVMF_VARS="${OMARCHY_ISO_TEST_WINDOWS_OVMF_VARS:-/tmp/OVMF_VARS.omarchy-iso-test-windows.fd}"
|
||||||
|
LAYOUT_LABEL="512MiB Windows ESP + existing data + 64GiB+ free"
|
||||||
|
fi
|
||||||
|
|
||||||
cleanup_mount() {
|
cleanup_mount() {
|
||||||
if (( MOUNTED )); then
|
if (( MOUNTED )); then
|
||||||
sudo umount "$MOUNT_DIR" >/dev/null 2>&1 || true
|
sudo umount "$MOUNT_DIR" >/dev/null 2>&1 || true
|
||||||
@@ -149,7 +172,7 @@ largest_free_region() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_test_disk() {
|
validate_test_disk() {
|
||||||
local part_count pttype free_space_b min_free_b
|
local part_count pttype free_space_b min_free_b want_parts numbers
|
||||||
local esp data valid=1
|
local esp data valid=1
|
||||||
|
|
||||||
[[ -f $TEST_DISK ]] || return 1
|
[[ -f $TEST_DISK ]] || return 1
|
||||||
@@ -162,9 +185,17 @@ validate_test_disk() {
|
|||||||
pttype=$(lsblk -dnro PTTYPE "$ACTIVE_NBD" 2>/dev/null || true)
|
pttype=$(lsblk -dnro PTTYPE "$ACTIVE_NBD" 2>/dev/null || true)
|
||||||
free_space_b=$(largest_free_region "$ACTIVE_NBD")
|
free_space_b=$(largest_free_region "$ACTIVE_NBD")
|
||||||
min_free_b=$((MIN_FREE_GIB * 1024 * 1024 * 1024))
|
min_free_b=$((MIN_FREE_GIB * 1024 * 1024 * 1024))
|
||||||
|
want_parts=2
|
||||||
|
(( GAP )) && want_parts=3
|
||||||
|
|
||||||
[[ $pttype == gpt ]] || valid=0
|
[[ $pttype == gpt ]] || valid=0
|
||||||
(( part_count == 2 )) || valid=0
|
(( part_count == want_parts )) || valid=0
|
||||||
|
|
||||||
|
# The whole point of the gap fixture: slot 3 free, slot 4 in use.
|
||||||
|
if (( GAP )); then
|
||||||
|
numbers=$(sudo parted -ms "$ACTIVE_NBD" unit B print 2>/dev/null | tail -n +3 | cut -d: -f1 | sort | tr '\n' ' ')
|
||||||
|
[[ $numbers == "1 2 4 " ]] || valid=0
|
||||||
|
fi
|
||||||
(( free_space_b >= min_free_b )) || valid=0
|
(( free_space_b >= min_free_b )) || valid=0
|
||||||
[[ $(sudo blkid -s TYPE -o value "$esp" 2>/dev/null || true) == vfat ]] || valid=0
|
[[ $(sudo blkid -s TYPE -o value "$esp" 2>/dev/null || true) == vfat ]] || valid=0
|
||||||
[[ $(sudo blkid -s LABEL -o value "$esp" 2>/dev/null || true) == WINDOWS_ESP ]] || valid=0
|
[[ $(sudo blkid -s LABEL -o value "$esp" 2>/dev/null || true) == WINDOWS_ESP ]] || valid=0
|
||||||
@@ -206,6 +237,18 @@ create_test_disk() {
|
|||||||
sudo parted --script "$ACTIVE_NBD" mkpart WINDOWS_ESP fat32 1MiB 513MiB
|
sudo parted --script "$ACTIVE_NBD" mkpart WINDOWS_ESP fat32 1MiB 513MiB
|
||||||
sudo parted --script "$ACTIVE_NBD" set 1 esp on
|
sudo parted --script "$ACTIVE_NBD" set 1 esp on
|
||||||
sudo parted --script "$ACTIVE_NBD" mkpart WINDOWS_DATA ext4 513MiB 20GiB
|
sudo parted --script "$ACTIVE_NBD" mkpart WINDOWS_DATA ext4 513MiB 20GiB
|
||||||
|
|
||||||
|
if (( GAP )); then
|
||||||
|
# Slot 3 is created and deleted, slot 4 stays. parted then fills 3 before
|
||||||
|
# 5, so an installer that assumes "highest + 1" (which would be 5 and 6)
|
||||||
|
# writes to device nodes that do not exist. The deleted region is
|
||||||
|
# deliberately smaller than the trailing free space so the installer still
|
||||||
|
# picks the tail as its largest free region.
|
||||||
|
sudo parted --script "$ACTIVE_NBD" mkpart SCRATCH ext4 20GiB 28GiB
|
||||||
|
sudo parted --script "$ACTIVE_NBD" mkpart WINDOWS_MORE ext4 28GiB 30GiB
|
||||||
|
sudo parted --script "$ACTIVE_NBD" rm 3
|
||||||
|
fi
|
||||||
|
|
||||||
sudo partprobe "$ACTIVE_NBD"
|
sudo partprobe "$ACTIVE_NBD"
|
||||||
sudo udevadm settle
|
sudo udevadm settle
|
||||||
|
|
||||||
@@ -281,7 +324,7 @@ cp /usr/share/edk2/x64/OVMF_VARS.4m.fd "$TEST_OVMF_VARS"
|
|||||||
gum style --border rounded --padding "0 1" --border-foreground 6 \
|
gum style --border rounded --padding "0 1" --border-foreground 6 \
|
||||||
"Synthetic Windows disk ready" \
|
"Synthetic Windows disk ready" \
|
||||||
"Disk: $TEST_DISK" \
|
"Disk: $TEST_DISK" \
|
||||||
"Layout: 512MiB Windows ESP + existing data + 64GiB+ free"
|
"Layout: $LAYOUT_LABEL"
|
||||||
|
|
||||||
if gum confirm "Start installation on this disk?"; then
|
if gum confirm "Start installation on this disk?"; then
|
||||||
ISO="$(select_iso)"
|
ISO="$(select_iso)"
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ if [[ ! -r $SETUP_FORM ]]; then
|
|||||||
fi
|
fi
|
||||||
source "$SETUP_FORM"
|
source "$SETUP_FORM"
|
||||||
|
|
||||||
|
# Partition creation helpers, shared with test/partition-numbering-test.sh so
|
||||||
|
# the numbering logic can be exercised without a real disk.
|
||||||
|
DISK_PARTITIONING="${DISK_PARTITIONING:-/usr/share/omarchy-iso/disk-partitioning.sh}"
|
||||||
|
[[ -r $DISK_PARTITIONING ]] ||
|
||||||
|
DISK_PARTITIONING="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../usr/share/omarchy-iso/disk-partitioning.sh"
|
||||||
|
if [[ ! -r $DISK_PARTITIONING ]]; then
|
||||||
|
echo "ERROR: disk partitioning helpers not found at $DISK_PARTITIONING" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
source "$DISK_PARTITIONING"
|
||||||
|
|
||||||
LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH")
|
LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH")
|
||||||
|
|
||||||
export GUM_CONFIRM_PROMPT_FOREGROUND="6"
|
export GUM_CONFIRM_PROMPT_FOREGROUND="6"
|
||||||
@@ -361,15 +372,7 @@ get_root_disk() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Compute partition device path, handling NVMe/mmcblk's pN naming.
|
# partition_path lives in disk-partitioning.sh alongside the creation helpers.
|
||||||
partition_path() {
|
|
||||||
local _disk="$1" _num="$2"
|
|
||||||
if [[ "$_disk" == *nvme* || "$_disk" == *mmcblk* ]]; then
|
|
||||||
echo "${_disk}p${_num}"
|
|
||||||
else
|
|
||||||
echo "${_disk}${_num}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# BitLocker writes the "-FVE-FS-" signature at offset 3 of the partition.
|
# BitLocker writes the "-FVE-FS-" signature at offset 3 of the partition.
|
||||||
detect_bitlocker() {
|
detect_bitlocker() {
|
||||||
@@ -416,13 +419,20 @@ detect_kernel() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Give udev a moment to create a device node after partitioning.
|
# wait_for_device lives in disk-partitioning.sh: it now reports failure, and
|
||||||
wait_for_device() {
|
# every caller here aborts on it.
|
||||||
local i
|
|
||||||
for i in 1 2 3 4 5; do
|
# disk_step and create_partition fail through this rather than abort() so a
|
||||||
[[ -b "$1" ]] && return 0
|
# half-built layout is undone before the user is handed back their disk.
|
||||||
sleep 1
|
disk_abort_hook() {
|
||||||
done
|
# Release the target before reclaiming its partitions: a mounted filesystem
|
||||||
|
# or an open LUKS mapping would keep parted from removing them.
|
||||||
|
umount -R /mnt 2>/dev/null || true
|
||||||
|
umount /mnt/btrfs-root 2>/dev/null || true
|
||||||
|
rmdir /mnt/btrfs-root 2>/dev/null || true
|
||||||
|
cryptsetup close omarchy_root 2>/dev/null || true
|
||||||
|
rollback_created_parts "$disk"
|
||||||
|
abort "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save the configurator answers the installer consumes: git identity and
|
# Save the configurator answers the installer consumes: git identity and
|
||||||
@@ -654,17 +664,17 @@ run_partition_decide() {
|
|||||||
|
|
||||||
kernel_choice=$(detect_kernel)
|
kernel_choice=$(detect_kernel)
|
||||||
|
|
||||||
local last_part
|
# Partition numbers are not knowable until parted assigns them (it fills the
|
||||||
last_part=$(lsblk -n -o PARTN "$disk" 2>/dev/null | grep -E '^[0-9]+$' | sort -n | tail -1)
|
# lowest free GPT slot, and freeing space by deleting a partition leaves one
|
||||||
efi_part_num=$(( ${last_part:-0} + 1 ))
|
# below the highest). run_partition_execute reads them back after creation;
|
||||||
root_part_num=$(( efi_part_num + 1 ))
|
# a dry run has nothing to read.
|
||||||
|
efi_part_num=""
|
||||||
efi_dev=$(partition_path "$disk" "$efi_part_num")
|
root_part_num=""
|
||||||
root_partition_device=$(partition_path "$disk" "$root_part_num")
|
efi_dev="<assigned at creation>"
|
||||||
|
root_partition_device="<assigned at creation>"
|
||||||
|
root_mapper="<assigned at creation>"
|
||||||
if [[ "$encrypt_installation" == "true" ]]; then
|
if [[ "$encrypt_installation" == "true" ]]; then
|
||||||
root_mapper="/dev/mapper/omarchy_root"
|
root_mapper="/dev/mapper/omarchy_root"
|
||||||
else
|
|
||||||
root_mapper="$root_partition_device"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,64 +682,103 @@ run_partition_decide() {
|
|||||||
# the pre-mounted (protected) install configuration. Runs after the user step
|
# the pre-mounted (protected) install configuration. Runs after the user step
|
||||||
# so LUKS can be formatted with the user's password.
|
# so LUKS can be formatted with the user's password.
|
||||||
run_partition_execute() {
|
run_partition_execute() {
|
||||||
local dry=false
|
local dry=false subvol
|
||||||
[[ "${1:-}" == "dry" ]] && dry=true
|
[[ "${1:-}" == "dry" ]] && dry=true
|
||||||
|
|
||||||
if $dry; then
|
if $dry; then
|
||||||
$needs_mklabel && say "[dry] would initialize GPT on $disk"
|
$needs_mklabel && say "[dry] would initialize GPT on $disk"
|
||||||
say "[dry] would create root partition at $root_partition_device"
|
say "[dry] would create an EFI partition and a root partition in the free region"
|
||||||
say "[dry] would create EFI partition at $efi_dev"
|
say "[dry] partition numbers are assigned by parted at creation, then read back"
|
||||||
say "[dry] encrypted=$encrypt_installation kernel=$kernel_choice"
|
say "[dry] encrypted=$encrypt_installation kernel=$kernel_choice"
|
||||||
else
|
else
|
||||||
step "Creating partitions on $disk"
|
step "Creating partitions on $disk"
|
||||||
if $needs_mklabel; then
|
if $needs_mklabel; then
|
||||||
parted --script "$disk" mklabel gpt
|
disk_step "initializing GPT on $disk" parted --script "$disk" mklabel gpt
|
||||||
partprobe "$disk" 2>/dev/null || true
|
partprobe "$disk" 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
fi
|
fi
|
||||||
parted --script "$disk" mkpart primary fat32 "${EFI_START_B}B" "${EFI_END_B}B"
|
|
||||||
parted --script "$disk" set "$efi_part_num" esp on
|
# create_partition reports the number parted actually used; nothing here
|
||||||
parted --script "$disk" name "$efi_part_num" "OMARCHY_EFI"
|
# may assume it. It sets created_partition_number rather than printing so
|
||||||
parted --script "$disk" mkpart primary btrfs "${ROOT_START_B}B" "${ROOT_END_B}B"
|
# the rollback bookkeeping survives (a $(...) would subshell it away).
|
||||||
parted --script "$disk" name "$root_part_num" "OMARCHY_ROOT"
|
create_partition "$disk" "$EFI_START_B" "$EFI_END_B" fat32 OMARCHY_EFI ||
|
||||||
|
disk_abort_hook "Could not create the EFI partition on $disk"
|
||||||
|
efi_part_num="$created_partition_number"
|
||||||
|
|
||||||
|
create_partition "$disk" "$ROOT_START_B" "$ROOT_END_B" btrfs OMARCHY_ROOT ||
|
||||||
|
disk_abort_hook "Could not create the root partition on $disk"
|
||||||
|
root_part_num="$created_partition_number"
|
||||||
|
|
||||||
|
disk_step "flagging partition $efi_part_num as ESP" \
|
||||||
|
parted --script "$disk" set "$efi_part_num" esp on
|
||||||
|
|
||||||
|
efi_dev=$(partition_path "$disk" "$efi_part_num")
|
||||||
|
root_partition_device=$(partition_path "$disk" "$root_part_num")
|
||||||
|
|
||||||
partprobe "$disk"
|
partprobe "$disk"
|
||||||
sync
|
sync
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
wait_for_device "$efi_dev"
|
wait_for_device "$efi_dev" || disk_abort_hook "EFI partition $efi_dev never appeared"
|
||||||
wait_for_device "$root_partition_device"
|
wait_for_device "$root_partition_device" ||
|
||||||
|
disk_abort_hook "Root partition $root_partition_device never appeared"
|
||||||
|
|
||||||
|
# Both partitions are ours and brand new, but the space they occupy may
|
||||||
|
# carry signatures from whatever was deleted to free it.
|
||||||
|
disk_step "clearing stale signatures on $efi_dev" wipefs -af "$efi_dev"
|
||||||
|
disk_step "clearing stale signatures on $root_partition_device" wipefs -af "$root_partition_device"
|
||||||
|
|
||||||
if [[ "$encrypt_installation" == "true" ]]; then
|
if [[ "$encrypt_installation" == "true" ]]; then
|
||||||
step "Setting up LUKS2 on $root_partition_device"
|
step "Setting up LUKS2 on $root_partition_device"
|
||||||
printf "%s" "$password" | cryptsetup luksFormat --type luks2 --batch-mode "$root_partition_device" -
|
# Kept as pipes rather than routed through disk_step: the passphrase must
|
||||||
printf "%s" "$password" | cryptsetup open "$root_partition_device" omarchy_root -
|
# not become an argv the process table can show. Folding stderr into
|
||||||
|
# stdout still lands any error in the install log.
|
||||||
|
if ! printf "%s" "$password" |
|
||||||
|
cryptsetup luksFormat --type luks2 --batch-mode "$root_partition_device" - 2>&1; then
|
||||||
|
disk_abort_hook "Formatting LUKS2 on $root_partition_device failed"
|
||||||
|
fi
|
||||||
|
if ! printf "%s" "$password" |
|
||||||
|
cryptsetup open "$root_partition_device" omarchy_root - 2>&1; then
|
||||||
|
disk_abort_hook "Opening the encrypted root on $root_partition_device failed"
|
||||||
|
fi
|
||||||
root_mapper="/dev/mapper/omarchy_root"
|
root_mapper="/dev/mapper/omarchy_root"
|
||||||
else
|
else
|
||||||
wipefs -af "$root_partition_device"
|
|
||||||
root_mapper="$root_partition_device"
|
root_mapper="$root_partition_device"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
step "Creating Btrfs filesystem and subvolumes"
|
step "Creating Btrfs filesystem and subvolumes"
|
||||||
mkfs.btrfs -f -L OMARCHY "$root_mapper"
|
disk_step "creating the Btrfs filesystem on $root_mapper" \
|
||||||
wait_for_device "$root_mapper"
|
mkfs.btrfs -f -L OMARCHY "$root_mapper"
|
||||||
|
wait_for_device "$root_mapper" || disk_abort_hook "Root device $root_mapper never appeared"
|
||||||
|
|
||||||
mkdir -p /mnt/btrfs-root
|
mkdir -p /mnt/btrfs-root
|
||||||
mount "$root_mapper" /mnt/btrfs-root
|
disk_step "mounting $root_mapper" mount "$root_mapper" /mnt/btrfs-root
|
||||||
btrfs subvolume create /mnt/btrfs-root/@
|
for subvol in @ @home @log @pkg; do
|
||||||
btrfs subvolume create /mnt/btrfs-root/@home
|
disk_step "creating subvolume $subvol" btrfs subvolume create "/mnt/btrfs-root/$subvol"
|
||||||
btrfs subvolume create /mnt/btrfs-root/@log
|
done
|
||||||
btrfs subvolume create /mnt/btrfs-root/@pkg
|
|
||||||
umount /mnt/btrfs-root
|
umount /mnt/btrfs-root
|
||||||
rmdir /mnt/btrfs-root
|
rmdir /mnt/btrfs-root
|
||||||
|
|
||||||
mount -o noatime,compress=zstd,subvol=@ "$root_mapper" /mnt
|
disk_step "mounting the target root" \
|
||||||
|
mount -o noatime,compress=zstd,subvol=@ "$root_mapper" /mnt
|
||||||
mkdir -p /mnt/home /mnt/var/log /mnt/var/cache/pacman/pkg /mnt"$esp_mount_in_target"
|
mkdir -p /mnt/home /mnt/var/log /mnt/var/cache/pacman/pkg /mnt"$esp_mount_in_target"
|
||||||
mount -o noatime,compress=zstd,subvol=@home "$root_mapper" /mnt/home
|
disk_step "mounting /home" \
|
||||||
mount -o noatime,compress=zstd,subvol=@log "$root_mapper" /mnt/var/log
|
mount -o noatime,compress=zstd,subvol=@home "$root_mapper" /mnt/home
|
||||||
mount -o noatime,compress=zstd,subvol=@pkg "$root_mapper" /mnt/var/cache/pacman/pkg
|
disk_step "mounting /var/log" \
|
||||||
|
mount -o noatime,compress=zstd,subvol=@log "$root_mapper" /mnt/var/log
|
||||||
|
disk_step "mounting the package cache" \
|
||||||
|
mount -o noatime,compress=zstd,subvol=@pkg "$root_mapper" /mnt/var/cache/pacman/pkg
|
||||||
|
|
||||||
mkfs.fat -F32 -n OMARCHY_EFI "$efi_dev"
|
disk_step "creating the ESP filesystem on $efi_dev" mkfs.fat -F32 -n OMARCHY_EFI "$efi_dev"
|
||||||
mount "$efi_dev" /mnt"$esp_mount_in_target"
|
disk_step "mounting the ESP" mount "$efi_dev" /mnt"$esp_mount_in_target"
|
||||||
|
|
||||||
|
# The orchestrator's first act is to verify this handoff. Check it here so
|
||||||
|
# a failure names the step that broke rather than surfacing later as
|
||||||
|
# "protected mode: /mnt is not a mountpoint".
|
||||||
|
mountpoint -q /mnt ||
|
||||||
|
disk_abort_hook "Target /mnt is not mounted; not starting the installer"
|
||||||
|
mountpoint -q "/mnt$esp_mount_in_target" ||
|
||||||
|
disk_abort_hook "ESP is not mounted at /mnt$esp_mount_in_target"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local luks_uuid_json="null" luks_uuid
|
local luks_uuid_json="null" luks_uuid
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# Disk partitioning helpers, shared by the ISO configurator and its tests.
|
||||||
|
#
|
||||||
|
# The rule here is: never predict a partition number. parted fills the lowest
|
||||||
|
# free GPT slot, not "highest existing + 1", so any disk whose numbering has a
|
||||||
|
# hole — exactly what deleting a partition to free space leaves behind, which
|
||||||
|
# is what our own partition tool tells the user to do — hands back a number we
|
||||||
|
# did not choose. Everything below reads back what was actually created.
|
||||||
|
#
|
||||||
|
# Sourced, not executed. The configurator defines abort() and
|
||||||
|
# disk_abort_hook(); tests get the plain fallbacks.
|
||||||
|
|
||||||
|
# Partitions this run created, in creation order. rollback_created_parts()
|
||||||
|
# undoes exactly these and nothing else.
|
||||||
|
created_parts=()
|
||||||
|
|
||||||
|
# Set by create_partition() instead of being printed: a command substitution
|
||||||
|
# would run it in a subshell and lose the created_parts bookkeeping.
|
||||||
|
created_partition_number=""
|
||||||
|
|
||||||
|
# Compute partition device path, handling NVMe/mmcblk's pN naming.
|
||||||
|
partition_path() {
|
||||||
|
local _disk="$1" _num="$2"
|
||||||
|
if [[ "$_disk" == *nvme* || "$_disk" == *mmcblk* ]]; then
|
||||||
|
echo "${_disk}p${_num}"
|
||||||
|
else
|
||||||
|
echo "${_disk}${_num}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Partition numbers as the on-disk GPT reports them. parted rather than lsblk
|
||||||
|
# on purpose: this is the table parted itself is about to modify, so discovery
|
||||||
|
# never depends on the kernel having re-read the partition table yet — and the
|
||||||
|
# same code works against an image file in tests, where lsblk sees nothing.
|
||||||
|
partition_numbers() {
|
||||||
|
parted -ms "$1" unit B print 2>/dev/null | tail -n +3 | cut -d: -f1
|
||||||
|
}
|
||||||
|
|
||||||
|
partition_size_bytes() {
|
||||||
|
parted -ms "$1" unit B print 2>/dev/null |
|
||||||
|
awk -F: -v n="$2" '$1 == n { gsub(/B/, "", $4); print $4; exit }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fail loudly. The former version looped over sleep and returned its status,
|
||||||
|
# so a device that never appeared was indistinguishable from one that did.
|
||||||
|
wait_for_device() {
|
||||||
|
local i
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
[[ -b "$1" ]] && return 0
|
||||||
|
udevadm settle 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_disk_abort() {
|
||||||
|
if declare -F disk_abort_hook >/dev/null; then
|
||||||
|
disk_abort_hook "$1"
|
||||||
|
elif declare -F abort >/dev/null; then
|
||||||
|
abort "$1"
|
||||||
|
fi
|
||||||
|
echo "Error: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run a disk-writing command, fold its stderr into stdout, and abort on a
|
||||||
|
# non-zero exit. The fold matters: .automated_script.sh tees stdout into
|
||||||
|
# /var/log/omarchy-install.log but sends stderr straight to the tty (gum draws
|
||||||
|
# its TUI there), so an unwrapped failure leaves no trace in the log the user
|
||||||
|
# uploads — and the configurator's next screen clears it off the display too.
|
||||||
|
disk_step() {
|
||||||
|
local desc="$1"
|
||||||
|
shift
|
||||||
|
local output status=0
|
||||||
|
output=$("$@" 2>&1) || status=$?
|
||||||
|
[[ -n $output ]] && printf '%s\n' "$output"
|
||||||
|
(( status == 0 )) && return 0
|
||||||
|
_disk_abort "$desc failed (exit $status)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create one partition and report the number parted actually assigned.
|
||||||
|
# Returns non-zero without touching created_parts if anything looks wrong;
|
||||||
|
# the caller decides how loudly to fail.
|
||||||
|
create_partition() {
|
||||||
|
local disk="$1" start="$2" end="$3" fstype="$4" name="$5"
|
||||||
|
local before after num actual want tolerance
|
||||||
|
|
||||||
|
created_partition_number=""
|
||||||
|
|
||||||
|
# Lexicographic sort on both sides: comm needs its inputs ordered the same
|
||||||
|
# way it compares them, and `sort -n` (1, 2, 10) is not that order.
|
||||||
|
before=$(partition_numbers "$disk" | sort)
|
||||||
|
|
||||||
|
parted --script "$disk" mkpart primary "$fstype" "${start}B" "${end}B" || return 1
|
||||||
|
partprobe "$disk" 2>/dev/null || true
|
||||||
|
udevadm settle 2>/dev/null || true
|
||||||
|
|
||||||
|
after=$(partition_numbers "$disk" | sort)
|
||||||
|
num=$(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$after") | head -1)
|
||||||
|
[[ -n $num ]] || return 1
|
||||||
|
|
||||||
|
# The number must be genuinely new. This is the safety property that keeps a
|
||||||
|
# numbering mistake from ever formatting a partition somebody else is using;
|
||||||
|
# it is cheaper and more reliable than sniffing the target for signatures,
|
||||||
|
# which would false-positive on remnants left in freed space.
|
||||||
|
grep -qx "$num" <<<"$before" && return 1
|
||||||
|
|
||||||
|
actual=$(partition_size_bytes "$disk" "$num")
|
||||||
|
[[ -n $actual ]] || return 1
|
||||||
|
want=$((end - start))
|
||||||
|
tolerance=$((1024 * 1024))
|
||||||
|
(( actual >= want - tolerance && actual <= want + tolerance )) || return 1
|
||||||
|
|
||||||
|
parted --script "$disk" name "$num" "$name" || true
|
||||||
|
|
||||||
|
created_parts+=("$num")
|
||||||
|
created_partition_number="$num"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Undo the partitions this run created, highest number first. Scoped strictly
|
||||||
|
# to created_parts: without this, a failed install leaves the user's freed
|
||||||
|
# space occupied by orphans, and the retry reports "not enough free space"
|
||||||
|
# with no way to connect that to what just happened.
|
||||||
|
rollback_created_parts() {
|
||||||
|
local disk="$1" n
|
||||||
|
(( ${#created_parts[@]} > 0 )) || return 0
|
||||||
|
for n in $(printf '%s\n' "${created_parts[@]}" | sort -rn); do
|
||||||
|
parted --script "$disk" rm "$n" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
partprobe "$disk" 2>/dev/null || true
|
||||||
|
created_parts=()
|
||||||
|
}
|
||||||
@@ -765,20 +765,26 @@ def _storage_intent(ctx: InstallContext) -> dict:
|
|||||||
|
|
||||||
def verify_protected_mounts(ctx: InstallContext) -> None:
|
def verify_protected_mounts(ctx: InstallContext) -> None:
|
||||||
target = ctx.target
|
target = ctx.target
|
||||||
if not _is_mountpoint(target):
|
|
||||||
raise RuntimeError(f"protected mode: {target} is not a mountpoint")
|
|
||||||
|
|
||||||
boot = _boot_intent(ctx)
|
boot = _boot_intent(ctx)
|
||||||
storage = _storage_intent(ctx)
|
storage = _storage_intent(ctx)
|
||||||
|
|
||||||
|
# Devices before the mountpoint: when the configurator hands over paths for
|
||||||
|
# partitions that do not exist, "root_device /dev/nvme0n1p12 does not
|
||||||
|
# exist" is diagnosable from the log alone, where "/mnt is not a
|
||||||
|
# mountpoint" sends everyone looking at the mount instead of the paths.
|
||||||
for key in ("esp_device", "root_device"):
|
for key in ("esp_device", "root_device"):
|
||||||
if not storage.get(key):
|
device = storage.get(key)
|
||||||
|
if not device:
|
||||||
raise RuntimeError(f"protected mode: omarchy_install.storage.{key} missing")
|
raise RuntimeError(f"protected mode: omarchy_install.storage.{key} missing")
|
||||||
|
if not Path(device).exists():
|
||||||
|
raise RuntimeError(f"protected mode: {key} {device} does not exist")
|
||||||
|
|
||||||
|
if not _is_mountpoint(target):
|
||||||
|
raise RuntimeError(f"protected mode: {target} is not a mountpoint")
|
||||||
|
|
||||||
esp_mp = target / boot["esp_mount"].lstrip("/")
|
esp_mp = target / boot["esp_mount"].lstrip("/")
|
||||||
if not _is_mountpoint(esp_mp):
|
if not _is_mountpoint(esp_mp):
|
||||||
esp_dev = storage["esp_device"]
|
esp_dev = storage["esp_device"]
|
||||||
if not Path(esp_dev).exists():
|
|
||||||
raise RuntimeError(f"protected mode: ESP device {esp_dev} does not exist")
|
|
||||||
info(f"› remounting protected ESP {esp_dev} at {esp_mp}")
|
info(f"› remounting protected ESP {esp_dev} at {esp_mp}")
|
||||||
esp_mp.mkdir(parents=True, exist_ok=True)
|
esp_mp.mkdir(parents=True, exist_ok=True)
|
||||||
subprocess.run(["mount", esp_dev, str(esp_mp)], check=True)
|
subprocess.run(["mount", esp_dev, str(esp_mp)], check=True)
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# create_partition() must report the number parted actually assigned, never a
|
||||||
|
# predicted one. The regression this pins: the configurator used to compute
|
||||||
|
# "highest existing partition + 1", while parted fills the lowest free GPT
|
||||||
|
# slot. Any disk whose numbering has a hole — what deleting a partition to
|
||||||
|
# free space leaves behind — then had every later command (cryptsetup, mkfs,
|
||||||
|
# mount) pointed at device nodes that did not exist, surfacing much later as
|
||||||
|
# "protected mode: /mnt is not a mountpoint".
|
||||||
|
#
|
||||||
|
# parted operates on image files directly, so this needs no root, no loop
|
||||||
|
# devices, and no real disk.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
LIB="$ROOT/configs/airootfs/usr/share/omarchy-iso/disk-partitioning.sh"
|
||||||
|
|
||||||
|
if ! command -v parted >/dev/null 2>&1; then
|
||||||
|
echo "SKIP: parted is not installed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=../configs/airootfs/usr/share/omarchy-iso/disk-partitioning.sh
|
||||||
|
source "$LIB"
|
||||||
|
|
||||||
|
WORK=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
|
IMG="$WORK/disk.img"
|
||||||
|
MIB=$((1024 * 1024))
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local label="$1" expected="$2" actual="$3"
|
||||||
|
if [[ $expected == "$actual" ]]; then
|
||||||
|
printf ' ok %s\n' "$label"
|
||||||
|
else
|
||||||
|
printf ' FAIL %s: expected %s, got %s\n' "$label" "$expected" "$actual"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# parted reports a partition's size inclusive of both endpoints, so it lands
|
||||||
|
# one sector above (end - start). Same slack create_partition allows.
|
||||||
|
check_size() {
|
||||||
|
local label="$1" want="$2" actual="${3:-0}" delta
|
||||||
|
delta=$((actual - want))
|
||||||
|
(( delta < 0 )) && delta=$((-delta))
|
||||||
|
if (( delta <= MIB )); then
|
||||||
|
printf ' ok %s\n' "$label"
|
||||||
|
else
|
||||||
|
printf ' FAIL %s: expected ~%s, got %s\n' "$label" "$want" "$actual"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# parted's end position is inclusive, so partitions laid back to back overlap
|
||||||
|
# by a sector and are refused. Leave a MiB between them.
|
||||||
|
mkpart_mib() {
|
||||||
|
parted --script "$IMG" mkpart "$1" "$2" "$(($3 * MIB))B" "$(($4 * MIB))B"
|
||||||
|
}
|
||||||
|
|
||||||
|
# A disk with holes: four partitions, then delete the middle two. Slots 2 and
|
||||||
|
# 3 are free while the highest number in use is 4 — the shape of the reported
|
||||||
|
# failure, where the user deleted a partition to make room.
|
||||||
|
build_holey_disk() {
|
||||||
|
rm -f "$IMG"
|
||||||
|
truncate -s 4G "$IMG"
|
||||||
|
parted --script "$IMG" mklabel gpt
|
||||||
|
mkpart_mib one ext4 1 200
|
||||||
|
mkpart_mib two ext4 201 400
|
||||||
|
mkpart_mib three ext4 401 600
|
||||||
|
mkpart_mib four ext4 601 800
|
||||||
|
parted --script "$IMG" rm 2
|
||||||
|
parted --script "$IMG" rm 3
|
||||||
|
created_parts=()
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "==> numbering on a disk with holes"
|
||||||
|
build_holey_disk
|
||||||
|
check "existing numbers" "1 4" "$(partition_numbers "$IMG" | sort | tr '\n' ' ' | sed 's/ $//')"
|
||||||
|
|
||||||
|
create_partition "$IMG" "$((1000 * MIB))" "$((1200 * MIB))" fat32 OMARCHY_EFI
|
||||||
|
check "create_partition succeeded (esp)" "0" "$?"
|
||||||
|
esp_num="$created_partition_number"
|
||||||
|
check "esp took the lowest free slot" "2" "$esp_num"
|
||||||
|
|
||||||
|
# Starts a MiB past the ESP, the way run_partition_decide aligns ROOT_START_B
|
||||||
|
# up from EFI_END_B + 1.
|
||||||
|
create_partition "$IMG" "$((1201 * MIB))" "$((2000 * MIB))" btrfs OMARCHY_ROOT
|
||||||
|
check "create_partition succeeded (root)" "0" "$?"
|
||||||
|
root_num="$created_partition_number"
|
||||||
|
check "root took the next free slot" "3" "$root_num"
|
||||||
|
|
||||||
|
# The old prediction. If either of these ever matches again, the regression is
|
||||||
|
# back: the highest number in use was 4, so the guess would have been 5 and 6.
|
||||||
|
check "esp is not the predicted number" "not-5" "$([[ $esp_num == 5 ]] && echo 5 || echo not-5)"
|
||||||
|
check "root is not the predicted number" "not-6" "$([[ $root_num == 6 ]] && echo 6 || echo not-6)"
|
||||||
|
|
||||||
|
check "both creations tracked for rollback" "2 3" "${created_parts[*]}"
|
||||||
|
check_size "esp size" "$((200 * MIB))" "$(partition_size_bytes "$IMG" "$esp_num")"
|
||||||
|
check_size "root size" "$((799 * MIB))" "$(partition_size_bytes "$IMG" "$root_num")"
|
||||||
|
|
||||||
|
echo "==> rollback reclaims only what this run created"
|
||||||
|
rollback_created_parts "$IMG"
|
||||||
|
check "created partitions removed" "1 4" "$(partition_numbers "$IMG" | sort | tr '\n' ' ' | sed 's/ $//')"
|
||||||
|
check "rollback list cleared" "0" "${#created_parts[@]}"
|
||||||
|
|
||||||
|
echo "==> numbering on a disk without holes"
|
||||||
|
rm -f "$IMG"
|
||||||
|
truncate -s 4G "$IMG"
|
||||||
|
parted --script "$IMG" mklabel gpt
|
||||||
|
mkpart_mib one ext4 1 200
|
||||||
|
mkpart_mib two ext4 201 400
|
||||||
|
created_parts=()
|
||||||
|
|
||||||
|
create_partition "$IMG" "$((1000 * MIB))" "$((1200 * MIB))" fat32 OMARCHY_EFI
|
||||||
|
check "appends when there is no hole" "3" "$created_partition_number"
|
||||||
|
|
||||||
|
echo "==> a refused creation stays out of the rollback list"
|
||||||
|
created_parts=()
|
||||||
|
create_partition "$IMG" "$((100 * MIB))" "$((300 * MIB))" ext4 OVERLAP
|
||||||
|
check "overlapping creation failed" "1" "$?"
|
||||||
|
check "nothing tracked" "0" "${#created_parts[@]}"
|
||||||
|
|
||||||
|
if (( failures > 0 )); then
|
||||||
|
printf '\n%d check(s) failed\n' "$failures"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '\nall checks passed\n'
|
||||||
Reference in new issue
Block a user