Files
threadx/scripts/check_clang.sh
T
Frédéric Desbiens acdc02b2fc Assembled the code behind feature macros, and fixed the POP it found (#608)
scripts/check_clang.sh assembled every source with default flags, so the
preprocessor discarded each #ifdef block before the assembler saw it. Nothing in
the tree had ever assembled a guarded path. That covers the VFP context save and
restore in ten ports, and 218 files carrying TX_LOW_POWER or
TX_ENABLE_EXECUTION_CHANGE_NOTIFY.

Turning those on found a defect. The Cortex-M0 and Cortex-M23 execution-profile
paths bracket their call with

    PUSH    {r0, lr}
    BL      _tx_execution_isr_enter
    POP     {r0, lr}

and the last of those is invalid on Armv6-M and Armv8-M Baseline, where the
16-bit Thumb POP takes r0-r7 and pc and nothing else. GNU rejects it as well --
"cannot honor width suffix" -- so TX_ENABLE_EXECUTION_CHANGE_NOTIFY and
TX_EXECUTION_PROFILE_ENABLE have never been buildable on either port with either
toolchain. Four files, all the same shape.

The fix pops into a scratch register and moves it, MOV to a high register being
permitted where POP is not. r1 is free: the BL may clobber r0-r3, which is the
reason r0 is saved in the first place. Disassembling the result gives
push {r0, lr} / bl / pop {r0, r1} / mov lr, r1 / bx lr, one 16-bit instruction
more than before and otherwise the same.

Two findings that were not defects, recorded in the script so they are not
rediscovered:

Cortex-R4 needs an -mfpu to assemble its VFP path, because its FPU is an option
rather than part of the core. GNU fails identically without one, so this is a
flags requirement and not a toolchain divergence.

The A profile ports must not be given one. Adding -mfpu=vfpv3-d16 uniformly broke
28 files with "register expected", because those ports save D16-D31 and a -d16
FPU does not have those registers. Their defaults were already right.

The new stage runs under --asm-only as well, needing no target C library, and
reports 37 of 37 VFP files, 8 of 8 TX_LOW_POWER and 218 of 218
TX_ENABLE_EXECUTION_CHANGE_NOTIFY. Restoring the POP for one run makes it fail
with 217 of 218 and name the file and the error, so the stage is not vacuous.
The other four stages are unchanged: 711 of 711 assembled, 185 of 185 common C
sources for each of nine cores, 42 of 42 script-driven examples and 5 of 5 CMake
images.

The fixed code is verified to assemble with both toolchains and to encode as
intended. It is not verified running: there is no Cortex-M0 or Cortex-M23 model
here, and these are context save and restore paths, so that gap is worth stating.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-08-13 10:14:53 -04:00

433 lines
18 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_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_LOW_POWER TX_ENABLE_EXECUTION_CHANGE_NOTIFY"
# 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:
# arm9 arm11 need newlib multilib variants for those CPUs, which are
# cortex_r4 cortex_r5 not present in every GNU toolchain packaging.
EXAMPLES_EXPECTED_TO_FAIL="arm9 arm11 cortex_r4 cortex_r5"
failures=0
skipped=""
# --------------------------------------------------------------------------
say ""
say "== Assembly sources of every Arm gnu port =="
total=0
for dir in ports/*/gnu/src ports_smp/*/gnu/src ports_module/*/gnu/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
for src in $(grep -rl "$macro" ports/*/gnu/src/*.S ports_smp/*/gnu/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