diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/build_freertos.sh | 17 | ||||
| -rwxr-xr-x | scripts/build_posix.sh | 17 | ||||
| -rwxr-xr-x | scripts/check_ai_disclosure.sh | 129 | ||||
| -rwxr-xr-x | scripts/check_clang.sh | 561 | ||||
| -rwxr-xr-x | scripts/check_gcc.sh | 748 | ||||
| -rwxr-xr-x | scripts/check_module_mmu_a7.sh | 120 | ||||
| -rwxr-xr-x | scripts/check_ports.sh | 315 | ||||
| -rwxr-xr-x | scripts/cmake_bootstrap.sh | 164 | ||||
| -rwxr-xr-x | scripts/copy_armv8_m.sh | 4 | ||||
| -rwxr-xr-x | scripts/count_assisted_by.sh | 79 | ||||
| -rwxr-xr-x | scripts/install.sh | 60 | ||||
| -rwxr-xr-x | scripts/install_riscv.sh | 52 | ||||
| -rwxr-xr-x | scripts/prepare_release.sh | 50 | ||||
| -rwxr-xr-x | scripts/test_freertos.sh | 17 | ||||
| -rwxr-xr-x | scripts/test_posix.sh | 17 | ||||
| -rwxr-xr-x | scripts/test_smp.sh | 2 | ||||
| -rwxr-xr-x | scripts/test_tx.sh | 2 | ||||
| -rw-r--r-- | scripts/tx_ci_common.sh | 113 |
18 files changed, 2438 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..8eecfbb3 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,104 @@ 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 + # The per-test ceiling. It has to clear the slowest test in any suite this + # script serves by enough that runner load cannot push a passing test over + # it, and still leave a timing-out test inside the 60 minute job timeout + # after until-pass has run it twice. GUIX's guix_bidi_text_draw_32bpp is the + # slowest known: measured between 831 and 957 seconds across six passing + # runs in October 2026, against a ceiling that was then 1000. + if [ -z "${CTEST_TIMEOUT}" ]; + then + ctest_timeout=1500 + else + ctest_timeout=${CTEST_TIMEOUT} + fi + # 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 ${ctest_timeout} -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 +217,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 +245,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 +} |
