diff --git a/MIGRATION.md b/MIGRATION.md index 4bcb2b3..acf4f9b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,63 +1,5 @@ # Laptop Migration: MacBook → x86_64 NixOS -## T490 + Windows 11 dual-boot (recommended flow) - -The T490 already has Windows 11 installed. To add NixOS alongside it without -losing Windows: - -### 1. Boot the NixOS installer from a Ventoy stick -From the live installer environment: -```bash -# Network access is required; either wifi-menu or plug in ethernet -# Then run the guided installer — it handles partitioning: -curl -L https://os.scoble.me/forgejo/sscoble/laptop-config/raw/branch/main/install.sh | sudo bash -``` - -The script: -- Detects `/dev/nvme0n1` and the existing ESP -- Shrinks the Windows NTFS partition with `ntfsresize` (no data loss) -- Creates a NixOS ext4 root partition in the freed space -- Mounts the existing Windows ESP at `/mnt/boot` (shared, not reformatted) -- Runs `nixos-install` with this flake -- Reboots into a systemd-boot menu with both NixOS and Windows entries - -### 2. After install -First boot → systemd-boot menu shows: -- `NixOS (default)` — most recent generation -- Older NixOS generations (up to 50, configurable in `modules/laptop.nix`) -- `Windows Boot Manager` — chainloads the standard Microsoft EFI binary at - `$ESP/EFI/Microsoft/Boot/bootmgfw.efi` - -If you ever want to skip the menu and boot straight to Windows, hold F12 at -the Lenovo splash and pick "Windows Boot Manager" from the firmware menu. - -### 3. Fall back to manual partition if the script fails -The script handles the common case. For unusual layouts (multiple disks, LVM, -existing NixOS partitions, etc.), do it by hand: -```bash -# Identify partitions -lsblk -f -# Target the existing Windows ESP for /boot -mount /dev/nvme0n1p1 /mnt/boot # adjust to your partition -# Create NixOS root -mkfs.ext4 -L nixos /dev/nvme0n1pX -mount /dev/nvme0n1pX /mnt -nixos-generate-config --root /mnt -nixos-install --flake git+https://os.scoble.me/forgejo/sscoble/laptop-config.git#laptop --root /mnt -``` - -### 4. NixOS 26.05 ("Yarara") compatibility -The flake pins `nixos-unstable`, which as of June 2026 tracks NixOS 26.05+. -Key 26.05 changes that affect this config: -- `boot.initrd.systemd.enable` is now default true. Most things Just Work. -- `boot.supportedFilesystems` is now an attrset (not a list) — nix warns but - the existing config still parses. -- `systemd.extraConfig` is removed; use `systemd.settings.Manager` instead. - We don't use `systemd.extraConfig` anywhere, so this is a no-op for us. -- The new `boot.loader.systemd-boot.windows` first-class option exists for - multi-drive dual-boot, but we don't need it — sharing a single ESP means - our explicit `extraEntries` works on all systemd-boot versions. - ## Order of operations ### 1. Install NixOS diff --git a/install.sh b/install.sh deleted file mode 100755 index 0f4a351..0000000 --- a/install.sh +++ /dev/null @@ -1,417 +0,0 @@ -#!/usr/bin/env bash -# 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 -# the ISO and dropped into a root shell. -# -# Required arguments (all explicit, no auto-detection): -# --disk PATH The physical disk to operate on (e.g. /dev/nvme0n1) -# --efi PATH The EFI System Partition (mounted at /mnt/boot) -# --nixos PATH The partition to format as ext4 and mount at /mnt -# -# Optional: -# --swap PATH Existing partition to use as swap (will mkswap + swapon) -# --new-size SIZE Create a NEW partition of this size (e.g. 200G) for NixOS. -# Use this when --nixos is a NEW partition that needs mkpart. -# 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. -# -# Examples: -# # 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 -# -# # Use a partition you already prepared (no shrink, no new partition) -# install.sh --disk /dev/nvme0n1 \ -# --efi /dev/nvme0n1p1 \ -# --nixos /dev/nvme0n1p5 -# -# # Dry-run: see what would happen -# install.sh --disk /dev/nvme0n1 --efi /dev/nvme0n1p1 \ -# --nixos /dev/nvme0n1p6 --new-size 200G --shrink /dev/nvme0n1p4 \ -# --dry-run -# -# 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 - -# === Defaults === -BOOTLOADER="systemd-boot" -HOST="laptop" -FLAKE_URL="" -NO_FETCH=0 -DRY_RUN=0 -YES=0 - -# === Color output === -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} $*"; } - -# === Help text === -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 === -[[ -d /sys/firmware/efi ]] || fail "system is not booted in UEFI mode" - -# === Validate that the partitions exist and are block devices === -[[ -b "$DISK" ]] || fail "--disk $DISK is not a block device" -[[ -b "$ESP_PART" ]] || fail "--efi $ESP_PART is not a block device" -[[ -b "$NIXOS_PART" ]] || fail "--nixos $NIXOS_PART is not a block device" -[[ -z "$SWAP_PART" || -b "$SWAP_PART" ]] || fail "--swap $SWAP_PART is not a block device" -[[ -z "$SHRINK_PART" || -b "$SHRINK_PART" ]] || fail "--shrink $SHRINK_PART is not a block device" - -# === Validate that --efi, --nixos, --swap, --shrink all belong to --disk === -# A partition belongs to a disk if lsblk shows it under the same parent. -# Walk: for each candidate, get its PKNAME (parent kernel name) and check -# it matches the basename of $DISK (e.g. nvme0n1, sda). -parent_of() { - lsblk -ndo PKNAME "$1" 2>/dev/null | head -1 -} -diskn=$(basename "$DISK") -for part in "$ESP_PART" "$NIXOS_PART" "$SWAP_PART" "$SHRINK_PART"; do - [[ -z "$part" ]] && continue - parent=$(parent_of "$part") - if [[ "$parent" != "$diskn" ]]; then - fail "$part is not a partition of $DISK (parent is '${parent:-none}')" - fi -done - -# === No partition may be used twice === -seen="$DISK|$ESP_PART|$NIXOS_PART" -[[ -n "$SWAP_PART" ]] && seen="$seen|$SWAP_PART" -[[ -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" - -# === Validate --efi is actually an ESP === -ESP_GUID="c12a7328-f81f-11d2-ba4b-00a0c93ec93b" -ESP_FSTYPE=$(lsblk -ndo FSTYPE "$ESP_PART" 2>/dev/null || true) -ESP_PTYPE=$(lsblk -ndo PARTTYPE "$ESP_PART" 2>/dev/null || true) -if [[ "$ESP_PTYPE" != "$ESP_GUID" ]]; then - fail "$ESP_PART is not an EFI System Partition (PARTTYPE=$ESP_PTYPE, expected $ESP_GUID)" -fi -if [[ "$ESP_FSTYPE" != "vfat" ]]; then - fail "$ESP_PART does not have a vfat filesystem (FSTYPE='$ESP_FSTYPE') — refusing to mount as ESP" -fi - -# === Validate --nixos isn't mounted as the running / or /boot === -if findmnt -n -o TARGET --source "$NIXOS_PART" 2>/dev/null | grep -qE '^/$'; then - fail "$NIXOS_PART is mounted as / — refusing to format the running root" -fi -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 - -# === Validate --swap is a real partition (not the same as NixOS root) === -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 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() { - 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 -} - -# === Build the plan === -plan_actions=() -[[ -n "$SHRINK_PART" ]] && plan_actions+=("shrink $SHRINK_PART by ~$NEW_SIZE") -[[ -n "$NEW_SIZE" ]] && plan_actions+=("create new partition on $DISK (size $NEW_SIZE) for NixOS root") -plan_actions+=("format $NIXOS_PART as ext4 (label: nixos)") -[[ -n "$SWAP_PART" ]] && plan_actions+=("mkswap + swapon on $SWAP_PART") -plan_actions+=("mount $NIXOS_PART at /mnt") -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 <} - Shrink target: ${SHRINK_PART:-} - New partition: ${NEW_SIZE:-} - 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 </dev/null || true) - case "$SHRINK_FSTYPE" in - ntfs) - log "Shrinking NTFS partition $SHRINK_PART (this can take several minutes)..." - ntfsfix -d "$SHRINK_PART" 2>/dev/null || true - nixos_bytes=$(size_to_bytes "$NEW_SIZE") - cur_bytes=$(lsblk -ndo SIZE -b "$SHRINK_PART") - new_bytes=$(( cur_bytes - nixos_bytes )) - ntfsresize -s --size "$new_bytes" "$SHRINK_PART" 2>&1 | tail -5 \ - || fail "ntfsresize failed; aborting" - partprobe "$DISK" - sleep 2 - ;; - ext4) - log "Shrinking ext4 partition $SHRINK_PART..." - e2fsck -f "$SHRINK_PART" || fail "e2fsck failed on $SHRINK_PART" - resize2fs "$SHRINK_PART" "$(size_to_bytes "$NEW_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 -fi - -# === Create new partition (if requested) === -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" - 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 - -# === Format NixOS root === -log "Formatting $NIXOS_PART as ext4..." -mkfs.ext4 -F -L nixos "$NIXOS_PART" - -# === Set up swap === -if [[ -n "$SWAP_PART" ]]; then - log "Setting up swap on $SWAP_PART..." - mkswap -L swap "$SWAP_PART" - swapon "$SWAP_PART" -fi - -# === Mount === -log "Mounting..." -mount "$NIXOS_PART" /mnt -mkdir -p /mnt/boot -mount "$ESP_PART" /mnt/boot -ok "Mounted: $NIXOS_PART → /mnt, $ESP_PART → /mnt/boot" - -# === Generate hardware config === -log "Generating hardware-configuration.nix..." -nixos-generate-config --root /mnt - -# === nixos-install === -log "Installing NixOS from flake: $FLAKE_URL#$HOST" -log "(First run builds the world; this can take 30-60 minutes.)" -nixos-install --flake "${FLAKE_URL}#${HOST}" --root /mnt --no-root-passwd - -ok "NixOS installed." - -cat <