From 521d02ecdd82cebca3e09edc0bc29a329990d5a5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 8 Aug 2026 19:34:15 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20linux=20=E5=8A=A0=E4=B8=80=E6=9D=A1=20llv?= =?UTF-8?q?m=20=E8=85=BF,=E5=B9=B6=E7=BB=99=E5=AE=83=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E8=A3=85=E4=BA=86=E7=B3=BB=E7=BB=9F=20ffmpeg=20=E7=9A=84?= =?UTF-8?q?=E5=AE=BF=E4=B8=BB=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #183 修的两个包(compat.ffmpeg 的 -idirafter 排在系统目录之后、compat.catch2 漏 #include )在这份 CI 里一直是绿的,不是因为它们对,而是因为这份 CI 结构上看不见它们: * mcpp 在 linux 的默认工具链是 gcc,而它通过 --sysroot 进 xlings subos 编 译 —— 那里的 /usr/include 干干净净,宿主机的头根本不在搜索路径上。 * 就算换了工具链,GitHub runner 的 /usr/include 里也没有 libav*,-idirafter 没有东西可输。 所以这条腿要同时改两件事才有意义:换 llvm(无 sysroot),并真的把 ffmpeg 的 dev 头装上。只做前者,本次这个 bug 照样照不出来。 ## 矩阵 emit() 多一个 toolchain 维度,linux 发两次(default + llvm),macos / windows 保持 default。job 名只在非 default 时才带工具链后缀,所以既有 job 名不变。 成本是实打实的:linux 从 3 个 shard 变 6 个,而实测 linux runner 并发是 3, 所以第二条腿是排在第一条后面跑,full run 的 linux 墙钟大致翻倍。要调的话 杠杆在 shards_for 上面那段注释里。 ## 三处必须跟着改的地方 * registry / toolstore 缓存键加 matrix.toolchain。两条腿的 runner.os 都是 Linux,而缓存装的是工具链和编译好的 compat 包 —— 共用一个条目会让 gcc 的产物替 llvm 回答,正好抹掉这条腿存在的理由。 * timings artifact 名加 toolchain。upload-artifact@v4 拒绝重名,不加的话两 条腿会抢 `timings-linux-0`,第二个直接把 job 弄失败。 * member-timings.tsv 只吃 default 腿。llvm 腿跑的是同一批成员,把每条腿都 glob 进去会让每个 (platform, member) 出现两行(sort -u 留不住,秒数不 同),而 shards_for 是把匹配行全加起来的 —— linux 的工作量会读成约两倍, shard 数被永久顶到上限。step summary 里则按腿分别列,两条腿是不同的构建, 平均它们谁也不描述。 ## 已知会红 catch2-v2-main 在装了系统 Catch2 v3 的机器上会失败:catch2_main.cpp 用 __has_include() 判 v2/v3,这个探测同样会落到系统目录。 GitHub runner 不装 catch2,所以这条腿上它是绿的(实测 #183 的 CI:linux / macos / windows 三平台 catch2-v2-main 全 ok)。真要修得等 per-version build blocks(mcpp#290)。 本地只在 llvm 下取样跑了 19/60 个成员,其余 41 个(grpc / protobuf / godot / llamacpp / openssl 等重型)受本机磁盘所限没跑 —— 这条腿的第一轮就是它们第一 次在 linux 上被 llvm 编译,可能还有别的既有问题被照出来。刻意不加 continue-on-error:一条非阻塞的腿很容易被永久无视,和一条长期红的腿是同一种病。 --- .github/workflows/validate.yml | 113 +++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 19 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e22be8b..27d653a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -410,10 +410,10 @@ jobs: shell: bash run: | full=0; [ "$MEMBERS" = "__ALL__" ] && full=1 - emit() { # platform os suffix ext mcpp xlings shards - for i in $(seq 0 $(( $7 - 1 ))); do - printf '{"platform":"%s","os":"%s","suffix":"%s","ext":"%s","mcpp":"%s","xlings":"%s","shard":%d,"shards":%d},' \ - "$1" "$2" "$3" "$4" "$5" "$6" "$i" "$7" + emit() { # platform os suffix ext mcpp xlings toolchain shards + for i in $(seq 0 $(( $8 - 1 ))); do + printf '{"platform":"%s","os":"%s","suffix":"%s","ext":"%s","mcpp":"%s","xlings":"%s","toolchain":"%s","shard":%d,"shards":%d},' \ + "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$i" "$8" done } # Shard count follows the WORK, per platform, measured. @@ -477,9 +477,27 @@ jobs: wn=$(shards_for windows 2) { printf '{"include":[' - emit linux ubuntu-latest linux-x86_64 tar.gz bin/mcpp registry/bin/xlings "$ln" - emit macos macos-15 macosx-arm64 tar.gz bin/mcpp registry/bin/xlings "$mn" - emit windows windows-latest windows-x86_64 zip bin/mcpp.exe registry/bin/xlings.exe "$wn" + # linux runs TWICE: once on mcpp's default toolchain and once on + # llvm. Not redundancy — the two differ in a way that decides + # whether a descriptor is correct. mcpp's linux default (gcc) + # reaches the compiler through --sysroot into a clean xlings + # subos, so the HOST /usr/include is not on the search path; + # llvm has no sysroot and the host's headers are. compat.ffmpeg + # put its vendored source root on -idirafter (below the system + # dirs) and was green here for months while being broken on any + # developer box with libavutil-dev installed — the gcc leg + # structurally cannot see that class of bug. + # + # Cost, measured: this doubles linux to 6 jobs (2 x 3 shards) at + # ~75min each, against a measured linux runner concurrency of 3 — + # so the second leg QUEUES behind the first rather than running + # beside it, and a full run's linux wall-clock roughly doubles. + # That is the price of the coverage; the levers if it hurts are + # in the shards_for comment above. + emit linux ubuntu-latest linux-x86_64 tar.gz bin/mcpp registry/bin/xlings default "$ln" + emit linux ubuntu-latest linux-x86_64 tar.gz bin/mcpp registry/bin/xlings llvm "$ln" + emit macos macos-15 macosx-arm64 tar.gz bin/mcpp registry/bin/xlings default "$mn" + emit windows windows-latest windows-x86_64 zip bin/mcpp.exe registry/bin/xlings.exe default "$wn" printf ']}' } | sed 's/,]}/]}/' > /tmp/matrix.json echo "matrix=$(cat /tmp/matrix.json)" >> "$GITHUB_OUTPUT" @@ -563,8 +581,10 @@ jobs: bash tests/check_graphics_install_side_effects.sh workspace: - # The shard suffix appears only when the platform is actually split. - name: workspace (${{ matrix.platform }}${{ matrix.shards == 1 && '' || format(' {0}/{1}', matrix.shard, matrix.shards) }}) + # The shard suffix appears only when the platform is actually split, and + # the toolchain only when it is not mcpp's default — so the existing job + # names are unchanged and only the new llvm leg is labelled. + name: workspace (${{ matrix.platform }}${{ matrix.toolchain == 'default' && '' || format(' {0}', matrix.toolchain) }}${{ matrix.shards == 1 && '' || format(' {0}/{1}', matrix.shard, matrix.shards) }}) needs: select if: needs.select.outputs.members != '' runs-on: ${{ matrix.os }} @@ -606,7 +626,11 @@ jobs: # only a Git-Bash convenience. h=$(git ls-files -s -- 'pkgs/**/*.lua' 'tests/**' '.github/workflows/validate.yml' \ | git hash-object --stdin) - echo "REGISTRY_CACHE_KEY=mcpp-registry-${{ runner.os }}-${{ env.MCPP_VERSION }}-$h" >> "$GITHUB_ENV" + # matrix.toolchain is in the key: both linux legs are runner.os + # Linux, and the cache holds TOOLCHAINS and built compat packages. + # Sharing one entry would let the gcc leg's objects answer for the + # llvm leg — exactly the confusion this second leg exists to catch. + echo "REGISTRY_CACHE_KEY=mcpp-registry-${{ runner.os }}-${{ matrix.toolchain }}-${{ env.MCPP_VERSION }}-$h" >> "$GITHUB_ENV" - name: Restore mcpp registry cache uses: actions/cache@v4 with: @@ -615,7 +639,7 @@ jobs: path: ~/.mcpp/registry key: ${{ env.REGISTRY_CACHE_KEY }} restore-keys: | - mcpp-registry-${{ runner.os }}-${{ env.MCPP_VERSION }}- + mcpp-registry-${{ runner.os }}-${{ matrix.toolchain }}-${{ env.MCPP_VERSION }}- # Host tools built from source (protoc, grpc_cpp_plugin, …). # # Measured on the run that added grpc-codegen: 636s to build protoc and @@ -639,9 +663,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.mcpp/build-cache/v1/tool - key: mcpp-toolstore-${{ runner.os }}-${{ env.MCPP_VERSION }}-${{ github.run_id }}-${{ matrix.platform }}-${{ matrix.shard }} + key: mcpp-toolstore-${{ runner.os }}-${{ matrix.toolchain }}-${{ env.MCPP_VERSION }}-${{ github.run_id }}-${{ matrix.platform }}-${{ matrix.shard }} restore-keys: | - mcpp-toolstore-${{ runner.os }}-${{ env.MCPP_VERSION }}- + mcpp-toolstore-${{ runner.os }}-${{ matrix.toolchain }}-${{ env.MCPP_VERSION }}- - name: Download mcpp shell: bash env: @@ -688,6 +712,43 @@ jobs: # plans, mcpp#232). The sandbox copy lands in ~/.mcpp/registry, so # the cache carries it across runs. + # ── Make the llvm leg look like a developer's box ───────────────── + # A bare GitHub runner has no libav* under /usr/include, so a + # descriptor that loses its vendored headers to the host's would still + # be green here. That is precisely how compat.ffmpeg's -idirafter hole + # survived: nothing in CI had a system ffmpeg to lose to. Installing + # the dev headers is what turns this leg into a real test of include + # ORDER rather than of include EXISTENCE. + # + # Deliberately only on the llvm leg. The default leg reaches its + # compiler through --sysroot, so it is immune by construction and + # installing there would change an existing job's environment for no + # coverage. Keeping the blast radius to the new leg means a red here + # is always about the thing this leg was added to watch. + - name: Install host ffmpeg dev headers (llvm leg only) + if: matrix.platform == 'linux' && matrix.toolchain == 'llvm' + shell: bash + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq \ + libavutil-dev libavcodec-dev libavformat-dev \ + libavfilter-dev libavdevice-dev libswscale-dev libswresample-dev + dpkg -l | grep -E 'libav(util|codec)-dev|libswscale-dev' + + # ── Select the toolchain for this leg ───────────────────────────── + # No version pin: `llvm` resolves to whatever the toolchain family's + # current release is for the PINNED MCPP_VERSION, which is the version + # that mcpp itself vouches for. Pinning a concrete llvm here would fail + # hard the day that mcpp release stops shipping it, and the point of + # this leg is the ABSENCE of a sysroot, not a particular llvm. + - name: Select toolchain + if: matrix.toolchain != 'default' + shell: bash + run: | + "$MCPP" toolchain install ${{ matrix.toolchain }} + "$MCPP" toolchain default ${{ matrix.toolchain }} + "$MCPP" toolchain list + # ── This shard's slice of the plan ──────────────────────────────── # `select` decided WHAT runs; this decides which part of it runs HERE. # Round-robin by position, which is what spreads the expensive members: @@ -780,7 +841,10 @@ jobs: if: always() && hashFiles('timings.tsv') != '' uses: actions/upload-artifact@v4 with: - name: timings-${{ matrix.platform }}-${{ matrix.shard }} + # The toolchain is part of the name because upload-artifact@v4 + # refuses duplicates: without it both linux legs would race to + # upload `timings-linux-0` and the second one would fail the job. + name: timings-${{ matrix.platform }}-${{ matrix.toolchain }}-${{ matrix.shard }} path: timings.tsv retention-days: 14 @@ -831,10 +895,13 @@ jobs: exit 0 fi - # Artifact name carries the platform: timings--. - for plat in linux macos windows; do + # Artifact name carries platform AND toolchain leg: + # timings---. Ranking per LEG, not per + # platform: the two linux legs are different builds and averaging + # them would describe neither. + for leg in linux-default linux-llvm macos-default windows-default; do rows=$(mktemp) - for f in timings/timings-$plat-*/timings.tsv; do + for f in timings/timings-$leg-*/timings.tsv; do [ -f "$f" ] && cat "$f" >> "$rows" done [ -s "$rows" ] || { rm -f "$rows"; continue; } @@ -842,7 +909,7 @@ jobs: total=$(awk -F'\t' '{s += $1} END {print s+0}' "$rows") count=$(wc -l < "$rows") { - echo "### $plat — ${count} member(s), ${total}s of member wall-clock" + echo "### $leg — ${count} member(s), ${total}s of member wall-clock" echo echo "| rank | seconds | share | member | result |" echo "|---:|---:|---:|---|---|" @@ -867,7 +934,15 @@ jobs: echo "# \t\t — from run ${{ github.run_id }}" echo "# refresh: download the member-timings artifact and replace this file" for plat in linux macos windows; do - for f in timings/timings-$plat-*/timings.tsv; do + # ONLY the default-toolchain leg feeds this table. The llvm leg + # runs the SAME members again, so globbing every leg would put + # two rows per (platform, member) into the file — `sort -u` + # keeps both, since the seconds differ — and shards_for sums + # every matching row. Linux work would read as roughly double + # and its shard count would be permanently pinned at the cap. + # Both legs are planned from these numbers, so the default leg + # is the right single baseline. + for f in timings/timings-$plat-default-*/timings.tsv; do [ -f "$f" ] || continue awk -F'\t' -v p="$plat" '{ printf "%s\t%s\t%s\n", p, $2, $1 }' "$f" done