summaryrefslogtreecommitdiff
path: root/scripts/check_gcc.sh
blob: b8b2d794cbbf1634d317a93ba2cdd849b254eb5c (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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
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