fix(ci): ABI 检查经 mcpp 解析工具链,而不是去载荷里翻 clang #27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # What this workflow asserts about the specification. | |
| # | |
| # A specification is a claim about programs, so the claims are tested by | |
| # building and running programs rather than by reading the text. There are four: | |
| # | |
| # declarations the two forms compile, under three compiler families and on | |
| # three systems, and declare the same entities | |
| # substitution a program's source is invariant under a change of | |
| # implementation, and the check that says so fails when it should | |
| # conformance the suite in this repository runs against the implementation | |
| # for each system and every observation holds | |
| # composability an implementation that provides three interfaces is examined | |
| # for three, rather than failing to link | |
| # | |
| # The three compiler families are covered because the specification is a | |
| # contract and a contract that holds only under the compiler its author used is | |
| # a description of that compiler. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| env: | |
| # A version verified to build these packages, not a measured minimum. The pin | |
| # exists for reproducibility rather than because an older mcpp is known to | |
| # fail. | |
| MCPP_VERSION: 2026.8.19.4 | |
| XLINGS_VERSION: v2026.8.17.2 | |
| XLINGS_NON_INTERACTIVE: '1' | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # The declarations compile, everywhere, in both forms. | |
| declarations: | |
| name: declarations (${{ matrix.os }}, ${{ matrix.toolchain }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-24.04, toolchain: 'gcc@16.1.0' } | |
| - { os: ubuntu-24.04, toolchain: 'llvm@22.1.8' } | |
| - { os: macos-14, toolchain: 'llvm@20.1.7' } | |
| - { os: windows-2022, toolchain: 'llvm@20.1.7' } | |
| - { os: windows-2022, toolchain: 'msvc@system' } | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install xlings and mcpp (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ | |
| | bash -s "$XLINGS_VERSION" | |
| echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" | |
| - name: Install xlings and mcpp (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| irm https://d2learn.org/xlings-install.ps1.txt | iex | |
| # The installer amends the user's environment; a later step in this | |
| # job reads none of it, so the directory is named here. | |
| "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | |
| - name: Install mcpp | |
| run: | | |
| # ⚠️ A LOOP, BECAUSE ONE `xlings update` CAN RETURN A STALE INDEX | |
| # WITHOUT SAYING SO. | |
| # | |
| # The index is published as an artifact behind a pointer, and that | |
| # pointer propagates asynchronously after a version bump is merged. | |
| # Measured on release day: an update run four minutes after the merge | |
| # printed `index updated`, and the install then failed with | |
| # | |
| # package 'mcpp@<ver>' not found in the synced index | |
| # (xim@artifact:<an older sha>, ...), synced 0 seconds ago | |
| # | |
| # Nothing had gone wrong. The update fetched the PREVIOUS artifact, | |
| # and "synced 0 seconds ago" describes when it was fetched rather than | |
| # what it contains — which is why the message reads as freshness. | |
| # | |
| # So this is not a retry around flakiness; it is the wait that a | |
| # single update does not perform. A pin naming a version that was | |
| # never published still fails, after the last attempt, and says which | |
| # of the two situations it is. | |
| for attempt in 1 2 3 4 5 6; do | |
| xlings update > /dev/null 2>&1 || true | |
| if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi | |
| if [ "$attempt" = 6 ]; then | |
| echo "::error::mcpp@$MCPP_VERSION never appeared in the index (6 attempts over 5 minutes). If it was just released, the pointer has not propagated; if the pin names a version that was never published, it never will." | |
| exit 1 | |
| fi | |
| echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s" | |
| sleep 60 | |
| done | |
| mcpp --version | |
| mcpp self config --mirror GLOBAL | |
| # The compiler family and version for this row. mcpp keeps its toolchains | |
| # in a sandbox of its own, so this selects rather than installs into the | |
| # system, and `mcpp test' and `mcpp run' have no flag for it --- which is | |
| # why it is set once here rather than passed to each command. | |
| - name: Select the toolchain | |
| run: | | |
| spec='${{ matrix.toolchain }}' | |
| case "$spec" in | |
| msvc*) mcpp toolchain default msvc ;; | |
| *) mcpp toolchain install "${spec%@*}" "${spec#*@}" | |
| mcpp toolchain default "$spec" ;; | |
| esac | |
| mcpp toolchain list | |
| # The C++ form. The modules are the artefact a C++ consumer imports, and | |
| # building them is what proves the compiler accepts them. | |
| # ⚠️ A 32-BIT TARGET, AND NOTHING ELSE IN THIS REPOSITORY IS ONE. | |
| # | |
| # Every row of every job here is hosted, and every hosted target this | |
| # specification serves is 64-bit. That is a structural blind spot rather | |
| # than an omission: the C ABI this repository freezes is instantiated at | |
| # two pointer widths, and only one of them was ever compiled. | |
| # | |
| # Measured 2026-08-20: `kal_node_info`'s frozen-layout assertion read | |
| # `offsetof(modified_ns) == sizeof(kal_uintptr)`, which is true at 64 bits | |
| # and false at 32 — the timestamp is a naturally-aligned `kal_u64`, so a | |
| # 32-bit `size` is followed by four bytes of padding and the offset stays | |
| # at eight. The failure surfaced in a CONSUMER (`mcpplibs/riscv-virt-rt`, | |
| # whose rv32 leg activates the openkal feature), which is the only place | |
| # the 32-bit layout existed at all. | |
| # | |
| # Compile-only: there is no 32-bit implementation to run against, and the | |
| # claim under test is about layout rather than behaviour. | |
| # ⚠️ A 32-BIT TARGET, AND NOTHING ELSE IN THIS REPOSITORY IS ONE. | |
| # | |
| # Every other row here is hosted, and every hosted target this | |
| # specification serves is 64-bit. That is a structural blind spot: the C | |
| # ABI this repository freezes is instantiated at two pointer widths, and | |
| # only one of them was ever compiled. | |
| # | |
| # Measured 2026-08-20: `kal_node_info`'s frozen-layout assertion read | |
| # `offsetof(modified_ns) == sizeof(kal_uintptr)`, true at 64 bits and | |
| # false at 32 — the timestamp is a naturally-aligned `kal_u64`, so a | |
| # 32-bit `size` is followed by four bytes of padding and the offset stays | |
| # at eight. It surfaced in a CONSUMER (`mcpplibs/riscv-virt-rt`'s rv32 | |
| # leg), the only place the 32-bit layout existed at all. | |
| # | |
| # ⚠️ THROUGH `mcpp`, NOT THROUGH A BORROWED clang. The first version of | |
| # this step globbed the LLVM payload for `clang++` — which is absent on | |
| # the gcc and msvc rows, and is named `clang++.exe` on Windows, so the | |
| # command expanded to nothing and the step died with `: command not | |
| # found` on four rows. mcpp resolves the toolchain for a target itself; | |
| # asking it is the only spelling that holds on every row. | |
| - name: The C ABI holds at thirty-two bits as well as sixty-four | |
| run: | | |
| set -euo pipefail | |
| mkdir -p /tmp/abi/src | |
| cat > /tmp/abi/mcpp.toml <<'EOF' | |
| [package] | |
| name = "abi" | |
| version = "0.1.0" | |
| EOF | |
| cat > /tmp/abi/src/main.cpp <<EOF | |
| #include <openkal/types.h> | |
| #include <openkal/fs.h> | |
| static_assert(__builtin_offsetof(kal_node_info, size) == 0); | |
| static_assert(__builtin_offsetof(kal_node_info, modified_ns) == 8); | |
| static_assert(sizeof(kal_node_info) == 24); | |
| static_assert(sizeof(kal_dir) == sizeof(kal_uintptr)); | |
| static_assert(sizeof(kal_file) == sizeof(kal_uintptr)); | |
| extern "C" void _start() { for (;;) {} } | |
| EOF | |
| cat > /tmp/abi/build.mcpp <<EOF | |
| import mcpp; | |
| import std; | |
| int main() { | |
| mcpp::include_dir("$GITHUB_WORKSPACE/include"); | |
| return 0; | |
| } | |
| EOF | |
| for t in riscv64-none-elf riscv32-none-elf; do | |
| ( cd /tmp/abi && rm -rf target && mcpp build --target "$t" ) \ | |
| || { echo "the frozen layout does not hold for $t"; exit 1; } | |
| echo "$t: layout holds" | |
| done | |
| - name: The module form compiles | |
| run: mcpp build | |
| # The C form, with the environment's own headers excluded --- because the | |
| # consumer this form exists for, a C library being ported onto openkal, is | |
| # compiled that way. The tool needs a driver it can pass -nostdinc to; the | |
| # toolchain that has no such spelling compiles the same declarations in | |
| # the translation unit the conformance suite carries, which every row of | |
| # the conformance job below builds. | |
| - name: The C form compiles without the environment's headers | |
| if: runner.os != 'Windows' | |
| run: | | |
| CC=cc bash tools/check-declarations.sh | |
| command -v clang >/dev/null && CC=clang bash tools/check-declarations.sh || true | |
| # --------------------------------------------------------------------------- | |
| # The property the specification exists for. | |
| substitution: | |
| name: substitution holds (${{ matrix.toolchain }}) | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 40 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| toolchain: ['gcc@16.1.0', 'llvm@22.1.8'] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install xlings | |
| run: | | |
| curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ | |
| | bash -s "$XLINGS_VERSION" | |
| echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" | |
| - name: Install mcpp | |
| run: | | |
| # ⚠️ A LOOP, BECAUSE ONE `xlings update` CAN RETURN A STALE INDEX | |
| # WITHOUT SAYING SO. | |
| # | |
| # The index is published as an artifact behind a pointer, and that | |
| # pointer propagates asynchronously after a version bump is merged. | |
| # Measured on release day: an update run four minutes after the merge | |
| # printed `index updated`, and the install then failed with | |
| # | |
| # package 'mcpp@<ver>' not found in the synced index | |
| # (xim@artifact:<an older sha>, ...), synced 0 seconds ago | |
| # | |
| # Nothing had gone wrong. The update fetched the PREVIOUS artifact, | |
| # and "synced 0 seconds ago" describes when it was fetched rather than | |
| # what it contains — which is why the message reads as freshness. | |
| # | |
| # So this is not a retry around flakiness; it is the wait that a | |
| # single update does not perform. A pin naming a version that was | |
| # never published still fails, after the last attempt, and says which | |
| # of the two situations it is. | |
| for attempt in 1 2 3 4 5 6; do | |
| xlings update > /dev/null 2>&1 || true | |
| if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi | |
| if [ "$attempt" = 6 ]; then | |
| echo "::error::mcpp@$MCPP_VERSION never appeared in the index (6 attempts over 5 minutes). If it was just released, the pointer has not propagated; if the pin names a version that was never published, it never will." | |
| exit 1 | |
| fi | |
| echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s" | |
| sleep 60 | |
| done | |
| mcpp self config --mirror GLOBAL | |
| # The compiler family and version for this row. mcpp keeps its toolchains | |
| # in a sandbox of its own, so this selects rather than installs into the | |
| # system, and `mcpp test' and `mcpp run' have no flag for it --- which is | |
| # why it is set once here rather than passed to each command. | |
| - name: Select the toolchain | |
| run: | | |
| spec='${{ matrix.toolchain }}' | |
| case "$spec" in | |
| msvc*) mcpp toolchain default msvc ;; | |
| *) mcpp toolchain install "${spec%@*}" "${spec#*@}" | |
| mcpp toolchain default "$spec" ;; | |
| esac | |
| mcpp toolchain list | |
| - name: Substitution holds | |
| working-directory: examples/substitution/app | |
| run: | | |
| # The property under test is that the source is invariant. The | |
| # checksum is taken before the first build and compared after the | |
| # second, so that a change made by either build would be detected. | |
| before="$(sha256sum src/main.cpp | cut -d' ' -f1)" | |
| mcpp build > /dev/null | |
| ./target/*/*/bin/app > with-fd.log 2>&1 | |
| grep -q 'the application produced this line' with-fd.log | |
| sed -i 's|openkal-fd = { path = "../impl-fd" }|openkal-discard = { path = "../impl-discard" }|' mcpp.toml | |
| rm -rf target | |
| mcpp build > /dev/null | |
| ./target/*/*/bin/app > with-discard.log 2>&1 | |
| if grep -q 'the application produced this line' with-discard.log; then | |
| echo "the discarding implementation produced output"; exit 1 | |
| fi | |
| after="$(sha256sum src/main.cpp | cut -d' ' -f1)" | |
| [ "$before" = "$after" ] || { echo "the source changed between builds"; exit 1; } | |
| - name: The surface checker detects an addition | |
| working-directory: examples/substitution/impl-fd | |
| run: | | |
| # The checker is only useful if it fails when it should. An | |
| # unspecified name is introduced, the checker is required to reject | |
| # it, and the name is then removed. | |
| mcpp build > /dev/null | |
| bash ../../../tools/check-surface.sh ../../../SURFACE.txt \ | |
| $(find target -name '*.o' | tr '\n' ' ') | |
| printf 'extern "C" void kal_vendor_extension(void) {}\n' > src/extra.cpp | |
| mcpp build > /dev/null | |
| if bash ../../../tools/check-surface.sh ../../../SURFACE.txt \ | |
| $(find target -name '*.o' | tr '\n' ' '); then | |
| echo "the checker accepted an unspecified name"; exit 1 | |
| fi | |
| rm -f src/extra.cpp | |
| # --------------------------------------------------------------------------- | |
| # The suite, against the implementation for each system. | |
| # | |
| # The implementations are checked out at the branch under test where they have | |
| # one and at their default branch otherwise, so that this job asserts what it | |
| # is for: that the specification as written here and the implementations as | |
| # written there agree today. | |
| conformance: | |
| name: conformance (${{ matrix.implementation }}, ${{ matrix.toolchain }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-24.04, toolchain: 'gcc@16.1.0', implementation: openkal-linux } | |
| - { os: ubuntu-24.04, toolchain: 'llvm@22.1.8', implementation: openkal-linux } | |
| - { os: macos-14, toolchain: 'llvm@20.1.7', implementation: openkal-macos } | |
| - { os: windows-2022, toolchain: 'llvm@20.1.7', implementation: openkal-windows } | |
| - { os: windows-2022, toolchain: 'msvc@system', implementation: openkal-windows } | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: The implementation | |
| run: | | |
| git clone --quiet https://github.com/mcpplibs/${{ matrix.implementation }}.git .impl | |
| branch='${{ github.head_ref || github.ref_name }}' | |
| if git -C .impl rev-parse --verify --quiet "origin/$branch" > /dev/null; then | |
| git -C .impl checkout --quiet "origin/$branch" | |
| echo "the implementation is at $branch $(git -C .impl rev-parse --short HEAD)" | |
| else | |
| echo "the implementation has no $branch; its default branch is used" \ | |
| "($(git -C .impl rev-parse --short HEAD))" | |
| fi | |
| # Whether the two are describing the same version of the contract. | |
| # | |
| # This job asserts that the specification as written here and the | |
| # implementation as written there agree today, and that assertion is | |
| # only meaningful when the implementation is written against this | |
| # version. During a release across five repositories they are out of | |
| # step for the seconds between one merge and the next, and the | |
| # compiler's report on that is a hundred errors about names a header | |
| # no longer has --- which names the symptom and not the cause. | |
| here="$(sed -n 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' mcpp.toml | head -1)" | |
| there="$(sed -n 's/^openkal[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .impl/mcpp.toml | head -1)" | |
| if [ "$here" != "$there" ]; then | |
| echo "::error::this is openkal $here and ${{ matrix.implementation }} is written against openkal $there. Nothing is wrong with either; they are not in step. Re-run after the implementation's branch has the matching version." >&2 | |
| exit 1 | |
| fi | |
| echo "both describe openkal $here" | |
| - name: Install xlings and mcpp (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ | |
| | bash -s "$XLINGS_VERSION" | |
| echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" | |
| - name: Install xlings and mcpp (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| irm https://d2learn.org/xlings-install.ps1.txt | iex | |
| "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | |
| - name: Install mcpp | |
| run: | | |
| # ⚠️ A LOOP, BECAUSE ONE `xlings update` CAN RETURN A STALE INDEX | |
| # WITHOUT SAYING SO. | |
| # | |
| # The index is published as an artifact behind a pointer, and that | |
| # pointer propagates asynchronously after a version bump is merged. | |
| # Measured on release day: an update run four minutes after the merge | |
| # printed `index updated`, and the install then failed with | |
| # | |
| # package 'mcpp@<ver>' not found in the synced index | |
| # (xim@artifact:<an older sha>, ...), synced 0 seconds ago | |
| # | |
| # Nothing had gone wrong. The update fetched the PREVIOUS artifact, | |
| # and "synced 0 seconds ago" describes when it was fetched rather than | |
| # what it contains — which is why the message reads as freshness. | |
| # | |
| # So this is not a retry around flakiness; it is the wait that a | |
| # single update does not perform. A pin naming a version that was | |
| # never published still fails, after the last attempt, and says which | |
| # of the two situations it is. | |
| for attempt in 1 2 3 4 5 6; do | |
| xlings update > /dev/null 2>&1 || true | |
| if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi | |
| if [ "$attempt" = 6 ]; then | |
| echo "::error::mcpp@$MCPP_VERSION never appeared in the index (6 attempts over 5 minutes). If it was just released, the pointer has not propagated; if the pin names a version that was never published, it never will." | |
| exit 1 | |
| fi | |
| echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s" | |
| sleep 60 | |
| done | |
| mcpp self config --mirror GLOBAL | |
| # The compiler family and version for this row. mcpp keeps its toolchains | |
| # in a sandbox of its own, so this selects rather than installs into the | |
| # system, and `mcpp test' and `mcpp run' have no flag for it --- which is | |
| # why it is set once here rather than passed to each command. | |
| - name: Select the toolchain | |
| run: | | |
| spec='${{ matrix.toolchain }}' | |
| case "$spec" in | |
| msvc*) mcpp toolchain default msvc ;; | |
| *) mcpp toolchain install "${spec%@*}" "${spec#*@}" | |
| mcpp toolchain default "$spec" ;; | |
| esac | |
| mcpp toolchain list | |
| # Every interface and every kind of examination. The exit status is the | |
| # verdict: 0 when every observation held, 1 when one did not, and 2 when | |
| # nothing was observed --- the last being the outcome a run that selected | |
| # no interface would otherwise pass silently. | |
| - name: Every interface, every kind of examination | |
| run: | | |
| bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl full | |
| # The suite is composable because openkal is: an implementation provides | |
| # an interface in whole or not at all, and a suite that examined all eight | |
| # unconditionally would fail to link against a conforming implementation | |
| # of three. Selecting three is therefore asserted to produce a report | |
| # rather than a link failure. | |
| - name: A selection of three interfaces is examined, not refused | |
| run: | | |
| rm -rf conformance/target | |
| bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl core,fs,task \ | |
| | tee selected.log | |
| # A report was produced, and nothing in it failed. | |
| grep -qE 'observations: [0-9]+ held, 0 did not hold' selected.log | |
| # An interface that was not selected is reported as not examined and | |
| # carries the reason, rather than being absent --- a report that | |
| # omitted it could not be distinguished from a report on an | |
| # implementation that provides it. | |
| grep -q 'openkal.process --- the interface was not selected' selected.log | |
| # And an interface that was selected was examined. | |
| grep -qE 'held +\[behaviour\].*' selected.log |