Skip to content

2026.10.8.1: LLVM 23.1.3 line, native Linux ARM64, the 32-bit MSVC coroutine note and no host system headers #369

2026.10.8.1: LLVM 23.1.3 line, native Linux ARM64, the 32-bit MSVC coroutine note and no host system headers

2026.10.8.1: LLVM 23.1.3 line, native Linux ARM64, the 32-bit MSVC coroutine note and no host system headers #369

name: ci-aarch64-fresh-install
# End-to-end "fresh install" of the whole ecosystem on a NATIVE aarch64 host,
# exactly as a new aarch64-Linux / Termux(-proot) user would:
#
# curl quick_install.sh | bash -> installs aarch64 xlings (static musl)
# xlings install mcpp -> installs aarch64 mcpp (static musl)
# mcpp new / build / run -> NATIVE aarch64 build (pulls the native
# musl-gcc toolchain from the ecosystem)
#
# Validates that every published aarch64 asset (xlings, mcpp, musl-gcc) lines
# up and that mcpp can build & run a real `import std` program natively on
# aarch64 — no cross, no qemu. Runs on GitHub's native ARM64 runner.
on:
workflow_dispatch:
inputs:
xim_pkgindex_ref:
description: 'Candidate xim-pkgindex SHA or branch; empty runs ordinary released fresh-install'
type: string
required: false
default: ''
mcpp_run_id:
description: 'CI run with a successful native build of exactly this dispatched mcpp head'
type: string
required: false
default: ''
schedule:
- cron: '0 6 * * 1' # weekly Mon 06:00 UTC
pull_request:
branches: [ main ]
paths:
- 'src/build/build_program.cppm'
- 'modules/platform/src/process.cppm'
- 'tests/e2e/168_build_mcpp_musl_host_static.sh'
- '.github/workflows/ci-aarch64-fresh-install.yml'
push:
branches: [ main ]
paths:
- '.github/workflows/ci-aarch64-fresh-install.yml'
permissions:
contents: read
actions: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fresh-install:
if: github.event_name != 'workflow_dispatch' || inputs.xim_pkgindex_ref == ''
name: fresh install + native build (aarch64 / glibc)
runs-on: ubuntu-24.04-arm
timeout-minutes: 60
env:
# Verbose every mcpp invocation — cold bootstrap path (src/cli.cppm).
MCPP_VERBOSE: "1"
steps:
# NB: the checkout deliberately comes LAST, after every fresh-install
# step below — see the comment above it.
- name: System info
run: |
uname -a
echo "arch: $(uname -m)" # aarch64 on this runner
- name: Fresh-install xlings (curl | bash)
env:
XLINGS_NON_INTERACTIVE: '1'
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
echo "$HOME/.xlings/bin" >> "$GITHUB_PATH"
- name: Verify xlings + GLOBAL mirror
run: |
xlings --version
xlings config --mirror GLOBAL 2>/dev/null || true
xlings update -y 2>/dev/null || xlings update 2>/dev/null || true
- name: Fresh-install mcpp via xlings
run: |
xlings install mcpp -y
mcpp --version
mcpp self config --mirror GLOBAL 2>/dev/null || true
- name: Refresh mcpp package index (force latest xim-pkgindex)
run: |
# mcpp seeds a baseline index with a freshness TTL marker, so a plain
# `index update` can no-op within the window. Force the latest index
# so this run validates the native build against current packages.
mcpp index update || true
idx="$HOME/.mcpp/registry/data/xim-pkgindex"
rm -rf "$idx"
# A bare `git clone` here fails the whole job on a runner DNS
# hiccup. The policy lives in .github/tools/git_clone_retry.sh —
# but this job checks the repository out LAST ON PURPOSE (a
# `.xlings.json` in the workspace re-points where `xlings install`
# writes, so an early checkout silently changes what the
# fresh-install steps above are testing). The helper therefore does
# not exist on disk yet, and the retry is spelled inline. Same
# policy: retry every failure, bounded, git's own message survives.
clone_retry() {
local rc dest="${@: -1}"
for i in 1 2 3 4; do
rc=0; git clone "$@" || rc=$?
[ "$rc" = 0 ] && return 0
[ "$i" = 4 ] && return "$rc"
[ -e "$dest" ] && rm -rf -- "$dest"
echo "clone_retry: attempt $i failed (exit $rc); retrying" >&2
sleep $((i * 5))
done
}
clone_retry --depth 1 https://github.com/openxlings/xim-pkgindex "$idx"
grep -n "skipping relocation\|os.isfile(path.join(bindir" "$idx/pkgs/m/musl-gcc.lua" | head -2 || true
- name: Native build + run an `import std` program
run: |
work=$(mktemp -d); cd "$work"
mcpp new hello
cd hello
# default src uses import std (C++23)
mcpp build
out=$(mcpp run 2>/dev/null || true)
echo "program output: $out"
bin=$(find target -type f -path '*/bin/hello' | head -1)
file "$bin"
file "$bin" | grep -q "ARM aarch64" || { echo "expected aarch64 ELF"; exit 1; }
- name: Self-host — build mcpp + xlings from source natively
run: |
# mcpp/xlings manifests pin a glibc default toolchain; on aarch64 the
# musl-static target is the published path, so build with --target.
#
# On a pull_request, self-host the code UNDER REVIEW rather than
# upstream main. Cloning main meant this gate never saw the PR at
# all: a change that breaks the from-source build passed here and
# only failed after merge, and — the way this surfaced — a fix to
# the repo's own bootstrap pin was untestable, because the fix was
# on the branch while the clone was of main. Outside a PR there is
# no head ref and main is exactly right.
ref='${{ github.event.pull_request.head.sha }}'
repo='${{ github.event.pull_request.head.repo.clone_url }}'
[ -n "$ref" ] || ref='${{ github.sha }}'
[ -n "$repo" ] || repo='https://github.com/mcpp-community/mcpp'
# fetch-by-sha rather than `clone --depth 1`, which cannot take one.
git init -q /tmp/mcpp-src
cd /tmp/mcpp-src
git remote add origin "$repo"
git fetch -q --depth 1 origin "$ref"
git checkout -q FETCH_HEAD
echo "self-hosting $repo @ $ref"
# The clone's .xlings.json declares `workspace.mcpp` — the BOOTSTRAP
# pin, hand-maintained and deliberately lagging the newest release.
# It is scoped to the working directory and beats anything installed,
# so every `mcpp` below resolved the bootstrap version, which the
# fresh-install steps above never installed:
#
# [error] xlings: version '2026.8.6.2' not found for 'mcpp'
# [error] available: 2026.8.8.2
#
# "newest release != bootstrap pin" is the NORMAL state, so this step
# failed on every run from the moment the two diverged — and it is
# weekly, so nothing pointed at it. This step tests the freshly
# installed RELEASED binary against a source tree; the bootstrap pin
# has no standing in that question. install_released_mcpp.sh removes
# it for exactly this reason on the x86_64 legs (its point 1); this
# leg was written separately and never got it.
rm -f .xlings.json
mcpp self config --mirror GLOBAL 2>/dev/null || true
mcpp build --target aarch64-linux-musl
# Absolute: it is used again after `cd /tmp/xlings-src` below.
m=$(find "$PWD/target/aarch64-linux-musl" -type f -path '*/bin/mcpp' | head -1)
file "$m" | grep -q "ARM aarch64" || { echo "expected aarch64 mcpp"; exit 1; }
"$m" --version
# MCPP_HOME must be carried over explicitly: mcpp derives it from the
# BINARY's location, so a binary sitting in /tmp/mcpp-src/target would
# otherwise adopt an empty home and re-bootstrap the whole ecosystem
# instead of reusing what the fresh-install steps above provisioned.
#
# Resolved HERE, before the cd below, and that placement is load-
# bearing: `mcpp` is the shim, so it obeys whatever workspace pin the
# CURRENT DIRECTORY carries — and the xlings checkout declares one too
# (`workspace.mcpp = 2026.8.6.1`, its own bootstrap). Run from there,
# this resolves a version nothing installed, MCPP_HOME comes back
# empty, and the step dies on the guard below instead of on the real
# cause. /tmp/mcpp-src has had its pin removed above, so ask from here.
export MCPP_HOME=$(mcpp self env | awk -F'= *' '/^MCPP_HOME/{print $2; exit}')
echo "reusing MCPP_HOME=$MCPP_HOME"
test -d "$MCPP_HOME" || { echo "could not determine MCPP_HOME"; exit 1; }
# Same inline retry as the index clone above, and for the same
# reason — the checkout that would provide the shared helper is
# deliberately the last step in this job.
clone_retry() {
local rc dest="${@: -1}"
for i in 1 2 3 4; do
rc=0; git clone "$@" || rc=$?
[ "$rc" = 0 ] && return 0
[ "$i" = 4 ] && return "$rc"
[ -e "$dest" ] && rm -rf -- "$dest"
echo "clone_retry: attempt $i failed (exit $rc); retrying" >&2
sleep $((i * 5))
done
}
clone_retry --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src
cd /tmp/xlings-src
# "$m", not `mcpp`: the just-built binary is the code under review,
# and building xlings with the INSTALLED one meant this half of the
# gate never saw the PR — the same defect the clone-ref comment above
# records, one line further down. It surfaced the same way: a fix for
# an aarch64-only failure in exactly this build could not be
# validated here, because the binary running it predated the fix.
"$m" build --target aarch64-linux-musl
x=$(find target/aarch64-linux-musl -type f -path '*/bin/xlings' | head -1)
file "$x" | grep -q "ARM aarch64" || { echo "expected aarch64 xlings"; exit 1; }
"$x" --version
# ── PR regression gate ────────────────────────────────────────────────
# Everything above is the fresh-install charter: it must run exactly as a
# new user's machine does. Check out only NOW — this repo's .xlings.json
# declares an `mcpp` WORKSPACE pin, so with the checkout present in
# $GITHUB_WORKSPACE `xlings install mcpp` installs workspace-scoped
# instead of globally: the steps above would silently validate the pinned
# version rather than the freshly published one, and bare `mcpp` stops
# resolving anywhere outside the workspace.
- uses: actions/checkout@v4
with:
persist-credentials: false
# The PR's own source, then the musl host-helper regression against it.
# This is the only runner where a musl toolchain is the NATIVE one, so it
# is the only place #295 can actually be reproduced.
- name: Build current mcpp source for native regression tests
run: |
# Third site of the same pin, and the reason to remove it here too:
# this job installs `xlings install mcpp` (bare = latest) and NOTHING
# else, so the bootstrap version the checkout pins is never on this
# runner. Obeying the pin here does not select an older builder — it
# selects one that does not exist. Same removal as the self-host step
# above, same reason as install_released_mcpp.sh point 1.
rm -f .xlings.json
mcpp build --target aarch64-linux-musl
self=$(find target/aarch64-linux-musl -type f -path '*/bin/mcpp' | head -1)
test -x "$self"
self=$(realpath "$self")
"$self" --version
echo "MCPP_SELF=$self" >> "$GITHUB_ENV"
- name: "Regression: build.mcpp host helper is self-contained (#295)"
run: MCPP="$MCPP_SELF" bash tests/e2e/168_build_mcpp_musl_host_static.sh
candidate-admission:
name: candidate index + current mcpp native admission
if: github.event_name == 'workflow_dispatch' && inputs.xim_pkgindex_ref != ''
runs-on: ubuntu-24.04-arm
timeout-minutes: 90
defaults:
run:
shell: bash
env:
XLINGS_NON_INTERACTIVE: '1'
MCPP_E2E_MIRROR: GLOBAL
MCPP_NATIVE_XIM_INDEX: ${{ github.workspace }}/_candidate-xim
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: openxlings/xim-pkgindex
ref: ${{ inputs.xim_pkgindex_ref }}
path: _candidate-xim
- name: Candidate admission contracts
run: |
echo "MCPP_NATIVE_REPORT_DIR=$RUNNER_TEMP/native-llvm-arm64" >> "$GITHUB_ENV"
python3 tests/scripts/test_native_candidate_admission.py
- name: Verify and download this head's successful native mcpp build
env:
GH_TOKEN: ${{ github.token }}
MCPP_SOURCE_RUN: ${{ inputs.mcpp_run_id }}
run: python3 .github/tools/download_native_admission_mcpp.py "$MCPP_SOURCE_RUN" "$RUNNER_TEMP/mcpp-built"
- uses: ./.github/actions/bootstrap-mcpp
- name: Seed the candidate index before installing the native runtime
run: |
set -euo pipefail
mkdir -p "$MCPP_NATIVE_REPORT_DIR"
git -C "$MCPP_NATIVE_XIM_INDEX" rev-parse HEAD | tee "$MCPP_NATIVE_REPORT_DIR/xim-commit.txt"
printf '%s\n' "$GITHUB_SHA" > "$MCPP_NATIVE_REPORT_DIR/mcpp-commit.txt"
python3 .github/tools/seed_native_xim_index.py "$HOME/.mcpp" "$MCPP_NATIVE_XIM_INDEX"
echo "MCPP_HOME=$HOME/.mcpp" >> "$GITHUB_ENV"
"$XLINGS_BIN" --version
- name: Use this head's mcpp against the candidate runtime
run: bash .github/actions/use-built-mcpp/use.sh linux-aarch64 GLOBAL
- name: Install, build, run and pack the native GNU default with candidate recipes
env:
MCPP: ${{ env.MCPP_FRESH }}
run: |
set -euo pipefail
bash .github/tools/check_aarch64_llvm_payload.sh 2>&1 | tee "$RUNNER_TEMP/native-llvm-arm64/admission.log"
grep -qF 'PASS: native ARM64 LLVM installs, builds and runs' "$RUNNER_TEMP/native-llvm-arm64/admission.log"
- name: GNU self-host with LLVM 23.1.3 on native ARM64
run: |
set -euo pipefail
python3 .github/tools/seed_native_xim_index.py --verify "$MCPP_HOME" "$MCPP_NATIVE_XIM_INDEX"
marker="$MCPP_NATIVE_REPORT_DIR/gnu-build-start"
touch "$marker"
"$MCPP_FRESH" build --toolchain llvm@23.1.3 --target aarch64-linux-gnu 2>&1 | tee "$MCPP_NATIVE_REPORT_DIR/gnu-selfhost.log"
mapfile -t binaries < <(find "$GITHUB_WORKSPACE/target/aarch64-linux-gnu" -type f -path '*/bin/mcpp' -newer "$marker")
[ "${#binaries[@]}" = 1 ] || { echo "expected one newly built GNU mcpp, found ${#binaries[@]}"; exit 1; }
gnu=$(realpath "${binaries[0]}")
readelf -hW "$gnu" | tee "$MCPP_NATIVE_REPORT_DIR/gnu-mcpp-header.txt"
grep -q 'Machine:.*AArch64' "$MCPP_NATIVE_REPORT_DIR/gnu-mcpp-header.txt"
readelf -lW "$gnu" | tee "$MCPP_NATIVE_REPORT_DIR/gnu-mcpp-program-headers.txt"
python3 - "$MCPP_HOME" "$MCPP_NATIVE_REPORT_DIR/gnu-mcpp-program-headers.txt" <<'PY'
import pathlib, re, sys
home = pathlib.Path(sys.argv[1]).resolve()
text = pathlib.Path(sys.argv[2]).read_text()
match = re.search(r'Requesting program interpreter: ([^\]]+)', text)
assert match, text
loader = pathlib.Path(match.group(1)).resolve()
loader.relative_to(home / 'registry/data/xpkgs/xim-x-glibc')
assert loader.name == 'ld-linux-aarch64.so.1', loader
PY
"$gnu" --version | tee "$MCPP_NATIVE_REPORT_DIR/gnu-mcpp-version.txt"
printf '%s\n' "$gnu" > "$MCPP_NATIVE_REPORT_DIR/gnu-mcpp-binary.txt"
echo "MCPP_NATIVE_GNU=$gnu" >> "$GITHUB_ENV"
- name: The GNU self-host runs the complete native unit and integration suite
run: |
set -euo pipefail
"$MCPP_NATIVE_GNU" test --toolchain llvm@23.1.3 --target aarch64-linux-gnu 2>&1 | tee "$MCPP_NATIVE_REPORT_DIR/gnu-tests.log"
- name: Preserve native ELF and link evidence when the suite fails
if: failure()
run: |
set -euo pipefail
evidence="$MCPP_NATIVE_REPORT_DIR/native-link-evidence"
mkdir -p "$evidence"
while IFS= read -r -d '' binary; do
relative="${binary#target/}"
mkdir -p "$evidence/$(dirname "$relative")"
cp "$binary" "$evidence/$relative"
readelf -aW "$binary" > "$evidence/$relative.readelf.txt"
done < <(find target/aarch64-linux-gnu -type f -path '*/bin/unit/test_elf_runtime' -print0)
while IFS= read -r -d '' plan; do
relative="${plan#target/}"
mkdir -p "$evidence/$(dirname "$relative")"
cp "$plan" "$evidence/$relative"
done < <(find target/aarch64-linux-gnu -type f -name build.ninja -print0)
- name: GNU mcpp builds the LLVM path host helper (804 LLVM case only)
env:
MCPP: ${{ env.MCPP_NATIVE_GNU }}
MCPP_E2E_804_LLVM_HOST_ONLY: '1'
run: |
set -euo pipefail
bash tests/e2e/804_a_path_host_tool_builds_with_its_chosen_toolchain.sh 2>&1 | tee "$MCPP_NATIVE_REPORT_DIR/gnu-llvm-host-helper.log"
grep -qF 'PASS: 804 native LLVM path host helper (LLVM case only)' "$MCPP_NATIVE_REPORT_DIR/gnu-llvm-host-helper.log"
- uses: actions/checkout@v4
with:
repository: mcpp-community/mcpp-index
ref: main
path: _native-mcpp-index
- name: Four real index members consume the native LLVM GNU ecosystem
run: |
set -euo pipefail
git -C _native-mcpp-index rev-parse HEAD | tee "$MCPP_NATIVE_REPORT_DIR/mcpp-index-commit.txt"
python3 .github/tools/seed_native_xim_index.py --verify "$MCPP_HOME" "$MCPP_NATIVE_XIM_INDEX"
adapter="$RUNNER_TEMP/mcpp-llvm-gnu-consumer"
cat > "$adapter" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
case "${1:-}" in
build|test|run) set -- "$@" --toolchain llvm@23.1.3 --target aarch64-linux-gnu ;;
esac
{ printf 'GNU consumer argv:'; printf ' %q' "$@"; printf '\n'; } >> "$MCPP_NATIVE_REPORT_DIR/index-consumer-argv.log"
exec "$MCPP_NATIVE_GNU" "$@"
SH
chmod +x "$adapter"
cd _native-mcpp-index
MCPP="$adapter" MCPP_VERBOSE=1 MCPP_TIMINGS="$MCPP_NATIVE_REPORT_DIR/index-members.tsv" \
bash tests/run_members.sh cjson sqlite3 fmtlib.fmt nlohmann.json 2>&1 | tee "$MCPP_NATIVE_REPORT_DIR/index-members.log"
python3 - "$MCPP_NATIVE_REPORT_DIR/index-members.tsv" <<'PY'
import pathlib, sys
rows = [line.split('\t') for line in pathlib.Path(sys.argv[1]).read_text().splitlines()]
assert len(rows) == 4, rows
assert {row[1] for row in rows} == {'cjson', 'sqlite3', 'fmtlib.fmt', 'nlohmann.json'}, rows
assert all(row[2] == 'ok' for row in rows), rows
PY
- name: The real openkal stack runs as native ARM64 without skipping
env:
MCPP: ${{ env.MCPP_NATIVE_GNU }}
MCPP_E2E_EXPECT_ARCH: aarch64
run: |
set -euo pipefail
bash tests/e2e/286_the_openkal_stack_still_builds.sh 2>&1 | tee "$RUNNER_TEMP/native-llvm-arm64/openkal.log"
! grep -q '^SKIP:' "$RUNNER_TEMP/native-llvm-arm64/openkal.log"
grep -qF 'OK: the openkal stack builds, links statically and runs' "$RUNNER_TEMP/native-llvm-arm64/openkal.log"
- uses: actions/upload-artifact@v4
if: always()
with:
name: candidate-native-ecosystem-admission
path: ${{ runner.temp }}/native-llvm-arm64
if-no-files-found: warn