Fix partition issue
This commit is contained in:
1 parent
7c50d72f40
commit
7d3b01eae9
5 files changed
+424
-64
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user