From e5136810c98c9d908092312fe32d2614fd78ca60 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 02:34:55 +0900 Subject: [PATCH 1/9] 2026.11.15.1: a long case divided by the cases it declared; the tests built with profile ci; main's test build reused; the matrix scan side by side; a cancelled run releases its group --- ...6-11-14-ci-and-release-time-cost-design.md | 43 +++++++++++ .github/actions/mcpp-unit/action.yml | 14 +++- .github/tools/source_mtimes.py | 73 +++++++++++++++++++ .github/workflows/ci-macos.yml | 4 + .github/workflows/ci-target-matrix.yml | 13 ++-- .github/workflows/ci-unit.yml | 52 ++++++++++++- .github/workflows/ci-windows.yml | 4 + .github/workflows/ci.yml | 3 + CHANGELOG.md | 28 +++++++ docs/08-testing.md | 6 ++ docs/specs/test.md | 3 + docs/zh/08-testing.md | 5 +- mcpp.toml | 11 ++- modules/versioning/src/version.cppm | 2 +- src/build/execute.cppm | 51 ++++++++++++- src/build/test_protocol.cppm | 7 ++ src/build/test_schedule.cppm | 72 +++++++++++++++--- tests/matrix/scan.sh | 44 ++++++++--- tests/scripts/test_source_mtimes.py | 63 ++++++++++++++++ tests/unit/test_test_schedule.cpp | 42 +++++++++++ 20 files changed, 502 insertions(+), 38 deletions(-) create mode 100755 .github/tools/source_mtimes.py create mode 100644 tests/scripts/test_source_mtimes.py diff --git a/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md b/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md index 86650ddab..0387f95af 100644 --- a/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md +++ b/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md @@ -226,3 +226,46 @@ Windows 把每个 `\n` 写成 `\r\n`;`write_config_value` 只认 `\n` 结尾 四个构建同时开始(最长为 Linux aarch64 交叉构建 39 分钟),`create-release` 在四者通过后创建 release,自分派至创建共 40 分钟;v2026.11.12.1 的构建依次进行,至最后一个构建结束约 104 分钟。 封存与发布两个作业被跳过:未带条件的作业只在其之前的全部作业(含间接的)成功时运行,未请求时被跳过的 canaries 因此跳过了二者。 二者改为各自声明所跟随的那一个作业(mcpp#933);v2026.11.13.1 以 `tag: v2026.11.13.1` 再次分派完成,该分派重建该 tag 的提交。 + +## 6. 第二轮:资源复用、编排与关键路径(负责人 2026-11-15 确认 W1–W4、子用例调度与 macOS 路径;W5 更大的 runner 不考虑) + +### 6.1 测量(PR 运行 43602794812,墙钟 97.6 分钟) + +75 个作业,runner 时间 606 分钟,排队合计 265 分钟。关键路径: + +| 区间(分钟) | 原因 | +|---|---| +| 0 → 9.1 | 上一次运行取消后,`target-matrix / coverage`(`if: always()`)仍排队 8.5 分钟等 runner,运行 6 s,期间并发组被占用 | +| 9.1 → 20.7 | 首批作业排队 11.4 分钟:组织内同时有三次全量 CI(main push、另一个 PR、本次) | +| 20.7 → 69.3 | archive:排队 9.3 分钟,构建 39.4 分钟(1900 条边,4 核上 36 分钟) | +| 69.3 → 97.4 | 四个分片 28.0 / 27.9 / 12.4 / 9.8 分钟 | + +细粒度分解: + +- **archive 的编译。** 本包缺省 profile 为 release,测试连同库以 `-O2` 编译。本机 ninja 日志:库与单元测试的编译器 + 时间 8988 s(库 3423、测试 5565);以 `-O0`、无调试信息为 4183 s(库 1709、测试 2471),减少 53%。4 核上约 + 36 分钟对 17 分钟,与 CI 的 36 分钟一致。 +- **分片。** 四个分片的工作量合计 198 分钟,16 个工作线程的理想值为 12.4 分钟。分片 1 与 3 的墙钟由两个套件决定: + `e2e/openkal/trees` 1373 s、`e2e/openkal/sets` 1375 s,利用率 43% 与 71%。两者的子用例在一个进程中依次运行, + 其中最长者为 trees/955 564 s、sets/963 310 s。列表(P3.4)不给出子用例,`split = "auto"` 因而无法划分它们。 +- **macOS。** 单元测试在 macOS 作业中构建并运行 31 分钟;`target-matrix / scan` 依次扫描 59 格,1762 s。 + `Toolchain: LLVM — build mcpp (self-host)` 是本提交的 mcpp 构建自身,不是重复构建,保留。 +- **缓存配额。** 仓库的 Actions 缓存已用 9.98 GB(上限 10 GB),测试构建目录放入缓存会逐出各宿主的 home 缓存。 + +### 6.2 决定与实现 + +| 编号 | 机制 | 实现 | +|---|---|---| +| W1 | main 的测试构建被复用 | main 的 archive 阶段把构建目录(含 `.source-blobs`:每个被跟踪文件的 blob)以 zstd 打包为 artifact `mcpp-unit-build-`;PR 的 archive 阶段取 main 最近一次成功运行的该 artifact,`source_mtimes.py restore` 把 blob 未变的文件设为早于任何输出的时间,变化的文件保留检出时间。用 artifact 而不用缓存,见 6.1 的配额 | +| W2 | CI 的测试 profile | `[profile.ci]`(`-O0`、无调试信息、strip);archive 阶段与 macOS、Windows 的单元作业以 `--profile ci` 构建测试(`mcpp-unit` 的 `profile` 输入) | +| W3 | 按声明的用例划分长用例 | mtest 0.9.0(P4.3):选择行 `!` 排除用例及其声明的用例,`?` 在用例被声明时选中、未被声明时不报告 `missing`;`program` 事件陈述 `selection: ["except", "optional"]`。mcpp 的 O10:程序陈述两者时,长于份额且存储中测得两个以上声明用例的列出用例按最内层声明用例划分,前 k−1 组以 `?` 选中,第 k 组选中该用例并以 `!` 排除前几组;用例的估计不小于其声明用例之和;存储中用例的用时按程序与标识记取(此前只按标识,`layers`、`sets` 在两个程序中同名) | +| W4 | 取消的运行立即释放并发组 | `coverage` 作业改为 `if: ${{ !cancelled() }}`;其余工作流的作业级条件已是此形式 | +| M1 | 矩阵扫描并行 | `scan.sh` 每格在独立目录中运行(`scan_cell`),`MATRIX_JOBS`(缺省为处理器数)格同时进行,按表的次序输出 | + +**未采纳。** 「列出子用例」(运行器请求时在列表模式中执行用例体):用例体本身就是叶子用例的测试,列出即运行测试。 +以历史与 `!`/`?` 选择达到同一目的,新增与改名的子用例都只运行一次。macOS 单元测试分片:macOS runner 的并发 +是排队的来源(6.1),增加作业会加重排队;以 W2 与 W3 缩短单个作业。 + +本地核验:W1 恢复后无改动的重建 0.4 s,改动一个接口模块的重建 88 s(冷构建 248 s);W3 演示工程四个子用例 +8.2 s → 3.0 s,改名的子用例只在余下组中运行,旧名在 `?` 组中不报告 `missing`;单元 +`TestSchedule/ACaseIsDividedByTheCasesItDeclared`、`TestTargets` 与 mtest `suites` 通过。 diff --git a/.github/actions/mcpp-unit/action.yml b/.github/actions/mcpp-unit/action.yml index 0314673c5..eec47323b 100644 --- a/.github/actions/mcpp-unit/action.yml +++ b/.github/actions/mcpp-unit/action.yml @@ -23,6 +23,12 @@ inputs: description: the test archive to run (`mcpp test --archive`); empty to build the tests here required: false default: '' + profile: + description: > + the profile the tests are built with when they are built here (`mcpp test --profile`); + empty for the package's default + required: false + default: '' shard: description: I/N, this job's shard of the tests (`mcpp test --shard`) required: false @@ -103,6 +109,7 @@ runs: env: ARCHIVE: ${{ inputs.archive }} SHARD: ${{ inputs.shard }} + PROFILE: ${{ inputs.profile }} SINCE: ${{ inputs.since }} PATTERNS: ${{ inputs.patterns }} TIMEOUT: ${{ inputs.timeout }} @@ -117,7 +124,12 @@ runs: fresh="$MCPP_FRESH" if command -v cygpath >/dev/null 2>&1; then fresh="$(cygpath -w "$fresh")"; fi export MCPP_BIN_MCPP="${MCPP_BIN_MCPP:-$fresh}" - if [ -n "$ARCHIVE" ]; then args+=(--archive "$ARCHIVE"); else args+=(--no-programs); fi + if [ -n "$ARCHIVE" ]; then + args+=(--archive "$ARCHIVE") + else + args+=(--no-programs) + [ -z "$PROFILE" ] || args+=(--profile "$PROFILE") + fi if [ -n "${MCPP_UNIT_FAILED:-}" ]; then args+=(--failed) else diff --git a/.github/tools/source_mtimes.py b/.github/tools/source_mtimes.py new file mode 100755 index 000000000..ddbd036c3 --- /dev/null +++ b/.github/tools/source_mtimes.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""The sources' times for an incremental build restored from a cache. + +A checkout writes every file with the time of the checkout, so a build directory +restored from a cache is older than every source and ninja rebuilds all of it. +The cache records each tracked file's blob beside the build (`record`); after a +checkout, a file whose blob is the one recorded is given a time older than any +output (`restore`), and a changed or new file keeps the checkout's, so exactly +what changed is rebuilt. + + source_mtimes.py record + source_mtimes.py restore +""" +import os +import subprocess +import sys + +STAMP = ".source-blobs" +OLD = 946684800 # 2000-01-01: older than every output a build writes + + +def blobs(): + """Each tracked file's blob; a file the working tree changes has none.""" + git = lambda *a: subprocess.run(["git", *a], check=True, capture_output=True).stdout + edited = {p.decode() for p in git("ls-files", "-m", "-z").split(b"\0") if p} + out = git("ls-files", "-s", "-z") + table = {} + for entry in out.split(b"\0"): + if not entry: + continue + meta, path = entry.split(b"\t", 1) + if path.decode() not in edited: + table[path.decode()] = meta.split()[1].decode() + return table + + +def main(argv): + if len(argv) != 3 or argv[1] not in ("record", "restore"): + print(__doc__.strip(), file=sys.stderr) + return 2 + stamp = os.path.join(argv[2], STAMP) + if argv[1] == "record": + os.makedirs(argv[2], exist_ok=True) + with open(stamp, "w", encoding="utf-8") as f: + for path, blob in sorted(blobs().items()): + f.write(f"{blob} {path}\n") + return 0 + if not os.path.isfile(stamp): + print(f"no {stamp}: the build directory is rebuilt whole") + return 0 + recorded = {} + with open(stamp, encoding="utf-8") as f: + for line in f: + blob, path = line.rstrip("\n").split(" ", 1) + recorded[path] = blob + same = changed = 0 + current = blobs() + for path in subprocess.run(["git", "ls-files", "-z"], check=True, + capture_output=True).stdout.decode().split("\0"): + if not path: + continue + blob = current.get(path) + if blob is not None and recorded.get(path) == blob and os.path.isfile(path): + os.utime(path, (OLD, OLD), follow_symlinks=False) + same += 1 + else: + changed += 1 + print(f"{same} file(s) as recorded, {changed} changed or new") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 75b51835e..e869bbeb9 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -321,6 +321,10 @@ jobs: uses: ./.github/actions/mcpp-unit with: name: ${{ matrix.image }} + # -O0 without debug information (mcpp.toml `[profile.ci]`): the + # package's default is release, and its tests took twice the compiler + # time with it (design 2026-11-14 §6). + profile: ci history: ${{ matrix.image }} since: ${{ env.MCPP_TEST_SINCE }} patterns: ${{ (matrix.image == 'xcode-27' && inputs.set == 'pr') && 'unit e2e/basics e2e/build e2e/macos e2e/modules e2e/runtime e2e/toolchain' || '' }} diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index e8e0b5417..dce57833e 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -408,13 +408,16 @@ jobs: # workflow is green. This job is the one place that reads the expected table # as a whole and requires the run to have covered it. # - # It runs even when a scan failed (`always()`), because "which hosts were - # covered" is a different question from "did they pass" — and a run where a - # host is missing entirely should say so in those words rather than leaving - # a reader to infer it from a job list. + # It runs even when a scan failed, because "which hosts were covered" is a + # different question from "did they pass" — and a run where a host is + # missing entirely should say so in those words rather than leaving a + # reader to infer it from a job list. Not in a cancelled run: `always()` + # queued it for a runner after the cancellation, and the concurrency group + # was held until it ran (run 43599371222: 8.5 minutes, the next run's + # start delayed by 9.1). name: coverage (every host the table names was scanned) needs: scan - if: always() + if: ${{ !cancelled() }} runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ci-unit.yml b/.github/workflows/ci-unit.yml index 6aa055a26..2764951b7 100644 --- a/.github/workflows/ci-unit.yml +++ b/.github/workflows/ci-unit.yml @@ -97,6 +97,30 @@ jobs: echo "the bootstrap $("$MCPP" --version 2>/dev/null | head -1) builds the package's program into the archive" fi + # MAIN'S TEST BUILD, REUSED (design 2026-11-14 §6, W1). The archive of + # main's last successful run carries its build directory and the blob of + # every tracked file it was built from. A file whose blob is the same is + # given a time older than every output; a changed one keeps the + # checkout's, so only what changed and what imports it is built again. + # The bootstrap mcpp is pinned, so the configuration is main's; a pin + # that moved builds whole, as a run without the artifact does. + - name: Main's test build + shell: bash + env: + GH_TOKEN: ${{ github.token }} + HOST: ${{ inputs.host }} + run: | + set -uo pipefail + run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow ci.yml --branch main --status success --limit 1 \ + --json databaseId -q '.[0].databaseId' 2>/dev/null || true) + [ -n "$run" ] || { echo "no successful run of main: the tests are built whole"; exit 0; } + if ! gh run download "$run" -R "$GITHUB_REPOSITORY" -n "mcpp-unit-build-$HOST" -D "$RUNNER_TEMP/build" 2>/dev/null; then + echo "main's run $run kept no test build for $HOST: the tests are built whole"; exit 0 + fi + tar --zstd -xf "$RUNNER_TEMP/build/target.tar.zst" && rm -f "$RUNNER_TEMP/build/target.tar.zst" + python3 .github/tools/source_mtimes.py restore target + echo "main's run $run: its test build restored" + # EVERY TEST, ON EVERY SET. The bootstrap mcpp that builds them selects # by change without the rule that a test running the package's program # is affected by the program's sources (2026.11.13.1), so it would leave @@ -110,7 +134,10 @@ jobs: export MCPP_VENDORED_XLINGS="$XLINGS_BIN" mkdir -p "$RUNNER_TEMP/unit" # shellcheck disable=SC2086 -- the flag is one word or none - "$MCPP" test --no-run ${{ steps.programs.outputs.flag }} --archive "$RUNNER_TEMP/unit/unit.tar.gz" 2>&1 | tee "$RUNNER_TEMP/archive.log" + # The profile `ci` (mcpp.toml): -O0 and no debug information. The + # default profile of this package is release (-O2); the tests built + # with it took 8988 s of compiler time, and 4183 s with `ci`. + "$MCPP" test --profile ci --no-run ${{ steps.programs.outputs.flag }} --archive "$RUNNER_TEMP/unit/unit.tar.gz" 2>&1 | tee "$RUNNER_TEMP/archive.log" n=$(sed -n 's/.*Archived \([0-9][0-9]*\) test.*/\1/p' "$RUNNER_TEMP/archive.log" | tail -1) [ -f "$RUNNER_TEMP/unit/unit.tar.gz" ] || n=0 echo "archived=${n:-0}" >> "$GITHUB_OUTPUT" @@ -146,6 +173,27 @@ jobs: if-no-files-found: error retention-days: 3 + # THE TEST BUILD FOR THE NEXT RUN: main's only, so a pull request + # starts from the build of the branch it merges into. + - name: Keep the test build + id: keep + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.archive.outputs.archived != '0' + shell: bash + run: | + set -euo pipefail + python3 .github/tools/source_mtimes.py record target + tar --zstd -cf "$RUNNER_TEMP/target.tar.zst" target + du -h "$RUNNER_TEMP/target.tar.zst" + - name: Upload the test build + if: steps.keep.outcome == 'success' + timeout-minutes: 10 + uses: actions/upload-artifact@v4 + with: + name: mcpp-unit-build-${{ inputs.host }} + path: ${{ runner.temp }}/target.tar.zst + compression-level: 0 + retention-days: 7 + # The tests a job that runs named cases takes (e2e-cases), built here # beside the others; the job names the program they test. - name: Write the tests other jobs run @@ -155,7 +203,7 @@ jobs: set -euo pipefail export MCPP_VENDORED_XLINGS="$XLINGS_BIN" # shellcheck disable=SC2086 -- the input is a list of test paths - "$MCPP" test ${{ inputs.tests }} --no-run ${{ steps.programs.outputs.flag }} --archive "dist/mcpp-tests.tar.gz" + "$MCPP" test ${{ inputs.tests }} --profile ci --no-run ${{ steps.programs.outputs.flag }} --archive "dist/mcpp-tests.tar.gz" - name: Upload the tests other jobs run if: inputs.tests != '' diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index e0cfad13b..132e8fbc8 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -73,6 +73,10 @@ jobs: uses: ./.github/actions/mcpp-unit with: name: windows + # -O0 without debug information (mcpp.toml `[profile.ci]`): the + # package's default is release, and its tests took twice the compiler + # time with it (design 2026-11-14 §6). + profile: ci history: windows mcpp: ${{ env.MCPP_SELF }} since: ${{ env.MCPP_TEST_SINCE }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f44e1b5eb..f33399331 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -202,6 +202,9 @@ jobs: - name: The release proposes its index entry from the sealed manifest, GitCode last run: python3 tests/scripts/test_index_pr.py + - name: A build restored from main's run rebuilds what changed + run: python3 tests/scripts/test_source_mtimes.py + - name: The protocol table of SPEC-007 names the engine's protocol run: python3 tests/scripts/test_protocol_table.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 399efa3cc..08c853bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.11.15.1] - 2026-11-15 + +### Added + +- `[test] split = "auto"` divides a listed case longer than its share by the + cases it declared, as the run store measured them (SPEC-017 O10, P4.3), when + the program's selection excludes and selects where declared (mtest 0.9.0): + `/ [] (i/k)`. Each part but the last selects the cases it + was given where they are still declared (`?`); the last selects the case + without them (`!`), so a case no run measured runs once. Two e2e suites + of 1373 s and 1375 s ran their cases one after another while the longest of + those cases takes 564 s. +- `[profile.ci]` in this package's manifest: the tests as CI builds them, -O0 + without debug information (the library and its unit tests: 8988 s of compiler + time at the default release profile, 4183 s with it). + +### Changed + +- The run store's case durations are kept by program and case id; two programs + with a case of one name (`layers`, `sets`) read each other's. +- CI: the archive stage reuses main's test build: an artifact of its build + directory with the blob of each tracked file, whose unchanged files are given + a time older than every output (`.github/tools/source_mtimes.py`); the tests + are built with `--profile ci` on Linux, macOS and Windows; the target-matrix + scan builds its cells side by side (`MATRIX_JOBS`); a cancelled run's + `coverage` job no longer waits for a runner and holds the concurrency group + (it held it 9.1 minutes in run 43599371222). + ## [2026.11.14.1] - 2026-11-14 ### Added diff --git a/docs/08-testing.md b/docs/08-testing.md index 360c493d6..0b516a34c 100644 --- a/docs/08-testing.md +++ b/docs/08-testing.md @@ -91,6 +91,12 @@ with one link. `[test] split = "case"` gives each case its own process, then divides a file the run store measured longer than its share of the run (the run's measured work over its workers and shards) into groups of its listed cases of about equal measured duration, named `/ (i/k)`. +A listed case longer than its share whose program's selection excludes and +selects where declared (SPEC-017 P4.3, mtest 0.9.0) is divided by the cases it +declared as the store measured them, which a list does not name +(2026.11.15.1+): `/ [] (i/k)`. Each part but the last +selects the cases it was given where they are still declared; the last selects +the case without them, so a case no run measured runs once, in it. `--split ` states the value for one invocation. A pattern names a program or a part of one as the run names it: `mcpp test unit/test_home` (or `home`) runs that file's part. diff --git a/docs/specs/test.md b/docs/specs/test.md index 5a7dceed2..b82deb094 100644 --- a/docs/specs/test.md +++ b/docs/specs/test.md @@ -109,6 +109,7 @@ - **P4.1** 有 `MTEST_SELECT` 时,程序**必须**恰好运行标识为该文件某一行的用例,比较前去掉一个行尾 `\r`,逐字节比较。不指向任何用例的标识以 `missing` 事件报告,并使运行失败。 - **P4.2** 模式属于运行器:提供 `fs/*` 的运行器按列表(P3)解析它,并传递标识。协议没有模式语法,因而也没有转义。 +- **P4.3 排除与按声明选择(mtest >= 0.9.0)。** 程序**可以**在 `program` 事件中以 `selection` 列出它理解的选择行:`except` 时,以 `!` 开头的一行把其后标识的用例及其声明的用例排除出选择,只有排除行的选择运行除这些之外的全部用例;`optional` 时,以 `?` 开头的一行选中其后标识的用例,该用例未被声明时**不**报告 `missing`。运行器**禁止**向未列出相应值的程序写这两种行。 - **mcpp 的实现(已实现,mcpp >= 2026.11.8.1)。** `mcpp test --cases` 以 `list` 模式运行每个实现协议的程序;`--case <模式>` 把每个程序中匹配的用例写成 `MTEST_SELECT`,列出用例之下的名字选中该用例(P3.4)。 #### P5. 报告 @@ -309,6 +310,7 @@ 1. **估计。** 运行之前,运行器从 B4 的存储读取最近的运行(至多 30 次,按 `started` 由新到旧):每个程序记录名与每个用例标识的最近用时。一个测试的估计是其所选用例用时之和,未选用例时为程序整体的用时。没有测量的程序记录取其最近目录(按 `/` 逐级向上)中已测程序记录的平均值,各级都没有时取已测程序记录中的最大值;没有测量的用例取同一列表中已测用例的平均值,都没有时取其程序估计按用例数的等分。状态为 `built` 或 `not-run` 的记录与跳过的用例不计入。 2. **顺序。** 测试**应当**按估计从长到短开始;占用 `machine` 的测试仍排在最后(P7.1)。 3. **划分。** `split = "auto"` 时,份额为全部测试的估计之和除以工作线程数与分片数;估计长于份额、且含两个以上列出用例的部分**必须**划分为 ⌈估计/份额⌉ 组(不多于用例数),每组按最长处理时间优先分配用例,组内保持列出顺序,名为 `<部分> (i/k)`。 + **按声明的用例划分(mcpp >= 2026.11.15.1)。** 程序的选择理解 `except` 与 `optional`(P4.3)时,估计长于份额、且存储中测得两个以上其声明的用例(P3.4:列表不给出这些用例)的列出用例**必须**按这些用例中最内层者划分为 ⌈估计/份额⌉ 组(不多于其数),名为 `<部分> [<用例>] (i/k)`:前 k−1 组以 `?` 行选中所分得的用例,第 k 组选中该用例并以 `!` 行排除前 k−1 组的用例。没有测量的声明用例因而只在第 k 组运行一次,已删除或改名的用例不使运行失败。一个用例的估计不小于其最内层声明用例的测得用时之和,因为被划分的运行只测得其余下部分。存储中用例的用时按其程序(部分所属的程序)与标识记取。 4. **分片。** `--shard I/N` 把每个测试按估计从长到短(相同时按名字)分配给已分配估计之和最小的分片(相同时取编号最小者),只运行第 I 个分片的测试。分配**必须**只取决于存储与本次运行的测试列表。分片**禁止**把本组(`GITHUB_RUN_ID`,无则为空)的分片运行读作历史,使同一存储上先后启动的各分片读到相同的历史。 5. **失败重跑。** `--failed` 读取存储中最近的一次运行:状态为 `fail` 或 `build-fail` 的程序记录与状态为 `fail` 或 `xpass` 的用例。所选程序为这些记录的 `part_of`(无则为其名字);每个失败的程序都有失败用例时,只运行这些用例(与 `--case` 相同);否则整体运行这些程序。最近一次运行没有失败时,运行器不运行任何测试,退出码为 0。 @@ -351,3 +353,4 @@ mcpp 以 feature(docs/50 §4)宣告它实现的条款;客户端按需门 | 2.0 | 2026-11-10 | 改为 mcpp 测试规范:合并 SPEC-017 v1.0、mtest 协议版本 1 的规范正文与 SPEC-004 的 `[test]` 键;分为基础能力 B1–B6 与可选能力 O1–O9;新增 B5 启动描述、B6 的 `test-capabilities` 与 `test-session`、O3 检查点细节、O8 输出归属、O9 测试会话(mcpp#914、mcpp#915) | | 2.1 | 2026-11-14 | P3.3 的位置与能力(`only`、`except`、`capabilities`、列为跳过时的 `reason`;mtest 0.8.0),及 mcpp 的实现 | | 2.2 | 2026-11-14 | O10 按测量调度(顺序、`split = "auto"`、`--shard`、`--failed`);B4 的 `shard` 与 `part_of` | +| 2.3 | 2026-11-15 | P4.3 排除与按声明选择(mtest 0.9.0);O10 按声明的用例划分,用例的用时按程序与标识记取 | diff --git a/docs/zh/08-testing.md b/docs/zh/08-testing.md index 432a9533a..b5ddcbb14 100644 --- a/docs/zh/08-testing.md +++ b/docs/zh/08-testing.md @@ -70,7 +70,10 @@ note,说明有多少文件仍各自是一个程序;它不形成任何组时 `<程序>/<文件>`——兼得每文件一个程序的隔离与并行,而只链接一次。`[test] split = "case"` 使每个用例一个进程,`"none"` 只运行一次,`"auto"`(2026.11.13.1+)先按文件划分,再把运行存储测得 长于其份额(本次运行测得的工作量除以工作线程数与分片数)的文件按其列出的用例划分为测得用时大致相等的 -若干组,名为 `<程序>/<文件> (i/k)`。`--split ` 为一次调用给出该值。模式按运行所用的名字指名程序或其一部分:`mcpp test unit/test_home`(或 `home`) +若干组,名为 `<程序>/<文件> (i/k)`。程序的选择支持排除与按声明选择(SPEC-017 P4.3,mtest 0.9.0)时, +长于份额的列出用例按存储测得的其声明的用例划分(列表不给出这些用例;2026.11.15.1+),名为 +`<程序>/<文件> [<用例>] (i/k)`:前几组只在用例仍被声明时选中所分得的用例,最后一组选中该用例并排除前几组 +的用例,没有测量的用例因而只在其中运行一次。`--split ` 为一次调用给出该值。模式按运行所用的名字指名程序或其一部分:`mcpp test unit/test_home`(或 `home`) 运行该文件的那一部分。 占用整台机器的用例(P7.1,资源 `machine`)运行时旁边没有其他测试(2026.11.13.1+)。mcpp diff --git a/mcpp.toml b/mcpp.toml index 0589631af..492af4814 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.14.1" +version = "2026.11.15.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] @@ -57,6 +57,15 @@ cflags = ["--coverage"] cxxflags = ["--coverage"] ldflags = ["--coverage"] +# The tests as CI builds them (ci-unit.yml, mcpp-unit): -O0 and no debug information. The +# package's default profile is release (-O2); building the library and its unit tests with it took +# 8988 s of compiler time, and with this 4183 s (design 2026-11-14 §6). A failure is read from +# mtest's report, which names the place of each check without debug information. +[profile.ci] +opt = 0 +debug = false +strip = true + [test] windows_code_page = "utf-8" programs = "directory" diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index b5ec58849..22ffc497f 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.11.14.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.15.1"; } // namespace mcpp diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 64073896c..d3ca83cbe 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -5166,7 +5166,8 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, for (std::string line; std::getline(in, line);) if (!line.empty()) ids.push_back(line); } - r.estimateMs = ts::estimate(history, r.name, ids); + // A part's cases are measured as cases of the program it is of. + r.estimateMs = ts::estimate(history, r.partOf.empty() ? r.name : r.partOf, ids); } // THE SHARD: every test assigned, longest first, to the shard with the // least work; this command runs its own. The assignment reads only the @@ -5952,8 +5953,44 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, } // A group the store measured longer than its share, as groups // `(i/k)` of its cases of about equal measured duration. - struct Part { std::string where; bool machine; std::vector ids; std::size_t index = 0, of = 1; }; + struct Part { std::string where; bool machine; std::vector ids; std::size_t index = 0, of = 1; + std::string within; }; std::vector parts; + // A CASE LONGER THAN ITS SHARE, DIVIDED BY THE CASES IT DECLARES + // (P3.4, P4.3). A list names only the cases a program declares + // at its top; the store names the cases they declared when they + // ran. Each part but the last selects the measured cases it was + // given where they are still declared (`?`); the last selects + // the case without them (`!`), so a case no run measured runs + // once, in it. Only a program whose selection understands both + // (mtest 0.9.0) is divided so. + if (measured && share > 0 && report.selectsWithin) { + for (auto& [key, ids] : groups) { + std::vector rest; + for (auto& id : ids) { + const auto inner = ts::measured_within(history, r.name, id); + const std::vector one{id}; + std::size_t k = 1; + if (inner.size() >= 2) + k = std::min(inner.size(), static_cast( + std::ceil(ts::estimate(history, r.name, one) / share))); + if (k <= 1) { rest.push_back(id); continue; } + auto chunks = ts::divide(history, r.name, inner, k); + std::vector last{id}; + for (std::size_t i = 0; i + 1 < chunks.size(); ++i) { + std::vector given; + for (auto const& c : chunks[i]) { + last.push_back("!" + c); + given.push_back("?" + c); + } + parts.push_back({key.first, key.second, std::move(given), i + 1, chunks.size(), id}); + } + parts.push_back({key.first, key.second, std::move(last), chunks.size(), chunks.size(), id}); + } + ids = std::move(rest); + } + std::erase_if(groups, [](auto const& g) { return g.second.empty(); }); + } for (auto& [key, ids] : groups) { std::size_t k = 1; if (measured && share > 0 && ids.size() > 1) @@ -5975,16 +6012,22 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, std::size_t n = 0; // A pattern that named a part, not its program, keeps that part. const bool whole = selected_by(testOpts.filters, r.name); - for (auto& [where, machine, ids, index, of] : parts) { + for (auto& [where, machine, ids, index, of, within] : parts) { Runnable part = r; const auto label = split == "case" ? where : part_label(r.partRoot, where); part.name = std::format("{}/{}", r.name, label); + // A part of a divided case is named by it as well. + if (!within.empty()) part.name += std::format(" [{}]", within); // Named apart only when its file has cases of both kinds. if (machine && groups.contains({where, false})) part.name += " (machine)"; if (of > 1) part.name += std::format(" ({}/{})", index, of); part.partOf = r.name; part.alone = machine; - part.declaredSecs = declared(&ids); + { + // A part of a divided case may run as long as the case states. + const std::vector whose{within}; + part.declaredSecs = declared(within.empty() ? &ids : &whose); + } if (!whole && !selected_by(testOpts.filters, part.name)) continue; part.protocolDir = r.protocolDir / "split" / std::to_string(n++); std::error_code ec; diff --git a/src/build/test_protocol.cppm b/src/build/test_protocol.cppm index 58cab055d..58ccb4ada 100644 --- a/src/build/test_protocol.cppm +++ b/src/build/test_protocol.cppm @@ -121,6 +121,10 @@ struct Report { // framework's library): it is one case, judged by its exit status. bool spoke = false; std::string framework; + // The program's selection leaves out what a line `!` names and selects + // what a line `?` names only where it is declared (P4.3, `"selection": + // ["except", "optional"]` in its `program` event; mtest 0.9.0). + bool selectsWithin = false; bool done = false; // the `done` event came std::vector listed; // `case` events (list mode) std::vector cases; // in the order they began @@ -289,6 +293,9 @@ take(Report& r, std::map& open, const nlohmann::json& r.spoke = true; r.level = number(e, "level"); r.framework = text(e, "framework"); + const auto selection = strings(e, "selection"); + r.selectsWithin = std::ranges::contains(selection, std::string("except")) + && std::ranges::contains(selection, std::string("optional")); } else if (kind == "case") { r.listed.push_back({id, text(e, "name"), text(e, "file"), number(e, "line"), text(e, "kind"), strings(e, "tags"), text(e, "expect"), bounds_of(e), diff --git a/src/build/test_schedule.cppm b/src/build/test_schedule.cppm index 00bbfbb5a..964798f6a 100644 --- a/src/build/test_schedule.cppm +++ b/src/build/test_schedule.cppm @@ -11,8 +11,9 @@ import nlohmann.json; export namespace mcpp::build::test_schedule { -// What earlier runs measured: the newest duration of each program record and -// of each case, by name and by case id. +// What earlier runs measured: the newest duration of each program record, by +// its name, and of each case, by its program and its id (`case_key`): two +// programs may each have a case `layers`. struct History { std::map programs; std::map cases; @@ -27,8 +28,18 @@ struct History { History read_history(const std::filesystem::path& store, std::size_t maxRuns = 30, std::optional shardsOf = std::nullopt); +// The key of a case in `History::cases`: its program (the program a part is of) +// and its id. +std::string case_key(std::string_view program, std::string_view id); + +// THE CASES A CASE DECLARES, AS RUNS MEASURED THEM (P3.4: a list does not name +// them): the innermost below `id` in program `program`, in id order. A runner +// divides a long case by them (P4.3). +std::vector measured_within(const History& h, std::string_view program, std::string_view id); + // The duration a run of `cases` of the program record `name` is expected to -// take; with no cases, the program run whole. +// take; with no cases, the program run whole. A case written `!` is left +// out of the cases that declare it (P4.3). // // WHAT NO RUN MEASURED. A program no run measured takes the mean of the // measured programs of its nearest directory (`e2e/run/x`: of `e2e/run/`, @@ -120,7 +131,10 @@ History read_history(const std::filesystem::path& store, std::size_t maxRuns, const auto dir = store / "runs" / text(run, "id"); if (!std::filesystem::is_directory(dir, ec)) continue; ++h.runs; + std::map programOf; // a part's record -> its program for (const auto& p : read_lines(dir / "programs.ndjson")) { + const auto of = text(p, "part_of"); + programOf[text(p, "name")] = of.empty() ? text(p, "name") : of; const auto status = text(p, "status"); const double ms = number(p, "ms"); // A test built and not run, or not run, measured nothing. @@ -129,7 +143,10 @@ History read_history(const std::filesystem::path& store, std::size_t maxRuns, } for (const auto& c : read_lines(dir / "cases.ndjson")) { const double ms = number(c, "ms"); - if (ms >= 0 && text(c, "status") != "skip") h.cases.try_emplace(text(c, "id"), ms); + if (ms < 0 || text(c, "status") == "skip") continue; + const auto record = text(c, "program"); + const auto of = programOf.find(record); + h.cases.try_emplace(case_key(of != programOf.end() ? of->second : record, text(c, "id")), ms); } } return h; @@ -165,12 +182,20 @@ std::vector case_estimates(const History& h, std::string_view name, std: std::vector est(ids.size(), -1); double sum = 0; std::size_t known = 0; - for (std::size_t i = 0; i < ids.size(); ++i) - if (auto it = h.cases.find(ids[i]); it != h.cases.end()) { - est[i] = it->second; - sum += it->second; - ++known; - } + for (std::size_t i = 0; i < ids.size(); ++i) { + // `?` (P4.3) is the case where it is declared. + const std::string_view id = ids[i].starts_with('?') ? std::string_view(ids[i]).substr(1) : ids[i]; + auto it = h.cases.find(case_key(name, id)); + if (it == h.cases.end()) continue; + // A case a run divided measured only its rest there: it takes at + // least what the cases it declared took. + double inner = 0; + for (auto const& c : measured_within(h, name, id)) + if (auto m = h.cases.find(case_key(name, c)); m != h.cases.end()) inner += m->second; + est[i] = (std::max)(it->second, inner); + sum += est[i]; + ++known; + } if (known == ids.size()) return est; const double each = known ? sum / static_cast(known) : program_estimate(h, name) / static_cast(ids.size()); @@ -181,11 +206,34 @@ std::vector case_estimates(const History& h, std::string_view name, std: } // namespace +std::string case_key(std::string_view program, std::string_view id) { + return std::format("{}\n{}", program, id); +} + +std::vector measured_within(const History& h, std::string_view program, std::string_view id) { + const auto prefix = case_key(program, id) + "/"; + const auto skip = case_key(program, "").size(); + std::vector keys; + for (auto it = h.cases.lower_bound(prefix); it != h.cases.end() && it->first.starts_with(prefix); ++it) + keys.push_back(it->first); + std::vector out; + for (std::size_t i = 0; i < keys.size(); ++i) { + // A case that declares a measured case is not one of the innermost. + if (i + 1 < keys.size() && keys[i + 1].starts_with(keys[i] + "/")) continue; + out.push_back(keys[i].substr(skip)); + } + return out; +} + double estimate(const History& h, std::string_view name, std::span cases) { if (cases.empty()) return program_estimate(h, name); + std::vector in, out; + for (const auto& c : cases) (c.starts_with('!') ? out : in).push_back(c.starts_with('!') ? c.substr(1) : c); double sum = 0; - for (double e : case_estimates(h, name, cases)) sum += e; - return sum; + for (double e : case_estimates(h, name, in)) sum += e; + if (!out.empty()) + for (double e : case_estimates(h, name, out)) sum -= e; + return (std::max)(sum, 0.0); } std::vector assign(std::span estimates, std::span names, diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index 72cb363a4..44df65540 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -153,9 +153,15 @@ emit() { printf 'scan: %s %s × %s: %s/%s (%ss)\n' "$1" "$3" "$4" "${10}" "${11}" "$((SECONDS - cell_began))" >&2 } -cell_began=$SECONDS -for tc in $(compilers); do - for t in $(targets); do +# ONE CELL, IN A DIRECTORY OF ITS OWN: its row on standard output, its notes on +# standard error. The cells are independent builds, so they run side by side +# (MATRIX_JOBS, the processors by default) and are printed in the table's +# order. Measured on macos-arm64 (run 43602794812): 59 cells one after another, +# 1762 s, of which no cell used more than one compiler at a time for long. +scan_cell() { + local tc="$1" t="$2" dir + dir="$(mktemp -d "$work/cell.XXXXXX")" && cd "$dir" || return + mkdir -p src cell_began=$SECONDS # graph 体系不覆盖裸机行,而这是一句声明,不是一次省略。 # @@ -174,7 +180,7 @@ for tc in $(compilers); do # changing one and missing the other is how it failed. if [ "$MODE" = graph ] && printf '%s' "$t" | grep -qE -- '-none-[a-z0-9]+$'; then echo "scan: 略过 graph × $t —— 裸机行不在这套依赖的论域内" >&2 - continue + return fi # iOS 行同理,而**理由是被实测出来的一条缺陷,不是一句论域声明**。 @@ -200,7 +206,7 @@ for tc in $(compilers); do # 期望。缺陷记在 .agents/docs 的 gap 表里,属于 openkal-musl。 if [ "$MODE" = graph ] && printf '%s' "$t" | grep -qE -- '-ios(-sim)?$'; then echo "scan: 略过 graph × $t —— openkal-musl 0.3.5 编不过 Apple cross(uint64_t/kal_u64 类型身份不同),见 scan.sh 注释" >&2 - continue + return fi # The compiler is the project's default, for the query and the build alike. A toolchain named @@ -243,13 +249,13 @@ for tc in $(compilers); do # 全红,原因(`timeout` 不存在)被这个重定向吞掉了。 q="$(run_limited "${MATRIX_QUERY_TIMEOUT:-300}" \ "$MCPP" why toolchain --target "$t" --format json \ - 2>"$work/q.err" | tr -d '\r')" + 2>q.err | tr -d '\r')" if [ -z "$q" ]; then # 查询本身没跑起来。这不是「这一格不支持」,而是「不知道」—— 两者必须 # 分开,否则一次环境故障会被整片读成「不支持」。 - echo "scan: $t × $tc 查询无输出: $(head -2 "$work/q.err" | tr '\n' ' ')" >&2 + echo "scan: $t × $tc 查询无输出: $(head -2 q.err | tr '\n' ' ')" >&2 emit "$MODE" "$HOST" "$t" "$tc" - - - - - mismatch query-failed - continue + return fi jq_get() { printf '%s' "$q" | jq_r "$1" 2>/dev/null; } st="$(jq_get '.data.status // "-"')" @@ -276,7 +282,7 @@ for tc in $(compilers); do fi emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" \ unsupported "$rs" - continue + return fi # ── 第二问:解析说可以,那它建得出来吗 ──────────────────────────── @@ -285,7 +291,7 @@ for tc in $(compilers); do # 读任何一行输出。 rm -rf target if run_limited "${MATRIX_TIMEOUT:-600}" "$MCPP" build --target "$t" \ - >"$work/b.out" 2>&1; then + >b.out 2>&1; then emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" ok none else # 与查询失败同型:`build-failed` 是对的分类,而没有证据说明为什么。 @@ -296,9 +302,25 @@ for tc in $(compilers); do # 而要看的正是后半截。判据的单位是一整行输出。`mcpp:` 行是编译器 # 监督自身的陈述(哪一次观察判定了失败),它不带 `error:`,却是原因。 echo "scan: $t × $tc 构建失败:" >&2 - grep -m4 -iE '^error|error:|^mcpp: ' "$work/b.out" | sed 's/^/ /' >&2 + grep -m4 -iE '^error|error:|^mcpp: ' b.out | sed 's/^/ /' >&2 emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" \ mismatch build-failed fi +} + +jobs="${MATRIX_JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)}" +n=0 +for tc in $(compilers); do + for t in $(targets); do + n=$((n + 1)); id="$(printf '%04d' "$n")" + ( scan_cell "$tc" "$t" > "$work/$id.row" 2> "$work/$id.log" ) & + # bash 3.2 (macOS's /bin/bash) has no `wait -n`. + while [ "$(jobs -rp | wc -l | tr -d ' ')" -ge "$jobs" ]; do sleep 1; done done done +wait +for row in "$work"/*.row; do + [ -e "$row" ] || continue + cat "${row%.row}.log" >&2 + cat "$row" +done diff --git a/tests/scripts/test_source_mtimes.py b/tests/scripts/test_source_mtimes.py new file mode 100644 index 000000000..029b56fb5 --- /dev/null +++ b/tests/scripts/test_source_mtimes.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""The sources' times for a build restored from main's run +(.github/tools/source_mtimes.py, design 2026-11-14 §6.2 W1); offline.""" + +from __future__ import annotations + +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[2] / ".github/tools/source_mtimes.py" +OLD = 946684800 + + +def git(cwd: Path, *args: str) -> None: + subprocess.run(["git", "-c", "user.name=t", "-c", "user.email=t@t", *args], + cwd=cwd, check=True, capture_output=True) + + +def tool(cwd: Path, *args: str) -> str: + return subprocess.run([sys.executable, str(SCRIPT), *args], cwd=cwd, check=True, + capture_output=True, text=True).stdout + + +class SourceMtimes(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + git(self.root, "init", "-q") + for name in ("same.cpp", "changed.cpp", "edited.cpp"): + (self.root / name).write_text(name) + git(self.root, "add", "-A") + git(self.root, "commit", "-qm", "a") + tool(self.root, "record", "target") + + def tearDown(self) -> None: + self.tmp.cleanup() + + def test_an_unchanged_file_is_older_than_the_build(self) -> None: + # The next commit changes one file; the working tree edits another and adds one. + (self.root / "changed.cpp").write_text("changed again") + git(self.root, "commit", "-qam", "b") + (self.root / "edited.cpp").write_text("edited") + (self.root / "new.cpp").write_text("new") + git(self.root, "add", "new.cpp") + out = tool(self.root, "restore", "target") + self.assertIn("1 file(s) as recorded, 3 changed or new", out) + self.assertEqual(os.stat(self.root / "same.cpp").st_mtime, OLD) + for name in ("changed.cpp", "edited.cpp", "new.cpp"): + self.assertGreater(os.stat(self.root / name).st_mtime, OLD, name) + + def test_without_a_record_nothing_changes(self) -> None: + before = os.stat(self.root / "same.cpp").st_mtime + out = tool(self.root, "restore", "elsewhere") + self.assertIn("rebuilt whole", out) + self.assertEqual(os.stat(self.root / "same.cpp").st_mtime, before) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_test_schedule.cpp b/tests/unit/test_test_schedule.cpp index e9173819b..012c0213a 100644 --- a/tests/unit/test_test_schedule.cpp +++ b/tests/unit/test_test_schedule.cpp @@ -113,6 +113,48 @@ UTest a_long_part_divides_into_equal_groups{"TestSchedule/ALongPartDividesIntoEq expect(eq(ts::divide(h, "p", ids, 9).size(), std::size_t{3})); }}; +UTest a_case_is_divided_by_the_cases_it_declared{"TestSchedule/ACaseIsDividedByTheCasesItDeclared", [] { + // The store names a part's cases under its own record; they are cases of + // the program the part is of. Two programs may each have a case `trees`. + Store s; + s.run("a", "2026-11-01T00:00:00Z", + R"({"name":"e2e/openkal/trees","part_of":"e2e/openkal","status":"pass","ms":1400})" "\n" + R"({"name":"e2e/other","status":"pass","ms":5})" "\n", + R"({"program":"e2e/openkal/trees","id":"trees","status":"pass","ms":1373})" "\n" + R"({"program":"e2e/openkal/trees","id":"trees/955","status":"pass","ms":564})" "\n" + R"({"program":"e2e/openkal/trees","id":"trees/992","status":"pass","ms":202})" "\n" + R"({"program":"e2e/openkal/trees","id":"trees/992/inner","status":"pass","ms":200})" "\n" + R"({"program":"e2e/openkal/trees","id":"trees/1078","status":"pass","ms":177})" "\n" + R"({"program":"e2e/other","id":"trees","status":"pass","ms":5})" "\n"); + const auto h = ts::read_history(s.dir); + // The innermost cases it declared, in id order. + expect(ts::measured_within(h, "e2e/openkal", "trees") == + std::vector{"trees/1078", "trees/955", "trees/992/inner"}); + expect(ts::measured_within(h, "e2e/other", "trees").empty()); + const std::vector one{"trees"}; + expect(eq(ts::estimate(h, "e2e/openkal", one), 1373.0)); + expect(eq(ts::estimate(h, "e2e/other", one), 5.0)); + // The rest of a divided case: the case without what the other parts run. + const std::vector rest{"trees", "!trees/955"}; + expect(eq(ts::estimate(h, "e2e/openkal", rest), 1373.0 - 564.0)); + const std::vector given{"?trees/955", "?trees/since-removed"}; + expect(eq(ts::estimate(h, "e2e/openkal", given), 564.0 + 564.0)); + + // A run that divided it measured the case's rest alone: the case takes at + // least what the cases it declared took. + Store d; + d.run("a", "2026-11-01T00:00:00Z", + R"j({"name":"p (1/3)","part_of":"p","status":"pass","ms":500})j" "\n" + R"j({"name":"p (2/3)","part_of":"p","status":"pass","ms":400})j" "\n" + R"j({"name":"p (3/3)","part_of":"p","status":"pass","ms":100})j" "\n", + R"j({"program":"p (3/3)","id":"t","status":"pass","ms":100})j" "\n" + R"j({"program":"p (1/3)","id":"t/x","status":"pass","ms":500})j" "\n" + R"j({"program":"p (2/3)","id":"t/y","status":"pass","ms":400})j" "\n"); + const auto hd = ts::read_history(d.dir); + const std::vector t{"t"}; + expect(eq(ts::estimate(hd, "p", t), 900.0)); +}}; + UTest a_shard_is_i_of_n{"TestSchedule/AShardIsIOfN", [] { const auto shard = ts::parse_shard("2/4"); expect(fatal(shard.has_value())); From 2551e437f39e61f59570e298be1d4a294acd9d22 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 04:05:16 +0900 Subject: [PATCH 2/9] mtest 0.9.0: the tests of this package and the scaffold of mcpp new, so a long case is divided by the cases it declares --- CHANGELOG.md | 3 +++ bench/mcpp.toml | 2 +- docs/05-dependencies.md | 4 ++-- docs/zh/05-dependencies.md | 4 ++-- mcpp.toml | 2 +- modules/buildmcpp/mcpp.toml | 2 +- modules/graph/mcpp.toml | 2 +- modules/manifest/mcpp.toml | 2 +- modules/toolchain-model/mcpp.toml | 2 +- modules/versioning/mcpp.toml | 2 +- src/build/test_protocol.cppm | 2 +- tests/canary/cxx/mcpp.toml | 2 +- tests/support/mcpp.toml | 2 +- 13 files changed, 17 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08c853bcd..9fa0ffb23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ ### Changed +- `mcpp new` scaffolds tests with mtest 0.9.0, the version this package's own + tests use, so a new package's long cases are divisible by the cases they + declare. - The run store's case durations are kept by program and case id; two programs with a case of one name (`layers`, `sets`) read each other's. - CI: the archive stage reuses main's test build: an artifact of its build diff --git a/bench/mcpp.toml b/bench/mcpp.toml index 7a6882fef..0b9e79da3 100644 --- a/bench/mcpp.toml +++ b/bench/mcpp.toml @@ -21,4 +21,4 @@ windows = "llvm@23.1.3" # small fixture. `mcpp build` ignores [dev-dependencies], so the harness itself # still takes nothing beyond `import std;`. [dev-dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" diff --git a/docs/05-dependencies.md b/docs/05-dependencies.md index 932d7ed64..56074e73d 100644 --- a/docs/05-dependencies.md +++ b/docs/05-dependencies.md @@ -67,8 +67,8 @@ not deprecated: ```toml [dev-dependencies] -mcpplibs.mtest = "0.8.5" -"mcpplibs.mtest.gtest" = "0.8.5" +mcpplibs.mtest = "0.9.0" +"mcpplibs.mtest.gtest" = "0.9.0" ``` ```toml diff --git a/docs/zh/05-dependencies.md b/docs/zh/05-dependencies.md index 666ee78b4..8499eee91 100644 --- a/docs/zh/05-dependencies.md +++ b/docs/zh/05-dependencies.md @@ -62,8 +62,8 @@ mcpp 把它读作这些点所拼写的选择器,警告该拼法已弃用(自 ```toml [dev-dependencies] -mcpplibs.mtest = "0.8.5" -"mcpplibs.mtest.gtest" = "0.8.5" +mcpplibs.mtest = "0.9.0" +"mcpplibs.mtest.gtest" = "0.9.0" ``` ```toml diff --git a/mcpp.toml b/mcpp.toml index 492af4814..53584709b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -178,7 +178,7 @@ members = [ # `mcpp build` ignores [dev-dependencies]; only `mcpp test` resolves them. [dev-dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" # The support of the end-to-end cases (tests/support, design 2026-11-12 §5.9). mcpp.test-support = { path = "tests/support" } diff --git a/modules/buildmcpp/mcpp.toml b/modules/buildmcpp/mcpp.toml index 1b3a11769..32fba3a26 100644 --- a/modules/buildmcpp/mcpp.toml +++ b/modules/buildmcpp/mcpp.toml @@ -41,4 +41,4 @@ mcpp.source-kind = { path = "../source-kind" } mcpp.toolchain-model = { path = "../toolchain-model" } [dev-dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" diff --git a/modules/graph/mcpp.toml b/modules/graph/mcpp.toml index d2dcfd013..0e5838f2a 100644 --- a/modules/graph/mcpp.toml +++ b/modules/graph/mcpp.toml @@ -17,4 +17,4 @@ description = "Algorithms over index-addressed directed graphs: topological orde license = "Apache-2.0" [dev-dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" diff --git a/modules/manifest/mcpp.toml b/modules/manifest/mcpp.toml index 055901c37..c3be033e2 100644 --- a/modules/manifest/mcpp.toml +++ b/modules/manifest/mcpp.toml @@ -30,4 +30,4 @@ mcpplibs.os = "0.4.1" # state its contract in isolation; the root `tests/unit/` states the facts two # layers must agree on. [dev-dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" diff --git a/modules/toolchain-model/mcpp.toml b/modules/toolchain-model/mcpp.toml index 757932182..21924bc76 100644 --- a/modules/toolchain-model/mcpp.toml +++ b/modules/toolchain-model/mcpp.toml @@ -29,7 +29,7 @@ mcpplibs.os = "0.4.1" # state its contract in isolation; the root `tests/unit/` states the facts two # layers must agree on. [dev-dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" # build.mcpp compiles data/builtin.toml (the built-in target rows and providers) # into a module of static tables, with mcpp's own TOML parser. diff --git a/modules/versioning/mcpp.toml b/modules/versioning/mcpp.toml index d240f4ffe..a579ee2ab 100644 --- a/modules/versioning/mcpp.toml +++ b/modules/versioning/mcpp.toml @@ -25,4 +25,4 @@ license = "Apache-2.0" # state its contract in isolation; the root `tests/unit/` states the same facts # as the engine consumes them, and the pair is the cross-check. [dev-dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" diff --git a/src/build/test_protocol.cppm b/src/build/test_protocol.cppm index 58ccb4ada..b65ff8173 100644 --- a/src/build/test_protocol.cppm +++ b/src/build/test_protocol.cppm @@ -18,7 +18,7 @@ inline constexpr int kVersion = 1; // The framework mcpp scaffolds tests with and suggests where a package has // none (docs/01, docs/08): mtest, by this release, from its repository. -inline constexpr std::string_view kMtestRelease = "0.8.5"; +inline constexpr std::string_view kMtestRelease = "0.9.0"; inline constexpr std::string_view kMtestRepo = "https://github.com/mcpp-community/mtest"; // The protocol's variables, removed from the environment a program inherits diff --git a/tests/canary/cxx/mcpp.toml b/tests/canary/cxx/mcpp.toml index 713dc13d3..71d0fbcc9 100644 --- a/tests/canary/cxx/mcpp.toml +++ b/tests/canary/cxx/mcpp.toml @@ -9,4 +9,4 @@ kind = "bin" main = "src/main.cpp" [dev-dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" diff --git a/tests/support/mcpp.toml b/tests/support/mcpp.toml index 34027e3c6..6a7a2c8bc 100644 --- a/tests/support/mcpp.toml +++ b/tests/support/mcpp.toml @@ -13,5 +13,5 @@ license = "Apache-2.0" kind = "lib" [dependencies] -mcpplibs.mtest = "0.8.5" +mcpplibs.mtest = "0.9.0" nlohmann.json = "3.12.0" From 18105bd0acdabafa434533ddc28760799769f3cd Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 04:36:05 +0900 Subject: [PATCH 3/9] test: the concurrent-children rounds compare warm starts; a program's first start on a fresh Windows runner outlasted the slow child --- tests/unit/test_run_concurrent_children.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/test_run_concurrent_children.cpp b/tests/unit/test_run_concurrent_children.cpp index 02c4015b8..672ba4093 100644 --- a/tests/unit/test_run_concurrent_children.cpp +++ b/tests/unit/test_run_concurrent_children.cpp @@ -59,7 +59,16 @@ Outcome run_when_open(const std::atomic& gate, const std::vector gate{false}; Outcome quick, slow; From f6da14c17c05151f17e6baead796fc6498e76109 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 05:14:47 +0900 Subject: [PATCH 4/9] An install for a project takes the home's state turn: it saves the home's environment too, and collided on Windows with another project's install --- CHANGELOG.md | 7 +++++++ src/provider/xlings/xlings.cpp | 7 ++++++- src/provider/xlings/xlings.cppm | 5 +++-- tests/unit/test_xlings.cpp | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fa0ffb23..7d01543fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,13 @@ `coverage` job no longer waits for a runner and holds the concurrency group (it held it 9.1 minutes in run 43599371222). +### Fixed + +- An install for a project takes the home's state turn: it saves the home's + environment (`subos/default/.xlings.json`) as well as the project's, and with + the turn in the project's directory it collided on Windows with an install of + another project ("Failed to write file"). One turn per home orders both. + ## [2026.11.14.1] - 2026-11-14 ### Added diff --git a/src/provider/xlings/xlings.cpp b/src/provider/xlings/xlings.cpp index 96d1e9ab1..10db4c30f 100644 --- a/src/provider/xlings/xlings.cpp +++ b/src/provider/xlings/xlings.cpp @@ -948,7 +948,12 @@ Invocation invocation(const Env& env, std::vector args) { } std::filesystem::path state_dir(const Env& env) { - const auto dir = env.projectDir.empty() ? env.home / "subos" : env.projectDir; + // The home's, for a project's install as well: an install at project scope + // also saves the home's environment (subos/default/.xlings.json), so a turn + // in the project's directory let it collide with an install of another + // project, or of none, on Windows (e2e images/1033, run 43632995462). One + // turn per home orders every writer of either state. + const auto dir = env.home / "subos"; std::error_code ec; auto real = std::filesystem::weakly_canonical(dir, ec); return ec ? dir : real; diff --git a/src/provider/xlings/xlings.cppm b/src/provider/xlings/xlings.cppm index c679803fe..7bb90829c 100644 --- a/src/provider/xlings/xlings.cppm +++ b/src/provider/xlings/xlings.cppm @@ -419,8 +419,9 @@ std::vector invocation_env(const Env& env); // The runtime the home's `abi` link selects (`xlings config --runtime`). void select_runtime(const Env& env, std::string_view runtimeId); -// Where the state an install writes lives, every link resolved: the project's -// home, or the home's `subos` (Provider::state_dir). +// Where the state an install writes lives, every link resolved: the home's +// `subos`, whose environment an install for a project saves as well +// (Provider::state_dir). std::filesystem::path state_dir(const Env& env); // Build full xlings interface command. diff --git a/tests/unit/test_xlings.cpp b/tests/unit/test_xlings.cpp index 1f7a7cacc..62f9e0a6f 100644 --- a/tests/unit/test_xlings.cpp +++ b/tests/unit/test_xlings.cpp @@ -783,3 +783,19 @@ UTest the_shells_active_subos_is_never_inherited{"XlingsInvocationEnv/TheShellsA expect(!project.present); }}; + +// An install for a project saves the home's environment too, so its writers +// and the home's take one turn: the state directory is the home's whichever +// scope the call is for. +UTest a_projects_install_takes_the_homes_turn{"XlingsState/AProjectsInstallTakesTheHomesTurn", [] { + auto home = make_tempdir("mcpp-state-home"); + auto project = make_tempdir("mcpp-state-project"); + std::filesystem::create_directories(home / "subos"); + const mcpp::provider::xlings::Env global{.home = home}; + const mcpp::provider::xlings::Env scoped{.home = home, .projectDir = project / ".mcpp"}; + expect(eq(mcpp::provider::xlings::state_dir(scoped), mcpp::provider::xlings::state_dir(global))); + expect(eq(mcpp::provider::xlings::state_dir(global), + std::filesystem::weakly_canonical(home / "subos"))); + std::filesystem::remove_all(home); + std::filesystem::remove_all(project); +}}; From de8ba2391111f8f6224e9b73f31d8d6ace1751db Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 06:15:40 +0900 Subject: [PATCH 5/9] A host tool built by two builds of one home is published by both; the legacy code-page check runs the tests as the job built them (profile ci) --- .github/tools/check_windows_home_legacy.py | 11 +++++++- .github/workflows/ci-windows.yml | 3 +- CHANGELOG.md | 6 ++++ src/build/prepare/features.cpp | 31 +++++++++----------- src/build/stage.cppm | 6 +++- tests/unit/test_build_stage.cpp | 33 ++++++++++++++++++++++ 6 files changed, 69 insertions(+), 21 deletions(-) diff --git a/.github/tools/check_windows_home_legacy.py b/.github/tools/check_windows_home_legacy.py index cdcbe05f9..942d6be2e 100644 --- a/.github/tools/check_windows_home_legacy.py +++ b/.github/tools/check_windows_home_legacy.py @@ -14,9 +14,18 @@ assert original.count(marker) == 1, 'identify the test-only code-page setting explicitly' +# The profile the job built the tests with (MCPP_TEST_PROFILE; empty: the +# manifest's default). Another profile builds every test again, which outlasted +# the timeout on the Windows row (run 43636471152). +profile = os.environ.get('MCPP_TEST_PROFILE', '') + + def run_case(passed_case): # The case by its identifier (mtest's): it runs in the process of its file. - result = subprocess.run([os.environ['MCPP'], '--offline', 'test', '--case', passed_case], cwd=root, + command = [os.environ['MCPP'], '--offline', 'test', '--case', passed_case] + if profile: + command += ['--profile', profile] + result = subprocess.run(command, cwd=root, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=600) sys.stdout.write(result.stdout) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 132e8fbc8..ee79f3483 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -85,7 +85,8 @@ jobs: shell: bash run: | export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") - MCPP="$MCPP_SELF" python .github/tools/check_windows_home_legacy.py + # The tests as the step above built them (profile ci). + MCPP="$MCPP_SELF" MCPP_TEST_PROFILE=ci python .github/tools/check_windows_home_legacy.py # WS8: this row has Visual Studio, so its answer is the "Windows with # usable MSVC" row of docs/01 and docs/20. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d01543fd..6cd4671be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,12 @@ environment (`subos/default/.xlings.json`) as well as the project's, and with the turn in the project's directory it collided on Windows with an install of another project ("Failed to write file"). One turn per home orders both. +- A host tool built by two builds of one home at once is published by both: + each writes through a temporary of its own (`mcpp.build.stage`), and a store + entry already equal to the tool is published. With one temporary name for + every writer, one removed the other's before its rename ("publishing host + tool ... failed: No such file or directory", the target matrix's cells side + by side). ## [2026.11.14.1] - 2026-11-14 diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index a1d38a7cc..6649f451b 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -46,6 +46,7 @@ import mcpp.build.build_program; import mcpp.build.tool_store; // #355 host tools: store layout + key + overrides import mcpp.build.backend; // BuildOptions for the tool sub-build import mcpp.build.ninja; // make_ninja_backend — driving that sub-build +import mcpp.build.stage; // publishing a built tool into the store import mcpp.provider; import mcpp.provider.select; import mcpp.provider.runtime_selection; @@ -1731,9 +1732,14 @@ step6_build_tool(PrepareState& state, HostToolCtx& ctx) { static std::expected step6_publish_tool(PrepareState& state, HostToolCtx& ctx) { - // Publish into the store: build out of place, then move — - // the same discipline mcpp.build.stage follows, so a - // concurrent consumer never observes a half-written entry. + // Publish into the store through mcpp.build.stage: a temporary of this + // writer's own beside the entry, then a rename, so a concurrent consumer + // never observes a half-written entry. Two processes that built the same + // key publish the same bytes; a destination already equal to them is + // published. With one temporary name for every writer, one writer removed + // the other's temporary before its rename ("publishing host tool + // 'openkal-tools:okld' failed: No such file or directory", the target + // matrix's cells side by side, run 43636471152). std::error_code cpEc; auto produced = ctx.subOutputDir / ctx.goal; if (!std::filesystem::exists(produced, cpEc)) { @@ -1742,27 +1748,16 @@ step6_publish_tool(PrepareState& state, HostToolCtx& ctx) { ctx.depName, ctx.toolName, produced.string())); } std::filesystem::create_directories(ctx.binOut.parent_path(), cpEc); - auto tmp = ctx.binOut; - tmp += ".tmp"; - std::filesystem::remove(tmp, cpEc); - std::filesystem::copy_file(produced, tmp, - std::filesystem::copy_options::overwrite_existing, cpEc); - if (cpEc) { + if (auto staged = mcpp::build::stage::stage_file(produced, ctx.binOut); !staged) { return std::unexpected(std::format( - "staging host tool '{}:{}' failed: {}", - ctx.depName, ctx.toolName, cpEc.message())); + "publishing host tool '{}:{}' failed: {}", + ctx.depName, ctx.toolName, staged.error().message)); } - std::filesystem::permissions(tmp, + std::filesystem::permissions(ctx.binOut, std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, cpEc); - std::filesystem::rename(tmp, ctx.binOut, cpEc); - if (cpEc) { - return std::unexpected(std::format( - "publishing host tool '{}:{}' failed: {}", - ctx.depName, ctx.toolName, cpEc.message())); - } // A path package's tree, so that its superseded entries are collected. std::filesystem::path toolSource; if (ctx.depIdx >= 1 && ctx.depIdx - 1 < state.dep_cache_identities.size() diff --git a/src/build/stage.cppm b/src/build/stage.cppm index 9501940a7..77416d5c2 100644 --- a/src/build/stage.cppm +++ b/src/build/stage.cppm @@ -154,8 +154,12 @@ namespace { std::filesystem::path temp_sibling(const std::filesystem::path& dst) { // Same directory as the destination so the rename stays on one filesystem. + // The writer's own name: processes that publish one destination at once + // (a host tool built by two builds of one home) must not share a temporary, + // and a clock alone is a microsecond on macOS. auto stamp = std::chrono::system_clock::now().time_since_epoch().count(); - auto name = dst.filename().string() + std::format(".tmp.{}", stamp); + auto name = dst.filename().string() + + std::format(".tmp.{}.{:x}", stamp, std::random_device{}()); return dst.parent_path() / name; } diff --git a/tests/unit/test_build_stage.cpp b/tests/unit/test_build_stage.cpp index f8ba7d036..827bb575b 100644 --- a/tests/unit/test_build_stage.cpp +++ b/tests/unit/test_build_stage.cpp @@ -370,3 +370,36 @@ UTest empty_source_list_is_an_error{"BuildStageFiles/EmptySourceListIsAnError", auto r = stage_files({}, tmp.path / "dst.bin", no_retry()); expect(fatal(!r.has_value())); }}; + +// Writers that publish one destination at once each succeed, and the +// destination is the source: each writes through a temporary of its own, so +// none removes another's before that one's rename (two builds of one home +// publishing a host tool they both built). +UTest writers_of_one_destination_do_not_collide{"BuildStage/WritersOfOneDestinationDoNotCollide", [] { + Tmp t; + const auto body = std::string(1 << 20, 'x'); + constexpr int kWriters = 8; + std::vector srcs; + for (int i = 0; i < kWriters; ++i) { + srcs.push_back(t.path / std::format("src{}", i) / "tool"); + write_file(srcs.back(), body); + } + const auto dst = t.path / "store" / "tool"; + std::filesystem::create_directories(dst.parent_path()); + std::atomic gate{false}; + std::array ok{}; + std::vector writers; + for (int i = 0; i < kWriters; ++i) + writers.emplace_back([&, i] { + while (!gate.load(std::memory_order_acquire)) std::this_thread::yield(); + ok[i] = stage_file(srcs[i], dst).has_value(); + }); + gate.store(true, std::memory_order_release); + for (auto& w : writers) w.join(); + for (int i = 0; i < kWriters; ++i) expect(ok[i]) << "writer " << i; + expect(read_file(dst) == body); + std::size_t left = 0; + for (const auto& e : std::filesystem::directory_iterator(dst.parent_path())) + left += e.path().filename() != "tool"; + expect(eq(left, std::size_t{0})) << "a temporary was left beside the destination"; +}}; From cc6f0a1513d2901b3eb9b08d1c82df3bd53a217f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 07:26:23 +0900 Subject: [PATCH 6/9] A case is divided only while its division measured shorter: parts record the case they divide, the store keeps the case whole and its longest part On CI the two e2e suites' cases share what the first fills; each part paid it again (sets 1375 s whole, 909/1109/1306 s as three parts). Design 6.3 records the measurements. --- ...6-11-14-ci-and-release-time-cost-design.md | 33 ++++++++++++ CHANGELOG.md | 9 ++-- docs/08-testing.md | 6 ++- docs/specs/test.md | 5 +- docs/zh/08-testing.md | 4 +- src/build/execute.cppm | 17 ++++-- src/build/test_schedule.cppm | 54 ++++++++++++++++++- tests/unit/test_test_schedule.cpp | 51 ++++++++++++++++-- 8 files changed, 164 insertions(+), 15 deletions(-) diff --git a/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md b/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md index 0387f95af..afe0f1390 100644 --- a/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md +++ b/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md @@ -269,3 +269,36 @@ Windows 把每个 `\n` 写成 `\r\n`;`write_config_value` 只认 `\n` 结尾 本地核验:W1 恢复后无改动的重建 0.4 s,改动一个接口模块的重建 88 s(冷构建 248 s);W3 演示工程四个子用例 8.2 s → 3.0 s,改名的子用例只在余下组中运行,旧名在 `?` 组中不报告 `missing`;单元 `TestSchedule/ACaseIsDividedByTheCasesItDeclared`、`TestTargets` 与 mtest `suites` 通过。 + +### 6.3 CI 上的测量(PR 运行 43626158365 与 43641874830) + +两次运行均在 main 尚无 `mcpp-unit-build-` artifact 时进行,W1 的复用在合入后的第一次 main 运行产生 artifact +之后才生效。 + +| 项 | 此前(43602794812) | 本 PR | 说明 | +|---|---|---|---| +| archive 构建(W2) | 39.4 分钟 | 14.3 分钟(855 s、862 s) | 冷构建,`--profile ci` | +| `target-matrix / scan (macos-arm64)`(M1) | 1762 s | 912 s | 59 格并行 | +| 整次运行墙钟 | 97.6 分钟 | 62.6 分钟 | 排队 89 分钟(此前 265 分钟),受组织内并发影响 | +| 分片(W3) | 28.0 / 27.9 / 12.4 / 9.8 分钟 | 24.4 / 21.9 / 20.0 / 22.0 分钟 | 见下 | + +**W3 的测量否定了按声明的用例无条件划分。** `e2e/openkal/sets` 整体 1375 s,划分为三组后各为 909、1109、1306 s; +`trees` 整体 1373 s,两组为 1070、1247 s。同一用例声明的用例共享第一个用例填充的状态(下载的工具链与包、构建 +缓存),每一组在各自的 runner 上重新付出,关键路径没有缩短,runner 时间约为两倍。而且划分后测得的内层用例 +用时偏大,用例的估计随之增大,下一次会划得更细。 + +**决定。** 划分以测量判定:各组在程序进程一行写 `divides`;存储同时记取用例最近一次整体运行的用时与最近一次 +划分中最长一组的用时;从未被划分的用例划分一次以取得测量,此后仅当最长一组不超过整体的五分之四时继续划分; +用例的估计取整体运行的用时(`test_schedule::division_pays`,单元 `TestSchedule/ADivisionIsKeptWhileItPays`)。 +合入后 main 的存储中这两个套件各有一次划分的记录,此后整体运行,直到记录移出 30 次运行的窗口后再测一次。 +缩短这两个套件的途径是其共享状态的复用(预填充的 home),不是划分。 + +**并行暴露的两个缺陷**(均已修复并有单元测试): + +- 项目范围的安装同时写 home 的环境(`subos/default/.xlings.json`),状态锁却取在项目目录,Windows 上与另一项目的 + 安装冲突(`images/1033`,运行 43632995462)。锁改为每个 home 一个(`XlingsState/AProjectsInstallTakesTheHomesTurn`)。 +- 两次构建同时发布同一宿主工具时共用一个临时文件名,一方在另一方改名前删除了它(macOS 扫描,运行 43636471152)。 + 发布改用 `mcpp.build.stage::stage_file`,临时文件名带随机部分(`BuildStage/WritersOfOneDestinationDoNotCollide`)。 + +Windows 的旧代码页检查此前以缺省 profile 运行两个用例,在 `--profile ci` 构建之后会重建全部测试并超时;现以同一 +profile 运行。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd4671be..0cfc8f6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,12 @@ the program's selection excludes and selects where declared (mtest 0.9.0): `/ [] (i/k)`. Each part but the last selects the cases it was given where they are still declared (`?`); the last selects the case - without them (`!`), so a case no run measured runs once. Two e2e suites - of 1373 s and 1375 s ran their cases one after another while the longest of - those cases takes 564 s. + without them (`!`), so a case no run measured runs once. A division is + kept while it pays: the cases of one case may share what the first fills, and + each part pays it again (`e2e/openkal/sets`, 1375 s whole, took 909, 1109 and + 1306 s as three parts). Each part records the case it divides (`divides`); a + case is divided once to be measured so, then only while its longest part took + at most four fifths of the case whole, and is estimated by its whole run. - `[profile.ci]` in this package's manifest: the tests as CI builds them, -O0 without debug information (the library and its unit tests: 8988 s of compiler time at the default release profile, 4183 s with it). diff --git a/docs/08-testing.md b/docs/08-testing.md index 0b516a34c..7ba92d85a 100644 --- a/docs/08-testing.md +++ b/docs/08-testing.md @@ -96,7 +96,11 @@ selects where declared (SPEC-017 P4.3, mtest 0.9.0) is divided by the cases it declared as the store measured them, which a list does not name (2026.11.15.1+): `/ [] (i/k)`. Each part but the last selects the cases it was given where they are still declared; the last selects -the case without them, so a case no run measured runs once, in it. +the case without them, so a case no run measured runs once, in it. The cases +of one case may share what the first of them fills (a cache, a toolchain), and +every part pays it again: a case is divided once to be measured so, and then +only while its longest part took at most four fifths of the case run whole +(each part records the case it divides, `divides`). `--split ` states the value for one invocation. A pattern names a program or a part of one as the run names it: `mcpp test unit/test_home` (or `home`) runs that file's part. diff --git a/docs/specs/test.md b/docs/specs/test.md index b82deb094..7412a801e 100644 --- a/docs/specs/test.md +++ b/docs/specs/test.md @@ -310,7 +310,8 @@ 1. **估计。** 运行之前,运行器从 B4 的存储读取最近的运行(至多 30 次,按 `started` 由新到旧):每个程序记录名与每个用例标识的最近用时。一个测试的估计是其所选用例用时之和,未选用例时为程序整体的用时。没有测量的程序记录取其最近目录(按 `/` 逐级向上)中已测程序记录的平均值,各级都没有时取已测程序记录中的最大值;没有测量的用例取同一列表中已测用例的平均值,都没有时取其程序估计按用例数的等分。状态为 `built` 或 `not-run` 的记录与跳过的用例不计入。 2. **顺序。** 测试**应当**按估计从长到短开始;占用 `machine` 的测试仍排在最后(P7.1)。 3. **划分。** `split = "auto"` 时,份额为全部测试的估计之和除以工作线程数与分片数;估计长于份额、且含两个以上列出用例的部分**必须**划分为 ⌈估计/份额⌉ 组(不多于用例数),每组按最长处理时间优先分配用例,组内保持列出顺序,名为 `<部分> (i/k)`。 - **按声明的用例划分(mcpp >= 2026.11.15.1)。** 程序的选择理解 `except` 与 `optional`(P4.3)时,估计长于份额、且存储中测得两个以上其声明的用例(P3.4:列表不给出这些用例)的列出用例**必须**按这些用例中最内层者划分为 ⌈估计/份额⌉ 组(不多于其数),名为 `<部分> [<用例>] (i/k)`:前 k−1 组以 `?` 行选中所分得的用例,第 k 组选中该用例并以 `!` 行排除前 k−1 组的用例。没有测量的声明用例因而只在第 k 组运行一次,已删除或改名的用例不使运行失败。一个用例的估计不小于其最内层声明用例的测得用时之和,因为被划分的运行只测得其余下部分。存储中用例的用时按其程序(部分所属的程序)与标识记取。 + **按声明的用例划分(mcpp >= 2026.11.15.1)。** 程序的选择理解 `except` 与 `optional`(P4.3)时,估计长于份额、且存储中测得两个以上其声明的用例(P3.4:列表不给出这些用例)的列出用例**必须**按这些用例中最内层者划分为 ⌈估计/份额⌉ 组(不多于其数),名为 `<部分> [<用例>] (i/k)`:前 k−1 组以 `?` 行选中所分得的用例,第 k 组选中该用例并以 `!` 行排除前 k−1 组的用例。没有测量的声明用例因而只在第 k 组运行一次,已删除或改名的用例不使运行失败。一个用例的估计取其最近一次整体运行的测得用时;没有整体运行时不小于其最内层声明用例的测得用时之和,因为被划分的运行只测得其余下部分。存储中用例的用时按其程序(部分所属的程序)与标识记取。 + **划分须经测量证明有效。** 同一用例声明的用例可能共享由第一个用例填充的状态(缓存、工具链),每一组都要重新付出这部分代价。被划分用例的各组在程序进程一行写 `divides`(所划分用例的标识)。存储中从未被划分的用例**必须**划分一次以取得测量;此后仅当其最近一次划分中最长一组的用时不超过其最近一次整体运行用时的五分之四时**必须**再次划分,否则整体运行;窗口内没有整体运行的记录时整体运行,以重新测得。 4. **分片。** `--shard I/N` 把每个测试按估计从长到短(相同时按名字)分配给已分配估计之和最小的分片(相同时取编号最小者),只运行第 I 个分片的测试。分配**必须**只取决于存储与本次运行的测试列表。分片**禁止**把本组(`GITHUB_RUN_ID`,无则为空)的分片运行读作历史,使同一存储上先后启动的各分片读到相同的历史。 5. **失败重跑。** `--failed` 读取存储中最近的一次运行:状态为 `fail` 或 `build-fail` 的程序记录与状态为 `fail` 或 `xpass` 的用例。所选程序为这些记录的 `part_of`(无则为其名字);每个失败的程序都有失败用例时,只运行这些用例(与 `--case` 相同);否则整体运行这些程序。最近一次运行没有失败时,运行器不运行任何测试,退出码为 0。 @@ -353,4 +354,4 @@ mcpp 以 feature(docs/50 §4)宣告它实现的条款;客户端按需门 | 2.0 | 2026-11-10 | 改为 mcpp 测试规范:合并 SPEC-017 v1.0、mtest 协议版本 1 的规范正文与 SPEC-004 的 `[test]` 键;分为基础能力 B1–B6 与可选能力 O1–O9;新增 B5 启动描述、B6 的 `test-capabilities` 与 `test-session`、O3 检查点细节、O8 输出归属、O9 测试会话(mcpp#914、mcpp#915) | | 2.1 | 2026-11-14 | P3.3 的位置与能力(`only`、`except`、`capabilities`、列为跳过时的 `reason`;mtest 0.8.0),及 mcpp 的实现 | | 2.2 | 2026-11-14 | O10 按测量调度(顺序、`split = "auto"`、`--shard`、`--failed`);B4 的 `shard` 与 `part_of` | -| 2.3 | 2026-11-15 | P4.3 排除与按声明选择(mtest 0.9.0);O10 按声明的用例划分,用例的用时按程序与标识记取 | +| 2.3 | 2026-11-15 | P4.3 排除与按声明选择(mtest 0.9.0);O10 按声明的用例划分并以测量判定是否保留(`divides`),用例的用时按程序与标识记取 | diff --git a/docs/zh/08-testing.md b/docs/zh/08-testing.md index b5ddcbb14..6c5167b6e 100644 --- a/docs/zh/08-testing.md +++ b/docs/zh/08-testing.md @@ -73,7 +73,9 @@ note,说明有多少文件仍各自是一个程序;它不形成任何组时 若干组,名为 `<程序>/<文件> (i/k)`。程序的选择支持排除与按声明选择(SPEC-017 P4.3,mtest 0.9.0)时, 长于份额的列出用例按存储测得的其声明的用例划分(列表不给出这些用例;2026.11.15.1+),名为 `<程序>/<文件> [<用例>] (i/k)`:前几组只在用例仍被声明时选中所分得的用例,最后一组选中该用例并排除前几组 -的用例,没有测量的用例因而只在其中运行一次。`--split ` 为一次调用给出该值。模式按运行所用的名字指名程序或其一部分:`mcpp test unit/test_home`(或 `home`) +的用例,没有测量的用例因而只在其中运行一次。同一用例声明的用例可能共享第一个用例填充的状态(缓存、工具链), +每一组都要重新付出:一个用例先被划分一次以取得测量,此后仅当其最长一组的用时不超过整体运行的五分之四时 +继续划分(每一组记录其划分的用例,`divides`)。`--split ` 为一次调用给出该值。模式按运行所用的名字指名程序或其一部分:`mcpp test unit/test_home`(或 `home`) 运行该文件的那一部分。 占用整台机器的用例(P7.1,资源 `machine`)运行时旁边没有其他测试(2026.11.13.1+)。mcpp diff --git a/src/build/execute.cppm b/src/build/execute.cppm index d3ca83cbe..84593a04d 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -4287,8 +4287,10 @@ struct TestResult { int protocol = 0; mcpp::build::test_protocol::Report report; // The program this record is a part of (`[test] split`), empty for a - // program run whole (feature `test-parts`). + // program run whole (feature `test-parts`), and the case the part divides + // (P4.3), empty for a part of whole cases. std::string partOf; + std::string divides; // The parts a listing (`--cases`) divides the program into: name and file. std::vector> parts; // Where it ran in the run's schedule, for the mtest store (P12): its start @@ -4353,6 +4355,9 @@ void write_mtest_store(const std::filesystem::path& dir, const std::vector 0) p["build_ms"] = t.buildMs; // The program a part is of: what `--failed` builds again. if (!t.partOf.empty()) p["part_of"] = t.partOf; + // The case it divides, so that a later run compares the division + // with the case run whole (`test_schedule::division_pays`). + if (!t.divides.empty()) p["divides"] = t.divides; if (t.report.usage.set) { auto u = mcpp::build::test_protocol::usage_json(t.report.usage); u["source"] = "program"; @@ -4438,6 +4443,7 @@ static void emit_test_json(const std::string& memberName, const TestResult& r) { {"protocol", r.protocol ? nlohmann::ordered_json(r.protocol) : nlohmann::ordered_json(nullptr)}, }; if (!r.partOf.empty()) record["part_of"] = r.partOf; + if (!r.divides.empty()) record["divides"] = r.divides; if (!r.parts.empty()) { auto parts = nlohmann::ordered_json::array(); for (auto const& [name, file] : r.parts) parts.push_back({{"name", name}, {"file", file}}); @@ -4640,6 +4646,7 @@ struct TestRunnable { // The program a part belongs to, when the run divides it (`[test] split`), // and the directory its parts are named below (`part_label`). std::string partOf; + std::string divides; // the case a part divides (P4.3) std::filesystem::path partRoot; // Its cases hold the machine (P7.1 `machine`): it runs with no other test // beside it. @@ -5244,6 +5251,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, t.startedMs = std::chrono::duration_cast(startedAt - tLoop).count(); t.worker = workerId; t.partOf = r.partOf; + t.divides = r.divides; t.alone = r.alone; if (r.select) { std::ifstream in(*r.select); @@ -5963,7 +5971,9 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // given where they are still declared (`?`); the last selects // the case without them (`!`), so a case no run measured runs // once, in it. Only a program whose selection understands both - // (mtest 0.9.0) is divided so. + // (mtest 0.9.0) is divided so, and a case only while the runs + // measured its division shorter than the case whole + // (`division_pays`): its cases may share what the first fills. if (measured && share > 0 && report.selectsWithin) { for (auto& [key, ids] : groups) { std::vector rest; @@ -5971,7 +5981,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, const auto inner = ts::measured_within(history, r.name, id); const std::vector one{id}; std::size_t k = 1; - if (inner.size() >= 2) + if (inner.size() >= 2 && ts::division_pays(history, r.name, id)) k = std::min(inner.size(), static_cast( std::ceil(ts::estimate(history, r.name, one) / share))); if (k <= 1) { rest.push_back(id); continue; } @@ -6022,6 +6032,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, if (machine && groups.contains({where, false})) part.name += " (machine)"; if (of > 1) part.name += std::format(" ({}/{})", index, of); part.partOf = r.name; + part.divides = within; part.alone = machine; { // A part of a divided case may run as long as the case states. diff --git a/src/build/test_schedule.cppm b/src/build/test_schedule.cppm index 964798f6a..fb99a46e3 100644 --- a/src/build/test_schedule.cppm +++ b/src/build/test_schedule.cppm @@ -17,6 +17,14 @@ export namespace mcpp::build::test_schedule { struct History { std::map programs; std::map cases; + // A CASE DIVIDED IS MEASURED BOTH WAYS (P4.3), by `case_key`: `whole`, its + // newest duration in a run that did not divide it; `divided`, the longest + // part of the newest run that did. The cases of one case may share a state + // the first of them fills (a cache, a toolchain), and every part pays it + // again: e2e/openkal/sets took 1375 s whole and 909, 1109 and 1306 s as + // three parts (run 43641874830). The runs say whether a division pays. + std::map whole; + std::map divided; std::size_t runs = 0; bool empty() const { return programs.empty() && cases.empty(); } }; @@ -32,6 +40,14 @@ History read_history(const std::filesystem::path& store, std::size_t maxRuns = 3 // and its id. std::string case_key(std::string_view program, std::string_view id); +// WHETHER A DIVISION OF CASE `id` PAYS: a case no run divided is divided once, +// to be measured so; then it is divided while its longest part took at most +// `kDivisionGain` of the case whole. With no whole duration in the window (a +// run of the store's newest that all divided it), it runs whole, to be +// measured again. +inline constexpr double kDivisionGain = 0.8; +bool division_pays(const History& h, std::string_view program, std::string_view id); + // THE CASES A CASE DECLARES, AS RUNS MEASURED THEM (P3.4: a list does not name // them): the innermost below `id` in program `program`, in id order. A runner // divides a long case by them (P4.3). @@ -132,21 +148,39 @@ History read_history(const std::filesystem::path& store, std::size_t maxRuns, if (!std::filesystem::is_directory(dir, ec)) continue; ++h.runs; std::map programOf; // a part's record -> its program + std::map dividesOf; // a part's record -> the case it divides + std::map longest; // a divided case -> its longest part here for (const auto& p : read_lines(dir / "programs.ndjson")) { const auto of = text(p, "part_of"); - programOf[text(p, "name")] = of.empty() ? text(p, "name") : of; + const auto program = of.empty() ? text(p, "name") : of; + programOf[text(p, "name")] = program; + const auto divides = text(p, "divides"); + if (!divides.empty()) dividesOf[text(p, "name")] = divides; const auto status = text(p, "status"); const double ms = number(p, "ms"); // A test built and not run, or not run, measured nothing. if (ms < 0 || status == "built" || status == "not-run") continue; h.programs.try_emplace(text(p, "name"), ms); + if (!divides.empty()) { + auto& l = longest[case_key(program, divides)]; + l = (std::max)(l, ms); + } } + for (const auto& [key, ms] : longest) h.divided.try_emplace(key, ms); for (const auto& c : read_lines(dir / "cases.ndjson")) { const double ms = number(c, "ms"); if (ms < 0 || text(c, "status") == "skip") continue; const auto record = text(c, "program"); const auto of = programOf.find(record); - h.cases.try_emplace(case_key(of != programOf.end() ? of->second : record, text(c, "id")), ms); + const auto id = text(c, "id"); + const auto key = case_key(of != programOf.end() ? of->second : record, id); + h.cases.try_emplace(key, ms); + // A part of a divided case measured that case, and the cases that + // declare it, only in part. + const auto d = dividesOf.find(record); + const bool partial = d != dividesOf.end() + && (d->second == id || d->second.starts_with(id + "/")); + if (!partial) h.whole.try_emplace(key, ms); } } return h; @@ -187,6 +221,14 @@ std::vector case_estimates(const History& h, std::string_view name, std: const std::string_view id = ids[i].starts_with('?') ? std::string_view(ids[i]).substr(1) : ids[i]; auto it = h.cases.find(case_key(name, id)); if (it == h.cases.end()) continue; + // A case run whole is what it took then; its declared cases measured + // in parts each paid what they share. + if (auto w = h.whole.find(case_key(name, id)); w != h.whole.end()) { + est[i] = w->second; + sum += est[i]; + ++known; + continue; + } // A case a run divided measured only its rest there: it takes at // least what the cases it declared took. double inner = 0; @@ -210,6 +252,14 @@ std::string case_key(std::string_view program, std::string_view id) { return std::format("{}\n{}", program, id); } +bool division_pays(const History& h, std::string_view program, std::string_view id) { + const auto key = case_key(program, id); + const auto d = h.divided.find(key); + if (d == h.divided.end()) return true; + const auto w = h.whole.find(key); + return w != h.whole.end() && d->second <= kDivisionGain * w->second; +} + std::vector measured_within(const History& h, std::string_view program, std::string_view id) { const auto prefix = case_key(program, id) + "/"; const auto skip = case_key(program, "").size(); diff --git a/tests/unit/test_test_schedule.cpp b/tests/unit/test_test_schedule.cpp index 012c0213a..f5b875b33 100644 --- a/tests/unit/test_test_schedule.cpp +++ b/tests/unit/test_test_schedule.cpp @@ -144,9 +144,9 @@ UTest a_case_is_divided_by_the_cases_it_declared{"TestSchedule/ACaseIsDividedByT // least what the cases it declared took. Store d; d.run("a", "2026-11-01T00:00:00Z", - R"j({"name":"p (1/3)","part_of":"p","status":"pass","ms":500})j" "\n" - R"j({"name":"p (2/3)","part_of":"p","status":"pass","ms":400})j" "\n" - R"j({"name":"p (3/3)","part_of":"p","status":"pass","ms":100})j" "\n", + R"j({"name":"p (1/3)","part_of":"p","divides":"t","status":"pass","ms":500})j" "\n" + R"j({"name":"p (2/3)","part_of":"p","divides":"t","status":"pass","ms":400})j" "\n" + R"j({"name":"p (3/3)","part_of":"p","divides":"t","status":"pass","ms":100})j" "\n", R"j({"program":"p (3/3)","id":"t","status":"pass","ms":100})j" "\n" R"j({"program":"p (1/3)","id":"t/x","status":"pass","ms":500})j" "\n" R"j({"program":"p (2/3)","id":"t/y","status":"pass","ms":400})j" "\n"); @@ -155,6 +155,51 @@ UTest a_case_is_divided_by_the_cases_it_declared{"TestSchedule/ACaseIsDividedByT expect(eq(ts::estimate(hd, "p", t), 900.0)); }}; +// A division is kept while it pays. The cases of one case may share what the +// first fills, and each part pays it again: the runs compare the longest part +// with the case run whole. +UTest a_division_is_kept_while_it_pays{"TestSchedule/ADivisionIsKeptWhileItPays", [] { + const auto whole = std::string( + R"j({"program":"p","id":"t","status":"pass","ms":1000})j" "\n" + R"j({"program":"p","id":"t/x","status":"pass","ms":600})j" "\n" + R"j({"program":"p","id":"t/y","status":"pass","ms":400})j" "\n"); + const auto parts = [](double first, double last) { + return std::format(R"j({{"name":"p [t] (1/2)","part_of":"p","divides":"t","status":"pass","ms":{}}})j" "\n" + R"j({{"name":"p [t] (2/2)","part_of":"p","divides":"t","status":"pass","ms":{}}})j" "\n", + first, last); + }; + const auto cases = std::string( + R"j({"program":"p [t] (1/2)","id":"t/x","status":"pass","ms":900})j" "\n" + R"j({"program":"p [t] (2/2)","id":"t","status":"pass","ms":850})j" "\n" + R"j({"program":"p [t] (2/2)","id":"t/y","status":"pass","ms":800})j" "\n"); + + // Never divided: divided once, to be measured so. + Store never; + never.run("a", "2026-11-01T00:00:00Z", R"j({"name":"p","status":"pass","ms":1000})j" "\n", whole); + expect(ts::division_pays(ts::read_history(never.dir), "p", "t")); + + // Divided, and its longest part took nine tenths of the case whole: not again. + Store slower; + slower.run("a", "2026-11-01T00:00:00Z", R"j({"name":"p","status":"pass","ms":1000})j" "\n", whole); + slower.run("b", "2026-11-02T00:00:00Z", parts(900, 850), cases); + const auto hs = ts::read_history(slower.dir); + expect(!ts::division_pays(hs, "p", "t")); + // And the case is estimated as it ran whole, not by its parts' cases. + const std::vector t{"t"}; + expect(eq(ts::estimate(hs, "p", t), 1000.0)); + + // Divided into parts of at most four fifths: kept. + Store faster; + faster.run("a", "2026-11-01T00:00:00Z", R"j({"name":"p","status":"pass","ms":1000})j" "\n", whole); + faster.run("b", "2026-11-02T00:00:00Z", parts(600, 500), cases); + expect(ts::division_pays(ts::read_history(faster.dir), "p", "t")); + + // Divided, and no run in the window ran it whole: run whole, measured again. + Store only; + only.run("b", "2026-11-02T00:00:00Z", parts(600, 500), cases); + expect(!ts::division_pays(ts::read_history(only.dir), "p", "t")); +}}; + UTest a_shard_is_i_of_n{"TestSchedule/AShardIsIOfN", [] { const auto shard = ts::parse_shard("2/4"); expect(fatal(shard.has_value())); From cc208723883a03ae23f26acebcf618682dc99a1d Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 07:26:40 +0900 Subject: [PATCH 7/9] design 6.3: main's first run after the merge measures the division once --- .agents/docs/2026-11-14-ci-and-release-time-cost-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md b/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md index afe0f1390..cba7d983f 100644 --- a/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md +++ b/.agents/docs/2026-11-14-ci-and-release-time-cost-design.md @@ -290,7 +290,7 @@ Windows 把每个 `\n` 写成 `\r\n`;`write_config_value` 只认 `\n` 结尾 **决定。** 划分以测量判定:各组在程序进程一行写 `divides`;存储同时记取用例最近一次整体运行的用时与最近一次 划分中最长一组的用时;从未被划分的用例划分一次以取得测量,此后仅当最长一组不超过整体的五分之四时继续划分; 用例的估计取整体运行的用时(`test_schedule::division_pays`,单元 `TestSchedule/ADivisionIsKeptWhileItPays`)。 -合入后 main 的存储中这两个套件各有一次划分的记录,此后整体运行,直到记录移出 30 次运行的窗口后再测一次。 +main 的存储中尚无划分的记录:合入后的第一次 main 运行把这两个套件各划分一次并记录,此后整体运行,直到记录移出 30 次运行的窗口后再测一次。 缩短这两个套件的途径是其共享状态的复用(预填充的 home),不是划分。 **并行暴露的两个缺陷**(均已修复并有单元测试): From f7b9e0de5603ec10699f8cb4456e508cf0553c55 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 07:41:28 +0900 Subject: [PATCH 8/9] The built-in platform is openkal-platform 0.28.0: the group on openkal 0.25.3 and the 32-bit, UEFI and DOS rows The e2e case of the default platform reads the engine's pin from its data; the target-row snapshot and the matrix's runtime cells follow the group. --- CHANGELOG.md | 6 +++ docs/21-the-target-triple.md | 2 +- docs/35-platforms.md | 2 +- docs/50-machine-output.md | 2 +- docs/zh/21-the-target-triple.md | 2 +- docs/zh/35-platforms.md | 2 +- docs/zh/50-machine-output.md | 2 +- modules/toolchain-model/data/builtin.toml | 14 ++--- tests/e2e/openkal/platforms.cpp | 23 ++++++--- tests/fixtures/target-rows.snapshot | 20 ++++---- tests/matrix/expected.tsv | 62 ++++++++++++----------- 11 files changed, 78 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cfc8f6a6..2ac39805f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,12 @@ ### Changed +- The built-in platform of the ok images and of the musl identities is + openkal-platform 0.28.0 (was 0.27.1): the group on openkal 0.25.3, and the + rows `i686-linux-musl`, `i686-openkal`, `x86_64-uefi` and `i686-dos-musl` + (a technical experiment). A project that names openkal 0.25.3 builds for + `x86_64-openkal` without stating a platform; with 0.27.1 the group's openkal + 0.25.0 refused it. - `mcpp new` scaffolds tests with mtest 0.9.0, the version this package's own tests use, so a new package's long cases are divisible by the cases they declare. diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 9f55715ff..ab82011de 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -581,7 +581,7 @@ vocabulary, nothing wired yet. | `x86_64-windows-gnu` | Linux and Windows by the gcc payload; every host by the MinGW-w64 base package | one identity, host-split at the distribution layer; the `mingw-llvm` provider serves it with clang and `xim:mingw-w64-sysroot` (llvm-mingw's target side: MinGW-w64 on UCRT, libc++, compiler-rt) where no gcc payload exists | | `aarch64-windows-gnu` | every host | the MinGW-w64 base package alone; no gcc payload targets it | | `*-freebsd` | every host, through a platform | the rows are built-in; a platform names each identity's FreeBSD base package (`xim:x86_64-freebsd-sysroot`, `xim:aarch64-freebsd-sysroot`) and is selected with `[target.] platform`, or by any selector the row meets (`[target.freebsd]`, `cfg(os = "freebsd")`); another platform package's provider does not compete with the chosen platform | -| `*-freebsd-musl` | every host, through the graph | openkal-platform's hosted part (0.27.1): openkal-musl over openkal-freebsd standalone, one static program; a request of `-freebsd` with no platform selected for it takes this identity (2026.11.10.2+), as `aarch64-macos` takes the musl identity on a host that is not a Mac | +| `*-freebsd-musl` | every host, through the graph | openkal-platform's hosted part (0.28.0): openkal-musl over openkal-freebsd standalone, one static program; a request of `-freebsd` with no platform selected for it takes this identity (2026.11.10.2+), as `aarch64-macos` takes the musl identity on a host that is not a Mac | | `x86_64-windows-msvc` | Windows | MSVC is located on the machine | | `x86_64-windows-musl` | Windows by payload; anywhere by graph | no gcc emits PE+musl, and LLVM cannot spell the triple | | `aarch64-macos` | macOS | the SDK is the machine's | diff --git a/docs/35-platforms.md b/docs/35-platforms.md index cdded3cf3..91f74e659 100644 --- a/docs/35-platforms.md +++ b/docs/35-platforms.md @@ -203,7 +203,7 @@ manifest, ```toml [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.27.1" # a release, or a series: "openkal-platform@^0.27" +platform = "openkal-platform@0.28.0" # a release, or a series: "openkal-platform@^0.28" ``` and a build that fails while the default is in use and the synced index holds a diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index db6972909..994abb34c 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -552,7 +552,7 @@ installation, which is located on the machine rather than installed by mcpp. A target row is `{target, note, toolchain, pin, status, default}`, and `status` is one of `installed` / `available` / `via dependency graph` / `planned`. A target row also carries `platform` (2026.10.27.1+): the platform package the -target table selects for it (`mcpplibs.openkal-platform@0.27.1` from 2026.11.12.1, `@0.26.0` in 2026.11.10.2, `@0.25.0` in 2026.11.10.1, `@0.24.4` from 2026.11.8.1, `@0.24.3` in 2026.11.7.2; the series `^0.24` in 2026.11.7.1), or `null`. +target table selects for it (`mcpplibs.openkal-platform@0.28.0` from 2026.11.15.1, `@0.27.1` from 2026.11.12.1, `@0.26.0` in 2026.11.10.2, `@0.25.0` in 2026.11.10.1, `@0.24.4` from 2026.11.8.1, `@0.24.3` in 2026.11.7.2; the series `^0.24` in 2026.11.7.1), or `null`. **`toolchain` and `pin` are not the same field twice.** `toolchain` is what the row is associated with — the installed payload on an installed row, the diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index 7c6daf903..6c53f5fab 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -535,7 +535,7 @@ system · `系统` 在机器上被找到,不是 mcpp 安装的 · `SDK` 平台 | `x86_64-windows-gnu` | Linux 与 Windows 经 gcc 载荷;每一台宿主经 MinGW-w64 基座包 | 一个身份,只在分发层按宿主拆分;没有 gcc 载荷之处,`mingw-llvm` 提供者以 clang 与 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧:UCRT 上的 MinGW-w64、libc++、compiler-rt)服务它 | | `aarch64-windows-gnu` | 每一台宿主 | 只由 MinGW-w64 基座包服务;没有 gcc 载荷以它为目标 | | `*-freebsd` | 每一台宿主,经平台 | 行是内置的;平台为每个身份指名 FreeBSD 基座包(`xim:x86_64-freebsd-sysroot`、`xim:aarch64-freebsd-sysroot`),以 `[target.<行>] platform` 或该行满足的任一选择器(`[target.freebsd]`、`cfg(os = "freebsd")`)选择;其他平台包的提供者不与所选平台竞争 | -| `*-freebsd-musl` | 每一台宿主,经依赖图 | openkal-platform 的 hosted 部分(0.27.1):openkal-freebsd 的 standalone 形态之上的 openkal-musl,一个静态程序;未为 `-freebsd` 选平台时,该请求取这一身份(2026.11.10.2+),正如非 Mac 宿主上的 `aarch64-macos` 取 musl 身份 | +| `*-freebsd-musl` | 每一台宿主,经依赖图 | openkal-platform 的 hosted 部分(0.28.0):openkal-freebsd 的 standalone 形态之上的 openkal-musl,一个静态程序;未为 `-freebsd` 选平台时,该请求取这一身份(2026.11.10.2+),正如非 Mac 宿主上的 `aarch64-macos` 取 musl 身份 | | `x86_64-windows-msvc` | Windows | MSVC 是在机器上被找到的 | | `x86_64-windows-musl` | payload 只在 Windows 上;图则任意宿主 | 没有 gcc 能发出 PE + musl,且 LLVM 拼不出这个三元组 | | `aarch64-macos` | macOS | SDK 是那台机器自己的 | diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md index ac444c854..75a43333d 100644 --- a/docs/zh/35-platforms.md +++ b/docs/zh/35-platforms.md @@ -168,7 +168,7 @@ error: openkal-musl is requested as '^0.27.0' by app, and the platform openkal-p ```toml [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.27.1" # 一个版本,或一个系列:"openkal-platform@^0.27" +platform = "openkal-platform@0.28.0" # 一个版本,或一个系列:"openkal-platform@^0.28" ``` 使用默认平台的构建失败、且已同步的索引中有更新版本时,最后输出一行 `tip:`,写出两个版本与上面这一行。 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 9c5f2fc3a..6edbe398c 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -488,7 +488,7 @@ mcpp toolchain list --format json 的形状是 `{target, note, toolchain, pin, status, default}`,`status` 取 `installed` / `available` / `via dependency graph` / `planned` 之一。 目标行另有 `platform`(2026.10.27.1+):目标表为该目标选择的平台包 -(2026.11.12.1 起为 `mcpplibs.openkal-platform@0.27.1`,2026.11.10.2 为 `@0.26.0`,2026.11.10.1 为 `@0.25.0`,2026.11.8.1 起为 `@0.24.4`,2026.11.7.2 为 `@0.24.3`;2026.11.7.1 为系列 `^0.24`),或 `null`。 +(2026.11.15.1 起为 `mcpplibs.openkal-platform@0.28.0`,2026.11.12.1 为 `@0.27.1`,2026.11.10.2 为 `@0.26.0`,2026.11.10.1 为 `@0.25.0`,2026.11.8.1 起为 `@0.24.4`,2026.11.7.2 为 `@0.24.3`;2026.11.7.1 为系列 `^0.24`),或 `null`。 **`toolchain` 与 `pin` 不是把同一个字段写了两遍。** `toolchain` 表示这一行 关联到什么 —— 已装的行是装好的载荷,词表行是它自己的约定。`pin` 只承载目标 diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml index 9161d1ec5..85836fd32 100644 --- a/modules/toolchain-model/data/builtin.toml +++ b/modules/toolchain-model/data/builtin.toml @@ -163,9 +163,11 @@ standard = true # 32-BIT ROWS (E11): registered intent, refused until served. No payload of the # index targets them. openkal states its 32-bit class (0.25.1, clause 5.5), and -# openkal-platform 0.28.0 serves `i686-linux-musl` and `i686-openkal` with the -# LLVM toolchain; the rows stay planned here until the engine's pin of that -# platform serves them. An explicit `[target.] toolchain` opts in early. +# openkal-platform 0.28.0, the engine's pin, serves `i686-linux-musl` and +# `i686-openkal` with the LLVM toolchain: a planned row its targets name is +# served by the platform (2026.11.14.1). The rows stay planned here, as a row +# no payload of the index serves; an explicit `[target.] toolchain` opts +# in as well. [[platform.define]] triple = "i686-linux-musl" loader = "/lib/ld-musl-i386.so.1" @@ -601,7 +603,7 @@ hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" -platform = "mcpplibs.openkal-platform@0.27.1" +platform = "mcpplibs.openkal-platform@0.28.0" [[platform.provider]] name = "openkal" @@ -611,7 +613,7 @@ hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" -platform = "mcpplibs.openkal-platform@0.27.1" +platform = "mcpplibs.openkal-platform@0.28.0" # The same arrangement on Windows, whose own interfaces a resource script # compiles against: their headers are the MinGW-w64 base's, which states them @@ -624,7 +626,7 @@ hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" -platform = "mcpplibs.openkal-platform@0.27.1" +platform = "mcpplibs.openkal-platform@0.28.0" system-headers = "xim:mingw-w64-sysroot@20261104-2" # ── Systems ────────────────────────────────────────────────────────────────── diff --git a/tests/e2e/openkal/platforms.cpp b/tests/e2e/openkal/platforms.cpp index 8dc8833f7..6f646021e 100644 --- a/tests/e2e/openkal/platforms.cpp +++ b/tests/e2e/openkal/platforms.cpp @@ -238,9 +238,18 @@ int main() { }; "1025 the default platform is one release"_test[s::llvm][s::python3] = [] { - // A a project stating no platform builds x86_64-openkal over openkal-platform 0.27.1 - // and locks it; B a lock recording another release does not decide, and is rewritten; - // C a platform the project states (a series) is taken as it is. + // A a project stating no platform builds x86_64-openkal over the engine's pin of + // openkal-platform and locks it; B a lock recording another release does not decide, + // and is rewritten; C a platform the project states (a series) is taken as it is. + // The pin is the engine's data (modules/toolchain-model/data/builtin.toml). + const auto pin = [] { + std::ifstream in(s::repository() / "modules/toolchain-model/data/builtin.toml", std::ios::binary); + const std::string text{std::istreambuf_iterator{in}, {}}; + std::smatch m; + const std::regex line{R"rx(platform\s*=\s*"mcpplibs\.openkal-platform@([0-9.]+)")rx"}; + return std::regex_search(text, m, line) ? m[1].str() : std::string{}; + }(); + expect(fatal(!pin.empty())) << "builtin.toml names no openkal-platform release"; auto app = s::project::scaffold("app"); const std::regex record{R"rx((\[package\."openkal-platform"\][^\[]*?version\s*=\s*")([^"]+)("))rx"}; const auto locked = [&] { @@ -250,20 +259,20 @@ int main() { }; auto r = app.mcpp({"build", "--target", "x86_64-openkal"}); expect(fatal(e2e::succeeded(r))) << "A: the openkal build failed: " << s::text(r); - expect(fatal(s::has_line(s::text(r), "Platform *openkal-platform 0.27.1"))) << "A: the build does not use openkal-platform 0.27.1: " << s::text(r); - expect(fatal(locked() == "0.27.1")) << "A: mcpp.lock does not record 0.27.1: " << app.read("mcpp.lock"); + expect(fatal(s::has_line(s::text(r), "Platform *openkal-platform " + pin))) << "A: the build does not use openkal-platform " << pin << ": " << s::text(r); + expect(fatal(locked() == pin)) << "A: mcpp.lock does not record " << pin << ": " << app.read("mcpp.lock"); // B { const auto text = app.read("mcpp.lock"); std::smatch m; - expect(fatal(std::regex_search(text, m, record) && m[2] == "0.27.1")) << "B: the fixture could not edit mcpp.lock: " << text; + expect(fatal(std::regex_search(text, m, record) && m[2] == pin)) << "B: the fixture could not edit mcpp.lock: " << text; app.write("mcpp.lock", m.prefix().str() + m[1].str() + "0.24.3" + m[3].str() + m.suffix().str()); } expect(fatal(locked() == "0.24.3")) << "B: the fixture could not edit mcpp.lock: " << app.read("mcpp.lock"); r = app.mcpp({"build", "--target", "x86_64-openkal"}); expect(fatal(e2e::succeeded(r))) << "B: the openkal build failed: " << s::text(r); expect(fatal(e2e::contains(s::text(r), "default; mcpp.lock recorded v0.24.3"))) << "B: the build does not say which release it takes over the lock's: " << s::text(r); - expect(fatal(locked() == "0.27.1")) << "B: mcpp.lock was not rewritten to 0.27.1: " << app.read("mcpp.lock"); + expect(fatal(locked() == pin)) << "B: mcpp.lock was not rewritten to " << pin << ": " << app.read("mcpp.lock"); // C app.write("mcpp.toml", app.read("mcpp.toml") + "\n[target.'cfg(os = \"openkal\")']\nplatform = \"openkal-platform@^0.24\"\n"); r = app.mcpp({"build", "--target", "x86_64-openkal"}); diff --git a/tests/fixtures/target-rows.snapshot b/tests/fixtures/target-rows.snapshot index f6c37a308..b7eae2b43 100644 --- a/tests/fixtures/target-rows.snapshot +++ b/tests/fixtures/target-rows.snapshot @@ -113,7 +113,7 @@ row aarch64-windows-gnu descriptor-keys windows sysroot xim:mingw-w64-sysroot@20261104-2 row x86_64-windows-musl - table tier=preview note=PE pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=preview note=PE pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str x86_64-windows-musl llvm x86_64-w64-windows-gnu llvm@15.0 x86_64-w64-windows-gnu @@ -170,7 +170,7 @@ row aarch64-macos descriptor-keys macosx sysroot row aarch64-macos-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str aarch64-macos-musl llvm arm64-apple-macos14.0 llvm@15.0 arm64-apple-macos15.0 @@ -208,7 +208,7 @@ row riscv64-linux-musl descriptor-keys linux sysroot row i686-linux-musl - table tier=planned note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=planned note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str i686-linux-musl llvm i686-unknown-linux-musl llvm@15.0 i686-unknown-linux-musl @@ -246,7 +246,7 @@ row i686-linux-gnu descriptor-keys linux sysroot row i686-openkal - table tier=planned note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=planned note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str i686-openkal llvm i686-unknown-none-elf llvm@15.0 i686-unknown-none-elf @@ -284,7 +284,7 @@ row x86_64-macos descriptor-keys macosx sysroot row x86_64-macos-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str x86_64-macos-musl llvm x86_64-apple-macos14.0 llvm@15.0 x86_64-apple-macos15.0 @@ -341,7 +341,7 @@ row aarch64-freebsd descriptor-keys freebsd sysroot row x86_64-freebsd-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str x86_64-freebsd-musl llvm x86_64-unknown-freebsd14.0 llvm@15.0 x86_64-unknown-freebsd14.0 @@ -360,7 +360,7 @@ row x86_64-freebsd-musl descriptor-keys freebsd sysroot row aarch64-freebsd-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str aarch64-freebsd-musl llvm aarch64-unknown-freebsd14.0 llvm@15.0 aarch64-unknown-freebsd14.0 @@ -817,7 +817,7 @@ row wasm32-emscripten descriptor-keys emscripten linux sysroot row x86_64-openkal - table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str x86_64-openkal llvm x86_64-unknown-none-elf llvm@15.0 x86_64-unknown-none-elf @@ -836,7 +836,7 @@ row x86_64-openkal descriptor-keys openkal linux sysroot row aarch64-openkal - table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.1 + table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.28.0 str aarch64-openkal llvm aarch64-unknown-none-elf llvm@15.0 aarch64-unknown-none-elf @@ -971,7 +971,7 @@ set linux-musl -> x86_64-linux-musl aarch64-linux-musl | set windows -> x86_64-windows-gnu aarch64-windows-gnu | set windows-msvc -> x86_64-windows-msvc | set macos -> aarch64-macos | -set openkal -> x86_64-openkal aarch64-openkal | mcpplibs.openkal-platform@0.27.1 +set openkal -> x86_64-openkal aarch64-openkal | mcpplibs.openkal-platform@0.28.0 set none-elf -> riscv64-none-elf riscv32-none-elf aarch64-none-elf x86_64-none-elf i686-none-elf | set none-eabihf -> thumbv7em-none-eabihf thumbv8m.main-none-eabihf armv7a-none-eabihf | set android -> aarch64-linux-android x86_64-linux-android | diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 35582e59a..15295672e 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -90,6 +90,8 @@ # 是 `capability-pin`;graph 体系里图中钉的版本在平台版本组之外,`platform-version-group`。 # - openkal 各行的平台为 openkal-platform 0.27.1,openkal 列为 openkal-llvm-runtime@0.20.1。 # +# 2026.11.15.1 改变的格:引擎钉的平台为 openkal-platform 0.28.0,openkal 列为 openkal-llvm-runtime@0.20.3。 +# # 中间五列是报告器输出,不参与支持判定。现有报告器把 aarch64-ios 的 # c-abi 输出为 glibc(payload),这不代表 iOS 产物实际链接 glibc。Apple # 目标的实际依赖由目标构建与运行门检查,不能从该报告字段推导。 @@ -227,7 +229,7 @@ graph windows-x86_64 x86_64-windows-msvc msvc@system - - - - - unsupported host- graph windows-x86_64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported platform-version-group graph windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin payload linux-aarch64 aarch64-freebsd-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-aarch64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-aarch64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported convention-unreplaced @@ -237,7 +239,7 @@ payload linux-aarch64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported conve payload linux-aarch64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf payload - - - ok none payload linux-aarch64 aarch64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-aarch64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-aarch64 aarch64-windows-gnu gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 aarch64-windows-gnu llvm@23.1.3 aarch64-w64-windows-gnu none ucrt(prebuilt) libc++(prebuilt) - ok none payload linux-aarch64 armv7a-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin @@ -275,23 +277,23 @@ payload linux-aarch64 thumbv8m.main-none-eabihf llvm@23.1.3 thumbv8m.main-none-e payload linux-aarch64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-freebsd-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 x86_64-freebsd-musl llvm@23.1.3 x86_64-unknown-freebsd14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-aarch64 x86_64-freebsd-musl llvm@23.1.3 x86_64-unknown-freebsd14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-aarch64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve -payload linux-aarch64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-aarch64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-aarch64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-aarch64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf payload - - - ok none payload linux-aarch64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-aarch64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve payload linux-aarch64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none ucrt(prebuilt) libc++(prebuilt) - ok none payload linux-aarch64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-aarch64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-x86_64 aarch64-freebsd-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-x86_64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported convention-unreplaced @@ -301,7 +303,7 @@ payload linux-x86_64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported conven payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf payload - - - ok none payload linux-x86_64 aarch64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-x86_64 aarch64-windows-gnu gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-windows-gnu llvm@23.1.3 aarch64-w64-windows-gnu none ucrt(prebuilt) libc++(prebuilt) - ok none payload linux-x86_64 armv7a-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin @@ -339,7 +341,7 @@ payload linux-x86_64 thumbv8m.main-none-eabihf llvm@23.1.3 thumbv8m.main-none-ea payload linux-x86_64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-freebsd-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-freebsd-musl llvm@23.1.3 x86_64-unknown-freebsd14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-x86_64 x86_64-freebsd-musl llvm@23.1.3 x86_64-unknown-freebsd14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-x86_64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos glibc(payload) libstdc++(payload) - ok none @@ -347,25 +349,25 @@ payload linux-x86_64 x86_64-linux-gnu llvm@23.1.3 x86_64-unknown-linux-gnu paylo payload linux-x86_64 x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced payload linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-x86_64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-x86_64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf payload - - - ok none payload linux-x86_64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload linux-x86_64 x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none ucrt(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none ucrt(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none -payload macos-arm64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload linux-x86_64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu payload musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none +payload macos-arm64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload macos-arm64 aarch64-ios llvm@23.1.3 arm64-apple-ios18.5 host glibc(payload) libc++(payload) - ok none payload macos-arm64 aarch64-ios-sim llvm@23.1.3 arm64-apple-ios18.5-simulator host sim(payload) libc++(payload) - ok none payload macos-arm64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload macos-arm64 aarch64-linux-gnu llvm@23.1.3 - - - - - unsupported host-cannot-serve -payload macos-arm64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload macos-arm64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload macos-arm64 aarch64-macos llvm@23.1.3 arm64-apple-macos14.0 host libSystem(payload) libc++(payload) - ok none -payload macos-arm64 aarch64-macos-musl llvm@23.1.3 arm64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload macos-arm64 aarch64-macos-musl llvm@23.1.3 arm64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload macos-arm64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf host - - - ok none -payload macos-arm64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload macos-arm64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload macos-arm64 aarch64-windows-gnu llvm@23.1.3 aarch64-w64-windows-gnu none ucrt(prebuilt) libc++(prebuilt) - ok none payload macos-arm64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi host - - - ok none payload macos-arm64 armv7a-none-eabihf llvm@23.1.3 armv7a-none-eabihf host - - - ok none @@ -384,27 +386,27 @@ payload macos-arm64 thumbv8m.base-none-eabi llvm@23.1.3 thumbv8m.base-none-eabi payload macos-arm64 thumbv8m.main-none-eabi llvm@23.1.3 thumbv8m.main-none-eabi host - - - ok none payload macos-arm64 thumbv8m.main-none-eabihf llvm@23.1.3 thumbv8m.main-none-eabihf host - - - ok none payload macos-arm64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capability-pin -payload macos-arm64 x86_64-freebsd-musl llvm@23.1.3 x86_64-unknown-freebsd14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload macos-arm64 x86_64-freebsd-musl llvm@23.1.3 x86_64-unknown-freebsd14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload macos-arm64 x86_64-ios-sim llvm@23.1.3 x86_64-apple-ios18.5-simulator host sim(payload) libc++(payload) - ok none payload macos-arm64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin -payload macos-arm64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none -payload macos-arm64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none -payload macos-arm64 x86_64-macos-musl llvm@23.1.3 x86_64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload macos-arm64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none +payload macos-arm64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none +payload macos-arm64 x86_64-macos-musl llvm@23.1.3 x86_64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload macos-arm64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf host - - - ok none -payload macos-arm64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload macos-arm64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload macos-arm64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none ucrt(prebuilt) libc++(prebuilt) - ok none -payload macos-arm64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none -payload windows-x86_64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14.0 none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload macos-arm64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu host musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none +payload windows-x86_64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14.0 none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload windows-x86_64 aarch64-freebsd-musl msvc@system - - - - - unsupported capability-pin payload windows-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload windows-x86_64 aarch64-linux-android msvc@system - - - - - unsupported capability-pin payload windows-x86_64 aarch64-linux-gnu llvm@23.1.3 - - - - - unsupported host-cannot-serve payload windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported convention-unreplaced -payload windows-x86_64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload windows-x86_64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported convention-unreplaced payload windows-x86_64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf none - - - ok none payload windows-x86_64 aarch64-none-elf msvc@system - - - - - unsupported capability-pin -payload windows-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload windows-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload windows-x86_64 aarch64-openkal msvc@system - - - - - unsupported capability-pin payload windows-x86_64 aarch64-windows-gnu llvm@23.1.3 aarch64-w64-windows-gnu none ucrt(prebuilt) libc++(prebuilt) - ok none payload windows-x86_64 aarch64-windows-gnu msvc@system - - - - - unsupported capability-pin @@ -442,21 +444,21 @@ payload windows-x86_64 thumbv8m.main-none-eabihf llvm@23.1.3 thumbv8m.main-none- payload windows-x86_64 thumbv8m.main-none-eabihf msvc@system - - - - - unsupported capability-pin payload windows-x86_64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capability-pin payload windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported capability-pin -payload windows-x86_64 x86_64-freebsd-musl llvm@23.1.3 x86_64-unknown-freebsd14.0 none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload windows-x86_64 x86_64-freebsd-musl llvm@23.1.3 x86_64-unknown-freebsd14.0 none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload windows-x86_64 x86_64-freebsd-musl msvc@system - - - - - unsupported capability-pin payload windows-x86_64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload windows-x86_64 x86_64-linux-android msvc@system - - - - - unsupported capability-pin payload windows-x86_64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported convention-unreplaced -payload windows-x86_64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload windows-x86_64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload windows-x86_64 x86_64-macos msvc@system - - - - - unsupported capability-pin payload windows-x86_64 x86_64-none-elf llvm@23.1.3 - - - - - unsupported lld-required-absent payload windows-x86_64 x86_64-none-elf msvc@system - - - - - unsupported capability-pin -payload windows-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload windows-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload windows-x86_64 x86_64-openkal msvc@system - - - - - unsupported capability-pin payload windows-x86_64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none ucrt(payload) libstdc++(payload) - ok none payload windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-windows-msvc llvm@23.1.3 x86_64-pc-windows-msvc none msvc(payload) msvc-stl(payload) - ok none payload windows-x86_64 x86_64-windows-msvc msvc@system x86_64-pc-windows-msvc none msvc(payload) (payload) - ok none -payload windows-x86_64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.1 ok none +payload windows-x86_64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu none musl(platform) libc++(platform) openkal-llvm-runtime@0.20.3 ok none payload windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin From 8ce9c602d815907ac72d7b8ddc834f36a652ae87 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 08:17:54 +0900 Subject: [PATCH 9/9] A polyglot pack of every system leaves out the systems the file is not started on (uefi, dos); one touch helper for the e2e cases, past the present --- CHANGELOG.md | 6 ++++++ src/pack/pack.cppm | 4 ++++ src/pack/polyglot.cppm | 10 ++++++++++ tests/e2e/build/basics.cpp | 3 +-- tests/e2e/build/flags.cpp | 3 +-- tests/e2e/build/link.cpp | 3 +-- tests/e2e/build/sources.cpp | 3 +-- tests/support/src/support.cppm | 8 ++++++++ tests/unit/test_pack_polyglot.cpp | 13 +++++++++++++ 9 files changed, 45 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac39805f..a1643cf5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,12 @@ environment (`subos/default/.xlings.json`) as well as the project's, and with the turn in the project's directory it collided on Windows with an install of another project ("Failed to write file"). One turn per home orders both. +- `mcpp pack --pkg-format polyglot --systems all` leaves out the systems a + polyglot file is not started on: openkal-platform 0.28.0 names `uefi-x86_64` + and `dos-i686` among its runner hosts, and the pack refused ("a polyglot file + holds no program of uefi"). They are reported as not covered, with + `--pkg-format single-file` as the way to make them; naming one of them + explicitly is still refused. - A host tool built by two builds of one home at once is published by both: each writes through a temporary of its own (`mcpp.build.stage`), and a store entry already equal to the tool is published. With one temporary name for diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 4d2622237..553060b93 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -3592,6 +3592,10 @@ std::vector single_file_rows(const Plan& plan) { if (all && r.skipped.empty() && plan.platform.active) if (auto why = detail::unbuilt_carrier(plan.platform, system, arch, true)) r.skipped = *why; + // `all` in one polyglot file: the systems the file is started on. + if (all && r.skipped.empty() && plan.opts.polyglot && !polyglot::holds(system)) + r.skipped = std::format("a polyglot file is started by a shell or by the Windows loader, " + "and {} starts a program of its own (--pkg-format single-file)", system); rows.push_back(std::move(r)); } return rows; diff --git a/src/pack/polyglot.cppm b/src/pack/polyglot.cppm index e10f802ec..06db93350 100644 --- a/src/pack/polyglot.cppm +++ b/src/pack/polyglot.cppm @@ -48,6 +48,12 @@ std::expected assemble(std::string_view name, const st // Whether `head`, the first bytes of a file, are a file `assemble` wrote. bool is_polyglot(std::string_view head); +// Whether the file can hold a program of `system`: a system that states its +// `uname` (the script selects by it), or a stated system without one (its +// program is the PE the file leads with). A program a firmware or another +// loader starts by itself (`uefi`, `dos`) is no program of such a file. +bool holds(std::string_view system); + } // namespace mcpp::pack::polyglot namespace mcpp::pack::polyglot { @@ -139,6 +145,10 @@ std::string stage_one(std::uint64_t at, std::uint64_t length) { } // namespace +bool holds(std::string_view system) { + return !uname_of(system).empty() || mcpp::toolchain::target::find_system(system) != nullptr; +} + bool is_polyglot(std::string_view head) { return head.starts_with("MZqFpD='\n") || head.starts_with("#!/bin/sh\n" "o=$0;"); } diff --git a/tests/e2e/build/basics.cpp b/tests/e2e/build/basics.cpp index 5d7e583c7..3c5b656c5 100644 --- a/tests/e2e/build/basics.cpp +++ b/tests/e2e/build/basics.cpp @@ -60,8 +60,7 @@ std::vector lines_of(std::string_view text) { return out; } -// A file's modification time moved past its present one (`sleep 1; touch file`). -void touch(const fs::path& f) { fs::last_write_time(f, fs::last_write_time(f) + std::chrono::seconds(2)); } +using s::touch; // The first file under `root` whose path ends with `tail` (`find -path '*'`). std::optional path_ending(const fs::path& root, std::string_view tail) { diff --git a/tests/e2e/build/flags.cpp b/tests/e2e/build/flags.cpp index aaea398f3..cdd12a0c4 100644 --- a/tests/e2e/build/flags.cpp +++ b/tests/e2e/build/flags.cpp @@ -60,8 +60,7 @@ std::vector lines_of(std::string_view text) { return out; } -// A file's modification time moved past its present one (`sleep 1; touch file`). -void touch(const fs::path& f) { fs::last_write_time(f, fs::last_write_time(f) + std::chrono::seconds(2)); } +using s::touch; // The first file under `root` whose path ends with `tail` (`find -path '*'`). std::optional path_ending(const fs::path& root, std::string_view tail) { diff --git a/tests/e2e/build/link.cpp b/tests/e2e/build/link.cpp index 965a4e34b..6689defb8 100644 --- a/tests/e2e/build/link.cpp +++ b/tests/e2e/build/link.cpp @@ -59,8 +59,7 @@ std::vector lines_of(std::string_view text) { return out; } -// A file's modification time moved past its present one (`sleep 1; touch file`). -void touch(const fs::path& f) { fs::last_write_time(f, fs::last_write_time(f) + std::chrono::seconds(2)); } +using s::touch; // The first file under `root` whose path ends with `tail` (`find -path '*'`). std::optional path_ending(const fs::path& root, std::string_view tail) { diff --git a/tests/e2e/build/sources.cpp b/tests/e2e/build/sources.cpp index 34a794aea..5e121c256 100644 --- a/tests/e2e/build/sources.cpp +++ b/tests/e2e/build/sources.cpp @@ -59,8 +59,7 @@ std::vector lines_of(std::string_view text) { return out; } -// A file's modification time moved past its present one (`sleep 1; touch file`). -void touch(const fs::path& f) { fs::last_write_time(f, fs::last_write_time(f) + std::chrono::seconds(2)); } +using s::touch; // The first file under `root` whose path ends with `tail` (`find -path '*'`). std::optional path_ending(const fs::path& root, std::string_view tail) { diff --git a/tests/support/src/support.cppm b/tests/support/src/support.cppm index 6bda91c11..172752ede 100644 --- a/tests/support/src/support.cppm +++ b/tests/support/src/support.cppm @@ -118,6 +118,14 @@ fs::file_time_type file_time(std::chrono::sys_time t) { return file_now + std::chrono::duration_cast(t - sys_now); } +// A file's modification time moved past the present and its own (`sleep 1; touch file`), so a +// build sees it changed. Its own time alone is not enough: the file was written before a build +// that may take longer than the step, whose outputs are then newer than the file touched +// (`basics/04` on Windows, run 43649417016). +inline void touch(const fs::path& f) { + fs::last_write_time(f, (std::max)(fs::file_time_type::clock::now(), fs::last_write_time(f)) + std::chrono::seconds(2)); +} + // ---- mcpp ---------------------------------------------------------------------------------------- // The mcpp under test: MCPP_E2E_MCPP when it is set (a released mcpp, tested by the cases of an diff --git a/tests/unit/test_pack_polyglot.cpp b/tests/unit/test_pack_polyglot.cpp index c03f48ca4..b3fb8de4e 100644 --- a/tests/unit/test_pack_polyglot.cpp +++ b/tests/unit/test_pack_polyglot.cpp @@ -92,3 +92,16 @@ UTest recognises_its_own_heads_only{"PackPolyglot/RecognisesItsOwnHeadsOnly", [] expect(!pg::is_polyglot("#!/bin/sh\necho hi\n")); expect(!pg::is_polyglot("")); }}; + +// The file is started by a shell, which selects by `uname`, or by the Windows +// loader, whose program leads it. A system a firmware or another loader starts +// (`uefi`, a runner host of openkal-platform 0.28.0) is no system of the file: +// `--systems all` leaves it out rather than refusing the pack. +UTest holds_the_systems_a_shell_or_the_windows_loader_starts{"PackPolyglot/HoldsTheSystemsAShellOrTheWindowsLoaderStarts", [] { + expect(mcpp::pack::polyglot::holds("linux")); + expect(mcpp::pack::polyglot::holds("macos")); + expect(mcpp::pack::polyglot::holds("freebsd")); + expect(mcpp::pack::polyglot::holds("windows")); + expect(!mcpp::pack::polyglot::holds("uefi")); + expect(!mcpp::pack::polyglot::holds("dos")); +}};