From 787e6ace0f05eecf69c3cee16230b5a09b281f67 Mon Sep 17 00:00:00 2001 From: Kunal <16601274+Tekunalogy@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:57:27 -0700 Subject: [PATCH 1/5] ci(image): Build the static IP Raspberry Pi image for releases Moves the static IP Pifile into the repo and builds it with pimod when a release is published, attaching dweOS-staticIP-RPi-image-.img.xz to the release. The image installs that release's release.tar.gz through a new image_install_script.sh instead of downloading the latest release from inside the build. Manual runs can build an image for any release, and PRs touching the Pifile build one against the latest release as a workflow artifact. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pi-image.yml | 75 +++++++++++++++++++ pimod/.gitignore | 4 + pimod/dweOS-static.Pifile | 133 +++++++++++++++++++++++++++++++++ pimod/image_install_script.sh | 55 ++++++++++++++ 4 files changed, 267 insertions(+) create mode 100644 .github/workflows/pi-image.yml create mode 100644 pimod/.gitignore create mode 100644 pimod/dweOS-static.Pifile create mode 100644 pimod/image_install_script.sh diff --git a/.github/workflows/pi-image.yml b/.github/workflows/pi-image.yml new file mode 100644 index 00000000..91212170 --- /dev/null +++ b/.github/workflows/pi-image.yml @@ -0,0 +1,75 @@ +name: Build Raspberry Pi Image +on: + release: + types: [published] + # Check Pifile changes against the latest release (artifact only) + pull_request: + paths: + - "pimod/**" + - ".github/workflows/pi-image.yml" + workflow_dispatch: + inputs: + tag: + description: "Release tag to build the image for (e.g. v0.7.7)" + required: true + upload_to_release: + description: "Attach the image to the release (otherwise it's only kept as a workflow artifact)" + type: boolean + default: false + +permissions: + contents: write # Required to upload release assets + +jobs: + build_static_ip_image: + # Preview builds are published as prereleases, and -blueos tags are Docker-only; + # only real releases get an image + if: >- + github.event_name != 'release' || + (!github.event.release.prerelease && !endsWith(github.event.release.tag_name, '-blueos')) + # Native arm64, so the arm64 image's chroot doesn't run under QEMU emulation + runs-on: ubuntu-24.04-arm + timeout-minutes: 180 + env: + # Empty for pull requests, which use the latest release + TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }} + UPLOAD: ${{ github.event_name == 'release' || inputs.upload_to_release == true }} + GH_TOKEN: ${{ github.token }} + steps: + # Releases build from their own tag; manual runs and PRs use the Pifile from + # the selected branch, so images can be made for releases that predate it + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Download release files" + run: | + if [ -z "$TAG" ]; then + TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName) + echo "TAG=$TAG" >> "$GITHUB_ENV" + fi + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern release.tar.gz --dir pimod + echo "IMAGE_NAME=dweOS-staticIP-RPi-image-$TAG.img.xz" >> "$GITHUB_ENV" + + - name: "Build image" + uses: Nature40/pimod@v0.9.3 + with: + pifile: pimod/dweOS-static.Pifile + + - name: "Compress image" + run: | + sudo chown "$USER" pimod/dweOS-static.img + xz -T0 -9 --stdout pimod/dweOS-static.img > "$IMAGE_NAME" + ls -lh "$IMAGE_NAME" + + - name: "Upload image to release" + if: env.UPLOAD == 'true' + run: gh release upload "$TAG" "$IMAGE_NAME" --repo "$GITHUB_REPOSITORY" --clobber + + - name: "Upload image as workflow artifact" + if: env.UPLOAD != 'true' + uses: actions/upload-artifact@v7 + with: + name: ${{ env.IMAGE_NAME }} + path: ${{ env.IMAGE_NAME }} + compression-level: 0 # Already xz-compressed + retention-days: 7 diff --git a/pimod/.gitignore b/pimod/.gitignore new file mode 100644 index 00000000..feb8a512 --- /dev/null +++ b/pimod/.gitignore @@ -0,0 +1,4 @@ +# Build inputs and outputs (see dweOS-static.Pifile) +*.img +*.img.xz +.cache/ diff --git a/pimod/dweOS-static.Pifile b/pimod/dweOS-static.Pifile new file mode 100644 index 00000000..8dcffd11 --- /dev/null +++ b/pimod/dweOS-static.Pifile @@ -0,0 +1,133 @@ +# Minimal Raspberry Pi OS Lite image with DWE OS preinstalled, static IP variant. +# The DHCP variant is dweOS-dhcp.Pifile; keep the two in sync. +# +# - user dwe / password deepwater (sudo) +# - hostname dweos (dweos.local), SSH enabled +# - locale en_US.UTF-8, US keyboard, Wi-Fi country US +# - static IP 192.168.2.2/24 (255.255.255.0) on eth0, gateway + DNS 192.168.2.1 +# - hardware PWM (pwm-2chan overlay) +# - DWE OS from release.tar.gz (next to this file) installed and enabled at boot +# +# CI (.github/workflows/pi-image.yml) builds this for every published release. +# Local build, from this directory: +# gh release download v0.7.7 -p release.tar.gz +# docker run --rm --privileged -v "$PWD":/work -w /work nature40/pimod pimod.sh dweOS-static.Pifile +# -> dweOS-static.img + +# The Pifile is sourced by pimod, so resolve paths relative to it rather than +# to wherever pimod was started. +PIFILE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ ! -f "${PIFILE_DIR}/release.tar.gz" ]]; then + echo "### Error: ${PIFILE_DIR}/release.tar.gz is missing (gh release download -p release.tar.gz)" >&2 + false +fi + +TO "${PIFILE_DIR}/dweOS-static.img" +FROM https://downloads.raspberrypi.org/raspios_lite_arm64/images/raspios_lite_arm64-2026-10-06/2026-10-06-raspios-trixie-arm64-lite.img.xz + +# Room for GStreamer and the Python venv; reclaimed again by SHRINK below. +PUMP 2G + +# --- User: dwe / deepwater ------------------------------------------------- +RUN bash -c ' +set -e +groups="" +for g in adm dialout cdrom sudo audio video plugdev games users input render netdev gpio i2c spi; do + getent group "$g" >/dev/null && groups="${groups:+$groups,}$g" +done +# The image ships a placeholder "pi" user (shell: nologin). Rename it with +# Raspberry Pi'"'"'s own userconf tool, which also sets bash as the shell and +# cancels the first-boot rename wizard. +if [ -x /usr/lib/userconf-pi/userconf ] && id pi >/dev/null 2>&1; then + /usr/lib/userconf-pi/userconf pi dwe "" +elif ! id dwe >/dev/null 2>&1; then + useradd -m -s /bin/bash dwe +fi +usermod -s /bin/bash -a -G "$groups" dwe +echo "dwe:deepwater" | chpasswd + +# Skip the first-boot user wizard and cloud-init, which would otherwise +# prompt for / override the user and network configuration. +systemctl disable userconfig.service 2>/dev/null || true +systemctl enable getty@tty1.service +rm -f /etc/ssh/sshd_config.d/rename_user.conf /etc/sudoers.d/010_pi-nopasswd +if [ -d /etc/cloud ]; then touch /etc/cloud/cloud-init.disabled; fi +' + +# --- Hostname: dweos (reachable as dweos.local via avahi) ------------------- +RUN bash -c ' +echo dweos > /etc/hostname +sed -i "s/^127\.0\.1\.1.*/127.0.1.1\tdweos/" /etc/hosts +grep -q "^127\.0\.1\.1" /etc/hosts || printf "127.0.1.1\tdweos\n" >> /etc/hosts +' + +# --- Locale en_US.UTF-8, US keyboard, Wi-Fi country US ---------------------- +RUN raspi-config nonint do_change_locale en_US.UTF-8 +# raspi-config's keyboard helper also reloads the live console, so only do +# the part that matters for the image. +RUN bash -c ' +sed -i "s/^XKBLAYOUT=.*/XKBLAYOUT=\"us\"/" /etc/default/keyboard +DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive keyboard-configuration +' +# Adds cfg80211.ieee80211_regdom=US to cmdline.txt. +RUN raspi-config nonint do_wifi_country US + +# --- SSH ------------------------------------------------------------------- +RUN systemctl enable ssh + +# --- Static IP 192.168.2.2/24, gateway/DNS 192.168.2.1 (NetworkManager) --- +RUN tee /etc/NetworkManager/system-connections/eth0-static.nmconnection <> /boot/firmware/config.txt' + +# --- Slim down --------------------------------------------------------------- +# Keep dpkg from installing docs, man pages and non-English locales (applies +# to the DWE OS dependencies below), then drop the ones already present. +RUN tee /etc/dpkg/dpkg.cfg.d/01-nodoc <.img.xz (xz -T0 -9). +ZERO +SHRINK diff --git a/pimod/image_install_script.sh b/pimod/image_install_script.sh new file mode 100644 index 00000000..0214f13c --- /dev/null +++ b/pimod/image_install_script.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Installs DWE OS from a release tarball while building a Raspberry Pi image +# (see dweOS-static.Pifile). Unlike install.sh, it doesn't download anything and +# only enables the service, since it runs inside pimod's chroot rather than on +# a booted Pi. +# +# Usage: image_install_script.sh /path/to/release.tar.gz + +set -euo pipefail + +if [ "$EUID" -ne 0 ]; then + echo "This script must be run as root" + exit 1 +fi + +if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then + echo "Usage: $0 /path/to/release.tar.gz" + exit 1 +fi + +TARBALL=$1 +INSTALL_DIR=/opt/DWE_OS_2 + +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT + +echo "Installing DWE OS from $TARBALL" + +tar -xzf "$TARBALL" -C "$WORK_DIR" + +rm -rf "$INSTALL_DIR" +mkdir -p "$INSTALL_DIR" +cp -r "$WORK_DIR"/release/. "$INSTALL_DIR"/ + +cd "$INSTALL_DIR" + +bash install_requirements.sh +bash create_venv.sh + +# Neither script stops on errors, so make sure the result is usable instead of +# shipping an image whose service can't start. +command -v gst-launch-1.0 >/dev/null || { + echo "GStreamer was not installed" + exit 1 +} +.venv/bin/python3 -c "import backend_py.src" || { + echo "The Python environment is missing dependencies" + exit 1 +} + +cp service/* /etc/systemd/system/ +systemctl enable dwe_os_2 + +echo "Successfully installed DWE OS" From d7fb5b509f4fae3158ee254fa1b648cbf02cbce4 Mon Sep 17 00:00:00 2001 From: Kunal <16601274+Tekunalogy@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:57:37 -0700 Subject: [PATCH 2/5] chore(image): Make image_install_script.sh executable Co-Authored-By: Claude Opus 5.5 --- pimod/image_install_script.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 pimod/image_install_script.sh diff --git a/pimod/image_install_script.sh b/pimod/image_install_script.sh old mode 100644 new mode 100755 From cd0f9078e1903305f3ac10647588da64126d0e1d Mon Sep 17 00:00:00 2001 From: Kunal <16601274+Tekunalogy@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:53:54 -0700 Subject: [PATCH 3/5] ci: Temporary workflow comparing the CI and hand-built Pi images Co-Authored-By: Claude Opus 5.5 --- .github/workflows/diag-image.yml | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/diag-image.yml diff --git a/.github/workflows/diag-image.yml b/.github/workflows/diag-image.yml new file mode 100644 index 00000000..fe70381e --- /dev/null +++ b/.github/workflows/diag-image.yml @@ -0,0 +1,70 @@ +name: Diagnose Pi Image (temporary) +on: + push: + branches: ["ci/pimod-static-image"] + paths: [".github/workflows/diag-image.yml"] + +permissions: + contents: read + actions: read + +jobs: + compare: + runs-on: ubuntu-24.04-arm + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Download images + run: | + mkdir -p imgs && cd imgs + gh release download v0.7.7 --repo "$GITHUB_REPOSITORY" -p dweOS-RPi-image-v0.7.7.img.xz + gh run download 38636448072 --repo "$GITHUB_REPOSITORY" -n dweOS-staticIP-RPi-image-v0.7.7.img.xz + ls -l + xz -T0 -d dweOS-RPi-image-v0.7.7.img.xz && mv dweOS-RPi-image-v0.7.7.img good.img + xz -T0 -d dweOS-staticIP-RPi-image-v0.7.7.img.xz && mv dweOS-staticIP-RPi-image-v0.7.7.img ci.img + ls -l + + - name: Inspect images + run: | + inspect() { + name=$1; img=imgs/$1.img; out=$1.txt + loop=$(sudo losetup -P --show -f -r "$img") + sudo mkdir -p /mnt/$name/boot /mnt/$name/root + sudo mount -o ro ${loop}p1 /mnt/$name/boot + sudo mount -o ro,noload ${loop}p2 /mnt/$name/root + r=/mnt/$name/root; b=/mnt/$name/boot + { + echo "## image size"; stat -c %s "$img" + echo "## partitions"; sudo sfdisk -d "$img" | sed 's/^label-id.*//' + echo "## blkid"; sudo blkid ${loop}p1 ${loop}p2 | sed "s|$loop||" + echo "## label-id"; sudo sfdisk -d "$img" | grep label-id + echo "## cmdline.txt"; cat $b/cmdline.txt + echo "## config.txt (non-comment)"; grep -v '^#' $b/config.txt | grep -v '^$' + echo "## boot files"; ls $b | sort + echo "## fstab"; cat $r/etc/fstab + echo "## hostname"; cat $r/etc/hostname + echo "## nm connections"; sudo ls -la $r/etc/NetworkManager/system-connections/ + for f in $r/etc/NetworkManager/system-connections/*; do echo "# $f"; sudo cat "$f"; done + echo "## resolv.conf"; ls -la $r/etc/resolv.conf + echo "## enabled units"; ls $r/etc/systemd/system/*.wants/ 2>/dev/null + echo "## kernel modules"; ls $r/lib/modules/ $r/usr/lib/modules/ 2>/dev/null + echo "## du"; sudo du -sh $r/usr $r/opt $r/var $r/etc $r/home $r/root $b 2>/dev/null | sed "s|/mnt/$name/||" + echo "## du usr/lib top"; sudo du -s $r/usr/lib/* 2>/dev/null | sort -rn | head -25 | sed "s|/mnt/$name/root||" + echo "## dpkg count"; grep -c '^Package:' $r/var/lib/dpkg/status + echo "## dpkg list"; awk '/^Package:/{p=$2} /^Version:/{print p, $2}' $r/var/lib/dpkg/status | sort + echo "## dwe_os"; ls -la $r/opt/DWE_OS_2 | awk '{print $1,$3,$4,$NF}' + echo "## users"; grep -E '^(dwe|pi):' $r/etc/passwd + } > $out 2>&1 + sudo umount /mnt/$name/boot /mnt/$name/root + sudo losetup -d $loop + } + inspect good + inspect ci + diff good.txt ci.txt > diff.txt || true + echo "===== DIFF (good vs ci) ====="; cat diff.txt + echo "===== GOOD ====="; sed -n '1,/## dpkg list/p' good.txt + + - uses: actions/upload-artifact@v7 + with: + name: diag + path: "*.txt" From 7c932f8983e4569a64e2d88b126cc82f3c086922 Mon Sep 17 00:00:00 2001 From: Kunal <16601274+Tekunalogy@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:02:51 -0700 Subject: [PATCH 4/5] ci: Remove temporary Pi image comparison workflow Co-Authored-By: Claude Opus 5.5 --- .github/workflows/diag-image.yml | 70 -------------------------------- 1 file changed, 70 deletions(-) delete mode 100644 .github/workflows/diag-image.yml diff --git a/.github/workflows/diag-image.yml b/.github/workflows/diag-image.yml deleted file mode 100644 index fe70381e..00000000 --- a/.github/workflows/diag-image.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Diagnose Pi Image (temporary) -on: - push: - branches: ["ci/pimod-static-image"] - paths: [".github/workflows/diag-image.yml"] - -permissions: - contents: read - actions: read - -jobs: - compare: - runs-on: ubuntu-24.04-arm - env: - GH_TOKEN: ${{ github.token }} - steps: - - name: Download images - run: | - mkdir -p imgs && cd imgs - gh release download v0.7.7 --repo "$GITHUB_REPOSITORY" -p dweOS-RPi-image-v0.7.7.img.xz - gh run download 38636448072 --repo "$GITHUB_REPOSITORY" -n dweOS-staticIP-RPi-image-v0.7.7.img.xz - ls -l - xz -T0 -d dweOS-RPi-image-v0.7.7.img.xz && mv dweOS-RPi-image-v0.7.7.img good.img - xz -T0 -d dweOS-staticIP-RPi-image-v0.7.7.img.xz && mv dweOS-staticIP-RPi-image-v0.7.7.img ci.img - ls -l - - - name: Inspect images - run: | - inspect() { - name=$1; img=imgs/$1.img; out=$1.txt - loop=$(sudo losetup -P --show -f -r "$img") - sudo mkdir -p /mnt/$name/boot /mnt/$name/root - sudo mount -o ro ${loop}p1 /mnt/$name/boot - sudo mount -o ro,noload ${loop}p2 /mnt/$name/root - r=/mnt/$name/root; b=/mnt/$name/boot - { - echo "## image size"; stat -c %s "$img" - echo "## partitions"; sudo sfdisk -d "$img" | sed 's/^label-id.*//' - echo "## blkid"; sudo blkid ${loop}p1 ${loop}p2 | sed "s|$loop||" - echo "## label-id"; sudo sfdisk -d "$img" | grep label-id - echo "## cmdline.txt"; cat $b/cmdline.txt - echo "## config.txt (non-comment)"; grep -v '^#' $b/config.txt | grep -v '^$' - echo "## boot files"; ls $b | sort - echo "## fstab"; cat $r/etc/fstab - echo "## hostname"; cat $r/etc/hostname - echo "## nm connections"; sudo ls -la $r/etc/NetworkManager/system-connections/ - for f in $r/etc/NetworkManager/system-connections/*; do echo "# $f"; sudo cat "$f"; done - echo "## resolv.conf"; ls -la $r/etc/resolv.conf - echo "## enabled units"; ls $r/etc/systemd/system/*.wants/ 2>/dev/null - echo "## kernel modules"; ls $r/lib/modules/ $r/usr/lib/modules/ 2>/dev/null - echo "## du"; sudo du -sh $r/usr $r/opt $r/var $r/etc $r/home $r/root $b 2>/dev/null | sed "s|/mnt/$name/||" - echo "## du usr/lib top"; sudo du -s $r/usr/lib/* 2>/dev/null | sort -rn | head -25 | sed "s|/mnt/$name/root||" - echo "## dpkg count"; grep -c '^Package:' $r/var/lib/dpkg/status - echo "## dpkg list"; awk '/^Package:/{p=$2} /^Version:/{print p, $2}' $r/var/lib/dpkg/status | sort - echo "## dwe_os"; ls -la $r/opt/DWE_OS_2 | awk '{print $1,$3,$4,$NF}' - echo "## users"; grep -E '^(dwe|pi):' $r/etc/passwd - } > $out 2>&1 - sudo umount /mnt/$name/boot /mnt/$name/root - sudo losetup -d $loop - } - inspect good - inspect ci - diff good.txt ci.txt > diff.txt || true - echo "===== DIFF (good vs ci) ====="; cat diff.txt - echo "===== GOOD ====="; sed -n '1,/## dpkg list/p' good.txt - - - uses: actions/upload-artifact@v7 - with: - name: diag - path: "*.txt" From 741423894ffae12587a9ddf92d726778bf7d4457 Mon Sep 17 00:00:00 2001 From: Brandon Stevens Date: Tue, 20 Oct 2026 14:12:50 -0700 Subject: [PATCH 5/5] ci: Remove comments --- .github/workflows/pi-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pi-image.yml b/.github/workflows/pi-image.yml index 91212170..64d42943 100644 --- a/.github/workflows/pi-image.yml +++ b/.github/workflows/pi-image.yml @@ -10,10 +10,10 @@ on: workflow_dispatch: inputs: tag: - description: "Release tag to build the image for (e.g. v0.7.7)" + description: "Release tag to build the image for" required: true upload_to_release: - description: "Attach the image to the release (otherwise it's only kept as a workflow artifact)" + description: "Attach the image to the release" type: boolean default: false