aurora892d77e
install.sh: full install from the Arch ISO
+544 −11
2 files changed
- M install.sh +75 −11
- A install/base.sh +469 −0
| @@ -7,25 +7,63 @@ | |||
| 7 | 7 | # hardware (Lenovo Legion, Intel + NVIDIA hybrid, linux-zen) and my habits. | |
| 8 | 8 | # It is NOT a general purpose installer and I'm not trying to make it one. | |
| 9 | 9 | # | |
| 10 | − | # Only run it on a FRESHLY INSTALLED, UNCONFIGURED Arch Linux system. | |
| 11 | − | # It installs a lot of packages, moves your existing configs out of the way, | |
| 12 | − | # changes your login shell and can enable system services. | |
| 10 | + | # From the Arch ISO it installs everything: it ERASES the disk you pick (or uses | |
| 11 | + | # partitions you pick), installs Arch, GRUB and your user, then the desktop. | |
| 12 | + | # On an installed system it only does the desktop part. Only run that on a | |
| 13 | + | # FRESHLY INSTALLED, UNCONFIGURED Arch Linux system: it installs a lot of packages, | |
| 14 | + | # moves your existing configs out of the way, changes your login shell and can | |
| 15 | + | # enable system services. | |
| 13 | 16 | # | |
| 14 | 17 | # If you just want a piece of the setup, copy the files you like instead. | |
| 15 | 18 | # | |
| 16 | 19 | # Usage: | |
| 17 | − | # ./install.sh interactive | |
| 18 | − | # ./install.sh --yes pre-select everything (still asks the fresh-system question) | |
| 19 | − | # ./install.sh --check compare installed packages with packages/*.txt, changes nothing | |
| 20 | + | # From the Arch ISO (as root, network up): | |
| 21 | + | # bash <(curl -fsSL https://git.krolyxon.com/aurora/raw/master/install.sh) | |
| 22 | + | # | |
| 23 | + | # ./install.sh on the Arch ISO: full install (disk, Arch, desktop) | |
| 24 | + | # on an installed Arch: the desktop only (= --desktop) | |
| 25 | + | # ./install.sh --desktop desktop only: packages, dotfiles (stow), zsh, services | |
| 26 | + | # ./install.sh --base Arch ISO only: disk, Arch, GRUB, user, no desktop | |
| 27 | + | # ./install.sh --yes pre-select everything, don't ask (still asks the fresh-system question) | |
| 28 | + | # ./install.sh --check compare installed packages with packages/*.txt, changes nothing | |
| 20 | 29 | ||
| 21 | 30 | trap 'log WARN "Interrupted by user"; exit 130' INT | |
| 22 | 31 | ||
| 23 | 32 | INSTALL_STATUS="none" # none | partial | complete | failed | |
| 33 | + | MODE="" # full | base | desktop | |
| 24 | 34 | AUTO_YES=0 | |
| 25 | 35 | CHECK_ONLY=0 | |
| 36 | + | PICKS="" # file with the packages to install instead of the pickers | |
| 26 | 37 | BACKUP_DIR="" | |
| 27 | 38 | ||
| 39 | + | REPO_URLS=(https://git.krolyxon.com/aurora https://github.com/krolyxon/aurora) | |
| 40 | + | ||
| 28 | 41 | currentDir="$(dirname "$(readlink -f "$0")")" | |
| 42 | + | ||
| 43 | + | ############### | |
| 44 | + | ## Bootstrap ## | |
| 45 | + | ############### | |
| 46 | + | # Started without the repo (bash <(curl ...)): clone it and run the clone's install.sh. | |
| 47 | + | # On the ISO the clone is temporary (the full install copies it to the new home); | |
| 48 | + | # elsewhere it's the checkout the dotfiles get stowed from, so it goes to ~/aurora. | |
| 49 | + | if [[ ! -f "$currentDir/install/lib.sh" ]]; then | |
| 50 | + | if [[ $EUID -eq 0 && -d /run/archiso ]]; then | |
| 51 | + | dest=/tmp/aurora | |
| 52 | + | command -v git >/dev/null || pacman -Sy --needed --noconfirm git || exit 1 | |
| 53 | + | else | |
| 54 | + | dest="$HOME/aurora" | |
| 55 | + | command -v git >/dev/null || sudo pacman -S --needed --noconfirm git || exit 1 | |
| 56 | + | fi | |
| 57 | + | if [[ ! -f "$dest/install/lib.sh" ]]; then | |
| 58 | + | [[ -e "$dest" ]] && { echo "$dest exists but isn't an aurora checkout, move it away first." >&2; exit 1; } | |
| 59 | + | for url in "${REPO_URLS[@]}"; do | |
| 60 | + | git clone "$url" "$dest" && break | |
| 61 | + | done | |
| 62 | + | fi | |
| 63 | + | [[ -f "$dest/install/lib.sh" ]] || { echo "Couldn't clone aurora." >&2; exit 1; } | |
| 64 | + | exec bash "$dest/install.sh" "$@" | |
| 65 | + | fi | |
| 66 | + | ||
| 29 | 67 | cd "$currentDir" || exit 1 | |
| 30 | 68 | PKG_DIR="$currentDir/packages" | |
| 31 | 69 | ||
| @@ -35,24 +73,37 @@ source "$currentDir/install/lib.sh" | |||
| 35 | 73 | ####################### | |
| 36 | 74 | ## Arguement Parsing ## | |
| 37 | 75 | ####################### | |
| 38 | − | for arg in "$@"; do | |
| 39 | − | case "$arg" in | |
| 76 | + | while (($#)); do | |
| 77 | + | case "$1" in | |
| 78 | + | --desktop) | |
| 79 | + | MODE=desktop | |
| 80 | + | ;; | |
| 81 | + | --base) | |
| 82 | + | MODE=base | |
| 83 | + | ;; | |
| 40 | 84 | --yes|--ci|--non-interactive) | |
| 41 | 85 | AUTO_YES=1 | |
| 42 | 86 | ;; | |
| 43 | 87 | --check) | |
| 44 | 88 | CHECK_ONLY=1 | |
| 45 | 89 | ;; | |
| 90 | + | --picks) | |
| 91 | + | # internal: the full install hands the pickers' answers to the desktop part | |
| 92 | + | PICKS="${2:-}" | |
| 93 | + | shift | |
| 94 | + | [[ -f "$PICKS" ]] || { log ERROR "--picks: no such file: $PICKS"; exit 1; } | |
| 95 | + | ;; | |
| 46 | 96 | -h|--help) | |
| 47 | 97 | usage | |
| 48 | 98 | exit 0 | |
| 49 | 99 | ;; | |
| 50 | 100 | *) | |
| 51 | − | log ERROR "Unknown option: $arg" | |
| 101 | + | log ERROR "Unknown option: $1" | |
| 52 | 102 | usage | |
| 53 | 103 | exit 1 | |
| 54 | 104 | ;; | |
| 55 | 105 | esac | |
| 106 | + | shift | |
| 56 | 107 | done | |
| 57 | 108 | ||
| 58 | 109 | if ((CHECK_ONLY)); then | |
| @@ -60,5 +111,18 @@ if ((CHECK_ONLY)); then | |||
| 60 | 111 | exit 0 | |
| 61 | 112 | fi | |
| 62 | 113 | ||
| 63 | − | # shellcheck source=install/desktop.sh | |
| 64 | − | source "$currentDir/install/desktop.sh" | |
| 114 | + | # No mode given: the ISO means a bare machine, anything else already runs Arch | |
| 115 | + | if [[ -z "$MODE" ]]; then | |
| 116 | + | if [[ $EUID -eq 0 && -d /run/archiso ]]; then MODE=full; else MODE=desktop; fi | |
| 117 | + | fi | |
| 118 | + | ||
| 119 | + | case "$MODE" in | |
| 120 | + | full|base) | |
| 121 | + | # shellcheck source=install/base.sh | |
| 122 | + | source "$currentDir/install/base.sh" | |
| 123 | + | ;; | |
| 124 | + | desktop) | |
| 125 | + | # shellcheck source=install/desktop.sh | |
| 126 | + | source "$currentDir/install/desktop.sh" | |
| 127 | + | ;; | |
| 128 | + | esac | |
| @@ -0,0 +1,469 @@ | |||
| 1 | + | # shellcheck shell=bash | |
| 2 | + | # Base system from the Arch ISO: disk, Arch, GRUB, user, then (unless --base) the desktop part | |
| 3 | + | # inside the new system's chroot. Sourced by install.sh, runs as root. | |
| 4 | + | # | |
| 5 | + | # Every question comes first and nothing is touched until the summary is confirmed | |
| 6 | + | # (except cfdisk in manual mode, which only writes when you tell it to). | |
| 7 | + | # After that it runs unattended. | |
| 8 | + | ||
| 9 | + | LOG=/tmp/aurora-install.log | |
| 10 | + | CRYPT_NAME=cryptroot | |
| 11 | + | ||
| 12 | + | ############## | |
| 13 | + | ## Preflight ## | |
| 14 | + | ############## | |
| 15 | + | if [[ $EUID -ne 0 || ! -d /run/archiso ]]; then | |
| 16 | + | log ERROR "The full install (and --base) only runs from the Arch ISO, as root." | |
| 17 | + | log INFO "On an installed system use: ./install.sh --desktop" | |
| 18 | + | exit 1 | |
| 19 | + | fi | |
| 20 | + | ||
| 21 | + | if [[ "$(cat /sys/firmware/efi/fw_platform_size 2>/dev/null)" != 64 ]]; then | |
| 22 | + | log ERROR "The ISO isn't booted in 64-bit UEFI mode. Legacy BIOS isn't supported." | |
| 23 | + | exit 1 | |
| 24 | + | fi | |
| 25 | + | ||
| 26 | + | if ! curl -fsI --max-time 10 https://archlinux.org >/dev/null; then | |
| 27 | + | log ERROR "No network." | |
| 28 | + | cat >&2 <<'EOF' | |
| 29 | + | Ethernet should just work. Wi-Fi: | |
| 30 | + | iwctl device list | |
| 31 | + | iwctl station <device> get-networks | |
| 32 | + | iwctl station <device> connect <SSID> | |
| 33 | + | (Wi-Fi networks you connect to here are carried over to the new system.) | |
| 34 | + | EOF | |
| 35 | + | exit 1 | |
| 36 | + | fi | |
| 37 | + | ||
| 38 | + | log INFO "Preparing the live system" | |
| 39 | + | timedatectl set-ntp true | |
| 40 | + | # the ISO fills its keyring at boot: wait for that before using pacman | |
| 41 | + | for _ in {1..60}; do | |
| 42 | + | systemctl is-active -q pacman-init.service && break | |
| 43 | + | sleep 1 | |
| 44 | + | done | |
| 45 | + | sed -i 's/^#\?ParallelDownloads.*/ParallelDownloads = 15/' /etc/pacman.conf | |
| 46 | + | pacman -Sy --needed --noconfirm archlinux-keyring gum || exit 1 | |
| 47 | + | ||
| 48 | + | ||
| 49 | + | ############## | |
| 50 | + | ## Helpers ## | |
| 51 | + | ############## | |
| 52 | + | abort() { | |
| 53 | + | log WARN "Aborted, nothing was changed." | |
| 54 | + | exit 1 | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | # ask VAR "Prompt" [default] | |
| 58 | + | ask() { | |
| 59 | + | local -n _out="$1" | |
| 60 | + | _out="$(gum input --prompt "$2: " --value "${3:-}")" || abort | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | # ask_secret VAR "Label" [allow_empty] | |
| 64 | + | ask_secret() { | |
| 65 | + | local -n _out="$1" | |
| 66 | + | local a b | |
| 67 | + | while :; do | |
| 68 | + | a="$(gum input --password --prompt "$2: ")" || abort | |
| 69 | + | if [[ -z "$a" ]]; then | |
| 70 | + | if ((${3:-0})); then _out=""; return; fi | |
| 71 | + | gum_warn "Can't be empty" >&2 | |
| 72 | + | continue | |
| 73 | + | fi | |
| 74 | + | b="$(gum input --password --prompt "$2 (again): ")" || abort | |
| 75 | + | [[ "$a" == "$b" ]] && { _out="$a"; return; } | |
| 76 | + | gum_warn "They don't match, try again" >&2 | |
| 77 | + | done | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | # yes/no question, VAR=1/0. ask_yn VAR "Question" default(yes|no) | |
| 81 | + | ask_yn() { | |
| 82 | + | local -n _out="$1" | |
| 83 | + | local rc | |
| 84 | + | gum confirm --default="$([[ $3 == yes ]] && echo true || echo false)" "$2" | |
| 85 | + | rc=$? | |
| 86 | + | ((rc > 1)) && abort # ctrl+c / esc | |
| 87 | + | ((rc == 0)) && _out=1 || _out=0 | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | # One lsblk column of one device, without padding | |
| 91 | + | field() { | |
| 92 | + | lsblk -dno "$1" "$2" | sed 's/^ *//; s/ *$//' | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | # The disk the ISO itself was booted from (a USB stick): never offered | |
| 96 | + | iso_disk="$(lsblk -no PKNAME "$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null)" 2>/dev/null | head -1)" | |
| 97 | + | ||
| 98 | + | disk_lines() { | |
| 99 | + | local name type | |
| 100 | + | while read -r name type; do | |
| 101 | + | [[ "$type" == disk && "$name" != /dev/zram* && "$name" != "/dev/$iso_disk" ]] || continue | |
| 102 | + | printf '%-16s %8s %s\n' "$name" "$(field SIZE "$name")" "$(field MODEL "$name")" | |
| 103 | + | done < <(lsblk -dpno NAME,TYPE) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | part_lines() { | |
| 107 | + | local name type | |
| 108 | + | while read -r name type; do | |
| 109 | + | [[ "$type" == part ]] || continue | |
| 110 | + | [[ -n "$iso_disk" && "$(field PKNAME "$name")" == "$iso_disk" ]] && continue | |
| 111 | + | [[ " $* " == *" $name "* ]] && continue # already picked | |
| 112 | + | printf '%-16s %8s %-11s %s\n' "$name" "$(field SIZE "$name")" \ | |
| 113 | + | "$(field FSTYPE "$name")" "$(field PARTTYPENAME "$name")" | |
| 114 | + | done < <(lsblk -lpno NAME,TYPE) | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | # pick VAR "Header" < <(lines): the first column of the chosen line. | |
| 118 | + | # Not in a pipe: VAR would only be set in the pipe's subshell. | |
| 119 | + | pick() { | |
| 120 | + | local -n _out="$1" | |
| 121 | + | local lines line | |
| 122 | + | lines="$(cat)" | |
| 123 | + | [[ -n "$lines" ]] || { log ERROR "Nothing to choose from: $2"; exit 1; } | |
| 124 | + | line="$(gum choose --header "$2" <<< "$lines")" || abort | |
| 125 | + | _out="${line%% *}" | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | # Run a command on a pseudo-terminal: the output goes through tee into the log, and | |
| 129 | + | # pacman/paru only draw their progress bars on a terminal (silent for minutes otherwise) | |
| 130 | + | with_tty() { | |
| 131 | + | script -qefc "$(printf '%q ' "$@")" /dev/null | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | STEP="" | |
| 135 | + | step() { | |
| 136 | + | STEP="$1" | |
| 137 | + | log INFO "== $1" | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | ||
| 141 | + | ############### | |
| 142 | + | ## Questions ## | |
| 143 | + | ############### | |
| 144 | + | gum style --border double --border-foreground 196 --padding "1 2" --bold \ | |
| 145 | + | "⚠️ aurora: full install | |
| 146 | + | ||
| 147 | + | This installs Arch Linux + MY desktop (Hyprland + aurora), built for MY machine | |
| 148 | + | (Lenovo Legion, Intel + NVIDIA hybrid, linux-zen). It is NOT general purpose. | |
| 149 | + | ||
| 150 | + | It will ERASE the disk you pick (or format the partitions you pick). | |
| 151 | + | Nothing happens until you confirm the summary at the end. | |
| 152 | + | ||
| 153 | + | Provided AS IS, WITHOUT ANY WARRANTY. Proceed at your OWN RISK." | |
| 154 | + | ||
| 155 | + | ## Disk ## | |
| 156 | + | DISK_MODE="$(gum choose --header "Partitioning" \ | |
| 157 | + | "auto erase a whole disk: 1 GiB EFI + the rest for Arch" \ | |
| 158 | + | "manual partition yourself (cfdisk), then pick the partitions")" || abort | |
| 159 | + | DISK_MODE="${DISK_MODE%% *}" | |
| 160 | + | ||
| 161 | + | DISK="" ESP="" ROOT="" FORMAT_ESP=1 | |
| 162 | + | if [[ "$DISK_MODE" == auto ]]; then | |
| 163 | + | pick DISK "Disk to ERASE" < <(disk_lines) | |
| 164 | + | else | |
| 165 | + | cfdisk_disk="" | |
| 166 | + | pick cfdisk_disk "Run cfdisk on" < <(disk_lines; echo "skip (already partitioned)") | |
| 167 | + | if [[ "$cfdisk_disk" != skip ]]; then | |
| 168 | + | cfdisk "$cfdisk_disk" | |
| 169 | + | partprobe "$cfdisk_disk" 2>/dev/null | |
| 170 | + | udevadm settle | |
| 171 | + | fi | |
| 172 | + | ||
| 173 | + | pick ESP "EFI system partition (mounted at /boot)" < <(part_lines) | |
| 174 | + | if (( $(lsblk -bdno SIZE "$ESP") < 500 * 1024 * 1024 )); then | |
| 175 | + | gum_warn "$ESP is smaller than 500 MiB. /boot holds two kernels + initramfs (~300 MiB)." >&2 | |
| 176 | + | gum confirm --default=false "Use it anyway?" || abort | |
| 177 | + | fi | |
| 178 | + | if [[ "$(field FSTYPE "$ESP")" == vfat ]]; then | |
| 179 | + | # an existing ESP probably boots another OS too: keep it unless asked | |
| 180 | + | ask_yn FORMAT_ESP "$ESP is already FAT. Format it? (No keeps other systems' boot loaders)" no | |
| 181 | + | else | |
| 182 | + | log INFO "$ESP isn't FAT yet, it will be formatted" | |
| 183 | + | fi | |
| 184 | + | ||
| 185 | + | pick ROOT "Root partition (will be FORMATTED)" < <(part_lines "$ESP") | |
| 186 | + | fi | |
| 187 | + | ||
| 188 | + | ## Encryption ## | |
| 189 | + | ask_yn LUKS "Encrypt the system with LUKS2? (asks for the passphrase at every boot)" yes | |
| 190 | + | LUKS_PASS="" | |
| 191 | + | ((LUKS)) && ask_secret LUKS_PASS "Encryption passphrase" | |
| 192 | + | ||
| 193 | + | ## Host and user ## | |
| 194 | + | while :; do | |
| 195 | + | ask HOSTNAME_NEW "Hostname" "arch" | |
| 196 | + | [[ "$HOSTNAME_NEW" =~ ^[a-z0-9][a-z0-9-]{0,62}$ ]] && break | |
| 197 | + | gum_warn "Lowercase letters, digits and '-' only" >&2 | |
| 198 | + | done | |
| 199 | + | while :; do | |
| 200 | + | ask USERNAME_NEW "Username" | |
| 201 | + | [[ "$USERNAME_NEW" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] && break | |
| 202 | + | gum_warn "Lowercase letters, digits, '_' and '-', starting with a letter" >&2 | |
| 203 | + | done | |
| 204 | + | ask_secret USER_PASS "Password for $USERNAME_NEW" | |
| 205 | + | ask_secret ROOT_PASS "Root password (empty: root login disabled, use sudo)" 1 | |
| 206 | + | ||
| 207 | + | ## Locale ## | |
| 208 | + | TIMEZONE="Asia/Kolkata" | |
| 209 | + | ask_yn tz_ok "Timezone $TIMEZONE?" yes | |
| 210 | + | if ! ((tz_ok)); then | |
| 211 | + | TIMEZONE="$(timedatectl list-timezones | gum filter --header "Timezone")" || abort | |
| 212 | + | fi | |
| 213 | + | while :; do | |
| 214 | + | ask LOCALE "Locale" "en_US.UTF-8" | |
| 215 | + | grep -q "^#\?${LOCALE//./\\.} " /etc/locale.gen && break | |
| 216 | + | gum_warn "Not in /etc/locale.gen" >&2 | |
| 217 | + | done | |
| 218 | + | while :; do | |
| 219 | + | ask KEYMAP "Console keymap" "us" | |
| 220 | + | localectl list-keymaps | grep -qx "$KEYMAP" && break | |
| 221 | + | gum_warn "Unknown keymap" >&2 | |
| 222 | + | done | |
| 223 | + | ||
| 224 | + | ## GRUB ## | |
| 225 | + | ask_yn GRUB_HIDDEN "Skip the GRUB menu at boot? (hold Shift/Esc to see it)" yes | |
| 226 | + | ask_yn OS_PROBER "Add other installed systems (Windows, ...) to GRUB? (os-prober)" no | |
| 227 | + | ||
| 228 | + | ## Desktop packages ## | |
| 229 | + | SELECTED_PKGS=() | |
| 230 | + | [[ "$MODE" == full ]] && pick_packages | |
| 231 | + | ||
| 232 | + | ## Summary ## | |
| 233 | + | case "$(grep -m1 -o 'GenuineIntel\|AuthenticAMD' /proc/cpuinfo)" in | |
| 234 | + | GenuineIntel) UCODE=(intel-ucode) ;; | |
| 235 | + | AuthenticAMD) UCODE=(amd-ucode) ;; | |
| 236 | + | *) UCODE=() ;; | |
| 237 | + | esac | |
| 238 | + | ||
| 239 | + | if [[ "$DISK_MODE" == auto ]]; then | |
| 240 | + | disk_desc="$DISK ($(field SIZE "$DISK") $(field MODEL "$DISK")): ERASED, 1 GiB EFI + Arch" | |
| 241 | + | CONFIRM_TARGET="$DISK" | |
| 242 | + | else | |
| 243 | + | disk_desc="EFI $ESP ($( ((FORMAT_ESP)) && echo formatted || echo kept)), Arch on $ROOT (formatted)" | |
| 244 | + | CONFIRM_TARGET="$ROOT" | |
| 245 | + | fi | |
| 246 | + | if [[ "$MODE" == full ]]; then | |
| 247 | + | desktop_desc="Hyprland + aurora, $(read_list desktop | wc -l) base + ${#SELECTED_PKGS[@]} picked packages" | |
| 248 | + | else | |
| 249 | + | desktop_desc="none (--base), run ./install.sh --desktop later" | |
| 250 | + | fi | |
| 251 | + | ||
| 252 | + | gum style --border rounded --padding "1 2" \ | |
| 253 | + | "Summary | |
| 254 | + | ||
| 255 | + | Disk: $disk_desc | |
| 256 | + | Encryption: $( ((LUKS)) && echo "LUKS2" || echo "none") | |
| 257 | + | System: $(read_list base | grep -c .) packages (linux-zen + linux) ${UCODE[*]}, GRUB, zram swap | |
| 258 | + | Host/user: $HOSTNAME_NEW / $USERNAME_NEW (sudo), root login $( [[ -n $ROOT_PASS ]] && echo enabled || echo disabled) | |
| 259 | + | Locale: $TIMEZONE, $LOCALE, keymap $KEYMAP | |
| 260 | + | GRUB: menu $( ((GRUB_HIDDEN)) && echo hidden || echo shown), os-prober $( ((OS_PROBER)) && echo on || echo off) | |
| 261 | + | Desktop: $desktop_desc" | |
| 262 | + | ||
| 263 | + | answer="$(gum input --placeholder "$CONFIRM_TARGET" \ | |
| 264 | + | --header "Type $CONFIRM_TARGET to FORMAT it and start the install")" || abort | |
| 265 | + | [[ "$answer" == "$CONFIRM_TARGET" ]] || abort | |
| 266 | + | ||
| 267 | + | ||
| 268 | + | ################## | |
| 269 | + | ## Installation ## | |
| 270 | + | ################## | |
| 271 | + | # From here on: unattended, logged, stop at the first error | |
| 272 | + | exec 3>&1 4>&2 | |
| 273 | + | exec > >(tee -a "$LOG") 2>&1 | |
| 274 | + | set -Eeuo pipefail | |
| 275 | + | trap 'log ERROR "Failed: $STEP (line $LINENO). Log: $LOG. /mnt is still mounted for a look around."' ERR | |
| 276 | + | trap 'rm -f /mnt/etc/sudoers.d/99-aurora-install' EXIT | |
| 277 | + | ||
| 278 | + | # A previous attempt may have left things mounted/open | |
| 279 | + | if mountpoint -q /mnt; then umount -R /mnt; fi | |
| 280 | + | if [[ -e "/dev/mapper/$CRYPT_NAME" ]]; then cryptsetup close "$CRYPT_NAME"; fi | |
| 281 | + | ||
| 282 | + | if [[ "$DISK_MODE" == auto ]]; then | |
| 283 | + | step "Partitioning $DISK" | |
| 284 | + | # old signatures (LUKS headers, filesystems) first, then the partition table | |
| 285 | + | for p in $(lsblk -lpno NAME,TYPE "$DISK" | awk '$2 == "part" { print $1 }'); do | |
| 286 | + | wipefs -af "$p" | |
| 287 | + | done | |
| 288 | + | wipefs -af "$DISK" | |
| 289 | + | sgdisk --zap-all "$DISK" | |
| 290 | + | sgdisk -n1:0:+1G -t1:ef00 -c1:EFI \ | |
| 291 | + | -n2:0:0 -t2:"$( ((LUKS)) && echo 8309 || echo 8304)" -c2:arch "$DISK" | |
| 292 | + | partprobe "$DISK" || true | |
| 293 | + | udevadm settle | |
| 294 | + | # partition paths from lsblk: nvme0n1p1 and sda1 alike | |
| 295 | + | mapfile -t parts < <(lsblk -lpno NAME,TYPE "$DISK" | awk '$2 == "part" { print $1 }') | |
| 296 | + | ESP="${parts[0]}" | |
| 297 | + | ROOT="${parts[1]}" | |
| 298 | + | fi | |
| 299 | + | ||
| 300 | + | ROOT_DEV="$ROOT" | |
| 301 | + | if ((LUKS)); then | |
| 302 | + | step "Encrypting $ROOT" | |
| 303 | + | printf '%s' "$LUKS_PASS" | cryptsetup luksFormat --type luks2 --batch-mode --key-file=- "$ROOT" | |
| 304 | + | printf '%s' "$LUKS_PASS" | cryptsetup open --key-file=- "$ROOT" "$CRYPT_NAME" | |
| 305 | + | ROOT_DEV="/dev/mapper/$CRYPT_NAME" | |
| 306 | + | LUKS_UUID="$(blkid -s UUID -o value "$ROOT")" | |
| 307 | + | fi | |
| 308 | + | ||
| 309 | + | step "Formatting and mounting" | |
| 310 | + | ((FORMAT_ESP)) && mkfs.fat -F32 -n EFI "$ESP" | |
| 311 | + | mkfs.ext4 -F -L arch "$ROOT_DEV" | |
| 312 | + | mount "$ROOT_DEV" /mnt | |
| 313 | + | mount --mkdir -o umask=0077 "$ESP" /mnt/boot | |
| 314 | + | ||
| 315 | + | step "Installing the base system (pacstrap)" | |
| 316 | + | mapfile -t base_pkgs < <(read_list base) | |
| 317 | + | with_tty pacstrap -K /mnt "${base_pkgs[@]}" "${UCODE[@]}" | |
| 318 | + | genfstab -U /mnt >> /mnt/etc/fstab | |
| 319 | + | ||
| 320 | + | step "Configuring the system" | |
| 321 | + | ln -sf "/usr/share/zoneinfo/$TIMEZONE" /mnt/etc/localtime | |
| 322 | + | arch-chroot /mnt hwclock --systohc | |
| 323 | + | sed -i "s/^#\(${LOCALE//./\\.} \)/\1/" /mnt/etc/locale.gen | |
| 324 | + | arch-chroot /mnt locale-gen | |
| 325 | + | echo "LANG=$LOCALE" > /mnt/etc/locale.conf | |
| 326 | + | echo "KEYMAP=$KEYMAP" > /mnt/etc/vconsole.conf | |
| 327 | + | echo "$HOSTNAME_NEW" > /mnt/etc/hostname | |
| 328 | + | cat > /mnt/etc/hosts <<EOF | |
| 329 | + | 127.0.0.1 localhost | |
| 330 | + | ::1 localhost | |
| 331 | + | 127.0.1.1 $HOSTNAME_NEW.localdomain $HOSTNAME_NEW | |
| 332 | + | EOF | |
| 333 | + | sed -i 's/^#\?ParallelDownloads.*/ParallelDownloads = 15/; s/^#Color$/Color/' /mnt/etc/pacman.conf | |
| 334 | + | # swap in compressed RAM instead of a partition/file (no hibernation anyway) | |
| 335 | + | cat > /mnt/etc/systemd/zram-generator.conf <<'EOF' | |
| 336 | + | [zram0] | |
| 337 | + | zram-size = min(ram / 2, 8192) | |
| 338 | + | compression-algorithm = zstd | |
| 339 | + | EOF | |
| 340 | + | ||
| 341 | + | CMDLINE="" | |
| 342 | + | if ((LUKS)); then | |
| 343 | + | step "initramfs with LUKS" | |
| 344 | + | # Use whatever hooks the installed mkinitcpio defaults to: systemd-based or busybox | |
| 345 | + | hooks="$(grep '^HOOKS=' /mnt/etc/mkinitcpio.conf)" | |
| 346 | + | if [[ " ${hooks//[()=]/ } " == *" systemd "* ]]; then | |
| 347 | + | sed -i '/^HOOKS=/ s/ filesystems/ sd-encrypt filesystems/' /mnt/etc/mkinitcpio.conf | |
| 348 | + | CMDLINE="rd.luks.name=$LUKS_UUID=$CRYPT_NAME root=/dev/mapper/$CRYPT_NAME" | |
| 349 | + | else | |
| 350 | + | sed -i '/^HOOKS=/ s/ filesystems/ encrypt filesystems/' /mnt/etc/mkinitcpio.conf | |
| 351 | + | CMDLINE="cryptdevice=UUID=$LUKS_UUID:$CRYPT_NAME root=/dev/mapper/$CRYPT_NAME" | |
| 352 | + | fi | |
| 353 | + | grep -q '^HOOKS=.*encrypt filesystems' /mnt/etc/mkinitcpio.conf | |
| 354 | + | arch-chroot /mnt mkinitcpio -P | |
| 355 | + | fi | |
| 356 | + | ||
| 357 | + | step "Users" | |
| 358 | + | arch-chroot /mnt useradd -m -G wheel -s /usr/bin/zsh "$USERNAME_NEW" | |
| 359 | + | printf '%s:%s\n' "$USERNAME_NEW" "$USER_PASS" | arch-chroot /mnt chpasswd | |
| 360 | + | if [[ -n "$ROOT_PASS" ]]; then | |
| 361 | + | printf 'root:%s\n' "$ROOT_PASS" | arch-chroot /mnt chpasswd | |
| 362 | + | else | |
| 363 | + | arch-chroot /mnt passwd -l root | |
| 364 | + | fi | |
| 365 | + | echo '%wheel ALL=(ALL:ALL) ALL' > /mnt/etc/sudoers.d/10-wheel | |
| 366 | + | chmod 0440 /mnt/etc/sudoers.d/10-wheel | |
| 367 | + | arch-chroot /mnt visudo -cf /etc/sudoers.d/10-wheel | |
| 368 | + | ||
| 369 | + | step "GRUB" | |
| 370 | + | arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=arch | |
| 371 | + | # also EFI/BOOT/BOOTX64.EFI, for firmware that forgets its boot entries; only on a disk that's all ours | |
| 372 | + | [[ "$DISK_MODE" == auto ]] && arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot --removable | |
| 373 | + | g=/mnt/etc/default/grub | |
| 374 | + | [[ -n "$CMDLINE" ]] && sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=\"\(.*\)\"|GRUB_CMDLINE_LINUX_DEFAULT=\"\1 $CMDLINE\"|" "$g" | |
| 375 | + | # zen is the default entry, plain linux stays as the fallback | |
| 376 | + | [[ -f /mnt/boot/vmlinuz-linux-zen ]] && echo 'GRUB_TOP_LEVEL="/boot/vmlinuz-linux-zen"' >> "$g" | |
| 377 | + | if ((GRUB_HIDDEN)); then | |
| 378 | + | sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/; s/^#\?GRUB_TIMEOUT_STYLE=.*/GRUB_TIMEOUT_STYLE=hidden/' "$g" | |
| 379 | + | fi | |
| 380 | + | ((OS_PROBER)) && sed -i 's/^#\?GRUB_DISABLE_OS_PROBER=.*/GRUB_DISABLE_OS_PROBER=false/' "$g" | |
| 381 | + | arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg | |
| 382 | + | ||
| 383 | + | step "Services" | |
| 384 | + | arch-chroot /mnt systemctl enable NetworkManager.service systemd-timesyncd.service fstrim.timer | |
| 385 | + | ||
| 386 | + | # Wi-Fi networks joined with iwctl on the ISO -> NetworkManager, so the first boot is online | |
| 387 | + | for psk in /var/lib/iwd/*.psk; do | |
| 388 | + | [[ -f "$psk" ]] || continue | |
| 389 | + | ssid="$(basename "$psk" .psk)" | |
| 390 | + | # iwd hex-encodes names with special characters as =<hex> | |
| 391 | + | [[ "$ssid" == =* ]] && ssid="$(printf '%b' "$(sed 's/../\\x&/g' <<< "${ssid#=}")")" | |
| 392 | + | key="$(sed -n 's/^Passphrase=//p' "$psk")" | |
| 393 | + | [[ -n "$key" ]] || key="$(sed -n 's/^PreSharedKey=//p' "$psk")" | |
| 394 | + | [[ -n "$key" ]] || continue | |
| 395 | + | mkdir -p /mnt/etc/NetworkManager/system-connections | |
| 396 | + | conn="/mnt/etc/NetworkManager/system-connections/${ssid//[^A-Za-z0-9._-]/_}.nmconnection" | |
| 397 | + | cat > "$conn" <<EOF | |
| 398 | + | [connection] | |
| 399 | + | id=$ssid | |
| 400 | + | uuid=$(cat /proc/sys/kernel/random/uuid) | |
| 401 | + | type=wifi | |
| 402 | + | ||
| 403 | + | [wifi] | |
| 404 | + | mode=infrastructure | |
| 405 | + | ssid=$ssid | |
| 406 | + | ||
| 407 | + | [wifi-security] | |
| 408 | + | key-mgmt=wpa-psk | |
| 409 | + | psk=$key | |
| 410 | + | ||
| 411 | + | [ipv4] | |
| 412 | + | method=auto | |
| 413 | + | ||
| 414 | + | [ipv6] | |
| 415 | + | method=auto | |
| 416 | + | EOF | |
| 417 | + | chmod 0600 "$conn" | |
| 418 | + | log INFO "Wi-Fi \"$ssid\" carried over" | |
| 419 | + | done | |
| 420 | + | ||
| 421 | + | step "Copying aurora to /home/$USERNAME_NEW/aurora" | |
| 422 | + | cp -a "$currentDir" "/mnt/home/$USERNAME_NEW/aurora" | |
| 423 | + | arch-chroot /mnt chown -R "$USERNAME_NEW:$USERNAME_NEW" "/home/$USERNAME_NEW/aurora" | |
| 424 | + | ||
| 425 | + | DESKTOP_OK=1 | |
| 426 | + | if [[ "$MODE" == full ]]; then | |
| 427 | + | step "Desktop (install.sh --desktop inside the new system)" | |
| 428 | + | # makepkg/paru call sudo: no password while this runs, the EXIT trap removes it again | |
| 429 | + | echo "$USERNAME_NEW ALL=(ALL) NOPASSWD: ALL" > /mnt/etc/sudoers.d/99-aurora-install | |
| 430 | + | chmod 0440 /mnt/etc/sudoers.d/99-aurora-install | |
| 431 | + | picks="/home/$USERNAME_NEW/.aurora-picks" | |
| 432 | + | printf '%s\n' "${SELECTED_PKGS[@]}" > "/mnt$picks" | |
| 433 | + | if ! with_tty arch-chroot /mnt runuser -u "$USERNAME_NEW" -- \ | |
| 434 | + | env HOME="/home/$USERNAME_NEW" USER="$USERNAME_NEW" LOGNAME="$USERNAME_NEW" \ | |
| 435 | + | SHELL=/usr/bin/zsh AURORA_CHROOT=1 \ | |
| 436 | + | bash "/home/$USERNAME_NEW/aurora/install.sh" --desktop --yes --picks "$picks"; then | |
| 437 | + | DESKTOP_OK=0 | |
| 438 | + | log ERROR "The desktop part failed. After the reboot, log in and run: ~/aurora/install.sh --desktop" | |
| 439 | + | fi | |
| 440 | + | rm -f "/mnt$picks" /mnt/etc/sudoers.d/99-aurora-install | |
| 441 | + | fi | |
| 442 | + | ||
| 443 | + | step "Finishing" | |
| 444 | + | trap - ERR | |
| 445 | + | set +Eeuo pipefail | |
| 446 | + | exec 1>&3 2>&4 3>&- 4>&- | |
| 447 | + | sleep 1 # let tee write the last lines | |
| 448 | + | install -m 0600 "$LOG" /mnt/var/log/aurora-install.log | |
| 449 | + | umount -R /mnt | |
| 450 | + | ((LUKS)) && cryptsetup close "$CRYPT_NAME" | |
| 451 | + | ||
| 452 | + | if ((DESKTOP_OK)); then | |
| 453 | + | gum style --border rounded --border-foreground 42 --padding "1 2" --bold \ | |
| 454 | + | "✔ Arch$([[ "$MODE" == full ]] && echo " + aurora") installed. | |
| 455 | + | ||
| 456 | + | Remove the install medium and reboot.$([[ "$MODE" == full ]] && echo " | |
| 457 | + | Log in on tty1 as $USERNAME_NEW: Hyprland and aurora start by themselves." || echo " | |
| 458 | + | For the desktop, log in and run: ~/aurora/install.sh --desktop") | |
| 459 | + | The install log is in /var/log/aurora-install.log." | |
| 460 | + | else | |
| 461 | + | gum style --border rounded --border-foreground 220 --padding "1 2" --bold \ | |
| 462 | + | "⚠ Arch is installed, the desktop part failed. | |
| 463 | + | ||
| 464 | + | Reboot, log in as $USERNAME_NEW and run: ~/aurora/install.sh --desktop | |
| 465 | + | The install log is in /var/log/aurora-install.log." | |
| 466 | + | fi | |
| 467 | + | ||
| 468 | + | gum confirm "Reboot now?" && reboot | |
| 469 | + | exit 0 | |