#!/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