diff options
Diffstat (limited to 'scripts/check_ports.sh')
| -rwxr-xr-x | scripts/check_ports.sh | 315 |
1 files changed, 315 insertions, 0 deletions
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 |
