summaryrefslogtreecommitdiff
path: root/scripts/check_ports.sh
blob: 4da7bc6329bff0adc6bed6342e0c821c554790de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
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