0.1.0 —— C++ 运行时上到 openkal,连同编译器自己的运行时和裸机的展开约定 #18
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. | |
| # | |
| # A C++ standard library is not portable in the sense a program is: it is | |
| # configured for one C library and compiled against that library's headers. The | |
| # claim here is therefore not "it builds" but "a program above it does the | |
| # things a C++ program does", and the one that settles it is an exception thrown | |
| # across frames and caught --- because that is the path a build proves nothing | |
| # about. Everything else this package could check, a runtime that was linked but | |
| # never worked would also satisfy. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| env: | |
| MCPP_VERSION: 2026.8.19.4 | |
| XLINGS_VERSION: v2026.8.17.2 | |
| XLINGS_NON_INTERACTIVE: '1' | |
| jobs: | |
| runtime: | |
| name: build the runtime and run what stands above it (${{ matrix.toolchain }}) | |
| runs-on: ubuntu-24.04 | |
| # ⚠️ Raised for the self-build step below, which is temporary. See the note | |
| # there: when mcpp#486 ships, that step goes and so does this. | |
| timeout-minutes: 90 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| toolchain: ['llvm@22.1.8'] | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install xlings and mcpp | |
| 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: | | |
| 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"; exit 1 | |
| fi | |
| sleep 60 | |
| done | |
| mcpp --version | |
| mcpp self config --mirror GLOBAL | |
| - name: Select the toolchain | |
| run: | | |
| spec='${{ matrix.toolchain }}' | |
| mcpp toolchain install "${spec%@*}" "${spec#*@}" | |
| mcpp toolchain default "$spec" | |
| - name: The runtime builds | |
| run: mcpp build | |
| # ⚠️ THE ONE OBSERVATION A BUILD CANNOT MAKE. | |
| # | |
| # An unwinder that cannot find the program's frame descriptions compiles, | |
| # links, and runs every path that does not throw. Measured while this | |
| # package was written: the whole example above printed its first line and | |
| # then `libc++abi: terminating due to uncaught exception', with | |
| # _Unwind_Backtrace walking zero frames --- and nothing named the cause. | |
| # So the throw is the check, and the destructor beside it is what says the | |
| # unwind was correct rather than merely non-fatal. | |
| - name: A C++ program above it throws across frames and catches | |
| run: | | |
| cd examples/cxx && mcpp run 2>&1 | tee out.log | |
| grep -q -- '-- failures: 0 --' out.log | |
| - name: import std above it | |
| run: | | |
| cd examples/import-std && mcpp run 2>&1 | tee out.log | |
| grep -q 'import std above openkal: 2 4 7' out.log | |
| # ⭐⭐ THE SAME PROGRAM ON A MACHINE WITH NO OPERATING SYSTEM. | |
| # | |
| # Everything above this step runs on a host, and a host has a C library, a | |
| # C++ runtime and an unwinder already installed. A program that reaches | |
| # one of them by mistake still works — so those steps can pass without | |
| # having exercised this package's own copies at all. | |
| # | |
| # There is nothing here to reach. The C library is openkal-musl, the | |
| # standard library and the unwinder are this package's, and beneath them | |
| # is firmware whose whole interface is `ecall`. ⇒ This is the step that | |
| # cannot go green by accident, which is why the design document makes it | |
| # the acceptance criterion rather than one more row. | |
| # | |
| # ⚠️ The assertion is on the OUTPUT and not on the exit status: firmware | |
| # that never reaches the payload exits zero, and so does a payload whose | |
| # console writes go nowhere. | |
| - name: Install the emulator | |
| if: matrix.toolchain == 'llvm@22.1.8' | |
| run: | | |
| xlings install xim:qemu-riscv -y | |
| XLINGS_HOME="$HOME/.mcpp/registry" xlings install xim:qemu-riscv -y | |
| # ⚠️ THE BUILD TOOL IS BUILT FROM A BRANCH, AND THAT IS TEMPORARY. | |
| # | |
| # This step needs four decisions that no released mcpp makes yet, each of | |
| # which belongs to the tool rather than to this package: | |
| # | |
| # `import std` is gated on whether a package PROVIDES a standard | |
| # library for the target rather than on whether the target is | |
| # freestanding; a package may carry its own std module source; | |
| # `-fno-exceptions` / `-fno-rtti` / `-ffreestanding` come off when the | |
| # graph supplies a C++ runtime built for the target; and unwind tables | |
| # go on, because the compiler turns them off for this kind of target | |
| # and a partial set of tables stops the walk rather than degrading it. | |
| # | |
| # They are on mcpp-community/mcpp#486. Until that is released this job | |
| # builds the tool the same way every other dependency here is taken — | |
| # from the branch — so the criterion below is actually enforced instead | |
| # of being a comment saying it was verified once on a laptop. | |
| # | |
| # ⇒ When #486 ships, delete this step and raise MCPP_VERSION. | |
| - name: The build tool, from the branch that has what this needs | |
| if: matrix.toolchain == 'llvm@22.1.8' | |
| run: | | |
| set -euo pipefail | |
| git clone --depth 1 -b feat/import-std-capability \ | |
| https://github.com/mcpp-community/mcpp "$RUNNER_TEMP/mcpp-src" | |
| cd "$RUNNER_TEMP/mcpp-src" | |
| # ⚠️ THE CLONE CARRIES A WORKSPACE PIN, AND IT NAMES A VERSION THE | |
| # INDEX NO LONGER HAS. | |
| # | |
| # xlings: version '2026.8.17.1' not found for 'mcpp' | |
| # available: 2026.8.19.4 | |
| # | |
| # `.xlings.json` at a repository root says which mcpp a build in that | |
| # tree uses, and mcpp's own bootstrap pin does not move when mcpp is | |
| # released — it is the version that was current when the pin was last | |
| # touched. Cloning the branch therefore imports a pin that is only | |
| # valid inside that repository's own CI, where the same file selects | |
| # what gets installed. | |
| # | |
| # Rewriting it to the version this job already installed is what makes | |
| # the two agree. It changes nothing about what is being tested: the | |
| # pin selects the tool that BUILDS mcpp, and what is under test is the | |
| # mcpp that comes out. | |
| printf '{\n "workspace": {\n "mcpp": "%s"\n }\n}\n' "$MCPP_VERSION" > .xlings.json | |
| # ⚠️ `--dev` AND NOT `--release`. What is under test is a set of | |
| # decisions the tool makes about compile flags; an optimisation level | |
| # changes none of them. Measured: the release self-build took over | |
| # half an hour of a sixty-minute job on a two-core runner, which is | |
| # most of the budget spent on something the test does not observe. | |
| mcpp build --dev | |
| BUILT=$(find target -type f -name mcpp -perm -u+x | head -1) | |
| [ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; } | |
| echo "$(cd "$(dirname "$BUILT")" && pwd)" >> "$GITHUB_PATH" | |
| "$BUILT" --version | |
| - name: The same source on bare metal, with exceptions | |
| if: matrix.toolchain == 'llvm@22.1.8' | |
| run: | | |
| set -euo pipefail | |
| cd examples/same-source | |
| Q=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-qemu-riscv/*/bin/qemu-system-riscv64 | head -1) | |
| # ⚠️ Anchored on the BARE NAME, so a checkout that already carries a | |
| # path is left alone rather than getting a path inside a path. The | |
| # manifest is required to carry the bare name; the note beside it says | |
| # why, and this is the step that relies on it. | |
| grep -q '"qemu-system-riscv64"' mcpp.toml \ | |
| || { echo "::error::the manifest no longer carries the bare emulator name"; exit 1; } | |
| sed -i "s|\"qemu-system-riscv64\"|\"$Q\"|" mcpp.toml | |
| mcpp run 2>&1 | tee out.log | |
| grep -q 'sorted: 2 4 7' out.log # containers + algorithms + the allocator | |
| grep -q 'caught: 42' out.log # the unwinder found the handler | |
| grep -q 'unwound: true' out.log # ⭐ and ran a destructor on the way | |
| grep -q 'import std over openkal: ok' out.log | |
| # ⭐⭐ AND THE SAME SOURCE ON THIS MACHINE, WHICH IS WHAT MAKES THE STEP | |
| # ABOVE A DEMONSTRATION RATHER THAN AN ILLUSTRATION. | |
| # | |
| # Nothing is edited between the two commands — no `#if`, no second | |
| # directory, no second source. The only thing that differs is the memory | |
| # layout, and `build.mcpp` states it behind a condition on the target OS | |
| # because that is a statement about the machine. | |
| - name: The same source, on this machine, over openkal-linux | |
| if: matrix.toolchain == 'llvm@22.1.8' | |
| run: | | |
| set -euo pipefail | |
| cd examples/same-source && mcpp run 2>&1 | tee host.log | |
| grep -q 'import std over openkal: ok' host.log | |
| # The four lines are the same four lines. | |
| diff <(grep -E '^(sorted|caught|unwound|import std over openkal):' out.log) \ | |
| <(grep -E '^(sorted|caught|unwound|import std over openkal):' host.log) | |
| # ⭐⭐ AND THE SAME SOURCE FOR TWO MACHINES THIS ONE IS NOT. | |
| # | |
| # The two steps above prove the source does not know which machine it is | |
| # for. These prove the BUILD does not need to be on it: one Linux host | |
| # produces a PE and a Mach-O, and the jobs below run them on the real | |
| # thing with nothing installed. | |
| # | |
| # ⚠️ THE ARTEFACT IS THE ARGUMENT, WHICH IS WHY THOSE JOBS INSTALL NOTHING. | |
| # Not mcpp, not a compiler, not a C runtime — the program carries its C | |
| # library, its C++ runtime and its unwinder, and what remains is the | |
| # operating system it was built for. A run that needed a redistributable | |
| # installed first would be demonstrating something weaker. | |
| - name: The same source, built here for Windows and for macOS | |
| if: matrix.toolchain == 'llvm@22.1.8' | |
| run: | | |
| set -euo pipefail | |
| cd examples/same-source | |
| mkdir -p "$RUNNER_TEMP/cross" | |
| for t in x86_64-windows-gnu aarch64-macos; do | |
| rm -rf target | |
| mcpp build --target "$t" | |
| a=$(find target -type f \( -name 'openkal-same-source' -o -name '*.exe' \) | head -1) | |
| [ -n "$a" ] || { echo "::error::$t produced no artefact"; exit 1; } | |
| echo "$t → $(file -b "$a")" | |
| cp "$a" "$RUNNER_TEMP/cross/" | |
| done | |
| # ⚠️ The format is asserted here rather than left to the run jobs. A | |
| # run that fails tells you the program did not work; this tells you | |
| # what was produced, and the two failures need different fixes. | |
| file "$RUNNER_TEMP/cross/openkal-same-source.exe" | grep -q 'PE32+ executable' | |
| file "$RUNNER_TEMP/cross/openkal-same-source" | grep -q 'Mach-O 64-bit arm64' | |
| - uses: actions/upload-artifact@v4 | |
| if: matrix.toolchain == 'llvm@22.1.8' | |
| with: | |
| name: cross-artifacts | |
| path: ${{ runner.temp }}/cross/ | |
| if-no-files-found: error | |
| # --------------------------------------------------------------------------- | |
| # ⭐⭐ THE ACCEPTANCE CRITERION FOR PORTABILITY OF THE ARTEFACT. | |
| # | |
| # A cross build that produces a well-formed file proves the compiler was told | |
| # the right target. It does not prove the program runs, and every difference | |
| # this ecosystem has had to find on these two platforms — the loader-bootstrapped | |
| # thread-local, the unwinder's search for its own tables, the personality | |
| # routine — links successfully and fails at run time. | |
| # | |
| # ⚠️ These jobs deliberately have NO toolchain steps. If one is ever added | |
| # because "the program needs it", that is the finding, not the fix. | |
| run-on-windows: | |
| name: the artefact built on Linux runs on Windows | |
| needs: runtime | |
| runs-on: windows-2022 | |
| timeout-minutes: 10 | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: { name: cross-artifacts, path: art } | |
| - name: It runs, and it unwinds | |
| run: | | |
| set -euo pipefail | |
| ./art/openkal-same-source.exe 2>&1 | tee out.log | |
| grep -q 'sorted: 2 4 7' out.log | |
| grep -q 'caught: 42' out.log | |
| # ⭐ The line a link cannot fake: a destructor ran during the unwind, | |
| # so libunwind found `.eh_frame` by reading the image rather than by | |
| # asking the operating system to enumerate modules. | |
| grep -q 'unwound: true' out.log | |
| grep -q 'import std over openkal: ok' out.log | |
| run-on-macos: | |
| name: the artefact built on Linux runs on macOS | |
| needs: runtime | |
| runs-on: macos-14 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: { name: cross-artifacts, path: art } | |
| - name: It runs, and it unwinds | |
| run: | | |
| set -euo pipefail | |
| # ⚠️ The executable bit does not survive an artefact upload. | |
| chmod +x art/openkal-same-source | |
| # ⚠️ AND THE SIGNATURE DOES. arm64 macOS refuses an unsigned image, so | |
| # this is asserted before the run: a failure here is "the linker did | |
| # not ad-hoc sign it", which is a different repair from "the program | |
| # crashed". | |
| codesign -dv art/openkal-same-source 2>&1 | grep -q 'adhoc\|Signature' | |
| ./art/openkal-same-source 2>&1 | tee out.log | |
| grep -q 'sorted: 2 4 7' out.log | |
| grep -q 'caught: 42' out.log | |
| grep -q 'unwound: true' out.log | |
| grep -q 'import std over openkal: ok' out.log |