install.sh (14227 bytes)
1 #!/bin/sh 2 # waybase installer: install KISS, Gentoo, Arch or Alpine with Nix and a 3 # Wayland compositor onto a blank disk. Runs as root from the waybase live 4 # image (Alpine), with network: sh /media/*/waybase/install.sh [ANSWERS] 5 # 6 # ANSWERS is an optional shell file that presets any of the variables below 7 # (see answers.example). Unset variables are asked for interactively. 8 # BASE kiss | gentoo | arch | alpine 9 # COMPOSITOR sway | river (river-classic 0.3) | labwc | dwl (a list installs several; the 10 # first one starts on tty1 login) 11 # DISK target disk, for example /dev/sda. ALL DATA ON IT IS ERASED. 12 # ENCRYPT yes | no (LUKS2 on the root partition) 13 # LUKS_PASS, USERNAME, USER_PASS, ROOT_PASS, HOSTNAME_NEW 14 # YES=1 skip the "type the disk name again" check 15 # SERIAL=1 login getty on ttyS0 and kernel console there (for tests) 16 # NIXPKGS flake ref for Nix-supplied packages (default: nixpkgs) 17 # FIRMWARE=no skip linux-firmware (VMs; real hardware usually needs it) 18 # FIRMWARE_TAG, FIRMWARE_LIST install the pinned files of LIST (SHA256 PATH lines, see firmware.sh) 19 # from linux-firmware tag FIRMWARE_TAG into /lib/firmware 20 set -eu 21 D=$(cd "$(dirname "$0")" && pwd) 22 M=/mnt/waybase 23 BASES="kiss gentoo arch alpine" 24 COMPOSITORS="sway river labwc dwl" 25 # shellcheck disable=SC1090 26 [ -n "${1:-}" ] && . "$(cd "$(dirname "$1")" && pwd)/${1##*/}" 27 28 log() { printf '\n=== %s\n' "$*"; } 29 die() { echo "ERROR: $*" >&2; exit 1; } 30 ask() { # ask VAR "prompt" [secret]: read VAR unless the answers file set it 31 eval "[ -n \"\${$1:-}\" ]" && return 0 32 printf '%s: ' "$2" 33 [ -n "${3:-}" ] && stty -echo 34 read -r v 35 [ -n "${3:-}" ] && { stty echo; echo; } 36 eval "$1=\$v" 37 } 38 menu() { # menu VAR "prompt" choice...: numbered menu unless VAR is set 39 eval "[ -n \"\${$1:-}\" ]" && return 0 40 var=$1 prompt=$2; shift 2 41 i=0; for o; do i=$((i + 1)); echo " $i) $o"; done 42 while :; do 43 printf '%s [1-%d]: ' "$prompt" $#; read -r n 44 case $n in '' | *[!0-9]*) continue ;; esac 45 [ "$n" -ge 1 ] && [ "$n" -le $# ] && break 46 done 47 eval "$var=\${$n}" 48 } 49 in_list() { case " $2 " in *" $1 "*) return 0 ;; esac; return 1; } 50 ch() { chroot $M /bin/sh -c "$*"; } 51 fetch() { # fetch URL: download into $M/tmp with retries (resumes), print the path 52 mkdir -p $M/tmp; f=$M/tmp/${1##*/} 53 curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -C - -o "$f" "$1" || curl -fsSL --retry 5 -o "$f" "$1" 54 echo "$f" 55 } 56 57 # --- questions ----------------------------------------------------------- 58 menu BASE "base system" $BASES 59 in_list "$BASE" "$BASES" || die "unknown BASE: $BASE" 60 menu COMPOSITOR "compositor" $COMPOSITORS 61 for c in $COMPOSITOR; do in_list "$c" "$COMPOSITORS" || die "unknown compositor: $c"; done 62 lsblk -dno NAME,SIZE,MODEL,TRAN | grep -v '^loop' || true 63 ask DISK "target disk (for example /dev/sda) - ALL DATA ON IT IS ERASED" 64 [ -b "$DISK" ] || die "not a block device: $DISK" 65 grep -q "^$DISK" /proc/mounts && die "$DISK is mounted (is it the boot media?)" 66 if [ "${YES:-}" != 1 ]; then 67 printf 'Type %s again to erase it: ' "$DISK"; read -r again 68 [ "$again" = "$DISK" ] || die aborted 69 fi 70 menu ENCRYPT "encrypt the root partition with LUKS2" yes no 71 if [ "$ENCRYPT" = yes ]; then ask LUKS_PASS "LUKS passphrase" s; fi 72 ask USERNAME "user name" 73 echo "$USERNAME" | grep -Eq '^[a-z_][a-z0-9_-]{0,31}$' || die "bad user name: $USERNAME" 74 ask USER_PASS "password for $USERNAME" s 75 ask ROOT_PASS "password for root" s 76 HOSTNAME_NEW=${HOSTNAME_NEW:-waybase} 77 NIXPKGS=${NIXPKGS:-nixpkgs} 78 if [ "${SERIAL:-}" = 1 ]; then CONSOLE="console=tty0 console=ttyS0,115200"; else CONSOLE=""; fi 79 80 # --- live tools ---------------------------------------------------------- 81 log "live tools" 82 if ! ip route | grep -q '^default'; then 83 for i in /sys/class/net/e*; do 84 i=${i##*/}; ip link set "$i" up && timeout 30 udhcpc -q -n -i "$i" >/dev/null 2>&1 && break 85 done 86 fi 87 ip route | grep -q '^default' || die "no network (plug in Ethernet, or run setup-interfaces)" 88 V=$(cut -d. -f1,2 /etc/alpine-release) 89 grep -q '^http' /etc/apk/repositories || 90 printf 'https://dl-cdn.alpinelinux.org/alpine/v%s/%s\n' "$V" main "$V" community >> /etc/apk/repositories 91 apk update -q 92 apk add -q sfdisk wipefs e2fsprogs cryptsetup dosfstools lsblk blkid grub grub-efi grub-bios \ 93 curl tar xz zstd mkinitfs util-linux-misc ca-certificates 94 modprobe -a ext4 vfat nls_cp437 nls_iso8859-1 dm_crypt 2>/dev/null || true 95 96 # --- disk ---------------------------------------------------------------- 97 # GPT: BIOS boot, ESP, /boot (ext4, unencrypted so GRUB needs no LUKS 98 # support), root (ext4, optionally on LUKS2). 99 log "partition $DISK" 100 case $DISK in *[0-9]) P=${DISK}p ;; *) P=$DISK ;; esac 101 wipefs -aq "$DISK" 102 sfdisk -q "$DISK" <<EOF 103 label: gpt 104 size=1MiB, type=21686148-6449-6E6F-744E-656564454649, name=bios 105 size=512MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name=esp 106 size=1GiB, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=boot 107 type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=root 108 EOF 109 mdev -s 2>/dev/null || true; sleep 1 110 mkfs.vfat -F32 -n ESP "${P}2" >/dev/null 111 mkfs.ext4 -qF -L boot "${P}3" 112 ROOTDEV=${P}4 113 if [ "$ENCRYPT" = yes ]; then 114 printf %s "$LUKS_PASS" | cryptsetup -q luksFormat --type luks2 "${P}4" - 115 printf %s "$LUKS_PASS" | cryptsetup open "${P}4" root - 116 ROOTDEV=/dev/mapper/root 117 fi 118 mkfs.ext4 -qF -L root "$ROOTDEV" 119 mkdir -p $M && mount "$ROOTDEV" $M 120 mkdir -p $M/boot && mount "${P}3" $M/boot 121 mkdir -p $M/boot/efi && mount "${P}2" $M/boot/efi 122 UB=$(blkid -s UUID -o value "${P}3") UE=$(blkid -s UUID -o value "${P}2") 123 UL=$(blkid -s UUID -o value "${P}4") UR=$(blkid -s UUID -o value "$ROOTDEV") 124 125 chroot_mounts() { 126 mkdir -p $M/proc $M/sys $M/dev $M/run $M/tmp 127 for f in proc sys dev; do mount --rbind /$f $M/$f; done 128 mount -t tmpfs tmpfs $M/run 129 rm -f $M/etc/resolv.conf; cp /etc/resolv.conf $M/etc/resolv.conf 130 } 131 132 # --- base system --------------------------------------------------------- 133 # Each backend defines: 134 # bootstrap base system in $M (calls chroot_mounts), init, seat, network 135 # pkg NAME native package(s) for a logical name; empty = take it from Nix 136 # pkg_install P.. install native packages; nonzero = fall back to Nix 137 # extra_pkgs logical names this base also needs (from Nix when not native) 138 # user_add USER, serial_getty, nix_service, boot_cmd CMD (run CMD at boot) 139 # nix_native install Nix from the base's own packages; nonzero = use the 140 # release tarball 141 # kernel kernel + initramfs; sets KERNEL, INITRD (relative to /boot), 142 # KARGS, CRYPTARGS 143 # shellcheck disable=SC1090 144 . "$D/backends/$BASE.sh" 145 log "bootstrap $BASE" 146 bootstrap 147 echo "$HOSTNAME_NEW" > $M/etc/hostname 148 cat > $M/etc/fstab <<EOF 149 UUID=$UR / ext4 defaults,noatime 0 1 150 UUID=$UB /boot ext4 defaults,noatime 0 2 151 UUID=$UE /boot/efi vfat defaults 0 2 152 EOF 153 154 log "user $USERNAME" 155 user_add "$USERNAME" 156 printf 'root:%s\n%s:%s\n' "$ROOT_PASS" "$USERNAME" "$USER_PASS" | ch chpasswd 157 [ "${SERIAL:-}" = 1 ] && serial_getty 158 159 # --- Nix ----------------------------------------------------------------- 160 log "Nix (multi-user daemon)" 161 mkdir -p $M/etc/nix 162 printf 'experimental-features = nix-command flakes\nbuild-users-group = nixbld\n' > $M/etc/nix/nix.conf 163 if ! nix_native; then 164 # Official release tarball, registered by hand (what its installer does, 165 # without needing bash or useradd, which KISS lacks). 166 NV=$(curl -fsSL https://nixos.org/nix/install | grep -o 'releases.nixos.org/nix/nix-[0-9.]*[0-9]' | head -1) 167 NV=${NV##*/nix-} 168 T=/tmp/nix-$NV-x86_64-linux 169 f=$(fetch "https://releases.nixos.org/nix/nix-$NV/nix-$NV-x86_64-linux.tar.xz") && tar -xJf "$f" -C $M/tmp && rm "$f" 170 # daemon-socket must exist: nix-daemon.socket has ConditionPathIsReadWrite on it 171 mkdir -p $M/nix/store $M/nix/var/nix/profiles/per-user $M/nix/var/nix/gcroots $M/nix/var/nix/daemon-socket 172 mv $M$T/store/* $M/nix/store/ 173 NIX=$(cd $M/nix/store && ls -d ./*-nix-"$NV" | head -1); NIX=/nix/store/${NIX#./} 174 CA=$(cd $M/nix/store && ls -d ./*-nss-cacert-* | head -1); CA=/nix/store/${CA#./} 175 if ! grep -q '^nixbld:' $M/etc/group; then 176 members="" 177 i=1; while [ $i -le 32 ]; do 178 members="$members${members:+,}nixbld$i" 179 echo "nixbld$i:x:$((30000 + i)):30000:Nix build user $i:/var/empty:/sbin/nologin" >> $M/etc/passwd 180 echo "nixbld$i:!:19000:0:99999:7:::" >> $M/etc/shadow 181 i=$((i + 1)) 182 done 183 echo "nixbld:x:30000:$members" >> $M/etc/group 184 fi 185 ch "$NIX/bin/nix-store --load-db < $T/.reginfo && $NIX/bin/nix-env -p /nix/var/nix/profiles/default -i $NIX $CA" >/dev/null 186 rm -rf "$M$T" 187 ch "chgrp nixbld /nix/store && chmod 1775 /nix/store" 188 fi 189 nix_service 190 NIXP=/nix/var/nix/profiles/waybase 191 cat > $M/etc/profile.d/waybase.sh <<EOF 192 # waybase: Nix in PATH, Nix-supplied programs, compositor on tty1 login. 193 for f in /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh /etc/profile.d/nix-daemon.sh.nix; do 194 [ -r "\$f" ] && . "\$f" && break 195 done 196 export PATH="$HOME/.nix-profile/bin:$NIXP/bin:/nix/var/nix/profiles/default/bin:$PATH" 197 export XDG_DATA_DIRS="$NIXP/share:\${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" 198 if [ "\$(id -u)" -ne 0 ] && [ -z "\${WAYLAND_DISPLAY:-}" ] && [ "\$(tty)" = /dev/tty1 ]; then 199 exec waybase-session 200 fi 201 EOF 202 203 # --- compositor, terminal, launcher, audio ------------------------------ 204 log "Wayland session: $COMPOSITOR" 205 want="$COMPOSITOR foot fuzzel pipewire wireplumber" 206 in_list dwl "$COMPOSITOR" && want="$want wmenu" 207 fromnix="" 208 for p in $want $(extra_pkgs); do 209 n=$(pkg "$p") 210 if [ -n "$n" ] && pkg_install "$n"; then echo "native: $p ($n)"; else fromnix="$fromnix $p"; fi 211 done 212 [ -e $M/etc/fonts/fonts.conf ] || fromnix="$fromnix dejavu_fonts fontconfig" 213 if [ -n "$fromnix" ]; then 214 # Nix GL apps on a foreign distro find Mesa through /run/opengl-driver. 215 fromnix="$fromnix mesa" 216 echo "from Nix:$fromnix" 217 refs="" 218 for p in $fromnix; do 219 case $p in river) p=river-classic ;; esac # river 0.4+ needs a separate WM 220 refs="$refs $NIXPKGS#$p" 221 done 222 ch "f=/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh; [ -r \$f ] && . \$f; nix profile install --profile $NIXP $refs" 223 [ -e $M/etc/fonts/fonts.conf ] || { mkdir -p $M/etc/fonts && cat > $M/etc/fonts/fonts.conf <<EOF 224 <?xml version="1.0"?> 225 <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd"> 226 <fontconfig><dir>$NIXP/share/fonts</dir><cachedir>/var/cache/fontconfig</cachedir></fontconfig> 227 EOF 228 } 229 boot_cmd "ln -sfn $NIXP /run/opengl-driver" 230 # Nix's dbus reads /etc/dbus-1/session.conf, and the store copy includes it back: drop that line. 231 case " $fromnix " in *" dbus "*) mkdir -p $M/etc/dbus-1; ch "sed '/etc.dbus-1.session.conf/d' $NIXP/share/dbus-1/session.conf > /etc/dbus-1/session.conf" ;; esac 232 fi 233 echo "$fromnix" > $M/etc/waybase-from-nix 234 set -- $COMPOSITOR 235 mkdir -p $M/usr/local/bin 236 cat > $M/usr/local/bin/waybase-session <<EOF 237 #!/bin/sh 238 # Start the default compositor ($1) with PipeWire. 239 : "\${XDG_RUNTIME_DIR:=/tmp/runtime-\$(id -u)}"; export XDG_RUNTIME_DIR 240 mkdir -p -m 700 "\$XDG_RUNTIME_DIR" 241 if [ ! -d /run/systemd/system ]; then # systemd user units start PipeWire on Arch 242 pipewire & sleep 1; wireplumber & pipewire-pulse & 243 fi 244 command -v dbus-run-session >/dev/null && exec dbus-run-session -- "\${1:-$1}" 245 exec "\${1:-$1}" 246 EOF 247 chmod 755 $M/usr/local/bin/waybase-session 248 249 # Small per-compositor configs: Super+Return = foot, Super+d = fuzzel. 250 H=$M/home/$USERNAME/.config 251 for c in $COMPOSITOR; do case $c in 252 sway) mkdir -p $H/sway && cat > $H/sway/config <<'EOF' 253 set $mod Mod4 254 bindsym $mod+Return exec foot 255 bindsym $mod+d exec fuzzel 256 bindsym $mod+Shift+q kill 257 bindsym $mod+Shift+e exit 258 bindsym $mod+h focus left 259 bindsym $mod+l focus right 260 bindsym $mod+1 workspace number 1 261 bindsym $mod+2 workspace number 2 262 bindsym $mod+Shift+1 move container to workspace number 1 263 bindsym $mod+Shift+2 move container to workspace number 2 264 EOF 265 ;; 266 river) mkdir -p $H/river && cat > $H/river/init <<'EOF' 267 #!/bin/sh 268 riverctl map normal Super Return spawn foot 269 riverctl map normal Super D spawn fuzzel 270 riverctl map normal Super+Shift Q close 271 riverctl map normal Super+Shift E exit 272 riverctl map normal Super J focus-view next 273 riverctl map normal Super K focus-view previous 274 riverctl default-layout rivertile 275 rivertile -view-padding 4 -outer-padding 4 & 276 EOF 277 chmod 755 $H/river/init ;; 278 labwc) mkdir -p $H/labwc && cat > $H/labwc/rc.xml <<'EOF' 279 <?xml version="1.0"?> 280 <labwc_config> 281 <keyboard> 282 <default /> 283 <keybind key="W-Return"><action name="Execute" command="foot" /></keybind> 284 <keybind key="W-d"><action name="Execute" command="fuzzel" /></keybind> 285 </keyboard> 286 </labwc_config> 287 EOF 288 ;; 289 dwl) ;; # dwl is configured at build time: Alt+Shift+Return = foot, Alt+p = wmenu 290 esac; done 291 ch "chown -R $USERNAME:\$(id -g $USERNAME) /home/$USERNAME" 292 293 # --- kernel, initramfs, bootloader -------------------------------------- 294 [ -n "${FIRMWARE_LIST:-}" ] && { log "pinned firmware"; sh "$D/firmware.sh" -r $M "${FIRMWARE_TAG:?FIRMWARE_TAG is required with FIRMWARE_LIST}" "$FIRMWARE_LIST"; } 295 log "kernel + initramfs" 296 kernel 297 if [ "$ENCRYPT" = yes ]; then ROOTARG="root=/dev/mapper/root $CRYPTARGS"; else ROOTARG="root=UUID=$UR"; fi 298 log "GRUB (UEFI removable path + BIOS)" 299 grub-install --target=x86_64-efi --efi-directory=$M/boot/efi --boot-directory=$M/boot --removable --no-nvram >/dev/null 300 grub-install --target=i386-pc --boot-directory=$M/boot "$DISK" >/dev/null 301 cat > $M/boot/grub/grub.cfg <<EOF 302 set timeout=3 303 menuentry "waybase: $BASE" { 304 search --no-floppy --fs-uuid --set=root $UB 305 linux /$KERNEL $ROOTARG rootfstype=ext4 rw $KARGS $CONSOLE 306 initrd /$INITRD 307 } 308 EOF 309 310 log "unmount" 311 sync 312 for p in /proc/[0-9]*; do # daemons left by package scripts (gpg-agent, ...) 313 [ "$(readlink "$p/root" 2>/dev/null)" = $M ] && kill "${p#/proc/}" 2>/dev/null || true 314 done 315 sleep 2 316 umount -l $M/proc $M/sys $M/dev $M/run 2>/dev/null || true 317 umount $M/boot/efi $M/boot $M 318 [ "$ENCRYPT" = yes ] && cryptsetup close root 319 log "DONE: $BASE with $COMPOSITOR on $DISK. Remove the install media and reboot."