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
259 lines
9.6 KiB
Bash
Executable file
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
|