Recently Written · git

waybase

Bootable installer: KISS, Gentoo, Arch or Alpine + Nix + a Wayland compositor (sway, river, labwc, dwl)

git clone https://github.com/equwal/waybase

Log | Files | Refs


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."