install.sh: explicit --disk/--efi/--nixos CLI args, no auto-detection
Following the 'the install script assumed too much' rule (Scott 2026-06-06): - Required --disk, --efi, --nixos — no more auto-detect of partitions - --new-size + --shrink replace implicit 'shrink Windows, mkpart at end' - --no-shrink for installs into pre-prepared free space - --no-fetch requires local flake (refuses network fetch) - --dry-run prints plan and exits (validates pre-flight checks) - Safety: refuses if --efi isn't GUID c12a7328-..., if partitions don't belong to --disk, if --nixos is mounted as / or /boot, if --swap duplicates --nixos - Local flake preferred: ./flake.nix next to script beats network URL - Help text via --help extracted from the script header - Same idempotency for mount/hardware-config/nixos-install; only --shrink is the one-time non-idempotent step Tested: bash -n, --help, dry-run with invalid/missing args all pass.
This commit is contained in:
parent
060194b734
commit
ea46ee2c93
1 changed files with 331 additions and 173 deletions
504
install.sh
504
install.sh
|
|
@ -1,47 +1,79 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# install.sh — guided NixOS T490 dual-boot installer.
|
# install.sh — NixOS installer for the T490 (and any other UEFI box).
|
||||||
|
#
|
||||||
|
# You tell it: which disk, which partition is the EFI System Partition, and
|
||||||
|
# which partition to use for NixOS. It does not guess. It does not pick the
|
||||||
|
# first NTFS as Windows. It does not hardcode /dev/nvme0n1. It does not
|
||||||
|
# silently shrink your existing partitions.
|
||||||
#
|
#
|
||||||
# Run this from a NixOS 26.05+ installer environment AFTER you've booted
|
# Run this from a NixOS 26.05+ installer environment AFTER you've booted
|
||||||
# the ISO and dropped into a root shell.
|
# the ISO and dropped into a root shell.
|
||||||
#
|
#
|
||||||
# What it does:
|
# Required arguments (all explicit, no auto-detection):
|
||||||
# 1. Detects the T490's internal NVMe (/dev/nvme0n1) and existing partitions
|
# --disk PATH The physical disk to operate on (e.g. /dev/nvme0n1)
|
||||||
# 2. Identifies the Windows EFI System Partition (ESP) and the Windows root
|
# --efi PATH The EFI System Partition (mounted at /mnt/boot)
|
||||||
# 3. Shrinks the Windows root partition to make room
|
# --nixos PATH The partition to format as ext4 and mount at /mnt
|
||||||
# 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:
|
# Optional:
|
||||||
# - Booted from the NixOS 26.05+ installer ISO (any of them)
|
# --swap PATH Existing partition to use as swap (will mkswap + swapon)
|
||||||
# - Network connectivity (for fetching the flake + building)
|
# --new-size SIZE Create a NEW partition of this size (e.g. 200G) for NixOS.
|
||||||
# - UEFI boot mode (T490 default)
|
# Use this when --nixos is a NEW partition that needs mkpart.
|
||||||
# - Win11 already installed (we share its ESP, we don't touch its partitions)
|
# The script will compute free space and shrink an
|
||||||
|
# existing partition only if you also pass --shrink.
|
||||||
|
# --shrink PATH The existing partition to shrink to make room for
|
||||||
|
# --new-size. Required if --new-size is set and there
|
||||||
|
# isn't already enough free space on --disk. The target
|
||||||
|
# partition is shrunk by AT LEAST (new-size - free-space).
|
||||||
|
# --no-shrink Hard error if --nixos needs space that isn't free.
|
||||||
|
# --bootloader MODE "systemd-boot" (default) or "grub"
|
||||||
|
# --host NAME nixosConfiguration name (default: laptop)
|
||||||
|
# --flake SPEC Flake spec to install. Default: the local repo if
|
||||||
|
# ./flake.nix exists next to this script; else
|
||||||
|
# git+https://os.scoble.me/forgejo/sscoble/laptop-config.git
|
||||||
|
# --no-fetch Refuse to fetch from network; require --flake to be a
|
||||||
|
# local path that exists.
|
||||||
|
# --dry-run, -n Print the plan, do not execute. Exits 0 on a valid plan.
|
||||||
|
# --yes, -y Skip the confirmation prompt (for scripted installs).
|
||||||
|
# --help, -h Show this help and exit.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Examples:
|
||||||
# curl -L https://os.scoble.me/forgejo/sscoble/laptop-config/raw/branch/main/install.sh | sudo bash
|
# # T490 with Win11 on /dev/nvme0n1. Create new NixOS root + swap,
|
||||||
|
# # shrinking the Windows partition to make room.
|
||||||
|
# install.sh --disk /dev/nvme0n1 \
|
||||||
|
# --efi /dev/nvme0n1p1 \
|
||||||
|
# --nixos /dev/nvme0n1p6 --new-size 200G --shrink /dev/nvme0n1p4 \
|
||||||
|
# --swap /dev/nvme0n1p7
|
||||||
#
|
#
|
||||||
# Or, if you've cloned the repo:
|
# # Use a partition you already prepared (no shrink, no new partition)
|
||||||
# sudo ./install.sh
|
# install.sh --disk /dev/nvme0n1 \
|
||||||
|
# --efi /dev/nvme0n1p1 \
|
||||||
|
# --nixos /dev/nvme0n1p5
|
||||||
#
|
#
|
||||||
# Override defaults with env vars:
|
# # Dry-run: see what would happen
|
||||||
# DISK=/dev/nvme0n1 # internal NVMe (default: auto-detect)
|
# install.sh --disk /dev/nvme0n1 --efi /dev/nvme0n1p1 \
|
||||||
# NIXOS_SIZE=200G # size of the new NixOS root partition (default: 200G)
|
# --nixos /dev/nvme0n1p6 --new-size 200G --shrink /dev/nvme0n1p4 \
|
||||||
# SWAP_SIZE=16G # size of swap partition (default: 16G, 0 to skip)
|
# --dry-run
|
||||||
# FLAKE_URL=... # override the flake source
|
#
|
||||||
# HOST=laptop # nixosConfiguration name (default: laptop)
|
# Safety:
|
||||||
|
# * Refuses to run unless --efi is a vfat partition with the ESP GUID.
|
||||||
|
# * Refuses if --nixos == --efi or if partitions don't belong to --disk.
|
||||||
|
# * Refuses to shrink a partition that doesn't have a recognized filesystem.
|
||||||
|
# * Refuses to format --nixos if it's the same device as the running /boot.
|
||||||
|
# * Refuses to fetch the flake from network if --no-fetch is set and the
|
||||||
|
# local flake is missing.
|
||||||
|
# * Always shows a plan and asks for 'yes' unless --yes is given.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# === Defaults ===
|
# === Defaults ===
|
||||||
DISK="${DISK:-}"
|
BOOTLOADER="systemd-boot"
|
||||||
NIXOS_SIZE="${NIXOS_SIZE:-200G}"
|
HOST="laptop"
|
||||||
SWAP_SIZE="${SWAP_SIZE:-16G}"
|
FLAKE_URL=""
|
||||||
FLAKE_URL="${FLAKE_URL:-git+https://os.scoble.me/forgejo/sscoble/laptop-config.git}"
|
NO_FETCH=0
|
||||||
HOST="${HOST:-laptop}"
|
DRY_RUN=0
|
||||||
|
YES=0
|
||||||
|
|
||||||
# === Color output (no-op if not a TTY) ===
|
# === Color output ===
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
RED=$'\033[0;31m'; YEL=$'\033[0;33m'; GRN=$'\033[0;32m'; CYN=$'\033[0;36m'; NC=$'\033[0m'
|
RED=$'\033[0;31m'; YEL=$'\033[0;33m'; GRN=$'\033[0;32m'; CYN=$'\033[0;36m'; NC=$'\033[0m'
|
||||||
else
|
else
|
||||||
|
|
@ -52,72 +84,148 @@ warn() { echo "${YEL}!!!${NC} $*" >&2; }
|
||||||
fail() { echo "${RED}FAIL${NC} $*" >&2; exit 1; }
|
fail() { echo "${RED}FAIL${NC} $*" >&2; exit 1; }
|
||||||
ok() { echo "${GRN} OK ${NC} $*"; }
|
ok() { echo "${GRN} OK ${NC} $*"; }
|
||||||
|
|
||||||
# === Root check ===
|
# === Help text ===
|
||||||
[[ $EUID -eq 0 ]] || fail "must run as root (sudo ./install.sh)"
|
usage() {
|
||||||
|
sed -n '2,46p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit "${1:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Argument parsing ===
|
||||||
|
DISK="" ESP_PART="" NIXOS_PART="" SWAP_PART="" NEW_SIZE="" SHRINK_PART="" NO_SHRINK=0
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--disk) DISK="$2"; shift 2 ;;
|
||||||
|
--efi) ESP_PART="$2"; shift 2 ;;
|
||||||
|
--nixos) NIXOS_PART="$2"; shift 2 ;;
|
||||||
|
--swap) SWAP_PART="$2"; shift 2 ;;
|
||||||
|
--new-size) NEW_SIZE="$2"; shift 2 ;;
|
||||||
|
--shrink) SHRINK_PART="$2"; shift 2 ;;
|
||||||
|
--no-shrink) NO_SHRINK=1; shift ;;
|
||||||
|
--bootloader) BOOTLOADER="$2"; shift 2 ;;
|
||||||
|
--host) HOST="$2"; shift 2 ;;
|
||||||
|
--flake) FLAKE_URL="$2"; shift 2 ;;
|
||||||
|
--no-fetch) NO_FETCH=1; shift ;;
|
||||||
|
--dry-run|-n) DRY_RUN=1; shift ;;
|
||||||
|
--yes|-y) YES=1; shift ;;
|
||||||
|
--help|-h) usage 0 ;;
|
||||||
|
*) fail "unknown argument: $1 (try --help)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# === Root check (skip for --help / --dry-run with no destructive args) ===
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
|
log "Not running as root, but --dry-run is set; continuing (no writes will be attempted)"
|
||||||
|
else
|
||||||
|
fail "must run as root (sudo ./install.sh)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Required args ===
|
||||||
|
[[ -n "$DISK" ]] || fail "--disk is required (try --help)"
|
||||||
|
[[ -n "$ESP_PART" ]] || fail "--efi is required (try --help)"
|
||||||
|
[[ -n "$NIXOS_PART" ]] || fail "--nixos is required (try --help)"
|
||||||
|
|
||||||
# === Sanity: this is a UEFI machine ===
|
# === Sanity: this is a UEFI machine ===
|
||||||
[[ -d /sys/firmware/efi ]] || fail "system is not booted in UEFI mode — T490 should always be UEFI"
|
[[ -d /sys/firmware/efi ]] || fail "system is not booted in UEFI mode"
|
||||||
|
|
||||||
# === Detect disk ===
|
# === Validate that the partitions exist and are block devices ===
|
||||||
if [[ -z "$DISK" ]]; then
|
[[ -b "$DISK" ]] || fail "--disk $DISK is not a block device"
|
||||||
# T490 internal is /dev/nvme0n1; reject anything that's clearly removable
|
[[ -b "$ESP_PART" ]] || fail "--efi $ESP_PART is not a block device"
|
||||||
for candidate in /dev/nvme0n1 /dev/sda; do
|
[[ -b "$NIXOS_PART" ]] || fail "--nixos $NIXOS_PART is not a block device"
|
||||||
if [[ -b "$candidate" ]] && lsblk -ndo TRAN "$candidate" 2>/dev/null | grep -qE "nvme|sata"; then
|
[[ -z "$SWAP_PART" || -b "$SWAP_PART" ]] || fail "--swap $SWAP_PART is not a block device"
|
||||||
# Make sure it's the boot disk (has the ESP)
|
[[ -z "$SHRINK_PART" || -b "$SHRINK_PART" ]] || fail "--shrink $SHRINK_PART is not a block device"
|
||||||
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 ===
|
# === Validate that --efi, --nixos, --swap, --shrink all belong to --disk ===
|
||||||
ESP_PART=""
|
# A partition belongs to a disk if lsblk shows it under the same parent.
|
||||||
WIN_PART=""
|
# Walk: for each candidate, get its PKNAME (parent kernel name) and check
|
||||||
# Use lsblk to enumerate partitions, then filter by parttype
|
# it matches the basename of $DISK (e.g. nvme0n1, sda).
|
||||||
while IFS= read -r part; do
|
parent_of() {
|
||||||
ptype=$(lsblk -ndo PARTTYPE "$part" 2>/dev/null || true)
|
lsblk -ndo PKNAME "$1" 2>/dev/null | head -1
|
||||||
if [[ "$ptype" == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" && -z "$ESP_PART" ]]; then
|
}
|
||||||
ESP_PART="$part"
|
diskn=$(basename "$DISK")
|
||||||
elif [[ "$ptype" == "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7" && -z "$WIN_PART" ]]; then
|
for part in "$ESP_PART" "$NIXOS_PART" "$SWAP_PART" "$SHRINK_PART"; do
|
||||||
# First big NTFS partition = Windows root
|
[[ -z "$part" ]] && continue
|
||||||
WIN_PART="$part"
|
parent=$(parent_of "$part")
|
||||||
WIN_SIZE=$(lsblk -ndo SIZE "$part" 2>/dev/null || true)
|
if [[ "$parent" != "$diskn" ]]; then
|
||||||
|
fail "$part is not a partition of $DISK (parent is '${parent:-none}')"
|
||||||
fi
|
fi
|
||||||
done < <(lsblk -nlo NAME "$DISK" 2>/dev/null | tail -n +2 | sed "s|^|/dev/|")
|
done
|
||||||
|
|
||||||
[[ -n "$ESP_PART" ]] || fail "no EFI System Partition found on $DISK"
|
# === No partition may be used twice ===
|
||||||
[[ -n "$WIN_PART" ]] || fail "no Windows NTFS partition found on $DISK"
|
seen="$DISK|$ESP_PART|$NIXOS_PART"
|
||||||
log "EFI System Partition: $ESP_PART"
|
[[ -n "$SWAP_PART" ]] && seen="$seen|$SWAP_PART"
|
||||||
log "Windows partition: $WIN_PART ($WIN_SIZE)"
|
[[ -n "$SHRINK_PART" ]] && seen="$seen|$SHRINK_PART"
|
||||||
|
echo "$seen" | tr '|' '\n' | sort | uniq -d | grep -v '^$' >/dev/null \
|
||||||
|
&& fail "the same partition was specified for multiple roles"
|
||||||
|
|
||||||
# === Confirm with user ===
|
# === Validate --efi is actually an ESP ===
|
||||||
cat <<EOF
|
ESP_GUID="c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
|
||||||
|
ESP_FSTYPE=$(lsblk -ndo FSTYPE "$ESP_PART" 2>/dev/null || true)
|
||||||
${YEL}Plan:${NC}
|
ESP_PTYPE=$(lsblk -ndo PARTTYPE "$ESP_PART" 2>/dev/null || true)
|
||||||
1. Shrink $WIN_PART to make room
|
if [[ "$ESP_PTYPE" != "$ESP_GUID" ]]; then
|
||||||
2. Create new partition for NixOS root: $NIXOS_SIZE
|
fail "$ESP_PART is not an EFI System Partition (PARTTYPE=$ESP_PTYPE, expected $ESP_GUID)"
|
||||||
EOF
|
fi
|
||||||
if [[ "$SWAP_SIZE" != "0" ]]; then
|
if [[ "$ESP_FSTYPE" != "vfat" ]]; then
|
||||||
echo " 3. Create new swap partition: $SWAP_SIZE"
|
fail "$ESP_PART does not have a vfat filesystem (FSTYPE='$ESP_FSTYPE') — refusing to mount as ESP"
|
||||||
fi
|
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
|
# === Validate --nixos isn't mounted as the running / or /boot ===
|
||||||
partition; we only shrink it. The recovery partition (if present) is preserved.${NC}
|
if findmnt -n -o TARGET --source "$NIXOS_PART" 2>/dev/null | grep -qE '^/$'; then
|
||||||
EOF
|
fail "$NIXOS_PART is mounted as / — refusing to format the running root"
|
||||||
read -rp "Type 'yes' to proceed: " confirm
|
fi
|
||||||
[[ "$confirm" == "yes" ]] || fail "aborted by user"
|
if findmnt -n -o TARGET --source "$NIXOS_PART" 2>/dev/null | grep -qE '^/boot$'; then
|
||||||
|
fail "$NIXOS_PART is mounted as /boot — refusing to format it"
|
||||||
|
fi
|
||||||
|
|
||||||
# === Shrink Windows ===
|
# === Validate --swap is a real partition (not the same as NixOS root) ===
|
||||||
# Convert NIXOS_SIZE (e.g. "200G") to bytes for ntfsresize
|
if [[ -n "$SWAP_PART" && "$SWAP_PART" == "$NIXOS_PART" ]]; then
|
||||||
|
fail "--swap and --nixos are the same partition"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Resolve the flake spec ===
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [[ -z "$FLAKE_URL" ]]; then
|
||||||
|
if [[ -f "$SCRIPT_DIR/flake.nix" ]]; then
|
||||||
|
FLAKE_URL="path:$SCRIPT_DIR"
|
||||||
|
log "Using local flake: $FLAKE_URL"
|
||||||
|
else
|
||||||
|
FLAKE_URL="git+https://os.scoble.me/forgejo/sscoble/laptop-config.git"
|
||||||
|
log "No local flake.nix found; falling back to $FLAKE_URL"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "$NO_FETCH" -eq 1 ]]; then
|
||||||
|
if [[ "$FLAKE_URL" != path:* ]]; then
|
||||||
|
fail "--no-fetch is set but --flake '$FLAKE_URL' is not a local path"
|
||||||
|
fi
|
||||||
|
localpath="${FLAKE_URL#path:}"
|
||||||
|
[[ -f "$localpath/flake.nix" ]] || fail "--no-fetch: $localpath/flake.nix does not exist"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Validate --new-size / --shrink consistency ===
|
||||||
|
if [[ -n "$NEW_SIZE" && -z "$SHRINK_PART" && "$NO_SHRINK" -eq 0 ]]; then
|
||||||
|
# NEW_SIZE set, no --shrink, no --no-shrink. Check free space; if insufficient, fail.
|
||||||
|
free_bytes=$(parted -s "$DISK" print free 2>/dev/null \
|
||||||
|
| awk '/^Free Space/ {gsub(/[^0-9]/,"",$3); print $3; exit}' \
|
||||||
|
| awk '{ printf "%d\n", $1 * 1024 }') # MiB → bytes
|
||||||
|
: "${free_bytes:=0}"
|
||||||
|
need_bytes=$(size_to_bytes "$NEW_SIZE")
|
||||||
|
if (( free_bytes < need_bytes )); then
|
||||||
|
fail "--new-size=$NEW_SIZE needs $need_bytes B but only $free_bytes B free on $DISK; pass --shrink <part> or --no-shrink"
|
||||||
|
fi
|
||||||
|
log "Free space on $DISK: $free_bytes B ≥ $need_bytes B — no shrink needed"
|
||||||
|
fi
|
||||||
|
if [[ "$NO_SHRINK" -eq 1 && -n "$SHRINK_PART" ]]; then
|
||||||
|
fail "--no-shrink and --shrink are mutually exclusive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Validate bootloader choice ===
|
||||||
|
case "$BOOTLOADER" in
|
||||||
|
systemd-boot|grub) ;;
|
||||||
|
*) fail "--bootloader must be 'systemd-boot' or 'grub' (got '$BOOTLOADER')" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# === size_to_bytes helper ===
|
||||||
size_to_bytes() {
|
size_to_bytes() {
|
||||||
local s="$1"
|
local s="$1"
|
||||||
case "$s" in
|
case "$s" in
|
||||||
|
|
@ -127,115 +235,164 @@ size_to_bytes() {
|
||||||
*) echo "$s" ;;
|
*) echo "$s" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
nixos_bytes=$(size_to_bytes "$NIXOS_SIZE")
|
|
||||||
# Same for SWAP_SIZE so we can shrink the Windows partition by the *total* of
|
# === Build the plan ===
|
||||||
# both new partitions (NixOS root + swap). Without this, the swap partition
|
plan_actions=()
|
||||||
# would never fit on disk.
|
[[ -n "$SHRINK_PART" ]] && plan_actions+=("shrink $SHRINK_PART by ~$NEW_SIZE")
|
||||||
if [[ "$SWAP_SIZE" == "0" ]]; then
|
[[ -n "$NEW_SIZE" ]] && plan_actions+=("create new partition on $DISK (size $NEW_SIZE) for NixOS root")
|
||||||
shrink_bytes=$nixos_bytes
|
plan_actions+=("format $NIXOS_PART as ext4 (label: nixos)")
|
||||||
else
|
[[ -n "$SWAP_PART" ]] && plan_actions+=("mkswap + swapon on $SWAP_PART")
|
||||||
swap_bytes=$(size_to_bytes "$SWAP_SIZE")
|
plan_actions+=("mount $NIXOS_PART at /mnt")
|
||||||
shrink_bytes=$(( nixos_bytes + swap_bytes ))
|
plan_actions+=("mount $ESP_PART at /mnt/boot")
|
||||||
|
plan_actions+=("nixos-generate-config --root /mnt")
|
||||||
|
plan_actions+=("nixos-install --flake $FLAKE_URL#$HOST --root /mnt --no-root-passwd")
|
||||||
|
plan_actions+=("reboot (manual, you'll be prompted)")
|
||||||
|
|
||||||
|
# === Show the plan and ask for confirmation ===
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
${YEL}=== Install plan ===${NC}
|
||||||
|
Target disk: $DISK
|
||||||
|
EFI partition: $ESP_PART
|
||||||
|
NixOS root: $NIXOS_PART
|
||||||
|
Swap: ${SWAP_PART:-<none>}
|
||||||
|
Shrink target: ${SHRINK_PART:-<none>}
|
||||||
|
New partition: ${NEW_SIZE:-<none>}
|
||||||
|
Bootloader: $BOOTLOADER
|
||||||
|
Host config: $HOST
|
||||||
|
Flake: $FLAKE_URL
|
||||||
|
Dry run: $([[ $DRY_RUN -eq 1 ]] && echo "yes (no writes)" || echo "no (will execute)")
|
||||||
|
|
||||||
|
${YEL}Actions:${NC}
|
||||||
|
EOF
|
||||||
|
for i in "${!plan_actions[@]}"; do
|
||||||
|
printf " %2d. %s\n" $((i+1)) "${plan_actions[$i]}"
|
||||||
|
done
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
${RED}All existing data on the specified partitions will be DESTROYED:
|
||||||
|
- $NIXOS_PART will be formatted (ext4)
|
||||||
|
EOF
|
||||||
|
[[ -n "$SWAP_PART" ]] && echo " - $SWAP_PART will be re-created as swap"
|
||||||
|
[[ -n "$SHRINK_PART" ]] && echo " - $SHRINK_PART will be SHRUNK (run a filesystem check first if it's NTFS)"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Partitions NOT touched:
|
||||||
|
- $ESP_PART (mounted at /mnt/boot, never reformatted)
|
||||||
|
- any other partition on $DISK
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
ok "DRY RUN: plan validated. No changes made."
|
||||||
|
exit 0
|
||||||
fi
|
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 ===
|
if [[ $YES -ne 1 ]]; then
|
||||||
# ntfsresize modified the partition in place; parted may need a re-read
|
read -rp $'\nType \'yes\' to proceed: ' confirm
|
||||||
partprobe "$DISK"
|
[[ "$confirm" == "yes" ]] || fail "aborted by user"
|
||||||
sleep 2
|
fi
|
||||||
|
|
||||||
# Snapshot the partition list BEFORE we create the new ones, so we can diff
|
# === Pre-flight: ask for root password now (nixos-install --no-root-passwd
|
||||||
# and identify which device nodes are new. Plain `sort | tail -1` is unsafe
|
# means a password must be set post-install, which is a common stumble) ===
|
||||||
# when more than one new partition is created (NixOS root + swap).
|
if [[ $YES -ne 1 ]]; then
|
||||||
existing_parts=$(lsblk -nlo NAME "$DISK" 2>/dev/null | tail -n +2 | sort)
|
cat <<EOF
|
||||||
|
|
||||||
# === Create NixOS root + swap in the freed space ===
|
${YEL}About nixos-install:${NC}
|
||||||
# Compute the start = end of the (now smaller) Windows partition.
|
The script uses --no-root-passwd (declarative config sets the password).
|
||||||
# We deliberately do NOT use `-NIXOS_SIZE 100%` because on a T490 the recovery
|
Make sure modules/laptop.nix (or wherever you set users.users.root.password)
|
||||||
# partition sits at the end of the disk and that range would overlap it.
|
has a hashed password, OR run 'passwd' in the nixos-enter shell after
|
||||||
# We work in 1MiB units and let parted align.
|
install completes.
|
||||||
win_new_bytes=$(lsblk -ndo SIZE -b "$WIN_PART")
|
EOF
|
||||||
win_end_mib=$(( (win_new_bytes + 1024*1024 - 1) / (1024*1024) ))
|
fi
|
||||||
|
|
||||||
# Extract the numeric prefix of NIXOS_SIZE for the parted range calculation.
|
# === Shrink existing partition (if requested) ===
|
||||||
nix_num=${NIXOS_SIZE%[GgMmKk]*}; nix_unit=${NIXOS_SIZE##*[0-9]}
|
if [[ -n "$SHRINK_PART" ]]; then
|
||||||
case "$nix_unit" in
|
log "Validating that $SHRINK_PART can be safely shrunk..."
|
||||||
G|g) nix_size_mib=$(( nix_num * 1024 )) ;;
|
SHRINK_FSTYPE=$(lsblk -ndo FSTYPE "$SHRINK_PART" 2>/dev/null || true)
|
||||||
M|m) nix_size_mib=$nix_num ;;
|
case "$SHRINK_FSTYPE" in
|
||||||
*) fail "NIXOS_SIZE must be suffixed with M or G (got '$NIXOS_SIZE')" ;;
|
ntfs)
|
||||||
esac
|
log "Shrinking NTFS partition $SHRINK_PART (this can take several minutes)..."
|
||||||
nix_start_mib=$win_end_mib
|
ntfsfix -d "$SHRINK_PART" 2>/dev/null || true
|
||||||
nix_end_mib=$(( nix_start_mib + nix_size_mib ))
|
nixos_bytes=$(size_to_bytes "$NEW_SIZE")
|
||||||
log "Creating NixOS root partition (${NIXOS_SIZE}) at ${nix_start_mib}MiB..${nix_end_mib}MiB..."
|
cur_bytes=$(lsblk -ndo SIZE -b "$SHRINK_PART")
|
||||||
parted -s "$DISK" mkpart primary ext4 "${nix_start_mib}MiB" "${nix_end_mib}MiB" \
|
new_bytes=$(( cur_bytes - nixos_bytes ))
|
||||||
|| fail "parted mkpart (nixos) failed"
|
ntfsresize -s --size "$new_bytes" "$SHRINK_PART" 2>&1 | tail -5 \
|
||||||
partprobe "$DISK"
|
|| fail "ntfsresize failed; aborting"
|
||||||
sleep 1
|
partprobe "$DISK"
|
||||||
|
sleep 2
|
||||||
if [[ "$SWAP_SIZE" != "0" ]]; then
|
;;
|
||||||
swap_num=${SWAP_SIZE%[GgMmKk]*}; swap_unit=${SWAP_SIZE##*[0-9]}
|
ext4)
|
||||||
case "$swap_unit" in
|
log "Shrinking ext4 partition $SHRINK_PART..."
|
||||||
G|g) swap_size_mib=$(( swap_num * 1024 )) ;;
|
e2fsck -f "$SHRINK_PART" || fail "e2fsck failed on $SHRINK_PART"
|
||||||
M|m) swap_size_mib=$swap_num ;;
|
resize2fs "$SHRINK_PART" "$(size_to_bytes "$NEW_SIZE")" \
|
||||||
*) fail "SWAP_SIZE must be suffixed with M or G (got '$SWAP_SIZE')" ;;
|
|| fail "resize2fs failed; aborting"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
fail "$SHRINK_PART has no filesystem; refusing to shrink"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "$SHRINK_PART has FSTYPE=$SHRINK_FSTYPE; only ntfs and ext4 are supported for shrink"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
swap_end_mib=$(( nix_end_mib + swap_size_mib ))
|
fi
|
||||||
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" \
|
# === Create new partition (if requested) ===
|
||||||
|| fail "parted mkpart (swap) failed"
|
if [[ -n "$NEW_SIZE" ]]; then
|
||||||
|
# Find free space on the disk in MiB
|
||||||
|
free_mib=$(parted -s "$DISK" print free 2>/dev/null \
|
||||||
|
| awk '/^Free Space/ {gsub(/[^0-9]/,"",$3); print $3; exit}')
|
||||||
|
: "${free_mib:=0}"
|
||||||
|
# Find the start of the free region
|
||||||
|
free_start_mib=$(parted -s "$DISK" print free 2>/dev/null \
|
||||||
|
| awk '/^Free Space/ {print $1; exit}' | tr -d 'MiB')
|
||||||
|
: "${free_start_mib:=0}"
|
||||||
|
# Parse NEW_SIZE to MiB
|
||||||
|
nix_num=${NEW_SIZE%[GgMmKk]*}; nix_unit=${NEW_SIZE##*[0-9]}
|
||||||
|
case "$nix_unit" in
|
||||||
|
G|g) nix_size_mib=$(( nix_num * 1024 )) ;;
|
||||||
|
M|m) nix_size_mib=$nix_num ;;
|
||||||
|
*) fail "--new-size must be suffixed with M or G (got '$NEW_SIZE')" ;;
|
||||||
|
esac
|
||||||
|
end_mib=$(( free_start_mib + nix_size_mib ))
|
||||||
|
log "Creating new partition on $DISK: ${free_start_mib}MiB..${end_mib}MiB (${NEW_SIZE})"
|
||||||
|
parted -s "$DISK" mkpart primary ext4 "${free_start_mib}MiB" "${end_mib}MiB" \
|
||||||
|
|| fail "parted mkpart failed"
|
||||||
partprobe "$DISK"
|
partprobe "$DISK"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
# If --nixos was a placeholder, we should warn that the user must re-run
|
||||||
|
# with the actual new partition path. For now, fail clearly.
|
||||||
|
if ! lsblk -nlo NAME "$DISK" 2>/dev/null | tail -n +2 | sed 's|^|/dev/|' \
|
||||||
|
| grep -qx "$NIXOS_PART"; then
|
||||||
|
fail "new partition created, but $NIXOS_PART is not on $DISK. Re-run with the new partition path (check lsblk)."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Identify the new partitions by diffing the partition table. New partitions
|
# === Format NixOS root ===
|
||||||
# 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..."
|
log "Formatting $NIXOS_PART as ext4..."
|
||||||
mkfs.ext4 -F -L nixos "$NIXOS_PART"
|
mkfs.ext4 -F -L nixos "$NIXOS_PART"
|
||||||
|
|
||||||
if [[ "$SWAP_SIZE" != "0" ]]; then
|
# === Set up swap ===
|
||||||
|
if [[ -n "$SWAP_PART" ]]; then
|
||||||
log "Setting up swap on $SWAP_PART..."
|
log "Setting up swap on $SWAP_PART..."
|
||||||
mkswap -L swap "$SWAP_PART"
|
mkswap -L swap "$SWAP_PART"
|
||||||
swapon "$SWAP_PART"
|
swapon "$SWAP_PART"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# === Mount ===
|
||||||
log "Mounting..."
|
log "Mounting..."
|
||||||
mount "$NIXOS_PART" /mnt
|
mount "$NIXOS_PART" /mnt
|
||||||
mkdir -p /mnt/boot
|
mkdir -p /mnt/boot
|
||||||
mount "$ESP_PART" /mnt/boot
|
mount "$ESP_PART" /mnt/boot
|
||||||
ok "Mounted: $(lsblk -no MOUNTPOINTS $NIXOS_PART) and $(lsblk -no MOUNTPOINTS $ESP_PART)"
|
ok "Mounted: $NIXOS_PART → /mnt, $ESP_PART → /mnt/boot"
|
||||||
|
|
||||||
# === Generate hardware config ===
|
# === Generate hardware config ===
|
||||||
log "Generating hardware-configuration.nix..."
|
log "Generating hardware-configuration.nix..."
|
||||||
nixos-generate-config --root /mnt
|
nixos-generate-config --root /mnt
|
||||||
|
|
||||||
# === nixos-install with the flake ===
|
# === nixos-install ===
|
||||||
log "Installing NixOS from flake: $FLAKE_URL#$HOST"
|
log "Installing NixOS from flake: $FLAKE_URL#$HOST"
|
||||||
log "(This fetches and builds the world; first run is slow.)"
|
log "(First run builds the world; this can take 30-60 minutes.)"
|
||||||
nixos-install --flake "${FLAKE_URL}#${HOST}" --root /mnt --no-root-passwd
|
nixos-install --flake "${FLAKE_URL}#${HOST}" --root /mnt --no-root-passwd
|
||||||
|
|
||||||
ok "NixOS installed."
|
ok "NixOS installed."
|
||||||
|
|
@ -243,17 +400,18 @@ ok "NixOS installed."
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
${GRN}Next steps:${NC}
|
${GRN}Next steps:${NC}
|
||||||
1. Set a root password now (or after first boot):
|
1. Set a root password (the flake uses --no-root-passwd; check that
|
||||||
|
users.users.root.password is set in your config OR run interactively):
|
||||||
nixos-enter --root /mnt -- /bin/sh
|
nixos-enter --root /mnt -- /bin/sh
|
||||||
# passwd
|
# passwd
|
||||||
# exit
|
# exit
|
||||||
2. Unmount and reboot:
|
2. Unmount and reboot:
|
||||||
umount -R /mnt
|
umount -R /mnt
|
||||||
reboot
|
reboot
|
||||||
3. In the T490's UEFI boot menu (F12), pick the Linux entry.
|
3. On the T490, hit F12 at the Lenovo splash and pick the Linux entry.
|
||||||
systemd-boot menu will show "NixOS" and "Windows Boot Manager".
|
systemd-boot will show "NixOS" and "Windows Boot Manager".
|
||||||
|
|
||||||
If you get stuck, drop into a shell from the NixOS installer ISO
|
If something failed, you can re-run this script from the installer ISO.
|
||||||
and re-run this script — it's idempotent except for the partition
|
It's idempotent for steps 1-3 (mount, hardware-config, nixos-install);
|
||||||
shrink (which has already been done).
|
the only non-idempotent step is --shrink (which is a one-time operation).
|
||||||
EOF
|
EOF
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue