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)"
|
||||
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
|
||||
MIN_FREE_GIB=64
|
||||
RECREATE=0
|
||||
GAP=0
|
||||
ISO=""
|
||||
|
||||
ACTIVE_NBD=""
|
||||
@@ -19,7 +18,7 @@ TEMPORARY_DISK=""
|
||||
|
||||
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:
|
||||
- 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
|
||||
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:
|
||||
--recreate Replace the existing test fixture
|
||||
--gap Build the numbering-hole variant (separate disk image)
|
||||
-h, --help Show this help
|
||||
|
||||
Paths can be overridden with:
|
||||
@@ -45,6 +52,10 @@ while (( $# > 0 )); do
|
||||
RECREATE=1
|
||||
shift
|
||||
;;
|
||||
--gap)
|
||||
GAP=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
@@ -66,6 +77,18 @@ while (( $# > 0 )); do
|
||||
esac
|
||||
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() {
|
||||
if (( MOUNTED )); then
|
||||
sudo umount "$MOUNT_DIR" >/dev/null 2>&1 || true
|
||||
@@ -149,7 +172,7 @@ largest_free_region() {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
[[ -f $TEST_DISK ]] || return 1
|
||||
@@ -162,9 +185,17 @@ validate_test_disk() {
|
||||
pttype=$(lsblk -dnro PTTYPE "$ACTIVE_NBD" 2>/dev/null || true)
|
||||
free_space_b=$(largest_free_region "$ACTIVE_NBD")
|
||||
min_free_b=$((MIN_FREE_GIB * 1024 * 1024 * 1024))
|
||||
want_parts=2
|
||||
(( GAP )) && want_parts=3
|
||||
|
||||
[[ $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
|
||||
[[ $(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
|
||||
@@ -206,6 +237,18 @@ create_test_disk() {
|
||||
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" 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 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 \
|
||||
"Synthetic Windows disk ready" \
|
||||
"Disk: $TEST_DISK" \
|
||||
"Layout: 512MiB Windows ESP + existing data + 64GiB+ free"
|
||||
"Layout: $LAYOUT_LABEL"
|
||||
|
||||
if gum confirm "Start installation on this disk?"; then
|
||||
ISO="$(select_iso)"
|
||||
|
||||
@@ -24,6 +24,17 @@ if [[ ! -r $SETUP_FORM ]]; then
|
||||
fi
|
||||
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")
|
||||
|
||||
export GUM_CONFIRM_PROMPT_FOREGROUND="6"
|
||||
@@ -361,15 +372,7 @@ get_root_disk() {
|
||||
fi
|
||||
}
|
||||
|
||||
# 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_path lives in disk-partitioning.sh alongside the creation helpers.
|
||||
|
||||
# BitLocker writes the "-FVE-FS-" signature at offset 3 of the partition.
|
||||
detect_bitlocker() {
|
||||
@@ -416,13 +419,20 @@ detect_kernel() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Give udev a moment to create a device node after partitioning.
|
||||
wait_for_device() {
|
||||
local i
|
||||
for i in 1 2 3 4 5; do
|
||||
[[ -b "$1" ]] && return 0
|
||||
sleep 1
|
||||
done
|
||||
# wait_for_device lives in disk-partitioning.sh: it now reports failure, and
|
||||
# every caller here aborts on it.
|
||||
|
||||
# disk_step and create_partition fail through this rather than abort() so a
|
||||
# half-built layout is undone before the user is handed back their disk.
|
||||
disk_abort_hook() {
|
||||
# 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
|
||||
@@ -654,17 +664,17 @@ run_partition_decide() {
|
||||
|
||||
kernel_choice=$(detect_kernel)
|
||||
|
||||
local last_part
|
||||
last_part=$(lsblk -n -o PARTN "$disk" 2>/dev/null | grep -E '^[0-9]+$' | sort -n | tail -1)
|
||||
efi_part_num=$(( ${last_part:-0} + 1 ))
|
||||
root_part_num=$(( efi_part_num + 1 ))
|
||||
|
||||
efi_dev=$(partition_path "$disk" "$efi_part_num")
|
||||
root_partition_device=$(partition_path "$disk" "$root_part_num")
|
||||
# Partition numbers are not knowable until parted assigns them (it fills the
|
||||
# lowest free GPT slot, and freeing space by deleting a partition leaves one
|
||||
# below the highest). run_partition_execute reads them back after creation;
|
||||
# a dry run has nothing to read.
|
||||
efi_part_num=""
|
||||
root_part_num=""
|
||||
efi_dev="<assigned at creation>"
|
||||
root_partition_device="<assigned at creation>"
|
||||
root_mapper="<assigned at creation>"
|
||||
if [[ "$encrypt_installation" == "true" ]]; then
|
||||
root_mapper="/dev/mapper/omarchy_root"
|
||||
else
|
||||
root_mapper="$root_partition_device"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -672,64 +682,103 @@ run_partition_decide() {
|
||||
# the pre-mounted (protected) install configuration. Runs after the user step
|
||||
# so LUKS can be formatted with the user's password.
|
||||
run_partition_execute() {
|
||||
local dry=false
|
||||
local dry=false subvol
|
||||
[[ "${1:-}" == "dry" ]] && dry=true
|
||||
|
||||
if $dry; then
|
||||
$needs_mklabel && say "[dry] would initialize GPT on $disk"
|
||||
say "[dry] would create root partition at $root_partition_device"
|
||||
say "[dry] would create EFI partition at $efi_dev"
|
||||
say "[dry] would create an EFI partition and a root partition in the free region"
|
||||
say "[dry] partition numbers are assigned by parted at creation, then read back"
|
||||
say "[dry] encrypted=$encrypt_installation kernel=$kernel_choice"
|
||||
else
|
||||
step "Creating partitions on $disk"
|
||||
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
|
||||
sleep 1
|
||||
fi
|
||||
parted --script "$disk" mkpart primary fat32 "${EFI_START_B}B" "${EFI_END_B}B"
|
||||
parted --script "$disk" set "$efi_part_num" esp on
|
||||
parted --script "$disk" name "$efi_part_num" "OMARCHY_EFI"
|
||||
parted --script "$disk" mkpart primary btrfs "${ROOT_START_B}B" "${ROOT_END_B}B"
|
||||
parted --script "$disk" name "$root_part_num" "OMARCHY_ROOT"
|
||||
|
||||
# create_partition reports the number parted actually used; nothing here
|
||||
# may assume it. It sets created_partition_number rather than printing so
|
||||
# the rollback bookkeeping survives (a $(...) would subshell it away).
|
||||
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"
|
||||
sync
|
||||
sleep 2
|
||||
|
||||
wait_for_device "$efi_dev"
|
||||
wait_for_device "$root_partition_device"
|
||||
wait_for_device "$efi_dev" || disk_abort_hook "EFI partition $efi_dev never appeared"
|
||||
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
|
||||
step "Setting up LUKS2 on $root_partition_device"
|
||||
printf "%s" "$password" | cryptsetup luksFormat --type luks2 --batch-mode "$root_partition_device" -
|
||||
printf "%s" "$password" | cryptsetup open "$root_partition_device" omarchy_root -
|
||||
# Kept as pipes rather than routed through disk_step: the passphrase must
|
||||
# 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"
|
||||
else
|
||||
wipefs -af "$root_partition_device"
|
||||
root_mapper="$root_partition_device"
|
||||
fi
|
||||
|
||||
step "Creating Btrfs filesystem and subvolumes"
|
||||
mkfs.btrfs -f -L OMARCHY "$root_mapper"
|
||||
wait_for_device "$root_mapper"
|
||||
disk_step "creating the Btrfs filesystem on $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
|
||||
mount "$root_mapper" /mnt/btrfs-root
|
||||
btrfs subvolume create /mnt/btrfs-root/@
|
||||
btrfs subvolume create /mnt/btrfs-root/@home
|
||||
btrfs subvolume create /mnt/btrfs-root/@log
|
||||
btrfs subvolume create /mnt/btrfs-root/@pkg
|
||||
disk_step "mounting $root_mapper" mount "$root_mapper" /mnt/btrfs-root
|
||||
for subvol in @ @home @log @pkg; do
|
||||
disk_step "creating subvolume $subvol" btrfs subvolume create "/mnt/btrfs-root/$subvol"
|
||||
done
|
||||
umount /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"
|
||||
mount -o noatime,compress=zstd,subvol=@home "$root_mapper" /mnt/home
|
||||
mount -o noatime,compress=zstd,subvol=@log "$root_mapper" /mnt/var/log
|
||||
mount -o noatime,compress=zstd,subvol=@pkg "$root_mapper" /mnt/var/cache/pacman/pkg
|
||||
disk_step "mounting /home" \
|
||||
mount -o noatime,compress=zstd,subvol=@home "$root_mapper" /mnt/home
|
||||
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"
|
||||
mount "$efi_dev" /mnt"$esp_mount_in_target"
|
||||
disk_step "creating the ESP filesystem on $efi_dev" mkfs.fat -F32 -n OMARCHY_EFI "$efi_dev"
|
||||
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
|
||||
|
||||
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:
|
||||
target = ctx.target
|
||||
if not _is_mountpoint(target):
|
||||
raise RuntimeError(f"protected mode: {target} is not a mountpoint")
|
||||
|
||||
boot = _boot_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"):
|
||||
if not storage.get(key):
|
||||
device = storage.get(key)
|
||||
if not device:
|
||||
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("/")
|
||||
if not _is_mountpoint(esp_mp):
|
||||
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}")
|
||||
esp_mp.mkdir(parents=True, exist_ok=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