diff --git a/pi4/build/README.md b/pi4/build/README.md index 3453ca7..b2b1849 100644 --- a/pi4/build/README.md +++ b/pi4/build/README.md @@ -1,6 +1,6 @@ # Stormux Pi Image Builder -Stormux provides an Arch-based Raspberry Pi image that already includes Fenrir, Orca, and other screen-reader friendly defaults. This repository hosts the scripts maintained by the Stormux community (originally crafted by Storm) so anyone can rebuild the image, customize the overlay, and share accessible Pi spins. +Stormux provides an Arch-based Raspberry Pi image that already includes Fenrir, Cthulhu, I38, and other screen-reader friendly defaults. This repository hosts the scripts maintained by the Stormux community (originally crafted by Storm) so anyone can rebuild the image, customize the overlay, and share accessible Pi spins. ## Purpose & Scope This repo captures the complete build pipeline for the Stormux Raspberry Pi images: bootstrapping Arch Linux ARM, layering the accessible defaults, installing Fenrir and helper tools, and producing a compressed ready-to-flash image. Running the script yourself lets you reproduce the same system the project ships for Raspberry Pi 4/5-class systems, tweak it, and publish new spins without reverse-engineering the original release. @@ -9,7 +9,7 @@ This repo captures the complete build pipeline for the Stormux Raspberry Pi imag - A 64-bit Arch Linux host (bare metal or VM) with at least 20 GB free disk space. - Root access on that host. The builder partitions loopback devices and must run as root. - Required packages: `arch-install-scripts`, `dosfstools`, `parted`, `qemu-user-static`, `qemu-user-static-binfmt`, plus standard developer tools that Arch already ships. -- A Raspberry Pi 4, Raspberry Pi 400, Raspberry Pi 5, or compatible board, plus a microSD card (8 GB or larger recommended). +- A Raspberry Pi 4, Raspberry Pi 400, Raspberry Pi 5, or compatible board, plus a microSD card (16 GB or larger recommended). Install dependencies on the build host with: ```bash @@ -31,13 +31,13 @@ The script is designed for an x86_64 Arch host so it can emulate ARM binaries wi 1. Become root (`sudo -i`) so the script can create loop devices and mount them. 2. From `/path/to/stormux`, run: ```bash - sudo ./pi4/build/build-stormux.sh -l en_US -s 6 + sudo ./pi4/build/build-stormux.sh -l en_US -s 8 ``` - The builder creates an aarch64 image for Raspberry Pi 4/5-class systems. - `-l en_US` selects the locale (use `es_ES`, `fr_FR`, etc.). - - `-s 6` sets the image size in gigabytes. + - `-s 8` sets the image size in gigabytes. - Add `-n my-stormux.img` to override the default filename. -3. Grab coffee. The script bootstraps Arch Linux ARM, provisions packages (Fenrir, Orca, NetworkManager, PipeWire, etc.), copies the overlay from `pi4/files/`, installs the latest `sas`, cleans up, compresses the image, and writes a SHA-1 checksum. +3. Grab coffee. The script bootstraps Arch Linux ARM, provisions packages (Fenrir, Cthulhu, I38, XLibre, NetworkManager, PipeWire, etc.), copies the overlay from `pi4/files/`, installs the latest `sas`, cleans up, compresses the image, and writes a SHA-1 checksum. 4. When the `build-stormux.sh` command returns, you should have an `.img.xz` file and matching `.sha1sum` in your working directory (for example `stormux-rpi4-5-aarch64-YYYY-MM-DD.img.xz`). If the build is interrupted, run `sudo umount -R /mnt && sudo losetup -D` to be sure nothing is still mounted. diff --git a/pi4/build/build-stormux.sh b/pi4/build/build-stormux.sh index a2991d2..872d3e2 100755 --- a/pi4/build/build-stormux.sh +++ b/pi4/build/build-stormux.sh @@ -68,6 +68,14 @@ cleanup_image() { return "$cleanupStatus" } +# shellcheck disable=SC2329 # validate_image_filesystems is invoked from finish_build +validate_image_filesystems() { + local rootPartition="${loopdev}p2" + + echo "Checking root filesystem..." + e2fsck -f -n "$rootPartition" +} + # shellcheck disable=SC2329 # cleanup is invoked via trap EXIT cleanup() { local status=$? # capture original exit status so failures propagate @@ -86,6 +94,19 @@ cleanup() { } finish_build() { + if [[ $mounted -eq 0 ]]; then + if ! umount -R /mnt; then + echo "Image build commands completed, but unmounting failed." + exit 1 + fi + mounted=1 + fi + + if ! validate_image_filesystems; then + echo "Image filesystem validation failed." + exit 1 + fi + trap - EXIT if ! cleanup_image; then @@ -148,7 +169,7 @@ declare -A command=( [h]="This help screen." [l:]="Language default is en_US." [n:]="Image name, default is stormux-rpi4-5-aarch64-.img" - [s:]="image size in GB, default is 6." + [s:]="image size in GB, default is 8." ) # Convert the keys of the associative array to a format usable by getopts @@ -178,7 +199,7 @@ while getopts "${args}" i ; do done # make sure variables are set, or use defaults. -export imageSize="${imageSize:-6G}" +export imageSize="${imageSize:-8G}" imageName="${imageName:-stormux-rpi4-5-aarch64-$(date '+%Y-%m-%d').img}" export imageName export imageLanguage="${imageLanguage:-en_US.UTF-8}" @@ -362,6 +383,7 @@ packages=( gstreamer gst-plugins-base gst-plugins-good + i38 ii # Keep Pi onboard firmware plus common USB/network chipset firmware without # pulling in unrelated desktop/server GPU firmware. @@ -373,6 +395,7 @@ packages=( man man-pages networkmanager + nodm-dgw openssh parted pipewire @@ -382,10 +405,13 @@ packages=( poppler python-dbus python-gobject + python-msgpack python-pyenchant python-pyte python-pyperclip + python-tornado raspberrypi-utils + cthulhu socat realtime-privileges rhvoice-voice-bdl @@ -401,6 +427,11 @@ packages=( vi xdg-user-dirs xdg-utils + xlibre-input-libinput + xlibre-video-dummy-with-vt + xlibre-video-fbdev + xlibre-xserver + xorg-xinit yay ) @@ -413,7 +444,7 @@ install_sas # See: https://archlinuxarm.org/forum/viewtopic.php?t=16672 if [[ -f /etc/mkinitcpio.d/linux-rpi.preset ]]; then echo "Configuring mkinitcpio preset to skip kms hook..." - sed -i "s/^default_options=.*/default_options=\"--skiphook kms\"/" /etc/mkinitcpio.d/linux-rpi.preset + sed -i "s/^default_options=.*/default_options=\"-S autodetect --skiphook kms\"/" /etc/mkinitcpio.d/linux-rpi.preset sed -i "s/^fallback_options=.*/fallback_options=\"-S autodetect --skiphook kms\"/" /etc/mkinitcpio.d/linux-rpi.preset fi @@ -435,8 +466,10 @@ echo 'Stormux \r (\l)' > /etc/issue echo >> /etc/issue # Create the stormux user useradd -m -g users -G wheel,realtime,audio,video,network,brlapi -s /bin/bash stormux -# Grant sudo privileges to the stormux user for package installation. -echo 'stormux ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/wheel +# Grant normal sudo privileges plus temporary first-boot passwordless sudo. +echo '%wheel ALL=(ALL) ALL' > /etc/sudoers.d/wheel +echo 'stormux ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/zz-stormux-first-boot +chmod 440 /etc/sudoers.d/wheel /etc/sudoers.d/zz-stormux-first-boot # Set the password for the root user echo -e "root\nroot" | passwd "root" # Set the password for the stormux user @@ -449,13 +482,39 @@ git config --global init.defaultBranch master # Create desktop, downloads, music, and other directories. xdg-user-dirs-update exit +mkdir -p /home/stormux/git +chown stormux:users /home/stormux/git +if ! sudo -u stormux git clone --depth 1 https://git.stormux.org/storm/I38 /home/stormux/git/I38; then + echo "Failed to clone I38." + exit 1 +fi +cd /home/stormux/git/I38 +sudo -u stormux ./i38.sh -D +cd / +if [[ -f /etc/nodm.conf ]]; then + sed -i \ + -e "s/{user}/stormux/g" \ + -e "s#^NODM_USER=.*#NODM_USER='stormux'#" \ + -e "s#^NODM_XSESSION=.*#NODM_XSESSION='/home/stormux/.xinitrc'#" \ + /etc/nodm.conf +fi +if command -v spd-conf > /dev/null 2>&1; then + spd-conf -n -C || echo "Warning: Speech Dispatcher system configuration failed." +fi +if [[ -f /etc/speech-dispatcher/speechd.conf ]]; then + if grep -qE '^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+' /etc/speech-dispatcher/speechd.conf; then + sed -i -E 's/^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+.*/DefaultModule rhvoice/' /etc/speech-dispatcher/speechd.conf + else + printf '\nDefaultModule rhvoice\n' >> /etc/speech-dispatcher/speechd.conf + fi +fi # Enable linger so that hopefully sound will start at login. mkdir -p /var/lib/systemd/linger touch /var/lib/systemd/linger/stormux systemctl --global enable pipewire.service pipewire-pulse.service /usr/share/fenrirscreenreader/tools/configure_pipewire.sh sudo -u stormux /usr/share/fenrirscreenreader/tools/configure_pipewire.sh -# Configure sudo for group wheel, remove nopasswd for the stormux user +# Ensure normal sudo remains configured after first-boot cleanup removes NOPASSWD. echo '%wheel ALL=(ALL) ALL' > /etc/sudoers.d/wheel # Set the hostname echo stormux > /etc/hostname @@ -466,7 +525,6 @@ services=( brltty.path cronie.service fake-hwclock.service - fenrirscreenreader.service log-to-ram-setup.service log-to-ram-sync.timer log-to-ram-shutdown.service diff --git a/pi4/files/etc/profile.d/stormux_first_boot.sh b/pi4/files/etc/profile.d/stormux_first_boot.sh index 843df65..d43b470 100755 --- a/pi4/files/etc/profile.d/stormux_first_boot.sh +++ b/pi4/files/etc/profile.d/stormux_first_boot.sh @@ -12,6 +12,19 @@ if ! [[ -x /usr/local/bin/configure-stormux ]]; then return fi +firstBootSudoersFile="/etc/sudoers.d/zz-stormux-first-boot" +firstBootAutologinFile="/etc/systemd/system/getty@tty1.service.d/stormux-first-boot-autologin.conf" +firstBootAutologinDir="${firstBootAutologinFile%/*}" + +cleanup_first_boot_access() { + trap - RETURN HUP INT TERM + if [[ -e "$firstBootSudoersFile" || -e "$firstBootAutologinFile" ]]; then + sudo -n /bin/sh -c "rm -f '$firstBootSudoersFile' '$firstBootAutologinFile'; rmdir --ignore-fail-on-non-empty '$firstBootAutologinDir'; systemctl daemon-reload" 2>/dev/null || true + fi +} + +trap cleanup_first_boot_access RETURN HUP INT TERM + # Volume calibration FIRST echo "Setting up audio volume..." volume=50 @@ -32,7 +45,7 @@ while [[ $volume -le 130 ]]; do clear pactl set-sink-volume @DEFAULT_SINK@ "${volume}%" 2>/dev/null spd-say "If this is loud enough, press enter." - if read -t4 ; then + if read -r -t4 ; then echo "Volume set to ${volume}%" break else @@ -47,16 +60,15 @@ if [[ -x /etc/audibleprompt.sh ]]; then export sudoFlags=("-A") fi +echo +echo "Starting Fenrir..." +spd-say "Starting Fenrir." +sudo "${sudoFlags[@]}" systemctl --quiet enable --now fenrirscreenreader.service + clear cat << "EOF" Hello, and welcome to Stormux! -Let's get you set up. After you press enter, you will be prompted for the sudo password. -When that happens, type the word stormux and press enter. -You will not receive any speech feedback for this process. -That is completely normal, and speech will return after you have typed the password. -Once again, the password is stormux in all lower case letters. - Please press enter to continue. EOF read -r diff --git a/pi4/files/etc/systemd/system/getty@tty1.service.d/stormux-first-boot-autologin.conf b/pi4/files/etc/systemd/system/getty@tty1.service.d/stormux-first-boot-autologin.conf new file mode 100644 index 0000000..a607888 --- /dev/null +++ b/pi4/files/etc/systemd/system/getty@tty1.service.d/stormux-first-boot-autologin.conf @@ -0,0 +1,3 @@ +[Service] +ExecStart= +ExecStart=-/usr/bin/agetty --autologin stormux --noclear %I $TERM diff --git a/pi4/files/usr/local/bin/configure-stormux b/pi4/files/usr/local/bin/configure-stormux index 841b1ce..0941085 100755 --- a/pi4/files/usr/local/bin/configure-stormux +++ b/pi4/files/usr/local/bin/configure-stormux @@ -28,6 +28,81 @@ fi # Volume calibration is now handled in stormux_first_boot.sh export DIALOGOPTS='--insecure --no-lines --visit-items' +i38StartAtBoot=0 +firstBootSudoersFile="/etc/sudoers.d/zz-stormux-first-boot" +firstBootAutologinFile="/etc/systemd/system/getty@tty1.service.d/stormux-first-boot-autologin.conf" +firstBootAutologinDir="${firstBootAutologinFile%/*}" + +read_yes_no() { + local answer + while true; do + echo "$1" + echo "Press y for yes or n for no followed by enter." + read -r answer + answer="${answer::1}" + case "${answer,,}" in + y) + return 0 + ;; + n) + return 1 + ;; + esac + echo "Please answer y or n." + done +} + +cleanup_first_boot_access() { + rm -f "$firstBootSudoersFile" "$firstBootAutologinFile" + rmdir --ignore-fail-on-non-empty "$firstBootAutologinDir" + systemctl daemon-reload +} + +configure_physical_screen() { + sudo "${sudoFlags[@]}" mkdir -p /etc/X11/xorg.conf.d + sudo "${sudoFlags[@]}" tee /etc/X11/xorg.conf.d/10-screendriver.conf > /dev/null << 'EOF' +Section "OutputClass" + Identifier "vc4" + MatchDriver "vc4" + Driver "modesetting" + Option "PrimaryGPU" "true" +EndSection +EOF +} + +configure_headless_screen() { + sudo "${sudoFlags[@]}" mkdir -p /etc/X11/xorg.conf.d + sudo "${sudoFlags[@]}" tee /etc/X11/xorg.conf.d/10-screendriver.conf > /dev/null << 'EOF' +Section "Monitor" + Identifier "dummy_monitor" + HorizSync 28.0-80.0 + VertRefresh 48.0-75.0 + Modeline "1920x1080" 172.80 1920 2040 2248 2576 1080 1081 1084 1118 +EndSection + +Section "Device" + Identifier "dummy_card" + Driver "dummy" + VideoRam 256000 + Option "UseFBDev" "false" +EndSection + +Section "Screen" + Identifier "dummy_screen" + Device "dummy_card" + Monitor "dummy_monitor" + SubSection "Display" + Depth 24 + Modes "1920x1080" + EndSubSection +EndSection + +Section "ServerLayout" + Identifier "dummy_layout" + Screen 0 "dummy_screen" +EndSection +EOF +} set_timezone() { # Get the list of timezones @@ -58,26 +133,40 @@ set_timezone() { timedatectl set-ntp true } # Offer to switch fenrir layout. -echo "Would you like to switch Fenrir to laptop layout?" -echo "Press y for yes or n for no followed by enter." -read -r continue -continue="${continue::1}" -if [[ "${continue,}" == "y" ]];then +if read_yes_no "Would you like to switch Fenrir to laptop layout?"; then sed -i 's/=desktop/=laptop/' /etc/fenrirscreenreader/settings/settings.conf clear systemctl restart fenrirscreenreader.service fi +if command -v i3 > /dev/null 2>&1 && systemctl list-unit-files nodm.service > /dev/null 2>&1; then + if read_yes_no "Will this Pi be connected to a physical screen?"; then + configure_physical_screen + else + configure_headless_screen + fi + if read_yes_no "Should I38 start at boot?"; then + sudo "${sudoFlags[@]}" systemctl --quiet enable nodm.service + i38StartAtBoot=1 + if read_yes_no "Should Fenrir run in the console at boot?"; then + sudo "${sudoFlags[@]}" systemctl --quiet enable fenrirscreenreader.service + else + sudo "${sudoFlags[@]}" systemctl --quiet disable fenrirscreenreader.service + fi + else + sudo "${sudoFlags[@]}" systemctl --quiet disable nodm.service + sudo "${sudoFlags[@]}" systemctl --quiet enable fenrirscreenreader.service + fi +fi + # Check for possible resize diskSource="$(df --output='source' / | tail -1)" diskSize="$(df -h --output='size' / | tail -1 | tr -cd '[:digit:].')" diskSize=${diskSize%.*} -if [[ $diskSize -le 7 ]]; then - echo "$diskSource is only $diskSize gigs, which means it probably needs to be resized. Would you like to do this now?" - echo "Press y for yes or n for no followed by enter." - read -r continue - continue="${continue::1}" - if [[ "${continue,}" == "y" ]];then +resizeThreshold=9 +if [[ $diskSize -le $resizeThreshold ]]; then + if read_yes_no "$diskSource is only $diskSize gigs, which means it probably needs to be resized. Would you like to do this now?"; then + resizeLog="/tmp/stormux-resize.log" # Extract base device name (handles mmcblk0p2, nvme0n1p2, sda2, etc.) if [[ "$diskSource" =~ (.*[0-9]+)p[0-9]+$ ]]; then # Handle mmcblk0p2, nvme0n1p2 style @@ -87,14 +176,19 @@ if [[ $diskSize -le 7 ]]; then # shellcheck disable=SC2001 diskDevice="$(echo "$diskSource" | sed 's/[0-9]*$//')" fi - echo "Yes" | sudo "${sudoFlags[@]}" parted ---pretend-input-tty "$diskDevice" resizepart 2 100% - sudo "${sudoFlags[@]}" resize2fs -f "$diskSource" + echo "Resizing the filesystem. This may take a few minutes." + if { echo "Yes" | sudo "${sudoFlags[@]}" parted ---pretend-input-tty "$diskDevice" resizepart 2 100%; } > "$resizeLog" 2>&1 \ + && { sudo "${sudoFlags[@]}" resize2fs -f "$diskSource"; } >> "$resizeLog" 2>&1; then + echo "Resize complete." + else + echo "Resize failed. Details are in $resizeLog." + fi fi fi if ! ping -c1 stormux.org &> /dev/null ; then echo "No internet connection detected. Press enter to open NetworkManager." - read -r continue + read -r _ echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock nmtui-connect echo "setting set focus#highlight=False" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock @@ -112,7 +206,7 @@ if ping -qc1 -W 1 stormux.org &> /dev/null; then read -rp "Would you like the time to sync as soon as the Pi connects to the internet? " answer answer="${answer:0:1}" if [[ "${answer,,}" == "y" ]]; then - systemctl enable time_sync_at_boot.service + systemctl --quiet enable time_sync_at_boot.service else echo "Time sync at boot skipped." echo "If you change your mind later, simply type:" @@ -141,5 +235,15 @@ echo "or you may configure your system manually." echo echo "Thank you for choosing Stormux." +echo +read -rp "Press enter to continue." + +cleanup_first_boot_access + +if [[ $i38StartAtBoot -eq 1 ]]; then + echo + echo "Starting I38..." + sudo "${sudoFlags[@]}" systemctl --quiet start nodm.service +fi exit 0 diff --git a/pi4/gui.md b/pi4/gui.md new file mode 100644 index 0000000..f75520b --- /dev/null +++ b/pi4/gui.md @@ -0,0 +1,26 @@ +# Ship I38 By Default On Pi Stormux + +## Planned + +- [ ] Build and boot-test a Pi image with the new GUI defaults. +- [ ] Port the proven Pi behavior to the x86_64 installer/profile after the Pi image is verified. + +## In Progress + +- No active implementation items. + +## Done + +- [x] Bump the default Pi image size from 6 GB to 8 GB. +- [x] Raise the first-boot resize prompt threshold to 9 GB. +- [x] Configure Speech Dispatcher during image creation when `spd-conf` is available. +- [x] Add I38, Cthulhu, XLibre, nodm, Speech Dispatcher, and startx support to the Pi image. +- [x] Generate the default user's I38 config during image creation with `i38.sh -D`. +- [x] Ask during first boot whether I38 should start at boot, and whether Fenrir should stay enabled for the console. +- [x] Convert the external `configure-stormux` I38 action into a removal/purge action. + +## Deferred x86_64 Follow-up + +- [ ] Replace interactive I38 setup in the x86_64 installer with the same non-interactive `i38.sh -D` path where appropriate. +- [ ] Review x86_64 package lists against the Pi package set after the Pi build is proven. +- [ ] Re-check nodm and Fenrir service policy for installed x86_64 systems.