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
35 changes: 29 additions & 6 deletions .github/tools/check_published_arm64_cn_ecosystem.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,35 @@
set -euo pipefail
phase="${1:?usage: ecosystem-cn.sh release VERSION}"
version="${2:?mcpp version under test required}"
base="${XLINGS_HOME:?explicit isolated XLINGS_HOME required}"
controller="${XLINGS_HOME:?explicit isolated XLINGS_HOME required}"
probes="${CN_PROBE_ROOT:?instance-owned probe root required}"
[[ "$probes" == "$controller/subos/cn-arm64-published/probes" ]] || exit 1
base="$probes/client-home"
[[ ! -e "$base" ]] || { echo 'consumer client home must be cold'; exit 1; }
[[ "$(uname -m)" == aarch64 ]] || { echo "native ARM64 required"; exit 1; }
[[ "$phase" == release ]] || { echo "published release only"; exit 1; }
[[ ! -d "${CN_HOST_CHECKOUT:?host checkout path required}" ]] || {
echo 'host checkout must not be visible in the SubOS'; exit 1;
}
xl="${CN_PUBLISHED_XLINGS:?published client path required}"
work="${CN_PROBE_RUN_DIR:-$base/probes/runs/$phase-$version-$(date +%Y%m%dT%H%M%S)}"
[[ "$work" == "$base/probes/runs/"* ]] || exit 1
work="${CN_PROBE_RUN_DIR:-$probes/runs/$phase-$version-$(date +%Y%m%dT%H%M%S)}"
[[ "$work" == "$probes/runs/"* ]] || exit 1
mkdir -p "$work"
exec > >(tee -a "$work/probe.log") 2>&1
export XLINGS_HOME="$base"
# A fresh client inside the namespace must not inherit its controller's
# project, active instance, or shim dispatch context. Filesystem isolation
# remains enforced by the already-entered SubOS, including the check above.
unset XLINGS_PROJECT_DIR XLINGS_ACTIVE_SUBOS XLINGS_ROOT_INSTANCE XLINGS_ROOT_LOGIN
unset XLINGS_SHIM_DEPTH XLINGS_SUBOS_LIB XLINGS_BIN
printf 'phase=%s version=%s XLINGS_HOME=%s sandbox_HOME=%s\n' "$phase" "$version" "$base" "$HOME"
cat /proc/self/mountinfo > "$work/subos-mountinfo.txt"
export MCPP_HOME="$work/mcpp-home"
export MCPP_VENDORED_XLINGS="$xl"
export MCPP_E2E_MIRROR=CN MCPP_E2E_LLVM_VERSION=23.1.3 MCPP_E2E_EXPECT_ARCH=aarch64
unset LD_LIBRARY_PATH LD_PRELOAD MCPP_TOOLCHAIN
"$xl" config --mirror CN
"$xl" update
"$xl" install "mcpp@$version" -g -y -u
MCPP="$base/data/xpkgs/xim-x-mcpp/$version/bin/mcpp"
[[ -x "$MCPP" ]] || { echo "mcpp not executable: $MCPP"; exit 1; }
Expand All @@ -43,8 +54,20 @@ project="${CN_PROBE_PROJECT:-hello}"
"$MCPP" new "$project"
cd "$work/$project"
"$MCPP" build 2>&1 | tee "$work/native-default-build.log"
grep -q 'Resolved llvm@23.1.3' "$work/native-default-build.log"
grep -Eq 'aarch64-(unknown-)?linux-gnu' "$work/native-default-build.log"
# Cold automatic provisioning and warm resolution have different progress
# messages. Assert the documented build facts instead of a human log banner.
"$MCPP" emit build-database --format json > "$work/native-build-database.json"
python3 - "$work/native-build-database.json" "$project" "$MCPP_HOME" <<'PYDEFAULT'
import json,pathlib,sys
envelope=json.load(open(sys.argv[1]))
assert envelope['kind']=='mcpp.build-database',envelope
database=envelope['data']['database']
selected=next(s for s in database['sets'] if s['name']==sys.argv[2])
toolchain=database['ide']['toolchains'][selected['ide']['toolchain']]
assert toolchain['family']=='clang' and toolchain['version']=='23.1.3',toolchain
assert toolchain['target'] in ('aarch64-linux-gnu','aarch64-unknown-linux-gnu'),toolchain
pathlib.Path(toolchain['driver']).resolve().relative_to(pathlib.Path(sys.argv[3]).resolve())
PYDEFAULT
mapfile -t native_bins < <(find target -type f -path "*/bin/$project")
[[ "${#native_bins[@]}" == 1 ]] || exit 1
readelf -hW "${native_bins[0]}" | tee "$work/native-header.txt"
Expand Down Expand Up @@ -157,5 +180,5 @@ for target in x86_64-linux-gnu aarch64-macos x86_64-windows-gnu; do
done
echo 'RELEASE BUILD PASS: CN install/default/new/build/run/pack, native openkal, four consumers and three target artifacts.'
cp "$work/openkal-source-sha.txt" "$work/cross/source-sha.txt"
printf '%s\n' "$work" > "$base/probes/last-run.txt"
printf '%s\n' "$work" > "$probes/last-run.txt"
echo 'Foreign target execution is pending the three target-system jobs.'
2 changes: 1 addition & 1 deletion .github/tools/check_version_pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ done
# literal:
if grep -qE "MCPP_PIN:[[:space:]]*['\"]?[0-9]" .github/workflows/ci-fresh-install.yml; then
bad "ci-fresh-install.yml hardcodes MCPP_PIN again — it must stay derived from
wait-index's releases-API lookup, or the index guard and the install jobs
resolve-version's releases-API lookup, or the index guard and the install jobs
can once more disagree about which version is under test (#265)"
fi

Expand Down
27 changes: 16 additions & 11 deletions .github/tools/run_published_arm64_cn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
set -euo pipefail
version=${1:?exact published mcpp version}
repo=${2:?checkout holding these tools and the canonical client pin}
backend=${3:-proot}
backend=${3:-bwrap}
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 2
[[ "$(uname -m)" == aarch64 ]] || exit 2
[[ "$backend" == proot || "$backend" == bwrap ]] || exit 2
base=${XLINGS_HOME:?cold isolated home required}
[[ ! -e "$base" ]] || { echo 'home must be cold'; exit 1; }
mkdir -p "$base/bootstrap" "$base/probes" "$base/reports"
mkdir -p "$base/bootstrap" "$base/reports"
cd "$base"
# Derive the client version from the repository's existing single pin.
xl_version=$(python3 - "$repo/src/provider/xlings/xlings.cppm" <<'PY'
Expand Down Expand Up @@ -43,17 +43,17 @@ xl="$base/bootstrap/xlings-$xl_version-linux-aarch64/bin/xlings"
"$xl" update
cp "$base/data/xim-pkgindex/.xlings-index-cache.json" "$base/reports/outer-published-index.json"
# Native backend is provided by distro infrastructure, never labeled an xim
# ARM package. The published bwrap package is x86-only. The client locates the
# system proot directly; bwrap's current locator needs this explicit private
# system-backend directory. Record the symlink and actual distro version.
# ARM package. Record its distro version and use the published client's
# documented isolation setup, which prepares a root-owned bwrap and a narrow
# AppArmor profile when needed, without disabling the host's restriction.
backend_package="$backend"
[[ "$backend" != bwrap ]] || backend_package=bubblewrap
dpkg-query -W "$backend_package" | tee "$base/reports/backend-version.txt"
command -v "$backend" | tee "$base/reports/backend-path.txt"
sha256sum "$(command -v "$backend")" > "$base/reports/backend.sha256"
if [[ "$backend" == bwrap ]]; then
mkdir -p "$base/data/xpkgs/xim-x-bwrap/ci-system-backend/bin"
ln -s /usr/bin/bwrap "$base/data/xpkgs/xim-x-bwrap/ci-system-backend/bin/bwrap"
"$xl" self doctor --isolation --fix -y | tee "$base/reports/isolation-doctor.txt"
"$xl" self doctor --isolation --json > "$base/reports/isolation-doctor.json"
fi
# Independently prove the exact mcpp archive is available through public CN.
mcpp_archive="mcpp-$version-linux-aarch64.tar.gz"
Expand All @@ -69,15 +69,20 @@ mcpp_binary_sha=$(sha256sum "${archive_binaries[0]}" | cut -d ' ' -f 1)
printf '%s\n' "$mcpp_binary_sha" > "$base/reports/mcpp-cn-binary.sha256"
"$xl" subos new cn-arm64-published
"$xl" subos runtime glibc@2.44.3 cn-arm64-published
cp "$repo/.github/tools/check_published_arm64_cn_ecosystem.sh" "$base/probes/ecosystem.sh"
printf -v command 'CN_HOST_CHECKOUT=%q CN_EXPECTED_BINARY_SHA=%q CN_PUBLISHED_XLINGS=%q bash %q release %q' "$repo" "$mcpp_binary_sha" "$xl" "$base/probes/ecosystem.sh" "$version"
# The controller home is read-only inside a real SubOS. The selected instance
# owns its writable workspace; its consumer uses a separate cold client home.
probes="$base/subos/cn-arm64-published/probes"
mkdir -p "$probes"
cp "$repo/.github/tools/check_published_arm64_cn_ecosystem.sh" "$probes/ecosystem.sh"
printf -v command 'CN_HOST_CHECKOUT=%q CN_EXPECTED_BINARY_SHA=%q CN_PUBLISHED_XLINGS=%q CN_PROBE_ROOT=%q bash %q release %q' "$repo" "$mcpp_binary_sha" "$xl" "$probes" "$probes/ecosystem.sh" "$version"
"$xl" subos use cn-arm64-published --sandbox "$backend" --cmd "$command"
work=$(cat "$base/probes/last-run.txt")
[[ "$work" == "$base/probes/runs/"* ]] || exit 1
work=$(cat "$probes/last-run.txt")
[[ "$work" == "$probes/runs/"* ]] || exit 1
cmp "$base/reports/release-tag-sha.txt" "$work/mcpp-source-sha.txt"
cp "$base/reports/release-tag-sha.txt" "$work/cross/engine-release-sha.txt"
cp "$work/mcpp-home/registry/data/xim-pkgindex/.xlings-index-cache.json" "$base/reports/inner-published-index.json"
cp "$work/mcpp-home/registry/.xlings.json" "$base/reports/inner-config.json"
cp "$probes/client-home/.xlings.json" "$base/reports/consumer-client-config.json"
cp "$base/.xlings.json" "$base/reports/outer-config.json"
echo "CN_REPORT=$base/reports" >> "$GITHUB_ENV"
echo "CN_CONSUMER_REPORT=$work" >> "$GITHUB_ENV"
Expand Down
21 changes: 13 additions & 8 deletions .github/workflows/ci-arm64-published-cn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ on:
description: Native distro backend used only as CI isolation infrastructure
required: true
type: choice
default: proot
options: [proot, bwrap]
default: bwrap
options: [bwrap, proot]
permissions:
contents: read
concurrency:
Expand Down Expand Up @@ -56,11 +56,12 @@ jobs:
${{ env.XLINGS_HOME }}/reports/*.json
${{ env.XLINGS_HOME }}/reports/*.txt
${{ env.XLINGS_HOME }}/reports/*.sha256
${{ env.XLINGS_HOME }}/probes/runs/*/probe.log
${{ env.XLINGS_HOME }}/probes/runs/*/*.txt
${{ env.XLINGS_HOME }}/probes/runs/*/*.sha256
${{ env.XLINGS_HOME }}/probes/runs/*/*.log
${{ env.XLINGS_HOME }}/probes/runs/*/members.tsv
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/probe.log
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/*.txt
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/*.json
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/*.sha256
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/*.log
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/members.tsv
if-no-files-found: warn
target-run:
needs: consume
Expand Down Expand Up @@ -90,7 +91,11 @@ jobs:
run: |
set -euo pipefail
cd art
sha256sum -c SHA256SUMS
if [ "$TARGET_SYSTEM" = macos ]; then
shasum -a 256 -c SHA256SUMS
else
sha256sum -c SHA256SUMS
fi
grep -Eq '^[0-9a-f]{40}$' source-sha.txt
grep -Eq '^[0-9a-f]{40}$' engine-release-sha.txt
grep -Eq '^[0-9a-f]{40}$' index-source-sha.txt
Expand Down
49 changes: 42 additions & 7 deletions .github/workflows/ci-fresh-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ jobs:
# Convert the race into a bounded wait: poll the index's mcpp.lua until
# it carries the released version (<=15 min), then let every job run.
# Non-workflow_run triggers (manual, cron, PR) skip the wait.
wait-index:
resolve-version:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
timeout-minutes: 20
timeout-minutes: 5
outputs:
version: ${{ steps.resolve.outputs.version }}
steps:
Expand All @@ -91,10 +91,19 @@ jobs:
esac
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "version under test: $VER"

wait-index:
needs: resolve-version
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
timeout-minutes: 20
outputs:
version: ${{ needs.resolve-version.outputs.version }}
steps:
- name: Wait for the PUBLISHED index artifact to track the released mcpp
if: ${{ github.event_name == 'workflow_run' }}
env:
VER: ${{ steps.resolve.outputs.version }}
VER: ${{ needs.resolve-version.outputs.version }}
run: |
# Poll the ARTIFACT, not the git file.
#
Expand Down Expand Up @@ -537,7 +546,10 @@ jobs:
# from BOTH sides: refused before `brew trust`, accepted after.
macos-brew-fresh:
name: macOS fresh install (Homebrew, ${{ matrix.image }})
needs: resolve-version
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
env:
MCPP_PIN: ${{ needs.resolve-version.outputs.version }}
# Same two images as macos-fresh: the formula declares `depends_on macos:
# :sonoma` + arm64, macos-14 is the oldest image satisfying it, and
# xcode-27 is macOS 27, the newest release.
Expand All @@ -561,6 +573,27 @@ jobs:
[ "$(sw_vers -productVersion | cut -d. -f1)" = 27 ] || { echo "::error::xcode-27 delivered macOS $(sw_vers -productVersion), not 27"; exit 1; }
fi

# The tap's update is asynchronous, independently of the xlings index.
# Read its published formula without tapping, so the one-liner below
# still exercises a machine that has never added this tap.
- name: Wait for the tap to carry the version under test
run: |
set -euo pipefail
deadline=$((SECONDS + 600))
ready=false
while (( SECONDS < deadline )); do
formula=$(curl -fsSL --retry 2 --retry-all-errors --max-time 30 \
https://raw.githubusercontent.com/mcpp-community/homebrew-mcpp/main/Formula/mcpp-m.rb 2>/dev/null || true)
version=$(printf '%s\n' "$formula" | sed -nE 's/^[[:space:]]*version "([^"]+)".*/\1/p' | head -1)
if [ "$version" = "$MCPP_PIN" ]; then
ready=true
break
fi
echo "Waiting for tap: expected $MCPP_PIN, observed ${version:-unavailable}"
sleep 15
done
[ "$ready" = true ] || { echo "::error::the tap never published mcpp $MCPP_PIN"; exit 1; }

# ① The command README.md documents, on a machine that has never tapped.
# Fully qualified, so Homebrew treats it as explicit intent.
- name: "Documented one-liner installs"
Expand All @@ -570,8 +603,8 @@ jobs:
# runtime, so a bare symlink into the Cellar would make MCPP_HOME the
# versioned Cellar dir and `brew upgrade` would drop every installed
# toolchain. Assert the binary on PATH is brew's and reports the
# version the FORMULA declares — not a hardcoded one, which would go
# stale on every release.
# exact release selected for every installation job. Agreement with
# an old formula alone does not validate the new release.
- name: "Installed binary is brew's, and agrees with the formula"
run: |
set -euo pipefail
Expand All @@ -583,8 +616,9 @@ jobs:
formula_version="$(brew info --json=v2 mcpp-community/mcpp/mcpp-m \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["formulae"][0]["versions"]["stable"])')"
echo "formula declares: $formula_version"
test "$formula_version" = "$MCPP_PIN"
mcpp --version
mcpp --version | grep -Fq "$formula_version"
test "$(mcpp --version)" = "mcpp $MCPP_PIN"

# ③ THE TRUST GATE, from the failing side first.
#
Expand Down Expand Up @@ -620,7 +654,8 @@ jobs:
brew install mcpp-m # short form
brew uninstall --force mcpp-m
brew install mcpp-community/mcpp/mcpp # the `mcpp` alias
brew upgrade mcpp-m || true # no-op when current; must not be refused
brew upgrade mcpp-m # no-op when current; must not be refused
test "$(mcpp --version)" = "mcpp $MCPP_PIN"

# ⑤ The only assertion a user actually cares about: it builds and runs.
# Exercises the launcher's MCPP_HOME/MCPP_VENDORED_XLINGS pinning,
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
python3 tests/scripts/test_check_workflow_assertions.py
python3 tests/scripts/test_release_install_verification.py
python3 .github/tools/check_workflow_assertions.py --check-open

- name: The release canary runner runs each command under the named bash
Expand Down
69 changes: 69 additions & 0 deletions tests/scripts/test_release_install_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Run the real Homebrew verification step against controlled command results.

An old formula and its matching old binary must not validate a newer release.
"""
from __future__ import annotations

import importlib.util
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = ROOT / ".github/workflows/ci-fresh-install.yml"
spec = importlib.util.spec_from_file_location(
"release_ci_assertions", ROOT / ".github/tools/check_workflow_assertions.py"
)
parser = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = parser
spec.loader.exec_module(parser)


def run_guard(formula: str, binary: str) -> subprocess.CompletedProcess[str]:
workflow = parser.parse(WORKFLOW)
job = next(j for j in workflow.jobs if j.key == "macos-brew-fresh")
step = next(s for s in job.steps if s.name.startswith("Installed binary is brew's"))
with tempfile.TemporaryDirectory() as directory:
prefix = Path(directory) / "brew"
bins = prefix / "bin"
bins.mkdir(parents=True)
(bins / "brew").write_text(r'''#!/bin/sh
case "$1" in
--prefix) printf '%s\n' "$CI_STUB_PREFIX" ;;
info) printf '{"formulae":[{"versions":{"stable":"%s"}}]}\n' "$CI_STUB_FORMULA" ;;
*) exit 99 ;;
esac
''')
(bins / "mcpp").write_text(r'''#!/bin/sh
[ "$1" = --version ] || exit 99
printf 'mcpp %s\n' "$CI_STUB_BINARY"
''')
for name in ("brew", "mcpp"):
(bins / name).chmod(0o755)
env = dict(os.environ, PATH=str(bins) + os.pathsep + os.environ["PATH"],
CI_STUB_PREFIX=str(prefix), CI_STUB_FORMULA=formula,
CI_STUB_BINARY=binary, MCPP_PIN="2026.10.21.1")
return subprocess.run(["bash", "-c", step.run], env=env,
capture_output=True, text=True, check=False)


class ReleasedHomebrewVersion(unittest.TestCase):
def test_matching_old_formula_and_binary_do_not_validate_the_new_release(self):
result = run_guard("2026.10.19.1", "2026.10.19.1")
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)

def test_the_requested_release_passes(self):
result = run_guard("2026.10.21.1", "2026.10.21.1")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)

def test_a_wrong_binary_is_rejected_even_with_the_new_formula(self):
result = run_guard("2026.10.21.1", "2026.10.19.1")
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)


if __name__ == "__main__":
unittest.main()
Loading