|
| 1 | +# Part of the EDG Compiler Project, under the Apache License v2.0 with LLVM |
| 2 | +# Exceptions. |
| 3 | +# See https://edgcpp.org/LICENSE.txt for license information. |
| 4 | +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 5 | +# |
| 6 | +# Builds one toolchain's preset and tests it with each of its configurations. |
| 7 | +# Called once per toolchain by build-and-test-ci.yml, which describes the |
| 8 | +# overall job graph, the test cache, and coverage; each test leg waits only on |
| 9 | +# this workflow's build, so the toolchains are independent of one another. |
| 10 | +# |
| 11 | +# There is deliberately no concurrency group here: in a called workflow, |
| 12 | +# github.workflow is the caller's name, so a group would make the toolchains |
| 13 | +# cancel each other. The caller's group covers this workflow. |
| 14 | + |
| 15 | +name: CI Toolchain |
| 16 | + |
| 17 | +on: |
| 18 | + workflow_call: |
| 19 | + inputs: |
| 20 | + preset: |
| 21 | + description: The CMake preset to build (also names the build artifact) |
| 22 | + required: true |
| 23 | + type: string |
| 24 | + build_dir: |
| 25 | + description: The preset's build directory (e.g., build/gcc) |
| 26 | + required: true |
| 27 | + type: string |
| 28 | + base: |
| 29 | + description: The directory under bases/docker/dev-env/ for EDG_BASE |
| 30 | + required: true |
| 31 | + type: string |
| 32 | + configs: |
| 33 | + description: A JSON list of the edgy configurations to test |
| 34 | + required: true |
| 35 | + type: string |
| 36 | + run_timestamp: |
| 37 | + description: The edgy run timestamp shared by all test legs |
| 38 | + required: true |
| 39 | + type: string |
| 40 | + test_inputs: |
| 41 | + description: The hash of the test inputs (part of the test cache key) |
| 42 | + required: true |
| 43 | + type: string |
| 44 | + |
| 45 | +permissions: |
| 46 | + contents: read |
| 47 | + |
| 48 | +# The steps refer to the inputs through these variables (in run scripts, as |
| 49 | +# shell variables rather than expressions). |
| 50 | +env: |
| 51 | + GCOVR_VERSION: '8.7' |
| 52 | + CI_PRESET: ${{ inputs.preset }} |
| 53 | + CI_BUILD_DIR: ${{ inputs.build_dir }} |
| 54 | + CI_BASE: ${{ inputs.base }} |
| 55 | + |
| 56 | +jobs: |
| 57 | + build: |
| 58 | + name: Build ${{ inputs.preset }} |
| 59 | + runs-on: ubuntu-latest |
| 60 | + timeout-minutes: 120 |
| 61 | + container: |
| 62 | + image: &ci-runner-image ghcr.io/edgcpp/ci-runner@sha256:da140ca90c420977f187279d7939fe0c7cb3c4b656e5ab2dc0f6b9ac2a43a255 |
| 63 | + defaults: &container-defaults |
| 64 | + run: |
| 65 | + shell: edg-container-exec "bash -eo pipefail {0}" |
| 66 | + |
| 67 | + steps: |
| 68 | + - name: Check out repository |
| 69 | + uses: actions/checkout@v7 |
| 70 | + with: |
| 71 | + persist-credentials: false |
| 72 | + |
| 73 | + - name: Configure ${{ env.CI_PRESET }} |
| 74 | + env: |
| 75 | + EDG_BASE: ${{ github.workspace }}/bases/docker/dev-env/${{ env.CI_BASE }} |
| 76 | + run: >- |
| 77 | + cmake --preset "${CI_PRESET}" -DEDG_CPFE_USE_GCOV=ON |
| 78 | + -DEDG_BUILD_IDENTIFIER=ci |
| 79 | +
|
| 80 | + - name: Build ${{ env.CI_PRESET }} |
| 81 | + env: |
| 82 | + EDG_BASE: ${{ github.workspace }}/bases/docker/dev-env/${{ env.CI_BASE }} |
| 83 | + run: cmake --build "${CI_BUILD_DIR}" |
| 84 | + |
| 85 | + # Runtime layout only: environment.sh / eccp need bin/, lib/, and the |
| 86 | + # per-target lib_linux_* / lib_win* dirs (eccp rewrites ECCP_LIBDIR to |
| 87 | + # lib_<target>). lib_src is build-only and omitted. The .gcno files |
| 88 | + # let the test legs produce coverage reports. |
| 89 | + - name: Upload build products |
| 90 | + uses: actions/upload-artifact@v7 |
| 91 | + with: |
| 92 | + name: build-${{ env.CI_PRESET }} |
| 93 | + path: | |
| 94 | + ${{ env.CI_BUILD_DIR }}/bin |
| 95 | + ${{ env.CI_BUILD_DIR }}/lib |
| 96 | + ${{ env.CI_BUILD_DIR }}/lib_linux_* |
| 97 | + ${{ env.CI_BUILD_DIR }}/lib_win* |
| 98 | + ${{ env.CI_BUILD_DIR }}/environment.sh |
| 99 | + ${{ env.CI_BUILD_DIR }}/src/**/*.gcno |
| 100 | + if-no-files-found: error |
| 101 | + retention-days: 1 |
| 102 | + compression-level: 6 |
| 103 | + |
| 104 | + test: |
| 105 | + name: Test ${{ matrix.config }} |
| 106 | + needs: build |
| 107 | + runs-on: ubuntu-latest |
| 108 | + timeout-minutes: 360 |
| 109 | + strategy: |
| 110 | + fail-fast: false |
| 111 | + matrix: |
| 112 | + config: ${{ fromJSON(inputs.configs) }} |
| 113 | + container: |
| 114 | + image: *ci-runner-image |
| 115 | + # Required for setarch ... -R (disable ASLR) used by the test harness. |
| 116 | + options: --privileged |
| 117 | + defaults: *container-defaults |
| 118 | + |
| 119 | + steps: |
| 120 | + - name: Check out repository |
| 121 | + uses: actions/checkout@v7 |
| 122 | + with: |
| 123 | + persist-credentials: false |
| 124 | + |
| 125 | + - name: Download ${{ env.CI_PRESET }} build |
| 126 | + uses: actions/download-artifact@v7 |
| 127 | + with: |
| 128 | + name: build-${{ env.CI_PRESET }} |
| 129 | + # upload-artifact strips the shared build directory prefix, leaving |
| 130 | + # bin/, lib/, and environment.sh at the artifact root. |
| 131 | + path: ${{ env.CI_BUILD_DIR }} |
| 132 | + |
| 133 | + - name: Restore execute bits on build binaries |
| 134 | + run: | |
| 135 | + # Artifact round-trip does not reliably preserve +x. |
| 136 | + chmod -R a+x "${CI_BUILD_DIR}/bin" |
| 137 | +
|
| 138 | + - name: Compute test cache key |
| 139 | + id: key |
| 140 | + env: |
| 141 | + CONFIG: ${{ matrix.config }} |
| 142 | + TEST_INPUTS: ${{ inputs.test_inputs }} |
| 143 | + run: | |
| 144 | + # Hash the test files this configuration can read, using the |
| 145 | + # image's tools (like edgy itself, so the selection rules match). |
| 146 | + # The helper reads git trees, and git refuses a repository owned |
| 147 | + # by another user (the workspace belongs to the runner's user, not |
| 148 | + # the container's) unless it is marked safe. |
| 149 | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" |
| 150 | + tests="$(EDG_TEST_HOME="${GITHUB_WORKSPACE}/tests" \ |
| 151 | + edg-test-inputs-hash --config "${CONFIG}")" |
| 152 | + echo "Test files: ${tests}" |
| 153 | + # Hash the names and contents of the downloaded build products. |
| 154 | + build="$(cd "${CI_BUILD_DIR}" && |
| 155 | + find . -type f -print0 | LC_ALL=C sort -z | |
| 156 | + xargs -0 sha256sum | sha256sum | cut -c1-64)" |
| 157 | + echo "Build products: ${build}" |
| 158 | + echo "key=tested-${CONFIG}-${TEST_INPUTS}-${tests}-${build}" \ |
| 159 | + >> "${GITHUB_OUTPUT}" |
| 160 | +
|
| 161 | + - name: Look for an earlier passing run of this build |
| 162 | + id: tested |
| 163 | + uses: actions/cache/restore@v6 |
| 164 | + with: |
| 165 | + key: ${{ steps.key.outputs.key }} |
| 166 | + path: .tmp/tested |
| 167 | + |
| 168 | + - name: Reuse earlier results |
| 169 | + if: ${{ steps.tested.outputs.cache-hit == 'true' }} |
| 170 | + run: | |
| 171 | + echo "::notice::This build already passed with the same test" \ |
| 172 | + "inputs: $(cat .tmp/tested/ci-meta.json)" |
| 173 | + mkdir -p coverage |
| 174 | + cp .tmp/tested/coverage/*.json coverage/ || true |
| 175 | +
|
| 176 | + - name: Run edgy (${{ matrix.config }}) |
| 177 | + id: edgy |
| 178 | + if: ${{ steps.tested.outputs.cache-hit != 'true' }} |
| 179 | + env: |
| 180 | + RUN_TIMESTAMP: ${{ inputs.run_timestamp }} |
| 181 | + CONFIG: ${{ matrix.config }} |
| 182 | + run: | |
| 183 | + mkdir -p .tmp |
| 184 | + cat > .tmp/extensive-ci-edg-base.sh <<EOF |
| 185 | + export EDG_BASE="${GITHUB_WORKSPACE}/bases/docker/dev-env/${CI_BASE}" |
| 186 | + EOF |
| 187 | +
|
| 188 | + cat > .tmp/extensive-ci.edgivk <<EOF |
| 189 | + prefix_source TEST_${CONFIG} ${CI_BUILD_DIR}/environment.sh |
| 190 | + prefix_source TEST_${CONFIG} .tmp/extensive-ci-edg-base.sh |
| 191 | + exec edgy --config="${CONFIG}" --run-timestamp="${RUN_TIMESTAMP}" --max-failure-count=10 --timeout=600 -A --meticulous all |
| 192 | + EOF |
| 193 | +
|
| 194 | + edg-prefix-env-loader .tmp/extensive-ci.edgivk |
| 195 | +
|
| 196 | + - name: Upload partial elist, elog, and .rt.tar.gz results |
| 197 | + if: ${{ failure() }} |
| 198 | + uses: actions/upload-artifact@v7 |
| 199 | + with: |
| 200 | + name: test-results-${{ matrix.config }} |
| 201 | + path: | |
| 202 | + tests/runs/${{ inputs.run_timestamp }}/**/*.elist |
| 203 | + tests/runs/${{ inputs.run_timestamp }}/**/*.elog |
| 204 | + tests/runs/${{ inputs.run_timestamp }}/**/*.rt.tar.gz |
| 205 | + if-no-files-found: warn |
| 206 | + retention-days: 1 |
| 207 | + |
| 208 | + # Coverage: the instrumented cpfe / cpfe-cp wrote .gcda files next to |
| 209 | + # the downloaded .gcno files under the build directory's src/. Each leg |
| 210 | + # saves a gcovr JSON report; the coverage job merges them and uploads |
| 211 | + # to Codecov. Coverage problems must never fail the test leg, hence |
| 212 | + # continue-on-error. |
| 213 | + - name: Install gcovr |
| 214 | + if: ${{ !cancelled() && steps.tested.outputs.cache-hit != 'true' }} |
| 215 | + continue-on-error: true |
| 216 | + run: | |
| 217 | + python3 -m venv "${RUNNER_TEMP}/gcovr" |
| 218 | + "${RUNNER_TEMP}/gcovr/bin/pip" install --quiet \ |
| 219 | + "gcovr==${GCOVR_VERSION}" |
| 220 | +
|
| 221 | + # cpfe and cpfe-cp compile the same sources; separate function merging |
| 222 | + # keeps gcovr from rejecting functions seen at different lines. A few |
| 223 | + # hot loops exceed gcovr's "suspicious hits" threshold; warn about them |
| 224 | + # instead of failing. |
| 225 | + - name: Generate coverage report |
| 226 | + if: ${{ !cancelled() && steps.tested.outputs.cache-hit != 'true' }} |
| 227 | + continue-on-error: true |
| 228 | + env: |
| 229 | + CONFIG: ${{ matrix.config }} |
| 230 | + run: | |
| 231 | + mkdir -p coverage |
| 232 | + "${RUNNER_TEMP}/gcovr/bin/gcovr" \ |
| 233 | + --root . \ |
| 234 | + --filter 'src/' \ |
| 235 | + --merge-mode-functions=separate \ |
| 236 | + --gcov-ignore-parse-errors=negative_hits.warn_once_per_file \ |
| 237 | + --gcov-ignore-parse-errors=suspicious_hits.warn_once_per_file \ |
| 238 | + -j "$(nproc)" \ |
| 239 | + --json "coverage/${CONFIG}.json" \ |
| 240 | + --print-summary \ |
| 241 | + "${CI_BUILD_DIR}" |
| 242 | +
|
| 243 | + - name: Upload coverage report |
| 244 | + if: ${{ !cancelled() && hashFiles('coverage/*.json') != '' }} |
| 245 | + continue-on-error: true |
| 246 | + uses: actions/upload-artifact@v7 |
| 247 | + with: |
| 248 | + name: coverage-${{ matrix.config }} |
| 249 | + path: coverage/${{ matrix.config }}.json |
| 250 | + if-no-files-found: warn |
| 251 | + retention-days: 1 |
| 252 | + |
| 253 | + # Record the pass (with its coverage report, so a run that skips edgy |
| 254 | + # still contributes coverage). Without a report, don't record it. |
| 255 | + - name: Prepare record of this passing run |
| 256 | + id: record |
| 257 | + if: >- |
| 258 | + ${{ steps.edgy.outcome == 'success' && |
| 259 | + hashFiles(format('coverage/{0}.json', matrix.config)) != '' }} |
| 260 | + env: |
| 261 | + CONFIG: ${{ matrix.config }} |
| 262 | + RUN_ID: ${{ github.run_id }} |
| 263 | + RUN_ATTEMPT: ${{ github.run_attempt }} |
| 264 | + TESTED_SHA: ${{ github.sha }} |
| 265 | + run: | |
| 266 | + mkdir -p .tmp/tested/coverage |
| 267 | + cp "coverage/${CONFIG}.json" .tmp/tested/coverage/ |
| 268 | + python3 - > .tmp/tested/ci-meta.json <<'EOF' |
| 269 | + import json, os |
| 270 | + print(json.dumps({ |
| 271 | + 'tested_sha': os.environ['TESTED_SHA'], |
| 272 | + 'run_id': int(os.environ['RUN_ID']), |
| 273 | + 'run_attempt': int(os.environ['RUN_ATTEMPT']), |
| 274 | + })) |
| 275 | + EOF |
| 276 | +
|
| 277 | + - name: Save record of this passing run |
| 278 | + if: ${{ steps.record.outcome == 'success' }} |
| 279 | + uses: actions/cache/save@v6 |
| 280 | + with: |
| 281 | + key: ${{ steps.key.outputs.key }} |
| 282 | + path: .tmp/tested |
0 commit comments