summaryrefslogtreecommitdiff
path: root/scripts/cmake_bootstrap.sh
blob: 855a79009aa323640125bb3268e99f2993124230 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
#!/bin/bash
##############################################################################
# Copyright (c) 2024 Microsoft Corporation
# 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.
#
# SPDX-License-Identifier: MIT
##############################################################################


set -e

function help() {
    echo "Usage: $0 [build|test] [all|<build_configuration> <build_configuration>...]"
    echo "Available build_configuration:"
    for build in ${build_configurations[*]}; do
        echo "  $build"
    done
    exit 1
}

function validate() {
    for build in ${build_configurations[*]}; do
        if [ "$1" == "$build" ]; then
            return
        fi
    done
    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
    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() {
    # -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() {
    # 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
        repeat_fail=2
    else
        repeat_fail=${CTEST_REPEAT_FAIL}
    fi
    # 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
    # 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
    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')
IFS=' '
read -ra build_configurations <<< "$result"

if [ $# -lt 1 ]; then
    help
fi

command=$1
shift

if [ "$#" == "0" ]; then
    builds=${build_configurations[0]}
elif [ "$*" == "all" ]; then
    builds=${build_configurations[@]}
else
    for item in $*; do
        validate $item
    done
    builds=$*
fi

if [ "$command" == "build" ]; then
    for item in $builds; do
        generate $item
        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_status=$?
        echo ""
    done
    [ $build_status -eq 0 ] || exit $build_status
elif [ "$command" == "test" ]; then
    cores=$(nproc)
    if [ -z "${CTEST_PARALLEL_LEVEL}" ];
    then
        # Run builds in parallel
        build_counts=$(echo $builds | wc -w)
        parallel_jobs=$(($cores / $build_counts))
        parallel_jobs=$(($parallel_jobs + 2))
        pids=""
        for item in $builds; do
            echo "Testing $item"
            test $item $parallel_jobs &
            pids+=" $!"
        done
        exit_code=0
        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. 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 || 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
else
    help
fi