diff options
Diffstat (limited to 'scripts/check_clang.sh')
| -rwxr-xr-x | scripts/check_clang.sh | 561 |
1 files changed, 561 insertions, 0 deletions
diff --git a/scripts/check_clang.sh b/scripts/check_clang.sh new file mode 100755 index 00000000..8956404b --- /dev/null +++ b/scripts/check_clang.sh @@ -0,0 +1,561 @@ +#!/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 six 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, 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_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, 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" + # 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 +fi + + +# -------------------------------------------------------------------------- +if [ "$asm_only" -eq 0 ]; then + say "" + say "== Module manager C sources, one per Arm module port ==" + + # Correcting the assembly glob above brought the module ports into the count, + # but only their assembly. Their C stayed outside every check: 27 files of + # portable module manager under common_modules, plus the per-port code under + # ports_module/<core>/gnu/module_manager/src. Nothing compiled either one, so + # by this script's own standard they read as covered while being unbuilt. + # + # 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="$*" + + # 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)) + # tx_thread_secure_stack.c carries GCC's optimize attribute, which + # clang does not implement and warns about. That is a toolchain + # divergence in a file GCC builds cleanly, so it is waived for that + # file alone -- a stray unknown attribute anywhere else in the port + # must still be reported. + src_extra="" + case "$src" in + */tx_thread_secure_stack.c) src_extra="-Wno-unknown-attributes" ;; + esac + + # A #pragma message is a deliberate notice to callers, not a defect + # in the file that carries it. txm_module_manager_absolute_load.c + # deprecates itself in favour of the extended entry point, and this + # stage compiles it once per port. + output="$("$CC" --target="$target" -mcpu="$cpu" $extra $port_extra \ + $src_extra "-Wno-#pragma-messages" \ + -I"$inc" -I"$kernel_inc" -Icommon_modules/inc \ + -Icommon_modules/module_manager/inc -c "$src" -o /dev/null 2>&1)" + 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 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 |
