Files
threadx/scripts/check_clang.sh
T
Frédéric Desbiens 9c32abb17d Assembled the module ports, which no check had ever compiled (#672)
scripts/check_clang.sh globbed ports_module/*/gnu/src, which does not exist --
the module ports keep their assembly in module_manager/src. The [ -d ] guard
skipped it in silence, so 116 assembly files across nine Arm module ports were
assembled by no check, with either compiler, in the script whose own comments
state three times that "a port that is simply absent from the count reads as
covered". Stage 1 goes from 724 of 724 to 840 of 840; the feature-macro stage
had the same gap and goes from 412 files to 469.

Correcting the path exposed five defects, and only one of them was a build
failure. The other four assembled cleanly and did the wrong thing, because GAS
runs the C preprocessor on .S and not on .s:

  ports_smp/cortex_a7_smp/gnu/src/tx_thread_smp_unprotect.s, the only .s in a
  directory of twenty-one .S, ignored all four of its own feature macros. It
  wrote the caller's LR into the protection structure on every unprotect -- a
  store guarded by TX_MPCORE_DEBUG_ENABLE -- sent an unconditional SEV, and
  returned through both BX lr and MOV pc, lr. Its cortex_a5_smp and
  cortex_a9_smp siblings are .S.

  ports_module/cortex_m33/.../tx_thread_stack_build.s emitted both arms of an
  #ifdef TX_SINGLE_MODE_SECURE, so the non-secure LR value overwrote the secure
  one and the secure build got the wrong frame.

  ports_module/cortex_m23/.../tx_thread_context_{save,restore}.S carried the
  POP {r0, lr} that check_clang.sh's own comment describes as the reason the
  feature-macro stage exists. The 16-bit Thumb POP takes r0-r7 and pc only.
  The identical fix already sits in ports/cortex_m23/gnu/src; the module copy
  never got it because nothing scanned it.

  ports_module/cortex_m23/.../tx_thread_secure_stack_initialize.S used MOV
  rather than MOVS for an 8-bit immediate, latent behind TX_SINGLE_MODE_SECURE.
  Both siblings in the same directory already use MOVS.

  ports_module/cortex_a7/gnu/module_manager/src is the one that failed to
  assemble, on GCC 14.3 as well as on LLVM: #define SYS_MODE was never
  expanded, so #SYS_MODE reached the assembler as an undefined symbol.

Twenty-nine .s files under gnu trees are renamed to .S. Every one of them is
already named .S by the build scripts that compile it, so this repairs those
scripts rather than churning them -- ports_module/cortex_a7's build_threadx.bat
names all eighteen with a capital S, and works today only on a case-insensitive
filesystem. Renaming rather than converting the #defines to GNU assignments is
what fixes the #ifdef blocks as well as the constants; the assignments would
have fixed two files and left twenty-seven silently ignoring their macros.

Files with no preprocessor directives are left as .s: they are not broken, and
check_ports.sh gains a check that keeps them that way. Only the gnu trees are
checked there -- the IAR, Arm Compiler 5 and Keil assemblers preprocess .s
themselves, and about three hundred files in this repository rely on that.

Verified with both toolchains on the same tree: 840 of 840 assembled by
ATfE 22.1.0 and by arm-gnu-toolchain 14.3.rel1, all five stages of
check_clang.sh green, and check_ports.sh green including the reproducibility
check. The new check was shown to fail by planting a copy of the file it was
written for.

No regression test accompanies this. The assembly it covers is executed by no
host test, and the check itself going from 724 files to 840 is the coverage
AGENTS.md asks for -- together with the new check_ports.sh section, which is
what stops the class recurring.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-08-28 09:24:30 -04:00

462 lines
19 KiB
Bash
Executable File

#!/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 an LLVM based toolchain, in five 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, then link the example builds, both the script-driven ones and those
# driven by CMake. Only the linking stages need a target C library.
#
# scripts/check_clang.sh # clang from PATH
# scripts/check_clang.sh --clang /path/to/clang
# CLANG=/path/to/clang scripts/check_clang.sh
#
# Options:
# --clang <path> Compiler to use; defaults to $CLANG then to clang.
# --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: the gnu ports are only ever built with GNU tooling, and GNU
# as accepts several non-canonical forms that LLVM's assembler rejects. Those
# forms accumulated unnoticed. This check also covers Arm Toolchain for
# Embedded, which is LLVM based and is the successor to Arm Compiler 6, so the
# ac6 code paths are exercised here as well.
#
# Arm Toolchain for Embedded releases:
# https://github.com/arm/arm-toolchain/releases
set -u
cd "$(dirname "$(realpath "$0")")/.."
CC="${CLANG:-clang}"
asm_only=0
no_examples=0
quiet=0
while [ "$#" -gt 0 ]; do
case "$1" in
--clang) [ "$#" -ge 2 ] || { echo "Error: --clang needs a path" >&2; exit 2; }; CC="$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,33p' "$0"; exit 0 ;;
*) echo "Error: unknown option '$1'" >&2; exit 2 ;;
esac
done
say() { [ "$quiet" -eq 1 ] || echo "$@"; }
fail() { echo " FAIL: $*"; }
if ! command -v "$CC" >/dev/null 2>&1 && [ ! -x "$CC" ]; then
echo "Error: compiler '$CC' not found."
echo "Pass --clang <path>, set CLANG, or install Arm Toolchain for Embedded:"
echo " https://github.com/arm/arm-toolchain/releases"
exit 1
fi
# Resolve to an absolute path. The example stage runs the build scripts from
# inside their own directories, so a relative compiler path would stop
# resolving there.
if [ -e "$CC" ]; then
CC="$(realpath "$CC")"
else
CC="$(command -v "$CC")"
fi
say ""
say "Using: $CC"
say " $("$CC" --version | head -1)"
# 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.
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. It is where "POP {r0, lr}" was found in the Cortex-M0
# and Cortex-M23 execution-profile paths: invalid on Armv6-M and Armv8-M
# Baseline, where the 16-bit POP takes r0-r7 and pc only, and rejected by GNU as
# well as by LLVM. Turning the feature on had never once been tried.
FEATURE_MACROS="TX_ENABLE_VFP_SUPPORT TX_ENABLE_FIQ_SUPPORT TX_LOW_POWER
TX_ENABLE_EXECUTION_CHANGE_NOTIFY"
# 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, needed only where -mcpu alone cannot
# assemble them. Cortex-R4's FPU is an option rather than part of the core, so
# both toolchains reject its VFP code without an -mfpu.
#
# Do not extend this to the A profile ports. They save D16-D31, which exists only
# on a 32-register FPU, so naming a -d16 FPU takes those registers away and turns
# 28 working files into "register expected". Their defaults are already correct.
declare -A VFP_EXTRA=(
[cortex_r4]="-mfpu=vfpv3-d16 -mfloat-abi=softfp"
)
# 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.
#
# These fail with the GNU toolchain too, so they are not LLVM problems. All four
# fail the same way, for the same reason:
#
# 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'". Reproduced with arm-none-eabi-gcc 13.2.1.
#
# Until 6.1.10 the four linked libc.a and libgcc.a checked in beside them, which
# supplied those symbols. That sweep removed the archives without updating the
# link lines, so for years the examples failed earlier still, on the missing
# files themselves. Fixing the link lines exposed the _fini gap underneath.
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. This
# glob read ports_module/*/gnu/src until 26 Aug 2026; that directory does not
# exist, the [ -d ] guard below skipped it in silence, and 116 files across
# nine Arm module ports were assembled by no check with either compiler. The
# count went from 724 to 840 when the path was corrected, and three of the new
# files did not assemble.
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="$*"
for src in "$dir"/*.S "$dir"/*.s; do
[ -f "$src" ] || continue
total=$((total + 1))
output="$("$CC" --target="$target" -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' ' ')"
fi
# --------------------------------------------------------------------------
say ""
say "== Assembly behind feature macros =="
for macro in $FEATURE_MACROS; do
macro_total=0
macro_bad=0
# The module ports are named here for the same reason as in the stage
# above: they were absent from this list until 26 Aug 2026 and so read as
# covered. Adding them found the Cortex-M23 module manager carrying the
# very POP {r0, lr} this comment describes, six months after the same fix
# landed in its non-module sibling.
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="$*"
# 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" --target="$target" -mcpu="$cpu" $extra $fpu \
-D"$macro" -c "$src" -o /dev/null 2>&1)"
if [ -n "$output" ]; then
fail "$src with -D$macro"
echo "$output" | grep "error:" | head -3 | 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="$*"
count=0; bad=0
for src in common/src/*.c; do
count=$((count + 1))
output="$("$CC" --target="$target" -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" | grep "error:" | head -3 | sed 's/^/ /'
bad=$((bad + 1)); failures=$((failures + 1))
fi
done
say " $core: $((count - bad)) of $count compiled"
done
fi
# --------------------------------------------------------------------------
if [ "$no_examples" -eq 0 ]; then
say ""
say "== Example builds, linked with lld =="
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))
rm -f "$dir"/*.o "$dir"/*.a "$dir"/*.out "$dir"/*.map 2>/dev/null || true
log="$(cd "$dir" && TOOLCHAIN=atfe ATFE_CLANG="$CC" ./build_threadx.sh 2>&1 && \
TOOLCHAIN=atfe ATFE_CLANG="$CC" ./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 TOOLCHAIN=atfe. Same compiler, same linker, same purpose as the
# stage above: confirm the images still link when the toolchain is not GNU.
if [ "$no_examples" -eq 0 ]; then
say ""
say "== CMake example builds, linked with lld =="
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)"
# ATFE_TOOLCHAIN_PATH follows --clang, so the stage uses the same
# compiler as every other stage rather than whatever is on PATH.
if cmake -S . -B "$build_dir" -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="cmake/${core}_clang.cmake" \
-DATFE_TOOLCHAIN_PATH="$(cd "$(dirname "$CC")" && pwd)" \
-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
# --------------------------------------------------------------------------
say ""
if [ "$failures" -eq 0 ]; then
say "All LLVM toolchain checks passed."
exit 0
fi
echo "$failures LLVM toolchain check(s) failed."
exit 1