From 3da87a88969db952113846244aeee3b9b80bb442 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Tue, 28 Jul 2026 23:44:39 +0800 Subject: [PATCH 1/2] =?UTF-8?q?probe:=20measure=20the=20GitCode=20upload?= =?UTF-8?q?=20path=20(TEMPORARY=20=E2=80=94=20do=20not=20merge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `publish-ecosystem`'s GitCode leg has blown its 180s per-asset cap on four releases now (0.0.94 / 0.0.97 / 0.0.105 / 2026.7.28.2), always on the two biggest tarballs, always costing ~5min of manual re-upload afterwards. Every fix so far has been a guess about WHY (raise the cap, skip-don't-retry, batch verify). This branch measures it instead. The path under test (tools/gtc `_upload_one`): GET a presigned OBS PUT URL from the GitCode API, then PUT the whole file body to file.gitcode.com. A server-side OBS->GitCode callback registers the asset; when only that callback fails we get `obs_callback ... code:400 ... EOF` for an upload that actually landed, which is why the probe always re-checks the public download URL and reports put_ok and serves independently. Four parallel jobs, one hypothesis each: baseline network + direction control (down/up, gitcode vs github) sizes-urllib is throughput flat (bandwidth wall) or collapsing (stall)? sizes-curl is Python's read-all-then-PUT the bottleneck, or the wire? concurrency per-connection or per-host cap? mirror_res.sh uploads assets SERIALLY within a host leg, so if it is per-connection then parallelising that loop is the entire fix. Local reference measured before pushing (mainland-CN host -> file.gitcode.com): 8MB in 4.35s = 1.84 MB/s, put=200, serves=206. At that rate the 34.8MB asset takes ~19s; CI cannot finish it in 180s, so the runner side is >=10x slower. That is the number these jobs exist to confirm and localise. Every other workflow is deleted on this branch on purpose: the probe is the only thing that should run here, and this branch is never merged. --- .github/tools/probe_gtc_upload.py | 187 +++++ .github/workflows/aur-publish.yml | 102 --- .github/workflows/bootstrap-macos.yml | 149 ---- .../workflows/ci-aarch64-fresh-install.yml | 141 ---- .github/workflows/ci-fresh-install.yml | 390 --------- .github/workflows/ci-linux-e2e.yml | 157 ---- .github/workflows/ci-linux.yml | 171 ---- .github/workflows/ci-macos-e2e.yml | 55 -- .github/workflows/ci-macos.yml | 318 ------- .github/workflows/ci-windows-e2e.yml | 71 -- .github/workflows/ci-windows.yml | 306 ------- .github/workflows/cross-build-test.yml | 287 ------- .github/workflows/probe-gitcode-upload.yml | 210 +++++ .github/workflows/release.yml | 774 ------------------ 14 files changed, 397 insertions(+), 2921 deletions(-) create mode 100755 .github/tools/probe_gtc_upload.py delete mode 100644 .github/workflows/aur-publish.yml delete mode 100644 .github/workflows/bootstrap-macos.yml delete mode 100644 .github/workflows/ci-aarch64-fresh-install.yml delete mode 100644 .github/workflows/ci-fresh-install.yml delete mode 100644 .github/workflows/ci-linux-e2e.yml delete mode 100644 .github/workflows/ci-linux.yml delete mode 100644 .github/workflows/ci-macos-e2e.yml delete mode 100644 .github/workflows/ci-macos.yml delete mode 100644 .github/workflows/ci-windows-e2e.yml delete mode 100644 .github/workflows/ci-windows.yml delete mode 100644 .github/workflows/cross-build-test.yml create mode 100644 .github/workflows/probe-gitcode-upload.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/tools/probe_gtc_upload.py b/.github/tools/probe_gtc_upload.py new file mode 100755 index 00000000..5706217a --- /dev/null +++ b/.github/tools/probe_gtc_upload.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""probe_gtc_upload.py — instrumented single-asset upload to a GitCode release. + +TEMPORARY diagnostic tool for the recurring `publish-ecosystem` failure where +the GitCode leg of tools/mirror_res.sh blows its 180s per-asset cap on the two +largest tarballs (0.0.94 / 0.0.97 / 0.0.105 / 2026.7.28.2). It is a measurement +harness, not a mirror: it reproduces exactly what tools/gtc `release upload` +does, but times every stage separately and reports one JSON line per upload so +the stages can be compared across transports and concurrency levels. + +The upload path under test (identical to tools/gtc `_upload_one`): + + 1. GET {api}/repos/{repo}/releases/{tag}/upload_url?file_name=X + -> {"url": , "headers": {...}} + 2. PUT with the whole file as the body + 3. (server-side) OBS calls back into the GitCode API to register the asset + +Stage 3 is invisible to the client except when it fails, which is where the +`obs_callback ... code:400 ... EOF` false negative comes from: the object is +in OBS but the registration callback errored, so the PUT reports failure for +an upload that actually landed. This tool always re-probes the public download +URL afterwards, so `put_ok` and `serves` are reported independently and that +false negative shows up as put_ok=false + serves=true. + +Usage: + probe_gtc_upload.py --repo xlings-res/mcpp --tag probe-x --file f.bin \ + [--method urllib|curl] [--label NAME] + +Auth: GITCODE_TOKEN env var (same as tools/gtc). +Output: one JSON object per line on stdout; human notes on stderr. +""" +import argparse +import json +import os +import subprocess +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +API_BASE = os.environ.get("GITCODE_API_BASE", "https://api.gitcode.com/api/v5") +DL_HOST = "https://gitcode.com" + + +def get_upload_url(repo, tag, fname, token): + """Stage 1 — ask GitCode for a presigned OBS PUT URL.""" + url = (f"{API_BASE}/repos/{repo}/releases/{tag}/upload_url" + f"?file_name={urllib.parse.quote(fname)}") + req = urllib.request.Request( + url, headers={"PRIVATE-TOKEN": token, "Accept": "application/json"}) + t0 = time.monotonic() + with urllib.request.urlopen(req, timeout=120) as r: + body = r.read() + dt = time.monotonic() - t0 + info = json.loads(body) + return info["url"], info.get("headers") or {}, dt + + +def put_urllib(put_url, headers, path): + """Stage 2, transport A — exactly what tools/gtc does today: read the + whole file into memory and hand it to urllib as a single PUT body.""" + with open(path, "rb") as f: + data = f.read() + req = urllib.request.Request(put_url, data=data, headers=headers, method="PUT") + t0 = time.monotonic() + try: + with urllib.request.urlopen(req, timeout=1800) as r: + status = r.status + resp = r.read().decode(errors="replace")[:300] + except urllib.error.HTTPError as e: + status = e.code + resp = e.read().decode(errors="replace")[:300] + except Exception as e: # noqa: BLE001 - report, don't raise + status = -1 + resp = f"{type(e).__name__}: {e}"[:300] + return status, resp, time.monotonic() - t0, {} + + +def put_curl(put_url, headers, path): + """Stage 2, transport B — same presigned URL, but curl streams the file + from disk instead of buffering it in the process. curl also reports its own + connect/TLS/first-byte breakdown, which is what separates "the network is + slow" from "the client is slow".""" + fmt = ("%{http_code} %{speed_upload} %{time_namelookup} %{time_connect} " + "%{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total}") + cmd = ["curl", "-sS", "-X", "PUT", "-T", path, "--max-time", "1800", "-o", "/tmp/put_body", + "-w", fmt] + for k, v in headers.items(): + cmd += ["-H", f"{k}: {v}"] + cmd.append(put_url) + t0 = time.monotonic() + p = subprocess.run(cmd, capture_output=True, text=True) + dt = time.monotonic() - t0 + detail, status, resp = {}, -1, (p.stderr or "")[:300] + if p.returncode == 0 and p.stdout.strip(): + parts = p.stdout.split() + try: + status = int(parts[0]) + detail = { + "curl_speed_upload_Bps": float(parts[1]), + "curl_t_namelookup": float(parts[2]), + "curl_t_connect": float(parts[3]), + "curl_t_appconnect": float(parts[4]), + "curl_t_pretransfer": float(parts[5]), + "curl_t_starttransfer": float(parts[6]), + "curl_t_total": float(parts[7]), + } + except (IndexError, ValueError): + pass + try: + resp = Path("/tmp/put_body").read_text(errors="replace")[:300] + except OSError: + resp = "" + else: + detail["curl_exit"] = p.returncode + return status, resp, dt, detail + + +def serves(repo, tag, fname): + """Independent truth: does the public download URL actually return bytes? + Ranged GET — HEAD lies on GitCode (returns a redirect stub).""" + url = f"{DL_HOST}/{repo}/releases/download/{tag}/{urllib.parse.quote(fname)}" + p = subprocess.run( + ["curl", "-sSL", "-o", "/dev/null", "-w", "%{http_code}", "-r", "0-0", url], + capture_output=True, text=True) + return p.stdout.strip(), url + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--repo", required=True) + ap.add_argument("--tag", required=True) + ap.add_argument("--file", required=True) + ap.add_argument("--method", choices=("urllib", "curl"), default="urllib") + ap.add_argument("--label", default="") + ap.add_argument("--no-verify", action="store_true") + args = ap.parse_args() + + token = os.environ.get("GITCODE_TOKEN", "") + if not token: + sys.exit("GITCODE_TOKEN is empty") + + path = Path(args.file) + size = path.stat().st_size + fname = path.name + + put_url, headers, t_url = get_upload_url(args.repo, args.tag, fname, token) + obs_host = urllib.parse.urlparse(put_url).netloc + + put = put_urllib if args.method == "urllib" else put_curl + status, resp, t_put, detail = put(put_url, headers, str(path)) + + rec = { + "label": args.label or fname, + "method": args.method, + "file": fname, + "size_bytes": size, + "size_mb": round(size / 1048576, 2), + "obs_host": obs_host, + "t_upload_url_s": round(t_url, 3), + "t_put_s": round(t_put, 2), + "put_status": status, + "put_ok": status == 200, + "throughput_MBps": round(size / 1048576 / t_put, 3) if t_put > 0 else None, + "put_response_head": resp.replace("\n", " ")[:200], + } + rec.update(detail) + + if not args.no_verify: + # Deliberately independent of put_ok: this is the check that exposes + # the obs_callback false negative (put_ok=false while serves=200/206). + code, url = serves(args.repo, args.tag, fname) + rec["verify_code"] = code + rec["serves"] = code in ("200", "206") + rec["download_url"] = url + + print(json.dumps(rec), flush=True) + sys.stderr.write( + f"[probe] {rec['label']}: {rec['size_mb']}MB via {args.method} " + f"-> put={rec['put_status']} in {rec['t_put_s']}s " + f"({rec['throughput_MBps']} MB/s) serves={rec.get('serves')}\n") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml deleted file mode 100644 index 661cb893..00000000 --- a/.github/workflows/aur-publish.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: aur-publish - -# Publish the `mcpp-bin` and `mcpp` AUR packages after a release. -# -# Triggers on COMPLETION of the `release` workflow (not on `release: -# published`): release.yml creates the GitHub Release in its first job but -# uploads the aarch64 / macOS / Windows assets in LATER jobs, so the aarch64 -# .sha256 that mcpp-bin needs only exists once the whole workflow finishes. -# -# Requires one repository secret: -# AUR_SSH_PRIVATE_KEY — private key whose public half is registered on the -# AUR account that owns mcpp / mcpp-bin. -# See scripts/aur/README.md → "Automated publishing" for the full setup. -on: - workflow_run: - workflows: [release] - types: [completed] - workflow_dispatch: - inputs: - version: - description: "Version to publish (default: [package].version in mcpp.toml)" - required: false - -concurrency: - group: aur-publish - cancel-in-progress: false - -jobs: - publish: - runs-on: ubuntu-latest - # On the workflow_run trigger, only proceed if the release actually - # succeeded (skip failed/cancelled release runs). - if: >- - github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success' - steps: - - name: Checkout released commit - uses: actions/checkout@v4 - with: - # workflow_run: the exact commit the release was built from. - # workflow_dispatch: default ref (HEAD of the branch). - ref: ${{ github.event.workflow_run.head_sha || github.ref }} - - - name: Refresh both PKGBUILDs to the release version - id: refresh - env: - # CI runs as root; force update.sh's template .SRCINFO path. - MCPP_AUR_NO_MAKEPKG: "1" - run: | - VER="${{ github.event.inputs.version }}" - if [ -z "$VER" ]; then - # mcpp.toml at the released commit carries the right version. - VER=$(grep -m1 -E '^\s*version\s*=' mcpp.toml | sed -E 's/.*"([^"]+)".*/\1/') - fi - echo "version=$VER" >> "$GITHUB_OUTPUT" - ./scripts/aur/update.sh "$VER" - - - name: Configure AUR SSH - run: | - install -dm700 ~/.ssh - printf '%s\n' "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur - chmod 600 ~/.ssh/aur - ssh-keyscan -t rsa,ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null - cat > ~/.ssh/config <<'EOF' - Host aur.archlinux.org - User aur - IdentityFile ~/.ssh/aur - IdentitiesOnly yes - EOF - - - name: Push to the AUR - env: - VER: ${{ steps.refresh.outputs.version }} - run: | - set -eu - git config --global user.name "mcpp-ci" - git config --global user.email "x.d2learn.org@gmail.com" - - publish() { # $1 = package name (= dir under scripts/aur/) - pkg="$1"; src="scripts/aur/${pkg}"; work="/tmp/aur-${pkg}" - # Clone the existing AUR repo; if the package doesn't exist yet - # (first publish), start an empty repo — AUR creates it on push. - if git clone "ssh://aur@aur.archlinux.org/${pkg}.git" "$work" 2>/dev/null \ - && [ -e "$work/.git" ]; then :; else - rm -rf "$work"; mkdir -p "$work" - git -C "$work" init -q - git -C "$work" remote add origin "ssh://aur@aur.archlinux.org/${pkg}.git" - fi - # AUR repos contain only PKGBUILD, .SRCINFO and local sources. - cp "$src/PKGBUILD" "$src/.SRCINFO" "$src/mcpp.sh" "$work/" - git -C "$work" add -A - if git -C "$work" diff --cached --quiet; then - echo ":: ${pkg}: no changes, skipping" - return 0 - fi - git -C "$work" commit -q -m "${pkg} ${VER}" - git -C "$work" push origin HEAD:master - echo ":: ${pkg}: published ${VER}" - } - - publish mcpp-bin - publish mcpp-m diff --git a/.github/workflows/bootstrap-macos.yml b/.github/workflows/bootstrap-macos.yml deleted file mode 100644 index 432db041..00000000 --- a/.github/workflows/bootstrap-macos.yml +++ /dev/null @@ -1,149 +0,0 @@ -name: bootstrap-macos - -# One-shot workflow to produce the first macOS mcpp binary. -# Uses xmake + xlings LLVM to compile mcpp from source. -# Once a macOS binary exists, mcpp can self-host for future releases. - -on: - workflow_dispatch: - -jobs: - bootstrap: - name: Bootstrap mcpp (macOS ARM64) - runs-on: macos-15 - timeout-minutes: 30 - env: - XLINGS_NON_INTERACTIVE: '1' - # Dormant (workflow_dispatch only), but kept in step with the rest — - # check_version_pins.sh holds it there. Floor: 0.4.69, below which the - # index cannot resolve two packages that share a short name. - XLINGS_VERSION: '2026.7.27.2' - steps: - - uses: actions/checkout@v4 - - - name: System info - run: | - uname -a - sw_vers - xcrun --show-sdk-path - - - name: Install xlings - run: | - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - curl -fsSL -o "${WORK}/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - "${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install - echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Install LLVM + xmake - run: | - xlings install llvm -y || xlings install llvm@20.1.7 -y - brew install xmake - LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname) - echo "LLVM_ROOT=$LLVM_ROOT" >> "$GITHUB_ENV" - "$LLVM_ROOT/bin/clang++" --version - xmake --version - - - name: Build mcpp with xmake - run: | - # Generate xmake.lua if not present - if [ ! -f xmake.lua ]; then - cat > xmake.lua << 'EOF' - add_rules("mode.release") - set_languages("c++23") - - package("cmdline") - set_homepage("https://github.com/mcpplibs/cmdline") - set_description("Modern C++ command-line parsing library") - set_license("Apache-2.0") - add_urls("https://github.com/mcpplibs/cmdline/archive/refs/tags/$(version).tar.gz") - add_versions("0.0.1", "3fb2f5495c1a144485b3cbb2e43e27059151633460f702af0f3851cbff387ef0") - on_install(function (package) - import("package.tools.xmake").install(package) - end) - package_end() - - add_requires("cmdline 0.0.1") - - target("mcpp") - set_kind("binary") - add_files("src/main.cpp") - add_files("src/**.cppm") - add_packages("cmdline") - add_includedirs("src/libs/json") - set_policy("build.c++.modules", true) - -- Static link libc++ for minimal runtime dependencies - add_ldflags("-static-libstdc++", {force = true}) - add_cxxflags("-stdlib=libc++", {force = true}) - add_ldflags("-stdlib=libc++", {force = true}) - EOF - fi - - # Configure with xlings LLVM - xmake f -y -m release --toolchain=llvm --sdk="$LLVM_ROOT" - # Build - xmake build -y mcpp - - - name: Verify built binary - run: | - MCPP=$(find build -name mcpp -type f -perm +111 | head -1) - test -x "$MCPP" - echo "=== file ===" - file "$MCPP" - echo "=== otool -L (dynamic deps) ===" - otool -L "$MCPP" - echo "=== version ===" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: Package - id: package - run: | - VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - TARBALL="mcpp-${VERSION}-macosx-arm64.tar.gz" - WRAPPER="mcpp-${VERSION}-macosx-arm64" - - mkdir -p "dist/$WRAPPER/bin" - cp "$MCPP" "dist/$WRAPPER/bin/mcpp" - strip "dist/$WRAPPER/bin/mcpp" 2>/dev/null || true - cp LICENSE "dist/$WRAPPER/" 2>/dev/null || true - cp README.md "dist/$WRAPPER/" 2>/dev/null || true - - cat > "dist/$WRAPPER/mcpp" << 'LAUNCHER' - #!/bin/sh - exec "$(dirname "$0")/bin/mcpp" "$@" - LAUNCHER - chmod +x "dist/$WRAPPER/mcpp" - - # Bundle xlings - XLINGS_BIN="$HOME/.xlings/subos/default/bin/xlings" - if [ -x "$XLINGS_BIN" ]; then - mkdir -p "dist/$WRAPPER/registry/bin" - cp "$XLINGS_BIN" "dist/$WRAPPER/registry/bin/xlings" - fi - - (cd dist && tar -czf "$TARBALL" "$WRAPPER") - (cd dist && shasum -a 256 "$TARBALL" > "$TARBALL.sha256") - - echo "tarball=dist/$TARBALL" >> "$GITHUB_OUTPUT" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke test - run: | - SMOKE=$(mktemp -d) - tar -xzf "${{ steps.package.outputs.tarball }}" -C "$SMOKE" - VERSION="${{ steps.package.outputs.version }}" - "$SMOKE/mcpp-${VERSION}-macosx-arm64/bin/mcpp" --version - "$SMOKE/mcpp-${VERSION}-macosx-arm64/mcpp" --version - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: mcpp-macosx-arm64 - path: | - dist/mcpp-*-macosx-arm64.tar.gz - dist/mcpp-*-macosx-arm64.tar.gz.sha256 diff --git a/.github/workflows/ci-aarch64-fresh-install.yml b/.github/workflows/ci-aarch64-fresh-install.yml deleted file mode 100644 index 679d5123..00000000 --- a/.github/workflows/ci-aarch64-fresh-install.yml +++ /dev/null @@ -1,141 +0,0 @@ -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: - schedule: - - cron: '0 6 * * 1' # weekly Mon 06:00 UTC - pull_request: - branches: [ main ] - paths: - - 'src/build/build_program.cppm' - - 'src/platform/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 - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - fresh-install: - 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" - git clone --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. - git clone --depth 1 https://github.com/mcpp-community/mcpp /tmp/mcpp-src - cd /tmp/mcpp-src - mcpp self config --mirror GLOBAL 2>/dev/null || true - mcpp build --target aarch64-linux-musl - m=$(find 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 - git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - mcpp 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: | - 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 diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml deleted file mode 100644 index 6853f09e..00000000 --- a/.github/workflows/ci-fresh-install.yml +++ /dev/null @@ -1,390 +0,0 @@ -name: ci-fresh-install - -# Fresh install CI — validates the released mcpp binary via xlings. -# Simulates a real first-time user on a clean machine (no caches). -# -# For each platform, tests every supported toolchain: -# 1. mcpp new hello → mcpp run (basic project) -# 2. mcpp build (build mcpp itself from source) -# -# This workflow tests released mcpp, not PR code. -# It runs on release publish, manual trigger, and daily schedule. - -on: - # NOTE: `release: published` never fires from the release pipeline — the - # release is created by release.yml with GITHUB_TOKEN, and GitHub - # suppresses workflow triggers from GITHUB_TOKEN-generated events. Kept - # only for releases created manually outside the pipeline. The reliable - # post-release hook is `workflow_run` below: a platform-generated event, - # exempt from that suppression, and requiring no cross-repo PAT. - release: - types: [ published ] - workflow_run: - workflows: [ release ] - types: [ completed ] - workflow_dispatch: - schedule: - # Run daily at 06:00 UTC to catch issues from xlings/runner updates - - cron: '0 6 * * *' - -concurrency: - group: ci-fresh-install - cancel-in-progress: false # use false to test in PRs, true to only test released mcpp - -# Version under test. Bare `xlings install mcpp` resolves "newest in the -# runner's index copy", which is NOT the same source the wait-index job -# polls (raw.githubusercontent.com) — on 2026-07-21 the guard reported -# "index tracks 0.0.102" and the jobs still installed 0.0.100 ten seconds -# later, which then met an index whose floor was 0.0.101 (#265). Pinning -# makes the version explicit: if the index cannot serve it yet, the job -# fails with `version not found` instead of silently testing an older -# binary. Bump together with the .xlings.json workspace pin at release. -env: - MCPP_PIN: '2026.7.28.2' - -jobs: - # ────────────────────────────────────────────────────────────────── - # Linux: gcc@16.1.0, musl-gcc@16.1.0, llvm@20.1.7 - # ────────────────────────────────────────────────────────────────── - # A4: post-release runs race the xim-pkgindex bump (a PR a maintainer - # merges asynchronously) — the 0.0.85 fresh-install failed with - # "version not found: available 0.0.84" 55s after the bump merged. - # 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: - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - timeout-minutes: 20 - steps: - - name: Wait for xim-pkgindex to track the released mcpp - if: ${{ github.event_name == 'workflow_run' }} - run: | - VER=$(curl -fsSL "https://api.github.com/repos/mcpp-community/mcpp/releases/latest" | python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'].lstrip('v'))") - echo "released: $VER — waiting for index..." - for i in $(seq 1 30); do - if curl -fsSL "https://raw.githubusercontent.com/openxlings/xim-pkgindex/main/pkgs/m/mcpp.lua" | grep -q "\"$VER\""; then - echo "index tracks $VER (after $((i*30))s)"; exit 0 - fi - sleep 30 - done - echo "::error::index never tracked $VER within 15min — merge the bump PR (openxlings/xim-pkgindex) and re-run" - exit 1 - - name: No wait needed (manual/cron trigger) - if: ${{ github.event_name != 'workflow_run' }} - run: echo "not a post-release run; skipping index wait" - - linux-fresh: - needs: [wait-index] - name: Linux fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - timeout-minutes: 60 - env: - # Verbose every mcpp invocation — fresh-install is the cold index/sandbox - # bootstrap path, exactly where extra diagnostics matter (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - - name: Install xlings + mcpp - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.7.27.2 - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - - name: Install mcpp and config mirror - run: | - # The release tarball bundles a pkgindex snapshot frozen at - # build time; refresh it so the pinned mcpp resolves. - xlings update - xlings install "mcpp@${MCPP_PIN}" -y -g # install to global - mcpp --version - mcpp self config --mirror GLOBAL - - echo "mcpp debug info:" - which mcpp - cat $HOME/.xlings/.xlings.json - - - name: "Default: mcpp new → run" - run: | - cd "$(mktemp -d)" - mcpp new hello_gcc - cd hello_gcc - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path (user report: `mcpp new ... --template imgui` failed with - # fetch 'imgui@0.0.6' exit 1 on hosts without a sha256sum binary). - - name: "Template: mcpp new --template imgui (fetch path)" - run: | - cd "$(mktemp -d)" - mcpp new abc1 --template imgui - test -f abc1/mcpp.toml - - - name: "Default: build mcpp" - run: | - mcpp clean - mcpp run - - - name: "musl-gcc: mcpp new → run" - run: | - mcpp toolchain install gcc 16.1.0-musl - mcpp toolchain default gcc@16.1.0-musl - cd "$(mktemp -d)" - mcpp new hello_musl - cd hello_musl - mcpp run - - - name: "musl-gcc: build mcpp" - run: | - mcpp toolchain default gcc@16.1.0-musl - mcpp clean - mcpp run - - - name: "gcc 16: mcpp new → run" - run: | - mcpp toolchain install gcc 16.1.0 - mcpp toolchain default gcc@16.1.0 - cd "$(mktemp -d)" - mcpp new hello_gcc16 - cd hello_gcc16 - mcpp run - - - name: "gcc 16: build mcpp" - run: | - mcpp toolchain default gcc@16.1.0 - mcpp clean - mcpp run - - - name: "LLVM: mcpp new → run" - run: | - mcpp toolchain install llvm 20.1.7 - mcpp toolchain default llvm@20.1.7 - cd "$(mktemp -d)" - mcpp new hello_llvm - cd hello_llvm - mcpp run - - - name: "LLVM: build mcpp" - run: | - mcpp toolchain default llvm@20.1.7 - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # Newer/rolling-glibc distros — reproduction surface for the - # bundled-glibc-vs-host-libtinfo `sh:` crash (host glibc > bundled). - # Plus older-glibc legs (the safe reverse direction) proving the - # musl-static mcpp + self-contained toolchain run end-to-end on old - # hosts. Runs the released mcpp inside distro containers. - # ────────────────────────────────────────────────────────────────── - linux-distro-matrix: - needs: [wait-index] - name: Linux distro (${{ matrix.distro }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - container: - image: ${{ matrix.image }} - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - # findutils (find) is mandatory on every leg: quick_install.sh - # locates the extracted xlings dir with `find`, so a missing find - # fails the install with exit 127 *before* mcpp ever runs. Minimal - # images (opensuse/tumbleweed) ship without it; arch's base merely - # bundles it by luck. List it explicitly everywhere — don't rely on - # the base image. - include: - - distro: fedora-latest - image: fedora:latest - setup: dnf -y install curl bash tar gzip xz git findutils binutils file glibc-langpack-en - - distro: arch - image: archlinux:latest - setup: pacman -Sy --noconfirm curl bash tar gzip xz git findutils binutils file - - distro: tumbleweed - image: opensuse/tumbleweed:latest - setup: zypper -n install curl bash tar gzip xz git findutils binutils file - - distro: debian-testing - image: debian:testing - setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - - distro: ubuntu-2004 - image: ubuntu:20.04 - setup: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - - distro: debian-11 - image: debian:11 - setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - env: - XLINGS_NON_INTERACTIVE: '1' - HOME: /root - steps: - - uses: actions/checkout@v4 - - - name: Install prerequisites (${{ matrix.distro }}) - run: ${{ matrix.setup }} - - - name: Install xlings + mcpp - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.7.27.2 - # Deliberately NOT writing to $GITHUB_PATH here. On container - # images that declare no PATH in their config (opensuse/ - # tumbleweed), appending a single dir to GITHUB_PATH makes the - # runner exec later steps' `sh` with only that dir on PATH — - # `sh` (in /usr/bin) vanishes and the next step dies with - # `exec: "sh": ... not found` / exit 127. Each step below already - # exports PATH itself, so the append is redundant anyway. - - - name: Configure mcpp - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - xlings update - xlings install "mcpp@${MCPP_PIN}" -y -g - mcpp --version - mcpp self config --mirror GLOBAL - - - name: "Regression: new → run (loader env must not crash /bin/sh)" - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - cd "$(mktemp -d)" - mcpp new hello_distro - cd hello_distro - mcpp run - - - name: "Self-containment: produced binary uses bundled loader" - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - cd "$(mktemp -d)" && mcpp new hc && cd hc && mcpp build - bin="$(find target -type f -name hc | head -1)" - interp="$(file "$bin" | grep -o 'interpreter [^,]*' | awk '{print $2}')" - echo "interp=$interp" - case "$interp" in - */.mcpp/*|*/registry/*|*xpkgs*) echo "OK bundled loader" ;; - *) echo "FAIL host loader: $interp"; exit 1 ;; - esac - - # ────────────────────────────────────────────────────────────────── - # macOS: llvm@20.1.7 - # ────────────────────────────────────────────────────────────────── - macos-fresh: - needs: [wait-index] - name: macOS fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - # macos-14: the support floor (mcpp ≥0.0.50 / xlings ≥0.4.50 ship - # minos=14.0 static-libc++ binaries). A fresh install passing here - # is the continuous proof of the macOS 14 floor — and of host-tool - # independence (this image has no sha256sum; macos-15 does). - runs-on: macos-14 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - # Pinned to kXlingsVersion like every other bootstrap (see - # .github/tools/check_version_pins.sh). Two floors this image needs, - # both long satisfied — do not pin below them: - # v0.4.50+: first xlings whose macosx binary runs on macOS 14 - # (older ones carry minos=15 and refuse to start). - # v0.4.51+: in-process sha256 — this image has no sha256sum - # binary, so pinned fetches failed before it. - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.7.27.2 - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - - name: Install mcpp and config mirror - run: | - # Refresh the bundled pkgindex snapshot so the pinned mcpp resolves. - xlings update - xlings install "mcpp@${MCPP_PIN}" -y -g # install to global - mcpp --version - mcpp self config --mirror GLOBAL - - echo "mcpp debug info:" - which mcpp - cat $HOME/.xlings/.xlings.json - - - name: "LLVM: mcpp new → run" - run: | - cd "$(mktemp -d)" - mcpp new hello_mac - cd hello_mac - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path — this is what broke on hosts without a sha256sum binary - # (stock macOS / bare Windows) before xlings 0.4.51 hashed - # in-process. - - name: "Template: mcpp new --template imgui (fetch path)" - run: | - cd "$(mktemp -d)" - mcpp new abc1 --template imgui - test -f abc1/mcpp.toml - - - name: "LLVM: build mcpp" - run: | - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # Windows: llvm@20.1.7 + MSVC STL - # ────────────────────────────────────────────────────────────────── - windows-fresh: - needs: [wait-index] - name: Windows fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: windows-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings - shell: pwsh - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex - - $xlingsbin = "$env:USERPROFILE\.xlings\subos\current\bin" - $env:PATH = "$xlingsbin;$env:PATH" - $xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - - name: Install mcpp and config mirror - shell: pwsh - run: | - # Refresh the bundled pkgindex snapshot so the pinned mcpp resolves. - xlings update - xlings install "mcpp@$env:MCPP_PIN" -y -g --verbose - - cat "$env:USERPROFILE\.xlings\.xlings.json" - mcpp --version - mcpp self config --mirror GLOBAL - - - name: "LLVM: mcpp new → run" - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new hello_win - Set-Location hello_win - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path (user report: `mcpp new abc1 --template imgui` failed with - # fetch 'imgui@0.0.6' exit 1 on bare Windows — no sha256sum binary - # outside git-bash; fixed by xlings 0.4.51 in-process hashing). - - name: "Template: mcpp new --template imgui (fetch path)" - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new abc1 --template imgui - if (!(Test-Path abc1/mcpp.toml)) { exit 1 } - - - name: "LLVM: build mcpp" - shell: pwsh - run: | - mcpp clean - mcpp run diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml deleted file mode 100644 index c05fa4b1..00000000 --- a/.github/workflows/ci-linux-e2e.yml +++ /dev/null @@ -1,157 +0,0 @@ -name: ci-linux-e2e - -# The e2e suite (tests/e2e/run_all.sh) split out of ci-linux.yml so it runs in -# PARALLEL with the build/unit/toolchain jobs instead of tacked on after them, -# and SHARDED across two runners on top of that. Both workflows share the same -# cache lineage (mcpp sandbox + xlings + target/), so each shard restores a warm -# build and the only added wall-clock vs. the inline version is one extra warm -# `mcpp build` per runner. -# -# Paired workflows: ci-linux.yml (build + unit + toolchain legs + integration), -# ci-macos.yml / ci-macos-e2e.yml, ci-windows.yml / ci-windows-e2e.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - name: e2e ${{ matrix.shard }}/2 (linux x86_64, self-host) - runs-on: ubuntu-24.04 - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - shard: [1, 2] - env: - # Round-robin slice of tests/e2e (see run_all.sh). Two shards halve the - # suite's wall-clock; the split is computed on the full file list, so a - # test's shard does not move when host capabilities differ. - E2E_SHARD: ${{ matrix.shard }}/2 - MCPP_HOME: /home/runner/.mcpp - # NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that - # assert mcpp's DEFAULT (quiet) output — e.g. 48_build_error_output and - # 53_namespaced_cache_label — which forced verbose would break. Verbose is - # set only in the fresh-install workflows (cold bootstrap, no such asserts). - # A specific test that needs verbose passes `--verbose` itself. - steps: - - uses: actions/checkout@v4 - - # Same cache lineage as ci-linux.yml so this job lands on a warm - # toolchain/sandbox instead of re-installing it. - - uses: ./.github/actions/bootstrap-mcpp - - - name: Configure mirror + Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: E2E suite - # Per-test 600s timeout lives in tests/e2e/run_all.sh and identifies - # WHICH test hung; this caps the whole suite so a hang fails fast. - timeout-minutes: 25 - run: | - # Point the e2e runner at the freshly-built binary, not the - # bootstrap one. Tests cd into mktemp -d, so $MCPP must be - # absolute or the relative path breaks under the temp cwd. - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - export MCPP - # Tests that set MCPP_HOME to a fresh tmpdir need an xlings to - # bootstrap from; surface the xlings binary installed above. - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - test -x "$MCPP_VENDORED_XLINGS" - # GitHub-hosted runners are outside CN; keep CI toolchain downloads on - # the global mirror while mcpp's default remains CN for fresh local - # sandboxes. E2E tests with their own MCPP_HOME read this variable. - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" self config - # Pin the global default so test 28 (default-toolchain path) gets a - # deterministic GNU answer instead of an auto-install pick. - "$MCPP" toolchain default gcc@16.1.0 - # Warm musl once so fresh-home e2e tests inherit the payload. - "$MCPP" toolchain install gcc 16.1.0-musl - bash tests/e2e/run_all.sh - - # ────────────────────────────────────────────────────────────────── - # Hermetic (no host toolchain): the ONLY environment class that - # faithfully reproduces issue #195. Standard runners ship gcc + - # libc6-dev, so a sandbox toolchain that leaks to the host's CRT - # still links "green" there; this container has no compiler and no - # host Scrt1.o, so any leak fails loudly. Builds PR code with the - # bootstrap mcpp, then runs the llvm flow end-to-end. - # ────────────────────────────────────────────────────────────────── - hermetic: - name: hermetic e2e (no host toolchain, container) - runs-on: ubuntu-24.04 - container: debian:stable-slim - timeout-minutes: 60 - env: - XLINGS_NON_INTERACTIVE: '1' - steps: - - name: Install base utilities (NO compiler) - run: | - apt-get update -qq - apt-get install -y -qq curl ca-certificates git xz-utils unzip - # The whole point of this job: no host toolchain, no host CRT. - ! command -v gcc - ! command -v cc - test ! -e /usr/lib/x86_64-linux-gnu/Scrt1.o - test ! -e /usr/lib/gcc - - - uses: actions/checkout@v4 - - # Payload cache (downloads only — the container still has no host - # toolchain, which is the property under test). - - name: Cache mcpp sandbox payloads - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-hermetic-${{ hashFiles('mcpp.toml') }} - restore-keys: | - mcpp-hermetic- - - - name: Bootstrap xlings + released mcpp - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.7.27.2 - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - xlings update - xlings install mcpp -y -g - MCPP_BOOT="$HOME/.xlings/subos/current/bin/mcpp" - "$MCPP_BOOT" --version - "$MCPP_BOOT" self config --mirror GLOBAL - echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" - - - name: Build PR mcpp from source (sandbox gcc only) - run: | - "$MCPP_BOOT" build - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: "issue #195 reproduction: manifest llvm toolchain, fresh" - run: | - cd "$(mktemp -d)" - "$MCPP" new hello195 - cd hello195 - printf '\n[toolchain]\nlinux = "llvm@22.1.8"\n' >> mcpp.toml - printf 'import std;\nint main() { std::println("hello {}", 195); return 0; }\n' > src/main.cpp - "$MCPP" run - - - name: Hermetic llvm e2e subset - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - export MCPP - bash tests/e2e/86_llvm_hermetic_link.sh - bash tests/e2e/37_llvm_import_std.sh diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml deleted file mode 100644 index 76eefa4f..00000000 --- a/.github/workflows/ci-linux.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: ci-linux - -# Self-host CI on Linux: mcpp builds mcpp. The bootstrap mcpp comes from -# `xlings install mcpp` (xim:mcpp in the xlings package index), so this -# workflow no longer depends on a previous-release tarball — the -# chicken-and-egg now lives upstream in the xlings index. -# -# SHAPE: four INDEPENDENT jobs, no `needs:` between them. Each restores the -# same cache lineage (see .github/actions/bootstrap-mcpp) and pays one warm -# `mcpp build` (~2.5 min) to get the PR's own binary, then does its own leg. -# That warm rebuild is far cheaper than serialising the legs behind a shared -# artifact would be: -# -# before: build → unit → gcc → musl → llvm → xlings ≈ 18 min (one job) -# after: max(build+unit, gcc, musl+llvm, xlings) ≈ 7-8 min -# -# The ~18 min e2e suite is a SEPARATE workflow (ci-linux-e2e.yml, sharded) -# that runs in parallel on the same caches. -# -# Paired workflows: ci-linux-e2e.yml, ci-macos.yml, ci-windows.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - # MCPP_HOME pinned so the cache keys below restore into the same path - # mcpp resolves at runtime. - MCPP_HOME: /home/runner/.mcpp - # Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). - # Safe here: this workflow no longer runs the e2e suite, which is what - # asserts mcpp's default quiet output (tests 48/53). - MCPP_VERBOSE: "1" - -jobs: - build-test: - name: build + unit tests (linux x86_64, self-host) - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - # Before the bootstrap, not after: this needs no toolchain and no mcpp, - # takes under a second, and the drift it catches (a stale xlings pin) - # would otherwise surface minutes later as an unrelated-looking - # dependency-resolution failure. Pure text extraction on purpose — it - # has to work when the build is broken. - - name: Check version / xlings pin consistency - run: bash .github/tools/check_version_pins.sh - - - uses: ./.github/actions/bootstrap-mcpp - - - name: Configure mirror + Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # Set GLOBAL mirror via xlings directly (bootstrap mcpp may lack --mirror flag) - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: Unit + integration tests via `mcpp test` - run: | - # Use freshly-built mcpp for test (it has --mirror support) - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - "$MCPP_FRESH" self config --mirror GLOBAL - "$MCPP_FRESH" test - - # A cold, from-scratch self-host build with the manifest-pinned GCC: the - # property `build-test` cannot cover, because it builds incrementally on a - # restored target/. `mcpp test` is deliberately NOT repeated here — it would - # be the same suite, same toolchain, same driver binary as `build-test`, - # differing only in incremental state. - toolchain-gcc: - name: "toolchain: gcc (cold self-host)" - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP_FRESH" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - - name: "Toolchain: GCC — cold rebuild with the PR binary" - run: | - "$MCPP" clean - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0" build.log - - # The two cheap legs share one runner: each is ~1 min after the warm build, - # so a runner apiece would cost more in setup than it saves in wall-clock. - toolchain-cross: - name: "toolchain: musl + llvm" - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP_FRESH" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - # Auto-installs gcc@16.1.0-musl on demand (cached across runs). - - name: "Toolchain: musl-gcc — build mcpp (--target)" - run: | - "$MCPP" clean - "$MCPP" build --target x86_64-linux-musl 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0 → x86_64-linux-musl" build.log - - - name: "Toolchain: LLVM — build mcpp" - run: | - "$MCPP" toolchain install llvm 20.1.7 - # Override project toolchain to use LLVM for this build - sed -i 's/^default = "gcc@16.1.0"/default = "llvm@20.1.7"/' mcpp.toml - "$MCPP" clean - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log - # Restore - sed -i 's/^default = "llvm@20.1.7"/default = "gcc@16.1.0"/' mcpp.toml - - # Integration: the mcpp built from THIS PR's source builds & runs a real - # external C++ project — xlings (openxlings/xlings ships its own mcpp.toml). - # MCPP_VENDORED_XLINGS only supplies the xlings package backend that mcpp - # resolves deps through. - integration-xlings: - name: "integration: mcpp builds & runs xlings" - runs-on: ubuntu-24.04 - timeout-minutes: 45 - env: - XLINGS_NON_INTERACTIVE: '1' - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP_FRESH" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - git clone --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - "$MCPP" run diff --git a/.github/workflows/ci-macos-e2e.yml b/.github/workflows/ci-macos-e2e.yml deleted file mode 100644 index 4c30b421..00000000 --- a/.github/workflows/ci-macos-e2e.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: ci-macos-e2e - -# The e2e suite on macOS ARM64, split out of ci-macos.yml (where it was 3.4 of -# the job's 8.4 min) so it runs in parallel with the toolchain/integration -# work — same shape as ci-linux-e2e.yml / ci-windows-e2e.yml. -# -# Not sharded: the macOS runners finish the suite in ~3.5 min, which is under -# this workflow's setup+build floor, so a second runner would buy nothing. -# -# Paired workflows: ci-macos.yml, ci-linux-e2e.yml, ci-windows-e2e.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - name: e2e suite (macOS ARM64, self-host) - runs-on: macos-15 - timeout-minutes: 30 - # NOTE: no MCPP_VERBOSE — the e2e suite asserts mcpp's default quiet - # output (tests 48/53). - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-macos-llvm - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - - - name: E2E suite - # Fail-fast on hung tests instead of burning the whole job budget. - # Per-test 600s timeout lives in run_all.sh. - timeout-minutes: 25 - run: | - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - test -x "$MCPP" - export MCPP - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - test -x "$MCPP_VENDORED_XLINGS" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" self config - # macOS default toolchain is LLVM - "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" - bash tests/e2e/run_all.sh diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml deleted file mode 100644 index 4290bbb7..00000000 --- a/.github/workflows/ci-macos.yml +++ /dev/null @@ -1,318 +0,0 @@ -name: ci-macos - -# macOS CI for mcpp — validates LLVM/Clang as the default macOS toolchain. -# Tests the full xlings → LLVM → C++23 import std pipeline on macOS ARM64. -# -# The e2e suite runs in PARALLEL in ci-macos-e2e.yml (same setup via -# .github/actions/setup-macos-llvm), not tacked onto this job. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-macos-${{ github.ref }} - cancel-in-progress: true - -jobs: - macos-xlings-llvm: - name: macOS ARM64 — xlings LLVM end-to-end - runs-on: macos-15 - timeout-minutes: 30 - # NOTE: no MCPP_VERBOSE here — keep this job's output shape identical to - # ci-macos-e2e.yml, which asserts mcpp's default quiet output (48/53). - steps: - - uses: actions/checkout@v4 - - - name: System info - run: | - uname -a - sw_vers - xcrun --show-sdk-path - echo "SDK: $(xcrun --show-sdk-version)" - - - uses: ./.github/actions/setup-macos-llvm - - - name: Inspect LLVM package structure - run: | - echo "=== bin/ ===" - ls "$LLVM_ROOT/bin/" | grep -E "^(clang|llvm|lld|ld)" | head -20 - echo "=== lib/ ===" - ls "$LLVM_ROOT/lib/" 2>/dev/null | head -10 - echo "=== share/libc++/ ===" - find "$LLVM_ROOT" -name "std.cppm" -o -name "std.compat.cppm" 2>/dev/null - echo "=== clang++.cfg ===" - cat "$LLVM_ROOT/bin/clang++.cfg" 2>/dev/null || echo "(no cfg file)" - echo "=== Target triple ===" - "$CXX" -dumpmachine - echo "=== Module manifest ===" - "$CXX" -print-library-module-manifest-path 2>/dev/null || echo "(not available)" - - - name: Test — non-module C++23 compilation - run: | - WORK=$(mktemp -d) - cd "$WORK" - cat > main.cpp << 'EOF' - #include - #include - int main() { - std::cout << std::format("Hello from LLVM on macOS! clang {}", __clang_version__) << std::endl; - return 0; - } - EOF - "$CXX" -std=c++23 -o hello main.cpp - ./hello - - - name: Test — import std (two-stage module compilation) - run: | - WORK=$(mktemp -d) - cd "$WORK" - - # Find std.cppm - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - if [ -z "$STD_CPPM" ]; then - echo "::error::std.cppm not found in LLVM package" - find "$LLVM_ROOT" -name "*.cppm" 2>/dev/null - exit 1 - fi - echo "std.cppm at: $STD_CPPM" - - echo "=== Step 1: Precompile std module ===" - mkdir -p pcm.cache - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - - echo "=== Step 2: Compile std.pcm → std.o ===" - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - echo "=== Step 3: Compile main.cpp with import std ===" - cat > main.cpp << 'EOF' - import std; - int main() { - std::println("C++23 import std works on macOS via xlings LLVM!"); - return 0; - } - EOF - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm -c main.cpp -o main.o - - echo "=== Step 4: Link ===" - "$CXX" main.o std.o -o hello_modules - echo "=== Step 5: Run ===" - ./hello_modules - - - name: Test — import std.compat - run: | - WORK=$(mktemp -d) - cd "$WORK" - - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - STD_COMPAT_CPPM=$(find "$LLVM_ROOT" -name "std.compat.cppm" -path "*/libc++/*" | head -1) - - if [ -z "$STD_COMPAT_CPPM" ]; then - echo "::warning::std.compat.cppm not found, skipping" - exit 0 - fi - echo "std.compat.cppm at: $STD_COMPAT_CPPM" - - mkdir -p pcm.cache - # Build std first - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - # Build std.compat (depends on std) - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - -fmodule-file=std=pcm.cache/std.pcm \ - --precompile "$STD_COMPAT_CPPM" -o pcm.cache/std.compat.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - -fmodule-file=std=pcm.cache/std.pcm \ - pcm.cache/std.compat.pcm -c -o std.compat.o - - cat > main.cpp << 'EOF' - import std.compat; - #include - int main() { - printf("std.compat works on macOS! %s\n", "success"); - return 0; - } - EOF - "$CXX" -std=c++23 \ - -fmodule-file=std=pcm.cache/std.pcm \ - -fmodule-file=std.compat=pcm.cache/std.compat.pcm \ - -c main.cpp -o main.o - "$CXX" main.o std.o std.compat.o -o compat_test - ./compat_test - - - name: Test — multi-module project - run: | - WORK=$(mktemp -d) - cd "$WORK" - - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - mkdir -p pcm.cache - - # Build std - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - # User module: greeter - cat > greeter.cppm << 'EOF' - export module greeter; - import std; - export namespace greeter { - std::string hello(std::string_view name) { - return std::format("Hello, {}! (from macOS module)", name); - } - } - EOF - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm \ - --precompile greeter.cppm -o pcm.cache/greeter.pcm - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm \ - pcm.cache/greeter.pcm -c -o greeter.o - - # Main - cat > main.cpp << 'EOF' - import std; - import greeter; - int main() { - std::println("{}", greeter::hello("mcpp")); - return 0; - } - EOF - "$CXX" -std=c++23 \ - -fmodule-file=std=pcm.cache/std.pcm \ - -fmodule-file=greeter=pcm.cache/greeter.pcm \ - -c main.cpp -o main.o - "$CXX" main.o greeter.o std.o -o multimod - ./multimod - - - name: Validate mcpp probe logic expectations - run: | - echo "=== Verifying mcpp's assumptions ===" - echo "1. -print-sysroot returns empty (mcpp falls back to xcrun):" - result=$("$CXX" -print-sysroot 2>/dev/null || true) - if [ -z "$result" ]; then - echo " PASS: empty (xcrun fallback needed)" - else - echo " INFO: $result" - fi - - echo "2. xcrun --show-sdk-path works:" - xcrun --show-sdk-path && echo " PASS" - - echo "3. -dumpmachine returns darwin triple:" - triple=$("$CXX" -dumpmachine) - echo " $triple" - echo "$triple" | grep -q "darwin" && echo " PASS: contains 'darwin'" - - echo "4. libc++ module manifest discoverable:" - manifest=$("$CXX" -print-library-module-manifest-path 2>/dev/null || true) - if [ -n "$manifest" ] && [ -f "$manifest" ]; then - echo " PASS: $manifest" - echo " Content:" - cat "$manifest" | head -20 - else - echo " INFO: manifest not via flag, using fallback path" - find "$LLVM_ROOT/share/libc++" -name "*.cppm" 2>/dev/null && echo " PASS: fallback exists" - fi - - echo "5. llvm-ar available:" - ls "$LLVM_ROOT/bin/llvm-ar" && echo " PASS" - - echo "6. clang-scan-deps available:" - ls "$LLVM_ROOT/bin/clang-scan-deps" && echo " PASS" || echo " WARN: not found" - - - name: Validate install.sh platform detection - run: | - uname_s=$(uname -s) - uname_m=$(uname -m) - echo "Platform: ${uname_s}-${uname_m}" - case "${uname_s}-${uname_m}" in - Darwin-arm64) echo "PASS: would select darwin-arm64" ;; - Darwin-x86_64) echo "PASS: would select darwin-x86_64" ;; - *) echo "FAIL: unexpected platform"; exit 1 ;; - esac - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - - - name: Unit + integration tests via `mcpp test` - run: | - # Use freshly-built mcpp (has --mirror support) - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - "$MCPP" self config --mirror GLOBAL - "$MCPP" test - - - name: Forensics — test-binary link + load state (on failure) - if: failure() - run: | - BIN=$(find target -path "*/bin/test_manifest" | head -1) - echo "binary: $BIN" - [ -n "$BIN" ] || exit 0 - echo "--- otool -L ---"; otool -L "$BIN" || true - echo "--- rpaths ---"; otool -l "$BIN" | grep -A2 LC_RPATH || true - echo "--- statically embedded libc++? ---" - nm "$BIN" 2>/dev/null | grep -cE "T __ZNSt3__1" || echo "0 (good: no libc++ code in binary)" - echo "--- direct run ---" - set +e - "$BIN" > run.out 2>&1 - echo "exit=$?" - head -20 run.out - sleep 5 - echo "--- newest crash report (termination) ---" - CR=$(ls -t "$HOME/Library/Logs/DiagnosticReports"/*.ips 2>/dev/null | head -1) - if [ -n "$CR" ]; then - python3 - "$CR" <<'PY' - import json, sys - lines = open(sys.argv[1]).read().splitlines() - meta = json.loads(lines[0]); body = json.loads("\n".join(lines[1:])) - print("proc:", meta.get("app_name"), "| exc:", body.get("exception", {})) - print("termination:", body.get("termination", {})) - t = [th for th in body.get("threads", []) if th.get("triggered")] - for fr in (t[0].get("frames", [])[:12] if t else []): - print(" ", fr.get("imageIndex"), fr.get("symbol", fr.get("imageOffset"))) - imgs = body.get("usedImages", []) - for i, im in enumerate(imgs[:12]): - print("img", i, im.get("path")) - PY - else - echo "none" - fi - - - name: "Toolchain: LLVM — build mcpp (self-host)" - run: | - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - test -x "$MCPP" - cp "$MCPP" /tmp/mcpp-fresh - MCPP=/tmp/mcpp-fresh - "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" - "$MCPP" clean - "$MCPP" build - "$MCPP" --version - - # Integration: the mcpp built from THIS PR's source (the self-host binary, - # $MCPP = /tmp/mcpp-fresh) builds & runs a real external C++ project — - # xlings (openxlings/xlings ships its own mcpp.toml). - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - MCPP=/tmp/mcpp-fresh # the freshly self-hosted binary built from this PR - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - git clone --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - "$MCPP" run diff --git a/.github/workflows/ci-windows-e2e.yml b/.github/workflows/ci-windows-e2e.yml deleted file mode 100644 index a6ff7086..00000000 --- a/.github/workflows/ci-windows-e2e.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: ci-windows-e2e - -# The e2e suite on Windows, split out of ci-windows.yml (where it was 9.7 of -# the job's 20.4 min) and sharded across two runners — same shape as -# ci-linux-e2e.yml. Shares the cache lineage in .github/actions/bootstrap-mcpp, -# so each shard restores a warm sandbox and pays one incremental `mcpp build`. -# -# Paired workflows: ci-windows.yml (build + unit + package, toolchains + -# regressions), ci-linux-e2e.yml, ci-macos-e2e.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - name: e2e ${{ matrix.shard }}/2 (windows x64, self-host) - runs-on: windows-latest - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - shard: [1, 2] - env: - MCPP_HOME: C:\Users\runneradmin\.mcpp - # Round-robin slice of tests/e2e (see run_all.sh). - E2E_SHARD: ${{ matrix.shard }}/2 - # NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that - # assert mcpp's DEFAULT (quiet) output — e.g. 48_build_error_output and - # 53_namespaced_cache_label — which forced verbose would break. - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \ - | sort -rn | head -1 | cut -d" " -f2-) - test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; } - MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF") - "$MCPP_SELF" --version - echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV" - - - name: E2E suite - shell: bash - # Fail-fast on hung tests instead of burning the whole job budget. - # Per-test 600s timeout lives in run_all.sh. - timeout-minutes: 25 - run: | - export MCPP="$MCPP_SELF" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP_SELF" self config --mirror GLOBAL - "$MCPP_SELF" toolchain default llvm@20.1.7 - bash tests/e2e/run_all.sh diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml deleted file mode 100644 index bb57f98b..00000000 --- a/.github/workflows/ci-windows.yml +++ /dev/null @@ -1,306 +0,0 @@ -name: ci-windows - -# Windows CI for mcpp — same flow as Linux (ci-linux.yml) and macOS (ci-macos.yml): -# xlings install mcpp → self-host build → smoke → package -# -# SHAPE: three INDEPENDENT jobs, no `needs:` between them; each restores the -# shared cache lineage (.github/actions/bootstrap-mcpp) and pays one warm -# `mcpp build` to get the PR's own binary. The e2e suite moved to -# ci-windows-e2e.yml (sharded ×2) — it was 9.7 of this job's 20.4 min. -# -# before: build → unit → xlings → stdin → e2e → toolchains → package ≈ 20 min -# after: max(build+unit+package, toolchains+integration) ≈ 8 min -# in parallel with ci-windows-e2e (≈ 8 min) - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-windows-${{ github.ref }} - cancel-in-progress: true - -env: - MCPP_HOME: C:\Users\runneradmin\.mcpp - -jobs: - build-test: - name: build + test + package (windows x64, self-host) - runs-on: windows-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \ - | sort -rn | head -1 | cut -d" " -f2-) - test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; } - MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF") - "$MCPP_SELF" --version - echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV" - - - name: Unit + integration tests via mcpp test - shell: bash - run: | - export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") - "$MCPP_SELF" test - - - name: Package Windows release zip - id: package - shell: bash - run: | - VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - WRAPPER="mcpp-${VERSION}-windows-x86_64" - ZIPNAME="${WRAPPER}.zip" - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - MCPP_BIN=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \ - | sort -rn | head -1 | cut -d" " -f2-) - test -n "$MCPP_BIN" || { echo "FAIL: no mcpp.exe in target/"; exit 1; } - - STAGING=$(mktemp -d) - mkdir -p "$STAGING/$WRAPPER/bin" "$STAGING/$WRAPPER/registry/bin" - cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp.exe" - printf '@echo off\r\n"%%~dp0bin\\mcpp.exe" %%*\r\n' > "$STAGING/$WRAPPER/mcpp.bat" - cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true - cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true - XLINGS_EXE="$USERPROFILE/.xlings/subos/default/bin/xlings.exe" - [ -f "$XLINGS_EXE" ] && cp "$XLINGS_EXE" "$STAGING/$WRAPPER/registry/bin/xlings.exe" - - mkdir -p dist - (cd "$STAGING" && 7z a -tzip "$ZIPNAME" "$WRAPPER") - cp "$STAGING/$ZIPNAME" "dist/$ZIPNAME" - (cd dist && sha256sum "$ZIPNAME" > "$ZIPNAME.sha256") - echo "zipname=$ZIPNAME" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke-test the packaged zip - shell: bash - run: | - ZIPNAME="${{ steps.package.outputs.zipname }}" - WRAPPER="${ZIPNAME%.zip}" - SMOKE=$(mktemp -d) - (cd "$SMOKE" && unzip -q "$GITHUB_WORKSPACE/dist/$ZIPNAME") - "$SMOKE/$WRAPPER/bin/mcpp.exe" --version - test -f "$SMOKE/$WRAPPER/registry/bin/xlings.exe" - test -f "$SMOKE/$WRAPPER/mcpp.bat" - echo "Smoke-test passed" - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: mcpp-windows-x86_64 - path: | - dist/*.zip - dist/*.sha256 - - # Everything that needs a toolchain other than the default, plus the two - # Windows-specific behavioural regressions. Kept on one runner: each leg is - # seconds-to-2-minutes, so per-leg runners would cost more setup than they - # save. - toolchains: - name: "toolchains + regressions (windows x64)" - runs-on: windows-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \ - | sort -rn | head -1 | cut -d" " -f2-) - test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; } - MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF") - echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV" - - # Integration: the mcpp built from THIS PR's source ($MCPP_SELF, the - # self-hosted binary) builds & runs a real external C++ project — xlings - # (openxlings/xlings ships its own mcpp.toml). MCPP_VENDORED_XLINGS only - # supplies the xlings package backend mcpp resolves deps through. - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - shell: bash - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") - git clone --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP_SELF" self config --mirror GLOBAL - "$MCPP_SELF" build - "$MCPP_SELF" run - - # Regression test for the Windows first-run "press Enter to advance" hang. - # Launches mcpp with an OPEN, EMPTY, never-closing stdin pipe. Without - # seal_stdin's Windows fix, any grandchild that reads stdin would inherit - # our pipe and block forever — caught by the timeout below. With the fix, - # every subprocess stdin is redirected from NUL → no possibility of hang. - - name: "Regression: mcpp survives open-empty-stdin (Windows hang fix)" - shell: pwsh - timeout-minutes: 15 - env: - MCPP_VENDORED_XLINGS: ${{ env.XLINGS_BIN }} - run: | - $ErrorActionPreference = 'Stop' - - # MCPP_SELF was set in a bash step as an MSYS-style path - # (e.g. /d/a/mcpp/...). PowerShell can't exec that — convert it - # to a native Windows path via the git-bash cygpath that ships - # on the runner. - $mcppExe = (& 'C:\Program Files\Git\usr\bin\cygpath.exe' -w $env:MCPP_SELF).Trim() - Write-Host "Resolved MCPP_SELF (Windows form): $mcppExe" - if (-not (Test-Path $mcppExe)) { - throw "MCPP_SELF after cygpath not found: $mcppExe" - } - - $tmp = Join-Path $env:RUNNER_TEMP ("stdin-hang-test-" + [guid]::NewGuid().ToString('N')) - New-Item -ItemType Directory -Path $tmp | Out-Null - Set-Location $tmp - & $mcppExe new hello_stdin - Set-Location hello_stdin - - function Invoke-McppWithOpenStdin { - param([string]$McppPath, [string]$McppArgs, [int]$TimeoutSeconds = 300) - - $psi = [System.Diagnostics.ProcessStartInfo]::new() - $psi.FileName = $McppPath - $psi.Arguments = $McppArgs - $psi.WorkingDirectory = (Get-Location).Path - $psi.UseShellExecute = $false - $psi.RedirectStandardInput = $true # parent holds child's stdin - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - # By default the child inherits the parent's env (we did not - # touch $psi.Environment) so MCPP_VENDORED_XLINGS / PATH / etc. - # propagate. - - $p = [System.Diagnostics.Process]::Start($psi) - - # Async-drain stdout/stderr so a full output buffer doesn't - # itself deadlock the child (separate failure mode from the - # stdin hang we're testing). - $stdoutTask = $p.StandardOutput.ReadToEndAsync() - $stderrTask = $p.StandardError.ReadToEndAsync() - - # NEVER write or close $p.StandardInput — the pipe stays open - # and empty for the lifetime of the child. Any grandchild that - # reads stdin will block on this pipe → caught by WaitForExit. - - if (-not $p.WaitForExit($TimeoutSeconds * 1000)) { - try { $p.Kill($true) } catch {} - Write-Host "----- captured stdout -----" - Write-Host $stdoutTask.Result - Write-Host "----- captured stderr -----" - Write-Host $stderrTask.Result - throw "REGRESSION: 'mcpp $McppArgs' HUNG with open-empty stdin after ${TimeoutSeconds}s. The Windows seal_stdin fix is not effective." - } - - Write-Host "----- stdout -----" - Write-Host $stdoutTask.Result - Write-Host "----- stderr -----" - Write-Host $stderrTask.Result - - if ($p.ExitCode -ne 0) { - throw "'mcpp $McppArgs' exited with code $($p.ExitCode) (no hang, but failed)." - } - } - - Write-Host '=== T1: mcpp --version (sanity, fast path) ===' - Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs '--version' -TimeoutSeconds 30 - - Write-Host '=== T2: mcpp build (full bootstrap + toolchain + dep resolve + compile) ===' - Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs 'build' -TimeoutSeconds 600 - - Write-Host '=== T3: mcpp run (post-build run path) ===' - Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs 'run' -TimeoutSeconds 120 - - Write-Host 'SUCCESS: mcpp completes with open-empty stdin → Windows seal_stdin fix verified.' - - - name: "Toolchain: LLVM — mcpp new → run" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - TMP=$(mktemp -d) - cd "$TMP" - "$MCPP_SELF" new hello_win - cd hello_win - "$MCPP_SELF" run - - # MinGW-w64 GCC via the xlings ecosystem (xim:mingw-gcc → xlings-res - # winlibs mirror): install → default → modules build/run → standalone - # exe. Same flow as e2e 97 but as a visible CI step. Payload is cached - # via the mcpp sandbox cache after the first run. - - name: "Toolchain: MinGW — install → build → run (xim:mingw-gcc)" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - MCPP="$MCPP_SELF" bash tests/e2e/97_mingw_toolchain.sh - - # windows-latest ships VS 2022 Enterprise with the VC workload, so - # msvc@system detection MUST succeed here — a failure is a regression - # in the discovery/identification chain (vswhere → env → paths). - # Runs BEFORE the LLVM self-host rebuild: that step cleans + rebuilds - # target/, invalidating this job's $MCPP_SELF fingerprint path. - - name: "Toolchain: MSVC — detection & selection (msvc@system)" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - - # Neutral cwd: the repo root's mcpp.toml [toolchain] would shadow - # the global default in `toolchain list` / doctor output. - TMP=$(mktemp -d); cd "$TMP" - - out=$("$MCPP_SELF" toolchain default msvc); echo "$out" - grep -q "Detected" <<<"$out" - grep -q "msvc@system" <<<"$out" - - "$MCPP_SELF" toolchain list | tee tc-list.txt - grep -E '\*\s*msvc' tc-list.txt - - "$MCPP_SELF" self doctor 2>&1 | tee doctor.txt || true - grep -qi "msvc" doctor.txt - - # native cl.exe build: full e2e (modules, import std, incremental) - cd "$GITHUB_WORKSPACE" - MCPP="$MCPP_SELF" bash tests/e2e/99_msvc_native_build.sh - - # restore the LLVM default for the remaining steps - "$MCPP_SELF" toolchain default llvm@20.1.7 - - - name: "Toolchain: LLVM — build mcpp (self-host)" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - cp "$MCPP_SELF" /tmp/mcpp-fresh.exe - MCPP=/tmp/mcpp-fresh.exe - "$MCPP" toolchain default llvm@20.1.7 - "$MCPP" clean --bmi-cache - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml deleted file mode 100644 index f2ef366d..00000000 --- a/.github/workflows/cross-build-test.yml +++ /dev/null @@ -1,287 +0,0 @@ -name: cross-build-test - -# mcpp cross-build test — the single source of truth for "which CROSS-build -# target combinations mcpp supports", verified end-to-end. -# -# Cross = host arch ≠ target arch. Verification targets are mcpp ITSELF and -# xlings (real, self-hosting C++23 module projects), cross-built from source for -# each target triple, arch-checked, and smoke-run under qemu-user. -# -# ── Supported cross matrix (built + verified below) ──────────────────────── -# target | toolchain | host→target | run -# ----------------------|---------------------------------|---------------|----- -# aarch64-linux-musl | aarch64-linux-musl-gcc@16.1.0 | x86_64→arm64 | qemu -# x86_64-w64-mingw32 | mingw-cross-gcc@16.1.0 (MSVCRT) | linux→windows | wine -# -# The mingw row is OS-cross (same arch, different OS/ABI: ELF→PE), so it lives -# in its own job below with wine verification instead of the qemu arch matrix. -# See .agents/docs/2026-07-15-mingw-linux-cross-windows-design.md. -# -# mcpp resolves a cross `--target -musl` build to the triple-named cross -# gcc musl toolchain from the xlings ecosystem (xim:-gcc, see -# src/build/prepare.cppm). Output is a fully static musl ELF (no PT_INTERP), -# which also makes the aarch64 artefact runnable natively in Termux/Android — -# qemu-aarch64 is the CI proxy for "does this cross artefact actually execute". -# -# ── NOT here ─────────────────────────────────────────────────────────────── -# * Same-arch builds (host arch == target arch) are NOT cross. The native musl -# static build `--target x86_64-linux-musl` (x86_64 host) is exercised by -# ci-linux.yml's "Toolchain: musl-gcc" step, and release.yml for the static -# release artefact. Keep them there; this file is cross-arch only. -# -# ── Planned cross rows (documented; NOT yet wired in mcpp — keep as comments) ─ -# * llvm/clang cross : clang is inherently a cross-compiler, but mcpp does not -# yet inject `-target ` + a cross sysroot for a -# clang toolchain; cross `--target` resolves to gcc musl -# only. Wire the clang cross path first, then add a row. -# * riscv64-linux-musl: add once xim:riscv64-linux-musl-gcc ships to -# xlings-res + xim-pkgindex. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - cross-build: - name: cross-build ${{ matrix.target }} (mcpp + xlings) - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-linux-musl - file_arch: "ARM aarch64" - qemu_bin: qemu-aarch64-static - env: - MCPP_HOME: /home/runner/.mcpp - # Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}- - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-v2- - - - name: Install qemu-user-static - run: | - sudo apt-get update -qq - sudo apt-get install -y qemu-user-static - ${{ matrix.qemu_bin }} --version | head -1 - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - # Must equal `pinned::kXlingsVersion` (src/xlings.cppm) and the - # xlings the release bundles — enforced by - # .github/tools/check_version_pins.sh. - # - # Floors worth remembering. 0.4.67 carried the - # multi-index_repo install fix (openxlings/xlings#374); 0.4.68 adds - # per-repo index artifact sources (openxlings/xlings#377) so the - # mcpplibs index syncs via artifact with git as fallback (mcpp#269); - # 0.4.69 keys the index by (namespace, name) so two packages sharing - # a short name in ONE index are both addressable (openxlings/xlings#381) - # — the floor for SPEC-001 short-name descriptors. - # A past 0.4.61 "download 404 - # for mcpp@" was NOT a version bug — the xlings-res/mcpp GitHub - # release assets were uploaded in a broken state (records present, - # blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX - # half is handled by the marker-clear below. - XLINGS_VERSION: '2026.7.27.2' - run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - # Force a real index re-sync even on a warm cache: drop the TTL refresh - # markers so `xlings update` actually pulls the latest index (sees the - # current bootstrap pin) while the toolchain payloads stay cached. - find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true - xlings config --mirror GLOBAL 2>/dev/null || true - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - # MCPP_BOOT is what actually runs the bootstrap build below, so it — - # not just MCPP — has to be the pinned binary. It used to be the shim - # in subos/default/bin, which resolves to whatever version xvm has - # selected; pinning only MCPP would have looked right and changed - # nothing. - MCPP_BOOT=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") - echo "MCPP=$MCPP_BOOT" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" - - - name: Self-host build (bootstrap mcpp -> fresh host mcpp) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP_BOOT" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP_BOOT" build - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - "$MCPP" self config --mirror GLOBAL - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: "Cross-build mcpp -> ${{ matrix.target }}" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build --target ${{ matrix.target }} - bin=$(find target/${{ matrix.target }} -type f -name mcpp | head -1) - [ -n "$bin" ] || { echo "no mcpp artefact for ${{ matrix.target }}"; exit 1; } - echo "== file =="; file "$bin" - file "$bin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; } - file "$bin" | grep -q "statically linked" || { echo "expected static"; exit 1; } - echo "MCPP_XBIN=$bin" >> "$GITHUB_ENV" - - - name: "Cross-build xlings -> ${{ matrix.target }}" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build --target ${{ matrix.target }} - xbin=$(find target/${{ matrix.target }} -type f -name xlings | head -1) - [ -n "$xbin" ] || { echo "no xlings artefact for ${{ matrix.target }}"; exit 1; } - echo "== file =="; file "$xbin" - file "$xbin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; } - file "$xbin" | grep -q "statically linked" || { echo "expected static"; exit 1; } - echo "XLINGS_XBIN=$xbin" >> "$GITHUB_ENV" - - - name: "Smoke-run cross artefacts under qemu" - run: | - RUN="${{ matrix.qemu_bin }}" - # mcpp is self-contained, so --version runs cleanly under bare qemu — - # this is the hard execution proof for the cross artefact. - echo "== mcpp --version ==" - mver=$($RUN "$MCPP_XBIN" --version) - echo "$mver"; echo "$mver" | grep -q "mcpp" || { echo "mcpp --version failed"; exit 1; } - # xlings expects a real runtime environment (sandbox/config) and may - # exit non-zero on a bare `--version` under qemu; its ELF arch + static - # linkage were already asserted in the build step, so treat execution - # here as best-effort rather than gating. - echo "== xlings --version (best-effort under qemu) ==" - xver=$($RUN "$XLINGS_XBIN" --version 2>&1 || true) - echo "$xver" - - # ── Linux → Windows MinGW cross (OS-cross, same arch: ELF→PE) ───────────── - # Builds a demo project for x86_64-w64-mingw32 with the from-source GCC-16 - # MSVCRT cross toolchain, asserts the artefact is a fully-static PE, and runs - # it under wine. Delegated to the e2e harness (tests/e2e/102_mingw_cross_wine.sh, - # `# requires: mingw-cross wine`) so the run_all cap-gating stays the single - # source of truth. See 2026-07-15-mingw-linux-cross-windows-design.md Part C. - mingw-cross-wine: - name: mingw-cross linux→windows (build + wine run) - runs-on: ubuntu-24.04 - timeout-minutes: 60 - env: - MCPP_HOME: /home/runner/.mcpp - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-mingw-cross-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-mingw-cross- - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-v2- - - # wine 的包集固定不变 —— 缓存整个 .deb 依赖闭包,命中时跳过 apt update - # 与下载(每轮省 ~1-2min)。镜像月度更新可能改变依赖缺口,dpkg -i 失败时 - # 由 apt-get -f 兜底并重新回填缓存。 - - name: Cache wine debs - uses: actions/cache@v4 - with: - path: ~/wine-debs - key: wine-debs-${{ runner.os }}-ubuntu24.04-v1 - - - name: Install wine - run: | - sudo dpkg --add-architecture i386 || true - if ls ~/wine-debs/*.deb >/dev/null 2>&1; then - sudo dpkg -i ~/wine-debs/*.deb 2>/dev/null \ - || { sudo apt-get update -qq; sudo apt-get install -f -y; } - else - sudo apt-get update -qq - sudo apt-get install -y --download-only wine64 wine \ - || sudo apt-get install -y --download-only wine - mkdir -p ~/wine-debs - cp /var/cache/apt/archives/*.deb ~/wine-debs/ 2>/dev/null || true - sudo apt-get install -y wine64 wine || sudo apt-get install -y wine - fi - wine --version - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.7.27.2' - run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true - xlings config --mirror GLOBAL 2>/dev/null || true - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - # MCPP_BOOT is what actually runs the bootstrap build below, so it — - # not just MCPP — has to be the pinned binary. It used to be the shim - # in subos/default/bin, which resolves to whatever version xvm has - # selected; pinning only MCPP would have looked right and changed - # nothing. - MCPP_BOOT=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") - echo "MCPP=$MCPP_BOOT" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" - - - name: Self-host build (fresh host mcpp) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP_BOOT" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP_BOOT" build - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - "$MCPP" self config --mirror GLOBAL - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: Install mingw-cross toolchain - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" toolchain install mingw-cross 16.1.0 - - - name: "e2e: cross-build + wine run" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - bash tests/e2e/102_mingw_cross_wine.sh diff --git a/.github/workflows/probe-gitcode-upload.yml b/.github/workflows/probe-gitcode-upload.yml new file mode 100644 index 00000000..dff757c1 --- /dev/null +++ b/.github/workflows/probe-gitcode-upload.yml @@ -0,0 +1,210 @@ +name: probe-gitcode-upload + +# TEMPORARY diagnostic workflow — delete with this branch. +# +# WHY. `publish-ecosystem`'s GitCode leg has now blown its 180s per-asset cap +# on four separate releases (0.0.94 / 0.0.97 / 0.0.105 / 2026.7.28.2), each +# time on the two biggest tarballs, each time costing ~5min of manual +# re-upload. Every previous fix was a guess about WHY it is slow (raise the +# cap, skip-don't-retry, batch verify). This workflow measures instead. +# +# Each job isolates ONE hypothesis, and they run in parallel so a single push +# answers all of them: +# +# baseline Is it the network, the direction, or GitCode specifically? +# Down/up control against GitHub from the same runner. +# sizes-urllib Does throughput collapse with size, or is it ~constant? +# (constant MB/s => a pure bandwidth wall, not a stall.) +# sizes-curl Is Python's read-it-all-then-PUT the bottleneck, or the wire? +# concurrency Is bandwidth per-connection or per-host? If per-connection, +# uploading assets in parallel is the whole fix — mirror_res.sh +# currently uploads them SERIALLY within a host leg. +# +# Every job writes NDJSON to the step summary so results are comparable +# without opening logs. + +on: + push: + branches: [ 'probe/**' ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: probe-gitcode-${{ github.ref }} + cancel-in-progress: true + +env: + GTC_REPO: xlings-res/mcpp + # A real, already-mirrored asset used as the download reference (34.8MB). + REF_ASSET: mcpp-2026.7.28.2-linux-x86_64.tar.gz + REF_TAG: 2026.7.28.2 + +jobs: + # ── H0: characterise the pipe itself, both directions, both hosts ──────── + baseline: + name: baseline — network + GitHub control + runs-on: ubuntu-latest + timeout-minutes: 25 + env: + GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} + GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} + steps: + - uses: actions/checkout@v4 + - name: Runner egress identity + run: | + echo "runner public IP / region:" + curl -s --max-time 20 https://ipinfo.io/json || echo "(ipinfo unavailable)" + + - name: Connect/TLS timing to each host + run: | + fmt=' dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s code=%{http_code}\n' + for h in https://api.gitcode.com https://gitcode.com https://api.github.com https://github.com; do + echo "$h" + curl -sS -o /dev/null --max-time 60 -w "$fmt" "$h" || echo " (failed)" + done + + - name: Download throughput — gitcode vs github (same 34.8MB asset) + run: | + fmt=' code=%{http_code} bytes=%{size_download} speed=%{speed_download} B/s total=%{time_total}s\n' + echo "gitcode.com:" + curl -sSL -o /dev/null --max-time 900 -w "$fmt" \ + "https://gitcode.com/${GTC_REPO}/releases/download/${REF_TAG}/${REF_ASSET}" || true + echo "github.com:" + curl -sSL -o /dev/null --max-time 900 -w "$fmt" \ + "https://github.com/${GTC_REPO}/releases/download/${REF_TAG}/${REF_ASSET}" || true + + - name: Upload control — 32MB to GitHub from this same runner + run: | + set -x + head -c 33554432 /dev/urandom > probe-github-32m.bin + gh release view "probe-${{ github.run_id }}" -R "$GTC_REPO" >/dev/null 2>&1 \ + || gh release create "probe-${{ github.run_id }}" -R "$GTC_REPO" \ + --title "probe ${{ github.run_id }}" --notes "temporary upload probe; safe to delete" + start=$SECONDS + gh release upload "probe-${{ github.run_id }}" probe-github-32m.bin -R "$GTC_REPO" --clobber + echo "GITHUB_UPLOAD_32MB_SECONDS=$((SECONDS - start))" + + # ── H1: is throughput size-dependent (stall) or flat (bandwidth wall)? ─── + sizes-urllib: + name: sizes — current urllib transport + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} + TAG: probe-${{ github.run_id }}-urllib + steps: + - uses: actions/checkout@v4 + - name: Create probe release + run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG" + - name: Upload 1 / 4 / 16 / 32 / 32 MB (serial) + run: | + # 32MB twice: run-to-run variance matters as much as the mean when + # deciding whether a fixed 180s cap can ever be safe. + for spec in 1 4 16 32 32; do + f="probe-urllib-${spec}m-$RANDOM.bin" + head -c $((spec * 1048576)) /dev/urandom > "$f" + python3 .github/tools/probe_gtc_upload.py \ + --repo "$GTC_REPO" --tag "$TAG" --file "$f" \ + --method urllib --label "urllib-${spec}MB" >> results.ndjson + rm -f "$f" + done + - name: Summary + if: always() + run: | + { echo '### sizes — urllib'; echo '```json'; cat results.ndjson; echo '```'; } \ + >> "$GITHUB_STEP_SUMMARY" + + # ── H2: is the Python client the bottleneck, or the wire? ──────────────── + sizes-curl: + name: sizes — curl streaming transport + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} + TAG: probe-${{ github.run_id }}-curl + steps: + - uses: actions/checkout@v4 + - name: Create probe release + run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG" + - name: Upload 1 / 4 / 16 / 32 / 32 MB (serial) + run: | + for spec in 1 4 16 32 32; do + f="probe-curl-${spec}m-$RANDOM.bin" + head -c $((spec * 1048576)) /dev/urandom > "$f" + python3 .github/tools/probe_gtc_upload.py \ + --repo "$GTC_REPO" --tag "$TAG" --file "$f" \ + --method curl --label "curl-${spec}MB" >> results.ndjson + rm -f "$f" + done + - name: Summary + if: always() + run: | + { echo '### sizes — curl'; echo '```json'; cat results.ndjson; echo '```'; } \ + >> "$GITHUB_STEP_SUMMARY" + + # ── H3: per-connection cap or per-host cap? This decides whether simply + # parallelising mirror_res.sh's serial per-asset loop is the fix. ── + concurrency: + name: concurrency — 4x8MB serial vs parallel + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} + TAG: probe-${{ github.run_id }}-conc + steps: + - uses: actions/checkout@v4 + - name: Create probe release + run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG" + - name: Serial 4x8MB + run: | + for i in 1 2 3 4; do head -c 8388608 /dev/urandom > "ser-$i.bin"; done + start=$SECONDS + for i in 1 2 3 4; do + python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \ + --file "ser-$i.bin" --method curl --label "serial-$i" >> results.ndjson + done + echo "SERIAL_WALL_SECONDS=$((SECONDS - start))" | tee -a wall.txt + - name: Parallel 4x8MB + run: | + for i in 1 2 3 4; do head -c 8388608 /dev/urandom > "par-$i.bin"; done + start=$SECONDS + for i in 1 2 3 4; do + python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \ + --file "par-$i.bin" --method curl --label "parallel-$i" >> "par-$i.json" & + done + wait + echo "PARALLEL_WALL_SECONDS=$((SECONDS - start))" | tee -a wall.txt + cat par-*.json >> results.ndjson + - name: Summary + if: always() + run: | + { echo '### concurrency'; echo '```'; cat wall.txt; echo '```'; + echo '```json'; cat results.ndjson; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + + # ── Leave no garbage on the resource repo ─────────────────────────────── + cleanup: + name: cleanup probe tags + needs: [baseline, sizes-urllib, sizes-curl, concurrency] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} + GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} + steps: + - name: Delete GitCode probe tags (deleting the tag deletes the release) + run: | + for t in "probe-${{ github.run_id }}-urllib" \ + "probe-${{ github.run_id }}-curl" \ + "probe-${{ github.run_id }}-conc"; do + code=$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE \ + -H "PRIVATE-TOKEN: $GITCODE_TOKEN" \ + "https://api.gitcode.com/api/v5/repos/${GTC_REPO}/tags/${t}" || echo ERR) + echo "delete $t -> $code" + done + - name: Delete GitHub probe release + run: | + gh release delete "probe-${{ github.run_id }}" -R "$GTC_REPO" --yes --cleanup-tag \ + || echo "(nothing to delete)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index db469053..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,774 +0,0 @@ -name: release - -# Self-host release: bootstrap mcpp from xlings (xim:mcpp), build the -# musl-static artefact via `mcpp pack --target x86_64-linux-musl -o ...`, -# inject xlings into the produced tarball for install.sh consumers, -# smoke-test, upload. - -on: - push: - tags: [ 'v*' ] - workflow_dispatch: - inputs: - tag: - description: 'tag to (re)build — leave blank to derive `v` from mcpp.toml and create the tag automatically' - required: false - -jobs: - build-release: - name: build + upload (linux / x86_64) - runs-on: ubuntu-24.04 - permissions: - contents: write # required to create releases + push tags - timeout-minutes: 60 - env: - # mcpp resolves MCPP_HOME from the binary's location by default, - # but here we want to share toolchains with the bootstrap sandbox, - # so we pin to a known path. - MCPP_HOME: /home/runner/.mcpp - steps: - # fetch-depth: 0 instead of fetch-tags: true — actions/checkout@v4 - # fails on push-tag triggers when both the ref'd tag and - # `fetch-tags: true` are set: - # "Cannot fetch both and refs/tags/vX.Y.Z to refs/tags/vX.Y.Z" - # Full-history fetch covers the resolve-tag step's needs without - # that contention. - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve target tag + commit - id: resolve - # Three trigger shapes converge here: - # 1. push: refs/tags/vX.Y.Z → use that tag, build at its commit - # 2. workflow_dispatch with `tag` input set: - # - tag exists on remote → check it out (rebuild scenario) - # - tag doesn't exist → use current HEAD; gh-release - # creates the tag at that commit on upload - # 3. workflow_dispatch with no input → derive `v` from - # mcpp.toml's [package].version, build at current HEAD; - # gh-release creates the tag. - run: | - if [ "${{ github.event_name }}" = "push" ]; then - TAG="${{ github.ref_name }}" - elif [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - else - VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - test -n "$VER" || { echo 'failed to read [package].version from mcpp.toml'; exit 1; } - TAG="v$VER" - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - # If the tag exists on remote AND we're on workflow_dispatch, - # check it out so we rebuild that exact commit. push-tag runs - # already start at the tag commit. - if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ - && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then - git checkout --detach "refs/tags/$TAG" - fi - echo "Resolved tag: $TAG (commit $(git rev-parse --short HEAD))" - - # Cache mcpp's sandbox: musl-gcc 15.1 + binutils + glibc + linux-headers - # + patchelf + ninja is ~800 MB on disk; without this every release - # rebuilds from cold install. Key on the workspace manifest so a - # toolchain change in mcpp.toml refreshes the cache. - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-release-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-release- - - # Cache xlings + xim:mcpp install. - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-${{ runner.os }}-release-xl0462-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-release-xl0462- - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - # Pin xlings to a known-good version. The upstream install - # script always grabs `latest` (no version override), so we - # download + self-install manually to avoid broken releases. - XLINGS_VERSION: '2026.7.27.2' - run: | - if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/openxlings/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - fi - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - # Pinned to .xlings.json — a bare `xlings install mcpp` resolves - # "newest in this runner's index copy" and put 0.0.105 (below the - # index floor) into this job. See .github/tools/install_pinned_mcpp.sh. - MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Build + pack release artefact (musl static) - id: stage - # Build for the musl-static target, strip the produced ELF, then - # let `mcpp pack` assemble the tarball (binary + top-level wrapper - # + README + LICENSE, contents at archive root). Inject xlings - # afterwards so install.sh consumers get a single self-contained - # bundle. - run: | - TAG="${{ steps.resolve.outputs.tag }}" - VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="mcpp-${VERSION}-linux-x86_64.tar.gz" - - # Build first so we can strip the ELF before pack copies it. - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build --target x86_64-linux-musl - ARTIFACT=$(find target/x86_64-linux-musl -type f -name mcpp | head -1) - test -n "$ARTIFACT" - file "$ARTIFACT" | grep -q 'statically linked' - # Strip — debug info on a static ELF balloons it ~7×. - strip "$ARTIFACT" - - # Pack with the freshly-built mcpp (not the bootstrap) so any - # fixes to the pack code path are exercised in the same release - # they ship in. MCPP_HOME is forced so the new binary uses the - # pinned sandbox instead of resolving relative to its own - # location under target/. - MCPP_HOME="$MCPP_HOME" "$ARTIFACT" pack \ - --target x86_64-linux-musl \ - --mode static \ - -o "${TARBALL_NAME}" - - # Inject xlings: extract → add registry/bin/xlings to the wrapper - # dir → re-tar preserving the wrapper. Since 0.0.4 the bundled - # xlings lives at /registry/bin/xlings (= /bin/xlings). - TARBALL="target/dist/${TARBALL_NAME}" - WRAPPER="${TARBALL_NAME%.tar.gz}" - test -f "$TARBALL" - INJECT=$(mktemp -d) - tar -xzf "$TARBALL" -C "$INJECT" - mkdir -p "$INJECT/$WRAPPER/registry/bin" - cp "$XLINGS_BIN" "$INJECT/$WRAPPER/registry/bin/xlings" - chmod +x "$INJECT/$WRAPPER/registry/bin/xlings" - (cd "$INJECT" && tar -czf "$GITHUB_WORKSPACE/${TARBALL}" "$WRAPPER") - rm -rf "$INJECT" - - # Stage final dist/ (tarball + sidecars) for upload. - mkdir -p dist - cp "$TARBALL" "dist/${TARBALL_NAME}" - (cd dist && cp "${TARBALL_NAME}" "mcpp-linux-x86_64.tar.gz") - (cd dist && sha256sum "${TARBALL_NAME}" "mcpp-linux-x86_64.tar.gz" > SHA256SUMS) - (cd dist && sha256sum "${TARBALL_NAME}" > "${TARBALL_NAME}.sha256") - (cd dist && sha256sum "mcpp-linux-x86_64.tar.gz" > "mcpp-linux-x86_64.tar.gz.sha256") - - # Top-level install.sh — fetched by `curl | bash`. - cp install.sh dist/install.sh - chmod +x dist/install.sh - - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "tarball=${TARBALL_NAME}" >> $GITHUB_OUTPUT - ls -la dist/ - - - name: Smoke-test the bundled tarball - # Extract to a scratch dir and run mcpp from there with MCPP_HOME - # unset — proves the release artefact is genuinely self-contained. - run: | - VERSION="${{ steps.stage.outputs.version }}" - TARBALL_NAME="${{ steps.stage.outputs.tarball }}" - # Wrapper dir inside the tarball matches its stem (mcpp pack - # ties the two together). - WRAPPER="${TARBALL_NAME%.tar.gz}" - SMOKE=$(mktemp -d) - tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" - ROOT="$SMOKE/$WRAPPER" - test -x "$ROOT/bin/mcpp" - test -x "$ROOT/registry/bin/xlings" - test -x "$ROOT/mcpp" - file "$ROOT/bin/mcpp" | grep -q 'statically linked' - env -u MCPP_HOME "$ROOT/bin/mcpp" --version - env -u MCPP_HOME "$ROOT/bin/mcpp" --help | head -10 - # Top-level wrapper reports the same version we're shipping. - env -u MCPP_HOME "$ROOT/mcpp" --version | grep -q "$VERSION" - # MCPP_HOME should auto-resolve to the extracted root. - out=$(env -u MCPP_HOME "$ROOT/bin/mcpp" self env) - echo "$out" | grep -q "MCPP_HOME *= *$ROOT" - - - name: Generate source tarball + xpkg.lua via mcpp publish - # Use the freshly-built mcpp to produce the source tarball + xpkg - # descriptor for mcpp-index. The release tarball wraps its - # contents in a `/` directory so the extract path - # is $PUB/$WRAPPER/bin/mcpp. - run: | - VERSION="${{ steps.stage.outputs.version }}" - TARBALL_NAME="${{ steps.stage.outputs.tarball }}" - WRAPPER="${TARBALL_NAME%.tar.gz}" - PUB=$(mktemp -d) - tar -xzf "dist/${TARBALL_NAME}" -C "$PUB" - MCPP_BIN="$PUB/$WRAPPER/bin/mcpp" - env -u MCPP_HOME "$MCPP_BIN" publish --dry-run --allow-dirty - test -f "target/dist/mcpp-${VERSION}.tar.gz" - test -f "target/dist/mcpp.lua" - cp "target/dist/mcpp-${VERSION}.tar.gz" dist/ - cp "target/dist/mcpp.lua" dist/ - ls -la dist/ - - - name: Extract release notes from CHANGELOG - id: notes - run: | - TAG="${{ steps.stage.outputs.tag }}" - VERSION="${{ steps.stage.outputs.version }}" - awk -v v="$VERSION" ' - /^## \[/ { - if (in_section) exit - if ($0 ~ "\\[" v "\\]") { in_section=1; next } - } - in_section { print } - ' CHANGELOG.md > dist/RELEASE_NOTES.md || true - if [ ! -s dist/RELEASE_NOTES.md ]; then - echo "(no CHANGELOG entry found for $VERSION)" > dist/RELEASE_NOTES.md - fi - echo "--- RELEASE_NOTES.md ---" - cat dist/RELEASE_NOTES.md - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.stage.outputs.tag }} - name: ${{ steps.stage.outputs.tag }} - body_path: dist/RELEASE_NOTES.md - draft: false - prerelease: false - files: | - dist/mcpp-${{ steps.stage.outputs.version }}-linux-x86_64.tar.gz - dist/mcpp-${{ steps.stage.outputs.version }}-linux-x86_64.tar.gz.sha256 - dist/mcpp-linux-x86_64.tar.gz - dist/mcpp-linux-x86_64.tar.gz.sha256 - dist/install.sh - dist/SHA256SUMS - dist/mcpp-${{ steps.stage.outputs.version }}.tar.gz - dist/mcpp.lua - - build-linux-aarch64: - name: build (linux / aarch64, cross) - runs-on: ubuntu-24.04 - needs: build-release - permissions: - contents: write - timeout-minutes: 70 - steps: - - uses: actions/checkout@v4 - - - name: Install system deps + qemu - run: | - sudo apt-get update -qq - sudo apt-get install -y curl git build-essential qemu-user-static - qemu-aarch64-static --version | head -1 - - - name: Resolve tag + version - id: resolve - run: | - VERSION=$(grep -E '^version' mcpp.toml | head -1 | sed 's/.*"\([^"]*\)".*/\1/') - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.7.27.2' - run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/openxlings/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Bootstrap mcpp + refresh index (latest, GLOBAL) - run: | - xlings config --mirror GLOBAL 2>/dev/null || true - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - "$MCPP" --version - - - name: Cross-build mcpp -> aarch64-linux-musl (this release's source) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # "$MCPP", not a bare `mcpp`: the bare form runs the PATH shim, which - # resolves to whatever version xvm has selected — so pinning the - # bootstrap would have looked correct and changed nothing here. - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - # The published bootstrap mcpp predates aarch64 cross-build support - # (the feature landed after the last release), so it resolves the - # x86_64 host musl-gcc for an aarch64 target. Two-stage instead: build - # THIS release's x86_64 mcpp first, then cross-build aarch64 with it. - "$MCPP" build --target x86_64-linux-musl - FRESH=$(find target/x86_64-linux-musl -type f -name mcpp | head -1) - test -n "$FRESH" - "$FRESH" build --target aarch64-linux-musl - BIN=$(find target/aarch64-linux-musl -type f -name mcpp | head -1) - test -n "$BIN" - file "$BIN" | grep -q 'ARM aarch64' - file "$BIN" | grep -q 'statically linked' - echo "MCPP_AARCH64=$GITHUB_WORKSPACE/$BIN" >> "$GITHUB_ENV" - - - name: Package aarch64 release (+ bundle aarch64 xlings) - id: stage - run: | - VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="mcpp-${VERSION}-linux-aarch64.tar.gz" - WRAPPER="mcpp-${VERSION}-linux-aarch64" - STAGING=$(mktemp -d) - mkdir -p "$STAGING/$WRAPPER/bin" - cp "$MCPP_AARCH64" "$STAGING/$WRAPPER/bin/mcpp" - # Strip with the cross toolchain's strip if present (binary is aarch64). - STRIP=$(find "$HOME/.mcpp" -name 'aarch64-linux-musl-strip' -type f 2>/dev/null | head -1) - [ -n "$STRIP" ] && "$STRIP" "$STAGING/$WRAPPER/bin/mcpp" 2>/dev/null || true - cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true - cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true - cat > "$STAGING/$WRAPPER/mcpp" << 'LAUNCHER' - #!/bin/sh - exec "$(dirname "$0")/bin/mcpp" "$@" - LAUNCHER - chmod +x "$STAGING/$WRAPPER/mcpp" - # Bundle the aarch64 xlings so install.sh consumers on aarch64 get an - # aarch64 xlings, not the x86_64 bootstrap one. The three literals - # below are pinned to the same version as XLINGS_VERSION; they are - # NOT interpolated from it, so check_version_pins.sh scans for them - # explicitly (they were absent from the old lock-step comment). - XLA="xlings-2026.7.27.2-linux-aarch64.tar.gz" - if curl -fsSL -o "/tmp/$XLA" \ - "https://github.com/openxlings/xlings/releases/download/v2026.7.27.2/$XLA"; then - tar -xzf "/tmp/$XLA" -C /tmp - XLBIN=$(find /tmp/xlings-2026.7.27.2-linux-aarch64 -path '*/bin/xlings' -type f | head -1) - if [ -n "$XLBIN" ]; then - mkdir -p "$STAGING/$WRAPPER/registry/bin" - cp "$XLBIN" "$STAGING/$WRAPPER/registry/bin/xlings" - chmod +x "$STAGING/$WRAPPER/registry/bin/xlings" - fi - fi - mkdir -p dist - (cd "$STAGING" && tar -czf "$GITHUB_WORKSPACE/dist/${TARBALL_NAME}" "$WRAPPER") - cp "dist/${TARBALL_NAME}" "dist/mcpp-linux-aarch64.tar.gz" - (cd dist && sha256sum "${TARBALL_NAME}" > "${TARBALL_NAME}.sha256") - (cd dist && sha256sum "mcpp-linux-aarch64.tar.gz" > "mcpp-linux-aarch64.tar.gz.sha256") - echo "tarball=${TARBALL_NAME}" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke-test the aarch64 tarball (qemu) - run: | - TARBALL_NAME="${{ steps.stage.outputs.tarball }}" - WRAPPER="${TARBALL_NAME%.tar.gz}" - SMOKE=$(mktemp -d) - tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" - ver=$(qemu-aarch64-static "$SMOKE/$WRAPPER/bin/mcpp" --version) - echo "$ver"; echo "$ver" | grep -q 'mcpp' - - - name: Upload aarch64 artifacts to release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.resolve.outputs.tag }} - files: | - dist/mcpp-${{ steps.resolve.outputs.version }}-linux-aarch64.tar.gz - dist/mcpp-${{ steps.resolve.outputs.version }}-linux-aarch64.tar.gz.sha256 - dist/mcpp-linux-aarch64.tar.gz - dist/mcpp-linux-aarch64.tar.gz.sha256 - - build-macos: - name: build (macOS / ARM64) - runs-on: macos-15 - needs: build-release - permissions: - contents: write - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve tag - id: resolve - run: | - if [ "${{ github.event_name }}" = "push" ]; then - TAG="${{ github.ref_name }}" - elif [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - else - VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - TAG="v$VER" - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ - && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then - git checkout --detach "refs/tags/$TAG" - fi - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-macos15-release-xl0462-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-macos15-release-xl0462- - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.7.27.2' - run: | - if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - curl -fsSL -o "${WORK}/${tarball}" \ - "https://github.com/openxlings/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - "${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install - fi - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - # Pinned to .xlings.json — a bare `xlings install mcpp` resolves - # "newest in this runner's index copy" and put 0.0.105 (below the - # index floor) into this job. See .github/tools/install_pinned_mcpp.sh. - MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Build mcpp from source (two-stage self-host) - env: - # macOS min-version support: target macOS 14 so the release runs - # on 14.0+ instead of only the runner's OS (the official LLVM - # static libc++ archives are built for macOS 14 — going lower - # needs a custom libc++ build, tracked as follow-up). Needs - # static LLVM libc++ — the system libc++ on older macOS lacks - # LLVM-20-era C++23 symbols (std::print's __is_posix_terminal - # etc.; minos-14 + dynamic libc++ dies at launch on macos-14 CI). - # See xlings .agents/docs/2026-06-05-macos-min-version-support.md. - MACOSX_DEPLOYMENT_TARGET: '14.0' - run: | - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - - # Stage 1: the bootstrap mcpp builds this release's source. The - # bootstrap's macOS link path predates the staticStdlib - # implementation (hardcoded -lc++), so stage 1 links the system - # libc++ — fine, it only needs to RUN on this runner. - "$MCPP" build - STAGE1=$(find target -path "*/bin/mcpp" | head -1) - STAGE1=$(cd "$(dirname "$STAGE1")" && pwd)/$(basename "$STAGE1") - "$STAGE1" --version - - # Stage 2: this release's mcpp rebuilds itself — flags.cppm's - # native staticStdlib link produces the static minos-14 binary. - # NOTE: stage 2 lands in a NEW fingerprint directory (its - # fingerprint includes the deployment target; the bootstrap's - # did not) — pick the most recently modified binary, not the - # first find hit. - "$STAGE1" build --no-cache - MCPP_BIN=$(ls -t $(find target -path "*/bin/mcpp" -type f) | head -1) - MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN") - test -x "$MCPP_BIN" - file "$MCPP_BIN" - otool -L "$MCPP_BIN" - echo "=== LC_BUILD_VERSION (must be minos 14.0) ===" - otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | head -6 - otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | grep -q "minos 14.0" \ - || { echo "FAIL: expected minos 14.0"; exit 1; } - if otool -L "$MCPP_BIN" | grep -q "libc++"; then - echo "FAIL: still linked against system libc++"; exit 1 - fi - "$MCPP_BIN" --version - echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV" - - - name: Package macOS release - id: stage - run: | - VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="mcpp-${VERSION}-macosx-arm64.tar.gz" - WRAPPER="mcpp-${VERSION}-macosx-arm64" - - # Create release layout - STAGING=$(mktemp -d) - mkdir -p "$STAGING/$WRAPPER/bin" - cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp" - # Strip (Mach-O) - strip "$STAGING/$WRAPPER/bin/mcpp" 2>/dev/null || true - # Copy metadata - cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true - cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true - - # Shell launcher (same as Linux) - cat > "$STAGING/$WRAPPER/mcpp" << 'LAUNCHER' - #!/bin/sh - exec "$(dirname "$0")/bin/mcpp" "$@" - LAUNCHER - chmod +x "$STAGING/$WRAPPER/mcpp" - - # Bundle xlings for install.sh consumers - XLINGS_BIN="$HOME/.xlings/subos/default/bin/xlings" - if [ -x "$XLINGS_BIN" ]; then - mkdir -p "$STAGING/$WRAPPER/registry/bin" - cp "$XLINGS_BIN" "$STAGING/$WRAPPER/registry/bin/xlings" - chmod +x "$STAGING/$WRAPPER/registry/bin/xlings" - fi - - # Create tarball - mkdir -p dist - (cd "$STAGING" && tar -czf "$GITHUB_WORKSPACE/dist/${TARBALL_NAME}" "$WRAPPER") - # Versionless alias - cp "dist/${TARBALL_NAME}" "dist/mcpp-macosx-arm64.tar.gz" - # SHA256 - (cd dist && shasum -a 256 "${TARBALL_NAME}" > "${TARBALL_NAME}.sha256") - (cd dist && shasum -a 256 "mcpp-macosx-arm64.tar.gz" > "mcpp-macosx-arm64.tar.gz.sha256") - - echo "tarball=${TARBALL_NAME}" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke-test the tarball - run: | - VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="${{ steps.stage.outputs.tarball }}" - WRAPPER="${TARBALL_NAME%.tar.gz}" - SMOKE=$(mktemp -d) - tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" - "$SMOKE/$WRAPPER/bin/mcpp" --version - "$SMOKE/$WRAPPER/mcpp" --version | grep -q "$VERSION" - - - name: Upload macOS artifacts to release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.resolve.outputs.tag }} - files: | - dist/mcpp-${{ steps.resolve.outputs.version }}-macosx-arm64.tar.gz - dist/mcpp-${{ steps.resolve.outputs.version }}-macosx-arm64.tar.gz.sha256 - dist/mcpp-macosx-arm64.tar.gz - dist/mcpp-macosx-arm64.tar.gz.sha256 - - build-windows: - name: build (Windows / x86_64) - runs-on: windows-latest - needs: build-release - permissions: - contents: write - timeout-minutes: 45 - env: - MCPP_HOME: C:\Users\runneradmin\.mcpp - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve tag - id: resolve - shell: bash - run: | - if [ "${{ github.event_name }}" = "push" ]; then - TAG="${{ github.ref_name }}" - elif [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - else - VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - TAG="v$VER" - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ - && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then - git checkout --detach "refs/tags/$TAG" - fi - - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~\.mcpp - key: mcpp-sandbox-${{ runner.os }}-release-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-release- - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~\.xlings - key: xlings-${{ runner.os }}-release-xl0462-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-release-xl0462- - - - name: Bootstrap mcpp via xlings - shell: bash - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.7.27.2' - run: | - # Captured before the `cd` below, in POSIX form: this step never - # returns to the workspace, and GITHUB_WORKSPACE is a backslash - # Windows path that git-bash tools mangle. - REPO_DIR="$(pwd)" - WORK=$(mktemp -d) - zipfile="xlings-${XLINGS_VERSION}-windows-x86_64.zip" - curl -fsSL -o "${WORK}/${zipfile}" \ - "https://github.com/openxlings/xlings/releases/download/v${XLINGS_VERSION}/${zipfile}" - cd "${WORK}" - unzip -q "${zipfile}" - "$WORK/xlings-${XLINGS_VERSION}-windows-x86_64/subos/default/bin/xlings.exe" self install - export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - echo "$USERPROFILE/.xlings/subos/default/bin" >> "$GITHUB_PATH" - xlings.exe --version - # Pinned + version-scoped lookup. The old `find | head -1` returned - # whichever version the directory walk reached first. - MCPP=$(bash "$REPO_DIR/.github/tools/install_pinned_mcpp.sh" "$REPO_DIR") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - XLINGS_BIN=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") - echo "XLINGS_BIN=$XLINGS_BIN" >> "$GITHUB_ENV" - echo "XLINGS_BIN_UNIX=$USERPROFILE/.xlings/subos/default/bin/xlings.exe" >> "$GITHUB_ENV" - echo "XLINGS_XPKGS=$USERPROFILE/.xlings/data/xpkgs" >> "$GITHUB_ENV" - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - - "$MCPP" build - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - MCPP_BIN=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \ - | sort -rn | head -1 | cut -d" " -f2-) - test -n "$MCPP_BIN" || { echo "FAIL: no mcpp.exe in target/"; exit 1; } - MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN") - echo "Self-hosted binary: $MCPP_BIN" - "$MCPP_BIN" --version - echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV" - - - name: Package Windows release zip - id: stage - shell: bash - run: | - VERSION="${{ steps.resolve.outputs.version }}" - WRAPPER="mcpp-${VERSION}-windows-x86_64" - ZIPNAME="${WRAPPER}.zip" - - STAGING=$(mktemp -d) - mkdir -p "$STAGING/$WRAPPER/bin" "$STAGING/$WRAPPER/registry/bin" - cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp.exe" - - # Windows batch launcher - printf '@echo off\r\n"%%~dp0bin\\mcpp.exe" %%*\r\n' > "$STAGING/$WRAPPER/mcpp.bat" - cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true - cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true - - # Bundle xlings.exe for install consumers - if [ -f "$XLINGS_BIN_UNIX" ]; then - cp "$XLINGS_BIN_UNIX" "$STAGING/$WRAPPER/registry/bin/xlings.exe" - fi - - # Pack with 7z (available on windows-latest) - mkdir -p dist - (cd "$STAGING" && 7z a -tzip "$ZIPNAME" "$WRAPPER") - cp "$STAGING/$ZIPNAME" "dist/$ZIPNAME" - # Versionless alias - cp "dist/$ZIPNAME" "dist/mcpp-windows-x86_64.zip" - # SHA256 - (cd dist && sha256sum "$ZIPNAME" > "$ZIPNAME.sha256") - (cd dist && sha256sum "mcpp-windows-x86_64.zip" > "mcpp-windows-x86_64.zip.sha256") - - echo "zipname=$ZIPNAME" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke-test the packaged zip - shell: bash - run: | - ZIPNAME="${{ steps.stage.outputs.zipname }}" - WRAPPER="${ZIPNAME%.zip}" - SMOKE=$(mktemp -d) - (cd "$SMOKE" && unzip -q "$GITHUB_WORKSPACE/dist/$ZIPNAME") - "$SMOKE/$WRAPPER/bin/mcpp.exe" --version - "$SMOKE/$WRAPPER/bin/mcpp.exe" --help | head -5 - test -f "$SMOKE/$WRAPPER/registry/bin/xlings.exe" - test -f "$SMOKE/$WRAPPER/mcpp.bat" - echo "Smoke-test passed" - - - name: Upload Windows artifacts to release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.resolve.outputs.tag }} - files: | - dist/mcpp-${{ steps.resolve.outputs.version }}-windows-x86_64.zip - dist/mcpp-${{ steps.resolve.outputs.version }}-windows-x86_64.zip.sha256 - dist/mcpp-windows-x86_64.zip - dist/mcpp-windows-x86_64.zip.sha256 - - # Publish this release into the xlings ecosystem, after ALL platform builds - # have uploaded their assets: - # ① mirror binaries → xlings-res/mcpp (GitHub + GitCode) so XLINGS_RES - # downloads resolve on every platform (incl. the CN/GitCode path); - # ② open a PR against openxlings/xim-pkgindex bumping mcpp to this version - # (a maintainer merges it — index git source is not on the critical path). - # Best-effort / non-blocking: a failure here never fails the release. - # Shared vendored scripts live in .github/tools/ (kept in sync with xlings). - publish-ecosystem: - needs: [build-release, build-linux-aarch64, build-macos, build-windows] - runs-on: ubuntu-latest - # A4 hardening: a single stuck upload once held this job >1h (6h default - # ceiling). The mirror script has per-file timeouts and (post-0.0.89) - # batch-upload + ranged-GET verification — normal runs are minutes; 30 - # is the generous backstop (20 was hit by the old per-asset verify loop). - timeout-minutes: 30 - env: - XLINGS_RES_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - XIM_PKGINDEX_TOKEN: ${{ secrets.XIM_PKGINDEX_TOKEN }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Determine version - id: version - run: echo "version=$(awk -F '\"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml)" >> "$GITHUB_OUTPUT" - - - name: Mirror binaries to xlings-res/mcpp (gh + gtc) - if: ${{ env.XLINGS_RES_TOKEN != '' }} - # Hard ceiling for the whole mirror segment. The script caps each asset - # at MIRROR_UPLOAD_TIMEOUT (180s) and abandons anything slower, so a - # healthy run lands well inside this; 10min is the backstop that keeps a - # pathological host from burning the job's budget the way v0.0.94 did - # (30min, killed, zero per-asset visibility). Whatever gets skipped is - # reported by name + size and pushed by hand. - timeout-minutes: 10 - env: - GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - run: | - chmod +x .github/tools/gtc .github/tools/mirror_res.sh - export PATH="$PWD/.github/tools:$PATH" - # A4: BLOCKING. Both mirror hosts serve users (GLOBAL + CN install - # paths); an incomplete mirror must fail here, visibly, instead of - # surfacing as a 404 in the first user's install (or fresh-install CI). - bash .github/tools/mirror_res.sh mcpp "${{ steps.version.outputs.version }}" - - - name: Open index bump PR (xim-pkgindex) - if: ${{ env.XIM_PKGINDEX_TOKEN != '' }} - env: - PKGINDEX_TOKEN: ${{ secrets.XIM_PKGINDEX_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - chmod +x .github/tools/bump_index.sh - # A4: BLOCKING — a missing bump PR means the index never learns the - # release exists and every post-release install of the new version 404s. - bash .github/tools/bump_index.sh mcpp "${{ steps.version.outputs.version }}" - - # Post-release verification (ci-fresh-install) is triggered via its - # `workflow_run: [release]` hook — a platform-generated event that is - # exempt from GITHUB_TOKEN trigger suppression and needs no cross-repo - # PAT. (A PAT-based dispatch step lived here briefly; it never worked — - # XIM_PKGINDEX_TOKEN's resource owner is the index org and cannot cover - # this repository.) From 5420962925d6dedbd39cf69afeb4e3516c5dddaa Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 29 Jul 2026 00:32:59 +0800 Subject: [PATCH 2/2] probe round 2: is the 12 KB/s budget per-connection or per-host? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 1 (run 30374882109) settled the why: US-East runner -> file.gitcode.com is a flat ~0.012 MB/s regardless of size and regardless of transport (curl matches urllib exactly), while the SAME runner downloads from gitcode.com at 3.87 MB/s and uploads to github.com at 16 MB/s, and a mainland-CN host reaches 1.84 MB/s to the same endpoint. So it is inbound rate limiting on the GitCode side for this egress, not the client and not cross-border bandwidth per se. At 12 KB/s the 34.8MB asset needs ~48 minutes, which is why four rounds of tuning MIRROR_UPLOAD_TIMEOUT never worked. Round 1 ran out of wall-clock (4x8MB serial alone took 2618s) before reaching the parallel comparison — and that is the measurement that picks the fix: per-connection budget -> N concurrent uploads scale, and parallelising mirror_res.sh's serial per-asset loop helps per-host budget -> parallelism buys nothing and the gitcode leg has to leave GitHub-hosted runners entirely scaling/ measures N=1 vs 4 vs 8 on 1MB files so it finishes either way, and also dumps the upload_url contract to see whether multipart/resumable upload is even offered. variance/ bounds the spread, which decides whether any fixed timeout can be safe. --- .github/workflows/probe-gitcode-upload.yml | 235 +++++++++------------ 1 file changed, 97 insertions(+), 138 deletions(-) diff --git a/.github/workflows/probe-gitcode-upload.yml b/.github/workflows/probe-gitcode-upload.yml index dff757c1..f6b98519 100644 --- a/.github/workflows/probe-gitcode-upload.yml +++ b/.github/workflows/probe-gitcode-upload.yml @@ -2,26 +2,34 @@ name: probe-gitcode-upload # TEMPORARY diagnostic workflow — delete with this branch. # -# WHY. `publish-ecosystem`'s GitCode leg has now blown its 180s per-asset cap -# on four separate releases (0.0.94 / 0.0.97 / 0.0.105 / 2026.7.28.2), each -# time on the two biggest tarballs, each time costing ~5min of manual -# re-upload. Every previous fix was a guess about WHY it is slow (raise the -# cap, skip-don't-retry, batch verify). This workflow measures instead. +# ROUND 1 (run 30374882109) already settled the "why": # -# Each job isolates ONE hypothesis, and they run in parallel so a single push -# answers all of them: +# US-East runner -> file.gitcode.com upload is a FLAT ~0.012 MB/s (12 KB/s), +# independent of file size AND of transport: +# urllib 1MB 88.8s | 4MB failed | 16MB 1529.9s (0.010-0.011 MB/s) +# curl 1MB 83.1s | 4MB 330.7s | 16MB 1218.4s (0.012-0.013 MB/s) +# 4x8MB serial: 672/650/675/599s, wall 2618s +# Controls from the SAME runner: +# download from gitcode.com 3.87 MB/s (~320x the upload rate) +# upload to github.com 16 MB/s +# Control from a mainland-CN host: 1.84 MB/s (~150x the runner's rate). # -# baseline Is it the network, the direction, or GitCode specifically? -# Down/up control against GitHub from the same runner. -# sizes-urllib Does throughput collapse with size, or is it ~constant? -# (constant MB/s => a pure bandwidth wall, not a stall.) -# sizes-curl Is Python's read-it-all-then-PUT the bottleneck, or the wire? -# concurrency Is bandwidth per-connection or per-host? If per-connection, -# uploading assets in parallel is the whole fix — mirror_res.sh -# currently uploads them SERIALLY within a host leg. +# So it is neither the Python client (curl matches it) nor cross-border +# bandwidth in general (the same runner downloads from the same host at +# 3.87 MB/s). It is inbound-upload rate limiting on file.gitcode.com for +# this egress. At 12 KB/s the 34.8MB asset needs ~48 MINUTES; the 180s cap +# never had a chance, which is why "raise/loosen the cap" failed four times. # -# Every job writes NDJSON to the step summary so results are comparable -# without opening logs. +# ROUND 2 (this file) answers the one question that decides the FIX, which +# round 1 ran out of wall-clock before reaching: is the 12 KB/s budget +# PER-CONNECTION or PER-HOST? +# per-connection -> N parallel uploads give ~N x aggregate, and +# parallelising mirror_res.sh's serial per-asset loop is +# a real (if partial) fix. +# per-host -> parallelism buys nothing and the mirror must move off +# GitHub-hosted runners entirely. +# `scaling` measures exactly that. `variance` bounds how much the rate moves +# over time, which decides whether ANY fixed timeout can be safe. on: push: @@ -37,174 +45,125 @@ concurrency: env: GTC_REPO: xlings-res/mcpp - # A real, already-mirrored asset used as the download reference (34.8MB). - REF_ASSET: mcpp-2026.7.28.2-linux-x86_64.tar.gz - REF_TAG: 2026.7.28.2 jobs: - # ── H0: characterise the pipe itself, both directions, both hosts ──────── - baseline: - name: baseline — network + GitHub control + # ── THE decisive test: does concurrency multiply the budget? ───────────── + scaling: + name: scaling — 1 vs 4 vs 8 concurrent 1MB uploads runs-on: ubuntu-latest - timeout-minutes: 25 + timeout-minutes: 50 env: GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} + TAG: probe-${{ github.run_id }}-scale steps: - uses: actions/checkout@v4 - - name: Runner egress identity - run: | - echo "runner public IP / region:" - curl -s --max-time 20 https://ipinfo.io/json || echo "(ipinfo unavailable)" - - - name: Connect/TLS timing to each host - run: | - fmt=' dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s code=%{http_code}\n' - for h in https://api.gitcode.com https://gitcode.com https://api.github.com https://github.com; do - echo "$h" - curl -sS -o /dev/null --max-time 60 -w "$fmt" "$h" || echo " (failed)" - done + - name: Create probe release + run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG" - - name: Download throughput — gitcode vs github (same 34.8MB asset) + - name: Inspect the upload_url contract (multipart/resumable available?) run: | - fmt=' code=%{http_code} bytes=%{size_download} speed=%{speed_download} B/s total=%{time_total}s\n' - echo "gitcode.com:" - curl -sSL -o /dev/null --max-time 900 -w "$fmt" \ - "https://gitcode.com/${GTC_REPO}/releases/download/${REF_TAG}/${REF_ASSET}" || true - echo "github.com:" - curl -sSL -o /dev/null --max-time 900 -w "$fmt" \ - "https://github.com/${GTC_REPO}/releases/download/${REF_TAG}/${REF_ASSET}" || true + # If the response carries multipart/part-size fields, a resumable + # chunked upload is possible and a stalled transfer could be resumed + # instead of restarted from byte zero. + python3 - <<'PY' + import json, os, urllib.request, urllib.parse + repo, tag = os.environ["GTC_REPO"], os.environ["TAG"] + url = (f"https://api.gitcode.com/api/v5/repos/{repo}/releases/{tag}" + f"/upload_url?file_name=probe-contract.bin") + req = urllib.request.Request(url, headers={ + "PRIVATE-TOKEN": os.environ["GITCODE_TOKEN"], "Accept": "application/json"}) + info = json.loads(urllib.request.urlopen(req, timeout=60).read()) + # Redact the signature so the log stays shareable. + u = urllib.parse.urlparse(info["url"]) + print("keys: ", sorted(info.keys())) + print("obs host: ", u.netloc) + print("obs path: ", u.path) + print("query params:", sorted(urllib.parse.parse_qs(u.query).keys())) + print("headers: ", {k: ("" if "auth" in k.lower() else v) + for k, v in (info.get("headers") or {}).items()}) + PY - - name: Upload control — 32MB to GitHub from this same runner + - name: N=1 (baseline for this run) run: | - set -x - head -c 33554432 /dev/urandom > probe-github-32m.bin - gh release view "probe-${{ github.run_id }}" -R "$GTC_REPO" >/dev/null 2>&1 \ - || gh release create "probe-${{ github.run_id }}" -R "$GTC_REPO" \ - --title "probe ${{ github.run_id }}" --notes "temporary upload probe; safe to delete" + head -c 1048576 /dev/urandom > s1-1.bin start=$SECONDS - gh release upload "probe-${{ github.run_id }}" probe-github-32m.bin -R "$GTC_REPO" --clobber - echo "GITHUB_UPLOAD_32MB_SECONDS=$((SECONDS - start))" + python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \ + --file s1-1.bin --method curl --label "n1" >> results.ndjson + echo "N1_WALL=$((SECONDS - start))" | tee -a wall.txt - # ── H1: is throughput size-dependent (stall) or flat (bandwidth wall)? ─── - sizes-urllib: - name: sizes — current urllib transport - runs-on: ubuntu-latest - timeout-minutes: 45 - env: - GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - TAG: probe-${{ github.run_id }}-urllib - steps: - - uses: actions/checkout@v4 - - name: Create probe release - run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG" - - name: Upload 1 / 4 / 16 / 32 / 32 MB (serial) + - name: N=4 concurrent run: | - # 32MB twice: run-to-run variance matters as much as the mean when - # deciding whether a fixed 180s cap can ever be safe. - for spec in 1 4 16 32 32; do - f="probe-urllib-${spec}m-$RANDOM.bin" - head -c $((spec * 1048576)) /dev/urandom > "$f" - python3 .github/tools/probe_gtc_upload.py \ - --repo "$GTC_REPO" --tag "$TAG" --file "$f" \ - --method urllib --label "urllib-${spec}MB" >> results.ndjson - rm -f "$f" + for i in 1 2 3 4; do head -c 1048576 /dev/urandom > "s4-$i.bin"; done + start=$SECONDS + for i in 1 2 3 4; do + python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \ + --file "s4-$i.bin" --method curl --label "n4-$i" > "s4-$i.json" & done - - name: Summary - if: always() - run: | - { echo '### sizes — urllib'; echo '```json'; cat results.ndjson; echo '```'; } \ - >> "$GITHUB_STEP_SUMMARY" + wait + echo "N4_WALL=$((SECONDS - start))" | tee -a wall.txt + cat s4-*.json >> results.ndjson - # ── H2: is the Python client the bottleneck, or the wire? ──────────────── - sizes-curl: - name: sizes — curl streaming transport - runs-on: ubuntu-latest - timeout-minutes: 45 - env: - GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - TAG: probe-${{ github.run_id }}-curl - steps: - - uses: actions/checkout@v4 - - name: Create probe release - run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG" - - name: Upload 1 / 4 / 16 / 32 / 32 MB (serial) + - name: N=8 concurrent run: | - for spec in 1 4 16 32 32; do - f="probe-curl-${spec}m-$RANDOM.bin" - head -c $((spec * 1048576)) /dev/urandom > "$f" - python3 .github/tools/probe_gtc_upload.py \ - --repo "$GTC_REPO" --tag "$TAG" --file "$f" \ - --method curl --label "curl-${spec}MB" >> results.ndjson - rm -f "$f" + for i in 1 2 3 4 5 6 7 8; do head -c 1048576 /dev/urandom > "s8-$i.bin"; done + start=$SECONDS + for i in 1 2 3 4 5 6 7 8; do + python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \ + --file "s8-$i.bin" --method curl --label "n8-$i" > "s8-$i.json" & done - - name: Summary + wait + echo "N8_WALL=$((SECONDS - start))" | tee -a wall.txt + cat s8-*.json >> results.ndjson + + - name: Verdict if: always() run: | - { echo '### sizes — curl'; echo '```json'; cat results.ndjson; echo '```'; } \ - >> "$GITHUB_STEP_SUMMARY" + # N4_WALL ~= N1_WALL -> per-connection budget: parallelism is the fix. + # N4_WALL ~= 4*N1_WALL -> per-host budget: parallelism buys nothing. + { echo '### scaling'; echo '```'; cat wall.txt; echo '```'; + echo '```json'; cat results.ndjson; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + cat wall.txt - # ── H3: per-connection cap or per-host cap? This decides whether simply - # parallelising mirror_res.sh's serial per-asset loop is the fix. ── - concurrency: - name: concurrency — 4x8MB serial vs parallel + # ── Can ANY fixed timeout be safe? Depends on the spread, not the mean. ── + variance: + name: variance — 8 sequential 1MB uploads runs-on: ubuntu-latest - timeout-minutes: 45 + timeout-minutes: 50 env: GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - TAG: probe-${{ github.run_id }}-conc + TAG: probe-${{ github.run_id }}-var steps: - uses: actions/checkout@v4 - name: Create probe release run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG" - - name: Serial 4x8MB + - name: 8 sequential 1MB uploads run: | - for i in 1 2 3 4; do head -c 8388608 /dev/urandom > "ser-$i.bin"; done - start=$SECONDS - for i in 1 2 3 4; do + for i in 1 2 3 4 5 6 7 8; do + head -c 1048576 /dev/urandom > "v-$i.bin" python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \ - --file "ser-$i.bin" --method curl --label "serial-$i" >> results.ndjson + --file "v-$i.bin" --method curl --label "v$i" >> results.ndjson + rm -f "v-$i.bin" done - echo "SERIAL_WALL_SECONDS=$((SECONDS - start))" | tee -a wall.txt - - name: Parallel 4x8MB - run: | - for i in 1 2 3 4; do head -c 8388608 /dev/urandom > "par-$i.bin"; done - start=$SECONDS - for i in 1 2 3 4; do - python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \ - --file "par-$i.bin" --method curl --label "parallel-$i" >> "par-$i.json" & - done - wait - echo "PARALLEL_WALL_SECONDS=$((SECONDS - start))" | tee -a wall.txt - cat par-*.json >> results.ndjson - name: Summary if: always() run: | - { echo '### concurrency'; echo '```'; cat wall.txt; echo '```'; - echo '```json'; cat results.ndjson; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + { echo '### variance'; echo '```json'; cat results.ndjson; echo '```'; } \ + >> "$GITHUB_STEP_SUMMARY" - # ── Leave no garbage on the resource repo ─────────────────────────────── cleanup: name: cleanup probe tags - needs: [baseline, sizes-urllib, sizes-curl, concurrency] + needs: [scaling, variance] if: always() runs-on: ubuntu-latest timeout-minutes: 10 env: GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} steps: - name: Delete GitCode probe tags (deleting the tag deletes the release) run: | - for t in "probe-${{ github.run_id }}-urllib" \ - "probe-${{ github.run_id }}-curl" \ - "probe-${{ github.run_id }}-conc"; do + for t in "probe-${{ github.run_id }}-scale" "probe-${{ github.run_id }}-var"; do code=$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE \ -H "PRIVATE-TOKEN: $GITCODE_TOKEN" \ "https://api.gitcode.com/api/v5/repos/${GTC_REPO}/tags/${t}" || echo ERR) echo "delete $t -> $code" done - - name: Delete GitHub probe release - run: | - gh release delete "probe-${{ github.run_id }}" -R "$GTC_REPO" --yes --cleanup-tag \ - || echo "(nothing to delete)"