Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 168 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ on:
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_VERSION: 2026.8.26.2
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'

Expand Down Expand Up @@ -45,24 +51,43 @@ jobs:
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: |
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
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

Expand All @@ -85,6 +110,102 @@ jobs:
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
Expand Down Expand Up @@ -345,23 +466,49 @@ jobs:
"$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: |
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 self config --mirror GLOBAL
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
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ mcpp.lock
# remove a file that is ALREADY TRACKED. This repository had two: one at the
# root and one under `examples/import-std/`. The first cleanup found only the
# root one, because the scan it used anchored the path at the beginning.

# The working trees the workflow substitutes for published dependencies.
.musl/
.spec/
.impl/
.openkal-*/
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-llvm-runtime"
version = "0.1.3"
version = "0.2.0"
description = "LLVM's C++ runtime libraries — libc++, libc++abi and libunwind — configured for openkal-musl rather than for a host C library."
license = "Apache-2.0"
authors = ["mcpplibs"]
Expand Down Expand Up @@ -198,7 +198,7 @@ sources = [
cflags = ["-DDISABLE_AARCH64_FMV=1"]

[dependencies]
openkal-musl = "0.3.5"
openkal-musl = "0.4.0"

[build]
cxx_standard = "c++23"
Expand Down
118 changes: 118 additions & 0 deletions tools/branch-graph.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
#
# THE ECOSYSTEM AS WRITTEN ON A BRANCH, RATHER THAN AS PUBLISHED.
#
# This package declares openkal-musl by version, which is what a published
# manifest must say. A change that spans the two repositories cannot be tested
# that way: the version named here does not exist in the index until the other
# half is released, and the run fails with
#
# E_NOT_FOUND: package 'openkal-musl@<version>' not found in the synced index
#
# --- which reads as a mistake in this manifest and is nothing of the kind. The
# remedy is to put the working trees in place of the versions, for the whole
# graph rather than for its first edge.
#
# ⚠️ WHY THIS IS A SCRIPT AND NOT A STEP. It was a step, in one job of two, and
# the other job resolved from the index and failed exactly as above the moment
# the versions moved. Two copies of a procedure that must agree are two copies
# that will not; one file called twice cannot drift.
#
# ⚠️ THE SUBSTITUTED PATHS ARE RELATIVE, AND DELIBERATELY SO.
#
# An absolute path names a directory of one machine. A manifest carrying one has
# been committed in this ecosystem and published, and every consumer resolving
# it was handed a path that exists nowhere. Relative paths cannot express that
# mistake. They are also the only form that works unchanged on all three hosts:
# `pwd` under MSYS reports `/d/a/...`, which is not a path the engine resolves,
# so an absolute form would need a Windows-only conversion here.
#
# ⚠️ `sed -i` IS NOT PORTABLE. BSD sed, which is macOS's, reads the argument
# after -i as a backup suffix; the same command that edits a file on Linux
# consumes the next expression on macOS. In-place editing goes through a
# temporary file below for that reason.
set -euo pipefail

branch="${1:-}"
[ -n "$branch" ] || { echo "usage: ${0##*/} <branch>" >&2; exit 2; }

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$root"

# Clones a repository of this ecosystem, preferring the branch under test when
# that repository has one of the same name.
fetch() {
local repo="$1" dir="$2"
rm -rf "$dir"
git clone --quiet "https://github.com/mcpplibs/$repo.git" "$dir"
if git -C "$dir" rev-parse --verify --quiet "origin/$branch" > /dev/null; then
git -C "$dir" checkout --quiet "origin/$branch"
printf ' %-22s %s %s\n' "$repo" "$branch" "$(git -C "$dir" rev-parse --short HEAD)"
else
printf ' %-22s default branch %s (it has no %s)\n' \
"$repo" "$(git -C "$dir" rev-parse --short HEAD)" "$branch"
fi
}

# In-place editing that both GNU and BSD sed perform identically.
edit() {
local file="$1" expr="$2"
sed -E "$expr" "$file" > "$file.substituted"
mv "$file.substituted" "$file"
}

echo "the stack under test:"
fetch openkal-musl .musl
fetch openkal .spec

manifests=(mcpp.toml .musl/mcpp.toml)

# ⚠️ EVERY BACKEND THE C LIBRARY NAMES, DISCOVERED RATHER THAN LISTED.
#
# openkal-musl names a backend per target --- linux, macos, windows, opensbi ---
# each conditional on the target it serves. Their versions all move with a change
# that spans these repositories, so each one left unsubstituted fails the same
# way, one link further down:
#
# E_NOT_FOUND: package 'openkal-linux@0.6.0' (the host build)
# E_NOT_FOUND: package 'openkal-opensbi@0.2.0' (the bare-metal one)
#
# The first was fixed by naming it, and the second appeared. A list written by
# hand is a list that is one entry short, so the set is read out of the manifest.
for backend in $(grep -oE '^openkal-[a-z]+ = \{ version' .musl/mcpp.toml | cut -d' ' -f1); do
fetch "$backend" ".$backend"

# The backend reaches the specification too, by whatever form its own
# manifest uses. From <root>/.<backend>/ the specification is ../.spec.
edit ".$backend/mcpp.toml" 's|^openkal = .*$|openkal = { path = "../.spec" }|'

edit .musl/mcpp.toml \
"s|^$backend = \\{ version = \"[^\"]*\"(.*)\$|$backend = { path = \"../.$backend\"\\1|"
grep -q "path = \"../.$backend\"" .musl/mcpp.toml \
|| { echo "::error::$backend was not substituted"; exit 1; }

manifests+=(".$backend/mcpp.toml")
done

edit .musl/mcpp.toml 's|^openkal = .*$|openkal = { path = "../.spec" }|'
edit mcpp.toml 's|^openkal-musl = .*$|openkal-musl = { path = "./.musl" }|'

# The substitution is asserted rather than assumed. One that matched nothing
# would leave the manifest naming a version, the resolver would fetch a
# published C library, and the run would report on that one while appearing to
# report on this branch.
grep -q 'path = "./.musl"' mcpp.toml \
|| { echo "::error::the C library substitution matched nothing"; exit 1; }
grep -q 'path = "../.spec"' .musl/mcpp.toml \
|| { echo "::error::the specification substitution matched nothing"; exit 1; }

# ⭐ THE CHECK THAT WOULD HAVE CAUGHT EVERY FAILURE ABOVE AT ITS FIRST OCCURRENCE:
# nothing anywhere in the substituted graph still names a version. The three
# defects this file records were each found by a build failing one link further
# down than the last; this asks the whole graph at once.
if grep -nE '^openkal[a-z-]* = ("|\{ version)' "${manifests[@]}"; then
echo "::error::something in the graph still names a version rather than a tree"
exit 1
fi

echo "the whole stack names working trees; nothing in it names a version"
Loading
Loading