laptop-config/install.sh
agent-engineering 060194b734 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
2026-06-06 05:48:45 -07:00

259 lines
9.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# install.sh — guided NixOS T490 dual-boot installer.
#
# Run this from a NixOS 26.05+ installer environment AFTER you've booted
# the ISO and dropped into a root shell.
#
# What it does:
# 1. Detects the T490's internal NVMe (/dev/nvme0n1) and existing partitions
# 2. Identifies the Windows EFI System Partition (ESP) and the Windows root
# 3. Shrinks the Windows root partition to make room
# 4. Creates a NixOS root + (optional) swap partition
# 5. Formats and mounts them
# 6. Runs nixos-install with this flake
# 7. Reboots into a dual-boot systemd-boot menu
#
# Prereqs:
# - Booted from the NixOS 26.05+ installer ISO (any of them)
# - Network connectivity (for fetching the flake + building)
# - UEFI boot mode (T490 default)
# - Win11 already installed (we share its ESP, we don't touch its partitions)
#
# Usage:
# curl -L https://os.scoble.me/forgejo/sscoble/laptop-config/raw/branch/main/install.sh | sudo bash
#
# Or, if you've cloned the repo:
# sudo ./install.sh
#
# Override defaults with env vars:
# DISK=/dev/nvme0n1 # internal NVMe (default: auto-detect)
# NIXOS_SIZE=200G # size of the new NixOS root partition (default: 200G)
# SWAP_SIZE=16G # size of swap partition (default: 16G, 0 to skip)
# FLAKE_URL=... # override the flake source
# HOST=laptop # nixosConfiguration name (default: laptop)
set -euo pipefail
# === Defaults ===
DISK="${DISK:-}"
NIXOS_SIZE="${NIXOS_SIZE:-200G}"
SWAP_SIZE="${SWAP_SIZE:-16G}"
FLAKE_URL="${FLAKE_URL:-git+https://os.scoble.me/forgejo/sscoble/laptop-config.git}"
HOST="${HOST:-laptop}"
# === Color output (no-op if not a TTY) ===
if [[ -t 1 ]]; then
RED=$'\033[0;31m'; YEL=$'\033[0;33m'; GRN=$'\033[0;32m'; CYN=$'\033[0;36m'; NC=$'\033[0m'
else
RED=''; YEL=''; GRN=''; CYN=''; NC=''
fi
log() { echo "${CYN}>>>${NC} $*"; }
warn() { echo "${YEL}!!!${NC} $*" >&2; }
fail() { echo "${RED}FAIL${NC} $*" >&2; exit 1; }
ok() { echo "${GRN} OK ${NC} $*"; }
# === Root check ===
[[ $EUID -eq 0 ]] || fail "must run as root (sudo ./install.sh)"
# === Sanity: this is a UEFI machine ===
[[ -d /sys/firmware/efi ]] || fail "system is not booted in UEFI mode — T490 should always be UEFI"
# === Detect disk ===
if [[ -z "$DISK" ]]; then
# T490 internal is /dev/nvme0n1; reject anything that's clearly removable
for candidate in /dev/nvme0n1 /dev/sda; do
if [[ -b "$candidate" ]] && lsblk -ndo TRAN "$candidate" 2>/dev/null | grep -qE "nvme|sata"; then
# Make sure it's the boot disk (has the ESP)
if lsblk -nlo PARTTYPE "$candidate"* 2>/dev/null | grep -qi "EFI System"; then
DISK="$candidate"
break
fi
fi
done
fi
[[ -n "$DISK" ]] || fail "could not auto-detect internal disk; set DISK=/dev/nvme0n1 explicitly"
log "Target disk: $DISK"
# === Identify partitions ===
ESP_PART=""
WIN_PART=""
# Use lsblk to enumerate partitions, then filter by parttype
while IFS= read -r part; do
ptype=$(lsblk -ndo PARTTYPE "$part" 2>/dev/null || true)
if [[ "$ptype" == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" && -z "$ESP_PART" ]]; then
ESP_PART="$part"
elif [[ "$ptype" == "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7" && -z "$WIN_PART" ]]; then
# First big NTFS partition = Windows root
WIN_PART="$part"
WIN_SIZE=$(lsblk -ndo SIZE "$part" 2>/dev/null || true)
fi
done < <(lsblk -nlo NAME "$DISK" 2>/dev/null | tail -n +2 | sed "s|^|/dev/|")
[[ -n "$ESP_PART" ]] || fail "no EFI System Partition found on $DISK"
[[ -n "$WIN_PART" ]] || fail "no Windows NTFS partition found on $DISK"
log "EFI System Partition: $ESP_PART"
log "Windows partition: $WIN_PART ($WIN_SIZE)"
# === Confirm with user ===
cat <<EOF
${YEL}Plan:${NC}
1. Shrink $WIN_PART to make room
2. Create new partition for NixOS root: $NIXOS_SIZE
EOF
if [[ "$SWAP_SIZE" != "0" ]]; then
echo " 3. Create new swap partition: $SWAP_SIZE"
fi
cat <<EOF
4. Mount $ESP_PART at /mnt/boot (shared with Windows)
5. Mount new NixOS root at /mnt
6. nixos-install with flake $FLAKE_URL#$HOST
7. Reboot — systemd-boot menu will show NixOS and Windows
${RED}All data on the disk remains intact. We do not touch the Windows
partition; we only shrink it. The recovery partition (if present) is preserved.${NC}
EOF
read -rp "Type 'yes' to proceed: " confirm
[[ "$confirm" == "yes" ]] || fail "aborted by user"
# === Shrink Windows ===
# Convert NIXOS_SIZE (e.g. "200G") to bytes for ntfsresize
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 - shrink_bytes).
win_size_bytes=$(lsblk -ndo SIZE -b "$WIN_PART")
new_win_bytes=$(( win_size_bytes - shrink_bytes ))
ntfsresize -s --size "$new_win_bytes" "$WIN_PART" 2>&1 | tail -5 \
|| fail "ntfsresize failed; aborting"
# === Recreate partition table entry ===
# ntfsresize modified the partition in place; parted may need a re-read
partprobe "$DISK"
sleep 2
# 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 + 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 1
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
mount "$ESP_PART" /mnt/boot
ok "Mounted: $(lsblk -no MOUNTPOINTS $NIXOS_PART) and $(lsblk -no MOUNTPOINTS $ESP_PART)"
# === Generate hardware config ===
log "Generating hardware-configuration.nix..."
nixos-generate-config --root /mnt
# === nixos-install with the flake ===
log "Installing NixOS from flake: $FLAKE_URL#$HOST"
log "(This fetches and builds the world; first run is slow.)"
nixos-install --flake "${FLAKE_URL}#${HOST}" --root /mnt --no-root-passwd
ok "NixOS installed."
cat <<EOF
${GRN}Next steps:${NC}
1. Set a root password now (or after first boot):
nixos-enter --root /mnt -- /bin/sh
# passwd
# exit
2. Unmount and reboot:
umount -R /mnt
reboot
3. In the T490's UEFI boot menu (F12), pick the Linux entry.
systemd-boot menu will show "NixOS" and "Windows Boot Manager".
If you get stuck, drop into a shell from the NixOS installer ISO
and re-run this script — it's idempotent except for the partition
shrink (which has already been done).
EOF