Krolyxon
Commit 892d77e

Branches

Refs →

aurorainstallbase.sh

469 lines · 16.6 KB · Bourne Again Shell (bash)

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
9LOG=/tmp/aurora-install.log
10CRYPT_NAME=cryptroot
11
12##############
13## Preflight ##
14##############
15if [[ $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
19fi
20
21if [[ "$(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
24fi
25
26if ! curl -fsI --max-time 10 https://archlinux.org >/dev/null; then
27 log ERROR "No network."
28 cat >&2 <<'EOF'
29Ethernet 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.)
34EOF
35 exit 1
36fi
37
38log INFO "Preparing the live system"
39timedatectl set-ntp true
40# the ISO fills its keyring at boot: wait for that before using pacman
41for _ in {1..60}; do
42 systemctl is-active -q pacman-init.service && break
43 sleep 1
44done
45sed -i 's/^#\?ParallelDownloads.*/ParallelDownloads = 15/' /etc/pacman.conf
46pacman -Sy --needed --noconfirm archlinux-keyring gum || exit 1
47
48
49##############
50## Helpers ##
51##############
52abort() {
53 log WARN "Aborted, nothing was changed."
54 exit 1
55}
56
57# ask VAR "Prompt" [default]
58ask() {
59 local -n _out="$1"
60 _out="$(gum input --prompt "$2: " --value "${3:-}")" || abort
61}
62
63# ask_secret VAR "Label" [allow_empty]
64ask_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)
81ask_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
91field() {
92 lsblk -dno "$1" "$2" | sed 's/^ *//; s/ *$//'
93}
94
95# The disk the ISO itself was booted from (a USB stick): never offered
96iso_disk="$(lsblk -no PKNAME "$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null)" 2>/dev/null | head -1)"
97
98disk_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
106part_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.
119pick() {
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)
130with_tty() {
131 script -qefc "$(printf '%q ' "$@")" /dev/null
132}
133
134STEP=""
135step() {
136 STEP="$1"
137 log INFO "== $1"
138}
139
140
141###############
142## Questions ##
143###############
144gum style --border double --border-foreground 196 --padding "1 2" --bold \
145"⚠️ aurora: full install
146
147This 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
150It will ERASE the disk you pick (or format the partitions you pick).
151Nothing happens until you confirm the summary at the end.
152
153Provided AS IS, WITHOUT ANY WARRANTY. Proceed at your OWN RISK."
154
155## Disk ##
156DISK_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
159DISK_MODE="${DISK_MODE%% *}"
160
161DISK="" ESP="" ROOT="" FORMAT_ESP=1
162if [[ "$DISK_MODE" == auto ]]; then
163 pick DISK "Disk to ERASE" < <(disk_lines)
164else
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")
186fi
187
188## Encryption ##
189ask_yn LUKS "Encrypt the system with LUKS2? (asks for the passphrase at every boot)" yes
190LUKS_PASS=""
191((LUKS)) && ask_secret LUKS_PASS "Encryption passphrase"
192
193## Host and user ##
194while :; 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
198done
199while :; 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
203done
204ask_secret USER_PASS "Password for $USERNAME_NEW"
205ask_secret ROOT_PASS "Root password (empty: root login disabled, use sudo)" 1
206
207## Locale ##
208TIMEZONE="Asia/Kolkata"
209ask_yn tz_ok "Timezone $TIMEZONE?" yes
210if ! ((tz_ok)); then
211 TIMEZONE="$(timedatectl list-timezones | gum filter --header "Timezone")" || abort
212fi
213while :; 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
217done
218while :; do
219 ask KEYMAP "Console keymap" "us"
220 localectl list-keymaps | grep -qx "$KEYMAP" && break
221 gum_warn "Unknown keymap" >&2
222done
223
224## GRUB ##
225ask_yn GRUB_HIDDEN "Skip the GRUB menu at boot? (hold Shift/Esc to see it)" yes
226ask_yn OS_PROBER "Add other installed systems (Windows, ...) to GRUB? (os-prober)" no
227
228## Desktop packages ##
229SELECTED_PKGS=()
230[[ "$MODE" == full ]] && pick_packages
231
232## Summary ##
233case "$(grep -m1 -o 'GenuineIntel\|AuthenticAMD' /proc/cpuinfo)" in
234 GenuineIntel) UCODE=(intel-ucode) ;;
235 AuthenticAMD) UCODE=(amd-ucode) ;;
236 *) UCODE=() ;;
237esac
238
239if [[ "$DISK_MODE" == auto ]]; then
240 disk_desc="$DISK ($(field SIZE "$DISK") $(field MODEL "$DISK")): ERASED, 1 GiB EFI + Arch"
241 CONFIRM_TARGET="$DISK"
242else
243 disk_desc="EFI $ESP ($( ((FORMAT_ESP)) && echo formatted || echo kept)), Arch on $ROOT (formatted)"
244 CONFIRM_TARGET="$ROOT"
245fi
246if [[ "$MODE" == full ]]; then
247 desktop_desc="Hyprland + aurora, $(read_list desktop | wc -l) base + ${#SELECTED_PKGS[@]} picked packages"
248else
249 desktop_desc="none (--base), run ./install.sh --desktop later"
250fi
251
252gum style --border rounded --padding "1 2" \
253"Summary
254
255Disk: $disk_desc
256Encryption: $( ((LUKS)) && echo "LUKS2" || echo "none")
257System: $(read_list base | grep -c .) packages (linux-zen + linux) ${UCODE[*]}, GRUB, zram swap
258Host/user: $HOSTNAME_NEW / $USERNAME_NEW (sudo), root login $( [[ -n $ROOT_PASS ]] && echo enabled || echo disabled)
259Locale: $TIMEZONE, $LOCALE, keymap $KEYMAP
260GRUB: menu $( ((GRUB_HIDDEN)) && echo hidden || echo shown), os-prober $( ((OS_PROBER)) && echo on || echo off)
261Desktop: $desktop_desc"
262
263answer="$(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
272exec 3>&1 4>&2
273exec > >(tee -a "$LOG") 2>&1
274set -Eeuo pipefail
275trap 'log ERROR "Failed: $STEP (line $LINENO). Log: $LOG. /mnt is still mounted for a look around."' ERR
276trap 'rm -f /mnt/etc/sudoers.d/99-aurora-install' EXIT
277
278# A previous attempt may have left things mounted/open
279if mountpoint -q /mnt; then umount -R /mnt; fi
280if [[ -e "/dev/mapper/$CRYPT_NAME" ]]; then cryptsetup close "$CRYPT_NAME"; fi
281
282if [[ "$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]}"
298fi
299
300ROOT_DEV="$ROOT"
301if ((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")"
307fi
308
309step "Formatting and mounting"
310((FORMAT_ESP)) && mkfs.fat -F32 -n EFI "$ESP"
311mkfs.ext4 -F -L arch "$ROOT_DEV"
312mount "$ROOT_DEV" /mnt
313mount --mkdir -o umask=0077 "$ESP" /mnt/boot
314
315step "Installing the base system (pacstrap)"
316mapfile -t base_pkgs < <(read_list base)
317with_tty pacstrap -K /mnt "${base_pkgs[@]}" "${UCODE[@]}"
318genfstab -U /mnt >> /mnt/etc/fstab
319
320step "Configuring the system"
321ln -sf "/usr/share/zoneinfo/$TIMEZONE" /mnt/etc/localtime
322arch-chroot /mnt hwclock --systohc
323sed -i "s/^#\(${LOCALE//./\\.} \)/\1/" /mnt/etc/locale.gen
324arch-chroot /mnt locale-gen
325echo "LANG=$LOCALE" > /mnt/etc/locale.conf
326echo "KEYMAP=$KEYMAP" > /mnt/etc/vconsole.conf
327echo "$HOSTNAME_NEW" > /mnt/etc/hostname
328cat > /mnt/etc/hosts <<EOF
329127.0.0.1 localhost
330::1 localhost
331127.0.1.1 $HOSTNAME_NEW.localdomain $HOSTNAME_NEW
332EOF
333sed -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)
335cat > /mnt/etc/systemd/zram-generator.conf <<'EOF'
336[zram0]
337zram-size = min(ram / 2, 8192)
338compression-algorithm = zstd
339EOF
340
341CMDLINE=""
342if ((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
355fi
356
357step "Users"
358arch-chroot /mnt useradd -m -G wheel -s /usr/bin/zsh "$USERNAME_NEW"
359printf '%s:%s\n' "$USERNAME_NEW" "$USER_PASS" | arch-chroot /mnt chpasswd
360if [[ -n "$ROOT_PASS" ]]; then
361 printf 'root:%s\n' "$ROOT_PASS" | arch-chroot /mnt chpasswd
362else
363 arch-chroot /mnt passwd -l root
364fi
365echo '%wheel ALL=(ALL:ALL) ALL' > /mnt/etc/sudoers.d/10-wheel
366chmod 0440 /mnt/etc/sudoers.d/10-wheel
367arch-chroot /mnt visudo -cf /etc/sudoers.d/10-wheel
368
369step "GRUB"
370arch-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
373g=/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"
377if ((GRUB_HIDDEN)); then
378 sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/; s/^#\?GRUB_TIMEOUT_STYLE=.*/GRUB_TIMEOUT_STYLE=hidden/' "$g"
379fi
380((OS_PROBER)) && sed -i 's/^#\?GRUB_DISABLE_OS_PROBER=.*/GRUB_DISABLE_OS_PROBER=false/' "$g"
381arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg
382
383step "Services"
384arch-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
387for 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]
399id=$ssid
400uuid=$(cat /proc/sys/kernel/random/uuid)
401type=wifi
402
403[wifi]
404mode=infrastructure
405ssid=$ssid
406
407[wifi-security]
408key-mgmt=wpa-psk
409psk=$key
410
411[ipv4]
412method=auto
413
414[ipv6]
415method=auto
416EOF
417 chmod 0600 "$conn"
418 log INFO "Wi-Fi \"$ssid\" carried over"
419done
420
421step "Copying aurora to /home/$USERNAME_NEW/aurora"
422cp -a "$currentDir" "/mnt/home/$USERNAME_NEW/aurora"
423arch-chroot /mnt chown -R "$USERNAME_NEW:$USERNAME_NEW" "/home/$USERNAME_NEW/aurora"
424
425DESKTOP_OK=1
426if [[ "$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
441fi
442
443step "Finishing"
444trap - ERR
445set +Eeuo pipefail
446exec 1>&3 2>&4 3>&- 4>&-
447sleep 1 # let tee write the last lines
448install -m 0600 "$LOG" /mnt/var/log/aurora-install.log
449umount -R /mnt
450((LUKS)) && cryptsetup close "$CRYPT_NAME"
451
452if ((DESKTOP_OK)); then
453 gum style --border rounded --border-foreground 42 --padding "1 2" --bold \
454"✔ Arch$([[ "$MODE" == full ]] && echo " + aurora") installed.
455
456Remove the install medium and reboot.$([[ "$MODE" == full ]] && echo "
457Log in on tty1 as $USERNAME_NEW: Hyprland and aurora start by themselves." || echo "
458For the desktop, log in and run: ~/aurora/install.sh --desktop")
459The install log is in /var/log/aurora-install.log."
460else
461 gum style --border rounded --border-foreground 220 --padding "1 2" --bold \
462"⚠ Arch is installed, the desktop part failed.
463
464Reboot, log in as $USERNAME_NEW and run: ~/aurora/install.sh --desktop
465The install log is in /var/log/aurora-install.log."
466fi
467
468gum confirm "Reboot now?" && reboot
469exit 0