install.sh: fix swap creation and partition-range bugs

Review of branch t490-dualboot (laptop-config#1) found three correctness
issues in the new install.sh. This commit fixes them:

1. parted mkpart range used '-NIXOS_SIZE 100%' which on a T490 with a
   recovery partition at the end of the disk would overlap that
   recovery partition and fail. The new code computes the start of the
   freed space as the new end of the (now shrunken) Windows partition
   and uses explicit MiB offsets, so the new partition lands in the
   space ntfsresize actually freed.

2. SWAP_SIZE was advertised in the user-facing plan (lines 100-106 of
   the original) but no swap partition was ever created and swapon was
   never run. The new code creates a linux-swap partition after the
   NixOS root, runs mkswap, and swapon. The total shrink applied to
   the Windows partition is now nixos_bytes + swap_bytes.

3. The 'sort | tail -1' trick for finding the new partition breaks
   when more than one new partition is created (NixOS root + swap
   would both qualify) and is fragile for disks with 10+ existing
   partitions. The new code snapshots the partition list before mkpart
   and uses comm -13 to diff, then picks the lowest-numbered new
   partition for the NixOS root and the next one for swap.

Reported-by: agent-engineering review of sscoble/laptop-config#1
This commit is contained in:
agent-engineering 2026-06-06 05:48:45 -07:00
parent 921fc1402a
commit 060194b734

View file

@ -118,17 +118,31 @@ read -rp "Type 'yes' to proceed: " confirm
# === Shrink Windows ===
# Convert NIXOS_SIZE (e.g. "200G") to bytes for ntfsresize
case "$NIXOS_SIZE" in
*G|*g) nixos_bytes=$(( ${NIXOS_SIZE%[Gg]} * 1024 * 1024 * 1024 )) ;;
*M|*m) nixos_bytes=$(( ${NIXOS_SIZE%[Mm]} * 1024 * 1024 )) ;;
*) nixos_bytes="$NIXOS_SIZE" ;;
esac
log "Shrinking Windows partition by $NIXOS_SIZE ($nixos_bytes bytes) — this may take a few minutes..."
size_to_bytes() {
local s="$1"
case "$s" in
*G|*g) echo $(( ${s%[Gg]} * 1024 * 1024 * 1024 )) ;;
*M|*m) echo $(( ${s%[Mm]} * 1024 * 1024 )) ;;
*K|*k) echo $(( ${s%[Kk]} * 1024 )) ;;
*) echo "$s" ;;
esac
}
nixos_bytes=$(size_to_bytes "$NIXOS_SIZE")
# Same for SWAP_SIZE so we can shrink the Windows partition by the *total* of
# both new partitions (NixOS root + swap). Without this, the swap partition
# would never fit on disk.
if [[ "$SWAP_SIZE" == "0" ]]; then
shrink_bytes=$nixos_bytes
else
swap_bytes=$(size_to_bytes "$SWAP_SIZE")
shrink_bytes=$(( nixos_bytes + swap_bytes ))
fi
log "Shrinking Windows partition by $NIXOS_SIZE (nixos) ${SWAP_SIZE:+ + $SWAP_SIZE (swap)} ($shrink_bytes bytes total) — this may take a few minutes..."
ntfsfix -d "$WIN_PART" 2>/dev/null || true
# ntfsresize syntax: -s = perform the shrink, --size = new size in bytes
# We want the new Windows partition to be (current size - nixos_bytes).
# We want the new Windows partition to be (current size - shrink_bytes).
win_size_bytes=$(lsblk -ndo SIZE -b "$WIN_PART")
new_win_bytes=$(( win_size_bytes - nixos_bytes ))
new_win_bytes=$(( win_size_bytes - shrink_bytes ))
ntfsresize -s --size "$new_win_bytes" "$WIN_PART" 2>&1 | tail -5 \
|| fail "ntfsresize failed; aborting"
@ -137,24 +151,78 @@ ntfsresize -s --size "$new_win_bytes" "$WIN_PART" 2>&1 | tail -5 \
partprobe "$DISK"
sleep 2
# === Find next available partition number ===
existing=$(lsblk -nlo NAME "$DISK" 2>/dev/null | wc -l)
log "Existing partitions on $DISK: $existing"
# Snapshot the partition list BEFORE we create the new ones, so we can diff
# and identify which device nodes are new. Plain `sort | tail -1` is unsafe
# when more than one new partition is created (NixOS root + swap).
existing_parts=$(lsblk -nlo NAME "$DISK" 2>/dev/null | tail -n +2 | sort)
# Create NixOS root partition in the freed space
log "Creating NixOS root partition ($NIXOS_SIZE)..."
parted -s -a opt "$DISK" mkpart primary ext4 -- -"$NIXOS_SIZE" 100% || fail "parted mkpart failed"
# === Create NixOS root + swap in the freed space ===
# Compute the start = end of the (now smaller) Windows partition.
# We deliberately do NOT use `-NIXOS_SIZE 100%` because on a T490 the recovery
# partition sits at the end of the disk and that range would overlap it.
# We work in 1MiB units and let parted align.
win_new_bytes=$(lsblk -ndo SIZE -b "$WIN_PART")
win_end_mib=$(( (win_new_bytes + 1024*1024 - 1) / (1024*1024) ))
# Extract the numeric prefix of NIXOS_SIZE for the parted range calculation.
nix_num=${NIXOS_SIZE%[GgMmKk]*}; nix_unit=${NIXOS_SIZE##*[0-9]}
case "$nix_unit" in
G|g) nix_size_mib=$(( nix_num * 1024 )) ;;
M|m) nix_size_mib=$nix_num ;;
*) fail "NIXOS_SIZE must be suffixed with M or G (got '$NIXOS_SIZE')" ;;
esac
nix_start_mib=$win_end_mib
nix_end_mib=$(( nix_start_mib + nix_size_mib ))
log "Creating NixOS root partition (${NIXOS_SIZE}) at ${nix_start_mib}MiB..${nix_end_mib}MiB..."
parted -s "$DISK" mkpart primary ext4 "${nix_start_mib}MiB" "${nix_end_mib}MiB" \
|| fail "parted mkpart (nixos) failed"
partprobe "$DISK"
sleep 2
sleep 1
# Find the new partition
NIXOS_PART=$(lsblk -nlo NAME "$DISK" 2>/dev/null | sed "s|^|/dev/|" | sort | tail -1)
log "New partition: $NIXOS_PART"
if [[ "$SWAP_SIZE" != "0" ]]; then
swap_num=${SWAP_SIZE%[GgMmKk]*}; swap_unit=${SWAP_SIZE##*[0-9]}
case "$swap_unit" in
G|g) swap_size_mib=$(( swap_num * 1024 )) ;;
M|m) swap_size_mib=$swap_num ;;
*) fail "SWAP_SIZE must be suffixed with M or G (got '$SWAP_SIZE')" ;;
esac
swap_end_mib=$(( nix_end_mib + swap_size_mib ))
log "Creating swap partition (${SWAP_SIZE}) at ${nix_end_mib}MiB..${swap_end_mib}MiB..."
parted -s "$DISK" mkpart primary linux-swap "${nix_end_mib}MiB" "${swap_end_mib}MiB" \
|| fail "parted mkpart (swap) failed"
partprobe "$DISK"
sleep 1
fi
# Identify the new partitions by diffing the partition table. New partitions
# are the ones that didn't exist before we ran mkpart.
new_parts=$(lsblk -nlo NAME "$DISK" 2>/dev/null | tail -n +2 | sort)
NIXOS_PART=$(comm -13 <(echo "$existing_parts") <(echo "$new_parts") \
| grep -v "$(basename "$DISK")" \
| head -1 \
| sed 's|^|/dev/|')
[[ -n "$NIXOS_PART" ]] || fail "could not identify the new NixOS partition"
log "New NixOS partition: $NIXOS_PART"
if [[ "$SWAP_SIZE" != "0" ]]; then
SWAP_PART=$(comm -13 <(echo "$existing_parts") <(echo "$new_parts") \
| grep -v "$NIXOS_PART" \
| grep -v "$(basename "$DISK")" \
| head -1 \
| sed 's|^|/dev/|')
[[ -n "$SWAP_PART" ]] || fail "could not identify the new swap partition"
log "New swap partition: $SWAP_PART"
fi
# === Format and mount ===
log "Formatting $NIXOS_PART as ext4..."
mkfs.ext4 -F -L nixos "$NIXOS_PART"
if [[ "$SWAP_SIZE" != "0" ]]; then
log "Setting up swap on $SWAP_PART..."
mkswap -L swap "$SWAP_PART"
swapon "$SWAP_PART"
fi
log "Mounting..."
mount "$NIXOS_PART" /mnt
mkdir -p /mnt/boot