Skip to content

Repin the C library, which now carries a caller's redirection into a spawn #78

Repin the C library, which now carries a caller's redirection into a spawn

Repin the C library, which now carries a caller's redirection into a spawn #78

Workflow file for this run

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:
inputs:
mcpp_ref:
description: "Branch of mcpp-community/mcpp to build and test against (empty = the released pin)"
required: false
default: ""
env:
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }}
MCPP_VERSION: 2026.8.27.1
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: bash tools/install-mcpp.sh
# ⭐ THE ENGINE EVERY STEP BELOW WILL USE, COMPARED AGAINST THE ONE BUILT.
#
# The step above appends a directory to GITHUB_PATH, which governs the
# steps that follow it, so that step cannot observe its own effect. Whether
# the appended spelling is one the runner accepts is a property of the
# runner and differs between hosts. Left unasserted, a cross-validation run
# on a host that ignores it builds this ecosystem with the released engine
# and reports the result as though the change under review had been tested.
- name: The engine on PATH is the one under review
run: |
set -euo pipefail
if [ -z "${MCPP_UNDER_REVIEW:-}" ]; then
echo " no source reference: this run tests $(mcpp --version)"
exit 0
fi
on_path=$(mcpp --version | awk '{print $2}')
if [ "$on_path" != "$MCPP_UNDER_REVIEW" ]; then
echo "::error::PATH resolves mcpp $on_path, and the build under review is $MCPP_UNDER_REVIEW"
echo " the directory appended to GITHUB_PATH did not take effect on this host"
command -v mcpp
exit 1
fi
echo " every step below runs $on_path, built from $MCPP_SOURCE_REF"
- name: Select the toolchain
run: |
spec='${{ matrix.toolchain }}'
mcpp toolchain install "${spec%@*}" "${spec#*@}"
mcpp toolchain default "$spec"
# THE C LIBRARY THIS RUNTIME IS CONFIGURED FOR, AS WRITTEN ON THE BRANCH
# UNDER TEST RATHER THAN AS PUBLISHED. The reasoning is in the script.
- name: The stack, as written on this branch
run: bash tools/branch-graph.sh '${{ github.head_ref || github.ref_name }}'
- 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.
# ⭐ THE UNWINDER, AND EVERY POSITION `__config_site` DECLARES.
#
# The throw and the destructor beside it are what say the runtime is
# really there. The rest of the probe asks whether the CLAIMS this package
# makes about the environment beneath are true: `std::filesystem` over
# openkal.fs, `std::random_device` over openkal.random, and the two
# operations openkal has no atom for --- which must be REFUSED rather than
# ignored, and which a probe checking only the supported ones would pass
# for a port that silently accepted them.
- name: A C++ program above it throws, catches, and uses what is declared
run: |
cd examples/cxx && mcpp run 2>&1 | tee out.log
grep -q -- '-- failures: 0 --' out.log
! grep -q '^FAIL:' 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
# WHAT __config_site DECLARES AND WHAT THE PORT BENEATH PROVIDES ARE
# RECONCILED HERE, RATHER THAN BEING CHECKED ONCE AND ASSUMED AFTERWARDS.
#
# `__config_site` is this package's statement about the environment it was
# configured for, and the environment is openkal-musl. A statement that
# drifts from what the port actually provides does not fail to build and
# does not fail to link: it produces a program that takes a path the
# environment cannot support, and reports nothing.
#
# `_LIBCPP_HAS_TERMINAL` is the position where that happened. It gates
# `std::__is_posix_terminal`, which is `isatty(fileno(stream))` and nothing
# else. The port answered `isatty` with TCGETS while musl asks with
# TIOCGWINSZ, so every `isatty` returned 0 --- for a real terminal as
# readily as for a pipe --- and `std::print` never took its terminal path.
# Nothing failed; a program deciding on colour or on line buffering decided
# wrongly and in silence.
#
# The remedy was to fix the port rather than to withdraw the declaration,
# so the declaration is now true and this step is what keeps it true.
#
# THE CRITERION IS A RELATION AND NOT A VALUE. Asserting "a terminal is
# detected" would need a terminal; asserting "a pipe is not" would pass
# throughout the defect. What must hold is that the two DIFFER, and that
# they differ the way the system's own C library says they do.
- name: What the runtime declares is what the port provides
run: |
set -euo pipefail
d="$(mktemp -d)"; mkdir -p "$d/src"
cat > "$d/mcpp.toml" <<TOML
[package]
name = "termdecl"
version = "0.1.0"
[dependencies]
openkal-llvm-runtime = { path = "$PWD" }
[targets.termdecl]
kind = "bin"
main = "src/main.cpp"
TOML
sed -i 's/^ //' "$d/mcpp.toml"
cat > "$d/src/main.cpp" <<'CPP'
#include <cstdio>
#include <unistd.h>
// The declaration this step exists to reconcile. If the package stops
// claiming a terminal, the program says so rather than failing to
// compile: the point is to compare the claim against the behaviour,
// and a build error would compare nothing.
int main() {
#if defined(_LIBCPP_HAS_TERMINAL) && _LIBCPP_HAS_TERMINAL
std::printf("declared=1 isatty=%d\n", isatty(1));
#else
std::printf("declared=0 isatty=%d\n", isatty(1));
#endif
}
CPP
sed -i 's/^ //' "$d/src/main.cpp"
( cd "$d" && mcpp build )
bin="$(find "$d/target" -type f -name termdecl | head -1)"
test -n "$bin" || { echo "::error::the probe did not build"; exit 1; }
# The control: the system's own C library, through the same harness.
# Without it a `script` that fails to allocate a pty would make the
# runtime look wrong.
printf '#include <stdio.h>\n#include <unistd.h>\nint main(void){ printf("%%d\\n", isatty(1)); return 0; }\n' > "$d/ctrl.c"
cc "$d/ctrl.c" -o "$d/ctrl"
ctrl_pipe="$("$d/ctrl" | cat | tr -d '\r')"
ctrl_tty="$(script -qec "$d/ctrl" /dev/null | tr -d '\r' | head -1)"
[ "$ctrl_pipe" = 0 ] && [ "$ctrl_tty" = 1 ] \
|| { echo "::error::the harness cannot tell a pty from a pipe (control gave $ctrl_pipe/$ctrl_tty)"
exit 1; }
out_pipe="$("$bin" | cat | tr -d '\r')"
out_tty="$(script -qec "$bin" /dev/null | tr -d '\r' | head -1)"
echo " control: pipe=$ctrl_pipe tty=$ctrl_tty"
echo " runtime: $out_pipe / $out_tty"
declared="${out_pipe#declared=}"; declared="${declared%% *}"
pipe_v="${out_pipe##*isatty=}"
tty_v="${out_tty##*isatty=}"
if [ "$declared" = 1 ]; then
# The claim is that a terminal can be detected, so the two must
# differ and must differ as the system's own library does.
[ "$pipe_v" = "$ctrl_pipe" ] && [ "$tty_v" = "$ctrl_tty" ] \
|| { echo "::error::the runtime declares _LIBCPP_HAS_TERMINAL but the port answers $pipe_v/$tty_v where the system answers $ctrl_pipe/$ctrl_tty"
exit 1; }
echo " ok the declaration holds: a terminal is distinguished from a pipe"
else
# The claim is that it cannot. Then it must not appear to: a
# declaration of 0 beside a working isatty is also a drift, and the
# remedy is to raise the declaration rather than leave it stale.
[ "$tty_v" = "$ctrl_tty" ] \
&& { echo "::error::the runtime declares no terminal support while the port detects one; the declaration is stale"
exit 1; }
echo " ok the declaration holds: no terminal support is claimed and none is present"
fi
# ⭐⭐ 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
- 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
# ⚠️ `--target riscv64-none-elf`, AND THE FLAG IS THE WHOLE STEP.
#
# This manifest carries no `[build] target`, so a bare `mcpp run`
# builds for the HOST and passes — the four lines below appear either
# way, which is the manifest's own claim and not an accident. A step
# named "on bare metal" that omits the flag therefore reports green
# over a host build and has never once reached OpenSBI. Measured
# 2026-08-24: the artefact was `target/x86_64-linux-gnu/…`.
mcpp run --target riscv64-none-elf 2>&1 | tee out.log
# The emulator's own banner, so a run that never left the host cannot
# satisfy this step by printing the four application lines.
grep -q 'Boot HART' out.log # firmware ran; this is OpenSBI
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.
#
# ⚠️ `tr -d '\r'` ON BOTH SIDES, AND IT IS NOT COSMETIC. The bare-metal
# run reaches the console through an emulated 16550 UART, and a serial
# console terminates lines with CRLF; the native run does not. Without
# this the diff reports four differing lines whose visible text is
# identical, which reads as a failure of the claim being tested rather
# than of the transport carrying it. The assertion is about what the
# program printed, not about how the bytes arrived.
diff <(grep -E '^(sorted|caught|unwound|import std over openkal):' out.log | tr -d '\r') \
<(grep -E '^(sorted|caught|unwound|import std over openkal):' host.log | tr -d '\r')
# ⭐⭐ 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'
# ⚠️ A CRASH IS DIAGNOSED HERE RATHER THAN GUESSED AT LATER. This is
# the first time an artefact of this stack has run on this system, and
# the interesting failures — the entry point's assumptions about what
# the kernel hands it, the thread pointer, the two borrowed names —
# all look identical from outside: `Segmentation fault: 11`.
# One CI cycle that prints a backtrace is worth several that do not.
if ! ./art/openkal-same-source > out.log 2>&1; then
echo "--- how far the loader got ---"
# ⚠️ To a file, then read. Piping into `tail` interleaves dyld's
# output with the shell's own report of the signal, and the last
# lines — the ones that say which initializer was running — are the
# ones that get lost.
DYLD_PRINT_INITIALIZERS=1 ./art/openkal-same-source > dyld.log 2>&1 || true
tail -40 dyld.log
echo "--- it did not run; what the debugger saw ---"
# ⭐ `lr` IS THE DATUM. A jump to address 0 leaves no frame to
# unwind, so `bt` says only "frame #0: 0x0" — which is the symptom
# restated. The link register still holds the return address of
# whoever made that call, and `image lookup` turns it into a name.
# ⚠️ `-k` AND NOT `-o`. In `--batch` lldb abandons the remaining
# `-o` commands after the first one that errors, and reading
# register state at a PC of 0 errors — so the three commands that
# would have said something never ran. `-k` is the list lldb
# executes WHEN THE PROCESS CRASHES, which is the case at hand.
lldb --batch \
-k 'register read pc lr sp fp x8 x9 x16 x17' \
-k 'image lookup --address $lr' \
-k 'thread backtrace all' \
-k 'image list -o -f' \
-o run \
-- ./art/openkal-same-source 2>&1 | tail -70 || true
echo "--- the image's own dependencies ---"
otool -L art/openkal-same-source || true
otool -l art/openkal-same-source | grep -A4 LC_MAIN || true
cat out.log
exit 1
fi
cat 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
# ---------------------------------------------------------------------------
# ⭐⭐ THE HOST DIMENSION — THE HALF OF THE CLAIM THE JOBS ABOVE DO NOT TOUCH.
#
# Everything above builds on Linux. That establishes "one host reaches every
# target" and leaves open the thing an N×N matrix would otherwise have to
# enumerate: whether the HOST matters. The scheme's answer is that it does not
# — the target side is a set of packages and the compiler is a retargetable
# clang, so N hosts × N targets collapses to N implementations plus one tool.
#
# ⚠️ THAT IS A CLAIM, AND CLAIMS OF THIS SHAPE HAVE BEEN WRONG HERE BEFORE:
# the Linux host needed four separate repairs before it reached PE, and every
# one of them was invisible until a build was actually run. Two more hosts
# cost two jobs; asserting the collapse without running them costs a paragraph
# and proves nothing.
#
# Each host builds all four targets and RUNS the one that is itself, which is
# the same criterion the Linux job applies to itself.
host-dimension:
name: ${{ matrix.host }} host reaches every target
needs: runtime
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- { host: macOS, runner: macos-14, native: aarch64-macos }
- { host: Windows, runner: windows-2022, native: x86_64-windows-gnu }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Install xlings (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 (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: bash tools/install-mcpp.sh
# ⭐ THE ENGINE EVERY STEP BELOW WILL USE, COMPARED AGAINST THE ONE BUILT.
#
# The step above appends a directory to GITHUB_PATH, which governs the
# steps that follow it, so that step cannot observe its own effect. Whether
# the appended spelling is one the runner accepts is a property of the
# runner and differs between hosts. Left unasserted, a cross-validation run
# on a host that ignores it builds this ecosystem with the released engine
# and reports the result as though the change under review had been tested.
- name: The engine on PATH is the one under review
run: |
set -euo pipefail
if [ -z "${MCPP_UNDER_REVIEW:-}" ]; then
echo " no source reference: this run tests $(mcpp --version)"
exit 0
fi
on_path=$(mcpp --version | awk '{print $2}')
if [ "$on_path" != "$MCPP_UNDER_REVIEW" ]; then
echo "::error::PATH resolves mcpp $on_path, and the build under review is $MCPP_UNDER_REVIEW"
echo " the directory appended to GITHUB_PATH did not take effect on this host"
command -v mcpp
exit 1
fi
echo " every step below runs $on_path, built from $MCPP_SOURCE_REF"
# ⭐ THE SAME ENGINE AND THE SAME STACK AS THE LINUX JOB, FROM A DIFFERENT
# HOST. This job had neither: it installed the released engine and
# resolved this ecosystem from the index, so a change spanning these
# repositories was validated on one host of three and reported as
# validated everywhere.
- name: Select the toolchain
run: |
set -euo pipefail
# ⚠️ INSTALL, THEN SELECT. `toolchain default` names a toolchain and
# does not fetch one, so selecting an absent payload fails with
# `llvm@22.1.8 is not installed` — measured on both rows of this job.
mcpp toolchain install llvm 22.1.8
mcpp toolchain default 'llvm@22.1.8'
- name: The stack, as written on this branch
run: bash tools/branch-graph.sh '${{ github.head_ref || github.ref_name }}'
- name: Every target, from this host
run: |
set -euo pipefail
cd examples/same-source
for t in x86_64-linux-gnu x86_64-windows-gnu aarch64-macos riscv64-none-elf; do
rm -rf target
mcpp build --target "$t"
a=$(find target -type f \( -name 'openkal-same-source' -o -name 'openkal-same-source.exe' \) | head -1)
[ -n "$a" ] || { echo "::error::$t produced no artefact on this host"; exit 1; }
echo "$t → $(file -b "$a" 2>/dev/null || echo built)"
done
# ⭐ And the one that is this machine, run rather than inspected — the same
# criterion the Linux job holds itself to, applied from a different host.
- name: The artefact for this host runs on it
run: |
set -euo pipefail
cd examples/same-source
rm -rf target
mcpp run --target '${{ matrix.native }}' 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