summaryrefslogtreecommitdiff
path: root/.circleci/config.yml
blob: 8c3f09111bed40904490d4c1b543a75e913de0d2 (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
version: 2.1

setup: true
orbs:
  continuation: circleci/continuation@1

jobs:
  set-matrix:
    executor: continuation/default
    docker:
      - image: cimg/base:current
    resource_class: large
    steps:
      - checkout
      - run:
          name: Set matrix
          command: |
            # PR-scoped selection (best-effort: any failure falls back to the full
            # matrix). CircleCI has no base-branch var; tinyusb PRs target master.
            # The selection lands in a FILE and never travels as an argv: a mass-sweep
            # diff selects hundreds of KB, and E2BIG would fail the step before the
            # `||` fallback could fire - leaving a full build labelled scoped, because
            # EXAMPLE_MAP/BUILD_FILTERED below have no such limit and stay scoped.
            SELECT_FILE=ci_select_out.json
            rm -f "$SELECT_FILE"
            if [ -n "${CIRCLE_PULL_REQUEST:-}" ]; then
              git fetch --no-tags origin master || true
              # both suites gate the selector: test_ci_select.py owns the rules,
              # test_ci_metrics.py owns the config2 sentinel contract this job rewrites
              if python3 test/hil/test/test_ci_select.py >/dev/null 2>&1 &&
                 python3 test/hil/test/test_ci_metrics.py >/dev/null 2>&1; then
                python3 tools/ci_select.py --base origin/master > "$SELECT_FILE" || rm -f "$SELECT_FILE"
              else
                echo "ci_select unit suite failed - using the full matrix"
              fi
            fi
            [ -s "$SELECT_FILE" ] || rm -f "$SELECT_FILE"

            # computed once, up front: it is both the fallback and what the scoping is
            # dropped back to further down, and a second invocation there would be an
            # unguarded command under `set -e` inside the very branch that exists to
            # keep the pipeline green
            FULL_MATRIX_JSON=$(python .github/scripts/ci_set_matrix.py 2>/dev/null) || FULL_MATRIX_JSON=''
            MATRIX_JSON=''
            if [ -f "$SELECT_FILE" ]; then
              # ci_set_matrix also falls open with rc 0, saying UNSCOPED on stderr. The
              # extras below must follow it, exactly as build.yml does: a full matrix
              # paired with a still-scoped -e list builds a fraction of each family and
              # tells code-metrics it was an unscoped run.
              MATRIX_JSON=$(python .github/scripts/ci_set_matrix.py --select-file "$SELECT_FILE" 2>ci_set_matrix.err) || MATRIX_JSON=''
              cat ci_set_matrix.err
              if [ -z "$MATRIX_JSON" ] || grep -q 'ci_set_matrix: UNSCOPED' ci_set_matrix.err; then
                SELECT_FILE=''
              fi
            fi
            [ -n "$MATRIX_JSON" ] || MATRIX_JSON="$FULL_MATRIX_JSON"
            echo "MATRIX_JSON=$MATRIX_JSON"

            EXAMPLE_MAP='{}'
            BUILD_FILTERED='false'
            if [ -f "$SELECT_FILE" ]; then
              EXAMPLE_MAP=$(jq -c '.build.family_examples // {}' < "$SELECT_FILE") || EXAMPLE_MAP='{}'
              BUILD_FILTERED=$(jq -r 'if (.build? | type) == "object" and .build.full == false then "true" else "false" end' < "$SELECT_FILE") || BUILD_FILTERED='false'
            fi

            # /pipeline/continue caps parameter values at 512 chars - a scoped map is
            # KBs, so both values ride inside the generated config itself (config max
            # is 3MB), swapped into the parameter defaults by sentinel-line match.
            # Fail-open: a sentinel that drifted (renamed comment, reformatted line)
            # must not red EVERY CircleCI pipeline. The rewrite is all-or-nothing
            # (config2.yml is only written once both substitutions succeeded).
            #
            # Done BEFORE the family entries are generated, and a failure drops the
            # scoping entirely: the checked-in defaults are {} / false = unfiltered, so
            # a scoped FAMILY list with unfiltered defaults would build a subset of
            # families while telling code-metrics it had built them all.
            if ! EXAMPLE_MAP="$EXAMPLE_MAP" BUILD_FILTERED="$BUILD_FILTERED" python3 - \<<'PYEOF'
            import os
            p = '.circleci/config2.yml'
            t = open(p).read()
            def yq(s):  # YAML single-quoted scalar
                return "'" + s.replace("'", "''") + "'"
            for env, tag in (('EXAMPLE_MAP', 'example-map-default'),
                             ('BUILD_FILTERED', 'build-filtered-default')):
                old = [l for l in t.splitlines() if l.strip().endswith(f'# {tag}: rewritten in-place by config.yml set-matrix')]
                assert len(old) == 1, f'{tag}: sentinel not found exactly once'
                line = old[0]
                new = line.split('default:')[0] + 'default: ' + yq(os.environ[env]) + f'  # {tag}'
                t = t.replace(line, new, 1)
            open(p, 'w').write(t)
            PYEOF
            then
              echo "warning: sentinel rewrite failed - dropping the scoping, full build"
              MATRIX_JSON="$FULL_MATRIX_JSON"
              EXAMPLE_MAP='{}'
              BUILD_FILTERED='false'
            fi

            BUILDSYSTEM_LIST=(
              "cmake"
              "make"
            )

            TOOLCHAIN_LIST=(
              "aarch64-gcc"
              "arm-clang"
              "arm-gcc"
              "esp-idf"
              "ft9xx-gcc"
              "msp430-gcc"
              "riscv-gcc"
            )

            # only build IAR if not forked PR, since IAR token is not shared
            if [ -z $CIRCLE_PR_USERNAME ]; then
              TOOLCHAIN_LIST+=("arm-iar")
            fi

            gen_build_entry() {
              local build_system="$1"
              local toolchain="$2"
              local family="$3"
              local build_args=""

              if [[ "$toolchain" == "arm-iar" || "$build_system" == "make" ]]; then
                build_args="--one-random"
              fi

              if [[ "$toolchain" == "esp-idf" ]]; then
                echo "      - build-vm:" >> .circleci/config2.yml
              else
                echo "      - build:" >> .circleci/config2.yml
              fi
              echo "          matrix:" >> .circleci/config2.yml
              echo "            alias: build-${build_system}-${toolchain}" >> .circleci/config2.yml
              echo "            parameters:" >> .circleci/config2.yml
              echo "              build-system: ['$build_system']" >> .circleci/config2.yml
              echo "              toolchain: ['$toolchain']" >> .circleci/config2.yml
              echo "              family: $family" >> .circleci/config2.yml
              echo "              resource_class: ['large']" >> .circleci/config2.yml
              echo "              build-args: ['$build_args']" >> .circleci/config2.yml
            }

            # Collect all build aliases for code-metrics requires (cmake only, exclude esp-idf)
            BUILD_ALIASES=()

            for build_system in "${BUILDSYSTEM_LIST[@]}"; do
              for toolchain in "${TOOLCHAIN_LIST[@]}"; do
                # make does not support these toolchains
                if [ "$build_system" == "make" ] && { [ "$toolchain" == "arm-clang" ] || [ "$toolchain" == "arm-iar" ] || [ "$toolchain" == "esp-idf" ]; }; then
                  continue
                fi

                FAMILY=$(echo $MATRIX_JSON | jq -r ".\"$toolchain\"")
                echo "FAMILY_${toolchain}=$FAMILY"

                if [ "$(echo "$FAMILY" | jq 'length')" = "0" ]; then
                  # an empty matrix parameter is a hard CircleCI config error, not a skip
                  echo "skip build-${build_system}-${toolchain}: no families selected"
                  continue
                fi

                gen_build_entry "$build_system" "$toolchain" "$FAMILY"
                ANY_BUILD=1

                # Only add cmake builds: excluding esp-idf or build_args="--one-random" to metrics requirements
                if [ "$build_system" == "cmake" ] && [ "$toolchain" != "esp-idf" ] && [ "$toolchain" != "arm-iar" ]; then
                  BUILD_ALIASES+=("build-${build_system}-${toolchain}")
                fi
              done
            done

            if [ ${#BUILD_ALIASES[@]} -gt 0 ]; then
              echo "      - code-metrics:" >> .circleci/config2.yml
              echo "          requires:" >> .circleci/config2.yml
              for alias in "${BUILD_ALIASES[@]}"; do
                echo "            - $alias" >> .circleci/config2.yml
              done
            fi
            if [ "${ANY_BUILD:-0}" != "1" ]; then
              # a workflow with zero jobs is invalid config
              echo "      - no-op" >> .circleci/config2.yml
            fi

      - continuation/continue:
          configuration_path: .circleci/config2.yml

workflows:
  set-matrix:
    jobs:
      - set-matrix