summaryrefslogtreecommitdiff
path: root/.github/workflows/build_util.yml
blob: 43262c650c4dd9460488146ed0c45984d760ebaa (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
name: Reusable build util

on:
  workflow_call:
    inputs:
      os:
        required: false
        type: string
        default: 'ubuntu-latest'
      build-system:
        required: true
        type: string
      toolchain:
        required: true
        type: string
      build-args:
        required: true
        type: string
      build-options:
        required: false
        default: ''
        type: string
      example-map:
        required: false
        default: ''
        type: string
      upload-artifacts:
        required: false
        default: false
        type: boolean
      upload-membrowse:
        required: false
        default: false
        type: boolean
      code-changed:
        required: false
        default: true
        type: boolean

jobs:
  family:
    # PR-scoped HIL selection can produce an empty build-args list for a toolchain
    # (e.g. a dwc2-only change with no riscv boards affected); an empty matrix
    # vector fails the job outright ("Matrix vector 'arg' does not contain any
    # values"), so skip cleanly instead.
    #
    # Why that is safe for callers: GitHub SKIPS the dependents of a skipped
    # `needs:` job, so this only works because the caller (hil-build) is itself a
    # *matrix* job - a matrix with one skipped leg and one successful leg
    # aggregates to success, and its dependents run.
    #
    # NOT covered: if every leg is empty the whole caller job skips, and so does
    # everything that needs it. That is fine only because an all-empty selection
    # means no board was selected for those rigs, so the rig jobs would have had
    # nothing to run anyway (see the invariant on hil-build in build.yml).
    if: inputs.build-args != '[]'
    runs-on: ${{ inputs.os }}
    strategy:
      fail-fast: false
      matrix:
        arg: ${{ fromJSON(inputs.build-args) }}
    steps:
      - name: Checkout TinyUSB
        uses: actions/checkout@v6
        with:
          fetch-depth: ${{ !inputs.upload-membrowse && 1 || 0 }}

      - name: Setup Toolchain
        id: setup-toolchain
        uses: ./.github/actions/setup_toolchain
        with:
          toolchain: ${{ inputs.toolchain }}

      - name: Get Dependencies
        uses: ./.github/actions/get_deps
        with:
          arg: ${{ matrix.arg }}

      - name: Resolve PR example filter
        if: inputs.example-map != '' && inputs.example-map != '{}'
        env:
          # values are PR-derived - keep them out of ${{ }} script interpolation
          # (env expansion word-splits but never re-parses shell metacharacters)
          EXAMPLE_MAP: ${{ inputs.example-map }}
          FAMILY: ${{ matrix.arg }}
        run: |
          # -e flags for this family; a family absent from the map builds everything
          EX_ARGS=$(printf '%s' "$EXAMPLE_MAP" | jq -r --arg fam "$FAMILY" '(.[$fam] // []) | map("-e " + .) | join(" ")') || EX_ARGS=''
          # the map's values are example dir names from the PR checkout, and `jq -r`
          # un-escapes them: a path with a newline (git allows it) would otherwise write
          # extra NAME=VALUE lines into GITHUB_ENV for every later step of this job.
          # Anything outside the example-name alphabet drops the filter (= build all),
          # which is the safe direction.
          case "$EX_ARGS" in
            *[!-A-Za-z0-9_/\ ]*)
              echo "::warning::unexpected characters in the example filter - building all examples"
              EX_ARGS='' ;;
          esac
          echo "EX_ARGS=$EX_ARGS"
          echo "EX_ARGS=$EX_ARGS" >> $GITHUB_ENV

      - name: Build
        if: ${{ inputs.code-changed }}
        env:
          IAR_LMS_BEARER_TOKEN: ${{ secrets.IAR_LMS_BEARER_TOKEN }}
        run: |
          if [ "${{ inputs.toolchain }}" == "esp-idf" ]; then
            # --target all never builds the *-membrowse/*-membrowse-upload custom
            # targets (they are not part of CMake's default `all`, and family_add_membrowse
            # only declares them at configure time) - membrowse itself never runs in this
            # step, so it does not need installing here.
            docker run --rm -e MEMBROWSE_API_KEY="$MEMBROWSE_API_KEY" -e CI="$CI" -v $PWD:/project -w /project espressif/idf:tinyusb bash -c "python tools/build.py ${{ inputs.build-options }} --target all ${{ matrix.arg }} $EX_ARGS"
          else
            BUILD_PY_ARGS="-s ${{ inputs.build-system }} ${{ steps.setup-toolchain.outputs.build_option }} ${{ inputs.build-options }} --target all"
            python tools/build.py $BUILD_PY_ARGS ${{ matrix.arg }} $EX_ARGS
          fi
        shell: bash

      - name: Membrowse Upload
        if: inputs.upload-membrowse == true
        continue-on-error: true
        env:
          MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }}
          ARG: ${{ matrix.arg }}
        run: |
          # A HIL variant leg (--build-name, an extra -D define, or --cflag beyond
          # the bare board) still configures with the same -DBOARD=<name> as the
          # plain build - membrowse's target-name is <board>/<example>, derived
          # from the board alone - so uploading it would overwrite the plain
          # build's (or another variant's) history under that same name. Only a
          # bare `-b <board> [-e ...]` leg, the plain build, uploads here; a
          # variant board still gets built (for the rig) by this same job, just
          # not uploaded from this leg.
          case " $ARG " in
            *' --build-name '*|*' -D'*|*' --cflag='*)
              echo "skip membrowse upload: variant build ($ARG)"; exit 0 ;;
          esac
          # code-changed false -> no elf -> membrowse uploads with --identical.
          # --ci-pinned-boards-only: families without a CI board were built as compile
          # smoke-checks only and must not upload.
          #
          # $EX_ARGS DOES reach tools/build.py here: resolve_ci_boards() needs it to
          # fall back exactly like the Build step above does when the pinned CI board
          # cannot build any PR-selected example (examples/device/cdc_dual_ports/skip.txt
          # vs stm32f407disco is the concrete case in resolve_ci_boards()'s docstring).
          # Without it, this step still resolved the pinned board even when the Build
          # step above had fallen back to a DIFFERENT one, and pushed --identical for
          # every example of a board this run never actually built. Under
          # --ci-pinned-boards-only, tools/build.py uses -e ONLY for that
          # board-eligibility check - it does not narrow which examples of the
          # resolved board get built/uploaded here, so this step's job (touch EVERY
          # example of the resolved board: real upload for the ones the Build step
          # above built, --identical for the rest) is unaffected.
          #
          # $ARG itself can carry -e too, though: hil_ci_set_matrix.py bakes
          # per-example -e flags into matrix.arg for a PR-scoped hil-build-esp
          # leg (unlike the cmake job's bare family arg above, which never does)
          # - left in, that would scope this step's upload down to the same
          # PR-selected examples the comment above says it must not. Strip them
          # (independently of $EX_ARGS, which is appended back below).
          ARG=$(printf '%s' "$ARG" | sed -E 's/ -e [^ ]+//g')
          BUILD_PY_ARGS="-s ${{ inputs.build-system }} ${{ steps.setup-toolchain.outputs.build_option }} ${{ inputs.build-options }}"
          CMD="python tools/build.py $BUILD_PY_ARGS --ci-pinned-boards-only --target examples-membrowse-upload -j 1 $ARG $EX_ARGS"
          if [ "${{ inputs.toolchain }}" == "esp-idf" ]; then
            # docker run only inherits what -e names explicitly (a GHA container gets
            # none of the runner's env by default): membrowse's --github metadata
            # detection (GITHUB_EVENT_NAME/GITHUB_SHA/...) and its fork-PR tokenless
            # auth need these to see the actual event, and GITHUB_SHA to report the
            # right commit. GITHUB_EVENT_PATH points outside the $PWD mount, so mount
            # it too (read-only); guard it since local/non-Actions runs leave it unset.
            DOCKER_ENV=(-e MEMBROWSE_API_KEY="$MEMBROWSE_API_KEY" -e CI="$CI"
                        -e GITHUB_ACTIONS="$GITHUB_ACTIONS" -e GITHUB_EVENT_NAME="$GITHUB_EVENT_NAME"
                        -e GITHUB_SHA="$GITHUB_SHA" -e GITHUB_REF_NAME="$GITHUB_REF_NAME"
                        -e GITHUB_EVENT_PATH="$GITHUB_EVENT_PATH")
            DOCKER_MOUNTS=(-v $PWD:/project)
            [ -n "$GITHUB_EVENT_PATH" ] && DOCKER_MOUNTS+=(-v "$GITHUB_EVENT_PATH:$GITHUB_EVENT_PATH:ro")
            docker run --rm "${DOCKER_ENV[@]}" "${DOCKER_MOUNTS[@]}" -w /project espressif/idf:tinyusb bash -c "pip install membrowse >/dev/null && $CMD"
          else
            $CMD
          fi
        shell: bash

      - name: Artifact name
        if: inputs.upload-artifacts == true
        env:
          ARG: ${{ matrix.arg }}
        run: |
          # -e example filters carry '/', which upload-artifact forbids in artifact
          # names; strip them from the NAME only (the build already consumed them).
          # Names without -e stay byte-identical to before. Two entries differing
          # only in their -e list cannot exist - the -e list is a function of
          # (board), and variant suffixes (--build-name/-D/--cflag) survive the
          # strip - so the stripped name is still unique per matrix entry.
          TAG=$(printf '%s' "$ARG" | sed -E 's/ -e [^ ]+//g')
          # board and example names come from the roster, which a PR can edit; a newline
          # in one would write extra NAME=VALUE lines into GITHUB_ENV for every later
          # step. There is no safe fallback name here - a wrong one mislabels the
          # firmware the rig then flashes - so refuse instead.
          case "$TAG" in
            *[!-A-Za-z0-9_/\ .=+]*)
              echo "::error::refusing to build an artifact name from '$ARG'"; exit 1 ;;
          esac
          echo "ARTIFACT_TAG=$TAG" >> $GITHUB_ENV

      - name: Upload Artifacts for Hardware Testing
        if: inputs.upload-artifacts == true && inputs.code-changed == true
        uses: actions/upload-artifact@v7
        with:
          name: binaries-${{ inputs.toolchain }}-${{ env.ARTIFACT_TAG }}
          path: |
            cmake-build/cmake-build-*/*/*/*.elf
            cmake-build/cmake-build-*/*/*/*.bin
            cmake-build/cmake-build-*/*/*/*.bin
            cmake-build/cmake-build-*/*/*/bootloader/bootloader.bin
            cmake-build/cmake-build-*/*/*/partition_table/partition-table.bin
            cmake-build/cmake-build-*/*/*/config.env
            cmake-build/cmake-build-*/*/*/flash_args
            cmake-build/hw/mcu/**/*.ld