From 876ff57b579f07b1e5c16887be24c6faeaed94d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9?= <1131761436@qq.com> Date: Fri, 7 Aug 2026 11:28:18 +0800 Subject: [PATCH] build(cardputerzero): add verified local image workflow --- stage2/05-cardputerzero/01-run.sh | 17 +- stage2/05-cardputerzero/02-run.sh | 2 + .../cardputerzero-extport-permissions.patch | 36 +++ stage4/02-cardputerzero/00-run.sh | 23 ++ tools/build-local-image.sh | 227 ++++++++++++++++++ tools/verify-image.sh | 40 ++- 6 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 stage2/05-cardputerzero/files/cardputerzero-extport-permissions.patch create mode 100755 tools/build-local-image.sh diff --git a/stage2/05-cardputerzero/01-run.sh b/stage2/05-cardputerzero/01-run.sh index 9e930e92ba..9ee5fc7a8b 100755 --- a/stage2/05-cardputerzero/01-run.sh +++ b/stage2/05-cardputerzero/01-run.sh @@ -27,6 +27,8 @@ printf 'DTOVERLAYS_REPO=%q\nDTOVERLAYS_REF=%q\nDTOVERLAYS_ARCHIVE=%q\nDTOVERLAYS "$DTOVERLAYS_REPO" "$DTOVERLAYS_REF" \ "$CHROOT_DTOVERLAYS_ARCHIVE" "$DTOVERLAYS_ARCHIVE_SHA256" \ > "${ROOTFS_DIR}/tmp/cardputerzero-dtoverlays.env" +install -m 0644 files/cardputerzero-extport-permissions.patch \ + "${ROOTFS_DIR}/tmp/cardputerzero-extport-permissions.patch" install -d -m 0755 "${ROOTFS_DIR}/opt" # The upstream config_setup target otherwise downloads this unpinned Gist. install -m 0755 files/rpi-config.py "${ROOTFS_DIR}/opt/rpi-config.py" @@ -71,6 +73,18 @@ else > /etc/cardputerzero-dtoverlays.commit fi +if git -C /tmp/dtoverlays apply --check \ + /tmp/cardputerzero-extport-permissions.patch; then + git -C /tmp/dtoverlays apply \ + /tmp/cardputerzero-extport-permissions.patch +elif git -C /tmp/dtoverlays apply --reverse --check \ + /tmp/cardputerzero-extport-permissions.patch; then + echo "CardputerZero ExtPort permissions are already present in DTOVERLAYS_REF" +else + echo "ERROR: CardputerZero ExtPort permissions do not apply to DTOVERLAYS_REF" + exit 1 +fi + KVER=$(ls /lib/modules/ | grep rpi-v8 | head -1) export KERNELDIR="/lib/modules/${KVER}/build" export EXTRADIR="/lib/modules/${KVER}/extra" @@ -86,6 +100,7 @@ depmod -a "${KVER}" # Clean up build artifacts only (keep all build tools for user driver builds) rm -rf /tmp/dtoverlays rm -f /tmp/cardputerzero-dtoverlays.env \ - /tmp/cardputerzero-dtoverlays.tar.gz + /tmp/cardputerzero-dtoverlays.tar.gz \ + /tmp/cardputerzero-extport-permissions.patch CHROOT diff --git a/stage2/05-cardputerzero/02-run.sh b/stage2/05-cardputerzero/02-run.sh index 9787739eaf..845de546de 100755 --- a/stage2/05-cardputerzero/02-run.sh +++ b/stage2/05-cardputerzero/02-run.sh @@ -94,6 +94,8 @@ install -m 644 files/splash.bmp "${ROOTFS_DIR}/boot/firmware/splash.bmp" on_chroot << CHROOT set -e dpkg -i "/tmp/${DEB_FILE}" +install -d -m 700 /var/cache/APPLaunch/updates +install -m 600 "/tmp/${DEB_FILE}" /var/cache/APPLaunch/updates/installed.deb rm -f "/tmp/${DEB_FILE}" CHROOT diff --git a/stage2/05-cardputerzero/files/cardputerzero-extport-permissions.patch b/stage2/05-cardputerzero/files/cardputerzero-extport-permissions.patch new file mode 100644 index 0000000000..4a23a0bb73 --- /dev/null +++ b/stage2/05-cardputerzero/files/cardputerzero-extport-permissions.patch @@ -0,0 +1,36 @@ +diff --git a/modules/CardputerZero/Makefile b/modules/CardputerZero/Makefile +--- a/modules/CardputerZero/Makefile ++++ b/modules/CardputerZero/Makefile +@@ -43,7 +43,9 @@ install_ko: $(Target_dtbo) + + install_led_permissions: + mkdir -p $(ROOTFSPATH)/usr/lib/tmpfiles.d/ ++ mkdir -p $(ROOTFSPATH)/usr/lib/udev/rules.d/ + install -m 0644 cardputerzero-leds.conf $(ROOTFSPATH)/usr/lib/tmpfiles.d/cardputerzero-leds.conf ++ install -m 0644 60-cardputerzero-leds.rules $(ROOTFSPATH)/usr/lib/udev/rules.d/60-cardputerzero-leds.rules + + clean: + $(MAKE) -C $(KERNELDIR) M=$(PWD) clean +diff --git a/modules/CardputerZero/cardputerzero-leds.conf b/modules/CardputerZero/cardputerzero-leds.conf +--- a/modules/CardputerZero/cardputerzero-leds.conf ++++ b/modules/CardputerZero/cardputerzero-leds.conf +@@ -1,5 +1,5 @@ + # Allow unprivileged users to control CardputerZero GPIO LEDs. +-z /sys/class/leds/grove_fun/brightness 0666 - - - +-z /sys/class/leds/ext_usb_gpio_fun/brightness 0666 - - - +-z /sys/class/leds/ext_5v_out/brightness 0666 - - - +-z /sys/class/leds/grove_5v_out/brightness 0666 - - - ++z /sys/class/leds/grove_fun/brightness 0664 root gpio - ++z /sys/class/leds/ext_usb_gpio_fun/brightness 0664 root gpio - ++z /sys/class/leds/ext_5v_out/brightness 0664 root gpio - ++z /sys/class/leds/grove_5v_out/brightness 0664 root gpio - +diff --git a/modules/CardputerZero/60-cardputerzero-leds.rules b/modules/CardputerZero/60-cardputerzero-leds.rules +new file mode 100644 +--- /dev/null ++++ b/modules/CardputerZero/60-cardputerzero-leds.rules +@@ -0,0 +1,5 @@ ++# LED class attributes are created after tmpfiles runs; apply permissions on add. ++ACTION=="add", SUBSYSTEM=="leds", KERNEL=="grove_fun", RUN+="/bin/chgrp gpio $sys$devpath/brightness", RUN+="/bin/chmod g+w $sys$devpath/brightness" ++ACTION=="add", SUBSYSTEM=="leds", KERNEL=="ext_usb_gpio_fun", RUN+="/bin/chgrp gpio $sys$devpath/brightness", RUN+="/bin/chmod g+w $sys$devpath/brightness" ++ACTION=="add", SUBSYSTEM=="leds", KERNEL=="ext_5v_out", RUN+="/bin/chgrp gpio $sys$devpath/brightness", RUN+="/bin/chmod g+w $sys$devpath/brightness" ++ACTION=="add", SUBSYSTEM=="leds", KERNEL=="grove_5v_out", RUN+="/bin/chgrp gpio $sys$devpath/brightness", RUN+="/bin/chmod g+w $sys$devpath/brightness" diff --git a/stage4/02-cardputerzero/00-run.sh b/stage4/02-cardputerzero/00-run.sh index 28afe7f7da..d9ae02fe08 100755 --- a/stage4/02-cardputerzero/00-run.sh +++ b/stage4/02-cardputerzero/00-run.sh @@ -18,3 +18,26 @@ if grep -Eq '^widgets_right=.*(^|[[:space:]])updater([[:space:]]|$)' \ echo "ERROR: failed to disable the wf-panel-pi updater widget" >&2 exit 1 fi + +# stage3/4 installs the desktop stack after the product OOBE was configured in +# stage2. Disable the display manager again at the final image layer so it +# cannot race the root LaunchWizard for DRM/VT and memory on the first boot. +on_chroot << 'CHROOT' +systemctl disable lightdm.service adbd.service cardputer-adb-hotplug.service +CHROOT + +rm -f "${ROOTFS_DIR}/etc/systemd/system/display-manager.service" \ + "${ROOTFS_DIR}/etc/systemd/system/graphical.target.wants/lightdm.service" \ + "${ROOTFS_DIR}/etc/systemd/system/multi-user.target.wants/adbd.service" \ + "${ROOTFS_DIR}/etc/systemd/system/multi-user.target.wants/cardputer-adb-hotplug.service" + +for link in \ + "${ROOTFS_DIR}/etc/systemd/system/display-manager.service" \ + "${ROOTFS_DIR}/etc/systemd/system/graphical.target.wants/lightdm.service" \ + "${ROOTFS_DIR}/etc/systemd/system/multi-user.target.wants/adbd.service" \ + "${ROOTFS_DIR}/etc/systemd/system/multi-user.target.wants/cardputer-adb-hotplug.service"; do + if [ -e "$link" ] || [ -L "$link" ]; then + echo "ERROR: forbidden first-boot service remains enabled: $link" >&2 + exit 1 + fi +done diff --git a/tools/build-local-image.sh b/tools/build-local-image.sh new file mode 100755 index 0000000000..ff29a3e914 --- /dev/null +++ b/tools/build-local-image.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +set -euo pipefail +trap 'rc=$?; echo "ERROR: build-local-image.sh line $LINENO: $BASH_COMMAND (exit $rc)" >&2' ERR + +PIGEN=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +WORKSPACE=$(CDPATH= cd -- "$PIGEN/.." && pwd) +LAUNCHER=${LAUNCHER_DIR:-"$WORKSPACE/launcher"} +DTO=${DTOVERLAYS_SOURCE_DIR:-"$WORKSPACE/m5stack-linux-dtoverlays"} +ARTIFACTS=${ARTIFACTS_DIR:-"$WORKSPACE/build-artifacts"} +BUILD_DATE=${BUILD_DATE:-$(date +%Y-%m-%d)} +IMAGE_BASENAME=${IMAGE_BASENAME:-"$BUILD_DATE-cardputerzero-trixie-arm64-local"} +CONFIG_FILE="$ARTIFACTS/one-click/$IMAGE_BASENAME.config" +INPUTS_FILE="$ARTIFACTS/one-click/$IMAGE_BASENAME.build-inputs" +LAUNCHER_OUTPUT=${LAUNCHER_OUTPUT_DIR:-"$ARTIFACTS/launcher-release"} +SCONS=${SCONS:-/home/m5stack/.venvs/cardputerzero-build/bin/scons} +JOBS=${JOBS:-$(nproc)} +SUDO=${SUDO:-sudo} + +usage() { + cat <<'EOF' +Usage: tools/build-local-image.sh + +Builds the complete APPLaunch package, a fresh pi-gen image, full offline +verification, and SHA-256 metadata. No environment variables are required. + +Optional environment: + SKIP_LAUNCHER_BUILD=1 Reuse APPLAUNCH_SOURCE_FILE or the last package. + APPLAUNCH_SOURCE_FILE=PATH Use this host-side applaunch arm64 .deb. + RECORDER_SOURCE_FILE=PATH Use this host-side Recorder arm64 .deb. + IMAGE_BASENAME=NAME Override the deploy filename stem. + KEEP_RAW_IMAGE=1 Keep the uncompressed .img after verification. + SCONS=PATH, JOBS=N Override the launcher build tools. + DTOVERLAYS_SOURCE_DIR=PATH Override the host dtoverlays source repository. + CONTINUE=1 Resume an existing pi-gen Docker container. + PREPARE_ONLY=1 Validate inputs and write manifests, then stop. +EOF +} + +if [ "${1:-}" = --help ]; then + usage + exit 0 +fi +if [ "$#" -ne 0 ]; then + usage >&2 + exit 2 +fi + +for command in git docker dpkg-deb sha256sum xz python3; do + command -v "$command" >/dev/null 2>&1 || { + echo "ERROR: required command not found: $command" >&2 + exit 1 + } +done +for repo in "$PIGEN" "$LAUNCHER" "$DTO"; do + git -C "$repo" rev-parse --verify HEAD >/dev/null + if git -C "$repo" diff --name-only --diff-filter=U | grep -q .; then + echo "ERROR: unresolved Git conflict in $repo" >&2 + exit 1 + fi +done +git -C "$LAUNCHER" submodule status --recursive | grep -Eq '^[-+U]' && { + echo "ERROR: launcher submodules do not match the parent gitlinks" >&2 + exit 1 +} + +free_bytes=$(df -PB1 "$WORKSPACE" | awk 'NR == 2 {print $4}') +minimum_bytes=$((30 * 1024 * 1024 * 1024)) +if [ "$free_bytes" -lt "$minimum_bytes" ]; then + echo "ERROR: at least 30 GiB free space is required for a full image build" >&2 + exit 1 +fi +docker ps >/dev/null +$SUDO -v + +mkdir -p "$ARTIFACTS/one-click" "$LAUNCHER_OUTPUT" \ + "$PIGEN/local-packages" "$PIGEN/local-sources" + +if [ "${SKIP_LAUNCHER_BUILD:-0}" != 1 ]; then + SCONS="$SCONS" JOBS="$JOBS" OUTPUT_DIR="$LAUNCHER_OUTPUT" \ + "$LAUNCHER/scripts/build_cardputerzero_release_local.sh" +fi + +APPLAUNCH_SOURCE_FILE=${APPLAUNCH_SOURCE_FILE:-} +if [ -z "$APPLAUNCH_SOURCE_FILE" ]; then + pointer="$LAUNCHER_OUTPUT/applaunch-package.path" + test -f "$pointer" || { + echo "ERROR: no launcher package result found: $pointer" >&2 + exit 1 + } + APPLAUNCH_SOURCE_FILE=$(sed -n '1p' "$pointer") +fi +test -f "$APPLAUNCH_SOURCE_FILE" +test "$(dpkg-deb -f "$APPLAUNCH_SOURCE_FILE" Package)" = applaunch +test "$(dpkg-deb -f "$APPLAUNCH_SOURCE_FILE" Architecture)" = arm64 +APPLAUNCH_VERSION=$(dpkg-deb -f "$APPLAUNCH_SOURCE_FILE" Version) +APPLAUNCH_SHA256=$(sha256sum "$APPLAUNCH_SOURCE_FILE" | awk '{print $1}') +STAGED_APPLAUNCH="$PIGEN/local-packages/applaunch-one-click-arm64.deb" +install -m 0644 "$APPLAUNCH_SOURCE_FILE" "$STAGED_APPLAUNCH" + +RECORDER_SOURCE_FILE=${RECORDER_SOURCE_FILE:-} +if [ -z "$RECORDER_SOURCE_FILE" ]; then + RECORDER_SOURCE_FILE=$(find "$PIGEN/local-packages" -maxdepth 1 -type f \ + -name 'm5cardputerzero-recorder_*_arm64.deb' -print | sort -V | tail -1) +fi +test -n "$RECORDER_SOURCE_FILE" +test -f "$RECORDER_SOURCE_FILE" +test "$(dpkg-deb -f "$RECORDER_SOURCE_FILE" Package)" = m5cardputerzero-recorder +test "$(dpkg-deb -f "$RECORDER_SOURCE_FILE" Architecture)" = arm64 +RECORDER_VERSION=$(dpkg-deb -f "$RECORDER_SOURCE_FILE" Version) +RECORDER_SHA256=$(sha256sum "$RECORDER_SOURCE_FILE" | awk '{print $1}') +STAGED_RECORDER="$PIGEN/local-packages/recorder-one-click-arm64.deb" +if [ "$RECORDER_SOURCE_FILE" != "$STAGED_RECORDER" ]; then + install -m 0644 "$RECORDER_SOURCE_FILE" "$STAGED_RECORDER" +fi + +DTOVERLAYS_REF=${DTOVERLAYS_REF:-$(git -C "$DTO" rev-parse HEAD)} +[[ "$DTOVERLAYS_REF" =~ ^[0-9a-f]{40}$ ]] || { + echo "ERROR: DTOVERLAYS_REF must be a 40-character commit" >&2 + exit 1 +} +git -C "$DTO" cat-file -e "$DTOVERLAYS_REF^{commit}" +DTO_ARCHIVE_NAME="m5stack-linux-dtoverlays-$DTOVERLAYS_REF.tar.gz" +DTO_ARCHIVE_HOST="$PIGEN/local-sources/$DTO_ARCHIVE_NAME" +DTO_ARCHIVE_TMP="$DTO_ARCHIVE_HOST.tmp" +git -C "$DTO" archive --format=tar.gz \ + --prefix="m5stack-linux-dtoverlays-$DTOVERLAYS_REF/" \ + -o "$DTO_ARCHIVE_TMP" "$DTOVERLAYS_REF" +mv -f "$DTO_ARCHIVE_TMP" "$DTO_ARCHIVE_HOST" +DTO_ARCHIVE_SHA256=$(sha256sum "$DTO_ARCHIVE_HOST" | awk '{print $1}') + +worktree_fingerprint() { + repo=$1 + { + git -C "$repo" diff --binary HEAD + git -C "$repo" ls-files --others --exclude-standard -z | + LC_ALL=C sort -z | + while IFS= read -r -d '' file; do + sha256sum -- "$repo/$file" + done + } | sha256sum | awk '{print $1}' +} + +cat >"$CONFIG_FILE" <"$INPUTS_FILE" <&1 | tee "$VERIFY_LOG" +grep -Fq 'ALL CHECKS PASSED' "$VERIFY_LOG" +( + cd "$PIGEN/deploy" + sha256sum "$IMAGE_BASENAME.img.xz" >"$IMAGE_BASENAME.img.xz.sha256" + sha256sum -c "$IMAGE_BASENAME.img.xz.sha256" +) +install -m 0644 "$INPUTS_FILE" "$PIGEN/deploy/$IMAGE_BASENAME.build-inputs" + +if [ "${KEEP_RAW_IMAGE:-0}" != 1 ]; then + rm -f "$RAW_IMAGE" +fi + +echo "Complete image: $IMAGE_XZ" +echo "Checksum: $IMAGE_XZ.sha256" +echo "Package manifest: $PIGEN/deploy/$IMAGE_BASENAME.build-inputs" +echo "Verification: $VERIFY_LOG" diff --git a/tools/verify-image.sh b/tools/verify-image.sh index fadc819ea8..df8248977a 100755 --- a/tools/verify-image.sh +++ b/tools/verify-image.sh @@ -309,6 +309,30 @@ else pass "console userconfig disabled; LaunchWizard owns first boot" fi +if debugfs -R "stat etc/systemd/system/display-manager.service" "$TMPDIR/root.ext4" 2>/dev/null | grep -q "Inode:"; then + fail "display-manager enabled during product OOBE" +else + pass "display-manager disabled during product OOBE" +fi + +if debugfs -R "stat etc/systemd/system/graphical.target.wants/lightdm.service" "$TMPDIR/root.ext4" 2>/dev/null | grep -q "Inode:"; then + fail "LightDM enabled during product OOBE" +else + pass "LightDM disabled during product OOBE" +fi + +if debugfs -R "stat etc/systemd/system/multi-user.target.wants/adbd.service" "$TMPDIR/root.ext4" 2>/dev/null | grep -q "Inode:"; then + fail "ADB enabled by default" +else + pass "ADB disabled by default" +fi + +if debugfs -R "stat etc/systemd/system/multi-user.target.wants/cardputer-adb-hotplug.service" "$TMPDIR/root.ext4" 2>/dev/null | grep -q "Inode:"; then + fail "ADB hotplug monitor enabled while ADB defaults off" +else + pass "ADB hotplug monitor disabled by default" +fi + if debugfs -R "stat etc/xdg/autostart/piwiz.desktop" "$TMPDIR/root.ext4" 2>/dev/null | grep -q "Inode:"; then fail "Raspberry Pi desktop piwiz should not be enabled" else @@ -430,8 +454,22 @@ else fail "cmatrix NOT installed" fi +CAMERA_PACKAGE="" +for package in cameraapp camera; do + PACKAGE_STATUS=$(sed -n "/^Package: ${package}$/,/^$/p" "$TMPDIR/dpkg_status") + PACKAGE_ARCH=$(printf '%s\n' "$PACKAGE_STATUS" | + awk '$1 == "Architecture:" { print $2; exit }') + if [ "$PACKAGE_ARCH" = "arm64" ]; then + CAMERA_PACKAGE="$package" + pass "$package installed (arm64)" + break + fi +done +if [ -z "$CAMERA_PACKAGE" ]; then + fail "CameraApp NOT installed as arm64 (accepted packages: cameraapp or camera)" +fi + CUSTOM_PACKAGES=( - "camera" "factorytest" "m5cardputerzero-cap-cc1101-nfc" "m5cardputerzero-cap-cc1101-subg-chat"