diff --git a/.github/workflows/os-real-boot.yml b/.github/workflows/os-real-boot.yml new file mode 100644 index 0000000000..7d0fb83575 --- /dev/null +++ b/.github/workflows/os-real-boot.yml @@ -0,0 +1,208 @@ +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/**" + - "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/**" + - "toolchain/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: os-real-boot-${{ github.ref }} + cancel-in-progress: true + +jobs: + real-boot: + name: Build + QEMU boot (${{ matrix.name }}) + runs-on: ubuntu-22.04 + timeout-minutes: 360 + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 2G + + strategy: + fail-fast: false + matrix: + include: + - name: x86_64 + target: x86 + subtarget: "64" + - name: aarch64 + target: armsr + subtarget: armv8 + + 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 \ + pkg-config fuse3 libfuse3-dev \ + qemu-system-arm qemu-system-x86 qemu-utils qemu-efi-aarch64 + + - name: Free runner disk space + shell: bash + run: | + set -euxo pipefail + mkdir -p ci-artifacts + { + echo "===== before cleanup =====" + df -h + echo + sudo du -sh /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache /usr/local/.ghcup 2>/dev/null || true + docker system df || true + } | tee "ci-artifacts/disk-cleanup-${{ matrix.name }}.txt" + + sudo rm -rf \ + /usr/share/dotnet \ + /usr/local/lib/android \ + /opt/ghc \ + /opt/hostedtoolcache/CodeQL \ + /opt/hostedtoolcache/go \ + /opt/hostedtoolcache/node \ + /opt/hostedtoolcache/Python \ + /usr/local/.ghcup || true + docker system prune -af --volumes || true + sudo apt-get clean + + { + echo + echo "===== after cleanup =====" + df -h + docker system df || true + } | tee -a "ci-artifacts/disk-cleanup-${{ matrix.name }}.txt" + + - 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: Limit compiler cache size + shell: bash + run: | + set -euxo pipefail + mkdir -p "$CCACHE_DIR" ci-artifacts + ccache --max-size="$CCACHE_MAXSIZE" + ccache --cleanup || true + ccache --show-stats | tee "ci-artifacts/ccache-before-${{ matrix.name }}.txt" + + - 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 + 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 + run: | + set -euxo pipefail + mkdir -p ci-artifacts + { + echo "===== before build =====" + df -h + echo + du -sh dl .ccache build_dir staging_dir tmp 2>/dev/null || true + } | tee "ci-artifacts/disk-before-build-${{ matrix.name }}.txt" + set +e + 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" + df -h | tee -a "ci-artifacts/disk-before-serial-retry-${{ matrix.name }}.txt" || true + 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 + 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/feeds/packages/libs/gpgme/Makefile b/feeds/packages/libs/gpgme/Makefile index 44bc374f79..0caa13269d 100644 --- a/feeds/packages/libs/gpgme/Makefile +++ b/feeds/packages/libs/gpgme/Makefile @@ -19,6 +19,8 @@ PKG_BUILD_PARALLEL:=1 include $(INCLUDE_DIR)/package.mk +CONFIGURE_VARS += ac_cv_path_GPGRT_CONFIG="$(STAGING_DIR)/usr/bin/gpgrt-config" + define Package/libgpgme SECTION:=libs CATEGORY:=Libraries 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/package/utils/argosfs/Makefile b/package/utils/argosfs/Makefile index 80a2e0a8d1..7b8d39f390 100644 --- a/package/utils/argosfs/Makefile +++ b/package/utils/argosfs/Makefile @@ -1,6 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=argosfs +PKG_VERSION:=0.1.0 PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git diff --git a/scripts/ci/capos-real-boot-config.sh b/scripts/ci/capos-real-boot-config.sh new file mode 100755 index 0000000000..05c4baf730 --- /dev/null +++ b/scripts/ci/capos-real-boot-config.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +target="$1" +subtarget="$2" + +patch_pinned_rust_feed_for_ci() { + local rust_values="feeds/packages/lang/rust/rust-values.mk" + + if [[ "$target/$subtarget" != "malta/le64" || ! -f "$rust_values" ]]; then + return 0 + fi + + if grep -q 'mips64el-unknown-linux-muslabi64' "$rust_values"; then + return 0 + fi + + python3 - <<'PY' +from pathlib import Path + +path = Path("feeds/packages/lang/rust/rust-values.mk") +text = path.read_text() +old = """else ifeq ($(ARCH),riscv64)\n RUSTC_TARGET_ARCH:=$(subst riscv64,riscv64gc,$(RUSTC_TARGET_ARCH))\nendif\n""" +new = """else ifeq ($(ARCH),riscv64)\n RUSTC_TARGET_ARCH:=$(subst riscv64,riscv64gc,$(RUSTC_TARGET_ARCH))\nelse ifeq ($(ARCH),mips64el)\n RUSTC_TARGET_ARCH:=$(subst linux-musl,linux-muslabi64,$(RUSTC_TARGET_ARCH))\nendif\n""" +if old not in text: + raise SystemExit("could not patch pinned rust feed target mapping") +path.write_text(text.replace(old, new, 1)) +PY +} + +case "$target/$subtarget" in + x86/64|armsr/armv8|malta/le64) + ;; + *) + echo "unsupported CapOS real boot CI target: $target/$subtarget" >&2 + exit 2 + ;; +esac + +patch_pinned_rust_feed_for_ci + +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/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]