summaryrefslogtreecommitdiff
path: root/scripts/cmake_bootstrap.sh
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/cmake_bootstrap.sh')
-rwxr-xr-xscripts/cmake_bootstrap.sh152
1 files changed, 143 insertions, 9 deletions
diff --git a/scripts/cmake_bootstrap.sh b/scripts/cmake_bootstrap.sh
index d66d668d..855a7900 100755
--- a/scripts/cmake_bootstrap.sh
+++ b/scripts/cmake_bootstrap.sh
@@ -31,22 +31,57 @@ function validate() {
help
}
+# CMake records the compiler it detected inside the build directory and keeps using it on
+# every later configure. Changing CC would otherwise be ignored without a word: the build
+# reports success while still using the compiler the directory was first configured with,
+# so anyone verifying a change against a second compiler would be reading stale results.
+# Only the C compiler is consulted, because these test trees declare LANGUAGES C.
+function compiler_changed() {
+ local build=$1
+ local recorded requested
+
+ [ -d "build/$build" ] || return 1
+
+ recorded=$(sed -n 's/^set(CMAKE_C_COMPILER "\(.*\)")$/\1/p' build/$build/CMakeFiles/*/CMakeCCompiler.cmake 2>/dev/null | head -1)
+ [ -n "$recorded" ] || return 1
+
+ requested=$(command -v "${CC:-gcc}" 2>/dev/null)
+ [ -n "$requested" ] || return 1
+
+ [ "$recorded" != "$requested" ]
+}
+
function generate() {
build=$1
- cmake -Bbuild/$build -GNinja -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../cmake/linux.cmake -DCMAKE_BUILD_TYPE=$build .
+ if compiler_changed $build; then
+ echo "Compiler changed since build/$build was configured. Reconfiguring from scratch."
+ rm -rf build/$build
+ fi
+ cmake -Bbuild/$build -GNinja -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../cmake/linux.cmake -DCMAKE_BUILD_TYPE=$build -DTX_COVERAGE=${TX_COVERAGE:-OFF} .
}
function build() {
- cmake --build build/$1
+ # -k 0 keeps Ninja going after a target fails, so one broken target no
+ # longer decides whether the targets after it exist. ctest reports a
+ # missing binary as a failing test, which turned a single link error into
+ # a failure count that varied with build scheduling order.
+ cmake --build build/$1 -- -k 0
}
function build_libs() {
+ if compiler_changed libs; then
+ echo "Compiler changed since build/libs was configured. Reconfiguring from scratch."
+ rm -rf build/libs
+ fi
cmake -Bbuild/libs -GNinja -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../cmake/linux.cmake libs
cmake --build build/libs
}
function test() {
- pushd build/$1
+ # Guard the pushd: with the caller capturing this function's status, set -e
+ # no longer aborts here, so a missing build directory would otherwise let
+ # ctest run in the source tree and report "no tests" as success.
+ pushd build/$1 || return 1
[ -z "${CTEST_PARALLEL_LEVEL}" ] && parallel="-j$2"
if [ -z "${CTEST_REPEAT_FAIL}" ];
then
@@ -54,15 +89,92 @@ function test() {
else
repeat_fail=${CTEST_REPEAT_FAIL}
fi
- ctest $parallel --timeout 1000 -O $1.txt -T test --no-compress-output --test-output-size-passed 4194304 --test-output-size-failed 4194304 --output-on-failure --repeat until-pass:${repeat_fail} --output-junit $1.xml
+ # ctest's status is captured rather than allowed to abort the function, and
+ # returned at the end. set -e would otherwise stop here on the first failing
+ # test, and every configuration after it would go untested -- and, before
+ # collection moved out of this function, uncovered as well. The gcda files
+ # exist by that point, so a failing run threw away coverage it had already
+ # collected, and the run whose behaviour changed is exactly the one whose
+ # coverage is worth reading. Measured: the failing run of 2026-08-18 produced
+ # test_reports artifacts and no coverage_report artifact at all. Collection
+ # itself now happens in collect_all_coverage, after every configuration has
+ # been tested, and it does not stop at the first failure either.
+ local status=0
+ ctest $parallel --timeout 1000 -O $1.txt -T test --no-compress-output --test-output-size-passed 4194304 --test-output-size-failed 4194304 --output-on-failure --repeat until-pass:${repeat_fail} --output-junit $1.xml || status=$?
popd
- grep -E "^(\s*[0-9]+|Total)" build/$1/$1.txt >build/$1.txt
+ # Tolerated because this is a summary for humans, and a ctest that died early
+ # enough to leave no matching line must not be what stops the coverage below.
+ grep -E "^(\s*[0-9]+|Total)" build/$1/$1.txt >build/$1.txt || true
sed -i "s/\x1B\[[0-9;]*[JKmsu]//g" build/$1.txt
- if [[ $1 = *"_coverage" ]]; then
+ return $status
+}
+
+# Coverage is collected for any configuration that was instrumented, which is
+# what TX_COVERAGE decides. The build-type match is kept for the one
+# configuration instrumented by its name, so building a single configuration by
+# hand behaves exactly as it did before.
+#
+# Three of the test trees -- freertos, posix and tx/cmake/riscv -- have no
+# coverage.sh at all, and this script serves them too. Say so rather than
+# failing, so that TX_COVERAGE=ON is harmless anywhere it does not apply.
+function collect_coverage() {
+ if [[ $1 = *"_coverage" ]] || [ "${TX_COVERAGE:-OFF}" = "ON" ]; then
+ if [ ! -x ./coverage.sh ]; then
+ echo "No coverage.sh in $(pwd); skipping coverage for $1."
+ return 0
+ fi
./coverage.sh $1
fi
}
+# Collection runs after every configuration has been tested, and strictly one at
+# a time.
+#
+# It used to sit inside test(), which was safe only while a single configuration
+# was instrumented. gcov writes its intermediate .gcov files into the directory
+# gcovr is rooted at, and coverage.sh roots every configuration at the repository
+# root so that the report can name files the way the repository does. Five
+# concurrent gcovr processes therefore share one scratch directory and delete
+# each other's output. The symptom is a gcovr SanityCheckError naming a .gcov
+# file that "doesn't exist but no error from GCOV detected", and it cost the
+# report for three of the five configurations while all 480 tests still passed --
+# a green suite with most of its coverage missing.
+#
+# Measured both ways: two gcovr invocations rooted at the repository root fail
+# when run concurrently and both succeed when run in sequence. CI happens to be
+# safe already, because test_tx.sh sets CTEST_PARALLEL_LEVEL=1 and takes the
+# serial branch below, but that is a coincidence of one caller rather than a
+# property of this script.
+#
+# Collection deliberately does not stop at the first failure, for the reason
+# given in test(): the configurations that did produce data should still report.
+#
+# The trade this makes, recorded rather than discovered later: a configuration
+# that hangs long enough to hit the job's own timeout now costs the reports for
+# the configurations that already finished, where collecting inside test() would
+# have kept them. That is judged the smaller risk -- a per-test timeout is a
+# ctest failure and the loop carries on, the job timeout sits at 60 minutes
+# against a suite that takes 5 to 25, and the alternative loses most of the
+# coverage on every parallel run instead of on a hang.
+function collect_all_coverage() {
+ local item status=0
+ for item in $builds; do
+ collect_coverage $item || status=$?
+ done
+
+ # Union the per-configuration reports. Only the union is a coverage figure:
+ # each configuration compiles a different set of TX_ feature macros, so a
+ # line one of them compiles out is absent from its denominator rather than
+ # uncovered in it, and an average of five percentages means nothing.
+ #
+ # Done here rather than as a separate workflow step so a local run produces
+ # the same merged report CI reads.
+ if [ "${TX_COVERAGE:-OFF}" = "ON" ] && [ -x ./coverage.sh ]; then
+ ./coverage.sh --merge || status=$?
+ fi
+ return $status
+}
+
cd $(dirname $0)
result=$(sed -n "/(BUILD_CONFIGURATIONS/,/)/p" CMakeLists.txt|sed ':label;N;s/\n/ /;b label'|grep -Pzo "[a-zA-Z0-9_]*build[a-zA-Z0-9_]*\s*"| tr -d '\0')
@@ -93,11 +205,16 @@ if [ "$command" == "build" ]; then
echo ""
done
+ # A failing configuration must not stop the ones after it: under set -e
+ # the loop would abort and leave them unbuilt, which then reads as a wall
+ # of missing-binary test failures. The status is accumulated and returned.
+ build_status=0
for item in $builds; do
echo "Building $item"
- build $item
+ build $item || build_status=$?
echo ""
done
+ [ $build_status -eq 0 ] || exit $build_status
elif [ "$command" == "test" ]; then
cores=$(nproc)
if [ -z "${CTEST_PARALLEL_LEVEL}" ];
@@ -116,13 +233,30 @@ elif [ "$command" == "test" ]; then
for p in $pids; do
wait $p || exit_code=$?
done
+ # A coverage failure turns the run red, but must not overwrite a test
+ # failure's status with its own.
+ coverage_status=0
+ collect_all_coverage || coverage_status=$?
+ [ $exit_code -ne 0 ] || exit_code=$coverage_status
exit $exit_code
else
- # Run builds in serial
+ # Run builds in serial. The status is collected the same way the parallel
+ # branch above collects it, so one failing configuration no longer stops
+ # the remaining ones from being tested. That mattered more after the
+ # suites moved to serial execution: a failure in the first configuration
+ # meant the other four never ran, and their coverage was never collected
+ # either.
+ exit_code=0
for item in $builds; do
echo "Testing $item"
- test $item $parallel_jobs
+ test $item $parallel_jobs || exit_code=$?
done
+ # A coverage failure turns the run red, but must not overwrite a test
+ # failure's status with its own.
+ coverage_status=0
+ collect_all_coverage || coverage_status=$?
+ [ $exit_code -ne 0 ] || exit_code=$coverage_status
+ exit $exit_code
fi
elif [ "$command" == "build_libs" ]; then
build_libs