|
| 1 | +#!/usr/bin/env bash |
| 2 | +# The two lanes that read this collection's C++ with something other than the |
| 3 | +# default Linux toolchain. A CI step names the lane: |
| 4 | +# |
| 5 | +# bash .github/scripts/check-toolchain-lanes.sh llvm |
| 6 | +# bash .github/scripts/check-toolchain-lanes.sh editor |
| 7 | +# |
| 8 | +# THREE COMBINATIONS READ THIS CODE, AND EACH ONE HAS REFUSED SOMETHING THE |
| 9 | +# OTHERS ACCEPT. GCC with libstdc++ builds it by default on Linux. clang with |
| 10 | +# libc++ builds it under `--toolchain llvm@…`, as on macOS. clang with |
| 11 | +# libstdc++'s `std` module is what an editor's clangd uses to read a project |
| 12 | +# built with GCC -- and no build ever compiled that combination, so a |
| 13 | +# `std::pair<std::string, value>` inside `value` (0.19.1) failed only there: |
| 14 | +# every build passed and every build program in an editor showed a module that |
| 15 | +# was "not found". docs/plugin-development.md §4 states the rule this enforces. |
| 16 | +# |
| 17 | +# Environment: MCPP (llvm); MCPP_EDITOR and MCPPLS (editor). |
| 18 | +set -euo pipefail |
| 19 | + |
| 20 | +ROOT=$(cd "$(dirname "$0")/../.." && pwd) |
| 21 | +LLVM="${MCPP_LLVM:-llvm@22.1.8}" |
| 22 | + |
| 23 | +fail() { echo "FAIL: $*"; exit 1; } |
| 24 | + |
| 25 | +# ── llvm: clang with libc++ ─────────────────────────────────────────────────── |
| 26 | +# |
| 27 | +# FULL BUILDS for the fixtures whose build needs nothing tied to libstdc++, each |
| 28 | +# with the criterion its program prints. `all-rules-compile` imports every |
| 29 | +# member and calls it, so the whole collection is compiled under libc++ by its |
| 30 | +# first line; the others run what the members produce. |
| 31 | +# |
| 32 | +# CONFIGURE ONLY for the Qt fixtures. Qt's official Linux binaries are built |
| 33 | +# against libstdc++ and a libc++ program cannot link them, which is the Qt |
| 34 | +# fixtures' business and not this lane's. `--configure-only` still compiles and |
| 35 | +# runs the build program -- the member and the library under libc++ -- and that |
| 36 | +# is the part this lane is for. |
| 37 | +# |
| 38 | +# NOT HERE: the fixtures that pin a device toolchain (cuda, hip, sycl, |
| 39 | +# multi-rule) already build with llvm@22.1.8 in the consumers job, and the |
| 40 | +# deps fixtures' libc++ rows are `vcpkg-libcxx` and `cmake-cache` there. |
| 41 | +llvm_full=( |
| 42 | + "all-rules-compile|^all-rules-compile ok" |
| 43 | + "plugin-logic|" |
| 44 | + "embed-consumer|^size=33 " |
| 45 | + "embed-module-consumer|^all ok" |
| 46 | + "embed-table-consumer|^all ok" |
| 47 | + "island-interface|^all ok" |
| 48 | + "archive-consumer|^archive-consumer: 'greetings from the archive'" |
| 49 | + "spirv-module-consumer|^all ok" |
| 50 | +) |
| 51 | +llvm_configure_only=(qt-consumer qt-widgets-consumer qt-sdk-consumer qt-import-only) |
| 52 | + |
| 53 | +llvm_lane() { |
| 54 | + : "${MCPP:?MCPP names the mcpp under test}" |
| 55 | + "$MCPP" toolchain install llvm "${LLVM#llvm@}" |
| 56 | + local entry fixture expect results declared passed |
| 57 | + for entry in "${llvm_full[@]}"; do |
| 58 | + fixture=${entry%%|*}; expect=${entry#*|} |
| 59 | + echo "── $fixture under $LLVM" |
| 60 | + cd "$ROOT/tests/$fixture" |
| 61 | + rm -rf target; mkdir -p target/ci |
| 62 | + "$MCPP" build --toolchain "$LLVM" > target/ci/llvm-build.log 2>&1 || |
| 63 | + { cat target/ci/llvm-build.log; fail "$fixture does not build under $LLVM"; } |
| 64 | + grep -q -- '-stdlib=libc++' target/*/*/build.ninja || |
| 65 | + fail "$fixture was not linked against libc++: the lane did not build what it says it built" |
| 66 | + if [ "$fixture" = plugin-logic ]; then |
| 67 | + # The kit's verdicts, counted against the cases the fixture declares, |
| 68 | + # as check-deps-and-qt.sh counts them under the default toolchain. |
| 69 | + results=$(find target -path '*plugins-testing/results.txt' | head -1) |
| 70 | + [ -n "$results" ] || fail "the kit wrote no results.txt under $LLVM" |
| 71 | + declared=$(grep -cE '^ \{ "' build.mcpp) |
| 72 | + passed=$(grep -c '^PASS ' "$results") |
| 73 | + [ "$declared" -gt 0 ] && [ "$passed" -eq "$declared" ] || |
| 74 | + { cat "$results"; fail "$passed of $declared plugin-logic cases passed under $LLVM"; } |
| 75 | + echo "ok: $passed of $declared plugin-logic cases passed under $LLVM" |
| 76 | + continue |
| 77 | + fi |
| 78 | + "$MCPP" run --toolchain "$LLVM" > target/ci/llvm-run.log 2>&1 || |
| 79 | + { cat target/ci/llvm-run.log; fail "$fixture built under $LLVM and did not run"; } |
| 80 | + grep -qE "$expect" target/ci/llvm-run.log || |
| 81 | + { cat target/ci/llvm-run.log; fail "$fixture ran under $LLVM without printing '$expect'"; } |
| 82 | + echo "ok: $fixture builds and runs under $LLVM" |
| 83 | + done |
| 84 | + for fixture in "${llvm_configure_only[@]}"; do |
| 85 | + echo "── $fixture under $LLVM, configured" |
| 86 | + cd "$ROOT/tests/$fixture" |
| 87 | + rm -rf target; mkdir -p target/ci |
| 88 | + "$MCPP" build --configure-only --toolchain "$LLVM" > target/ci/llvm-configure.log 2>&1 || |
| 89 | + { cat target/ci/llvm-configure.log; fail "$fixture's build program does not compile or run under $LLVM"; } |
| 90 | + echo "ok: $fixture's build program compiles and runs under $LLVM" |
| 91 | + done |
| 92 | +} |
| 93 | + |
| 94 | +# ── editor: clang with libstdc++, through mcppls ────────────────────────────── |
| 95 | +# |
| 96 | +# What an editor shows for each fixture's `build.mcpp`, read by mcppls with its |
| 97 | +# own clangd over the default (GCC) toolchain's standard library. A module the |
| 98 | +# build program imports that clangd cannot compile is an error on the import |
| 99 | +# line, so `counts.error` is the criterion; every diagnostic goes to the job |
| 100 | +# summary, so a failure names the line without downloading anything. |
| 101 | +# |
| 102 | +# A FILE THAT WAS NOT READ IS A FAILURE, NOT A PASS. mcppls says so with |
| 103 | +# `build-program-context` when the mcpp it runs describes no build programs |
| 104 | +# (before 2026.10.11.1); zero errors from a file nobody compiled is the silent |
| 105 | +# pass this lane exists to prevent. |
| 106 | +editor_lane() { |
| 107 | + : "${MCPP_EDITOR:?MCPP_EDITOR names the mcpp mcppls runs}" |
| 108 | + : "${MCPPLS:?MCPPLS names the mcppls binary of the payload}" |
| 109 | + export PATH="$(dirname "$MCPP_EDITOR"):$PATH" |
| 110 | + export MCPPLS_CACHE_DIR="${RUNNER_TEMP:-/tmp}/mcppls-cache" |
| 111 | + local summary="${GITHUB_STEP_SUMMARY:-/dev/null}" failed=0 checked=0 dir fixture out |
| 112 | + { |
| 113 | + echo "### build.mcpp in the editor ($("$MCPPLS" --version | head -1), $("$MCPP_EDITOR" --version | head -1))" |
| 114 | + echo |
| 115 | + echo "| fixture | errors | warnings | diagnostics |" |
| 116 | + echo "|---|---|---|---|" |
| 117 | + } >> "$summary" |
| 118 | + for dir in "$ROOT"/tests/*/; do |
| 119 | + fixture=$(basename "$dir") |
| 120 | + [ -f "$dir/build.mcpp" ] || continue |
| 121 | + echo "── $fixture" |
| 122 | + out="${RUNNER_TEMP:-/tmp}/editor-$fixture.json" |
| 123 | + # mcppls exits 1 when it reports an error and still writes the report, |
| 124 | + # so the report decides; only a run that wrote none is its own failure. |
| 125 | + local status=0 |
| 126 | + (cd "$dir" && "$MCPPLS" diagnostics build.mcpp --format json --timeout 300) > "$out" || status=$? |
| 127 | + python3 -c 'import json, sys; json.load(open(sys.argv[1]))' "$out" 2> /dev/null || |
| 128 | + { echo "| $fixture | — | — | mcppls diagnostics exited $status without a report |" >> "$summary"; failed=1; continue; } |
| 129 | + checked=$((checked + 1)) |
| 130 | + python3 - "$fixture" "$out" "$summary" <<'EOF' || failed=1 |
| 131 | +import json, sys |
| 132 | +fixture, path, summary = sys.argv[1:4] |
| 133 | +report = json.load(open(path)) |
| 134 | +counts = report["counts"] |
| 135 | +found = [d for f in report["files"] for d in f["diagnostics"]] |
| 136 | +unread = [d for d in found if d.get("code") == "build-program-context"] |
| 137 | +incomplete = [f["file"] for f in report["files"] if not f.get("complete", True)] |
| 138 | +cells = "<br>".join( |
| 139 | + f"{d['severity']} {d['location']['file']}:{d['location']['line']} `{d.get('code', '')}` {d['message'][:240]}".replace("|", "\\|") |
| 140 | + for d in found) or "none" |
| 141 | +with open(summary, "a") as out: |
| 142 | + out.write(f"| {fixture} | {counts['error']} | {counts['warning']} | {cells} |\n") |
| 143 | +for d in found: |
| 144 | + print(f" {d['severity']} {d['location']['file']}:{d['location']['line']} [{d.get('code', '')}] {d['message']}") |
| 145 | +if counts["error"] or unread or incomplete: |
| 146 | + why = "errors" if counts["error"] else "not read" if unread else "incomplete" |
| 147 | + print(f"FAIL: {fixture}'s build.mcpp in the editor: {why}") |
| 148 | + sys.exit(1) |
| 149 | +print(f"ok: {fixture}'s build.mcpp has no error in the editor") |
| 150 | +EOF |
| 151 | + done |
| 152 | + [ "$checked" -gt 0 ] || fail "no fixture's build.mcpp was checked" |
| 153 | + [ "$failed" = 0 ] || fail "an editor reports errors in a fixture's build.mcpp (the job summary lists them)" |
| 154 | + echo "ok: $checked build programs read in the editor without an error" |
| 155 | +} |
| 156 | + |
| 157 | +case "${1:-}" in |
| 158 | + llvm) llvm_lane ;; |
| 159 | + editor) editor_lane ;; |
| 160 | + *) echo "usage: $0 llvm|editor"; exit 2 ;; |
| 161 | +esac |
0 commit comments