#!/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 four stages: assemble # every assembly source of every Arm gnu port, compile the common C sources for # one core per architecture profile, link the example builds that have a script # driver, then link those driven by CMake. Only the two 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 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 , 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" ) # 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 # -------------------------------------------------------------------------- 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