From 8addbadaef266ea17844ed393cb7566b24170f3a Mon Sep 17 00:00:00 2001 From: Yuhang Cao Date: Wed, 24 Jun 2026 03:09:21 +0000 Subject: [PATCH 01/28] ci: add real qemu boot checks for CapOS --- .github/workflows/os-real-boot.yml | 155 ++++++++++++++ .github/workflows/os-sanity.yml | 1 + .../files/90-capos-webpanel-uhttpd | 12 ++ scripts/ci/capos-real-boot-config.sh | 65 ++++++ scripts/ci/qemu-port-check.sh | 193 ++++++++++++++++++ target/linux/armsr/image/Makefile | 2 +- tests/webpanel_runtime_config_smoke.sh | 29 +++ 7 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/os-real-boot.yml create mode 100755 scripts/ci/capos-real-boot-config.sh create mode 100755 scripts/ci/qemu-port-check.sh create mode 100755 tests/webpanel_runtime_config_smoke.sh diff --git a/.github/workflows/os-real-boot.yml b/.github/workflows/os-real-boot.yml new file mode 100644 index 0000000000..f2ccad668f --- /dev/null +++ b/.github/workflows/os-real-boot.yml @@ -0,0 +1,155 @@ +name: OS Real Boot Check + +on: + pull_request: + branches: ["main"] + paths: + - ".config" + - ".github/workflows/os-real-boot.yml" + - "config/**" + - "feeds.conf*" + - "include/**" + - "package/capos/**" + - "package/utils/argosfs/**" + - "scripts/ci/**" + - "target/linux/generic/**" + - "target/linux/x86/**" + - "target/linux/armsr/**" + - "target/linux/malta/**" + - "toolchain/**" + push: + branches: ["main"] + paths: + - ".config" + - ".github/workflows/os-real-boot.yml" + - "config/**" + - "feeds.conf*" + - "include/**" + - "package/capos/**" + - "package/utils/argosfs/**" + - "scripts/ci/**" + - "target/linux/generic/**" + - "target/linux/x86/**" + - "target/linux/armsr/**" + - "target/linux/malta/**" + - "toolchain/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: os-real-boot-${{ github.ref }} + cancel-in-progress: false + +jobs: + real-boot: + name: Build + QEMU boot (${{ matrix.name }}) + runs-on: ubuntu-22.04 + timeout-minutes: 360 + + strategy: + fail-fast: false + matrix: + include: + - name: x86_64 + target: x86 + subtarget: "64" + - name: aarch64 + target: armsr + subtarget: armv8 + - name: mips64el + target: malta + subtarget: le64 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Install build and QEMU prerequisites + shell: bash + run: | + set -euxo pipefail + sudo apt-get update + sudo apt-get install -y \ + bash build-essential ccache clang flex bison g++ gawk gcc-multilib \ + gettext git jq libelf-dev libncurses-dev libssl-dev make patch perl \ + python3 python3-distutils python3-pyelftools python3-setuptools \ + rsync unzip wget zlib1g-dev file curl netcat-openbsd \ + qemu-system-arm qemu-system-mips qemu-system-x86 qemu-utils qemu-efi-aarch64 + + - name: Cache downloads + uses: actions/cache@v4 + with: + path: dl + key: capos-dl-${{ matrix.name }}-${{ hashFiles('feeds.conf.default', 'feeds.conf', 'package/**/Makefile', 'target/linux/**/Makefile') }} + restore-keys: | + capos-dl-${{ matrix.name }}- + capos-dl- + + - name: Cache compiler output + uses: actions/cache@v4 + with: + path: .ccache + key: capos-ccache-${{ matrix.name }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: | + capos-ccache-${{ matrix.name }}-${{ github.ref_name }}- + capos-ccache-${{ matrix.name }}- + + - name: Configure CapOS target + shell: bash + run: | + set -euxo pipefail + mkdir -p ci-artifacts + if [ ! -f feeds.conf ] && [ -f feeds.conf.default ]; then + cp feeds.conf.default feeds.conf + fi + ./scripts/feeds list >/dev/null + ./scripts/feeds install -a + scripts/ci/capos-real-boot-config.sh "${{ matrix.target }}" "${{ matrix.subtarget }}" + make defconfig + cp .config "ci-artifacts/config-${{ matrix.name }}" + ./scripts/diffconfig.sh | tee "ci-artifacts/diffconfig-${{ matrix.name }}.txt" + + - name: Download sources + shell: bash + run: | + set -euxo pipefail + make -j"$(nproc)" download V=s + + - name: Build image + shell: bash + run: | + set -euxo pipefail + mkdir -p ci-artifacts + set +e + make -j"$(nproc)" V=s 2>&1 | tee "ci-artifacts/build-${{ matrix.name }}.log" + status=${PIPESTATUS[0]} + set -e + if [ "$status" -ne 0 ]; then + echo "parallel build failed; retrying serially for a deterministic error log" | tee -a "ci-artifacts/build-${{ matrix.name }}.log" + make -j1 V=sc 2>&1 | tee -a "ci-artifacts/build-${{ matrix.name }}.log" + fi + + - name: QEMU boot and probe 2000/tcp + shell: bash + run: | + set -euxo pipefail + CAPOS_CI_ARTIFACT_DIR=ci-artifacts \ + CAPOS_CI_BOOT_TIMEOUT_S=360 \ + scripts/ci/qemu-port-check.sh "${{ matrix.target }}" "${{ matrix.subtarget }}" + + - name: Upload diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: capos-real-boot-${{ matrix.name }} + path: | + ci-artifacts/** + logs/** + bin/targets/${{ matrix.target }}/${{ matrix.subtarget }}/*.manifest + bin/targets/${{ matrix.target }}/${{ matrix.subtarget }}/*sha256sums + if-no-files-found: ignore + retention-days: 14 diff --git a/.github/workflows/os-sanity.yml b/.github/workflows/os-sanity.yml index 43724d1a23..341579080d 100644 --- a/.github/workflows/os-sanity.yml +++ b/.github/workflows/os-sanity.yml @@ -71,6 +71,7 @@ jobs: tests/capbox_logic_tests.sh tests/webpanel_frontend_smoke.sh tests/webpanel_proxy_smoke.sh + tests/webpanel_runtime_config_smoke.sh - name: Kconfig sanity (defconfig) shell: bash diff --git a/package/capos/capos-webpanel/files/90-capos-webpanel-uhttpd b/package/capos/capos-webpanel/files/90-capos-webpanel-uhttpd index 8a2105ed42..8c2526a24e 100644 --- a/package/capos/capos-webpanel/files/90-capos-webpanel-uhttpd +++ b/package/capos/capos-webpanel/files/90-capos-webpanel-uhttpd @@ -1,5 +1,17 @@ #!/bin/sh +# CapOS exposes its built-in web panel on stable, documented ports instead of +# OpenWrt's stock 80/443 defaults. +uci -q delete uhttpd.main.listen_http +uci add_list uhttpd.main.listen_http='0.0.0.0:2000' +uci add_list uhttpd.main.listen_http='[::]:2000' +uci -q delete uhttpd.main.listen_https +uci add_list uhttpd.main.listen_https='0.0.0.0:2020' +uci add_list uhttpd.main.listen_https='[::]:2020' +uci -q set uhttpd.main.home='/www' +uci -q set uhttpd.main.cgi_prefix='/cgi-bin' +uci -q set uhttpd.main.redirect_https='0' + # ArgosFS currently rejects fsync for px5g output under /etc on first boot. # Generate uhttpd's self-signed certificate on tmpfs instead. uci -q set uhttpd.main.cert='/tmp/uhttpd.crt' diff --git a/scripts/ci/capos-real-boot-config.sh b/scripts/ci/capos-real-boot-config.sh new file mode 100755 index 0000000000..26204e41a8 --- /dev/null +++ b/scripts/ci/capos-real-boot-config.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +target="$1" +subtarget="$2" + +case "$target/$subtarget" in + x86/64|armsr/armv8|malta/le64) + ;; + *) + echo "unsupported CapOS real boot CI target: $target/$subtarget" >&2 + exit 2 + ;; +esac + +cat > .config <> .config <<'CONFIG' +CONFIG_GRUB_IMAGES=y +# CONFIG_GRUB_EFI_IMAGES is not set +# CONFIG_ISO_IMAGES is not set +# CONFIG_VDI_IMAGES is not set +# CONFIG_VMDK_IMAGES is not set +# CONFIG_VHDX_IMAGES is not set +CONFIG_TARGET_SERIAL="ttyS0" +CONFIG_GRUB_BOOTOPTS="panic=5" +CONFIG + ;; + armsr/armv8) + cat >> .config <<'CONFIG' +CONFIG_GRUB_EFI_IMAGES=y +# CONFIG_VMDK_IMAGES is not set +CONFIG_TARGET_SERIAL="ttyAMA0" +CONFIG_GRUB_BOOTOPTS="panic=5" +CONFIG + ;; + malta/le64) + cat >> .config <<'CONFIG' +CONFIG_TARGET_SERIAL="ttyS0" +CONFIG_KERNEL_CMDLINE="panic=5" +CONFIG + ;; +esac diff --git a/scripts/ci/qemu-port-check.sh b/scripts/ci/qemu-port-check.sh new file mode 100755 index 0000000000..f560da2468 --- /dev/null +++ b/scripts/ci/qemu-port-check.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +target="$1" +subtarget="$2" +bindir="bin/targets/$target/$subtarget" +artifact_dir="${CAPOS_CI_ARTIFACT_DIR:-ci-artifacts}" +boot_timeout_s="${CAPOS_CI_BOOT_TIMEOUT_S:-360}" +probe_url="${CAPOS_CI_PROBE_URL:-http://192.168.1.1:2000/cgi-bin/cap/api}" +mkdir -p "$artifact_dir" + +log="$artifact_dir/qemu-${target}-${subtarget}.log" +response="$artifact_dir/probe-${target}-${subtarget}.json" +workdir="$(mktemp -d)" +qemu_pid="" +tap_wan="capwan$$" +tap_lan="caplan$$" + +cleanup() { + if [[ -n "$qemu_pid" ]] && kill -0 "$qemu_pid" 2>/dev/null; then + kill "$qemu_pid" 2>/dev/null || true + sleep 1 + kill -9 "$qemu_pid" 2>/dev/null || true + fi + sudo ip link set "$tap_wan" down 2>/dev/null || true + sudo ip link set "$tap_lan" down 2>/dev/null || true + sudo ip tuntap del dev "$tap_wan" mode tap 2>/dev/null || true + sudo ip tuntap del dev "$tap_lan" mode tap 2>/dev/null || true + rm -rf "$workdir" +} +trap cleanup EXIT + +require_file() { + local pattern="$1" + local found + found="$(find "$bindir" -maxdepth 1 -type f -name "$pattern" | sort | head -n 1 || true)" + if [[ -z "$found" ]]; then + echo "missing artifact matching $bindir/$pattern" >&2 + find "$bindir" -maxdepth 1 -type f | sort >&2 || true + exit 1 + fi + printf '%s\n' "$found" +} + +require_any_file() { + local found="" + local pattern + for pattern in "$@"; do + found="$(find "$bindir" -maxdepth 1 -type f -name "$pattern" | sort | head -n 1 || true)" + if [[ -n "$found" ]]; then + printf '%s\n' "$found" + return 0 + fi + done + echo "missing artifact matching any of: $*" >&2 + find "$bindir" -maxdepth 1 -type f | sort >&2 || true + exit 1 +} + +prepare_image() { + local src="$1" + local dst="$workdir/$(basename "${src%.gz}")" + if [[ "$src" == *.gz ]]; then + gzip -dc "$src" > "$dst" + printf '%s\n' "$dst" + else + printf '%s\n' "$src" + fi +} + +setup_taps() { + sudo modprobe tun || true + sudo ip tuntap add dev "$tap_wan" mode tap user "$(id -un)" + sudo ip tuntap add dev "$tap_lan" mode tap user "$(id -un)" + sudo ip link set "$tap_wan" up + sudo ip addr add 192.168.1.2/24 dev "$tap_lan" + sudo ip link set "$tap_lan" up +} + +start_qemu() { + local disk kernel rootfs firmware + : > "$log" + setup_taps + + case "$target/$subtarget" in + x86/64) + disk="$(prepare_image "$(require_file '*-combined.img.gz')")" + qemu-system-x86_64 \ + -machine pc,accel=tcg \ + -cpu max \ + -smp 2 \ + -m 1024 \ + -nographic \ + -no-reboot \ + -drive "file=$disk,format=raw,if=virtio" \ + -netdev "tap,id=wan,ifname=$tap_wan,script=no,downscript=no" \ + -device "virtio-net-pci,netdev=wan,mac=52:54:00:12:34:10" \ + -netdev "tap,id=lan,ifname=$tap_lan,script=no,downscript=no" \ + -device "virtio-net-pci,netdev=lan,mac=52:54:00:12:34:11" \ + >"$log" 2>&1 & + ;; + armsr/armv8) + disk="$(prepare_image "$(require_file '*-combined-efi.img.gz')")" + firmware="" + for candidate in \ + /usr/share/AAVMF/AAVMF_CODE.fd \ + /usr/share/qemu-efi-aarch64/QEMU_EFI.fd \ + /usr/share/edk2/aarch64/QEMU_EFI.fd; do + if [[ -r "$candidate" ]]; then + firmware="$candidate" + break + fi + done + if [[ -z "$firmware" ]]; then + echo "missing AArch64 UEFI firmware" >&2 + exit 1 + fi + qemu-system-aarch64 \ + -machine virt,accel=tcg,gic-version=max \ + -cpu cortex-a57 \ + -smp 2 \ + -m 1024 \ + -nographic \ + -no-reboot \ + -bios "$firmware" \ + -drive "file=$disk,format=raw,if=virtio" \ + -netdev "tap,id=wan,ifname=$tap_wan,script=no,downscript=no" \ + -device "virtio-net-pci,netdev=wan,mac=52:54:00:12:34:20" \ + -netdev "tap,id=lan,ifname=$tap_lan,script=no,downscript=no" \ + -device "virtio-net-pci,netdev=lan,mac=52:54:00:12:34:21" \ + >"$log" 2>&1 & + ;; + malta/le64) + kernel="$(require_file '*-vmlinux-initramfs.elf')" + rootfs="$(prepare_image "$(require_any_file '*-rootfs-argosfs.img.gz' '*-argosfs-rootfs.img.gz' '*-argosfs.img.gz')")" + qemu-system-mips64el \ + -machine malta \ + -cpu MIPS64R2-generic \ + -smp 2 \ + -m 512 \ + -nographic \ + -no-reboot \ + -kernel "$kernel" \ + -append "argosfs.images=/dev/sda rootwait console=ttyS0,115200 panic=5" \ + -drive "file=$rootfs,format=raw" \ + -netdev "tap,id=wan,ifname=$tap_wan,script=no,downscript=no" \ + -device "pcnet,netdev=wan,mac=52:54:00:12:34:30" \ + -netdev "tap,id=lan,ifname=$tap_lan,script=no,downscript=no" \ + -device "pcnet,netdev=lan,mac=52:54:00:12:34:31" \ + >"$log" 2>&1 & + ;; + *) + echo "unsupported target for QEMU boot check: $target/$subtarget" >&2 + exit 2 + ;; + esac + qemu_pid=$! +} + +probe_service() { + local deadline=$((SECONDS + boot_timeout_s)) + local last_status=0 + while (( SECONDS < deadline )); do + if ! kill -0 "$qemu_pid" 2>/dev/null; then + echo "QEMU exited before the service probe succeeded" >&2 + tail -n 240 "$log" >&2 || true + return 1 + fi + if curl -fsS --connect-timeout 2 --max-time 5 "$probe_url" -o "$response"; then + if grep -q '"service"[[:space:]]*:[[:space:]]*"capos-webpanel-api"' "$response"; then + echo "CapOS webpanel responded on $probe_url" + return 0 + fi + last_status=1 + else + last_status=$? + fi + sleep 3 + done + + echo "timed out waiting for CapOS webpanel on $probe_url (last curl status: $last_status)" >&2 + tail -n 240 "$log" >&2 || true + return 1 +} + +start_qemu +probe_service diff --git a/target/linux/armsr/image/Makefile b/target/linux/armsr/image/Makefile index 612063e8b2..ff69168f84 100644 --- a/target/linux/armsr/image/Makefile +++ b/target/linux/armsr/image/Makefile @@ -104,7 +104,7 @@ define Device/generic $(call Device/efi-default) DEVICE_TITLE := Generic EFI Boot GRUB2_VARIANT := generic - FILESYSTEMS := ext4 squashfs + FILESYSTEMS := ext4 squashfs argosfs UBOOT := $(if $(CONFIG_aarch64),qemu_armv8,qemu_armv7) DEVICE_PACKAGES += kmod-amazon-ena kmod-e1000e kmod-vmxnet3 kmod-rtc-rx8025 \ kmod-i2c-mux-pca954x kmod-gpio-pca953x partx-utils kmod-wdt-sp805 \ diff --git a/tests/webpanel_runtime_config_smoke.sh b/tests/webpanel_runtime_config_smoke.sh new file mode 100755 index 0000000000..85a78c513e --- /dev/null +++ b/tests/webpanel_runtime_config_smoke.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT="$ROOT_DIR/package/capos/capos-webpanel/files/90-capos-webpanel-uhttpd" +TMPDIR="$(mktemp -d)" +LOG="$TMPDIR/uci.log" +cleanup() { rm -rf "$TMPDIR"; } +trap cleanup EXIT + +mkdir -p "$TMPDIR/bin" +cat > "$TMPDIR/bin/uci" <<'UCI' +#!/usr/bin/env bash +if [[ "${1:-}" == "-q" ]]; then + shift +fi +printf '%s\n' "$*" >> "$UCI_LOG" +exit 0 +UCI +chmod +x "$TMPDIR/bin/uci" + +env PATH="$TMPDIR/bin:$PATH" UCI_LOG="$LOG" sh "$SCRIPT" + +grep -Fx "add_list uhttpd.main.listen_http=0.0.0.0:2000" "$LOG" +grep -Fx "add_list uhttpd.main.listen_https=0.0.0.0:2020" "$LOG" +grep -Fx "set uhttpd.main.cgi_prefix=/cgi-bin" "$LOG" + +echo "webpanel runtime config smoke passed" From 9d035e71eb6f82c07efcaca642fb68887620c797 Mon Sep 17 00:00:00 2001 From: Yuhang Cao Date: Wed, 24 Jun 2026 04:32:04 +0000 Subject: [PATCH 02/28] ci: reduce real boot build log noise --- .github/workflows/os-real-boot.yml | 16 ++-- scripts/ci/run-with-log.sh | 124 +++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) create mode 100755 scripts/ci/run-with-log.sh diff --git a/.github/workflows/os-real-boot.yml b/.github/workflows/os-real-boot.yml index f2ccad668f..277c558dc3 100644 --- a/.github/workflows/os-real-boot.yml +++ b/.github/workflows/os-real-boot.yml @@ -117,7 +117,9 @@ jobs: shell: bash run: | set -euxo pipefail - make -j"$(nproc)" download V=s + scripts/ci/run-with-log.sh "ci-artifacts/download-${{ matrix.name }}.log" \ + "Download sources (${{ matrix.name }})" -- \ + make -j"$(nproc)" download V=s - name: Build image shell: bash @@ -125,12 +127,16 @@ jobs: set -euxo pipefail mkdir -p ci-artifacts set +e - make -j"$(nproc)" V=s 2>&1 | tee "ci-artifacts/build-${{ matrix.name }}.log" - status=${PIPESTATUS[0]} + scripts/ci/run-with-log.sh "ci-artifacts/build-${{ matrix.name }}.log" \ + "Parallel image build (${{ matrix.name }})" -- \ + make -j"$(nproc)" V=s + status=$? set -e if [ "$status" -ne 0 ]; then - echo "parallel build failed; retrying serially for a deterministic error log" | tee -a "ci-artifacts/build-${{ matrix.name }}.log" - make -j1 V=sc 2>&1 | tee -a "ci-artifacts/build-${{ matrix.name }}.log" + echo "parallel build failed; retrying serially for a deterministic error log" + scripts/ci/run-with-log.sh --append "ci-artifacts/build-${{ matrix.name }}.log" \ + "Serial image build retry (${{ matrix.name }})" -- \ + make -j1 V=sc fi - name: QEMU boot and probe 2000/tcp diff --git a/scripts/ci/run-with-log.sh b/scripts/ci/run-with-log.sh new file mode 100755 index 0000000000..79bbc8ac3d --- /dev/null +++ b/scripts/ci/run-with-log.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat >&2 <<'USAGE' +usage: scripts/ci/run-with-log.sh [--append]