summaryrefslogtreecommitdiff
path: root/.github
diff options
context:
space:
mode:
Diffstat (limited to '.github')
-rw-r--r--.github/workflows/module_qemu.yml151
-rw-r--r--.github/workflows/r52_fvp.yml5
-rw-r--r--.github/workflows/regression_template.yml106
3 files changed, 259 insertions, 3 deletions
diff --git a/.github/workflows/module_qemu.yml b/.github/workflows/module_qemu.yml
new file mode 100644
index 00000000..7cc62709
--- /dev/null
+++ b/.github/workflows/module_qemu.yml
@@ -0,0 +1,151 @@
+name: module_qemu
+
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+#
+# Builds the module execution harness in test/tx/cmake/module_qemu for each
+# Cortex-M module port it covers and EXECUTES every image on QEMU's MPS2
+# board for that core, asserting each image's self-reported result.
+#
+# Why this exists: until this workflow landed, no Cortex-M module port ran in
+# CI. gcc_check compiles the module manager sources once per port and links
+# nothing that loads a module; the host tests under test/tx/module_manager
+# compile a handful of portable module manager sources against stubs. A
+# module's start-up, its kernel calls through the supervisor call, the MPU
+# programming on every context switch and the fault path that terminates a
+# misbehaving module are only exercised by executing them, and r52_fvp does
+# that for the Cortex-R52 alone.
+#
+# What it covers, per port:
+#
+# - cortex_m3 on mps2-an385 and cortex_m4 on mps2-an386, the latter built
+# for its FPU with the hard-float ABI;
+# - a protected module loaded, started, exercising the kernel, stopped and
+# unloaded twice, with its memory given back;
+# - a protected module faulting on a read of kernel memory, reported to the
+# application and terminated -- which is also the proof that QEMU's MPU is
+# enforcing at all;
+# - the same module built privileged and unprotected, whose identical read
+# succeeds.
+#
+# QEMU comes from the Ubuntu archive. The images report through semihosting
+# and end the simulation themselves, so a hang shows up as the runner's
+# timeout rather than as a pass.
+
+on:
+ # push as well as pull_request, for the reason gcc_check.yml states at
+ # length: dev needs its own baseline so a bad squash-merge is caught then
+ # rather than by whoever opens the next pull request.
+ push:
+ branches: [ master, dev ]
+ paths:
+ - ".github/workflows/module_qemu.yml"
+ - "common/**"
+ - "common_modules/**"
+ - "ports_module/cortex_m3/**"
+ - "ports_module/cortex_m4/**"
+ - "test/tx/cmake/module_qemu/**"
+ - "scripts/tx_ci_common.sh"
+ pull_request:
+ branches: [ master, dev ]
+ # Duplicated rather than shared through a YAML anchor, as in r52_fvp.yml.
+ # **Edit both.**
+ paths:
+ - ".github/workflows/module_qemu.yml"
+ - "common/**"
+ - "common_modules/**"
+ - "ports_module/cortex_m3/**"
+ - "ports_module/cortex_m4/**"
+ - "test/tx/cmake/module_qemu/**"
+ - "scripts/tx_ci_common.sh"
+
+permissions:
+ contents: read
+
+jobs:
+ module_qemu:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+
+ env:
+ # Pinned deliberately and shared with gcc_check.yml, ci_cortex_m.yml and
+ # r52_fvp.yml through one cache entry; change them together.
+ GCC_VERSION: 14.3.rel1
+
+ steps:
+ - name: Check out the repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ # Path and key match gcc_check.yml's AArch32 entry exactly, so the
+ # workflows share one cache entry.
+ - name: Cache the AArch32 Arm GNU toolchain
+ id: cache-arm32
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: toolchain
+ key: arm-gnu-toolchain-${{ env.GCC_VERSION }}-x86_64-arm-none-eabi
+
+ # The checksum suffix is .sha256asc and not .sha256; see gcc_check.yml.
+ - name: Install the AArch32 Arm GNU toolchain
+ if: steps.cache-arm32.outputs.cache-hit != 'true'
+ run: |
+ set -eu
+ base="https://developer.arm.com/-/media/Files/downloads/gnu/${GCC_VERSION}/binrel"
+ archive="arm-gnu-toolchain-${GCC_VERSION}-x86_64-arm-none-eabi.tar.xz"
+ mkdir -p toolchain && cd toolchain
+ curl -fsSLO "$base/$archive"
+ curl -fsSLO "$base/$archive.sha256asc"
+ sha256sum -c "$archive.sha256asc"
+ tar xf "$archive"
+ rm -f "$archive"
+
+ # Through the bounded, retried apt helpers in tx_ci_common.sh, for the
+ # reasons that file records: on this runner pool a bare apt call can stall
+ # for hours. Ninja is installed only if the image stopped shipping it.
+ - name: Install QEMU
+ run: |
+ set -eu
+ . scripts/tx_ci_common.sh
+ packages="qemu-system-arm"
+ command -v ninja >/dev/null 2>&1 || packages="$packages ninja-build"
+ retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" update || true
+ # shellcheck disable=SC2086
+ retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" install -y --no-install-recommends $packages
+ qemu-system-arm --version | head -1
+
+ - name: Build and run the module harness
+ run: |
+ set -eu
+ export PATH="$GITHUB_WORKSPACE/toolchain/arm-gnu-toolchain-${GCC_VERSION}-x86_64-arm-none-eabi/bin:$PATH"
+ arm-none-eabi-gcc --version | head -1
+
+ status=0
+ for port in cortex_m3 cortex_m4; do
+ build="build/module_qemu_$port"
+ echo ""
+ echo "=============================================================="
+ echo "== $port"
+ echo "=============================================================="
+
+ if ! cmake -S test/tx/cmake/module_qemu -B "$build" -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE="$PWD/test/tx/cmake/module_qemu/arm-none-eabi-qemu.cmake" \
+ -DMODULE_QEMU_PORT="$port"; then
+ echo "::error::$port: CMake configure failed"
+ status=1
+ continue
+ fi
+ if ! cmake --build "$build"; then
+ echo "::error::$port: the images did not build"
+ status=1
+ continue
+ fi
+ # --no-tests=error: the tests are registered only when QEMU and
+ # Python are found, and ctest's default would pass on none.
+ if ! ctest --test-dir "$build" --output-on-failure --no-tests=error; then
+ echo "::error::$port: one or more images failed on QEMU, or no tests were registered"
+ status=1
+ fi
+ done
+
+ exit $status
diff --git a/.github/workflows/r52_fvp.yml b/.github/workflows/r52_fvp.yml
index 2293e1bd..b7184abd 100644
--- a/.github/workflows/r52_fvp.yml
+++ b/.github/workflows/r52_fvp.yml
@@ -22,7 +22,8 @@ name: r52_fvp
#
# - the default configuration -- the images gcc_check's CMake stage builds,
# but run rather than merely linked, plus the FVP module manager example,
-# whose two images are the module isolation tests;
+# whose two images are the module isolation tests. They link every portable
+# module manager source, so common_modules/ is among the trigger paths;
# - the feature configuration -- the same images with VFP and the hard float
# ABI, FIQ, IRQ nesting and FIQ nesting. Those four options gate whole
# assembly blocks (the VMRS/VSTMDB/VLDMIA sequences, the FIQ context paths,
@@ -44,6 +45,7 @@ on:
- "CMakeLists.txt"
- "cmake/cortex_r52*.cmake"
- "common/**"
+ - "common_modules/**"
- "ports/cortex_r52/**"
- "ports_module/cortex_r52/**"
pull_request:
@@ -58,6 +60,7 @@ on:
- "CMakeLists.txt"
- "cmake/cortex_r52*.cmake"
- "common/**"
+ - "common_modules/**"
- "ports/cortex_r52/**"
- "ports_module/cortex_r52/**"
diff --git a/.github/workflows/regression_template.yml b/.github/workflows/regression_template.yml
index 392f6a96..decfadd4 100644
--- a/.github/workflows/regression_template.yml
+++ b/.github/workflows/regression_template.yml
@@ -41,6 +41,21 @@ on:
default: 15
required: false
type: number
+ # Minutes for the test step. Sixty covers every ThreadX suite, whose
+ # longest observed run is 37 minutes. NetX Duo runs all 43 configurations
+ # of its full suite in this one step, which took 201 minutes on 25 Oct
+ # 2026, so it passes a larger value.
+ test_timeout_minutes:
+ default: 60
+ required: false
+ type: number
+ # Keep the .deb files apt downloads in the install step in the Actions
+ # cache, so a later run installs them without fetching them again. See the
+ # apt cache steps below.
+ apt_cache:
+ default: true
+ required: false
+ type: boolean
skip_test:
default: false
required: false
@@ -123,6 +138,59 @@ jobs:
with:
submodules: true
+ # The apt cache. The install scripts retry and bound every fetch, but a
+ # mirror can be slow without being down: on 23 Oct 2026 it served packages
+ # at 50 to 250 KB/s while indexes still came at 12 MB/s, and the ThreadX and
+ # RISC-V installs timed out on all three attempts inside an 11 MB and a 58 MB
+ # file. With the .deb files restored from the Actions cache, apt fetches
+ # only the indexes and whatever changed since the cache was saved.
+ #
+ # It is done here rather than in the install scripts so that every caller's
+ # script gets it unchanged: an apt.conf.d entry points apt-get's archive
+ # directory at the restored one. A restored file cannot alter what is
+ # installed, because apt checks every .deb against the hash in the signed
+ # index before using it, and fetches it again if the two differ.
+ #
+ # The key carries the runner image, whose package versions the cache
+ # matches, and the install script, whose package list it holds. A miss on
+ # either still restores the closest older cache, so a new image or one
+ # added package costs only the packages that changed.
+ - name: Prepare the apt cache key
+ id: apt_key
+ if: ${{ inputs.apt_cache }}
+ env:
+ INSTALL_SCRIPT: ${{ inputs.install_script }}
+ run: |
+ echo "dir=${RUNNER_TEMP}/apt-archives" >> "$GITHUB_OUTPUT"
+ echo "image=${ImageOS}-${ImageVersion}" >> "$GITHUB_OUTPUT"
+ echo "script=$( (sha256sum "$INSTALL_SCRIPT" 2> /dev/null || echo none) | cut -c1-16)" >> "$GITHUB_OUTPUT"
+
+ - name: Restore the apt cache
+ id: apt_restore
+ if: ${{ inputs.apt_cache }}
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ steps.apt_key.outputs.dir }}
+ key: apt-${{ steps.apt_key.outputs.image }}-${{ steps.apt_key.outputs.script }}
+ restore-keys: |
+ apt-${{ steps.apt_key.outputs.image }}-
+ apt-
+
+ # apt downloads as the unprivileged _apt user, which must own partial/.
+ # Keep-Downloaded-Packages is set because the apt front end deletes what it
+ # installed by default; apt-get keeps it, but a caller's script may use
+ # either.
+ - name: Point apt at the cache
+ if: ${{ inputs.apt_cache }}
+ env:
+ APT_DIR: ${{ steps.apt_key.outputs.dir }}
+ run: |
+ sudo mkdir -p "$APT_DIR/partial"
+ sudo chown _apt:root "$APT_DIR/partial"
+ printf 'Dir::Cache::Archives "%s/";\nAPT::Keep-Downloaded-Packages "true";\nBinary::apt::APT::Keep-Downloaded-Packages "true";\n' "$APT_DIR" \
+ | sudo tee /etc/apt/apt.conf.d/99-ci-archive-cache > /dev/null
+ echo "apt cache: $(find "$APT_DIR" -maxdepth 1 -name '*.deb' | wc -l) packages restored"
+
# apt and PyPI have stalled this step twice, for 55 minutes and for over two
# hours, against a normal 27 to 152 seconds. Nothing here had a timeout, so a
# stall ran until the six hour job limit and cost a whole run rather than
@@ -131,6 +199,29 @@ jobs:
timeout-minutes: ${{ inputs.install_timeout_minutes }}
run: ${{ inputs.install_script }}
+ # Saved straight after the install rather than at the end of the job, so a
+ # run whose tests fail still leaves the packages for the next one. autoclean
+ # drops .deb files the indexes no longer list, so the cache does not grow
+ # with every superseded version. apt's lock file and partial/ are root-only
+ # and are removed so the cache action, which runs unprivileged, can read the
+ # rest.
+ - name: Save the apt cache
+ if: ${{ inputs.apt_cache && steps.apt_restore.outputs.cache-hit != 'true' }}
+ env:
+ APT_DIR: ${{ steps.apt_key.outputs.dir }}
+ run: |
+ sudo apt-get autoclean -qq
+ sudo rm -rf "$APT_DIR/partial" "$APT_DIR/lock"
+ sudo chmod -R a+rX "$APT_DIR"
+ echo "apt cache: $(find "$APT_DIR" -maxdepth 1 -name '*.deb' | wc -l) packages, $(du -sh "$APT_DIR" | cut -f1)"
+
+ - name: Upload the apt cache
+ if: ${{ inputs.apt_cache && steps.apt_restore.outputs.cache-hit != 'true' }}
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ steps.apt_key.outputs.dir }}
+ key: apt-${{ steps.apt_key.outputs.image }}-${{ steps.apt_key.outputs.script }}
+
# TX_COVERAGE instruments every build configuration rather than only the one
# whose name ends in _coverage. It has to be set for the build as well as the
# test: the build is where -fprofile-arcs is decided, and the test run is
@@ -142,7 +233,7 @@ jobs:
run: ${{ inputs.build_script }}
- name: Test
- timeout-minutes: 60
+ timeout-minutes: ${{ inputs.test_timeout_minutes }}
env:
TX_COVERAGE: ${{ inputs.skip_coverage && 'OFF' || 'ON' }}
run: ${{ inputs.test_script }}
@@ -276,9 +367,20 @@ jobs:
# run_tests that is cancelled -- which is what a job that never gets a runner
# ends up as -- leaves both false and lets the deploy proceed to download an
# artifact the cancelled job never uploaded.
+ #
+ # The one exception is a caller passing skip_test, such as the deploy jobs
+ # of ThreadX and NetX Duo: run_tests is skipped by design, and the steps below
+ # download the coverage the caller's other jobs uploaded. The caller's needs
+ # gates those jobs. !cancelled() is required because an if without a status
+ # function implies success(), which is false when run_tests is skipped.
deploy_code_coverage:
runs-on: ubuntu-24.04
- if: ((github.event_name == 'push') || (github.event_name == 'workflow_dispatch')) && !inputs.skip_coverage && !inputs.skip_deploy && needs.run_tests.result == 'success'
+ if: >-
+ !cancelled() &&
+ ((github.event_name == 'push') || (github.event_name == 'workflow_dispatch')) &&
+ !inputs.skip_coverage && !inputs.skip_deploy &&
+ (needs.run_tests.result == 'success' ||
+ (inputs.skip_test && needs.run_tests.result == 'skipped'))
needs: run_tests
environment:
name: github-pages