summaryrefslogtreecommitdiff
path: root/scripts/check_ports.sh
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/check_ports.sh')
-rwxr-xr-xscripts/check_ports.sh315
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