|
| 1 | +#!/usr/bin/env bash |
| 2 | +# install-image.sh — SourceOS clean-disk installer for the public ISO (PC / ARM). |
| 3 | +# |
| 4 | +# This is the BLANK-DISK path: it creates a fresh GPT layout (ESP + root) on a |
| 5 | +# target disk and installs the SourceOS GNOME desktop. For Apple Silicon use the |
| 6 | +# Asahi path (get-sourceos.sh); for a disk that is already partitioned by the |
| 7 | +# Asahi flow use install-on-device.sh. |
| 8 | +# |
| 9 | +# SAFETY: this ERASES the target disk. It requires an explicit disk argument, |
| 10 | +# refuses the disk the live system is running from, shows the plan, and waits |
| 11 | +# for you to type the disk name to confirm. |
| 12 | +# |
| 13 | +# Usage (run as root in the SourceOS live installer): |
| 14 | +# sudo install-image.sh # interactive: pick from a list |
| 15 | +# sudo install-image.sh /dev/nvme0n1 # non-interactive target |
| 16 | +# |
| 17 | +# Env overrides: FLAKE_REF (default github:SourceOS-Linux/source-os), |
| 18 | +# MODULE (default desktop-gnome), HOSTNAME (default sourceos). |
| 19 | +set -euo pipefail |
| 20 | + |
| 21 | +FLAKE_REF="${FLAKE_REF:-github:SourceOS-Linux/source-os}" |
| 22 | +MODULE="${MODULE:-desktop-gnome}" |
| 23 | +TARGET_HOSTNAME="${HOSTNAME:-sourceos}" |
| 24 | +MNT=/mnt |
| 25 | + |
| 26 | +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' |
| 27 | +ok() { printf " ${GREEN}✓${NC} %s\n" "$*"; } |
| 28 | +info() { printf " ${CYAN}·${NC} %s\n" "$*"; } |
| 29 | +warn() { printf " ${YELLOW}!${NC} %s\n" "$*"; } |
| 30 | +die() { printf " ${RED}✗ ERROR:${NC} %s\n" "$*" >&2; exit 1; } |
| 31 | + |
| 32 | +[[ $(id -u) -eq 0 ]] || die "Run as root (sudo install-image.sh)" |
| 33 | + |
| 34 | +# ── Identify the disk the live system runs from, so we never offer/erase it ── |
| 35 | +LIVE_SRC="$(findmnt -no SOURCE / 2>/dev/null || true)" |
| 36 | +LIVE_DISK="" |
| 37 | +[[ -n "$LIVE_SRC" ]] && LIVE_DISK="/dev/$(lsblk -no PKNAME "$LIVE_SRC" 2>/dev/null | head -1)" |
| 38 | + |
| 39 | +list_disks() { lsblk -dno NAME,SIZE,MODEL,TYPE | awk '$NF=="disk"{print "/dev/"$1" "$2" "substr($0, index($0,$3))}'; } |
| 40 | + |
| 41 | +# ── Choose target ───────────────────────────────────────────────────────────── |
| 42 | +TARGET="${1:-}" |
| 43 | +if [[ -z "$TARGET" ]]; then |
| 44 | + echo; info "Available disks (the live USB/ISO disk is excluded):"; echo |
| 45 | + list_disks | grep -v -- "${LIVE_DISK:-__none__}" | sed 's/^/ /' |
| 46 | + echo |
| 47 | + read -rp " Target disk (e.g. /dev/nvme0n1 or /dev/sda): " TARGET |
| 48 | +fi |
| 49 | +[[ -b "$TARGET" ]] || die "Not a block device: $TARGET" |
| 50 | +[[ "$TARGET" != "$LIVE_DISK" ]] || die "Refusing to erase the live medium ($TARGET)." |
| 51 | + |
| 52 | +# ── Confirm (typed) ─────────────────────────────────────────────────────────── |
| 53 | +SIZE="$(lsblk -dno SIZE "$TARGET")" |
| 54 | +echo |
| 55 | +warn "About to ERASE ${TARGET} (${SIZE}) and install SourceOS:" |
| 56 | +echo " 1. New GPT label" |
| 57 | +echo " 2. ESP 512 MiB FAT32 → /boot" |
| 58 | +echo " 3. Root rest ext4 → / (GNOME desktop)" |
| 59 | +echo |
| 60 | +read -rp " Type the disk name ('${TARGET}') to confirm: " CONFIRM |
| 61 | +[[ "$CONFIRM" == "$TARGET" ]] || die "Confirmation did not match. Aborted — nothing changed." |
| 62 | + |
| 63 | +# Partition suffix: nvme0n1 -> nvme0n1p1 ; sda -> sda1 |
| 64 | +part() { case "$TARGET" in *[0-9]) echo "${TARGET}p$1" ;; *) echo "${TARGET}$1" ;; esac; } |
| 65 | +ESP="$(part 1)"; ROOT="$(part 2)" |
| 66 | + |
| 67 | +# ── Partition + format ──────────────────────────────────────────────────────── |
| 68 | +info "Partitioning ${TARGET}..." |
| 69 | +wipefs -a "$TARGET" >/dev/null 2>&1 || true |
| 70 | +sgdisk --zap-all "$TARGET" >/dev/null |
| 71 | +sgdisk -n1:0:+512M -t1:ef00 -c1:EFI "$TARGET" >/dev/null |
| 72 | +sgdisk -n2:0:0 -t2:8300 -c2:nixos "$TARGET" >/dev/null |
| 73 | +partprobe "$TARGET" 2>/dev/null || true; udevadm settle 2>/dev/null || true; sleep 1 |
| 74 | + |
| 75 | +info "Formatting..." |
| 76 | +mkfs.fat -F32 -n EFI "$ESP" >/dev/null |
| 77 | +mkfs.ext4 -F -L nixos "$ROOT" >/dev/null |
| 78 | +ok "ESP=$ESP ROOT=$ROOT" |
| 79 | + |
| 80 | +# ── Mount ───────────────────────────────────────────────────────────────────── |
| 81 | +mount "$ROOT" "$MNT" |
| 82 | +mkdir -p "$MNT/boot" |
| 83 | +mount "$ESP" "$MNT/boot" |
| 84 | + |
| 85 | +# ── Compose a per-machine flake: hardware-config + the SourceOS GNOME module ── |
| 86 | +info "Generating hardware configuration..." |
| 87 | +nixos-generate-config --root "$MNT" --no-filesystems >/dev/null 2>&1 || nixos-generate-config --root "$MNT" |
| 88 | +# Keep the generated hardware-configuration.nix; replace configuration with a |
| 89 | +# flake that pulls SourceOS and applies the desktop-gnome module. |
| 90 | +NIXDIR="$MNT/etc/nixos" |
| 91 | +mkdir -p "$NIXDIR" |
| 92 | +cat > "$NIXDIR/flake.nix" <<EOF |
| 93 | +{ |
| 94 | + description = "SourceOS machine"; |
| 95 | + inputs.sourceos.url = "${FLAKE_REF}"; |
| 96 | + inputs.nixpkgs.follows = "sourceos/nixpkgs"; |
| 97 | + outputs = { self, nixpkgs, sourceos }: { |
| 98 | + nixosConfigurations.${TARGET_HOSTNAME} = nixpkgs.lib.nixosSystem { |
| 99 | + modules = [ |
| 100 | + ./hardware-configuration.nix |
| 101 | + sourceos.nixosModules.${MODULE} |
| 102 | + { networking.hostName = "${TARGET_HOSTNAME}"; } |
| 103 | + ]; |
| 104 | + }; |
| 105 | + }; |
| 106 | +} |
| 107 | +EOF |
| 108 | +ok "Wrote $NIXDIR/flake.nix (module: ${MODULE})" |
| 109 | + |
| 110 | +# ── Install ─────────────────────────────────────────────────────────────────── |
| 111 | +info "Running nixos-install (this builds the system; grab a coffee)..." |
| 112 | +nixos-install --root "$MNT" --flake "$NIXDIR#${TARGET_HOSTNAME}" --no-channel-copy |
| 113 | + |
| 114 | +# ── Password ────────────────────────────────────────────────────────────────── |
| 115 | +echo; info "Set a password for the 'sourceos' user:" |
| 116 | +nixos-enter --root "$MNT" -c 'passwd sourceos' |
| 117 | + |
| 118 | +echo |
| 119 | +ok "SourceOS installed to ${TARGET}." |
| 120 | +info "Remove the USB and reboot. After first boot you can apply the GNOME polish layer:" |
| 121 | +info " bash <(curl -fsSL https://raw.githubusercontent.com/SourceOS-Linux/source-os/main/profiles/linux-dev/workstation-v0/gnome/apply.sh)" |
| 122 | +echo |
| 123 | +read -rp " Reboot now? [y/N] " R; [[ "${R:-N}" =~ ^[Yy]$ ]] && { umount -R "$MNT"; reboot; } |
0 commit comments