summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorFrédéric Desbiens <[email protected]>2026-10-02 10:42:44 -0400
committerGitHub <[email protected]>2026-10-02 10:42:44 -0400
commit93387b0a60382a29579bbad5045c68328e3751df (patch)
tree42be790d602cb76f55090e5a7c418df7ea2b3428 /scripts
parent44d7c95c582d415c4ad84527180b29c93c3bf664 (diff)
parent28281cd5647d4671236ca08d19bd18ed15b2265a (diff)
Merge pull request #783 from eclipse-threadx/devv6.5.2.202603_rel
Merged changes ahead of the v6.5.2.202603 release
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/build_freertos.sh17
-rwxr-xr-xscripts/build_posix.sh17
-rwxr-xr-xscripts/check_ai_disclosure.sh129
-rwxr-xr-xscripts/check_clang.sh561
-rwxr-xr-xscripts/check_gcc.sh748
-rwxr-xr-xscripts/check_module_mmu_a7.sh120
-rwxr-xr-xscripts/check_ports.sh315
-rwxr-xr-xscripts/cmake_bootstrap.sh152
-rwxr-xr-xscripts/copy_armv8_m.sh4
-rwxr-xr-xscripts/count_assisted_by.sh79
-rwxr-xr-xscripts/install.sh60
-rwxr-xr-xscripts/install_riscv.sh52
-rwxr-xr-xscripts/prepare_release.sh50
-rwxr-xr-xscripts/test_freertos.sh17
-rwxr-xr-xscripts/test_posix.sh17
-rwxr-xr-xscripts/test_smp.sh2
-rwxr-xr-xscripts/test_tx.sh2
-rw-r--r--scripts/tx_ci_common.sh113
18 files changed, 2426 insertions, 29 deletions
diff --git a/scripts/build_freertos.sh b/scripts/build_freertos.sh
new file mode 100755
index 00000000..c3058b11
--- /dev/null
+++ b/scripts/build_freertos.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+##############################################################################
+# Copyright (C) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+$(dirname `realpath $0`)/../test/freertos/cmake/run.sh build all
diff --git a/scripts/build_posix.sh b/scripts/build_posix.sh
new file mode 100755
index 00000000..ac81c8ff
--- /dev/null
+++ b/scripts/build_posix.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+##############################################################################
+# Copyright (c) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+$(dirname `realpath $0`)/../test/posix/cmake/run.sh riscv32 build all
diff --git a/scripts/check_ai_disclosure.sh b/scripts/check_ai_disclosure.sh
new file mode 100755
index 00000000..5a08a44c
--- /dev/null
+++ b/scripts/check_ai_disclosure.sh
@@ -0,0 +1,129 @@
+#!/bin/bash
+###############################################################################
+# Copyright (c) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+###############################################################################
+#
+# Fail the build if a file's AI disclosure comment has drifted from the one
+# accepted form.
+#
+# A file that was edited with AI assistance carries exactly one line:
+#
+# Portions of this file were generated with AI assistance.
+#
+# written with the comment character that file already uses. It names no
+# product, no model and no version, and a file carries at most one of it, ever.
+#
+# The text is fixed for a reason that is easy to miss. An earlier convention
+# named the product and the model -- "Some portions generated by <product>
+# (<model>)" -- and the result was not attribution but accumulation: each tool
+# that touched a file failed to recognise the line another tool had left, and
+# appended its own. Files reached three stacked lines, and one product ended
+# up spelled four different ways across the tree, which made the record
+# unusable for the one question it was meant to answer.
+#
+# Precise attribution lives on the commit instead, where the Assisted-by
+# trailer is dated and attached to the diff it describes:
+#
+# scripts/count_assisted_by.sh --list <rev> -- <path>
+#
+# A file-level flag answers WHETHER; the history answers WHO. A header line
+# cannot hold the second honestly, because the code it names gets rewritten
+# and the line stays.
+#
+# The AI Disclosure paragraph in a new file's copyright header is different and
+# is not checked here: it keeps its product and model version, because a file
+# is created once and that record cannot grow.
+#
+# This script is excluded from its own scan. It has to spell the rejected
+# forms in order to look for them.
+
+set -euo pipefail
+
+readonly ROOT="$(cd "$(dirname "$(realpath "$0")")/.." && pwd)"
+readonly SELF='scripts/check_ai_disclosure.sh'
+readonly FIXED='Portions of this file were generated with AI assistance.'
+
+cd "${ROOT}"
+
+# Tracked files only. A build tree is not this repository's text to police,
+# and scanning one would make the check depend on whether somebody had built.
+mapfile -d '' -t FILES < <(git ls-files -z | grep -zZv "^${SELF}$")
+
+status=0
+
+report() {
+ printf '%s\n\n' "$1" >&2
+ printf '%s\n\n' "$2" >&2
+ status=1
+}
+
+# 1. The superseded per-edit form, which names a product and a model.
+hits="$(grep -nI 'Some portions generated by' -- "${FILES[@]}" 2>/dev/null || true)"
+if [ -n "${hits}" ]; then
+ report "AI disclosure check FAILED: superseded per-edit form.
+
+Replace each of these with the fixed line, keeping the file's comment
+character:
+
+ ${FIXED}" "${hits}"
+fi
+
+# 2. A doubled comment marker, such as '; //' or '@ //'. Assembly dialects
+# differ -- armasm and IAR use ';', GNU as uses '@' or '//' -- and writing
+# both is a symptom of a tool guessing rather than reading the file.
+hits="$(grep -nIE '(//|[;@#])[[:space:]]*//[[:space:]]*Portions of this file were generated' \
+ -- "${FILES[@]}" 2>/dev/null || true)"
+if [ -n "${hits}" ]; then
+ report "AI disclosure check FAILED: doubled comment marker.
+
+Use the single comment character the rest of the file uses." "${hits}"
+fi
+
+# 3. More than one disclosure line in a file. This is the failure the fixed
+# text exists to prevent, so it is worth catching directly rather than
+# inferring it from the form.
+hits="$(grep -cIF "${FIXED}" -- "${FILES[@]}" 2>/dev/null | awk -F: '$NF > 1' || true)"
+if [ -n "${hits}" ]; then
+ report "AI disclosure check FAILED: more than one disclosure line.
+
+A file carries at most one, ever. Keep the first and delete the rest; the
+commit trailer, not the header, records which agents have touched the file." "${hits}"
+fi
+
+# 4. A near miss. A line that is clearly meant to be the disclosure but is
+# not spelled exactly right defeats every deduplication that follows it.
+#
+# Markdown is exempt from this one check. The contribution guide has to
+# quote the accepted text and explain when it applies, so matching the bare
+# phrase there reports prose rather than drift. A superseded form in a
+# Markdown file is still caught by check 1.
+mapfile -d '' -t SOURCES < <(printf '%s\0' "${FILES[@]}" | grep -zZv '\.md$' || true)
+hits=''
+if [ "${#SOURCES[@]}" -gt 0 ]; then
+ hits="$(grep -nIF 'AI assistance' -- "${SOURCES[@]}" 2>/dev/null \
+ | grep -vF "${FIXED}" || true)"
+fi
+if [ -n "${hits}" ]; then
+ report "AI disclosure check FAILED: the text is not spelled exactly.
+
+The accepted text, character for character, is:
+
+ ${FIXED}" "${hits}"
+fi
+
+if [ "${status}" -eq 0 ]; then
+ echo "AI disclosure check passed."
+fi
+
+exit "${status}"
diff --git a/scripts/check_clang.sh b/scripts/check_clang.sh
new file mode 100755
index 00000000..8956404b
--- /dev/null
+++ b/scripts/check_clang.sh
@@ -0,0 +1,561 @@
+#!/bin/bash
+##############################################################################
+# Copyright (c) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+# Builds the Arm ports with an LLVM based toolchain, in six stages: assemble
+# every assembly source of every Arm gnu port, assemble again the parts guarded
+# by feature macros, compile the common C sources for one core per architecture
+# profile, compile the module manager C sources once per Arm module port, then
+# link the example builds, both the script-driven ones and those driven by
+# CMake. Only the linking stages need a target C library.
+#
+# scripts/check_clang.sh # clang from PATH
+# scripts/check_clang.sh --clang /path/to/clang
+# CLANG=/path/to/clang scripts/check_clang.sh
+#
+# Options:
+# --clang <path> Compiler to use; defaults to $CLANG then to clang.
+# --asm-only Skip the C sources and the example builds.
+# --no-examples Skip the example builds.
+# --quiet Print only failures and the summary.
+#
+# Exit status is 0 when everything builds and 1 otherwise.
+#
+# Why this exists: the gnu ports are only ever built with GNU tooling, and GNU
+# as accepts several non-canonical forms that LLVM's assembler rejects. Those
+# forms accumulated unnoticed. This check also covers Arm Toolchain for
+# Embedded, which is LLVM based and is the successor to Arm Compiler 6, so the
+# ac6 code paths are exercised here as well.
+#
+# Arm Toolchain for Embedded releases:
+# https://github.com/arm/arm-toolchain/releases
+
+set -u
+
+cd "$(dirname "$(realpath "$0")")/.."
+
+CC="${CLANG:-clang}"
+asm_only=0
+no_examples=0
+quiet=0
+
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --clang) [ "$#" -ge 2 ] || { echo "Error: --clang needs a path" >&2; exit 2; }; CC="$2"; shift 2 ;;
+ --asm-only) asm_only=1; no_examples=1; shift ;;
+ --no-examples) no_examples=1; shift ;;
+ --quiet) quiet=1; shift ;;
+ -h|--help) sed -n '17,33p' "$0"; exit 0 ;;
+ *) echo "Error: unknown option '$1'" >&2; exit 2 ;;
+ esac
+done
+
+say() { [ "$quiet" -eq 1 ] || echo "$@"; }
+fail() { echo " FAIL: $*"; }
+
+if ! command -v "$CC" >/dev/null 2>&1 && [ ! -x "$CC" ]; then
+ echo "Error: compiler '$CC' not found."
+ echo "Pass --clang <path>, set CLANG, or install Arm Toolchain for Embedded:"
+ echo " https://github.com/arm/arm-toolchain/releases"
+ exit 1
+fi
+
+# Resolve to an absolute path. The example stage runs the build scripts from
+# inside their own directories, so a relative compiler path would stop
+# resolving there.
+if [ -e "$CC" ]; then
+ CC="$(realpath "$CC")"
+else
+ CC="$(command -v "$CC")"
+fi
+
+say ""
+say "Using: $CC"
+say " $("$CC" --version | head -1)"
+
+# Each port directory is mapped explicitly to a target triple and CPU. Do not
+# replace this with prefix matching: cortex_a5* also matches cortex_a53 and
+# cortex_a55, which are AArch64, and assembling those as ARM32 produces a flood
+# of misleading errors.
+declare -A PORT_TARGET=(
+ [cortex_m0]="arm-none-eabi cortex-m0 -mthumb"
+ [cortex_m0+]="arm-none-eabi cortex-m0plus -mthumb"
+ [cortex_m3]="arm-none-eabi cortex-m3 -mthumb"
+ [cortex_m4]="arm-none-eabi cortex-m4 -mthumb"
+ [cortex_m7]="arm-none-eabi cortex-m7 -mthumb"
+ [cortex_m23]="arm-none-eabi cortex-m23 -mthumb"
+ [cortex_m33]="arm-none-eabi cortex-m33 -mthumb"
+ [cortex_m52]="arm-none-eabi cortex-m52 -mthumb -mfloat-abi=hard"
+ [cortex_m55]="arm-none-eabi cortex-m55 -mthumb -mfloat-abi=hard"
+ [cortex_m85]="arm-none-eabi cortex-m85 -mthumb -mfloat-abi=hard"
+ [cortex_a5]="arm-none-eabi cortex-a5"
+ [cortex_a7]="arm-none-eabi cortex-a7"
+ [cortex_a8]="arm-none-eabi cortex-a8"
+ [cortex_a9]="arm-none-eabi cortex-a9"
+ [cortex_a12]="arm-none-eabi cortex-a12"
+ [cortex_a15]="arm-none-eabi cortex-a15"
+ [cortex_a17]="arm-none-eabi cortex-a17"
+ [cortex_a5_smp]="arm-none-eabi cortex-a5"
+ [cortex_a7_smp]="arm-none-eabi cortex-a7"
+ [cortex_a9_smp]="arm-none-eabi cortex-a9"
+ [cortex_r4]="arm-none-eabi cortex-r4"
+ [cortex_r5]="arm-none-eabi cortex-r5"
+ [cortex_r52]="arm-none-eabi cortex-r52"
+ [cortex_a34]="aarch64-none-elf cortex-a34"
+ [cortex_a35]="aarch64-none-elf cortex-a35"
+ [cortex_a53]="aarch64-none-elf cortex-a53"
+ [cortex_a55]="aarch64-none-elf cortex-a55"
+ [cortex_a57]="aarch64-none-elf cortex-a57"
+ [cortex_a65]="aarch64-none-elf cortex-a65"
+ [cortex_a65ae]="aarch64-none-elf cortex-a65ae"
+ [cortex_a72]="aarch64-none-elf cortex-a72"
+ [cortex_a73]="aarch64-none-elf cortex-a73"
+ [cortex_a75]="aarch64-none-elf cortex-a75"
+ [cortex_a76]="aarch64-none-elf cortex-a76"
+ [cortex_a76ae]="aarch64-none-elf cortex-a76ae"
+ [cortex_a77]="aarch64-none-elf cortex-a77"
+ [cortex_a34_smp]="aarch64-none-elf cortex-a34"
+ [cortex_a35_smp]="aarch64-none-elf cortex-a35"
+ [cortex_a53_smp]="aarch64-none-elf cortex-a53"
+ [cortex_a55_smp]="aarch64-none-elf cortex-a55"
+ [cortex_a57_smp]="aarch64-none-elf cortex-a57"
+ [cortex_a65_smp]="aarch64-none-elf cortex-a65"
+ [cortex_a65ae_smp]="aarch64-none-elf cortex-a65ae"
+ [cortex_a72_smp]="aarch64-none-elf cortex-a72"
+ [cortex_a73_smp]="aarch64-none-elf cortex-a73"
+ [cortex_a75_smp]="aarch64-none-elf cortex-a75"
+ [cortex_a76_smp]="aarch64-none-elf cortex-a76"
+ [cortex_a76ae_smp]="aarch64-none-elf cortex-a76ae"
+ [cortex_a77_smp]="aarch64-none-elf cortex-a77"
+ [cortex_a78_smp]="aarch64-none-elf cortex-a78"
+)
+
+# Assembly guarded by a feature macro is invisible to the stage above, which
+# assembles with default flags and so lets the preprocessor discard every #ifdef
+# block before the assembler sees it. These are the macros a user can turn on;
+# each file carrying one is assembled again with it defined.
+#
+# This is not hypothetical. It is where "POP {r0, lr}" was found in the Cortex-M0
+# and Cortex-M23 execution-profile paths: invalid on Armv6-M and Armv8-M
+# Baseline, where the 16-bit POP takes r0-r7 and pc only, and rejected by GNU as
+# well as by LLVM. Turning the feature on had never once been tried.
+FEATURE_MACROS="TX_ENABLE_VFP_SUPPORT TX_ENABLE_FIQ_SUPPORT TX_LOW_POWER
+ TX_ENABLE_EXECUTION_CHANGE_NOTIFY TX_EXECUTION_PROFILE_ENABLE"
+
+# TX_ENABLE_IRQ_NESTING and TX_ENABLE_FIQ_NESTING are deliberately not here.
+# They guard no assembly in the trees this script walks: the nesting start and end
+# routines are separate files compiled unconditionally, and the macros only feed
+# the TX_PORT_SPECIFIC_BUILD_OPTIONS bitfield in tx_port.h. Adding them would
+# assemble nothing new and imply coverage that does not exist.
+
+# Extra flags for the VFP paths, per core, needed only where -mcpu alone cannot
+# assemble them. Cortex-R4's FPU is an option rather than part of the core, so
+# both toolchains reject its VFP code without an -mfpu.
+#
+# Do not extend this to the A profile ports. They save D16-D31, which exists only
+# on a 32-register FPU, so naming a -d16 FPU takes those registers away and turns
+# 28 working files into "register expected". Their defaults are already correct.
+declare -A VFP_EXTRA=(
+ [cortex_r4]="-mfpu=vfpv3-d16 -mfloat-abi=softfp"
+)
+
+# One core per architecture profile for the C sources. Compiling all of them
+# for every core would multiply the run time without adding coverage, since the
+# port headers differ by profile rather than by core.
+#
+# cortex_r52 earns a slot of its own next to cortex_r5 because Armv8-R AArch32
+# is a separate profile rather than a variant of Armv7-R. That port is written
+# by hand instead of generated from ports_arch, and its tx_port.h differs
+# accordingly, so cortex_r5 does not stand in for it.
+C_CORES="cortex_m0 cortex_m4 cortex_m23 cortex_m33 cortex_m55 cortex_a7 cortex_a53 cortex_r5 cortex_r52"
+
+# Example builds driven by CMake rather than by a build_threadx.sh pair. These
+# are covered by their own stage below, so the script-driven loop passes over
+# them without reporting them as a gap.
+CMAKE_EXAMPLE_CORES="cortex_r52"
+
+# Example builds that are not expected to link, with the reason. Named by
+# their port directory, which covers both ports/ and ports_smp/. Listed
+# explicitly rather than silently skipped, so the gaps stay visible.
+#
+# These fail with the GNU toolchain too, so they are not LLVM problems. All four
+# fail the same way, for the same reason:
+#
+# their linker scripts define the .init and .fini sections but not the _init
+# and _fini symbols. Those come from crti.o and crtn.o, which -nostartfiles
+# leaves out, so newlib's fini.c cannot resolve them and the link ends with
+# "undefined reference to `_fini'". Reproduced with arm-none-eabi-gcc 13.2.1.
+#
+# Until 6.1.10 the four linked libc.a and libgcc.a checked in beside them, which
+# supplied those symbols. That sweep removed the archives without updating the
+# link lines, so for years the examples failed earlier still, on the missing
+# files themselves. Fixing the link lines exposed the _fini gap underneath.
+EXAMPLES_EXPECTED_TO_FAIL="arm9 arm11 cortex_r4 cortex_r5"
+
+failures=0
+skipped=""
+
+# --------------------------------------------------------------------------
+say ""
+say "== Assembly sources of every Arm gnu port =="
+
+total=0
+# The module ports keep their assembly in module_manager/src, not src. This
+# glob read ports_module/*/gnu/src until 26 Aug 2026; that directory does not
+# exist, the [ -d ] guard below skipped it in silence, and 116 files across
+# nine Arm module ports were assembled by no check with either compiler. The
+# count went from 724 to 840 when the path was corrected, and three of the new
+# files did not assemble.
+for dir in ports/*/gnu/src ports_smp/*/gnu/src ports_module/*/gnu/module_manager/src; do
+ [ -d "$dir" ] || continue
+ core="$(echo "$dir" | cut -d/ -f2)"
+ spec="${PORT_TARGET[$core]:-}"
+ if [ -z "$spec" ]; then
+ skipped="$skipped $core"
+ continue
+ fi
+ # shellcheck disable=SC2086
+ set -- $spec
+ target="$1"; cpu="$2"; shift 2; extra="$*"
+
+ for src in "$dir"/*.S "$dir"/*.s; do
+ [ -f "$src" ] || continue
+ total=$((total + 1))
+ output="$("$CC" --target="$target" -mcpu="$cpu" $extra -c "$src" -o /dev/null 2>&1)"
+ if [ -n "$output" ]; then
+ fail "$src"
+ echo "$output" | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ done
+done
+say " $((total - failures)) of $total assembled"
+if [ -n "$skipped" ]; then
+ say " not Arm, skipped:$(echo $skipped | tr ' ' '\n' | sort -u | tr '\n' ' ')"
+fi
+
+# --------------------------------------------------------------------------
+say ""
+say "== Assembly behind feature macros =="
+
+for macro in $FEATURE_MACROS; do
+ macro_total=0
+ macro_bad=0
+ # The module ports are named here for the same reason as in the stage
+ # above: they were absent from this list until 26 Aug 2026 and so read as
+ # covered. Adding them found the Cortex-M23 module manager carrying the
+ # very POP {r0, lr} this comment describes, six months after the same fix
+ # landed in its non-module sibling.
+ for src in $(grep -rl "$macro" ports/*/gnu/src/*.S ports_smp/*/gnu/src/*.S \
+ ports_module/*/gnu/module_manager/src/*.S \
+ 2>/dev/null | sort); do
+ core="$(echo "$src" | cut -d/ -f2)"
+ spec="${PORT_TARGET[$core]:-}"
+ [ -n "$spec" ] || continue
+ # shellcheck disable=SC2086
+ set -- $spec
+ target="$1"; cpu="$2"; shift 2; extra="$*"
+
+ # The FPU flags apply to the VFP paths only; the other macros guard no
+ # floating-point code and do not need them.
+ fpu=""
+ if [ "$macro" = "TX_ENABLE_VFP_SUPPORT" ]; then
+ fpu="${VFP_EXTRA[$core]:-}"
+ fi
+
+ macro_total=$((macro_total + 1))
+ output="$("$CC" --target="$target" -mcpu="$cpu" $extra $fpu \
+ -D"$macro" -c "$src" -o /dev/null 2>&1)"
+ if [ -n "$output" ]; then
+ fail "$src with -D$macro"
+ echo "$output" | grep "error:" | head -3 | sed 's/^/ /'
+ macro_bad=$((macro_bad + 1))
+ failures=$((failures + 1))
+ fi
+ done
+ if [ "$macro_total" -eq 0 ]; then
+ say " $macro: no assembly is guarded by it"
+ else
+ say " $macro: $((macro_total - macro_bad)) of $macro_total assembled"
+ fi
+done
+
+# --------------------------------------------------------------------------
+if [ "$asm_only" -eq 0 ]; then
+ say ""
+ say "== Common C sources, one core per architecture profile =="
+
+ for core in $C_CORES; do
+ spec="${PORT_TARGET[$core]:-}"
+ [ -n "$spec" ] || continue
+ # shellcheck disable=SC2086
+ set -- $spec
+ target="$1"; cpu="$2"; shift 2; extra="$*"
+
+ count=0; bad=0
+ for src in common/src/*.c; do
+ count=$((count + 1))
+ output="$("$CC" --target="$target" -mcpu="$cpu" $extra \
+ -Iports/"$core"/gnu/inc -Icommon/inc -c "$src" -o /dev/null 2>&1)"
+ if [ -n "$output" ]; then
+ fail "$core: $src"
+ # Show the error lines when there are any, and otherwise
+ # whatever the compiler did say -- a FAIL with nothing under it
+ # sends the reader off to reproduce the command by hand.
+ if echo "$output" | grep -q "error:"; then
+ echo "$output" | grep "error:" | head -3 | sed 's/^/ /'
+ else
+ echo "$output" | head -3 | sed 's/^/ /'
+ fi
+ bad=$((bad + 1)); failures=$((failures + 1))
+ fi
+ done
+ say " $core: $((count - bad)) of $count compiled"
+ done
+fi
+
+
+# --------------------------------------------------------------------------
+if [ "$asm_only" -eq 0 ]; then
+ say ""
+ say "== Module manager C sources, one per Arm module port =="
+
+ # Correcting the assembly glob above brought the module ports into the count,
+ # but only their assembly. Their C stayed outside every check: 27 files of
+ # portable module manager under common_modules, plus the per-port code under
+ # ports_module/<core>/gnu/module_manager/src. Nothing compiled either one, so
+ # by this script's own standard they read as covered while being unbuilt.
+ #
+ # Each module port ships its own tx_port.h and txm_module_port.h, carrying the
+ # control-block extensions the dispatch code needs, so a port is compiled
+ # against its own headers rather than the base port's.
+ module_skipped=""
+ for dir in ports_module/*/gnu/module_manager/src; do
+ [ -d "$dir" ] || continue
+ core="$(echo "$dir" | cut -d/ -f2)"
+ spec="${PORT_TARGET[$core]:-}"
+ if [ -z "$spec" ]; then
+ module_skipped="$module_skipped $core"
+ continue
+ fi
+
+ inc="ports_module/$core/gnu/inc"
+ if [ ! -f "$inc/tx_port.h" ] || [ ! -f "$inc/txm_module_port.h" ]; then
+ module_skipped="$module_skipped $core(headers)"
+ continue
+ fi
+
+ # shellcheck disable=SC2086
+ set -- $spec
+ target="$1"; cpu="$2"; shift 2; extra="$*"
+
+ # An SMP port's control blocks come from common_smp; pairing it with the
+ # single-core headers hides _tx_thread_smp_protect behind an implicit
+ # declaration instead of compiling the port that is actually shipped.
+ case "$core" in
+ *_smp) kernel_inc="common_smp/inc" ;;
+ *) kernel_inc="common/inc" ;;
+ esac
+
+ # The TrustZone ports carry cmse_nonsecure_entry, which needs -mcmse to
+ # be honoured rather than ignored.
+ port_extra=""
+ if [ -f "$inc/tx_secure_interface.h" ]; then
+ port_extra="-mcmse"
+ fi
+
+ count=0; bad=0
+ for src in common_modules/module_manager/src/*.c "$dir"/*.c; do
+ [ -f "$src" ] || continue
+ count=$((count + 1))
+ # tx_thread_secure_stack.c carries GCC's optimize attribute, which
+ # clang does not implement and warns about. That is a toolchain
+ # divergence in a file GCC builds cleanly, so it is waived for that
+ # file alone -- a stray unknown attribute anywhere else in the port
+ # must still be reported.
+ src_extra=""
+ case "$src" in
+ */tx_thread_secure_stack.c) src_extra="-Wno-unknown-attributes" ;;
+ esac
+
+ # A #pragma message is a deliberate notice to callers, not a defect
+ # in the file that carries it. txm_module_manager_absolute_load.c
+ # deprecates itself in favour of the extended entry point, and this
+ # stage compiles it once per port.
+ output="$("$CC" --target="$target" -mcpu="$cpu" $extra $port_extra \
+ $src_extra "-Wno-#pragma-messages" \
+ -I"$inc" -I"$kernel_inc" -Icommon_modules/inc \
+ -Icommon_modules/module_manager/inc -c "$src" -o /dev/null 2>&1)"
+ if [ -n "$output" ]; then
+ fail "$core: $src"
+ # Show the error lines when there are any, and otherwise
+ # whatever the compiler did say -- a FAIL with nothing under it
+ # sends the reader off to reproduce the command by hand.
+ if echo "$output" | grep -q "error:"; then
+ echo "$output" | grep "error:" | head -3 | sed 's/^/ /'
+ else
+ echo "$output" | head -3 | sed 's/^/ /'
+ fi
+ bad=$((bad + 1)); failures=$((failures + 1))
+ fi
+ done
+ say " $core: $((count - bad)) of $count compiled"
+ done
+ if [ -n "$module_skipped" ]; then
+ say " no target mapping, skipped:$module_skipped"
+ fi
+fi
+
+
+# --------------------------------------------------------------------------
+if [ "$no_examples" -eq 0 ]; then
+ say ""
+ say "== Example builds, linked with lld =="
+
+ example_ok=0
+ example_total=0
+ example_known=""
+ example_nodriver=""
+ example_nosample=""
+ for dir in ports/*/gnu/example_build ports_smp/*/gnu/example_build; do
+ [ -d "$dir" ] || continue
+ core="$(echo "$dir" | cut -d/ -f2)"
+
+ # Anything on the expected-to-fail list is reported before any other
+ # filter is applied, so a name placed there can never drop out of the
+ # output. arm9 and arm11 are the cases that matter: they are Arm ports
+ # with example drivers, but they carry no PORT_TARGET entry, so the
+ # Arm test below would discard them.
+ case " $EXAMPLES_EXPECTED_TO_FAIL " in
+ *" $core "*) example_known="$example_known $core"; continue ;;
+ esac
+
+ # Arm ports only, the same rule the assembly stage applies. Naming
+ # linux or mips32 as a gap here would be noise, not information.
+ [ -n "${PORT_TARGET[$core]:-}" ] || continue
+
+ # Covered by the CMake stage below rather than here.
+ case " $CMAKE_EXAMPLE_CORES " in
+ *" $core "*) continue ;;
+ esac
+
+ # A driverless example is not covered by this stage, so say so rather
+ # than dropping out in silence. A port that is simply absent from the
+ # count reads as covered.
+ if [ ! -f "$dir/build_threadx.sh" ]; then
+ example_nodriver="$example_nodriver $core"
+ continue
+ fi
+ if [ ! -f "$dir/build_threadx_sample.sh" ]; then
+ example_nosample="$example_nosample $core"
+ continue
+ fi
+ example_total=$((example_total + 1))
+
+ rm -f "$dir"/*.o "$dir"/*.a "$dir"/*.out "$dir"/*.map 2>/dev/null || true
+ log="$(cd "$dir" && TOOLCHAIN=atfe ATFE_CLANG="$CC" ./build_threadx.sh 2>&1 && \
+ TOOLCHAIN=atfe ATFE_CLANG="$CC" ./build_threadx_sample.sh 2>&1)" || true
+
+ if [ -f "$dir/sample_threadx.out" ]; then
+ example_ok=$((example_ok + 1))
+ else
+ fail "$core: example build produced no image"
+ # Not filtered on "error": a missing tool reports "command not
+ # found" or "Permission denied", and filtering hid exactly that.
+ echo "$log" | tail -6 | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ rm -f "$dir"/*.o "$dir"/*.a "$dir"/*.out "$dir"/*.map 2>/dev/null || true
+ done
+ say " $example_ok of $example_total example builds linked"
+ if [ -n "$example_known" ]; then
+ say " known not to link, see the list at the top of this script:$example_known"
+ fi
+ if [ -n "$example_nosample" ]; then
+ say " has build_threadx.sh but no build_threadx_sample.sh, so not linked:$example_nosample"
+ fi
+ if [ -n "$example_nodriver" ]; then
+ say " no script driver, so outside this stage:$example_nodriver"
+ fi
+fi
+# --------------------------------------------------------------------------
+# The Cortex-R52 examples are built by CMake, so they need a toolchain file
+# rather than TOOLCHAIN=atfe. Same compiler, same linker, same purpose as the
+# stage above: confirm the images still link when the toolchain is not GNU.
+if [ "$no_examples" -eq 0 ]; then
+ say ""
+ say "== CMake example builds, linked with lld =="
+
+ if ! command -v cmake >/dev/null 2>&1 || ! command -v ninja >/dev/null 2>&1; then
+ say " skipped: cmake and ninja are both required"
+ else
+ for core in $CMAKE_EXAMPLE_CORES; do
+ build_dir="$(mktemp -d)"
+ # ATFE_TOOLCHAIN_PATH follows --clang, so the stage uses the same
+ # compiler as every other stage rather than whatever is on PATH.
+ if cmake -S . -B "$build_dir" -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE="cmake/${core}_clang.cmake" \
+ -DATFE_TOOLCHAIN_PATH="$(cd "$(dirname "$CC")" && pwd)" \
+ -DTX_R52_BUILD_FVP_EXAMPLE=ON \
+ -DTX_R52_ENABLE_MPU=ON >"$build_dir/configure.log" 2>&1; then
+ # Read the image list from the generated graph instead of
+ # repeating it here, so adding a target cannot silently escape
+ # this check. The images are EXCLUDE_FROM_ALL, so "ninja" alone
+ # would build none of them.
+ #
+ # The cmake_object_order_depends_target_* entries are CMake's
+ # own ordering phonies, one per real image and named after it.
+ # Counting those doubled the total and reported ten images built
+ # where there are five.
+ images="$(ninja -C "$build_dir" -t targets all 2>/dev/null \
+ | grep -oE '^[A-Za-z0-9_]+\.elf' \
+ | grep -v '^cmake_' | sort -u)"
+ if [ -z "$images" ]; then
+ fail "$core: no .elf targets found in the CMake graph"
+ failures=$((failures + 1))
+ else
+ built=0; total=0
+ for image in $images; do
+ total=$((total + 1))
+ if ninja -C "$build_dir" "$image" \
+ >"$build_dir/$image.log" 2>&1; then
+ built=$((built + 1))
+ else
+ fail "$core: $image did not build"
+ tail -6 "$build_dir/$image.log" | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ done
+ say " $core: $built of $total images linked"
+ fi
+ else
+ fail "$core: CMake configure failed"
+ tail -6 "$build_dir/configure.log" | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ rm -rf "$build_dir"
+ done
+ fi
+fi
+
+# --------------------------------------------------------------------------
+say ""
+if [ "$failures" -eq 0 ]; then
+ say "All LLVM toolchain checks passed."
+ exit 0
+fi
+
+echo "$failures LLVM toolchain check(s) failed."
+exit 1
diff --git a/scripts/check_gcc.sh b/scripts/check_gcc.sh
new file mode 100755
index 00000000..b8b2d794
--- /dev/null
+++ b/scripts/check_gcc.sh
@@ -0,0 +1,748 @@
+#!/bin/bash
+##############################################################################
+# Copyright (c) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+# Builds the Arm ports with the GNU toolchain, in seven stages: assemble every
+# assembly source of every Arm gnu port, assemble again the parts guarded by
+# feature macros, compile the common C sources for one core per architecture
+# profile, compile the module manager C sources once per Arm module port, link
+# the example builds, both the script-driven ones and those driven by CMake,
+# and finally assert that the option combinations the Cortex-R52 port refuses
+# are in fact refused. Only the linking stages need a target C library.
+#
+# scripts/check_gcc.sh # both drivers from PATH
+# scripts/check_gcc.sh --arm-none-eabi /path/to/toolchain/bin \
+# --aarch64-none-elf /path/to/toolchain/bin
+#
+# Options:
+# --arm-none-eabi <path> arm-none-eabi-gcc, or the directory holding it.
+# Defaults to $ARM_NONE_EABI_GCC then to PATH.
+# --aarch64-none-elf <path> aarch64-none-elf-gcc, or its directory.
+# Defaults to $AARCH64_NONE_ELF_GCC then to PATH.
+# --asm-only Skip the C sources and the example builds.
+# --no-examples Skip the example builds.
+# --quiet Print only failures and the summary.
+#
+# Exit status is 0 when everything builds and 1 otherwise.
+#
+# Why this exists: GCC 14 on Linux is the project's default compiler, it is
+# what the gnu ports exist for, and it is what nearly every downstream user
+# builds with -- and until this script landed, nothing in CI compiled a line
+# of any port with it. The only cross-compilation check that ran was the LLVM
+# one, so the ATfE path was better guarded than the GNU one, on ports whose
+# directory is literally named gnu.
+#
+# This is the companion to scripts/check_clang.sh and deliberately mirrors it
+# stage for stage. They are two scripts rather than one with a --toolchain flag
+# because the flag surface differs (a prefixed driver against --target=), the
+# C library differs, and the set of examples that can link differs. Folding
+# them together makes it easy to weaken one check while working on the other.
+#
+# It compiles and links; it executes nothing. The Cortex-R52 FVP ctest suite is
+# a separate matter, and the RISC-V, MIPS, RX and ARC families are outside it
+# entirely -- every skip is printed by name below.
+#
+# Arm GNU toolchain releases:
+# https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
+
+set -u
+
+cd "$(dirname "$(realpath "$0")")/.."
+
+# Two toolchains, not one. Arm ships arm-none-eabi (AArch32: every M and R
+# profile core and the A32 A-profile ports) and aarch64-none-elf as separate
+# downloads, and PORT_TARGET below maps every port to one of exactly those two
+# triples. Both are required: making a missing one a soft skip would let a run
+# cover half the tree and still say "all checks passed", which is the failure
+# this script was written to end.
+CC_ARM="${ARM_NONE_EABI_GCC:-}"
+CC_AARCH64="${AARCH64_NONE_ELF_GCC:-}"
+asm_only=0
+no_examples=0
+quiet=0
+
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --arm-none-eabi)
+ [ "$#" -ge 2 ] || { echo "Error: --arm-none-eabi needs a path" >&2; exit 2; }
+ CC_ARM="$2"; shift 2 ;;
+ --aarch64-none-elf)
+ [ "$#" -ge 2 ] || { echo "Error: --aarch64-none-elf needs a path" >&2; exit 2; }
+ CC_AARCH64="$2"; shift 2 ;;
+ --asm-only) asm_only=1; no_examples=1; shift ;;
+ --no-examples) no_examples=1; shift ;;
+ --quiet) quiet=1; shift ;;
+ -h|--help) sed -n '17,36p' "$0"; exit 0 ;;
+ *) echo "Error: unknown option '$1'" >&2; exit 2 ;;
+ esac
+done
+
+say() { [ "$quiet" -eq 1 ] || echo "$@"; }
+fail() { echo " FAIL: $*"; }
+
+# The C stages treat any compiler output as a failure, and a #pragma message is
+# a deliberate notice to callers rather than a defect in the file that carries
+# it -- txm_module_manager_absolute_load.c deprecates itself in favour of the
+# extended entry point, and the module manager stage compiles it once per port.
+#
+# check_clang.sh suppresses these at the compiler with -Wno-#pragma-messages.
+# GCC has no equivalent: the note is unconditional, and neither -Wno-pragmas
+# nor any other -W option silences it -- verified with 14.3.rel1. So it is
+# filtered out of the output here instead, along with the source quote and
+# caret GCC prints beneath it. The skip ends at the next line that starts a
+# diagnostic of its own, so an error following a waived note is still reported.
+strip_pragma_messages() {
+ awk '
+ /note: .#pragma message:/ { skip = 1; next }
+ skip && /^ *[0-9]* *\|/ { next }
+ { skip = 0; print }
+ '
+}
+
+# Accept either the driver itself or the directory holding it, since a
+# toolchain is unpacked as a tree and naming its bin directory is the natural
+# thing to reach for. Resolve to an absolute path: the example stages run the
+# build scripts from inside their own directories, so a relative path would
+# stop resolving there.
+resolve_gcc() {
+ triple="$1"
+ given="$2"
+
+ if [ -z "$given" ]; then
+ command -v "${triple}-gcc" >/dev/null 2>&1 || return 1
+ command -v "${triple}-gcc"
+ return 0
+ fi
+ if [ -d "$given" ]; then
+ for candidate in "$given/${triple}-gcc" "$given/bin/${triple}-gcc"; do
+ [ -x "$candidate" ] && { realpath "$candidate"; return 0; }
+ done
+ return 1
+ fi
+ [ -x "$given" ] || return 1
+ realpath "$given"
+}
+
+missing=""
+CC_ARM="$(resolve_gcc arm-none-eabi "$CC_ARM")" || missing="$missing arm-none-eabi"
+CC_AARCH64="$(resolve_gcc aarch64-none-elf "$CC_AARCH64")" || missing="$missing aarch64-none-elf"
+
+if [ -n "$missing" ]; then
+ echo "Error: compiler(s) not found:$missing"
+ echo "Pass --arm-none-eabi and --aarch64-none-elf, set ARM_NONE_EABI_GCC and"
+ echo "AARCH64_NONE_ELF_GCC, or put both drivers on PATH. Downloads:"
+ echo " https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads"
+ exit 1
+fi
+
+say ""
+say "Using: $CC_ARM"
+say " $("$CC_ARM" --version | head -1)"
+say " $CC_AARCH64"
+say " $("$CC_AARCH64" --version | head -1)"
+
+# The example build scripts and the CMake toolchain file call the drivers by
+# their bare prefixed names, so both bin directories go on PATH for the linking
+# stages. Done once, here, rather than per stage: a second toolchain arriving
+# on PATH halfway through a run is exactly the kind of difference that makes a
+# failure irreproducible.
+PATH="$(dirname "$CC_ARM"):$(dirname "$CC_AARCH64"):$PATH"
+export PATH
+
+# Pick the driver for a port from its triple, so the map below stays the single
+# place a port's architecture is decided.
+cc_for() {
+ case "$1" in
+ aarch64-none-elf) echo "$CC_AARCH64" ;;
+ *) echo "$CC_ARM" ;;
+ esac
+}
+
+# Each port directory is mapped explicitly to a target triple and CPU. Do not
+# replace this with prefix matching: cortex_a5* also matches cortex_a53 and
+# cortex_a55, which are AArch64, and assembling those as ARM32 produces a flood
+# of misleading errors.
+#
+# Copied verbatim from scripts/check_clang.sh. Keep the two identical -- a port
+# covered by one check and not the other is worse than one covered by neither,
+# because the checks list implies parity that does not exist.
+declare -A PORT_TARGET=(
+ [cortex_m0]="arm-none-eabi cortex-m0 -mthumb"
+ [cortex_m0+]="arm-none-eabi cortex-m0plus -mthumb"
+ [cortex_m3]="arm-none-eabi cortex-m3 -mthumb"
+ [cortex_m4]="arm-none-eabi cortex-m4 -mthumb"
+ [cortex_m7]="arm-none-eabi cortex-m7 -mthumb"
+ [cortex_m23]="arm-none-eabi cortex-m23 -mthumb"
+ [cortex_m33]="arm-none-eabi cortex-m33 -mthumb"
+ [cortex_m52]="arm-none-eabi cortex-m52 -mthumb -mfloat-abi=hard"
+ [cortex_m55]="arm-none-eabi cortex-m55 -mthumb -mfloat-abi=hard"
+ [cortex_m85]="arm-none-eabi cortex-m85 -mthumb -mfloat-abi=hard"
+ [cortex_a5]="arm-none-eabi cortex-a5"
+ [cortex_a7]="arm-none-eabi cortex-a7"
+ [cortex_a8]="arm-none-eabi cortex-a8"
+ [cortex_a9]="arm-none-eabi cortex-a9"
+ [cortex_a12]="arm-none-eabi cortex-a12"
+ [cortex_a15]="arm-none-eabi cortex-a15"
+ [cortex_a17]="arm-none-eabi cortex-a17"
+ [cortex_a5_smp]="arm-none-eabi cortex-a5"
+ [cortex_a7_smp]="arm-none-eabi cortex-a7"
+ [cortex_a9_smp]="arm-none-eabi cortex-a9"
+ [cortex_r4]="arm-none-eabi cortex-r4"
+ [cortex_r5]="arm-none-eabi cortex-r5"
+ [cortex_r52]="arm-none-eabi cortex-r52"
+ [cortex_a34]="aarch64-none-elf cortex-a34"
+ [cortex_a35]="aarch64-none-elf cortex-a35"
+ [cortex_a53]="aarch64-none-elf cortex-a53"
+ [cortex_a55]="aarch64-none-elf cortex-a55"
+ [cortex_a57]="aarch64-none-elf cortex-a57"
+ [cortex_a65]="aarch64-none-elf cortex-a65"
+ [cortex_a65ae]="aarch64-none-elf cortex-a65ae"
+ [cortex_a72]="aarch64-none-elf cortex-a72"
+ [cortex_a73]="aarch64-none-elf cortex-a73"
+ [cortex_a75]="aarch64-none-elf cortex-a75"
+ [cortex_a76]="aarch64-none-elf cortex-a76"
+ [cortex_a76ae]="aarch64-none-elf cortex-a76ae"
+ [cortex_a77]="aarch64-none-elf cortex-a77"
+ [cortex_a34_smp]="aarch64-none-elf cortex-a34"
+ [cortex_a35_smp]="aarch64-none-elf cortex-a35"
+ [cortex_a53_smp]="aarch64-none-elf cortex-a53"
+ [cortex_a55_smp]="aarch64-none-elf cortex-a55"
+ [cortex_a57_smp]="aarch64-none-elf cortex-a57"
+ [cortex_a65_smp]="aarch64-none-elf cortex-a65"
+ [cortex_a65ae_smp]="aarch64-none-elf cortex-a65ae"
+ [cortex_a72_smp]="aarch64-none-elf cortex-a72"
+ [cortex_a73_smp]="aarch64-none-elf cortex-a73"
+ [cortex_a75_smp]="aarch64-none-elf cortex-a75"
+ [cortex_a76_smp]="aarch64-none-elf cortex-a76"
+ [cortex_a76ae_smp]="aarch64-none-elf cortex-a76ae"
+ [cortex_a77_smp]="aarch64-none-elf cortex-a77"
+ [cortex_a78_smp]="aarch64-none-elf cortex-a78"
+)
+
+# Assembly guarded by a feature macro is invisible to the stage above, which
+# assembles with default flags and so lets the preprocessor discard every #ifdef
+# block before the assembler sees it. These are the macros a user can turn on;
+# each file carrying one is assembled again with it defined.
+#
+# This is not hypothetical, and it is not only clang's finding. The Cortex-M23
+# module manager carried "POP {r0, lr}" in exactly these paths -- invalid on
+# Armv8-M Baseline, where the 16-bit POP takes r0-r7 and pc only -- for six
+# months after the identical fix landed in its non-module sibling, because no
+# glob in either script reached ports_module until #672.
+FEATURE_MACROS="TX_ENABLE_VFP_SUPPORT TX_ENABLE_FIQ_SUPPORT TX_LOW_POWER
+ TX_ENABLE_EXECUTION_CHANGE_NOTIFY TX_EXECUTION_PROFILE_ENABLE"
+
+# TX_ENABLE_IRQ_NESTING and TX_ENABLE_FIQ_NESTING are deliberately not here.
+# They guard no assembly in the trees this script walks: the nesting start and
+# end routines are separate files compiled unconditionally, and the macros only
+# feed the TX_PORT_SPECIFIC_BUILD_OPTIONS bitfield in tx_port.h. Adding them
+# would assemble nothing new and imply coverage that does not exist.
+
+# Extra flags for the VFP paths, per core.
+#
+# check_clang.sh's equivalent map has one entry and a comment saying "Do not
+# extend this to the A profile ports ... their defaults are already correct."
+# That is true of clang and false of GCC, so do not copy that comment here.
+# arm-none-eabi-gcc defaults to -mfloat-abi=soft, which disables the FPU
+# outright, and every VFP file then fails with
+#
+# selected processor does not support 'vmrs r1,FPSCR' in ARM mode
+#
+# -mfloat-abi=hard alone is the fix, and it is the right one: it selects the
+# core's own default FPU rather than naming one. Naming a -d16 FPU is the trap
+# the clang script warns about -- the A-profile paths save D16-D31, which exist
+# only on a 32-register FPU, so a -d16 choice turns 28 working files into
+# "register expected".
+#
+# Cortex-R4 is the exception, in both scripts and for the same reason: its FPU
+# is an option rather than part of the core, so -mfloat-abi=hard alone gives
+# "selected architecture lacks an FPU" and an explicit -mfpu is required. The
+# value matches check_clang.sh's, so the two scripts say the same thing about
+# the same port.
+#
+# Every entry below was measured against arm-gnu-toolchain 14.3.rel1. A core
+# that acquires a VFP-guarded file without an entry here fails loudly rather
+# than silently, which is the intended behaviour.
+declare -A VFP_EXTRA=(
+ [cortex_a5]="-mfloat-abi=hard"
+ [cortex_a7]="-mfloat-abi=hard"
+ [cortex_a8]="-mfloat-abi=hard"
+ [cortex_a9]="-mfloat-abi=hard"
+ [cortex_a12]="-mfloat-abi=hard"
+ [cortex_a15]="-mfloat-abi=hard"
+ [cortex_a17]="-mfloat-abi=hard"
+ [cortex_a5_smp]="-mfloat-abi=hard"
+ [cortex_a7_smp]="-mfloat-abi=hard"
+ [cortex_a9_smp]="-mfloat-abi=hard"
+ [cortex_r4]="-mfpu=vfpv3-d16 -mfloat-abi=softfp"
+ [cortex_r5]="-mfloat-abi=hard"
+ [cortex_r52]="-mfloat-abi=hard"
+)
+
+# One core per architecture profile for the C sources. Compiling all of them
+# for every core would multiply the run time without adding coverage, since the
+# port headers differ by profile rather than by core.
+#
+# cortex_r52 earns a slot of its own next to cortex_r5 because Armv8-R AArch32
+# is a separate profile rather than a variant of Armv7-R. That port is written
+# by hand instead of generated from ports_arch, and its tx_port.h differs
+# accordingly, so cortex_r5 does not stand in for it.
+C_CORES="cortex_m0 cortex_m4 cortex_m23 cortex_m33 cortex_m55 cortex_a7 cortex_a53 cortex_r5 cortex_r52"
+
+# Example builds driven by CMake rather than by a build_threadx.sh pair. These
+# are covered by their own stage below, so the script-driven loop passes over
+# them without reporting them as a gap.
+CMAKE_EXAMPLE_CORES="cortex_r52"
+
+# Example builds that are not expected to link, with the reason. Named by
+# their port directory, which covers both ports/ and ports_smp/. Listed
+# explicitly rather than silently skipped, so the gaps stay visible.
+#
+# The same four as check_clang.sh, and the reason there is stated in terms of
+# GNU tooling because that is where it was reproduced:
+#
+# their linker scripts define the .init and .fini sections but not the _init
+# and _fini symbols. Those come from crti.o and crtn.o, which -nostartfiles
+# leaves out, so newlib's fini.c cannot resolve them and the link ends with
+# "undefined reference to `_fini'".
+#
+# The 27 AArch64 examples failed the same way until #673, which links crti.o
+# and crtn.o back. They are deliberately absent from this list: they link.
+EXAMPLES_EXPECTED_TO_FAIL="arm9 arm11 cortex_r4 cortex_r5"
+
+failures=0
+skipped=""
+
+# --------------------------------------------------------------------------
+say ""
+say "== Assembly sources of every Arm gnu port =="
+
+total=0
+# The module ports keep their assembly in module_manager/src, not src.
+# check_clang.sh read ports_module/*/gnu/src until #672; that directory does
+# not exist, the [ -d ] guard skipped it in silence, and 116 files across nine
+# Arm module ports were assembled by no check with either compiler. This script
+# has never had that hole and must not acquire it.
+for dir in ports/*/gnu/src ports_smp/*/gnu/src ports_module/*/gnu/module_manager/src; do
+ [ -d "$dir" ] || continue
+ core="$(echo "$dir" | cut -d/ -f2)"
+ spec="${PORT_TARGET[$core]:-}"
+ if [ -z "$spec" ]; then
+ skipped="$skipped $core"
+ continue
+ fi
+ # shellcheck disable=SC2086
+ set -- $spec
+ target="$1"; cpu="$2"; shift 2; extra="$*"
+ CC="$(cc_for "$target")"
+
+ for src in "$dir"/*.S "$dir"/*.s; do
+ [ -f "$src" ] || continue
+ total=$((total + 1))
+ output="$("$CC" -mcpu="$cpu" $extra -c "$src" -o /dev/null 2>&1)"
+ if [ -n "$output" ]; then
+ fail "$src"
+ echo "$output" | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ done
+done
+say " $((total - failures)) of $total assembled"
+if [ -n "$skipped" ]; then
+ say " not Arm, skipped:$(echo $skipped | tr ' ' '\n' | sort -u | tr '\n' ' ')"
+ say " (arm9 and arm11 are Arm; they are skipped for having no PORT_TARGET entry)"
+fi
+
+# --------------------------------------------------------------------------
+say ""
+say "== Assembly behind feature macros =="
+
+for macro in $FEATURE_MACROS; do
+ macro_total=0
+ macro_bad=0
+ for src in $(grep -rl "$macro" ports/*/gnu/src/*.S ports_smp/*/gnu/src/*.S \
+ ports_module/*/gnu/module_manager/src/*.S \
+ 2>/dev/null | sort); do
+ core="$(echo "$src" | cut -d/ -f2)"
+ spec="${PORT_TARGET[$core]:-}"
+ [ -n "$spec" ] || continue
+ # shellcheck disable=SC2086
+ set -- $spec
+ target="$1"; cpu="$2"; shift 2; extra="$*"
+ CC="$(cc_for "$target")"
+
+ # The FPU flags apply to the VFP paths only; the other macros guard no
+ # floating-point code and do not need them.
+ fpu=""
+ if [ "$macro" = "TX_ENABLE_VFP_SUPPORT" ]; then
+ fpu="${VFP_EXTRA[$core]:-}"
+ fi
+
+ macro_total=$((macro_total + 1))
+ output="$("$CC" -mcpu="$cpu" $extra $fpu \
+ -D"$macro" -c "$src" -o /dev/null 2>&1)"
+ if [ -n "$output" ]; then
+ fail "$src with -D$macro"
+ # Not filtered on "error": GCC prefixes its diagnostics with an
+ # "Assembler messages:" line and a missing tool says nothing of the
+ # kind, so filtering would hide the case worth seeing.
+ echo "$output" | head -4 | sed 's/^/ /'
+ macro_bad=$((macro_bad + 1))
+ failures=$((failures + 1))
+ fi
+ done
+ if [ "$macro_total" -eq 0 ]; then
+ say " $macro: no assembly is guarded by it"
+ else
+ say " $macro: $((macro_total - macro_bad)) of $macro_total assembled"
+ fi
+done
+
+# --------------------------------------------------------------------------
+if [ "$asm_only" -eq 0 ]; then
+ say ""
+ say "== Common C sources, one core per architecture profile =="
+
+ for core in $C_CORES; do
+ spec="${PORT_TARGET[$core]:-}"
+ [ -n "$spec" ] || continue
+ # shellcheck disable=SC2086
+ set -- $spec
+ target="$1"; cpu="$2"; shift 2; extra="$*"
+ CC="$(cc_for "$target")"
+
+ count=0; bad=0
+ for src in common/src/*.c; do
+ count=$((count + 1))
+ output="$("$CC" -mcpu="$cpu" $extra \
+ -Iports/"$core"/gnu/inc -Icommon/inc -c "$src" -o /dev/null 2>&1)"
+ if [ -n "$output" ]; then
+ fail "$core: $src"
+ echo "$output" | head -4 | sed 's/^/ /'
+ bad=$((bad + 1)); failures=$((failures + 1))
+ fi
+ done
+ say " $core: $((count - bad)) of $count compiled"
+ done
+fi
+
+# --------------------------------------------------------------------------
+if [ "$asm_only" -eq 0 ]; then
+ say ""
+ say "== Module manager C sources, one per Arm module port =="
+
+ # #689 added this stage to check_clang.sh alone, so the module manager C
+ # stayed unbuilt by the project's declared default compiler: 28 files of
+ # portable module manager under common_modules, plus the three to nine
+ # per-port files under ports_module/<core>/gnu/module_manager/src. This is
+ # the GCC half, and it is deliberately the same stage -- same ports, same
+ # headers, same counts -- because a port covered by one check and not the
+ # other implies a parity the checks list does not have.
+ #
+ # Each module port ships its own tx_port.h and txm_module_port.h, carrying
+ # the control-block extensions the dispatch code needs, so a port is
+ # compiled against its own headers rather than the base port's.
+ module_skipped=""
+ for dir in ports_module/*/gnu/module_manager/src; do
+ [ -d "$dir" ] || continue
+ core="$(echo "$dir" | cut -d/ -f2)"
+ spec="${PORT_TARGET[$core]:-}"
+ if [ -z "$spec" ]; then
+ module_skipped="$module_skipped $core"
+ continue
+ fi
+
+ inc="ports_module/$core/gnu/inc"
+ if [ ! -f "$inc/tx_port.h" ] || [ ! -f "$inc/txm_module_port.h" ]; then
+ module_skipped="$module_skipped $core(headers)"
+ continue
+ fi
+
+ # shellcheck disable=SC2086
+ set -- $spec
+ target="$1"; cpu="$2"; shift 2; extra="$*"
+ CC="$(cc_for "$target")"
+
+ # An SMP port's control blocks come from common_smp; pairing it with the
+ # single-core headers hides _tx_thread_smp_protect behind an implicit
+ # declaration instead of compiling the port that is actually shipped.
+ case "$core" in
+ *_smp) kernel_inc="common_smp/inc" ;;
+ *) kernel_inc="common/inc" ;;
+ esac
+
+ # The TrustZone ports carry cmse_nonsecure_entry, which needs -mcmse to
+ # be honoured rather than ignored.
+ port_extra=""
+ if [ -f "$inc/tx_secure_interface.h" ]; then
+ port_extra="-mcmse"
+ fi
+
+ count=0; bad=0
+ for src in common_modules/module_manager/src/*.c "$dir"/*.c; do
+ [ -f "$src" ] || continue
+ count=$((count + 1))
+ output="$("$CC" -mcpu="$cpu" $extra $port_extra \
+ -I"$inc" -I"$kernel_inc" -Icommon_modules/inc \
+ -Icommon_modules/module_manager/inc \
+ -c "$src" -o /dev/null 2>&1 | strip_pragma_messages)"
+ if [ -n "$output" ]; then
+ fail "$core: $src"
+ # Show the error lines when there are any, and otherwise
+ # whatever the compiler did say -- a FAIL with nothing under it
+ # sends the reader off to reproduce the command by hand.
+ if echo "$output" | grep -q "error:"; then
+ echo "$output" | grep "error:" | head -3 | sed 's/^/ /'
+ else
+ echo "$output" | head -3 | sed 's/^/ /'
+ fi
+ bad=$((bad + 1)); failures=$((failures + 1))
+ fi
+ done
+ say " $core: $((count - bad)) of $count compiled"
+ done
+ if [ -n "$module_skipped" ]; then
+ say " no target mapping, skipped:$module_skipped"
+ fi
+fi
+
+# --------------------------------------------------------------------------
+if [ "$no_examples" -eq 0 ]; then
+ say ""
+ say "== Example builds, linked with GNU ld =="
+
+ example_ok=0
+ example_total=0
+ example_known=""
+ example_nodriver=""
+ example_nosample=""
+ for dir in ports/*/gnu/example_build ports_smp/*/gnu/example_build; do
+ [ -d "$dir" ] || continue
+ core="$(echo "$dir" | cut -d/ -f2)"
+
+ # Anything on the expected-to-fail list is reported before any other
+ # filter is applied, so a name placed there can never drop out of the
+ # output. arm9 and arm11 are the cases that matter: they are Arm ports
+ # with example drivers, but they carry no PORT_TARGET entry, so the
+ # Arm test below would discard them.
+ case " $EXAMPLES_EXPECTED_TO_FAIL " in
+ *" $core "*) example_known="$example_known $core"; continue ;;
+ esac
+
+ # Arm ports only, the same rule the assembly stage applies. Naming
+ # linux or mips32 as a gap here would be noise, not information.
+ [ -n "${PORT_TARGET[$core]:-}" ] || continue
+
+ # Covered by the CMake stage below rather than here.
+ case " $CMAKE_EXAMPLE_CORES " in
+ *" $core "*) continue ;;
+ esac
+
+ # A driverless example is not covered by this stage, so say so rather
+ # than dropping out in silence. A port that is simply absent from the
+ # count reads as covered.
+ if [ ! -f "$dir/build_threadx.sh" ]; then
+ example_nodriver="$example_nodriver $core"
+ continue
+ fi
+ if [ ! -f "$dir/build_threadx_sample.sh" ]; then
+ example_nosample="$example_nosample $core"
+ continue
+ fi
+ example_total=$((example_total + 1))
+
+ # Clean on both sides. A stale sample_threadx.out from a previous
+ # toolchain makes a failed link report success, because the success
+ # test below is the existence of the output file rather than the exit
+ # status of the driver.
+ rm -f "$dir"/*.o "$dir"/*.a "$dir"/*.out "$dir"/*.map 2>/dev/null || true
+
+ # TOOLCHAIN is *unset*, not set: the build scripts already default to
+ # GNU with `: "${TOOLCHAIN:=gnu}"`, and a stray TOOLCHAIN=atfe from a
+ # developer's shell or an earlier command would otherwise make this
+ # stage silently check the other compiler.
+ log="$(cd "$dir" && unset TOOLCHAIN && ./build_threadx.sh 2>&1 && \
+ ./build_threadx_sample.sh 2>&1)" || true
+
+ if [ -f "$dir/sample_threadx.out" ]; then
+ example_ok=$((example_ok + 1))
+ else
+ fail "$core: example build produced no image"
+ # Not filtered on "error": a missing tool reports "command not
+ # found" or "Permission denied", and filtering hid exactly that.
+ echo "$log" | tail -6 | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ rm -f "$dir"/*.o "$dir"/*.a "$dir"/*.out "$dir"/*.map 2>/dev/null || true
+ done
+ say " $example_ok of $example_total example builds linked"
+ if [ -n "$example_known" ]; then
+ say " known not to link, see the list at the top of this script:$example_known"
+ fi
+ if [ -n "$example_nosample" ]; then
+ say " has build_threadx.sh but no build_threadx_sample.sh, so not linked:$example_nosample"
+ fi
+ if [ -n "$example_nodriver" ]; then
+ say " no script driver, so outside this stage:$example_nodriver"
+ fi
+fi
+
+# --------------------------------------------------------------------------
+# The Cortex-R52 examples are built by CMake, so they need a toolchain file
+# rather than the build_threadx.sh pair. Same compiler, same purpose as the
+# stage above.
+if [ "$no_examples" -eq 0 ]; then
+ say ""
+ say "== CMake example builds, linked with GNU ld =="
+
+ if ! command -v cmake >/dev/null 2>&1 || ! command -v ninja >/dev/null 2>&1; then
+ say " skipped: cmake and ninja are both required"
+ else
+ for core in $CMAKE_EXAMPLE_CORES; do
+ build_dir="$(mktemp -d)"
+ # ARM_TOOLCHAIN_PATH follows --arm-none-eabi, so this stage uses
+ # the same compiler as every other stage rather than the absolute
+ # path cmake/cortex_r52.cmake pins by default.
+ if cmake -S . -B "$build_dir" -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE="cmake/${core}.cmake" \
+ -DARM_TOOLCHAIN_PATH="$(dirname "$CC_ARM")" \
+ -DTX_R52_BUILD_FVP_EXAMPLE=ON \
+ -DTX_R52_ENABLE_MPU=ON >"$build_dir/configure.log" 2>&1; then
+ # Read the image list from the generated graph instead of
+ # repeating it here, so adding a target cannot silently escape
+ # this check. The images are EXCLUDE_FROM_ALL, so "ninja" alone
+ # would build none of them.
+ #
+ # The cmake_object_order_depends_target_* entries are CMake's
+ # own ordering phonies, one per real image and named after it.
+ # Counting those doubled the total and reported ten images built
+ # where there are five.
+ images="$(ninja -C "$build_dir" -t targets all 2>/dev/null \
+ | grep -oE '^[A-Za-z0-9_]+\.elf' \
+ | grep -v '^cmake_' | sort -u)"
+ if [ -z "$images" ]; then
+ fail "$core: no .elf targets found in the CMake graph"
+ failures=$((failures + 1))
+ else
+ built=0; total=0
+ for image in $images; do
+ total=$((total + 1))
+ if ninja -C "$build_dir" "$image" \
+ >"$build_dir/$image.log" 2>&1; then
+ built=$((built + 1))
+ else
+ fail "$core: $image did not build"
+ tail -6 "$build_dir/$image.log" | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ done
+ say " $core: $built of $total images linked"
+ fi
+ else
+ fail "$core: CMake configure failed"
+ tail -6 "$build_dir/configure.log" | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ rm -rf "$build_dir"
+ done
+ fi
+fi
+
+# --------------------------------------------------------------------------
+# Option combinations the port refuses at configure time.
+#
+# ports/cortex_r52/gnu/CMakeLists.txt rejects two combinations outright rather
+# than letting them reach the compiler. Both are cheap to get wrong in a way
+# that is invisible: a typo in a variable name makes the condition constant,
+# and a guard that never fires is indistinguishable from one that is never
+# tripped. So these assert the exit status AND the text, because a guard that
+# fires for the wrong reason would otherwise pass.
+#
+# No example flags are needed: the port CMakeLists is included by the toolchain
+# file alone, so the guards are reached without configuring anything else. That
+# keeps the stage to three configures, two of which stop almost immediately.
+#
+# The third is the positive counterpart, and it is not decoration: a guard that
+# rejects everything would satisfy both negative cases on its own. Asserting
+# that the supported combination still configures is what distinguishes a guard
+# that works from one that is merely always on.
+#
+# This is the only negative check in the script. Everything else here asserts
+# that something builds; this stage asserts that two things refuse to, and that
+# a third still does not.
+if [ "$asm_only" -eq 0 ]; then
+ say ""
+ say "== Option combinations the Cortex-R52 port must refuse =="
+
+ if ! command -v cmake >/dev/null 2>&1; then
+ say " skipped: cmake is required"
+ else
+ # name | expected text in the error | options
+ refuse_case() {
+ case_name="$1"; want="$2"; shift 2
+ build_dir="$(mktemp -d)"
+ if cmake -S . -B "$build_dir" -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE=cmake/cortex_r52.cmake \
+ -DARM_TOOLCHAIN_PATH="$(dirname "$CC_ARM")" \
+ "$@" >"$build_dir/configure.log" 2>&1; then
+ fail "$case_name: configure succeeded, but this combination cannot build"
+ failures=$((failures + 1))
+ elif ! grep -q "$want" "$build_dir/configure.log"; then
+ fail "$case_name: configure failed, but not on the expected guard"
+ fail " wanted text: $want"
+ tail -6 "$build_dir/configure.log" | sed 's/^/ /'
+ failures=$((failures + 1))
+ else
+ say " $case_name: refused"
+ fi
+ rm -rf "$build_dir"
+ }
+
+ refuse_case "VFP without a hard float ABI" \
+ "TX_R52_ENABLE_VFP requires TX_R52_FLOAT_ABI=hard" \
+ -DTX_R52_ENABLE_VFP=ON
+
+ refuse_case "FIQ nesting without FIQ" \
+ "TX_R52_ENABLE_FIQ_NESTING requires TX_R52_ENABLE_FIQ" \
+ -DTX_R52_ENABLE_FIQ_NESTING=ON
+
+ # The counterpart: the supported spelling of the first case must still
+ # configure, so a guard cannot pass this stage by rejecting everything.
+ build_dir="$(mktemp -d)"
+ if cmake -S . -B "$build_dir" -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE=cmake/cortex_r52.cmake \
+ -DARM_TOOLCHAIN_PATH="$(dirname "$CC_ARM")" \
+ -DTX_R52_ENABLE_VFP=ON -DTX_R52_FLOAT_ABI=hard \
+ -DTX_R52_ENABLE_FIQ=ON -DTX_R52_ENABLE_FIQ_NESTING=ON \
+ >"$build_dir/configure.log" 2>&1; then
+ say " the supported combination: accepted"
+ else
+ fail "the supported combination was refused"
+ tail -6 "$build_dir/configure.log" | sed 's/^/ /'
+ failures=$((failures + 1))
+ fi
+ rm -rf "$build_dir"
+ fi
+fi
+
+# --------------------------------------------------------------------------
+say ""
+if [ "$failures" -eq 0 ]; then
+ say "All GNU toolchain checks passed."
+ exit 0
+fi
+
+echo "$failures GNU toolchain check(s) failed."
+exit 1
diff --git a/scripts/check_module_mmu_a7.sh b/scripts/check_module_mmu_a7.sh
new file mode 100755
index 00000000..593edb31
--- /dev/null
+++ b/scripts/check_module_mmu_a7.sh
@@ -0,0 +1,120 @@
+#!/bin/bash
+##############################################################################
+# Copyright (c) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+# Runs the Cortex-A7 module range check against a live MMU.
+#
+# The host test for that check stands a simulated page map behind the port's
+# CP15 primitive, so it never executes an address translation. An encoding that
+# named the wrong operation, or a PAR fault bit read the wrong way round, would
+# pass it. This check builds a bare-metal image that turns the MMU on and drives
+# the real check through the real translations, then runs it under an emulator.
+#
+# scripts/check_module_mmu_a7.sh
+#
+# Environment:
+# CROSS_CC arm-none-eabi C compiler; defaults to arm-none-eabi-gcc.
+# QEMU Arm system emulator; defaults to qemu-system-arm.
+#
+# Exit status is 0 when the image runs and every expectation holds, 1 when an
+# expectation fails, and 0 with a notice when the tools are absent, so that a
+# machine without a cross toolchain does not fail the build.
+
+set -u
+
+CC="${CROSS_CC:-arm-none-eabi-gcc}"
+QEMU_BIN="${QEMU:-qemu-system-arm}"
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+SRC="$ROOT/test/tx/module_manager/threadx_module_manager_cortex_a7_mmu_test.c"
+PORT="$ROOT/ports_module/cortex_a7/gnu/module_manager/src/txm_module_manager_inside_data_check.c"
+
+for tool in "$CC" "$QEMU_BIN"; do
+ if ! command -v "$tool" >/dev/null 2>&1 && [ ! -x "$tool" ]; then
+ echo "Skipping: $tool not found."
+ echo " Needs an arm-none-eabi toolchain and qemu-system-arm."
+ exit 0
+ fi
+done
+
+# The port supplies the two architecture primitives from its register setup
+# file, which cannot be compiled here because it also builds the module page
+# tables. They are provided by the image itself, copied from that file, so what
+# runs is the same instruction sequence the port issues.
+work="$(mktemp -d)"
+trap 'rm -rf "$work"' EXIT
+
+cat > "$work/primitives.c" <<'PRIMITIVES'
+#define TX_SOURCE_CODE
+#include "tx_api.h"
+#include "txm_module.h"
+
+UINT _txm_module_manager_address_probe(ULONG address, UINT write_request)
+{
+ULONG translation;
+
+ if (write_request == ((UINT) TXM_MODULE_MANAGER_ACCESS_WRITE))
+ {
+ __asm volatile ("MCR p15, 0, %0, c7, c8, 3" : : "r"(address) : );
+ }
+ else
+ {
+ __asm volatile ("MCR p15, 0, %0, c7, c8, 2" : : "r"(address) : );
+ }
+
+ __asm volatile ("ISB");
+ __asm volatile ("MRC p15, 0, %0, c7, c4, 0" : "=r"(translation) : : );
+
+ if (translation & TXM_ADDRESS_TRANSLATION_FAULT_BIT)
+ {
+ return(TX_FALSE);
+ }
+
+ return(TX_TRUE);
+}
+
+ULONG _txm_module_manager_current_asid_get(VOID)
+{
+ULONG contextidr;
+
+ __asm volatile ("MRC p15, 0, %0, c13, c0, 1" : "=r"(contextidr) : : );
+
+ return(contextidr & TXM_CONTEXTIDR_ASID_MASK);
+}
+PRIMITIVES
+
+"$CC" -mcpu=cortex-a7 -marm -O1 -std=gnu99 -Wall -Wextra -Werror \
+ --specs=rdimon.specs \
+ -I"$ROOT/ports_module/cortex_a7/gnu/inc" \
+ -I"$ROOT/common/inc" \
+ -I"$ROOT/common_modules/inc" \
+ -I"$ROOT/common_modules/module_manager/inc" \
+ -o "$work/mmu_test.elf" "$SRC" "$PORT" "$work/primitives.c" || {
+ echo "FAIL: the Cortex-A7 MMU test image did not build."
+ exit 1
+}
+
+output="$("$QEMU_BIN" -M realview-pb-a8 -cpu cortex-a7 -m 128 -nographic \
+ -semihosting -kernel "$work/mmu_test.elf" 2>/dev/null)"
+status=$?
+
+echo "$output"
+
+if [ $status -ne 0 ] || ! echo "$output" | grep -q "SUCCESS!"; then
+ echo "FAIL: the Cortex-A7 MMU test did not report success."
+ exit 1
+fi
+
+exit 0
diff --git a/scripts/check_ports.sh b/scripts/check_ports.sh
new file mode 100755
index 00000000..4da7bc63
--- /dev/null
+++ b/scripts/check_ports.sh
@@ -0,0 +1,315 @@
+#!/bin/bash
+##############################################################################
+# Copyright (C) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+# Consistency checks for the port trees. Run it before cutting a release, or
+# any time the ports have been touched:
+#
+# scripts/check_ports.sh
+#
+# Options:
+# --no-regen Skip the reproducibility check, which needs a clean tree.
+# --quiet Print only failures and the summary.
+#
+# Exit status is 0 when every check passes and 1 otherwise, so the same
+# command serves CI and the command line.
+#
+# Each check exists because a real defect reached the repository through it:
+#
+# 1. Reproducibility. The Cortex-M ports are generated by the copy scripts,
+# but fixes were applied to the generated copies instead of the source for
+# eight months. The next run of those scripts would have reverted them.
+#
+# 2. Preprocessor balance. A fix left ports/cortex_m85/iar/inc/tx_port.h with
+# one more #endif than #if, so that header could not compile.
+#
+# 3. Code at file scope. A fix left a second, headerless copy of a function
+# body in ports/cortex_m4/ac6/inc/tx_port.h, which placed statements
+# outside any function. See issue 569.
+#
+# 4. Lowercase .s under a gnu tree. GAS preprocesses .S and not .s, so a
+# .s file's #ifdef blocks are assembled whichever way the macro is set.
+# Twenty-nine files were in that state, including one non-module SMP
+# kernel port, and only one of them failed to assemble.
+#
+# The last section reports, without failing, on port families that have no copy
+# script and so cannot be checked for reproducibility.
+#
+# Headers under example_build are skipped: those trees vendor third party SDK
+# code, which is not ours to hold to these rules.
+
+set -u
+
+cd "$(dirname "$(realpath "$0")")/.."
+
+regen=1
+quiet=0
+for arg in "$@"; do
+ case "$arg" in
+ --no-regen) regen=0 ;;
+ --quiet) quiet=1 ;;
+ -h|--help) sed -n '17,30p' "$0"; exit 0 ;;
+ *) echo "Unknown option: $arg" >&2; exit 2 ;;
+ esac
+done
+
+failures=0
+
+say() { [ "$quiet" -eq 1 ] || echo "$@"; }
+fail() { echo " FAIL: $*"; failures=$((failures + 1)); }
+
+# --------------------------------------------------------------------------
+# 1. The generated ports must be reproducible from ports_arch.
+# --------------------------------------------------------------------------
+say ""
+say "== Generated ports are reproducible from ports_arch =="
+
+if [ "$regen" -eq 0 ]; then
+ say " skipped (--no-regen)"
+elif ! command -v git >/dev/null 2>&1; then
+ say " skipped (git not available)"
+elif [ -n "$(git status --porcelain -uno)" ]; then
+ fail "the working tree has uncommitted changes; commit or stash them, or pass --no-regen"
+else
+ # A generator that fails or is missing must not leave the tree looking
+ # clean, which would be a false pass.
+ generator_failed=0
+ run_generator() {
+ if ! "$@" >/dev/null 2>&1; then
+ fail "generator failed: $*"
+ generator_failed=1
+ fi
+ }
+
+ run_generator ./scripts/copy_armv7_m.sh
+ run_generator ./scripts/copy_armv8_m.sh
+ run_generator ./scripts/copy_module_armv7_m.sh
+ run_generator env -C ports_arch/ARMv7-A ./update.sh --port-sets tx \
+ --copy-common-files --copy-port-files --copy-example --patch-files
+ run_generator env -C ports_arch/ARMv8-A ./update.sh --port-sets tx,tx_smp \
+ --copy-common-files --copy-port-files --copy-example --patch-files
+
+ drift="$(git status --porcelain -uno)"
+ if [ -n "$drift" ]; then
+ fail "running the copy scripts changed $(echo "$drift" | wc -l) file(s)."
+ echo " The ports below were edited directly instead of through ports_arch."
+ echo " Re-apply the change to ports_arch and re-run the copy scripts."
+ echo "$drift" | sed 's/^/ /'
+ git checkout -- . >/dev/null 2>&1
+ else
+ say " ok: the copy scripts change nothing"
+ fi
+fi
+
+# --------------------------------------------------------------------------
+# 2. Preprocessor directives must balance in every port header.
+# --------------------------------------------------------------------------
+say ""
+say "== Preprocessor directives balance =="
+
+unbalanced=0
+while IFS= read -r f; do
+ result="$(awk '
+ /^[ \t]*#[ \t]*(if|ifdef|ifndef)/ { depth++ }
+ /^[ \t]*#[ \t]*endif/ {
+ depth--
+ if (depth < 0 && first == 0) { first = NR }
+ }
+ END { printf "%d %d", depth, first }
+ ' "$f")"
+ depth="${result% *}"
+ first="${result#* }"
+ if [ "$depth" -ne 0 ]; then
+ if [ "$first" -ne 0 ]; then
+ fail "$f: #endif without a matching #if at line $first (final depth $depth)"
+ else
+ fail "$f: $depth unterminated #if (final depth $depth)"
+ fi
+ unbalanced=$((unbalanced + 1))
+ fi
+done < <(find ports ports_arch ports_module ports_smp -name "*.h" -type f \
+ ! -path "*/example_build/*" 2>/dev/null | sort)
+
+[ "$unbalanced" -eq 0 ] && say " ok: every port header balances"
+
+# --------------------------------------------------------------------------
+# 3. No statements outside a function body in a port header.
+# --------------------------------------------------------------------------
+# Tracks brace depth, ignoring preprocessor lines, comments and strings, and
+# reports any statement that lands at depth zero. Declarations, typedefs,
+# externs and macro definitions are expected there; assignments, calls and
+# dereferences are not, and an orphaned function body shows up as exactly that.
+say ""
+say "== No code at file scope in port headers =="
+
+orphans=0
+while IFS= read -r f; do
+ result="$(awk '
+ # Skip preprocessor lines, including multi-line macro bodies, whose
+ # continuations are statements by design.
+ /^[ \t]*#/ { if (/\\[ \t]*$/) { in_macro = 1 }; next }
+ in_macro { if (!/\\[ \t]*$/) { in_macro = 0 }; next }
+
+ # Skip comments, both kinds, including continuation lines.
+ in_comment { if (/\*\//) { in_comment = 0 }; next }
+ /^[ \t]*\/\// { next }
+ /\/\*/ { if (!/\*\//) { in_comment = 1 }; next }
+
+ {
+ line = $0
+ gsub(/"[^"]*"/, "", line)
+ gsub(/\/\/.*$/, "", line)
+ stripped = line
+ gsub(/^[ \t]+|[ \t]+$/, "", stripped)
+
+ # A statement at file scope ends in a semicolon and either assigns,
+ # dereferences or opens a control structure. Declarations and
+ # prototypes also end in a semicolon but match none of these.
+ if (depth == 0 && stripped ~ /;[ \t]*$/ &&
+ (stripped ~ /^\*\(/ ||
+ stripped ~ /^[A-Za-z_][A-Za-z0-9_]*([ \t]*(\[[^]]*\]|->[ \t]*[A-Za-z_][A-Za-z0-9_]*|\.[A-Za-z_][A-Za-z0-9_]*))*[ \t]*=[^=]/ ||
+ stripped ~ /^(return|if|while|for|switch|do)[ \t(]/)) {
+ print NR ": " stripped
+ found++
+ }
+
+ n = gsub(/{/, "{", line); m = gsub(/}/, "}", line)
+ depth += n - m
+ if (depth < 0) { depth = 0 }
+ }
+ END { exit (found > 0 ? 1 : 0) }
+ ' "$f")"
+ if [ -n "$result" ]; then
+ fail "$f: statement(s) outside any function body"
+ echo "$result" | sed 's/^/ /'
+ orphans=$((orphans + 1))
+ fi
+done < <(find ports ports_arch ports_module ports_smp -name "*.h" -type f \
+ ! -path "*/example_build/*" 2>/dev/null | sort)
+
+[ "$orphans" -eq 0 ] && say " ok: no port header carries code at file scope"
+
+# --------------------------------------------------------------------------
+# 4. GNU assembly that uses the preprocessor must be named .S, not .s.
+# --------------------------------------------------------------------------
+# GAS runs the C preprocessor on .S and not on .s. In a .s file every line
+# beginning with # is just a comment, so nothing fails and nothing is
+# substituted: a #define constant reaches the assembler as an undefined
+# symbol, an #ifdef block is assembled whatever the macro says, and an
+# #if/#else pair emits *both* branches. The port silently ignores its own
+# feature macros.
+#
+# Measured 26 Aug 2026, when a corrected glob in check_clang.sh first offered
+# the module ports to a compiler. Twenty-nine files were affected, and the
+# damage was not only cosmetic:
+#
+# ports_smp/cortex_a7_smp/gnu/src/tx_thread_smp_unprotect.s -- the only .s
+# in a directory of twenty-one .S -- wrote the caller's LR into the
+# protection structure on every unprotect, a store guarded by
+# TX_MPCORE_DEBUG_ENABLE, and returned through both BX lr and MOV pc, lr.
+#
+# ports_module/cortex_m33/.../tx_thread_stack_build.s emitted both arms of
+# an #ifdef TX_SINGLE_MODE_SECURE, so the second LR value overwrote the
+# first and the secure build got the non-secure one.
+#
+# ports_module/cortex_a7/.../tx_thread_schedule.s did fail to assemble, on
+# GCC 14.3 as well as on LLVM, because #define SYS_MODE was never expanded.
+# That is the only one of the twenty-nine any compiler complained about.
+#
+# Only the gnu trees are checked. The IAR, Arm Compiler 5 and Keil assemblers
+# preprocess .s themselves, so the same combination is correct there, and
+# around three hundred files in this repository depend on it.
+say ""
+say "== GNU assembly using the preprocessor is named .S =="
+
+lowercase=0
+while IFS= read -r f; do
+ first="$(grep -nE '^[ \t]*#[ \t]*(define|include|if|ifdef|ifndef|else|elif|endif|undef)\b' \
+ "$f" | head -1)"
+ if [ -n "$first" ]; then
+ fail "$f: preprocessor directive in a .s file, which GAS does not preprocess"
+ echo " line $first"
+ echo " Rename the file to .S. Check the callers first: a build script"
+ echo " that already names it .S is the usual sign of how this happened."
+ lowercase=$((lowercase + 1))
+ fi
+done < <(find ports ports_arch ports_module ports_smp -name "*.s" -type f \
+ -path "*/gnu/*" 2>/dev/null | sort)
+
+[ "$lowercase" -eq 0 ] && say " ok: no .s file under a gnu tree uses the preprocessor"
+
+# --------------------------------------------------------------------------
+# 5. Cortex-R5 execution-profile guards accept both configuration names.
+# --------------------------------------------------------------------------
+# Profiling storage uses TX_EXECUTION_PROFILE_ENABLE, while
+# TX_ENABLE_EXECUTION_CHANGE_NOTIFY remains a supported compatibility symbol.
+# Every Cortex-R5 profiling hook must recognize both symbols.
+say ""
+say "== Cortex-R5 execution-profile guards accept both symbols =="
+
+profile_guards_missing=0
+while IFS=: read -r file line text; do
+ case "$text" in
+ *TX_EXECUTION_PROFILE_ENABLE*) ;;
+ *)
+ fail "$file:$line accepts the legacy execution-profile symbol only"
+ profile_guards_missing=$((profile_guards_missing + 1))
+ ;;
+ esac
+done < <(grep -Rn "TX_ENABLE_EXECUTION_CHANGE_NOTIFY" ports/cortex_r5/*/src 2>/dev/null)
+
+[ "$profile_guards_missing" -eq 0 ] && \
+ say " ok: every Cortex-R5 profiling hook accepts both symbols"
+
+# --------------------------------------------------------------------------
+# 6. Report only: families with no copy script.
+# --------------------------------------------------------------------------
+# These are maintained by hand, so a fix applied to one toolchain can silently
+# miss the others. Nothing here fails the run; it is a prompt to look.
+say ""
+say "== Families with no copy script (report only) =="
+
+if [ "$quiet" -eq 0 ]; then
+ for family in ports/cortex_m0 ports/cortex_m0+ ports/cortex_m23; do
+ [ -d "$family" ] || continue
+ for probe in "dsb sy" "isb sy"; do
+ have=""; missing=""
+ for header in "$family"/*/inc/tx_port.h; do
+ [ -f "$header" ] || continue
+ grep -q "0xE000ED04" "$header" || continue
+ tool="$(basename "$(dirname "$(dirname "$header")")")"
+ if grep -q "$probe" "$header"; then
+ have="$have $tool"
+ else
+ missing="$missing $tool"
+ fi
+ done
+ if [ -n "$have" ] && [ -n "$missing" ]; then
+ echo " $family: \"$probe\" present in$have but absent in$missing"
+ fi
+ done
+ done
+ say " (nothing above means the toolchains within each family agree)"
+fi
+
+# --------------------------------------------------------------------------
+say ""
+if [ "$failures" -eq 0 ]; then
+ say "All port consistency checks passed."
+ exit 0
+fi
+
+echo "$failures port consistency check(s) failed."
+exit 1
diff --git a/scripts/cmake_bootstrap.sh b/scripts/cmake_bootstrap.sh
index d66d668d..855a7900 100755
--- a/scripts/cmake_bootstrap.sh
+++ b/scripts/cmake_bootstrap.sh
@@ -31,22 +31,57 @@ function validate() {
help
}
+# CMake records the compiler it detected inside the build directory and keeps using it on
+# every later configure. Changing CC would otherwise be ignored without a word: the build
+# reports success while still using the compiler the directory was first configured with,
+# so anyone verifying a change against a second compiler would be reading stale results.
+# Only the C compiler is consulted, because these test trees declare LANGUAGES C.
+function compiler_changed() {
+ local build=$1
+ local recorded requested
+
+ [ -d "build/$build" ] || return 1
+
+ recorded=$(sed -n 's/^set(CMAKE_C_COMPILER "\(.*\)")$/\1/p' build/$build/CMakeFiles/*/CMakeCCompiler.cmake 2>/dev/null | head -1)
+ [ -n "$recorded" ] || return 1
+
+ requested=$(command -v "${CC:-gcc}" 2>/dev/null)
+ [ -n "$requested" ] || return 1
+
+ [ "$recorded" != "$requested" ]
+}
+
function generate() {
build=$1
- cmake -Bbuild/$build -GNinja -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../cmake/linux.cmake -DCMAKE_BUILD_TYPE=$build .
+ if compiler_changed $build; then
+ echo "Compiler changed since build/$build was configured. Reconfiguring from scratch."
+ rm -rf build/$build
+ fi
+ cmake -Bbuild/$build -GNinja -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../cmake/linux.cmake -DCMAKE_BUILD_TYPE=$build -DTX_COVERAGE=${TX_COVERAGE:-OFF} .
}
function build() {
- cmake --build build/$1
+ # -k 0 keeps Ninja going after a target fails, so one broken target no
+ # longer decides whether the targets after it exist. ctest reports a
+ # missing binary as a failing test, which turned a single link error into
+ # a failure count that varied with build scheduling order.
+ cmake --build build/$1 -- -k 0
}
function build_libs() {
+ if compiler_changed libs; then
+ echo "Compiler changed since build/libs was configured. Reconfiguring from scratch."
+ rm -rf build/libs
+ fi
cmake -Bbuild/libs -GNinja -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../cmake/linux.cmake libs
cmake --build build/libs
}
function test() {
- pushd build/$1
+ # Guard the pushd: with the caller capturing this function's status, set -e
+ # no longer aborts here, so a missing build directory would otherwise let
+ # ctest run in the source tree and report "no tests" as success.
+ pushd build/$1 || return 1
[ -z "${CTEST_PARALLEL_LEVEL}" ] && parallel="-j$2"
if [ -z "${CTEST_REPEAT_FAIL}" ];
then
@@ -54,15 +89,92 @@ function test() {
else
repeat_fail=${CTEST_REPEAT_FAIL}
fi
- ctest $parallel --timeout 1000 -O $1.txt -T test --no-compress-output --test-output-size-passed 4194304 --test-output-size-failed 4194304 --output-on-failure --repeat until-pass:${repeat_fail} --output-junit $1.xml
+ # ctest's status is captured rather than allowed to abort the function, and
+ # returned at the end. set -e would otherwise stop here on the first failing
+ # test, and every configuration after it would go untested -- and, before
+ # collection moved out of this function, uncovered as well. The gcda files
+ # exist by that point, so a failing run threw away coverage it had already
+ # collected, and the run whose behaviour changed is exactly the one whose
+ # coverage is worth reading. Measured: the failing run of 2026-08-18 produced
+ # test_reports artifacts and no coverage_report artifact at all. Collection
+ # itself now happens in collect_all_coverage, after every configuration has
+ # been tested, and it does not stop at the first failure either.
+ local status=0
+ ctest $parallel --timeout 1000 -O $1.txt -T test --no-compress-output --test-output-size-passed 4194304 --test-output-size-failed 4194304 --output-on-failure --repeat until-pass:${repeat_fail} --output-junit $1.xml || status=$?
popd
- grep -E "^(\s*[0-9]+|Total)" build/$1/$1.txt >build/$1.txt
+ # Tolerated because this is a summary for humans, and a ctest that died early
+ # enough to leave no matching line must not be what stops the coverage below.
+ grep -E "^(\s*[0-9]+|Total)" build/$1/$1.txt >build/$1.txt || true
sed -i "s/\x1B\[[0-9;]*[JKmsu]//g" build/$1.txt
- if [[ $1 = *"_coverage" ]]; then
+ return $status
+}
+
+# Coverage is collected for any configuration that was instrumented, which is
+# what TX_COVERAGE decides. The build-type match is kept for the one
+# configuration instrumented by its name, so building a single configuration by
+# hand behaves exactly as it did before.
+#
+# Three of the test trees -- freertos, posix and tx/cmake/riscv -- have no
+# coverage.sh at all, and this script serves them too. Say so rather than
+# failing, so that TX_COVERAGE=ON is harmless anywhere it does not apply.
+function collect_coverage() {
+ if [[ $1 = *"_coverage" ]] || [ "${TX_COVERAGE:-OFF}" = "ON" ]; then
+ if [ ! -x ./coverage.sh ]; then
+ echo "No coverage.sh in $(pwd); skipping coverage for $1."
+ return 0
+ fi
./coverage.sh $1
fi
}
+# Collection runs after every configuration has been tested, and strictly one at
+# a time.
+#
+# It used to sit inside test(), which was safe only while a single configuration
+# was instrumented. gcov writes its intermediate .gcov files into the directory
+# gcovr is rooted at, and coverage.sh roots every configuration at the repository
+# root so that the report can name files the way the repository does. Five
+# concurrent gcovr processes therefore share one scratch directory and delete
+# each other's output. The symptom is a gcovr SanityCheckError naming a .gcov
+# file that "doesn't exist but no error from GCOV detected", and it cost the
+# report for three of the five configurations while all 480 tests still passed --
+# a green suite with most of its coverage missing.
+#
+# Measured both ways: two gcovr invocations rooted at the repository root fail
+# when run concurrently and both succeed when run in sequence. CI happens to be
+# safe already, because test_tx.sh sets CTEST_PARALLEL_LEVEL=1 and takes the
+# serial branch below, but that is a coincidence of one caller rather than a
+# property of this script.
+#
+# Collection deliberately does not stop at the first failure, for the reason
+# given in test(): the configurations that did produce data should still report.
+#
+# The trade this makes, recorded rather than discovered later: a configuration
+# that hangs long enough to hit the job's own timeout now costs the reports for
+# the configurations that already finished, where collecting inside test() would
+# have kept them. That is judged the smaller risk -- a per-test timeout is a
+# ctest failure and the loop carries on, the job timeout sits at 60 minutes
+# against a suite that takes 5 to 25, and the alternative loses most of the
+# coverage on every parallel run instead of on a hang.
+function collect_all_coverage() {
+ local item status=0
+ for item in $builds; do
+ collect_coverage $item || status=$?
+ done
+
+ # Union the per-configuration reports. Only the union is a coverage figure:
+ # each configuration compiles a different set of TX_ feature macros, so a
+ # line one of them compiles out is absent from its denominator rather than
+ # uncovered in it, and an average of five percentages means nothing.
+ #
+ # Done here rather than as a separate workflow step so a local run produces
+ # the same merged report CI reads.
+ if [ "${TX_COVERAGE:-OFF}" = "ON" ] && [ -x ./coverage.sh ]; then
+ ./coverage.sh --merge || status=$?
+ fi
+ return $status
+}
+
cd $(dirname $0)
result=$(sed -n "/(BUILD_CONFIGURATIONS/,/)/p" CMakeLists.txt|sed ':label;N;s/\n/ /;b label'|grep -Pzo "[a-zA-Z0-9_]*build[a-zA-Z0-9_]*\s*"| tr -d '\0')
@@ -93,11 +205,16 @@ if [ "$command" == "build" ]; then
echo ""
done
+ # A failing configuration must not stop the ones after it: under set -e
+ # the loop would abort and leave them unbuilt, which then reads as a wall
+ # of missing-binary test failures. The status is accumulated and returned.
+ build_status=0
for item in $builds; do
echo "Building $item"
- build $item
+ build $item || build_status=$?
echo ""
done
+ [ $build_status -eq 0 ] || exit $build_status
elif [ "$command" == "test" ]; then
cores=$(nproc)
if [ -z "${CTEST_PARALLEL_LEVEL}" ];
@@ -116,13 +233,30 @@ elif [ "$command" == "test" ]; then
for p in $pids; do
wait $p || exit_code=$?
done
+ # A coverage failure turns the run red, but must not overwrite a test
+ # failure's status with its own.
+ coverage_status=0
+ collect_all_coverage || coverage_status=$?
+ [ $exit_code -ne 0 ] || exit_code=$coverage_status
exit $exit_code
else
- # Run builds in serial
+ # Run builds in serial. The status is collected the same way the parallel
+ # branch above collects it, so one failing configuration no longer stops
+ # the remaining ones from being tested. That mattered more after the
+ # suites moved to serial execution: a failure in the first configuration
+ # meant the other four never ran, and their coverage was never collected
+ # either.
+ exit_code=0
for item in $builds; do
echo "Testing $item"
- test $item $parallel_jobs
+ test $item $parallel_jobs || exit_code=$?
done
+ # A coverage failure turns the run red, but must not overwrite a test
+ # failure's status with its own.
+ coverage_status=0
+ collect_all_coverage || coverage_status=$?
+ [ $exit_code -ne 0 ] || exit_code=$coverage_status
+ exit $exit_code
fi
elif [ "$command" == "build_libs" ]; then
build_libs
diff --git a/scripts/copy_armv8_m.sh b/scripts/copy_armv8_m.sh
index c66cc11e..f17d7bb9 100755
--- a/scripts/copy_armv8_m.sh
+++ b/scripts/copy_armv8_m.sh
@@ -18,7 +18,7 @@ set -e
ports_folder="ports"
source_folder="ports_arch/ARMv8-M/threadx"
-target_mcu="cortex_m33 cortex_m55 cortex_m85"
+target_mcu="cortex_m33 cortex_m52 cortex_m55 cortex_m85"
source_inc_folder="$source_folder/inc"
target_inc_mcu=$target_mcu
target_inc_ide="ac6 gnu iar"
@@ -112,4 +112,4 @@ do
rm -f $target
cp -f $src $target
done
-done \ No newline at end of file
+done
diff --git a/scripts/count_assisted_by.sh b/scripts/count_assisted_by.sh
new file mode 100755
index 00000000..57cdf65d
--- /dev/null
+++ b/scripts/count_assisted_by.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+##############################################################################
+# Copyright (C) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+# Report the Assisted-by attributions in a repository's history.
+#
+# scripts/count_assisted_by.sh # HEAD, grouped by value
+# scripts/count_assisted_by.sh origin/dev # a branch
+# scripts/count_assisted_by.sh origin/dev -- ports # a path
+# scripts/count_assisted_by.sh --list origin/dev # one line per commit
+#
+# Why not git's own trailer parser: git reads trailers only from a block at the
+# very end of a message. A squash merge concatenates the branch's commit
+# messages, which leaves every trailer but the last one buried mid-message, and
+# an indented trailer is skipped as well. Both forms are real attributions and
+# both are invisible to --format='%(trailers:key=Assisted-by)'. This reads whole
+# commit bodies instead, so the count is complete.
+
+set -u
+
+list=0
+if [ "${1:-}" = "--list" ]; then
+ list=1
+ shift
+fi
+
+rev="${1:-}"
+if [ -n "$rev" ] && [ "$rev" != "--" ]; then
+ shift
+else
+ rev="HEAD"
+fi
+
+if [ "${1:-}" = "--" ]; then
+ shift
+fi
+
+# %x02 opens a record and %x01 separates the hash from the body, so bodies
+# containing blank lines survive the pass through awk.
+git log "$rev" --format='%x02%H%x01%B' ${1+"$@"} \
+ | tr '\n' '\003' | tr '\002' '\n' \
+ | awk -F'\001' -v list="$list" '
+ NF >= 2 {
+ body = $2
+ gsub(/\003/, "\n", body)
+ n = split(body, line, "\n")
+ for (i = 1; i <= n; i++) {
+ if (line[i] ~ /^[[:space:]]*Assisted-by:[[:space:]]*/) {
+ value = line[i]
+ sub(/^[[:space:]]*Assisted-by:[[:space:]]*/, "", value)
+ sub(/[[:space:]]+$/, "", value)
+ if (value == "")
+ continue
+ if (list)
+ print substr($1, 1, 8) " " value
+ else
+ count[value]++
+ }
+ }
+ }
+ END {
+ if (list)
+ exit
+ for (value in count)
+ printf "%6d %s\n", count[value], value
+ }' \
+ | { if [ "$list" -eq 1 ]; then cat; else sort -rn; fi; }
diff --git a/scripts/install.sh b/scripts/install.sh
index bc868477..cb80c899 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -16,8 +16,39 @@
# Remove large folder to save space
rm -rf /opt/hostedtoolcache
-sudo apt-get update
-sudo apt-get install -y \
+# The network helpers -- retry, TIMEOUT, TIMEOUT_LONG and APT_OPTIONS -- live in
+# tx_ci_common.sh, alongside the comments recording why each of them is shaped
+# the way it is. They were defined here until the RISC-V suite was enabled in
+# CI, which put a second install script on every pull request's critical path
+# with none of them.
+. "$(dirname "$(realpath "$0")")/tx_ci_common.sh"
+
+# THE UPDATE IS NOT THE GATE, AND IT MUST NOT BE. apt-get update fails if ANY
+# configured repository serves a bad index, including ones this project does
+# not use. On a GitHub runner the image carries Google's and Microsoft's
+# repositories, and a Hash Sum mismatch from Google's -- their CDN caught
+# mid-publish, index and Release file eight hours apart -- failed this script
+# three attempts running and turned a build red over a browser nobody was
+# installing.
+#
+# The alternative of disabling third-party sources before updating is wrong
+# here: this script also runs on a contributor's own machine, where silently
+# rewriting their apt configuration would be a far worse thing to do than
+# tolerating a stale index.
+#
+# So a failed update WARNS and the install below is the gate. Nothing is
+# weakened by that: apt-get install still fails hard on a package it cannot
+# find, so an archive that is genuinely unreachable still stops the script --
+# one step later, and saying which package it could not get.
+if ! retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" update; then
+ echo ""
+ echo "install.sh: apt-get update did not fully succeed."
+ echo "install.sh: continuing, because a repository this project does not"
+ echo "install.sh: use can fail an update. The install below is the real"
+ echo "install.sh: gate and fails if any package needed is unavailable."
+ echo ""
+fi
+retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" install -y \
gcc-multilib \
git \
g++ \
@@ -28,10 +59,27 @@ sudo apt-get install -y \
tofrodos \
gawk \
cmake \
- software-properties-common
+ software-properties-common || exit 1
-python3 -m pip install --upgrade pip
-pip3 install gcovr==4.1
+retry "${TIMEOUT[@]}" python3 -m pip install --retries 3 --timeout 30 --upgrade pip || exit 1
+# gcovr was pinned to 4.1, released in 2018. That version cannot do what merging
+# the coverage of several build configurations needs: it has no --json and no
+# --add-tracefile, both of which arrived later. The pin is exact rather than
+# floating so the coverage percentage stays comparable between runs -- the
+# denominator is a property of the tool as much as of the tree -- and it is moved
+# by hand, because it lives in a shell script that Dependabot cannot parse.
+#
+# Measured before bumping, on the default_build_coverage tree of test/tx, over
+# the same gcda with the same gcov, varying only the gcovr version: 4.1, 7.0,
+# 8.3 and 8.6 all report lines-valid 3827 across 177 files and branches-valid
+# 1994. The denominator does not move with the tool, so this bump moves no
+# number and any movement in a later run belongs to a later change.
+#
+# The numerator does move, but not because of gcovr: tx_thread_system_resume.c
+# line 529 is executed on some runs of the suite and not others, so the same
+# tree reports 3826 or 3827 covered lines with every test passing either way.
+# That is a property of the suite, not of this pin.
+retry "${TIMEOUT[@]}" pip3 install --retries 3 --timeout 30 gcovr==8.6 || exit 1
# Upgrade cmake to the latest version.
-pip install --upgrade cmake
+retry "${TIMEOUT[@]}" pip install --retries 3 --timeout 30 --upgrade cmake || exit 1
diff --git a/scripts/install_riscv.sh b/scripts/install_riscv.sh
index 41642e70..21e3ac85 100755
--- a/scripts/install_riscv.sh
+++ b/scripts/install_riscv.sh
@@ -10,26 +10,67 @@
# SPDX-License-Identifier: MIT
##############################################################################
+# Portions of this file were generated with AI assistance.
+
# Install RISC-V bare-metal cross-compiler toolchain and QEMU for CI.
set -e
+# retry, TIMEOUT, TIMEOUT_LONG, APT_OPTIONS and fetch. Shared with install.sh,
+# which is where they were written and where the reasons for their shape are
+# recorded.
+. "$(dirname "$(realpath "$0")")/tx_ci_common.sh"
+
+# The release tag is pinned so that what CI compiles with is answerable from the
+# repository, and the digests pin the bytes behind the tag. A tag can be deleted
+# and re-pushed and a release asset can be replaced; either would be picked up
+# silently without these. Both digests were taken from the releases API and then
+# checked against the bytes the CDN actually serves, 10 Sep 2026. Moving the tag
+# means refreshing all three lines together.
RELEASE_TAG="2026.04.26"
BASE_URL="https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/${RELEASE_TAG}"
# Use ubuntu-24.04 binaries to match ubuntu-latest runners.
RV32_TARBALL="riscv32-elf-ubuntu-24.04-gcc.tar.xz"
+RV32_SHA256="73c9a5adbb38b779312e5b3fc10d624484364ef1f870314571ae92c486ba9917"
RV64_TARBALL="riscv64-elf-ubuntu-24.04-gcc.tar.xz"
+RV64_SHA256="4a66a329653c9cfb869b826cae6d70c603bedcc1e13d419d3e3e92b99e0816b1"
echo "=== Installing QEMU and build tools ==="
-sudo apt-get update -qq
-sudo apt-get install -y -qq qemu-system-misc ninja-build cmake
+
+# The update warns rather than gates, for the reason install.sh sets out at
+# length: apt-get update fails if ANY configured repository serves a bad index,
+# including the third-party ones the runner image carries and this project does
+# not use. The install below is the gate, and it still fails hard on a package
+# it cannot get. Under set -e a bare update would have ended the job -- and with
+# the RISC-V suite running on every pull request, ended it for everybody.
+if ! retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" update; then
+ echo ""
+ echo "install_riscv.sh: apt-get update did not fully succeed."
+ echo "install_riscv.sh: continuing, because a repository this project does"
+ echo "install_riscv.sh: not use can fail an update. The install below is"
+ echo "install_riscv.sh: the real gate."
+ echo ""
+fi
+# --no-install-recommends, because qemu-system-misc recommends a desktop media
+# stack it does not need here. With recommends the install fetches 67 packages
+# and 87 MB, 26 of them gstreamer, pulseaudio, v4l and codec libraries, for a
+# job that runs qemu-system-riscv64 headless. That download is what pushes the
+# install past its two-minute budget on a slow mirror, and a larger budget is
+# not available: TIMEOUT_LONG is already sized so three attempts fit the step cap.
+retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" install -y \
+ --no-install-recommends \
+ qemu-system-misc \
+ ninja-build \
+ cmake || exit 1
echo "=== Downloading RISC-V GCC toolchain (${RELEASE_TAG}) ==="
# Both tarballs extract into riscv/ with non-overlapping prefixes
# (riscv32-unknown-elf-* and riscv64-unknown-elf-*).
-for tarball in "$RV32_TARBALL" "$RV64_TARBALL"; do
+for spec in "${RV32_TARBALL}:${RV32_SHA256}" "${RV64_TARBALL}:${RV64_SHA256}"; do
+ tarball="${spec%%:*}"
+ sha256="${spec##*:}"
echo "Downloading ${tarball} ..."
- wget --no-verbose "${BASE_URL}/${tarball}" -O "/tmp/${tarball}"
+ fetch "${BASE_URL}/${tarball}" "/tmp/${tarball}" "${sha256}" || exit 1
sudo tar xJf "/tmp/${tarball}" -C /opt
rm "/tmp/${tarball}"
done
@@ -40,4 +81,7 @@ echo "$TOOLCHAIN_BIN" >> "$GITHUB_PATH"
echo "=== Verifying installation ==="
"$TOOLCHAIN_BIN/riscv32-unknown-elf-gcc" --version | head -1
"$TOOLCHAIN_BIN/riscv64-unknown-elf-gcc" --version | head -1
+# Both QEMU system emulators are used -- run.sh selects one per architecture --
+# so both are worth failing on here rather than at the first test.
+qemu-system-riscv32 --version | head -1
qemu-system-riscv64 --version | head -1
diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh
index c33fa8f8..02e3df83 100755
--- a/scripts/prepare_release.sh
+++ b/scripts/prepare_release.sh
@@ -15,6 +15,7 @@
#
# Copyright (C) 2026 Eclipse ThreadX contributors
# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
set -eu
@@ -78,6 +79,20 @@ printf "\nThreadX release preparation\n"
printf " Repository : %s\n" "${REPO_ROOT}"
printf " Current version : %s\n" "${CURR_VER}"
printf " Target version : %s\n\n" "${VERSION}"
+
+# --------------------------------------------------------------------------
+# Port consistency checks
+# --------------------------------------------------------------------------
+# Run before anything is branched or rewritten, so a release is never cut on
+# top of ports that have drifted from ports_arch or headers that cannot
+# compile. Set SKIP_PORT_CHECKS=1 to proceed anyway.
+if [ "${SKIP_PORT_CHECKS:-0}" = "1" ]; then
+ printf "Skipping the port consistency checks (SKIP_PORT_CHECKS=1).\n\n"
+elif ! "${SCRIPT_DIR}/check_ports.sh"; then
+ printf "\nRelease preparation stopped: the port consistency checks failed.\n"
+ printf "Fix the problems above, or set SKIP_PORT_CHECKS=1 to proceed anyway.\n"
+ exit 1
+fi
printf "Proceed with update? [y/N] "
read -r CONFIRM
case "${CONFIRM}" in
@@ -120,8 +135,6 @@ sed -i -E "s|(#define THREADX_HOTFIX_VERSION[[:space:]]+)'[^']*'|\1${HOTFIX_DEFI
git -C "${REPO_ROOT}" add "${API_HEADER}"
git -C "${REPO_ROOT}" commit -F - <<'COMMIT_EOF'
Updated version number constants
-
-Co-authored-by: Copilot <[email protected]>
COMMIT_EOF
printf "Committed version constant updates.\n"
@@ -146,23 +159,48 @@ PORT_FILES=$(printf "%s" "${PORT_FILES}" | grep -v '^[[:space:]]*$' || true)
if [ -z "${PORT_FILES}" ]; then
printf "Warning: No port header files found. Skipping port version string commit.\n"
else
+ # A port advertises its release in the string the library reports at run time.
+ # The pattern accepts three or four dotted numbers and tolerates a stray
+ # letter before the first one, so a port that writes its version slightly
+ # differently is still reached.
+ VERSION_RE='Version[[:space:]]+[A-Za-z]?[0-9]+(\.[0-9]+)+[a-z]*'
while IFS= read -r port_file; do
- if [ -n "${port_file}" ] && grep -qE "Version [0-9]" "${port_file}" 2>/dev/null; then
- sed -i -E "s/Version [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[a-z]*/Version ${VERSION}/g" "${port_file}"
+ if [ -n "${port_file}" ] && grep -qE "${VERSION_RE}" "${port_file}" 2>/dev/null; then
+ sed -i -E "s/${VERSION_RE}/Version ${VERSION}/g" "${port_file}"
printf " Updated: %s\n" "${port_file#${REPO_ROOT}/}"
fi
done <<EOF
${PORT_FILES}
EOF
+ # Every port header that names a release must now name this one. A port
+ # whose string is shaped in some other way is not rewritten above, and
+ # without this check the pass reports success while leaving that port
+ # advertising the previous release for the life of the version.
+ STALE=""
+ while IFS= read -r port_file; do
+ [ -n "${port_file}" ] || continue
+ advertised=$(grep -hoE '"[^"]*[0-9]+\.[0-9]+\.[0-9]+[^"]*"' "${port_file}" 2>/dev/null || true)
+ [ -n "${advertised}" ] || continue
+ if ! printf "%s" "${advertised}" | grep -qF "${VERSION}"; then
+ STALE="${STALE} ${port_file#${REPO_ROOT}/}
+"
+ fi
+ done <<EOF
+${PORT_FILES}
+EOF
+ if [ -n "${STALE}" ]; then
+ printf "\nError: these port headers still advertise another release:\n%s" "${STALE}" >&2
+ printf "Correct them so the substitution above reaches them, then re-run.\n" >&2
+ exit 1
+ fi
+
git -C "${REPO_ROOT}" add -u
if git -C "${REPO_ROOT}" diff --cached --quiet; then
printf "No port version string changes staged. Skipping commit.\n"
else
git -C "${REPO_ROOT}" commit -F - <<'COMMIT_EOF'
Updated port version strings
-
-Co-authored-by: Copilot <[email protected]>
COMMIT_EOF
printf "Committed port version string updates.\n"
fi
diff --git a/scripts/test_freertos.sh b/scripts/test_freertos.sh
new file mode 100755
index 00000000..9edd20be
--- /dev/null
+++ b/scripts/test_freertos.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+##############################################################################
+# Copyright (C) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+CTEST_PARALLEL_LEVEL=4 $(dirname `realpath $0`)/../test/freertos/cmake/run.sh test all
diff --git a/scripts/test_posix.sh b/scripts/test_posix.sh
new file mode 100755
index 00000000..ce851cf4
--- /dev/null
+++ b/scripts/test_posix.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+##############################################################################
+# Copyright (c) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+$(dirname `realpath $0`)/../test/posix/cmake/run.sh riscv32 test all
diff --git a/scripts/test_smp.sh b/scripts/test_smp.sh
index 72be13c0..51f1f867 100755
--- a/scripts/test_smp.sh
+++ b/scripts/test_smp.sh
@@ -11,4 +11,4 @@
##############################################################################
-CTEST_PARALLEL_LEVEL=4 $(dirname `realpath $0`)/../test/smp/cmake/run.sh test all
+CTEST_PARALLEL_LEVEL=1 $(dirname `realpath $0`)/../test/smp/cmake/run.sh test all
diff --git a/scripts/test_tx.sh b/scripts/test_tx.sh
index 3a219431..1434995b 100755
--- a/scripts/test_tx.sh
+++ b/scripts/test_tx.sh
@@ -11,4 +11,4 @@
##############################################################################
-CTEST_PARALLEL_LEVEL=4 $(dirname `realpath $0`)/../test/tx/cmake/run.sh test all
+CTEST_PARALLEL_LEVEL=1 $(dirname `realpath $0`)/../test/tx/cmake/run.sh test all
diff --git a/scripts/tx_ci_common.sh b/scripts/tx_ci_common.sh
new file mode 100644
index 00000000..4aff4819
--- /dev/null
+++ b/scripts/tx_ci_common.sh
@@ -0,0 +1,113 @@
+##############################################################################
+# Copyright (c) 2026 Eclipse ThreadX contributors
+#
+# This program and the accompanying materials are made available under the
+# terms of the MIT License which is available at
+# https://opensource.org/licenses/MIT.
+#
+# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+# The AI-generated portions may be considered public domain (CC0-1.0)
+# and not subject to the project's licence. The human contributor has
+# reviewed and verified that the code is correct.
+#
+# SPDX-License-Identifier: MIT and CC0-1.0
+##############################################################################
+
+# Network helpers shared by the CI install scripts. Sourced, not executed --
+# there is no shebang, and nothing happens here beyond defining APT_OPTIONS,
+# TIMEOUT, TIMEOUT_LONG, retry and fetch.
+#
+# They lived in install.sh until the RISC-V suite was enabled in CI, at which
+# point a second install script was on every pull request's critical path with
+# none of them. The lessons below were paid for once; a script that reaches the
+# network in this project should not have to learn them again.
+#
+# The two callers differ in one way worth knowing: install_riscv.sh runs under
+# set -e and install.sh does not. That is why install.sh spells out `|| exit 1`
+# on the calls that must stop it -- without that, a failed fetch there would be
+# carried forward into a build that then failed somewhere less obvious.
+
+# Everything the install scripts do reaches the network, and on this runner pool
+# that is not dependable. apt-get update stalled seven times in a single day,
+# once for more than two hours, each time with the Azure mirror returning
+# nothing and the fallback to archive.ubuntu.com then going silent. Nothing
+# bounded a fetch and nothing retried one, so a mirror being down cost a whole
+# run rather than a few seconds.
+#
+# The Acquire options make a slow mirror give up sooner. The retry loop below
+# covers a mirror that is down rather than merely slow.
+APT_OPTIONS=(-o Acquire::Retries=3
+ -o Acquire::http::Timeout=20
+ -o Acquire::https::Timeout=20)
+
+# Two minutes per attempt, killed outright if it ignores the first signal. Three
+# attempts plus backoff bounds a command at about six and a half minutes.
+#
+# Each command is wrapped in timeout rather than left to bound itself. apt's own
+# Acquire timeouts were tried first and did not help: a run still sat inside a
+# single apt-get update for nine and a half minutes without producing a line,
+# having got as far as fetching noble-security InRelease, so the retry loop never
+# got a turn and the step timeout was what eventually killed it. Whatever apt is
+# waiting on there, it is not something Acquire::http::Timeout covers. timeout
+# does not care where the wait is.
+#
+# timeout goes under sudo, not over it, so that it signals apt itself. Signalling
+# sudo instead risks the kill landing on sudo while apt carries on holding the
+# dpkg lock, which would leave every retry failing for a different reason than
+# the one being retried.
+TIMEOUT=(timeout --kill-after=10 120)
+
+# Three minutes, for a single large download rather than a package operation.
+# The two RISC-V toolchain tarballs are about 500 MB each and took 39 seconds
+# apiece on 10 Sep 2026, so this is a margin of roughly four and a half.
+#
+# It is deliberately not larger. The install step in regression_template.yml is
+# capped at ten minutes, and a per-attempt timeout long enough to swallow that
+# cap would leave the retry loop below with no turn to take -- which is the
+# exact failure the TIMEOUT comment above records apt producing. Three attempts
+# at three minutes plus backoff still does not fit inside ten, so the step
+# timeout remains the outer backstop for a server that is genuinely down; what
+# the retries buy is recovery from the transient case, which is the common one,
+# and a log that says which attempt failed rather than a bare cancelled step.
+TIMEOUT_LONG=(timeout --kill-after=10 180)
+
+retry() {
+ local attempt
+ for attempt in 1 2 3; do
+ if "$@"; then
+ return 0
+ fi
+ echo "tx_ci_common: '$*' failed or timed out on attempt ${attempt}"
+ sleep $((attempt * 10))
+ done
+ echo "tx_ci_common: '$*' failed after 3 attempts"
+ return 1
+}
+
+# fetch <url> <destination> <sha256>
+#
+# Downloads with retries and verifies the digest before the caller is allowed to
+# unpack anything. --tries=1 hands retrying to the loop above rather than letting
+# wget retry inside a single timeout window and burn it.
+#
+# The digest is not an independent trust root: it is checked against bytes from
+# the same host that publishes the expected value, so it does not prove the
+# release was not tampered with at source. What it does buy is that the bytes
+# are pinned. A tag can be deleted and re-pushed and an asset can be replaced,
+# and today either would be picked up silently; with this, the build stops and
+# says which file failed and what it got.
+fetch() {
+ local url=$1
+ local dest=$2
+ local sha=$3
+
+ retry "${TIMEOUT_LONG[@]}" wget --no-verbose --tries=1 "$url" -O "$dest" || return 1
+
+ if ! echo "${sha} ${dest}" | sha256sum --check --status; then
+ echo "tx_ci_common: checksum mismatch for ${url}" >&2
+ echo "tx_ci_common: expected ${sha}" >&2
+ echo "tx_ci_common: actual $(sha256sum "$dest" | cut -d' ' -f1)" >&2
+ rm -f "$dest"
+ return 1
+ fi
+}