Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
Scott Scoble
ea46ee2c93 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.
2026-06-06 07:20:24 -07:00
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
Hermes
921fc1402a T490 + Windows 11 dual-boot support
- modules/laptop.nix: add systemd-boot extraEntries for Windows Boot Manager
  Chainloads $ESP/EFI/Microsoft/Boot/bootmgfw.efi. sort-key 'z_windows' puts
  it after NixOS generations in the menu. Explicit entry works on all
  systemd-boot versions; NixOS 26.05 'Yarara' may also autodetect on
  shared ESP, in which case the duplicate is harmless.
- install.sh: guided dual-boot installer. Shrinks Win11 NTFS with ntfsresize
  (no data loss), creates NixOS ext4 root, mounts shared ESP, runs
  nixos-install with this flake. Idempotent except for the partition shrink.
- MIGRATION.md: new T490 + Windows 11 dual-boot section, with manual
  fallback and NixOS 26.05 compatibility notes.
2026-06-05 19:53:56 -07:00
3 changed files with 496 additions and 1 deletions

View file

@ -1,5 +1,63 @@
# 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

417
install.sh Executable file
View file

@ -0,0 +1,417 @@
#!/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 <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() {
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 <<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
if [[ $YES -ne 1 ]]; then
read -rp $'\nType \'yes\' to proceed: ' confirm
[[ "$confirm" == "yes" ]] || fail "aborted by user"
fi
# === Pre-flight: ask for root password now (nixos-install --no-root-passwd
# means a password must be set post-install, which is a common stumble) ===
if [[ $YES -ne 1 ]]; then
cat <<EOF
${YEL}About nixos-install:${NC}
The script uses --no-root-passwd (declarative config sets the password).
Make sure modules/laptop.nix (or wherever you set users.users.root.password)
has a hashed password, OR run 'passwd' in the nixos-enter shell after
install completes.
EOF
fi
# === Shrink existing partition (if requested) ===
if [[ -n "$SHRINK_PART" ]]; then
log "Validating that $SHRINK_PART can be safely shrunk..."
SHRINK_FSTYPE=$(lsblk -ndo FSTYPE "$SHRINK_PART" 2>/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 <<EOF
${GRN}Next steps:${NC}
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
# passwd
# exit
2. Unmount and reboot:
umount -R /mnt
reboot
3. On the T490, hit F12 at the Lenovo splash and pick the Linux entry.
systemd-boot will show "NixOS" and "Windows Boot Manager".
If something failed, you can re-run this script from the installer ISO.
It's idempotent for steps 1-3 (mount, hardware-config, nixos-install);
the only non-idempotent step is --shrink (which is a one-time operation).
EOF

View file

@ -212,7 +212,27 @@
i18n.defaultLocale = "en_US.UTF-8";
# === Boot ===
boot.loader.systemd-boot.enable = true;
# systemd-boot with Windows dual-boot entry.
# NixOS 26.05 "Yarara" ships systemd-boot with Windows auto-detection on
# shared ESP, but we keep the explicit entry for clarity and as a fallback
# for older systemd-boot versions (the entry is harmless when autodetect
# also fires — systemd-boot de-duplicates by EFI binary path).
#
# extraEntries is an attrset: key = filename under $BOOT/loader/entries/,
# value = the .conf file content. Format follows the Boot Loader Spec:
# https://uapi-group.org/specifications/specs/boot_loader_specification/
boot.loader.systemd-boot = {
enable = true;
configurationLimit = 50; # keep more NixOS generations in the menu
extraEntries = {
"windows.conf" = ''
title Windows Boot Manager
efi /EFI/Microsoft/Boot/bootmgfw.efi
sort-key z_windows
type oneshot
'';
};
};
boot.loader.efi.canTouchEfiVariables = true;
# === SSH Server ===