Skip to content

Commit 7d7aabf

Browse files
authored
2026.11.13.1: toolchain payloads as data, build directories by lineage, UEFI through openkal, test feedback (mcpp#927), end-to-end cases in C++ with places and capabilities (mtest 0.8.0), canaries (#930)
* wip: a row's GCC payload as data (gcc-triple); the COFF object format * tests/support and the first C++ end-to-end case; mtest 0.7.1 tests/support (mcpp.test.support) is the support of the end-to-end cases (design 2026-11-12 §5.9): a project as a table of files, mcpp run with the shared home or a fresh one, the readings of a file's lines, a JSON document and an executable's header (ELF, PE, Mach-O) made by this process. A case file imports mcpplibs.mtest itself, which begins it at mtest's entry. tests/e2e/basics holds 02 (new, build, run) as the first case. mtest 0.7.1 is the dev-dependency and the engine CI pins; the unit suite holds under it (169 programs, 2627 cases). * e2e: the migration inventory of the 752 scripts and the keep baseline tests/e2e/MIGRATION.tsv gives each script its subsystem (25 directory-style names), its class (sink, merge, keep; design 2026-11-12 §5.9) and its planned destination: e2e/<subsystem> for a merged case, unit/<file> for a sunk one, keep for a script that stays. Assigned from each script's name, header and the commands it runs. merge 707, sink 12, keep 33. tests/e2e/keep.tsv is the baseline of the keep class (emulators, a physical device, upgrades and machine-state homes, CI-only and measurement legs); it may only shrink. * e2e: the test subsystem as C++ cases (38 scripts) The 38 scripts MIGRATION.tsv assigns to `test` (mcpp test: discovery, selection, reports, the JSON stream, timeouts, workspaces, --no-run and archives, the launch description, the mtest protocol, the store, the fast path) are 39 cases of the program e2e/test (tests/e2e/test/*.cpp, 12 files, one process each). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags, and a case skips with "missing capability: <word>" where the machine lacks one. The scripts are removed and MIGRATION.tsv names each one's case. Scripts: 15 16 17 18 72 78 90 94 152 153 154 155 157 158 159 160 164 178 213 673 745 854 855 856 865 989 993 1026 1034 1035 1037 1038 1050 1052 1065 1068 1069 1070. Old run (the scripts, against this branch's mcpp, Linux x86_64, gcc and llvm in ~/.mcpp): 37 passed, 1 skipped (94 requires macos; started directly it exits 0 on Linux, its otool leg being conditional). Sequential time 227 s. New run (`mcpp test e2e/test`): 38 passed, 0 failed, 1 skipped (94, missing capability: macos). 72 s of runs, 12 programs at once. tests/support gains: has(word) and needs({...}) (run_all.sh's capability probes; an unknown word fails the case), on_path, glob (a shell's `*` per component), has_line/no_line (grep -E per line), lacks, contains_nocase, count_lines, ninja_log_outputs, mcpp_command (a mcpp for an e2e::session) and project::scaffold (`mcpp new` in the case's directory). Every assertion is kept. Where a reading changed: - 159: `grep -qv '"test":"01-b/0"'` held for any line without the record; the case asserts what it meant, that no line has it (stronger). - 989: the evidence the script printed after a failure (file, xattr, codesign) is a diagnostic, not an assertion; not carried over. - 1070 O8: the script compared the output after the shell had stripped its trailing newline; the case accepts the text with or without it. - 993 is two cases: A-E, and the named-runner leg, which keeps `requires:python3` because its runner is a Python program. - python3 (993 A-E, 1026, 1035, 1037, 1050, 1065, 1070) read JSON for the scripts; the cases read it with nlohmann::json and no longer require it. - 90: its `requires: gcc` stood on line 8, which run_all.sh does not read, so it ran on every host; the case does not require gcc either. - 1052 sourced _toolchain_env.sh without using it; dropped. - 178 and 1068 drive concurrent mcpp processes through e2e::session instead of `&`, a fifo and kill. 1026 still unpacks with tar, 1038 still uses git, 94 still reads otool. * e2e: the build-program subsystem as C++ cases (66 scripts) The 66 scripts MIGRATION.tsv assigns to `build-program` (build.mcpp: its directives and protocol, its environment and cache, dependencies' programs, host tools and host modules, build plugins and the build programs' graph, accelerator requests and the probe channel, payload overrides) are 66 cases of the program e2e/build-program (tests/e2e/build-program/*.cpp, 6 files: accel, cross, deps, graph, hosttools, protocol). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 58 89 92 110 111 112 125 139 143 144 145 168 181 186 187 189 193 194 309 310 311 312 334 600 605 612 617 618 622 624 625 626 632 633 651 665 683 693 698 712 720 775 779 800 802 804 823 824 826 831 859 873 874 877 902 903 904 905 906 908 910 958 997 1011 1036 1051. 168 and 804 keep their scripts: ci-aarch64-fresh-install.yml runs them by path. The other 64 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 65 passed, 1 failed (779). New run (`mcpp test e2e/build-program`): 65 passed, 1 failed (779), 0 skipped. 779 fails on this host in both runs for one reason outside mcpp: it builds its plugin with the `gcc` PATH names, here an xlings subos gcc whose shared objects record NEEDED libc.so (the linker script), which the loader cannot load; /usr/bin/gcc records none. Under the script the plugin built and `mcpp run` failed loading it; under `mcpp test` PATH names another subos gcc that does not find crti.o. tests/support gains: home kinds `linked` (the case's home with the shared registry linked, as `ln -s ~/.mcpp/registry`), `inherited` (_inherit_toolchain.sh) and `payloads` (the same without config and subos), made under the system's temporary directory (glibc refuses an install directory over 255 bytes, which a case's own directory exceeds); case_home, shared_home, newest_payload, payload_root, family_version (and llvm_version, gcc_version, musl_gcc_version, mingw_cross_version: _toolchain_env.sh), repository, listing, newest_file, write_executable, run, has_line_nocase, last_line, last_starting, count_matching. Every assertion is kept. Where a reading changed: - `mcpp run 2>&1 | tail -1` is read as the last line of the program's standard output (the shell's interleaving put the program's line last). - 168: `file`'s "statically linked" and readelf's interpreter are read from the ELF header: no PT_INTERP and no DT_NEEDED. - 802: readelf's NEEDED is read from the header; 804: the .comment strings are read from the file's bytes (`clang version <llvm version>`, `GCC: (...) 16.1.0`), the llvm version being the one _toolchain_env.sh chooses (23.1.3 here, the literal the script grepped). - 310: `strings -a` and `nm -C` become the file's bytes and `nm -C` when PATH has it. - 720, 877, 903: python3 read JSON for the scripts; the cases read it with nlohmann::json and no longer require python3. 1011 required python3 and did not use it; the case does not require it. - 600, 605, 612, 626, 1036 and the other `mcpp new` fixtures keep `mcpp new`; fixtures the scripts wrote whole are written whole. - 698's Windows long-path reading runs powershell where the script did. * e2e: the deps subsystem as C++ cases (49 scripts) The 49 merge scripts MIGRATION.tsv assigns to `deps` (path, git and index dependencies, how a dependency is named and resolved, mcpp.lock, tool packages a graph declares twice, floors a dependency states, patches and `embed`, entry targets, the advice an edge states) are 49 cases of the program e2e/deps (tests/e2e/deps/*.cpp, 12 files: advice, edges, entry, floors, git, graph, identity, lock, names, path, resolve, tools). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 09 12 24 27 31 32 33 61 62 63 79 161 162 163 165 196 203 326 603 627 628 629 661 679 682 713 816 821 822 844 939 943 944 945 946 948 980 990 1001 1002 1018 1027 1032 1046 1048 1060 1066 1071 1074. No workflow runs any of them by path; all 49 are removed. 23_remove_update.sh is a sink row and stays for the sink step. Old run (the scripts, against this branch's mcpp, Linux x86_64): 49 passed. New run (`mcpp test e2e/deps`): 49 passed, 0 failed, 0 skipped. Every assertion is kept. Where a reading changed: - python3 read JSON or rewrote a lock for 679, 682, 844, 1002, 1027 and 1071; the cases do it in C++ (nlohmann::json, the FNV-1a digest, the lock entry's lines) and 844, 939 (which did not use it) and 1071 no longer require python3. 844's os.utime is std::filesystem::last_write_time. - 628 read `mcpp why toolchain --format json` with jq only when jq was on PATH; the case always reads it. - `ls -1 <store> | wc -l` (627) counts the store's own entries; `touch -d '30 days ago'` (945) sets the time 30 days back. - 822 took its own release from `mcpp --version | awk '{print $2}'`, the case from the second word of that line. - 816's `LC_ALL=C grep $'\r'` and `$'\x1b'` are byte searches of the output. - 713 skips when git is missing, as the script did (exit 77); the other git cases skip the same way. - Diagnostics the scripts printed on failure (`<(find ...)`, `diff`) are not kept; the failing comparison shows both sides instead. - Homes the scripts made with `MCPP_HOME=$TMP/home` (627, 628, 629) are fresh case homes; `_inherit_toolchain.sh` homes are `home::inherited`. docs/specs and docs/06 still name 163, 603, 627, 628 and 679 as the tests of their rules; this change does not touch docs/. * e2e: the actions subsystem as C++ cases (37 scripts) The 37 scripts MIGRATION.tsv assigns to `actions` (actions a build program declares and their roles, checks and their stamps, `prepare`, generated files, device sources and the actions that consume them, rule packages, project build hooks, what an action writes and reads, action progress) are 37 cases of the program e2e/actions (tests/e2e/actions/*.cpp, 10 files: checks, commands, declared, devices, generated, hooks, outputs, prepare, progress, rules). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 51 107 124 142 188 313 314 315 317 606 607 608 609 610 611 613 619 623 630 631 648 659 660 780 790 791 792 793 798 799 808 827 896 897 982 1013 1015. No workflow runs any of them by path; all 37 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 36 passed, 1 failed (790). New run (`mcpp test e2e/actions`): 36 passed, 1 failed (790), 0 skipped. 790 fails on this host in both runs for the reason 779 does (batch 2): its install.sh links a shared library with the `gcc` PATH names, an xlings subos gcc whose objects record NEEDED libc.so, a linker script the loader cannot load ("invalid ELF header" when the program runs, under the script); under `mcpp test` PATH names a subos gcc that does not find crti.o. tests/support gains archive_members and archive_symbols: an `ar` archive's members and its symbol index, read by this process (System V/GNU and BSD). Every assertion is kept. Where a reading changed: - 608's `ar t` and `nm` are read from the archive (its members and its symbol index); the script read nm only when PATH had it. - 606 read `mcpp why toolchain --format json` with jq; 808, 897 and 982 read JSON with python3 without requiring it. The cases use nlohmann::json. - 896's mtime in nanoseconds (python3) and 142/188/660's `stat %Y` are std::filesystem::last_write_time; 827's `find -newer <ref>` compares the destination's time with the one it had. - 1013 and 1015 keep their Python drivers: the actions they declare run Python workers, and 1013 drives the build through a terminal and a process group. Each case writes its script's driver unchanged and runs python3 on it; both keep requiring python3. - 317's Ctrl-C leg sends SIGINT through `sh -c` as the script did (Unix only, as before). - 659's `type -P true` is the `true` PATH names; 799's `pwd -P` is the canonical path. - `mcpp run 2>&1 | grep '^X=' | tail -1` is the last such line of the program's standard output. * e2e: the features subsystem as C++ cases (37 scripts) The 37 scripts MIGRATION.tsv assigns to `features` (feature activation, the --strict schema gate, feature defines and their propagation, capabilities and their providers, feature-deps, target-conditional configuration, feature-gated sources, default features and forwarding, layer predicates) are 37 cases of the program e2e/features (tests/e2e/features/*.cpp, 5 files: activation, conditional, defaults, layers, sources). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 67 71 80 81 82 83 85 86 100 106 108 119 126 127 128 138 146 149 195 247 308 328 601 615 637 654 677 710 711 714 739 813 895 1007 1012 1019 1045. No workflow runs any of them by path; all 37 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 37 passed. New run (`mcpp test e2e/features`): 37 passed, 0 failed, 0 skipped. Every assertion is kept. Where a reading changed: - python3 and jq readings of JSON are nlohmann::json. - 108 reads the host architecture from the compiler's predefined macros instead of `uname -m`. - 739's shell glob over the whole failed build (`*"sdk.h"*"No such file"*`) is the same search: "sdk.h", then one of the three phrases after it. - 813's `grep -rl` over the home's std-module records reads the records in sorted order, so "the first record holding the flag" is one record on every run (the script took find's order). - sed's line edits (1012, 711, 813) are the same edits on the manifest text; a fixture line an edit needs and does not find fails the case. * e2e: the pack subsystem as C++ cases (37 scripts) The 37 scripts MIGRATION.tsv assigns to `pack` (the packing modes and the closures they carry, library packages and the gate a prebuilt package keeps, formats a package provides, the report and the axes of a package, packs over workspace members) are 37 cases of the program e2e/pack (tests/e2e/pack/*.cpp, 4 files: formats, library, members, modes). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 30 215 216 242 243 244 245 249 250 251 252 253 254 261 264 265 638 649 652 662 669 689 722 723 801 828 829 841 867 868 869 870 871 954 1008 1043 1061. No workflow runs any of them by path; all 37 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 36 passed, 1 failed (669). New run (`mcpp test e2e/pack`): 36 passed, 1 failed (669), 0 skipped. 669 fails on this host in both runs at the step 779 and 790 fail at (batches 2 and 4): it links a prebuilt library with the `g++` PATH names. Under the script that is an xlings subos g++ whose library records NEEDED libc.so, so the pack refuses it; under `mcpp test` the subos g++ does not find crti.o. tests/support gains readers for what the scripts asked of binutils, tar, zip and python3: ELF sections and symbol tables (defined or all), DT_SONAME and whether a search path is DT_RPATH or DT_RUNPATH; a tar archive's members and their extraction (by the machine's tar); a zip's central directory; SHA-256. Every assertion is kept. Where a reading changed: - `file`, readelf, nm and the scripts' python3 ELF readers (read_tag, has_symtab, elf_facts) are read from the files by these readers; 243's `ar t` is read from the archive (the script read it when ar worked). - python3 readings of JSON (723, 841, 869, 870, 954) are nlohmann::json; 954's tarfile/zipfile/hashlib readings are tar's member list, the zip central directory and SHA-256. - 871 compares compile intervals on the .ninja_log's absolute clock (each edge ends at its output's recorded mtime and begins its duration earlier), allowing 50 ms for the write that precedes a compiler's exit. `mcpp test` runs ninja twice (the program, then the tests) and each run appends with its own relative clock, so the script's comparison of the relative columns paired edges of different runs; on this host its awk compared them as strings and found nothing either way. Two compiles run at once here overlap by about 300 ms. - `find | head -1` is the first path in sorted order. - sed edits of a manifest (244's abi tag, 828, 1043, 1061) are the same edits on its text; 244's runs per line, as sed's does. - `cp -r` copies (638) keep links as links. * e2e: the build subsystem as C++ cases (45 scripts) The 45 scripts MIGRATION.tsv assigns to `build` (a new project built and run, incremental rebuilds, libraries, convention mode, C and assembly sources, profiles, package-owned and per-target flags, include directories, response files and link inputs, the C driver, link flags as written, paths with spaces or no UTF-8 spelling, serial and concurrent plans) are 45 cases of the program e2e/build (tests/e2e/build/*.cpp, 4 files: basics, flags, link, sources). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 02 04 07 08 25 26 50 54 68 70 73 87 104 105 109 115 116 117 123 141 148 179 190 191 238 246 316 620 765 776 777 795 861 882 894 907 909 929 938 991 1003 1006 1047 1054 1062. No workflow runs any of them by path; all 45 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 45 passed. New run (`mcpp test e2e/build`): 45 passed, 0 failed, 0 skipped (907 passed on its rerun after a fixture edit in the case was corrected). Every assertion is kept. Where a reading changed: - `file`, readelf, nm (238's `nm -D --defined-only`), `ar t` (07) and `strings -a` (929) are read from the files by tests/support's readers; 929's `strings` is the printable runs of four or more bytes, as strings prints them. - python3 readings of JSON (765, 894) are nlohmann::json; 148's python3 fixture generators are C++ that writes the same files. - `sleep 1; touch f` (04, 104) moves the file's time two seconds on. - 938 starts the program from a thread of the case while it relinks, as the script did from a background subshell. - `find | head -1` is the first path in sorted order. * cfg: interface, the kernel-abi layer's provided interfaces A layer key answered by membership in the resolved kernel-abi provider's [kernel-abi] provides-interfaces, as accelerator is a set. A provider that states no list provides every interface; a graph without the layer provides none. A package selects its sources by what the implementation beneath it provides and names no implementation. The provider lookup is one function, shared with the requires-interfaces check. * design: toolchain payloads as data (section 16) * design: gcc 16.2.0 and the 17 trunk snapshot (section 16.4) * toolchain: payloads as data, and any package of the index as a toolchain Which package a family's toolchain is for a target on a host is the [[toolchain.payload]] table of data/builtin.toml (name, aliases, family, serves, hosts with exclusion, frontend directory and names, builds-for, glibc-fixup, family-version), and [[toolchain.offer]] what a host is offered. mcpp.toolchain.payloads reads both directions: to_xim_package, identify_xim_payload, available_toolchain_indexes and the payload names normalize_spec accepts. No payload is named in compat, and registry names none. xim:<name>@<version> accepts any package: it is installed and resolved as path:<root> with the table its .mcpp-toolchain.json states (schema 2: family, prefix, sysroot, tools, each inside the payload); the family is the descriptor's or its C++ driver's. check_payload_names.py counts the table's payload names in the sources against a baseline, as check_system_names.py counts systems; the refactor lowers the system-name baseline. * docs: the payload table, a family's versions and trunk snapshots, any package as a toolchain * design: workspace concurrency, the build directory named by its lineage, and its size (section 17) * build directory named by its lineage; lock wait in Finished; split debug in clean --report; debug = "lines" A configuration's build directory is named by its lineage (compiler family, target, standard library family, standard, configuration flags). The evolving parts (mcpp, compiler build, standard library version, runtime contract) are recorded inside it. An upgrade keeps every path the commands name, so ninja compiles only what the release changed. Before, the directory took the whole fingerprint's name, and its takeover changed every member's commands. A compiler build that differs empties the directory in place. Finished states the time spent waiting for a build directory. clean --report gives split debug information its own column. [profile] debug = "lines" keeps the line tables alone (-g1). Design 2026-11-12 section 17. * the graph runs mcpp through the build directory's link to it A build directory's graph names mcpp as engine/epoch-<N>/mcpp, a hard link (else a copy) the planner makes the running engine under the directory's lock. mcpp's own path holds its version under xlings, and every command that ran it -- the capped compile, BMI comparison, link wrapping, staging, module bindings -- changed with each release. Measured on a two-member workspace, an engine that differs in its version alone: 18 of 153 objects and BMIs compiled again before, none after. The link's path carries kCacheEpoch. Fast-path admission accepts a link to this engine; a self-contained home is stated for the programs mcpp starts, since it cannot be derived from the link's place. In-flight compiler records are dropped when another engine wrote them. * tests: the build directory's lineage, its engine link and an upgrade that compiles nothing e2e 1000 gains G-I: the directory named by its lineage and recording the fingerprint, the graph running mcpp through engine/epoch-<N>/mcpp, the same mcpp from another path compiling nothing, and a compiler change emptying the directory in place. 878 states that the program a graph runs is the engine that wrote it. 960 and the accel cases read the new configuration-change warning. unit/test_graph_engine covers the link. * design: workspace test feedback, mcpp#927 (section 18) * e2e: the cli subsystem as C++ cases (17 scripts) The 17 scripts MIGRATION.tsv assigns to `cli` and that migrate here (--help and --version, the error paths of a manifest and a source, usage errors kept off standard output, the exit-code contract, --jobs and the `--` boundary, --toolchain on run, test and pack, package templates and a transactional scaffold, a build's output and status streams, a terminal's status row, the control contract, and mcpp under signals) are 17 cases of the program e2e/cli (tests/e2e/cli/*.cpp, 5 files: commands, output, signals, templates, terminal). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags, and 936's `# resources: machine` its resource. Scripts: 01 05 48 69 202 204 231 329 340 671 842 843 864 866 936 941 1016. No workflow runs any of them by path; all 17 are removed. 845 (--play-game restores the terminal) is kept as a script: its case finds the prepare action alive three seconds after a Ctrl-C ended mcpp, which criterion G3 forbids, while the script's own G3 check compares a directory name with pgrep's list of pids and cannot fail. MIGRATION.tsv records it; it waits for the engine owner. Old run (the 18 scripts, against this branch's mcpp, Linux x86_64): 18 passed. New run (`mcpp test e2e/cli`): 17 passed, 0 failed, 0 skipped. tests/support gains `process`: a program held by its pid, signalled alone, whose standard streams are a pseudo-terminal, one pipe for both streams, a file, /dev/null or a pipe whose reader has ended, and which starts with every signal at its default; `processes()` reads /proc; `strip_escapes` removes CSI sequences. Every assertion is kept. Where a reading changed: - `2>&1 | cat` and `2>&1 | tee` (864, 866) are both standard streams of mcpp on one pipe of `process`; the shell's `&`, `kill` and `wait` (340, 936 H, 1016) and Python's subprocess with `restore_signals` (941) are `process`; 941 B's `returncode == -SIGPIPE` is the signal `process` reports. - Python's pseudo-terminals (843, 866 D) are `process` with a terminal; 843's screen model is C++ with the same rules (carriage return, line feed, CSI A, J, K), over the code points of the UTF-8 the terminal received. - 842 R5's Python timer reads the pipe in 50 ms steps and takes the time at which the `error: build failed` line first appears and at which mcpp ends. - python3 readings of JSON (866 B, 936) are nlohmann::json; 936's `normcase(normpath(...))` comparisons are `lexically_normal` path comparisons, which do not fold case on Windows. - 69's `sed | grep -oE '"targets":[...]'` is a regular expression over the log after `\"` is unescaped; 01's awk reading of mcpp.toml is a regular expression. - `sleep 1; touch mark; find -newer mark` (231) is the clock's time taken after a second's wait, compared with each object's write time. - 05's build with no manifest runs in a directory outside the repository, and 202 runs in a project of its own: a case's directory lies inside the repository, whose mcpp.toml mcpp would find, and `mcpp pack --pkg-format bogus` builds the project it finds before it refuses the value (the script, run from tests/e2e, built the repository). * e2e: the modules subsystem as C++ cases (30 scripts) The 30 scripts MIGRATION.tsv assigns to `modules` (several modules and partitions built and run, the p1689 scanner and ninja's dyndep, module units in .cpp files, transitive and indirect imports, the std module compiled once, what the scanner reads, `[build] module_extensions` and the lib root that follows it, BMIs and their cache and staging, the split schedule, and modules across programs, members and homes) are 30 cases of the program e2e/modules (tests/e2e/modules/*.cpp, 5 files: basics, bmi, extensions, programs, scanner). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags, and the `# resources: machine` of 170, 234 and 1039 their resource. Scripts: 03 19 20 21 49 113 118 167 170 171 217 218 234 236 260 263 634 635 636 639 807 847 849 857 858 898 924 940 1039 1064. No workflow runs any of them by path; all 30 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 29 passed, 1 failed. 218 fails on its reading alone: it looks for the graph in `target/*/<fingerprint>/build.ninja`, and the build directory is named by the configuration's lineage since this branch's engine (2da705cd); with that one line reading the lineage instead, the script passes. New run (`mcpp test e2e/modules`): 30 passed, 0 failed, 0 skipped. tests/support gains `another_home` (a further home of a case, for a script that exported MCPP_HOME again) and `file_identity` (`stat -c '%i %Y %s'`). Every assertion is kept. Where a reading changed: - 218 finds the graph in the directory `--print-fingerprint` names as `Lineage:`; its assertion that module_extensions leaves the fingerprint unchanged is kept. - python3 readings (924's module maps, 1039's newest std-module record, 49's entry.json) are C++ and nlohmann::json; 234's inline replay check is C++ (write times and tests/support's sha256), and its sections 6, 8 and 9 run tests/e2e/helpers' Python checks as the script did. - 1039's two background builds are two threads of the case, each running mcpp. 1039 is a case of bmi beside 170 and 171: it removes the shared home's std entry they stage from, and its `machine` resource keeps them apart only within one program (in another program, 171 found the entry gone); 858's `export MCPP_HOME` to a second and a third home is `another_home`. - 171's `stat -c '%i %Y %s'` is `file_identity`, `chmod 444` is a permission change, `cmp` a comparison of the bytes, and its Windows holder the same PowerShell command run as an `e2e::session`; 170's `cmp` likewise. - `touch` of an existing file (170, 171, 234, 858) sets its write time to now; `sleep 1; touch marker; find -newer marker` (218, 847) is the clock's time taken before a second's wait, compared with the file's write time. - `ls -d target/x86_64-linux-*/` and `./target/*/*/bin/app` (20, 21, 849) are globs; `[ -d "$LLVM_ROOT" ]` (118, 217) is the llvm payload's directory; 635's `head -c 3 | od` compares the first three bytes; 639's `printf '%b'` sources are C++ string literals. - The commands that name no project (858's cache commands, 171's `mcpp stage`) run in a directory outside the repository, as the scripts' did: a case's directory lies inside the repository, whose mcpp.toml mcpp would find. * a toolchain named with --toolchain opts a planned row in The planned refusal says no toolchain is published for the row; a build that names one on the command line has one, as a build whose [target.<row>] section names one does. MCPP_TOOLCHAIN, a standing preference, does not opt in. A platform's conformance run (openkal-platform's i686-linux-musl row) named its toolchain with --toolchain and was refused. * the planned opt-in counts MCPP_TOOLCHAIN; a carrier reads its row's object format and suffix --toolchain is stated as MCPP_TOOLCHAIN, so the two are one statement and both opt a planned row in. The carrier of a system a platform defines took its section directive and program suffix from the system's name: a PE row such as x86_64-uefi was assembled as ELF and named without .efi. Both now come from the row (Triple::object_format, artifact_naming). * cenv answers by the row's container; cfg(object-format = ...) selects by it * tests: case directories under the system's temporary directory A case made its files in mtest::temp_dir(), which `mcpp test` places under the repository's target/ (test-protocol/<run>/<case>). mcpp run there in a directory with no manifest of its own found the repository's mcpp.toml above it and built the repository: one such run took over the released engine's build directory. tests/support gains `case_dir()`: the directory of the case under the system's temporary directory (`mcpp-e2e-case-XXXXXXXX`), made on first use. `project` and `project::scaffold` make their files there, and the cases of the earlier subsystems use it in place of mtest::temp_dir() (222 uses in 54 files; no case depended on the old location). The case's homes and its directory are filed under the runner's directory of the case, which mtest removes when the case ends: a directory whose case has ended is removed at the next use of the support, and the rest when the program ends. * e2e: the cache subsystem as C++ cases (22 scripts) The 22 scripts MIGRATION.tsv assigns to `cache` (the global build cache across projects and keyed by what an entry was compiled against, its modes and maintenance commands, an entry's layout and a hit's std BMI, the fast path and what it compares, a configuration's build directory over time, `clean --stale` and `clean --report`, the sweeps by age and by budget, and what a build writes once) are 22 cases of the program e2e/cache (tests/e2e/cache/*.cpp, 3 files: entries, fastpath, directories). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. No script declared a resource. Scripts: 172 173 174 184 212 324 609 645 832 878 879 935 959 998 999 1000 1005 1022 1028 1029 1030 1031. No workflow runs any of them by path; all 22 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 20 passed, 2 failed. 174 fails on an engine defect of this branch: `mcpp build --cache=off` empties the build directory after the plan made its link to the engine (`engine/epoch-<N>/mcpp`, design 2026-11-12 §17), and every step then fails with "not found" (a project, `mcpp build`, then `mcpp build --cache=off`). 832 failed once with a SIGSEGV of mcpp in nlohmann's lexer while resolving the toolchain in the shared home, during a concurrent run; it passed when run again. New run (`mcpp test e2e/cache`): 22 passed (21 ok, 174 an expected failure), 0 failed, 0 skipped. 174 carries `known_gap{"build --cache=off removes the engine link the graph runs", "2026-12-15"}`: every assertion is kept, the run reports it as xfail, and from 2026-12-16 it is judged as any case. Every assertion is kept. Where a reading changed: - python3 readings (174's `cache list --json` and entry.json, 935's result record, 998's programs.json) are nlohmann::json; 174's strip of the `[build] cache` override writes the manifest without it; 1029's os.utime walk sets the write times of the entry's files and directories. - `touch -d '<n> days ago'` and `touch -t 200001010000` (609, 1000, 1005, 998) set write times; 1000's `find -mtime -1` compares the write time with a day ago; 174's `touch -d @1000` sets the time 1000 s after the epoch. - `sleep` is a wait of the case; 935's two background builds are a thread of the case and the case itself, each running mcpp. - 878's and 1000's engine at another path is the program under test copied there and started by its path with the same environment; 878's `grep -v '^engine='` and awk rewrite of `$mcpp`, and 879's awk ageing of `depSources=`, are line rewrites of the record and the graph. - `ar t` (998) is `archive_members`; `stat -c %i` (998) is the inode of `file_identity`; `readelf -S` (998, 999), `readelf --debug-dump` (173) and `gdb` (999) run as the scripts ran them, where the machine has them; git (1028) likewise. - 1022's `cp -a` is a copy of the tree that keeps its links and its write times; 1031's `cp -R` is a recursive copy. - 645's "NOT MEASURED" exit 0 when an unchanged second build is not replayed is a skip naming the recorded request; its machine-default leg without a second installed version ends the case as passed, as the script's exit 0 did. 324's NOTE (the warm build not replayed) printed and asserted nothing, and is left out. - 1029's cache commands, which name no project, run in the case's directory, outside the repository. * a defined row states its link emulation without its machine; design: UEFI converges into openkal (section 19) * design: the measured UEFI result and the interface-selected optional interfaces (section 19) * a provider states the link, base and runner of its identities over either base * a runner named by the environment replaces the requirements of the one it replaces * build: make the graph's engine link again after a cold build clears the directory A cold build (--cache off, --no-cache) removed the output directory after the plan had named its link to the engine, so every step of the graph failed on a mcpp that was not there. The link is made again where the link spec is regenerated. Where the running engine's file was replaced while it ran, the link is copied from the running image. * e2e: cache 174 is judged as any case again `build --cache off` makes the graph's engine link again after the cold build clears the build directory (ac9fba36), so 174 no longer carries `known_gap{"build --cache=off removes the engine link the graph runs", "2026-12-15"}`; its MIGRATION.tsv row drops the gap's note. Every assertion is unchanged. Run (this branch's mcpp, Linux x86_64): - `mcpp test e2e/cache`: 22 passed (174 an ordinary pass), 0 failed, 0 skipped. - `mcpp test e2e/features`: 37 passed, 0 failed, 0 skipped; 739, which failed on the same defect, passes. * e2e: the workspace subsystem as C++ cases (25 scripts) The 25 scripts MIGRATION.tsv assigns to `workspace` (the members a command takes, what a member inherits from its workspace at every position of the graph and however it was fetched, where a workspace's statements are written and what they reach, and the workspace as one graph per configuration) are 25 cases of the program e2e/workspace (tests/e2e/workspace/*.cpp, 4 files: members, inheritance, statements, graph). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. No script declared a resource. Scripts: 35 122 321 770 772 773 774 805 806 833 834 836 839 850 851 852 853 860 872 880 885 893 900 911 912. No workflow runs any of them by path; all 25 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 25 passed, 0 failed, 0 skipped. New run (`mcpp test e2e/workspace`): 25 passed, 0 failed, 0 skipped. tests/support gains `cdb_arguments` (the words of a compile database's entry for a file: its `arguments`, else its `command` split), which 321, 770, 774, 805 and 893 read and the cdb subsystem will. Every assertion is kept. Where a reading changed: - python3 readings of compile_commands.json (321's entry_of, 770's count and before, 774's and 805's checks, 893's release entry) are `cdb_arguments`; 872's comparison of two build databases and its argument-file check are nlohmann::json. 900's tomllib reading of the archived manifest runs as the script ran it (python3 -I), the support having no TOML reader. - 805's `ls xim-x-gcc | sort -V | tail -1` of the case's inherited home is `newest_payload("xim-x-gcc")`, the shared home's newest gcc, which the inherited home links. - `tar -xzf -O` (772, 900) is an extraction into the case's directory and a read; `cmp` and `sha256sum` (772) compare bytes and `sha256`. - `.ninja_log` readings (833's grep -c and tail -n +N, 851's and 852's awk, 872's compiles_after, 912's count) are `ninja_log_outputs`, counted from the number of outputs recorded before; 860's start and end columns are the log's rows read whole. - `find target -mindepth 2 -maxdepth 2 -type d ! -name '.*'` (833) is a glob of `*/*` kept to directories; `find -path '*/bin/*' -name` (850, 872, 880, 893) is the first file of that name under a `bin` directory; 872's newest release `.ninja_log` is the graph directory beside the dev one. - 834's wall time measured by a python3 wrapper is the outcome's duration. - 885's awk extraction of a ninja build statement and its indented lines is a line walk of build.ninja; its `gsub(...) > 1` per line is a count per line. - 880's and 885's home with `[xlings] home` naming the shared registry is a fresh home of the case holding that config.toml. - `sleep 1.1` (833) is a wait of the case; `touch` (839) sets the write time to now; 850's and 880's `sed` rewrites of a manifest are string rewrites. - 772's released client ($MCPP_BOOT) runs when the variable names a released mcpp other than this one, as the script's did; otherwise that leg is left out, as the script's SKIP line printed and asserted nothing. * e2e: the run subsystem as C++ cases (19 scripts) The 19 scripts MIGRATION.tsv assigns to `run` (the environment `run` and `test` give a program, the runner of a target and its named and feature-chosen runners, what `run` hands back of the program, and what a runner receives) are 19 cases of the program e2e/run (tests/e2e/run/*.cpp, 4 files: environment, runners, program, contract). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. No script declared a resource; 927 carries `resource{"machine"}`, since it appends to the shared home's config.toml for one run and restores it, and it is the only case of the subsystem that needs the machine. Scripts: 62 65 74 114 156 166 330 333 337 339 656 672 674 684 862 863 883 927 1024. No workflow runs any of them by path; all 19 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 18 passed, 1 failed. 62 fails on this machine's host `gcc` (the xlings shim, gcc 16.1.0 built from source): the plugin it links with `gcc -shared` records DT_NEEDED `libc.so` (the linker script's name), so the program's dlopen() reaches the private glibc's `libc.so` and fails with "invalid ELF header"; the case fails the same way. An environment failure, not an engine one. New run (`mcpp test e2e/run`): 18 passed, 1 failed (62, as above), 0 skipped. Every assertion is kept. Where a reading changed: - `ls target | head -1` (330, 339, 656, 672, 674) is the first entry of target/ whose name does not start with a dot, as `ls` lists them. - python3 readings (333's launch description and SBOM, 1024's descriptions, record rewrite and NDJSON stream) are nlohmann::json. - 883's drive.py (pty.fork) is `process` on a pseudo-terminal of a new session; its subprocess return codes of a signal death (negative in Python) are the shell status and the signal `process` reports; its check that the terminated program is gone (`os.kill(pid, 0)`) is the absence of /proc/<pid>; the piped run is the command's input. - `dd ... seek=18 conv=notrunc` and `od -j18 -N2` (330) write and read the two bytes at offset 18; `chmod -x` / `+x` (863) remove and restore the execute permissions; `file`'s interpreter (74) is the ELF header's PT_INTERP; `realpath` (62) is the canonical path. - `cmp` of captured streams (862) compares the outcome's standard output and standard error with the program's own; `head -c`, `tail -c`, `tail -n 2` and the line after `Running` are string prefixes, suffixes and the following line. - `sed -i` rewrites of a manifest or a lock (330, 333, 339, 674) write the rewritten text, or the text kept before the append; 337's `awk` of `features=` in .build_cache is the last such line, the record holding one. - `uname -m` (684, 927) runs as the scripts ran it; 927's PATH prefix and RECORD are the run's environment; 166's look at `$HOME/.mcpp` is kept. - `touch` (114, 330) sets the write time to now; `sleep 1.1` (114) is a wait of the case; `$TMP/runner.log` truncation (330) truncates the file. * e2e: the cdb subsystem as C++ cases, first part (8 scripts) Eight of the scripts MIGRATION.tsv assigns to `cdb` (the root compile database a build writes and when, `build --configure-only`, and `emit build-database`'s S1 document) are 8 cases of the program e2e/cdb (tests/e2e/cdb/*.cpp, 3 files: root, configure, emit). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. No script declared a resource. The other 15 scripts of the subsystem are still scripts: 11 to follow, and 783 784 785 786, which ci-linux-e2e.yml's llvm step runs by path and holds to the lines they print. Scripts: 47 76 77 211 688 781 782 937. No workflow runs any of them by path; all 8 are removed. Old run (the 23 scripts of the subsystem, against this branch's mcpp, Linux x86_64): 23 passed, 0 failed, 0 skipped. New run (`mcpp test e2e/cdb`): 8 passed, 0 failed, 0 skipped. Every assertion is kept. Where a reading changed: - jq (47) and python3 readings of the databases and envelopes (76, 211, 688) are nlohmann::json; 47's skip when jq is absent is gone, the reading needing nothing of the machine. 688's schema check runs tests/e2e/_json_schema_subset.py with python3 -I, as the script ran it. - 688's tree_digest (os.walk and hashlib) is the case's listing of the tree and `sha256` of each file; its WORK_DIR (the planning pass's directory) is removed by the case, as the script's trap removed it; its cold home is the case's inherited home; `cp -R` is a recursive copy. - 781's `touch .before-build2; sleep 1; find -newer` is the clock's time taken before a second's wait, compared with each graph's write time; `diff` and `cmp` (781, 937) compare the files' text. - 937's `grep -v '^compile_commands = '` rewrite leaves `[build]` without the key, as the script's did; `unset MCPP_COMPILE_COMMANDS` removes the variable from each run. - `touch` (782, 937) sets the write time to now; `printf >>` appends. * e2e: the cdb subsystem as C++ cases, second part (11 scripts; 4 kept) The other scripts MIGRATION.tsv assigns to `cdb` (what `emit build-database` describes when planning fails or defers, the words of a flag, what a rule generates, the private work directory, the sets of a workspace's database and of the build programs, and what emit watches) are 11 further cases of the program e2e/cdb, now 19 cases in 5 files (root, configure, emit, planning, sets). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. No script declared a resource. Scripts: 731 736 787 788 789 809 815 817 840 913 921. No workflow runs any of them by path; all 11 are removed. Kept: 783 784 785 786. ci-linux-e2e.yml's llvm step ("Compile database e2e that needs llvm") runs each by path and holds it to the lines it prints, since no e2e shard has llvm; they move when that step runs the e2e/cdb cases instead. MIGRATION.tsv marks them `keep` with that reason, and keep.tsv lists them. Old run (the 23 scripts of the subsystem, against this branch's mcpp, Linux x86_64): 23 passed, 0 failed, 0 skipped. New run (`mcpp test e2e/cdb`): 19 passed, 0 failed, 0 skipped. Every assertion is kept. Where a reading changed: - python3 readings of envelopes, databases and compile_commands.json (736, 787, 788, 789, 809, 815, 817, 840's check.py and root.py, 913, 921) are nlohmann::json and `cdb_arguments`; 736's walk of every list of strings is a recursive walk of the document. - 817's tree_digest is the case's listing and `sha256`; its config.toml rewrite (`grep -v '^binary' | sed '/^\[xlings\]/d'`, then [xlings] binary) is the same line filter; its xlings stub is written as the script wrote it. - 921's os.utime(ns=10**18) sets that time on each host lock through the file clock; the comparison of time and text is kept. - 913's `find -maxdepth 2` is a relative depth of at most two; its realpath comparison is `weakly_canonical`. - 731's pipeline (`mcpp emit ... | { readlink /proc/self/fd/0; cat; }`) runs as the script's shell ran it, by `sh -c`: the reader's pipe is a property of that pipeline. - `touch` (736) sets the write time to now; `printf >>` appends. * e2e: the cdb subsystem as C++ cases, third part (4 scripts; the llvm step runs the cases) The four scripts kept for ci-linux-e2e.yml's llvm step (one database per configuration, the database replayed from its directory, a module interface's language, and the std unit with its emit agreement, provides and build-id) are 4 cases of the program e2e/cdb, in a new suite `toolchains` (tests/e2e/cdb/toolchains.cpp); e2e/cdb is now 23 cases in 6 files. Each case is named by its script's sentence and keeps its number. No script declared a resource. Scripts: 783 784 785 786, all removed. MIGRATION.tsv marks them merged; keep.tsv no longer lists them. The hermetic job's step "Compile database e2e that needs llvm" now runs `"$MCPP_BOOT" test e2e/cdb --case toolchains` with the PATH it set before and the llvm the job installed before, and holds the run to the `ok` line of each of the four cases and to "4 passed, 0 failed, 0 skipped": a skip or a failure fails the step, as a missing PASS line did. The bootstrap drives the test so that the test build reuses the step "Build PR mcpp from source"; the mcpp under test is that build's binary (MCPP_BIN_MCPP). The step no longer installs python3, which no case reads with. This step is unverified in CI. Old run (783-786, against this branch's mcpp, Linux x86_64): 4 passed, 0 failed, 0 skipped. New run (`mcpp test e2e/cdb`): 23 passed, 0 failed, 0 skipped (`--case toolchains`: 4 passed). Every assertion is kept; the lines the step required are each a case's assertions. Where a reading changed: - The capability words: 783 and 784 declared `gcc`, which the probe reads as g++ on PATH; they run no g++ of the PATH but the home's gcc@16.1.0 toolchain, and the hermetic container that runs them has no g++ on PATH (it ran them by path, where the word was not read). 786 declared `python3`, which only its readings used. The four cases require `llvm`. - python3 readings of compile_commands.json and of the S1 document are nlohmann::json; 784's and 785's subprocess replays of an entry's arguments in its directory are `e2e::run` with that argv and directory; 784's commonpath test is a relative path from the canonical project that does not leave it. - 786's `list(ide.toolchains.values())[0]` is the first toolchain in the document's key order, the document having one. * e2e: the index subsystem as C++ cases (34 scripts) The 34 scripts MIGRATION.tsv assigns to `index` (what the index commands state and the [indices] forms, a project's local path index and a home's own index, the routes an index takes into a build, when an index is refreshed and what a failed resolution says, and what a project asks of xlings) are 34 cases of the program e2e/index (tests/e2e/index/*.cpp, 5 files: listing, local, routes, refresh, xlings). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. 614 declared `resources: machine` and carries resource{"machine"}; it is the one case of the subsystem that does, and mtest serialises that resource only within one test program, which `mcpp test e2e/index` runs per file. Scripts: 06 11 22 42 43 44 52 53 60 75 76 93 120 121 147 169 173 185 192 210 327 331 335 614 730 732 733 734 735 737 803 812 949 1067. No workflow runs any of them by path (two workflow comments name 53); all 34 are removed. The index 173 is this subsystem's; the cache 173 was migrated before. Sinks: MIGRATION.tsv marked 06, 42, 43, 44 and 93 to sink into unit tests (test_emit_xpkg, test_indices, test_lockfile, test_xpkg_parse). No earlier batch sank a script into a unit, and each of the five asserts a command's output or file effect (`emit xpkg` and its --output, `index list`, `index pin` and `index unpin` rewriting mcpp.toml, `xpkg parse` and its --json and --format json), which the unit layer reaches only through the parsers. They are e2e cases of e2e/index::listing; their rows say so. Old run (the 34 scripts, against this branch's mcpp, Linux x86_64): 34 passed, 0 failed, 0 skipped. New run (`mcpp test e2e/index`): 34 passed, 0 failed, 0 skipped. Every assertion is kept, including those on files a script wrote itself (43's and 44's lockfiles). Where a reading changed: - python3 readings (93's parse documents, 733's effects and codes, 735's first diagnostic, 803's FNV-1a of the declared address) are nlohmann::json and a 64-bit FNV-1a loop; 733's `grep -w` stays a word match over each effect and each code. - `touch -d '30 days ago'` (173, 949) sets the write time thirty days back. - Line rewrites keep their filters: 733's, 734's and 812's config.toml (`grep -v '^binary'`, `sed '/^\[xlings\]/d'`, 734 also `^auto_refresh` and `^\[index\]$`, then [xlings] binary), 22's repo line, 169's and 735's dependency lines. The xlings stubs (52, 60, 732, 733, 734, 812) are written as the scripts wrote them; 812's unquoted heredoc is its text with the index directory substituted. - 732's background mcpp and `kill -TERM` are a process of the case and its signal; `kill -0` runs by `sh -c`; `bounded 60` is the command's 60 s timeout; `date +%s` is the steady clock. - 185's `grep -A 4 "<first 40 characters of the last error>"` reads those characters as a fixed string (grep read them as a basic expression) and looks at each match and the four lines after it. - `find` counts and searches (120's build.ninja outside .build-mcpp, 147's accel_*.o, 737's ld-linux-* loaders) are the case's directory walks and globs; 614's `ls | sort -V -r` orders the versions by numeric components. - 737's `mv a b` into an existing directory b keeps that meaning (the second shaping moves the payload into the first one's directory). - 22's cold home and its inherited home are two homes of the case (the script inherited into the cold one after its checks); `cache dir` is compared with the second. 52's fake registry links the shared home's payloads and 803's home links the shared home's registry, where the scripts named $HOME/.mcpp. - 1067's `cmp -s` is a byte comparison and its `grep borrower` looks for the borrowing home's directory name. - Skips: 730's SKIP (exit 77, no compat descriptor) and 614's SKIP lines (which exited 0 and passed) are skips of the case naming the same reason. - 06's lua check runs where lua is on PATH, as the script's did; 22's git runs as the script ran it. * e2e: the self subsystem as C++ cases (17 scripts) The 17 scripts MIGRATION.tsv assigns to `self` to merge (a home's configuration and its index migration, a project's xlings environment, a SubOS without its description, the doctor offline, [index.repos.<name>] in an existing home, the shell's active SubOS, the vendored xlings, a home selection, the doctor's resource checks, a SubOS selection that is root-local, a suggested command's quoting, and a home at a spaced or non-ASCII path) are 17 cases of the program e2e/self (tests/e2e/self/*.cpp, 4 files: config, subos, machine, homes). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. 205 and 931 declared `resources: machine`; they carry resource{"machine"} and are the two cases of machine.cpp, since mtest serialises that resource only within one test program and `mcpp test e2e/self` runs each file as one. Scripts: 10 38 39 46 88 151 205 221 237 604 681 686 687 919 920 931 932, all removed, with tests/e2e/_home_selection.py and _doctor_network.py, which only 919 and 920 ran. No workflow runs any of them by path. Kept (unchanged, keep.tsv): 27 30 846 914 915 916 917 918, machine state and upgrades of a home, as MIGRATION.tsv states. tests/support gains `local_server` and `connection`: a TCP server on 127.0.0.1 at a port the system chooses, each connection handled on its own thread (POSIX; constructing one on Windows fails the case). 920 uses it for the HTTP resource that refuses, stalls and trickles and for the TLS peer that refuses the handshake, which _doctor_network.py served with http.server and socketserver. Old run (the 17 scripts, against this branch's mcpp, Linux x86_64, with MCPP_HOME=~/.mcpp and MCPP_VENDORED_XLINGS set as the e2e job sets them): 17 passed, 0 failed, 0 skipped. New run (`mcpp test e2e/self`): 17 passed, 0 failed, 0 skipped. Every assertion is kept. Where a reading changed: - 919's and 920's Python programs are the cases themselves: subprocess runs are `e2e::run` with the same environment, working directory and timeouts; 919's four concurrent writers are processes of the case; Path.resolve() is weakly_canonical; json readings are nlohmann::json. 919's write-fault leg keeps its two branches (Windows: a read-only locator; POSIX: a 0500 origin, and no proof where the account bypasses directory permissions). - 920's provider is the first of MCPP_NETWORK_PROVIDER, MCPP_VENDORED_XLINGS and XLINGS_BIN that names a file, else the shared home's registry/bin/xlings; without one the case skips naming them (the script raised KeyError, and on a machine whose XLINGS_BIN names the xlings bin directory it failed to start that directory). On Windows the case skips: its oracle is the POSIX server. - python3 readings (151's entry checks, 237's runtime and rewritten description, 681's entry_url, same_json and re.sub rewrites) are nlohmann::json and std::regex with the same patterns. - `cmp -s` (151, 681) is a byte comparison; `cp -R` (205) is a recursive copy keeping links; `ln -s` makes links; `mkdir` of an empty directory (221) is made by writing and removing a file in it. - 205's field11 (`sed -n 's/^[[:space:]]*\[11\][[:space:]]*//p' | tail -1`) is the last such line; 931's `eval` of the suggested command runs it by `bash -c` with the program under test in place of `mcpp`. - 687's MCPP_INHERIT_CONFIG=0 is the inherited home with its copied config.toml removed; its `PATH=/usr/bin:/bin command -v xlings` is a look for xlings in those two directories; `env -u` removes the variable. - 237's MCPP_HOME_REAL default is $HOME/.mcpp, as the script's; 88's and 205's $MCPP_HOME is the shared home. - The python3 word stays a tag and a requirement of 151, 919 and 920, as the scripts declared it. * a compiler without C++ modules refuses the graph's C++ sources before the first compile GCC before 14 has neither -fmodules nor P1689 scanning (-fdeps-format), through which every C++ unit is built; DJGPP's GCC 12.2.0 failed at the first scan with four unrecognised options. The capability now follows the version, and prepare refuses a graph with C++ units on such a compiler, naming it, the first C++ source and the remedy. C units build as before. * an interrupt asks each program to end before ending its unit; waits poll with the lock free Ctrl-C ended ninja's unit outright, which does not reach the commands ninja starts in process groups of their own, and an action outlived mcpp (e2e 845). Each program is now asked to end (terminate), given up to 2 s, and what still runs is then ended with its unit. The waits of a started program poll with the wait lock released between polls: a blocking or timed wait held it, and the request to end the program, which takes it, waited behind the program (an __action-stamp ignored SIGTERM for as long as its action ran). Measured: an interrupted build leaves no action running. * test programs by package, the test summary by segment, an index reader that waits, the size of a directory left beside (mcpp#927) - [test] programs = "package": the files a package discovers are one program, named by the first glob's fixed directory; "directory" notes the files it leaves as programs of their own, and a setting that changes no program is a degradation --strict refuses (2026-11-12 §18 P1). - The test summary states plan, programs, build and run, adding up to the total (§18 P2). - A process that skips the index refresh another holds, and has no index, waits for that refresh instead of reading none (§18 P3). - The configuration-changed warning gives the size of the directory left beside and the command that drops it (§18 P6). * cli: mcpplibs.cmdline 0.0.4; actions return their status and the run dispatches at any depth The adaptation 0.0.1 required is gone: a wrapper that captured each action's status through a shared local, and a dispatch table in each parent command (emit, xpkg, cache, index, self). Each subcommand states its own action; a parent named without a subcommand is answered by cmdline (the subcommands, on standard error, status 2). * design: implementation record for 2026.11.13.1 (§15.3, §18.3, §19.3) * docs: test programs by package, the test summary by segment, the size of a test build; CHANGELOG 2026.11.13.1 * release: 2026.11.13.1 * build: the engine link reads a replaced image where procfs presents it, without naming a system; system-names baseline shrinks * canaries: tests/canary (c, cxx), the matrix with its measured expectations, the nightly workflow (design 2026-11-12 §5.7) * AGENTS.md: the canaries and their matrix * spec test B1.2: programs = "package", and the note of "directory" * test parts named by their path below the program's directory; a test program's other files take part in the object census A package's program holds files of several directories: tests/b.cpp and tests/unit/b.cpp were two parts named tests/b and two compile edges of obj/b.cpp.ddi, which ninja refused. A part is named by its file's path below the common directory of the program's files (the stem in a program of one directory, as before), recorded for the fast path and the archive; the census that disambiguates object paths counts a test program's other files. Unit test: a GCC's modules by its version. * docs: a package's parts by their path; design record * e2e: test programs by package and the note of directory (1081, 1082), the size of a directory left beside (1083), a command without its subcommand, the summary by segment * run: a program that ends during an interrupt ends its unit with it The request asks each program to end and ends the units of those still running after two seconds; a program that ended within them was no longer live, and what it had started in its unit outlived mcpp (the shared compiler cap's cancellation check: the compiler under the driver stayed alive). * e2e: the openkal subsystem as C++ cases (45 scripts; 6 kept beside their cases) The 45 scripts MIGRATION.tsv assigns to `openkal` (the openkal stack as packages, the target side's layers, the target base, ok images, platform packages, target sets and the legs of a pack, and the packages of images) are 45 cases of the program e2e/openkal (tests/e2e/openkal/*.cpp, 8 files: stack, layers, bases, images, machine, platforms, sets, trees). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags (1033 declared none and has none). 951 declared `resources: machine`; it carries resource{"machine"} and is the case of machine.cpp. Files: stack 269 285 286 287 289 1073; layers 305 743 950 994 1056 1059; bases 925 926 930; images 942 947 953 960 996 1009 1033; machine 951; platforms 952 1020 1021 1025 1040 1049 1057; sets 963 964 983 984 985 986 987; trees 955 956 957 961 962 992 995 1078. Removed (39): 269 305 743 925 926 930 942 947 950 951 952 953 955 956 957 960 961 962 963 964 983 984 985 986 987 994 995 996 1009 1020 1021 1025 1040 1049 1056 1057 1059 1073 1078, and tests/e2e/_sysroot_fixture.sh, which only 925 and 930 sourced. Kept beside their cases (6), as batch 2 kept 168 and 804: 285 287 289 (openkal-cross.yml runs them by path and holds them to their lines), 286 (openkal-cross.yml, ci-aarch64-fresh-install.yml and .github/tools/check_published_arm64_cn_ecosystem.sh run it by path), 992 (cross-build-test.yml runs it by path for its wine leg) and 1033 (host-requirements.tsv requires its result on linux, macos and windows, and check_e2e_coverage.py reads that from the scripts' reports). They go when those steps run the cases. MIGRATION.tsv says so in their rows. No workflow, doc or coverage list names a removed script. Old run (the 45 scripts, against this branch's mcpp, Linux x86_64, with MCPP_HOME=~/.mcpp and MCPP_VENDORED_XLINGS set as the e2e job sets them): 43 passed, 2 failed, 0 skipped. 287 failed on this machine's PATH llvm-nm, an xlings shim that cannot load libz.so.1 (with it off PATH the script passes). 925 failed at S2: "the stated base reused the default output directory". New run (`mcpp test e2e/openkal`, suite by suite): 44 passed, 1 failed, 0 skipped. 925 fails at the same assertion; with S2's and S4's directory assertions made non-fatal, those two are the only checks of its 36 that fail. Since 2026.11.13.1 a build directory is named by its lineage and the base is part of the runtime contract, which evolves: a stated sysroot, and a sysroot rewritten in place, take over the default base's directory. The assertions are kept. Every assertion is kept. Where a reading changed: - `file -b` readings (286, 287, 289, 1073) are the header read here: "statically linked" is no interpreter and no needed library, "LSB executable" is ET_EXEC, "PE32+" a wide PE; 1073's "FreeBSD" is EI_OSABI 9 or a FreeBSD note. readelf -l/-d/-h (286, 925, 926, 930, 1040, 1057) are the interpreter, RUNPATH, needed libraries and class from that header; 996's `readelf -S | grep ' C '` is SHF_COMPRESSED read from the section headers; 947's `readelf -h` entry is e_entry, its symbol address still the llvm payload's llvm-nm. - nm (287, 950) is the symbol tables read here; 287 no longer depends on the nm on PATH. 287's objdump is the one the script chose. - python3 and jq readings (305, 942, 951, 952, 953, 955-964, 983-987, 994, 1025) are nlohmann::json, std::regex and the header reader; 994's subprocess compiles are `e2e::run` in the case directory; 1025's re.sub is the first match replaced; 957's and 985's tarfile readings are the machine's tar; 957's zipfile reading of contents is `python3 -I -m zipfile -e` (python3 is its declared word). - 925's make_glibc_sysroot (_sysroot_fixture.sh) is the case's function with the same layout and linker-script rewrite; `touch -d '2001-01-01'` sets that local time; the S3 sed rewrites each __GLIBC_MINOR__ line. Its machine-default leg appends [target.x86_64-linux-gnu] to the case's inherited home's config.toml, where the script appended to the shared home's and restored it. - 1009's MCPP_HOME and 1020's and 1021's _inherit_toolchain.sh homes are the case's fresh and inherited homes. - `env -i HOME PATH=/usr/bin:/bin` runs the launcher by env -i; `cmp`/cksum (942, 951) compare bytes; `grep -A<n>` (951, 1021) looks at the line and the n after it; `tail -c` (952) reads the file's last bytes; 992's dd of one byte writes the copy with that byte changed, and its trap's chmod -R u+w is the case's guard (956 has it too). - `uname -m`/`uname -s` are the build's architecture and system; a script's SKIP (exit 77) is a skip of the case with the same reason; 285's guard SKIP lines (one exited 0) are skips; its guards keep the script's whole-text order (`*"kernel-abi"*"graph"*`); 289 skips when no row built. * a compiler without C++ modules builds the C++ units that need none A GCC before 14 (DJGPP's 12.2.0) has neither -fmodules nor P1689. Its units that declare and import no module are compiled as translation units without module flags, from mcpp's own scan; th…
1 parent fd2a93d commit 7d7aabf

1,008 files changed

Lines changed: 54175 additions & 80401 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md‎

Lines changed: 326 additions & 0 deletions
Large diffs are not rendered by default.

‎.agents/docs/2026-11-14-ci-and-release-time-cost-design.md‎

Lines changed: 222 additions & 0 deletions
Large diffs are not rendered by default.

‎.agents/docs/README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
1818
---
1919
```
2020

21-
360 records.
21+
361 records.
2222

2323
## By subject
2424

@@ -42,6 +42,10 @@ Records that declare one. Everything else is listed by date below.
4242

4343
- [BMI publication, compiler capacity and active download budgets](2026-11-06-bmi-publication-and-compiler-cap-closeout.md) — active
4444

45+
### ci
46+
47+
- [CI 与发布的时间成本:按测量调度的测试、分片、并行发布](2026-11-14-ci-and-release-time-cost-design.md) — active
48+
4549
### compiler-scheduling
4650

4751
- [Compiler attempt lifetime and output recovery](2026-11-06-compiler-attempt-recovery.md) — active
@@ -193,6 +197,7 @@ Records that declare one. Everything else is listed by date below.
193197

194198
### 2026-11
195199

200+
- [CI 与发布的时间成本:按测量调度的测试、分片、并行发布](2026-11-14-ci-and-release-time-cost-design.md) — active
196201
- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — superseded by [2026-11-12-ecosystem-architecture-optimization-design.md](2026-11-12-ecosystem-architecture-optimization-design.md)
197202
- [生态架构优化方案:四个平面的收敛、已合入项的纠正与新系统的准入(mcpp × openkal × xlings)](2026-11-12-ecosystem-architecture-optimization-design.md) — active
198203
- [测试与依赖解析补充方案:mcpp、mcpp test、mtest、mcpp-ide](2026-11-10-test-and-resolution-supplement-design.md) — active

‎.agents/skills/mcpp-release/SKILL.md‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,26 @@ Tag push 会自动触发 `release.yml` workflow。
115115

116116
### 4. 监控 Release CI
117117

118-
Release workflow 包含**四个平台**的构建,外加一个生态发布 job:
118+
Release workflow 的四个平台构建**同时开始**,各自把归档留作 artifact;`create-release`
119+
在四个构建都通过后一次创建带全部归档的 GitHub release(设计 2026-11-14):
119120

120121
| Job | 平台 | 产物 | 依赖 |
121122
|-----|------|------|------|
122-
| `build-release` | Linux x86_64 | `mcpp-X.Y.Z-linux-x86_64.tar.gz` | 无(先执行) |
123-
| `build-linux-aarch64` | Linux aarch64(交叉) | `mcpp-X.Y.Z-linux-aarch64.tar.gz` | 等 Linux x86_64 完成 |
124-
| `build-macos` | macOS ARM64 | `mcpp-X.Y.Z-macosx-arm64.tar.gz` | 等 Linux 完成 |
125-
| `build-windows` | Windows x86_64 | `mcpp-X.Y.Z-windows-x86_64.zip` | 等 Linux 完成 |
126-
| `publish-ecosystem` | — | 镜像到 xlings-res 双端 + 开索引 bump PR | 等**全部四个**构建完成 |
123+
| `canaries` | — | 生态项目(xlings、mcppls)用候选版本构建 | 仅手动发布且勾选 `canaries` 时运行 |
124+
| `build-release` | Linux x86_64 | `mcpp-X.Y.Z-linux-x86_64.tar.gz`、源码包、`mcpp.lua`、`install.sh` | 无 |
125+
| `build-linux-aarch64` | Linux aarch64(交叉) | `mcpp-X.Y.Z-linux-aarch64.tar.gz` | 无 |
126+
| `build-macos` | macOS ARM64 | `mcpp-X.Y.Z-macosx-arm64.tar.gz` | 无 |
127+
| `build-windows` | Windows x86_64 | `mcpp-X.Y.Z-windows-x86_64.zip` | 无 |
128+
| `create-release` | — | GitHub release、覆盖全部归档的 `SHA256SUMS` | 四个构建(及运行时的 canaries) |
129+
| `release-manifest` | — | `mcpp-release.json` | `create-release` |
130+
| `publish-ecosystem` | — | 镜像到 xlings-res 双端 + 开索引 bump PR | `release-manifest` |
131+
132+
canaries 默认不运行(tag push 与未勾选的手动发布):xlings 与 mcppls 的构建约 40 分钟,
133+
由各项目在发布后以自己的 CI 固定新版本验证。需要在创建 release 前验证时,手动发布:
134+
135+
```bash
136+
gh workflow run release.yml --repo mcpp-community/mcpp -f canaries=true
137+
```
127138

128139
`build-linux-aarch64` 是两段式的(bootstrap 先构出本 release 的 x86_64 mcpp,再用它交叉构建
129140
aarch64),因为 bootstrap 装的是**上一个已发布版本**,可能不认新特性。
@@ -138,7 +149,7 @@ gh run view <run-id>
138149
# 如果失败,下载日志分析
139150
gh api repos/mcpp-community/mcpp/actions/runs/<run-id>/logs \
140151
-H "Accept: application/vnd.github+json" > /tmp/release-logs.zip
141-
unzip -p /tmp/release-logs.zip "build + upload (linux _ x86_64)/8_Smoke-test the bundled tarball.txt"
152+
unzip -p /tmp/release-logs.zip "build (linux _ x86_64)/8_Smoke-test the bundled tarball.txt"
142153
```
143154

144155
### 5. 验证 Release 产物
@@ -189,7 +200,7 @@ release 规则验证最终资产,不要套用 Linux 的 strip 判断。
189200

190201
### publish-ecosystem:镜像 + 索引(发布的后半程)
191202

192-
四个构建 job 全绿后自动执行,做两件事:
203+
`release-manifest` 封存之后自动执行,做两件事:
193204

194205
1. **镜像到 `xlings-res/mcpp` 双端**(GitHub + GitCode),由
195206
`.github/tools/mirror_res.sh` 完成 —— 单 leg 内资产**并发上传**
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: e2e-cases
2+
description: >
3+
Named cases of the C++ e2e suite, each held to its `ok` line: a case that is
4+
skipped (a capability absent, another platform) or fails fails the step, so a
5+
job that exists to run a case cannot pass without running it. The cases run
6+
from the test archive build.yml wrote for the host (`mcpp-tests-<host>`,
7+
`mcpp test --archive`, docs/08): no job that runs them builds them, and a
8+
host with Visual Studio masked could not.
9+
10+
inputs:
11+
cases:
12+
description: the cases, one `<suite>/<case>` per line
13+
required: true
14+
mcpp:
15+
description: the mcpp that runs the tests
16+
required: true
17+
host:
18+
description: the host of the archive (linux-x86_64, windows-x86_64)
19+
required: true
20+
from-main:
21+
description: >
22+
'true': the archive of main's last successful CI run, for a workflow that
23+
runs outside it (ci-fresh-install); otherwise this run's.
24+
required: false
25+
default: 'false'
26+
under-test:
27+
description: >
28+
The mcpp the cases test: MCPP_E2E_MCPP, which the cases' support reads
29+
before the archive's program, and MCPP_BIN_MCPP, which an archive written
30+
with --no-programs takes. Empty: the archive's own build.
31+
required: false
32+
default: ''
33+
token:
34+
description: a token that reads this repository's actions
35+
required: false
36+
default: ${{ github.token }}
37+
38+
runs:
39+
using: composite
40+
steps:
41+
- name: The tests of this run
42+
if: inputs.from-main != 'true'
43+
uses: actions/download-artifact@v4
44+
with:
45+
name: mcpp-tests-${{ inputs.host }}
46+
path: ${{ runner.temp }}/mcpp-tests
47+
48+
- name: The tests of main's last CI run
49+
if: inputs.from-main == 'true'
50+
shell: bash
51+
env:
52+
GH_TOKEN: ${{ inputs.token }}
53+
HOST: ${{ inputs.host }}
54+
run: |
55+
set -euo pipefail
56+
run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow ci.yml --branch main --status success --limit 1 \
57+
--json databaseId -q '.[0].databaseId')
58+
[ -n "$run" ] || { echo "::error::no successful CI run of main holds the tests"; exit 1; }
59+
rm -rf "$RUNNER_TEMP/mcpp-tests"
60+
gh run download "$run" -R "$GITHUB_REPOSITORY" -n "mcpp-tests-$HOST" -D "$RUNNER_TEMP/mcpp-tests"
61+
echo "the tests of main's run $run"
62+
63+
- name: The cases
64+
shell: bash
65+
env:
66+
CASES: ${{ inputs.cases }}
67+
RUNNER_MCPP: ${{ inputs.mcpp }}
68+
UNDER_TEST: ${{ inputs.under-test }}
69+
run: |
70+
set -euo pipefail
71+
cases=()
72+
while IFS= read -r c; do
73+
c="${c#"${c%%[![:space:]]*}"}"; c="${c%"${c##*[![:space:]]}"}"
74+
if [ -n "$c" ]; then cases+=("$c"); fi
75+
done <<< "$CASES"
76+
[ "${#cases[@]}" -gt 0 ] || { echo "::error::no case named"; exit 1; }
77+
args=(--archive "$RUNNER_TEMP/mcpp-tests/mcpp-tests.tar.gz")
78+
for c in "${cases[@]}"; do args+=(--case "$c"); done
79+
# The system's own spelling of the path: mcpp reads MCPP_BIN_MCPP
80+
# itself, and a Windows mcpp does not read an MSYS path.
81+
if [ -n "$UNDER_TEST" ]; then
82+
under="$UNDER_TEST"
83+
if command -v cygpath >/dev/null 2>&1; then under="$(cygpath -w "$under")"; fi
84+
export MCPP_E2E_MCPP="$under" MCPP_BIN_MCPP="$under"
85+
fi
86+
log="$RUNNER_TEMP/e2e-cases.log"
87+
status=0
88+
"$RUNNER_MCPP" test "${args[@]}" 2>&1 | tee "$log" || status=$?
89+
for c in "${cases[@]}"; do
90+
tr -d '\r' < "$log" | grep -qxF " ok $c" || { echo "::error::$c did not pass (a skip or a failure; its line above)"; status=1; }
91+
done
92+
exit "$status"

‎.github/actions/e2e-mtest/action.yml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ description: >
99
1010
inputs:
1111
host:
12-
description: the host's name in the stores' artifact names (linux, macos, windows)
12+
description: >
13+
the host's name in the stores' artifact names (linux, macos, windows), or
14+
`*` for every host's (the plan of ci.yml)
1315
required: true
1416
token:
1517
description: a token that reads this repository's actions
@@ -55,7 +57,7 @@ runs:
5557
# Another host's own sources (src/native/windows/...) run on no Linux test,
5658
# so its suite has no map and every test of it is affected.
5759
- name: The coverage map
58-
if: inputs.host == 'linux'
60+
if: inputs.host == 'linux' || inputs.host == '*'
5961
shell: bash
6062
env:
6163
GH_TOKEN: ${{ inputs.token }}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
name: mcpp-unit
2+
description: >
3+
The unit and C++ e2e tests by `mcpp test` (docs/08 "The order of a run";
4+
design 2026-11-14): the measured history of main's last CI run taken into the
5+
run store, so that the tests run longest first, a part longer than its share
6+
runs as groups of its cases (`--split auto`) and `--shard` assigns tests by
7+
duration; on a re-run of a failed job, only what its previous attempt failed
8+
(`--failed`); the job's own runs kept as `mtest-unit-<name>-a<attempt>`.
9+
Run it after use-built-mcpp, from the repository's root.
10+
11+
inputs:
12+
name:
13+
description: the job's name in the stores' artifact names (linux-1, macos-15, windows)
14+
required: true
15+
history:
16+
description: >
17+
the stores of main's last CI run to read the measured durations from
18+
(`mtest-unit-<history>*`); empty when the job was given its history
19+
(the shards of one run take it from their archive job)
20+
required: false
21+
default: ''
22+
archive:
23+
description: the test archive to run (`mcpp test --archive`); empty to build the tests here
24+
required: false
25+
default: ''
26+
shard:
27+
description: I/N, this job's shard of the tests (`mcpp test --shard`)
28+
required: false
29+
default: ''
30+
since:
31+
description: the commit a pull request's change is read from (`mcpp test --since`)
32+
required: false
33+
default: ''
34+
patterns:
35+
description: the tests to run, as `mcpp test` patterns separated by spaces; empty for all
36+
required: false
37+
default: ''
38+
mcpp:
39+
description: the mcpp that runs the tests; empty for this commit's (MCPP_FRESH)
40+
required: false
41+
default: ''
42+
timeout:
43+
description: the seconds a test may run (`mcpp test --timeout`)
44+
required: false
45+
default: '1200'
46+
stores:
47+
description: the stores to keep, relative to the workspace, separated by spaces
48+
required: false
49+
default: target/mtest
50+
token:
51+
description: a token that reads this repository's actions
52+
required: false
53+
default: ${{ github.token }}
54+
55+
runs:
56+
using: composite
57+
steps:
58+
- name: The measured history of main's last CI run
59+
if: inputs.history != ''
60+
shell: bash
61+
env:
62+
GH_TOKEN: ${{ inputs.token }}
63+
KEY: ${{ inputs.history }}
64+
run: |
65+
set -uo pipefail
66+
py=python3; command -v python3 >/dev/null 2>&1 || py=python
67+
run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow ci.yml --branch main --status success --limit 1 \
68+
--json databaseId -q '.[0].databaseId' 2>/dev/null || true)
69+
if [ -z "$run" ]; then echo "no successful run of main: the tests run in the order they are listed"; exit 0; fi
70+
d="$RUNNER_TEMP/unit-history"
71+
rm -rf "$d"
72+
if ! gh run download "$run" -R "$GITHUB_REPOSITORY" -p "mtest-unit-$KEY*" -D "$d" 2>/dev/null; then
73+
echo "main's run $run kept no store for $KEY: the tests run in the order they are listed"; exit 0
74+
fi
75+
"$py" .github/tools/test_history.py take target/mtest "$d" "$d"/*/
76+
77+
# A RE-RUN RUNS WHAT FAILED. The store of the attempt before this one, when
78+
# it kept one, names what failed; a job that failed before its tests ran
79+
# kept none and runs them all again.
80+
- name: The attempt before this one
81+
if: github.run_attempt != '1'
82+
shell: bash
83+
env:
84+
GH_TOKEN: ${{ inputs.token }}
85+
NAME: ${{ inputs.name }}
86+
run: |
87+
set -uo pipefail
88+
py=python3; command -v python3 >/dev/null 2>&1 || py=python
89+
prev="mtest-unit-$NAME-a$((GITHUB_RUN_ATTEMPT - 1))"
90+
d="$RUNNER_TEMP/unit-previous"
91+
rm -rf "$d"
92+
if gh run download "$GITHUB_RUN_ID" -R "$GITHUB_REPOSITORY" -n "$prev" -D "$d" 2>/dev/null \
93+
&& [ -f "$d/index.ndjson" ]; then
94+
"$py" .github/tools/test_history.py take target/mtest "$d"
95+
echo "MCPP_UNIT_FAILED=1" >> "$GITHUB_ENV"
96+
echo "the attempt before kept $prev: what it failed runs again"
97+
else
98+
echo "the attempt before kept no store: every test runs"
99+
fi
100+
101+
- name: The tests via `mcpp test`
102+
shell: bash
103+
env:
104+
ARCHIVE: ${{ inputs.archive }}
105+
SHARD: ${{ inputs.shard }}
106+
SINCE: ${{ inputs.since }}
107+
PATTERNS: ${{ inputs.patterns }}
108+
TIMEOUT: ${{ inputs.timeout }}
109+
RUNS: ${{ inputs.mcpp }}
110+
run: |
111+
set -euo pipefail
112+
args=(test --timeout "$TIMEOUT" --split auto)
113+
# THE PROGRAM THE TESTS RUN IS THIS COMMIT'S, AS BUILD.YML BUILT IT
114+
# (rule R1): it is not built again here (`--no-programs`), and an
115+
# archive written without it takes it from the same variable. A native
116+
# path: on Windows the variable is read by mcpp and the tests, not bash.
117+
fresh="$MCPP_FRESH"
118+
if command -v cygpath >/dev/null 2>&1; then fresh="$(cygpath -w "$fresh")"; fi
119+
export MCPP_BIN_MCPP="${MCPP_BIN_MCPP:-$fresh}"
120+
if [ -n "$ARCHIVE" ]; then args+=(--archive "$ARCHIVE"); else args+=(--no-programs); fi
121+
if [ -n "${MCPP_UNIT_FAILED:-}" ]; then
122+
args+=(--failed)
123+
else
124+
[ -z "$SHARD" ] || args+=(--shard "$SHARD")
125+
[ -z "$SINCE" ] || args+=(--since "$SINCE")
126+
# shellcheck disable=SC2206 -- the patterns are words
127+
[ -z "$PATTERNS" ] || args+=($PATTERNS)
128+
fi
129+
echo "mcpp ${args[*]}"
130+
"${RUNS:-$MCPP_FRESH}" "${args[@]}"
131+
132+
# The job's own runs, and not the history it was given, are kept.
133+
- name: The runs of this job
134+
if: always()
135+
shell: bash
136+
env:
137+
STORES: ${{ inputs.stores }}
138+
run: |
139+
py=python3; command -v python3 >/dev/null 2>&1 || py=python
140+
for s in $STORES; do [ ! -d "$s" ] || "$py" .github/tools/test_history.py own "$s" "$GITHUB_RUN_ID"; done
141+
142+
- uses: ./.github/actions/test-report
143+
if: always()
144+
with:
145+
name: mtest-unit-${{ inputs.name }}-a${{ github.run_attempt }}
146+
stores: ${{ inputs.stores }}

‎.github/actions/use-built-mcpp/use.sh‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ if [ -n "$tc" ]; then
8888
fi
8989

9090
{
91-
echo "MCPP_BOOT=$boot"
91+
# The released binary install_pinned_mcpp.sh named, when it named one.
92+
echo "MCPP_BOOT=${MCPP_BOOT:-$boot}"
9293
echo "MCPP=$bin"
9394
echo "MCPP_FRESH=$bin"
9495
if [ -n "${XLINGS_BIN:-}" ]; then echo "MCPP_VENDORED_XLINGS=$XLINGS_BIN"; fi

‎.github/release-canaries.toml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Release canaries (the 2026-09-28 ecosystem design, WS10).
22
#
33
# Projects of this ecosystem built with the candidate mcpp before a release is
4-
# tagged: release.yml runs .github/workflows/release-canaries.yml first, and a
5-
# canary that fails blocks the tag.
4+
# created: a release started by hand with `canaries` (release.yml) runs
5+
# .github/workflows/release-canaries.yml beside its builds, and a canary that
6+
# fails blocks the release. A tag pushed, or a release started without it,
7+
# runs none (design 2026-11-14: their build took 48 of 109 minutes).
68
#
79
# Only the ecosystem's own repositories are canaries. A downstream project (a
810
# user's application, or a fork of one) validates a release in its own pull

‎.github/tools/built_artifact.sh‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
# THE PROGRAM A BUILD MADE, AS THE BUILD REPORTS IT.
3+
#
4+
# built_artifact.sh <mcpp> <name> [build arguments...]
5+
#
6+
# Runs `<mcpp> build --message-format json [arguments]` and prints the absolute
7+
# path of the artifact named <name>, from the build's `artifact` events
8+
# (docs/50-machine-output.md §9): one line per product, whose object has one
9+
# `path`. Read with grep and sed (`-E`, as GNU and BSD sed read it), so that a
10+
# machine with no JSON reader (the hermetic container) can ask it. The build's narration stays on standard
11+
# error. A failed build, or one that reports no such artifact, exits non-zero.
12+
#
13+
# Instead of a search of target/: a self-hosting build keeps the engine it
14+
# runs under target/<triple>/<fingerprint>/engine/epoch-N/mcpp, and a search
15+
# for a file named mcpp took that copy (an x86_64 program) for the product of
16+
# an aarch64 build (2026-11-14).
17+
set -euo pipefail
18+
[ $# -ge 2 ] || { echo "usage: $0 <mcpp> <name> [build arguments...]" >&2; exit 2; }
19+
mcpp=$1; name=$2; shift 2
20+
report=$(mktemp)
21+
trap 'rm -f "$report"' EXIT
22+
"$mcpp" build --message-format json "$@" > "$report"
23+
found=$(grep -F '"event":"artifact"' "$report" | grep -F "\"name\":\"$name\"" \
24+
| sed -nE 's/.*"path":"(([^"\\]|\\.)*)".*/\1/p' | tail -1 | sed 's/\\\\/\\/g' || true)
25+
[ -n "$found" ] || { echo "the build reported no artifact named $name" >&2; exit 1; }
26+
printf '%s\n' "$found"

0 commit comments

Comments
 (0)