From 65be4ac8c4c3363a87a1b95f01d2630f47d86d0c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 16 Oct 2026 04:07:44 +0900 Subject: [PATCH 1/6] probe: luban inside -- vim and terminfo, generation directory entries, mount --move in a user namespace, more host distributions --- .github/probe/inside.sh | 144 ++ .github/probe/pty_run.py | 81 ++ .github/workflows/gitee-sync.yml | 21 - .github/workflows/mirror-binaries.yml | 52 - .github/workflows/probe.yml | 111 ++ .github/workflows/release.yml | 1229 ----------------- .github/workflows/xlings-ci-aarch64.yml | 405 ------ .github/workflows/xlings-ci-fresh-install.yml | 217 --- .github/workflows/xlings-ci-host-matrix.yml | 173 --- .github/workflows/xlings-ci-linux.yml | 1043 -------------- .github/workflows/xlings-ci-luban-fresh.yml | 85 -- .github/workflows/xlings-ci-macos.yml | 458 ------ .github/workflows/xlings-ci-windows.yml | 541 -------- tests/host-matrix/bare.sh | 4 +- tests/host-matrix/vm.sh | 5 + 15 files changed, 344 insertions(+), 4225 deletions(-) create mode 100755 .github/probe/inside.sh create mode 100755 .github/probe/pty_run.py delete mode 100644 .github/workflows/gitee-sync.yml delete mode 100644 .github/workflows/mirror-binaries.yml create mode 100644 .github/workflows/probe.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/xlings-ci-aarch64.yml delete mode 100644 .github/workflows/xlings-ci-fresh-install.yml delete mode 100644 .github/workflows/xlings-ci-host-matrix.yml delete mode 100644 .github/workflows/xlings-ci-linux.yml delete mode 100644 .github/workflows/xlings-ci-luban-fresh.yml delete mode 100644 .github/workflows/xlings-ci-macos.yml delete mode 100644 .github/workflows/xlings-ci-windows.yml diff --git a/.github/probe/inside.sh b/.github/probe/inside.sh new file mode 100755 index 000000000..3361e14ec --- /dev/null +++ b/.github/probe/inside.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# O1 (vim and terminfo), O2 (directory entries of a generation; bwrap bind +# cost), O3 (mount --move inside a user namespace), with xlings $XV on an +# Ubuntu runner. Prints observations; never asserts. +set -uo pipefail +XV="${XV:-2026.10.15.4}" +T="$RUNNER_TEMP/p"; mkdir -p "$T/logs"; cd "$T" +S="${GITHUB_STEP_SUMMARY:-/dev/null}" +HERE="$(cd "$(dirname "$0")" && pwd)" +say() { echo "$*"; echo "$*" >> "$S"; } +block() { + local title="$1"; shift; local out rc + out="$("$@" 2>&1)"; rc=$? + echo "== $title (exit $rc)"; echo "$out" + { echo; echo "**$title** (exit $rc)"; echo '```'; echo "$out" | tail -n "${LINES_:-40}"; echo '```'; } >> "$S" +} + +sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 +curl -fsSL -o xl.tgz "https://github.com/openxlings/xlings/releases/download/v$XV/xlings-$XV-linux-x86_64.tar.gz" +mkdir -p inst && tar -xzf xl.tgz -C inst +(cd inst/*/ && ./bin/xlings self install logs/install.log 2>&1 +export PATH="$HOME/.xlings/bin:$PATH" +X() { env -u XLINGS_PROJECT_DIR XLINGS_NON_INTERACTIVE=1 "$HOME/.xlings/bin/xlings" "$@"; } +L() { env -u XLINGS_PROJECT_DIR "$HOME/.xlings/bin/luban" "$@"; } +block "versions" bash -c "xlings --version; luban --version; uname -r; bwrap --version 2>&1 || true" + +say "### making core and desktop" +LINES_=12 block "luban new pcore --edition core -y" timeout 2400 env XLINGS_NON_INTERACTIVE=1 "$HOME/.xlings/bin/luban" new pcore --edition core -y +LINES_=12 block "luban new pdesk --edition desktop -y" timeout 2400 env XLINGS_NON_INTERACTIVE=1 "$HOME/.xlings/bin/luban" new pdesk --edition desktop -y + +gen_of() { # the current generation directory of a system + for base in /xlings "$HOME/.xlings"; do + local g; g="$(readlink -f "$base/subos/$1/root" 2>/dev/null)" + [[ -n "$g" && -f "$g/links.tsv" ]] && { echo "$g"; return; } + done + find / -path "*/subos/$1/root.gen/*/links.tsv" 2>/dev/null | sort -V | tail -1 | xargs -r dirname +} + +say "### O2: directory entries of a generation" +for sys in pcore pdesk; do + G="$(gen_of $sys)" + say "$sys generation: ${G:-none}" + [[ -n "$G" ]] || continue + block "$sys: entries by kind" python3 - "$G" <<'PY' +import os, sys, collections +g = sys.argv[1] +rows = [l.rstrip("\n").split("\t") for l in open(os.path.join(g, "links.tsv")) if "\t" in l] +kinds = collections.Counter(); dirs = [] +for rel, target in rows[:]: + t = target if os.path.isabs(target) else os.path.join(g, os.path.dirname(rel), target) + if os.path.isdir(t): + kinds["dir"] += 1; dirs.append((rel, target)) + elif os.path.exists(t): + kinds["file"] += 1 + else: + kinds["dangling"] += 1 +print("total", len(rows), dict(kinds)) +by = collections.Counter("/".join(r.split("/")[:3]) for r, _ in dirs) +for k, v in by.most_common(25): print(f" {v:4d} {k}") +open(os.path.join(os.environ.get("RUNNER_TEMP", "/tmp"), f"dirs-{os.path.basename(os.path.dirname(os.path.dirname(g)))}.txt"), "w").write( + "\n".join(t if os.path.isabs(t) else os.path.join(g, os.path.dirname(r), t) for r, t in dirs)) +print("first dir entries:"); [print(" ", r, "->", t) for r, t in dirs[:15]] +PY + block "$sys: /usr entry of the root tree" bash -c "ls -la $(dirname "$(dirname "$G")")/rootfs/ 2>/dev/null | grep -E ' usr| bin| lib' ; ls -la $(dirname "$(dirname "$G")")/root" +done + +say "### O2: bwrap start-up cost per bind" +BW="$(command -v bwrap || ls "$HOME"/.xlings/data/xpkgs/xim-x-bwrap/*/bin/bwrap 2>/dev/null | head -1)" +say "bwrap: $BW ($("$BW" --version 2>&1))" +DIRS="$(cat "$RUNNER_TEMP"/dirs-pdesk.txt "$RUNNER_TEMP"/dirs-pcore.txt 2>/dev/null | sort -u)" +block "bwrap time vs number of --ro-bind" python3 - "$BW" < len(pool): break + args = list(base) + for i, d in enumerate(pool[:n]): + args += ["--dir", f"/mnt/b{i}", "--ro-bind", d, f"/mnt/b{i}"] + args += ["--", "/bin/true"] + ts = [] + for _ in range(15): + t0 = time.perf_counter(); r = subprocess.run(args, capture_output=True); ts.append((time.perf_counter() - t0) * 1000) + if r.returncode: print(" n=%d failed: %s" % (n, r.stderr.decode()[:200])); break + print(f" binds={n:5d} median={statistics.median(ts):7.1f} ms max={max(ts):7.1f} ms") +PY + +say "### O1: vim and terminfo" +G="$(gen_of pcore)" +VIM="$(readlink -f "$G/usr/bin/vim" 2>/dev/null)" +block "vim payload" bash -c "echo '$VIM'; file '$VIM'; '$VIM' --version | head -3; '$VIM' --version | grep -iE 'terminfo|tgetent|tlib' ; strings '$VIM' | grep -E '^/[a-z/]*terminfo' | sort -u | head" +block "terminfo dirs present on the runner" bash -c "ls -d /etc/terminfo /lib/terminfo /usr/share/terminfo /usr/lib/terminfo 2>&1" +printf 'hello\n' > "$T/t.txt" +for mode in hidden present; do + hide=() + [[ $mode == hidden ]] && for d in /etc/terminfo /lib/terminfo /usr/share/terminfo /usr/lib/terminfo; do [[ -d $d ]] && hide+=(--tmpfs "$d"); done + block "O1 vim, TERM=xterm-256color, terminfo $mode" python3 "$HERE/pty_run.py" --out "$T/logs/vim-$mode.bin" \ + --step 'hello|:q\r' -- "$BW" --unshare-user --ro-bind / / --dev /dev --proc /proc --bind "$T" "$T" "${hide[@]}" \ + --setenv TERM xterm-256color --setenv HOME "$T" -- "$VIM" -u NONE -N "$T/t.txt" +done +block "O1 inside the system: luban enter pcore, vim, :q" python3 "$HERE/pty_run.py" --out "$T/logs/enter-vim.bin" --wait 90 \ + --step '@pcore|vim /tmp/x.txt\r' --step '~|ihello\x1b:wq\r' --step '@pcore|ls /usr/share/terminfo; echo TERM=$TERM; exit\r' \ + -- env TERM=xterm-256color "$HOME/.xlings/bin/luban" enter pcore + +say "### O3: mount --move inside a user namespace" +mkdir -p "$T/genA/usr/bin" "$T/genB/usr/bin"; echo A > "$T/genA/usr/bin/which-gen"; echo B > "$T/genB/usr/bin/which-gen" +cat > "$T/o3.sh" <<'SH' +set -u +T="$1" +echo "caps: $(grep CapEff /proc/self/status)" +cat /usr/bin/which-gen +# 1. stack: bind the new generation directly over /usr +if mount --bind "$T/genB/usr" /usr 2>&1; then echo "stack bind: ok -> $(cat /usr/bin/which-gen)"; else echo "stack bind: failed"; fi +umount /usr 2>/dev/null && echo "unmount top: ok -> $(cat /usr/bin/which-gen)" +# 2. stage + move +mkdir -p /mnt/stage +if mount --bind "$T/genB/usr" /mnt/stage 2>&1 && mount --move /mnt/stage /usr 2>&1; then echo "stage+move: ok -> $(cat /usr/bin/which-gen)"; else echo "stage+move: failed"; fi +# 3. read-only remount of the new one +mount -o remount,bind,ro /usr 2>&1 && echo "remount ro: ok" || echo "remount ro: failed" +# 4. lazy detach of what is underneath, then a reader keeps its file +exec 9&1 && echo "lazy umount: ok; held fd still reads: $(cat <&9); now /usr has: $(cat /usr/bin/which-gen 2>&1)" +# 5. drop the capability for a child (what session-init would do before the shell) +python3 - <<'PY' +import ctypes, os +libc = ctypes.CDLL(None, use_errno=True) +PR_CAPBSET_DROP = 24 +for cap in range(0, 41): + libc.prctl(PR_CAPBSET_DROP, cap, 0, 0, 0) +pid = os.fork() +if pid == 0: + os.execvp("sh", ["sh", "-c", "grep -E 'CapEff|CapBnd' /proc/self/status; mount --bind /tmp /mnt 2>&1 || echo 'child mount: refused (expected)'"]) +os.waitpid(pid, 0) +PY +SH +block "O3 without --cap-add (as xlings starts bwrap today)" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --ro-bind / / --dev /dev --proc /proc \ + --tmpfs /mnt --bind "$T" "$T" --ro-bind "$T/genA/usr" /usr -- sh "$T/o3.sh" "$T" +block "O3 with --cap-add CAP_SYS_ADMIN" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_SYS_ADMIN --ro-bind / / --dev /dev --proc /proc \ + --tmpfs /mnt --bind "$T" "$T" --ro-bind "$T/genA/usr" /usr -- sh "$T/o3.sh" "$T" +block "O3 /usr that came from the parent namespace (locked), with CAP_SYS_ADMIN" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_SYS_ADMIN \ + --bind / / --dev /dev --proc /proc --tmpfs /mnt -- sh -c "mount --bind $T/genB/usr /usr && echo 'stack over host /usr: ok' ; umount -l /usr && echo 'umount stacked: ok'; mkdir -p /mnt/s && mount --bind $T/genB/usr /mnt/s && mount --move /mnt/s /usr && echo 'move onto host /usr: ok' || echo 'move onto host /usr: failed'" diff --git a/.github/probe/pty_run.py b/.github/probe/pty_run.py new file mode 100755 index 000000000..3b7536192 --- /dev/null +++ b/.github/probe/pty_run.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Run a command on a pty, type a script into it, and record every byte. + + pty_run.py --out [--cols 120 --rows 40] [--step "|"]... -- + +Each step waits (up to --wait seconds) until the output so far matches the +regex, then types the text (\\r and \\x1b escapes are honoured). Prints a +summary of terminal sequences seen. Never asserts. +""" +import argparse, fcntl, os, pty, re, select, struct, sys, termios, time + +p = argparse.ArgumentParser() +p.add_argument("--out", required=True) +p.add_argument("--cols", type=int, default=120) +p.add_argument("--rows", type=int, default=40) +p.add_argument("--wait", type=float, default=60) +p.add_argument("--step", action="append", default=[]) +p.add_argument("--tail", type=float, default=3) +p.add_argument("argv", nargs=argparse.REMAINDER) +a = p.parse_args() +argv = a.argv[1:] if a.argv and a.argv[0] == "--" else a.argv + +pid, fd = pty.fork() +if pid == 0: + os.execvp(argv[0], argv) +fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", a.rows, a.cols, 0, 0)) +buf = bytearray() + +def pump(timeout): + end = time.time() + timeout + while time.time() < end: + r, _, _ = select.select([fd], [], [], 0.1) + if r: + try: + d = os.read(fd, 65536) + except OSError: + return False + if not d: + return False + buf.extend(d) + return True + +alive = True +for step in a.step: + pat, _, text = step.partition("|") + text = text.encode().decode("unicode_escape").encode("latin-1") + deadline = time.time() + a.wait + start = len(buf) + while alive and time.time() < deadline: + if re.search(pat.encode(), bytes(buf[max(0, start - 2048):])): + break + alive = pump(0.3) + time.sleep(0.3) + if alive: + os.write(fd, text) + alive = pump(0.5) +if alive: + pump(a.tail) +try: + os.kill(pid, 9) +except ProcessLookupError: + pass +open(a.out, "wb").write(bytes(buf)) +b = bytes(buf) +seq = { + "alt-screen enter \\e[?1049h": b"\x1b[?1049h", + "alt-screen leave \\e[?1049l": b"\x1b[?1049l", + "alt-screen enter \\e[?47h": b"\x1b[?47h", + "alt-screen leave \\e[?47l": b"\x1b[?47l", + "smcup \\e7\\e[?47h": b"\x1b7\x1b[?47h", +} +print(f"bytes: {len(b)}") +for k, v in seq.items(): + print(f" {k}: {'yes' if v in b else 'no'}") +for w in [b"can't access tty", b"no job control", b"tcgetpgrp", b"Inappropriate ioctl", + b"cannot set terminal process group", b"Not a tty"]: + if w in b: + print(f" warning text seen: {w.decode()}") +text = re.sub(rb"\x1b\[[0-9;?]*[A-Za-z]|\x1b[()][0-9A-Za-z]|\x1b[=>78]|\r", b"", b).decode("utf-8", "replace") +print("---- text (last 40 lines) ----") +print("\n".join(text.splitlines()[-40:])) diff --git a/.github/workflows/gitee-sync.yml b/.github/workflows/gitee-sync.yml deleted file mode 100644 index 8f2944d0f..000000000 --- a/.github/workflows/gitee-sync.yml +++ /dev/null @@ -1,21 +0,0 @@ -# https://github.com/mqyqingfeng/Blog/issues/236 - -name: gitee-sync -on: - push: - branches: - - main -jobs: - repo-sync: - runs-on: ubuntu-latest - steps: - - name: Mirror the Github organization repos to Gitee. - uses: Yikun/hub-mirror-action@master - with: - src: 'github/openxlings' - dst: 'gitee/Sunrisepeak' - dst_key: ${{ secrets.GITEE_SSH_KEY }} - dst_token: ${{ secrets.GITEE_TOKEN }} - static_list: "xlings" - force_update: true - debug: true \ No newline at end of file diff --git a/.github/workflows/mirror-binaries.yml b/.github/workflows/mirror-binaries.yml deleted file mode 100644 index 345909be9..000000000 --- a/.github/workflows/mirror-binaries.yml +++ /dev/null @@ -1,52 +0,0 @@ -# On-demand (re)mirror of release binaries to xlings-res (GitHub + GitCode). -# -# release.yml runs the same script automatically; this workflow is the rescue -# path when that job was cancelled, timed out, or GitCode dropped some assets -# — re-running the whole release pipeline just to top up a mirror is wasteful. -# The script is idempotent: assets that already verify (200 + size match) are -# skipped, so a re-run only uploads what is actually missing. -# -# NOTE — GitCode big assets (>~8 MiB) still CANNOT be uploaded from here: this -# runs on a GitHub-hosted runner, which hits the same cross-border upload wall -# as release.yml (probe PR #371). For those, run `tools/mirror-latest.sh` from -# a CN environment instead. This workflow is still useful for re-mirroring the -# GitHub side and the small GitCode assets. Runbook: -# .agents/docs/2026-07-15-gitcode-large-asset-mirror-runbook.md -# -# Counterpart of xim-pkgindex's publish-sub-indexes.yml (same on-demand -# top-up pattern for index artifacts). -name: Mirror Binaries (manual) - -on: - workflow_dispatch: - inputs: - version: - description: 'Release version to mirror (e.g. 0.4.65)' - required: true - type: string - project: - description: 'Project whose binaries to mirror' - type: choice - options: [xlings, luban-os, xdesk, mcpp] - default: xlings - -permissions: - contents: read - -jobs: - mirror: - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - XLINGS_RES_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - steps: - - uses: actions/checkout@v4 - - - name: Mirror binaries to xlings-res (gh + gtc) - if: ${{ env.XLINGS_RES_TOKEN != '' }} - run: | - chmod +x tools/gtc tools/mirror_res.sh - export PATH="$PWD/tools:$PATH" - bash tools/mirror_res.sh "${{ inputs.project }}" "${{ inputs.version }}" diff --git a/.github/workflows/probe.yml b/.github/workflows/probe.yml new file mode 100644 index 000000000..5d03a73f8 --- /dev/null +++ b/.github/workflows/probe.yml @@ -0,0 +1,111 @@ +# [temp][do not merge] Probe for +# .agents/docs/2026-10-16-luban-inside-experience-and-ecosystem-design.md +# (O1 vim and terminfo, O2 generation directory entries and bind cost, +# O3 mount --move in a user namespace, O4 more host distributions). +# Every script prints what it observes and never asserts. +name: probe + +on: + pull_request: + workflow_dispatch: + +env: + XV: 2026.10.15.4 + +jobs: + inside: + name: O1 O2 O3 (ubuntu-24.04, a core and a desktop system) + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - name: probe + run: bash .github/probe/inside.sh + - uses: actions/upload-artifact@v4 + if: always() + with: + name: inside + path: ${{ runner.temp }}/p/logs + + # O4: distributions as user spaces (containers), as the host matrix runs + # them, with the scenarios that make and enter a system. + container: + name: O4 ${{ matrix.image }} (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - { runner: ubuntu-24.04, arch: x86_64, image: "rockylinux:9" } + - { runner: ubuntu-24.04, arch: x86_64, image: "almalinux:9" } + - { runner: ubuntu-24.04, arch: x86_64, image: "quay.io/centos/centos:stream10" } + - { runner: ubuntu-24.04, arch: x86_64, image: "amazonlinux:2023" } + - { runner: ubuntu-24.04, arch: x86_64, image: "alpine:3.20" } + - { runner: ubuntu-24.04, arch: x86_64, image: "debian:13" } + - { runner: ubuntu-24.04, arch: x86_64, image: "opensuse/leap:15.6" } + - { runner: ubuntu-24.04-arm, arch: aarch64, image: "debian:12" } + - { runner: ubuntu-24.04-arm, arch: aarch64, image: "fedora:latest" } + - { runner: ubuntu-24.04-arm, arch: aarch64, image: "alpine:3.20" } + steps: + - uses: actions/checkout@v4 + with: + path: xlings + - name: release + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p build + gh release download "v$XV" -R openxlings/xlings -p "xlings-$XV-linux-${{ matrix.arch }}.tar.gz" -O build/release.tar.gz + - name: run (container) + continue-on-error: true + env: + IMAGE: ${{ matrix.image }} + run: | + sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 || true + docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined \ + --security-opt systempaths=unconfined --cap-add SYS_ADMIN \ + -v "$PWD:/w" -w /w "$IMAGE" sh -c 'command -v bash >/dev/null || apk add -q bash; bash -c "bash xlings/tests/host-matrix/bare.sh && bash xlings/tests/host-matrix/run.sh --tarball /w/build/release.tar.gz --evidence /w/evidence --scenarios \"install edition terminal\""' + - name: summary + if: always() + run: | + { echo "### ${{ matrix.image }} (${{ matrix.arch }})"; cat evidence/summary.md 2>/dev/null || echo "(no summary)"; } >> "$GITHUB_STEP_SUMMARY" + for f in evidence/*.ndjson; do [ -f "$f" ] || continue; echo "#### $f"; grep -E '"(fail|error)"' "$f" | head -5; done >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: o4-${{ strategy.job-index }} + path: evidence/ + if-no-files-found: ignore + + # O4: Rocky 9 as a kernel and SELinux host (KVM). + vm-rocky: + name: O4 rocky-9 (KVM) + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + with: + path: xlings + - name: release + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p build + gh release download "v$XV" -R openxlings/xlings -p "xlings-$XV-linux-x86_64.tar.gz" -O build/release.tar.gz + - name: run (vm) + continue-on-error: true + run: | + bash xlings/tools/ci/xlings_tools.sh qemu-system + sudo chmod 666 /dev/kvm + bash xlings/tests/host-matrix/vm.sh rocky-9 --tarball build/release.tar.gz --evidence evidence --scenarios "install edition terminal" + - name: summary + if: always() + run: | + { echo "### rocky-9 (KVM)"; cat evidence/summary.md 2>/dev/null || echo "(no summary)"; } >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: o4-rocky-vm + path: evidence/ + if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index fcfb56cc4..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,1229 +0,0 @@ -name: Release - -on: - workflow_dispatch: - inputs: - version: - description: 'Release version (e.g., 0.2.0). Leave empty to use version from core/config.cppm' - required: false - type: string - -env: - GIT_TERMINAL_PROMPT: 0 - # CI runs on github.com; force GLOBAL mirror so xim doesn't try gitee - # endpoints (which need auth and aren't reachable in CI). - XLINGS_RELEASE_MIRROR: GLOBAL - # Pinned bootstrap xlings used to install mcpp. Bump together with - # XIM_PKGINDEX_REF so release uses a known-good xlings + package index pair. - # - # v2026.8.8.1 stamped `subos_info.runtime` from a CONSTANT when it created - # the default SubOS -- `glibc@2.39` on a home that then resolved and ran - # glibc 2.44. The declaration was false from the moment it was written, and - # mcpp reads it as authoritative (correctly: an exact runtime identity is - # what stops an INTERP and a RUNPATH coming from two different glibcs). - # mcpp-community/mcpp#406 was that lie surfacing two repos away. - # - # v2026.8.10.1 fixed the lie by declaring the version actually active. It - # did NOT fix the cause: the binding was still a constant, so it only stayed - # true while the index happened to agree with it. 2026.8.27.5 takes the - # binding from the index and makes resolution honour it, which is what - # finally decouples this pin from whatever the index publishes. - # - # ⚠️ Below 2026.8.27.2 this pin cannot survive a packaging revision of a - # runtime package -- it installs one version while declaring another. That - # is what kept glibc 2.44.2 out of the index; see xlings#573. - BOOTSTRAP_XLINGS_VERSION: v2026.8.27.5 - # Must contain the mcpp version pinned in .xlings.json (2026.10.5.3). - XIM_PKGINDEX_REF: ebf1fbb3417ec513b923279044433887b3af8850 - -jobs: - build-linux: - runs-on: ubuntu-24.04 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: The runner's own tools (T0; nothing installed) - run: | - for t in curl git cc make jq tar xz python3; do command -v "$t" >/dev/null || { echo "::error::the runner image has no $t"; exit 1; }; done - - - name: Install bootstrap xlings (pinned ${{ env.BOOTSTRAP_XLINGS_VERSION }}) - env: - XLINGS_NON_INTERACTIVE: 1 - XLINGS_VERSION: ${{ env.BOOTSTRAP_XLINGS_VERSION }} - run: | - # Retried, piped through a file, and asserted -- three fixes for one - # failure mode. `curl … | bash` reports the exit status of BASH, and - # bash fed an empty stdin exits 0, so a network reset installs - # nothing and the step goes GREEN. Measured on the 2026.9.4.1 - # release: `curl: (35) Recv failure: Connection reset by peer` here, - # step reported success, and the run died two steps later with - # `xlings: command not found` (exit 127) pointing at the wrong - # thing entirely. - for attempt in 1 2 3; do - curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 20 \ - https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ - -o /tmp/xlings-quick-install.sh && break - echo "bootstrap download failed (attempt $attempt/3); retrying" - sleep 10 - done - test -s /tmp/xlings-quick-install.sh \ - || { echo "::error::could not download quick_install.sh"; exit 1; } - bash /tmp/xlings-quick-install.sh - # The assertion that makes a silent no-op impossible: whatever the - # installer thought it did, the binary either exists here or it does - # not. - test -x "$HOME/.xlings/bin/xlings" \ - || { echo "::error::bootstrap reported success but installed no xlings"; exit 1; } - echo "XLINGS_HOME=$HOME/.xlings" >> "$GITHUB_ENV" - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Cache xlings package payloads - uses: actions/cache@v4 - with: - path: ~/.xlings/data/xpkgs - key: xlings-deps-${{ runner.os }}-${{ hashFiles('.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - xlings-deps-${{ runner.os }}- - - - name: Cache mcpp sandbox and project packages - id: mcpp-cache - uses: actions/cache@v4 - with: - path: | - ~/.mcpp - !~/.mcpp/bmi - !~/.mcpp/build-cache - .mcpp - !.mcpp/bmi - !.mcpp/build-cache - # BMIs are NOT cached, and are deleted after every restore. That - # is the fix, and it took three prefix retirements to find. - # - # A restored BMI set never works. `import 'std' has CRC mismatch` - # follows, which reads like a compiler bug. What hid it is that the - # old guard wiped the BMIs on an INEXACT restore -- so retiring the - # key prefix forced an inexact restore, fired the guard, and produced - # one clean run. Each retirement therefore looked like a fix and was - # an accident; the next run got an exact hit, skipped the guard, and - # failed again. Read straight off two consecutive runs of THIS - # workflow: the one that passed shows "Drop stale BMIs" as `success`, - # the one that failed shows it `skipped`. - # - # So the wipe is unconditional now, and the BMI directories are - # excluded from the cache as well -- the exclusion is an optimisation - # (do not store bytes we delete on arrival), the unconditional wipe is - # the guarantee, and it cannot depend on exclude-pattern semantics. - # - # What the cache is FOR is `registry/data/xpkgs`: ~800 MB of payloads - # that are toolchain-independent and survive a round trip fine. - # - # The key still names the workflow that produced it. mcpp.toml - # declares both the dev toolchain (gcc@16.1.0) and the release target - # (gcc@15.1.0-musl), so a CI build and a release build hash the same - # -- sharing one key across workflows is wrong on its own terms even - # though it was not what caused this. - key: mcpp-v4-release-${{ runner.os }}-${{ hashFiles('mcpp.toml', '.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - mcpp-v4-release-${{ runner.os }}- - # An inexact restore (the `restore-keys` fallback) brings BMIs built - # under a different dependency set. Mixing them with anything rebuilt - # here fails as `import 'std' has CRC mismatch` — which reads like a - # compiler bug and is a cache restored for the wrong inputs. Measured on - # PR #480, where `mcpplibs.xpkg` went 0.0.47 → 0.0.48: the fallback - # cache's tinyhttps/cmdline BMIs no longer matched the `std` the fresh - # xpkg build produced. - # - # Only the BMIs go. The payload store under registry/data/xpkgs is - # dependency-independent and is what makes the ~800 MB cache worth - # restoring at all. - # Evidence for task #56, gathered where the failure actually happens. - # - # Four hypotheses were refuted on a developer machine (2026-08-06): two - # toolchains sharing one BMI store; the tar round trip destroying - # metadata (the .gcm set survives byte-for-byte, mtimes included); a full - # project rebuild against restored BMIs; and prune_registry running after - # the restore. None reproduces it locally, so the next honest step is to - # record what CI has that a laptop does not, rather than guess a fifth - # hypothesis. - # - # What to look for: two entries that disagree on driver_identity or - # sysroot for the same cpp_standard. That would mean the restored set is - # internally inconsistent, which is the answer. - # - # continue-on-error on purpose: a diagnostic must never be the reason a - # release is blocked. Set XLINGS_KEEP_RESTORED_BMIS=1 on a throwaway - # branch to skip the wipe below and let the failure happen with this - # record in hand. - - name: Record the restored BMI set (diagnostic for #56) - shell: bash - continue-on-error: true - run: | - echo "cache-hit: ${{ steps.mcpp-cache.outputs.cache-hit }}" - for d in ~/.mcpp/bmi .mcpp/bmi ~/.mcpp/build-cache .mcpp/build-cache; do - if [ -d "$d" ]; then - echo " $d: $(du -sh "$d" 2>/dev/null | cut -f1), $(find "$d" -name '*.gcm' 2>/dev/null | wc -l) .gcm" - else - echo " $d: absent" - fi - done - echo " --- build identity per BMI dir (first 40) ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | head -40 | - while read -r f; do - printf ' %-18s %s %s\n' \ - "$(basename "$(dirname "$f")")" \ - "$(grep -o '"cpp_standard": *"[^"]*"' "$f" | cut -d'"' -f4)" \ - "$(grep -o '"driver_identity": *"[^"]*"' "$f" | cut -d'"' -f4)" - done - echo " --- distinct driver identities ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | - xargs -r grep -ho '"driver_identity": *"[^"]*"' | sort -u - - - name: Drop restored BMIs - if: env.XLINGS_KEEP_RESTORED_BMIS != '1' - shell: bash - run: | - rm -rf ~/.mcpp/bmi ~/.mcpp/build-cache .mcpp/bmi .mcpp/build-cache - - - name: Prepare bootstrap package index - run: | - rm -rf "$XLINGS_HOME/data/xim-pkgindex" - bash tests/e2e/prepare_fixture_index.sh "$XLINGS_HOME/data/xim-pkgindex" - - - name: Install build dependencies (mcpp) - run: | - cd "$GITHUB_WORKSPACE" - xlings config --mirror GLOBAL - # Refresh the index before resolving anything out of it. - # - # Without this the run resolves against whatever index snapshot the - # bootstrap shipped with, which is not the one this PR needs: a - # freshly published `mcpplibs.xpkg@0.0.50` reported as "not found" - # while older entries from the same index downloaded fine. The - # failure names the package, so it reads as an unpublished - # dependency rather than as an index nobody refreshed. - xlings update || true - xlings install -y - - # Installing is not switching, and this cache makes the difference - # visible. `xlings install` lays the payload down; the shim keeps - # resolving whatever version was already active -- the client says so - # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp - # restored from a cache that line is the ONLY sign that the mcpp - # about to run is the old one. Measured 2026-08-06: .xlings.json - # pinned 2026.8.6.1 while the run used the cached build, whose index - # snapshot predated the dependency this PR needs -- surfacing as - # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably - # had it. - # The pin comes from .xlings.json at runtime, not a literal -- a - # literal here is a value that changes, and the next mcpp bump would - # leave every workflow switching to the previous one. - pin="$(python3 -c 'import json;print(json.load(open(".xlings.json"))["workspace"]["mcpp"])' 2>/dev/null || true)" - [ -n "$pin" ] && xlings use mcpp "$pin" || true - - mcpp self version - - - name: Reinitialize mcpp registry (GLOBAL) - run: | - # Drop the resolved indexes and mirror config so they are - # re-resolved against GLOBAL, but KEEP registry/data/xpkgs -- the - # downloaded and extracted package payloads. - # - # Those payloads are sha256-pinned by their recipe, so they are - # mirror-independent: keeping them cannot leak a CN artifact into a - # GLOBAL build. Wiping them made every run re-fetch every transitive - # source from its upstream host, so CI failed wholesale whenever one - # of those hosts was unreachable -- e.g. compat.bzip2 (a transitive - # dep of compat.libarchive, and absent from mcpp.lock, which only - # pins direct deps) fetched from sourceware.org. It also made the - # ~800 MB mcpp cache restored one step earlier almost worthless. - prune_registry() { - [ -d "$1" ] || return 0 - find "$1" -mindepth 1 -maxdepth 1 ! -name data ! -name subos -exec rm -rf {} + # the subos links into the payloads, and packages into it - [ -d "$1/data" ] || return 0 - find "$1/data" -mindepth 1 -maxdepth 1 ! -name xpkgs -exec rm -rf {} + - } - prune_registry "$HOME/.mcpp/registry" - prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" - mcpp self config --mirror GLOBAL - - # Force the index refresh, then ASSERT what landed. - # - # The prune above drops the resolved indexes, and mcpp's own - # re-fetch happily uses whatever it finds in the restored cache -- - # so a freshly published dependency reads as "not found" against an - # index that demonstrably has it. Measured 2026-08-06: - # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the - # published artifact, the rolling pointer and releases/latest all - # carried the entry. - # - # `mcpp index update` is the refresh; `mcpp index status` prints the - # revision each index landed on, which is the one datum that makes a - # stale-index failure readable instead of surfacing two layers away - # as `@ not found`. - mcpp index update || true - mcpp index status || true - # PER PLATFORM, not a whole-file grep. - # - # A grep for `["0.0.53"]` passes on a file where the entry exists - # only under `xpm.linux` -- which is exactly the bug this assert was - # written to catch and did not: both bumps edited the linux section - # alone, linux CI went green, and macOS/Windows failed against a - # file that literally contains the string. Slice the section for - # THIS platform first. - - - name: Prepare fixture index repo - run: | - bash tests/e2e/prepare_fixture_index.sh - - # The dist-profile artifact gate (binary must be -O3 + link-time strip: - # file says "stripped" with no debug_info, size <= 80 MB; a fallback to - # the dev profile fails the run) lives inside tools/linux_release.sh, - # which is the only build step for the artifact in this job -- the - # checks necessarily execute here and are not duplicated in the - # workflow. - - name: Build (linux_release) - run: | - chmod +x ./tools/linux_release.sh - SKIP_NETWORK_VERIFY=1 ./tools/linux_release.sh - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: xlings-linux-x86_64 - path: build/xlings-*-linux-x86_64.tar.gz - - # The Luban OS app (a window: glibc and X11, through SDL), released with - # this xlings as its own payload -- the index's xim:luban-os depends on - # the index's glibc and X11, which an install patches it onto. - - name: Build the Luban OS app (luban_os_release) - run: | - bash tools/ci/xlings_tools.sh patchelf - mcpp build -p luban-os --profile dist - bash tools/luban_os_release.sh - # xdesk, the desktop compositor of the desktop editions: its own - # payload too (xim:xdesk). - mcpp build -p xdesk-app --profile dist - bash tools/xdesk_release.sh - - - name: Upload the Luban OS app - uses: actions/upload-artifact@v4 - with: - name: luban-os-linux-x86_64 - path: | - build/luban-os-*-linux-x86_64.tar.gz - build/luban-os-*-linux-x86_64.tar.gz.sha256 - build/xdesk-*-linux-x86_64.tar.gz - build/xdesk-*-linux-x86_64.tar.gz.sha256 - - # Cross-build the aarch64-linux-musl release artefact on an x86_64 runner via - # mcpp's cross toolchain (xim:aarch64-linux-musl-gcc). Uses the latest mcpp - # (built from main) and a freshly-updated package index, so the produced - # binary tracks the current ecosystem. The ELF can't execute here, so the - # functional/network verify steps are skipped (SKIP_RUN_VERIFY/SKIP_NETWORK_VERIFY) - # — self-init runs on the user's aarch64 device via `xlings self install`. - build-linux-aarch64: - runs-on: ubuntu-24.04 - timeout-minutes: 70 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: The runner's own tools (T0; nothing installed) - run: | - for t in curl git cc make jq tar xz python3; do command -v "$t" >/dev/null || { echo "::error::the runner image has no $t"; exit 1; }; done - - - name: Install bootstrap xlings (pinned ${{ env.BOOTSTRAP_XLINGS_VERSION }}) - env: - XLINGS_NON_INTERACTIVE: 1 - XLINGS_VERSION: ${{ env.BOOTSTRAP_XLINGS_VERSION }} - run: | - # Retried, piped through a file, and asserted -- three fixes for one - # failure mode. `curl … | bash` reports the exit status of BASH, and - # bash fed an empty stdin exits 0, so a network reset installs - # nothing and the step goes GREEN. Measured on the 2026.9.4.1 - # release: `curl: (35) Recv failure: Connection reset by peer` here, - # step reported success, and the run died two steps later with - # `xlings: command not found` (exit 127) pointing at the wrong - # thing entirely. - for attempt in 1 2 3; do - curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 20 \ - https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ - -o /tmp/xlings-quick-install.sh && break - echo "bootstrap download failed (attempt $attempt/3); retrying" - sleep 10 - done - test -s /tmp/xlings-quick-install.sh \ - || { echo "::error::could not download quick_install.sh"; exit 1; } - bash /tmp/xlings-quick-install.sh - # The assertion that makes a silent no-op impossible: whatever the - # installer thought it did, the binary either exists here or it does - # not. - test -x "$HOME/.xlings/bin/xlings" \ - || { echo "::error::bootstrap reported success but installed no xlings"; exit 1; } - echo "XLINGS_HOME=$HOME/.xlings" >> "$GITHUB_ENV" - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Cache xlings package payloads - uses: actions/cache@v4 - with: - path: ~/.xlings/data/xpkgs - key: xlings-deps-aarch64-${{ runner.os }}-${{ hashFiles('.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - xlings-deps-aarch64-${{ runner.os }}- - - - name: Bootstrap mcpp + refresh index (latest, GLOBAL) - run: | - xlings config --mirror GLOBAL - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - xlings install mcpp -y - mcpp --version - - - name: Build cross-capable mcpp from source (latest main) - run: | - git clone --depth 1 https://github.com/mcpp-community/mcpp /tmp/mcpp-src - cd /tmp/mcpp-src - # Avoid recursively resolving mcpp's development pin while the - # already-installed mcpp bootstraps the source tree. - mv .xlings.json .xlings.json.bootstrap - mcpp self config --mirror GLOBAL - - # Force the index refresh, then ASSERT what landed. - # - # The prune above drops the resolved indexes, and mcpp's own - # re-fetch happily uses whatever it finds in the restored cache -- - # so a freshly published dependency reads as "not found" against an - # index that demonstrably has it. Measured 2026-08-06: - # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the - # published artifact, the rolling pointer and releases/latest all - # carried the entry. - # - # `mcpp index update` is the refresh; `mcpp index status` prints the - # revision each index landed on, which is the one datum that makes a - # stale-index failure readable instead of surfacing two layers away - # as `@ not found`. - mcpp index update || true - mcpp index status || true - # PER PLATFORM, not a whole-file grep. - # - # A grep for `["0.0.53"]` passes on a file where the entry exists - # only under `xpm.linux` -- which is exactly the bug this assert was - # written to catch and did not: both bumps edited the linux section - # alone, linux CI went green, and macOS/Windows failed against a - # file that literally contains the string. Slice the section for - # THIS platform first. - mcpp 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" - - # The dist-profile artifact gate (stripped + no debug_info + size cap) - # lives inside tools/linux_release.sh, the only build step for the - # artifact -- the mcpp source build above produces the cross build - # TOOL, not the release artifact, so the gate necessarily runs below. - - name: Build (linux_release — aarch64 cross) - run: | - chmod +x ./tools/linux_release.sh - MCPP_BIN="$MCPP" MCPP_TARGET=aarch64-linux-musl \ - SKIP_RUN_VERIFY=1 SKIP_NETWORK_VERIFY=1 \ - ./tools/linux_release.sh - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: xlings-linux-aarch64 - path: build/xlings-*-linux-aarch64.tar.gz - - build-macos: - runs-on: macos-15 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install bootstrap xlings (pinned ${{ env.BOOTSTRAP_XLINGS_VERSION }}) - env: - XLINGS_NON_INTERACTIVE: 1 - XLINGS_VERSION: ${{ env.BOOTSTRAP_XLINGS_VERSION }} - run: | - # Retried, piped through a file, and asserted -- three fixes for one - # failure mode. `curl … | bash` reports the exit status of BASH, and - # bash fed an empty stdin exits 0, so a network reset installs - # nothing and the step goes GREEN. Measured on the 2026.9.4.1 - # release: `curl: (35) Recv failure: Connection reset by peer` here, - # step reported success, and the run died two steps later with - # `xlings: command not found` (exit 127) pointing at the wrong - # thing entirely. - for attempt in 1 2 3; do - curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 20 \ - https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ - -o /tmp/xlings-quick-install.sh && break - echo "bootstrap download failed (attempt $attempt/3); retrying" - sleep 10 - done - test -s /tmp/xlings-quick-install.sh \ - || { echo "::error::could not download quick_install.sh"; exit 1; } - bash /tmp/xlings-quick-install.sh - # The assertion that makes a silent no-op impossible: whatever the - # installer thought it did, the binary either exists here or it does - # not. - test -x "$HOME/.xlings/bin/xlings" \ - || { echo "::error::bootstrap reported success but installed no xlings"; exit 1; } - echo "XLINGS_HOME=$HOME/.xlings" >> "$GITHUB_ENV" - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Cache xlings package payloads - uses: actions/cache@v4 - with: - path: ~/.xlings/data/xpkgs - key: xlings-deps-${{ runner.os }}-${{ hashFiles('.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - xlings-deps-${{ runner.os }}- - - - name: Cache mcpp sandbox and project packages - id: mcpp-cache - uses: actions/cache@v4 - with: - path: | - ~/.mcpp - !~/.mcpp/bmi - !~/.mcpp/build-cache - .mcpp - !.mcpp/bmi - !.mcpp/build-cache - # `mcpp-v3-`: the v1 keyspace holds entries saved by runs that - # built against a fallback-restored BMI set and failed. Those are - # poisoned at their exact key, so the guard below -- which only fires - # on an INEXACT restore -- can never reach them. Retiring the prefix - # is what discards them; the guard is what stops it recurring. - key: mcpp-v4-release-${{ runner.os }}-${{ hashFiles('mcpp.toml', '.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - mcpp-v4-release-${{ runner.os }}- - # An inexact restore brings BMIs built under a different dependency set. - # Mixing them with anything rebuilt here fails as `import 'std' has CRC - # mismatch`, which reads like a compiler bug. Only the BMIs go -- the - # payload store is dependency-independent and is what makes the ~800 MB - # cache worth restoring. - # Evidence for task #56, gathered where the failure actually happens. - # - # Four hypotheses were refuted on a developer machine (2026-08-06): two - # toolchains sharing one BMI store; the tar round trip destroying - # metadata (the .gcm set survives byte-for-byte, mtimes included); a full - # project rebuild against restored BMIs; and prune_registry running after - # the restore. None reproduces it locally, so the next honest step is to - # record what CI has that a laptop does not, rather than guess a fifth - # hypothesis. - # - # What to look for: two entries that disagree on driver_identity or - # sysroot for the same cpp_standard. That would mean the restored set is - # internally inconsistent, which is the answer. - # - # continue-on-error on purpose: a diagnostic must never be the reason a - # release is blocked. Set XLINGS_KEEP_RESTORED_BMIS=1 on a throwaway - # branch to skip the wipe below and let the failure happen with this - # record in hand. - - name: Record the restored BMI set (diagnostic for #56) - shell: bash - continue-on-error: true - run: | - echo "cache-hit: ${{ steps.mcpp-cache.outputs.cache-hit }}" - for d in ~/.mcpp/bmi .mcpp/bmi ~/.mcpp/build-cache .mcpp/build-cache; do - if [ -d "$d" ]; then - echo " $d: $(du -sh "$d" 2>/dev/null | cut -f1), $(find "$d" -name '*.gcm' 2>/dev/null | wc -l) .gcm" - else - echo " $d: absent" - fi - done - echo " --- build identity per BMI dir (first 40) ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | head -40 | - while read -r f; do - printf ' %-18s %s %s\n' \ - "$(basename "$(dirname "$f")")" \ - "$(grep -o '"cpp_standard": *"[^"]*"' "$f" | cut -d'"' -f4)" \ - "$(grep -o '"driver_identity": *"[^"]*"' "$f" | cut -d'"' -f4)" - done - echo " --- distinct driver identities ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | - xargs -r grep -ho '"driver_identity": *"[^"]*"' | sort -u - - - name: Drop restored BMIs - if: env.XLINGS_KEEP_RESTORED_BMIS != '1' - shell: bash - run: | - rm -rf ~/.mcpp/bmi ~/.mcpp/build-cache .mcpp/bmi .mcpp/build-cache - - - - name: Prepare bootstrap package index - run: | - rm -rf "$XLINGS_HOME/data/xim-pkgindex" - bash tests/e2e/prepare_fixture_index.sh "$XLINGS_HOME/data/xim-pkgindex" - - - name: Install build dependencies (mcpp) - run: | - cd "$GITHUB_WORKSPACE" - xlings config --mirror GLOBAL - # Refresh the index before resolving anything out of it. - # - # Without this the run resolves against whatever index snapshot the - # bootstrap shipped with, which is not the one this PR needs: a - # freshly published `mcpplibs.xpkg@0.0.50` reported as "not found" - # while older entries from the same index downloaded fine. The - # failure names the package, so it reads as an unpublished - # dependency rather than as an index nobody refreshed. - xlings update || true - xlings install -y - - # Installing is not switching, and this cache makes the difference - # visible. `xlings install` lays the payload down; the shim keeps - # resolving whatever version was already active -- the client says so - # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp - # restored from a cache that line is the ONLY sign that the mcpp - # about to run is the old one. Measured 2026-08-06: .xlings.json - # pinned 2026.8.6.1 while the run used the cached build, whose index - # snapshot predated the dependency this PR needs -- surfacing as - # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably - # had it. - # The pin comes from .xlings.json at runtime, not a literal -- a - # literal here is a value that changes, and the next mcpp bump would - # leave every workflow switching to the previous one. - pin="$(python3 -c 'import json;print(json.load(open(".xlings.json"))["workspace"]["mcpp"])' 2>/dev/null || true)" - [ -n "$pin" ] && xlings use mcpp "$pin" || true - - mcpp self version - - - name: Reinitialize mcpp registry (GLOBAL) - run: | - # Drop the resolved indexes and mirror config so they are - # re-resolved against GLOBAL, but KEEP registry/data/xpkgs -- the - # downloaded and extracted package payloads. - # - # Those payloads are sha256-pinned by their recipe, so they are - # mirror-independent: keeping them cannot leak a CN artifact into a - # GLOBAL build. Wiping them made every run re-fetch every transitive - # source from its upstream host, so CI failed wholesale whenever one - # of those hosts was unreachable -- e.g. compat.bzip2 (a transitive - # dep of compat.libarchive, and absent from mcpp.lock, which only - # pins direct deps) fetched from sourceware.org. It also made the - # ~800 MB mcpp cache restored one step earlier almost worthless. - prune_registry() { - [ -d "$1" ] || return 0 - find "$1" -mindepth 1 -maxdepth 1 ! -name data ! -name subos -exec rm -rf {} + # the subos links into the payloads, and packages into it - [ -d "$1/data" ] || return 0 - find "$1/data" -mindepth 1 -maxdepth 1 ! -name xpkgs -exec rm -rf {} + - } - prune_registry "$HOME/.mcpp/registry" - prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" - mcpp self config --mirror GLOBAL - - # Force the index refresh, then ASSERT what landed. - # - # The prune above drops the resolved indexes, and mcpp's own - # re-fetch happily uses whatever it finds in the restored cache -- - # so a freshly published dependency reads as "not found" against an - # index that demonstrably has it. Measured 2026-08-06: - # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the - # published artifact, the rolling pointer and releases/latest all - # carried the entry. - # - # `mcpp index update` is the refresh; `mcpp index status` prints the - # revision each index landed on, which is the one datum that makes a - # stale-index failure readable instead of surfacing two layers away - # as `@ not found`. - mcpp index update || true - mcpp index status || true - # PER PLATFORM, not a whole-file grep. - # - # A grep for `["0.0.53"]` passes on a file where the entry exists - # only under `xpm.linux` -- which is exactly the bug this assert was - # written to catch and did not: both bumps edited the linux section - # alone, linux CI went green, and macOS/Windows failed against a - # file that literally contains the string. Slice the section for - # THIS platform first. - - # The dist-profile artifact gate (binary must be -O3 + link-time strip: - # no LC_SYMTAB load command, size <= 80 MB) lives inside - # tools/macos_release.sh, which is the only build step for the artifact - # in this job -- the checks necessarily execute here and are not - # duplicated in the workflow. - - name: Build (macos_release) - env: - # macOS min-version support: floor 14.0 (the official LLVM - # static libc++ archives are built for macOS 14; lower needs a - # custom libc++ build). The mcpp from the bootstrap index - # (0.0.50+) links static LLVM libc++ via lld natively; - # macos_release.sh asserts minos + no system libc++ dylib when - # this env is set. - MACOSX_DEPLOYMENT_TARGET: '14.0' - run: | - chmod +x ./tools/macos_release.sh - SKIP_NETWORK_VERIFY=1 ./tools/macos_release.sh - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: xlings-macosx-arm64 - path: build/xlings-*-macosx-arm64.tar.gz - - # The Luban OS app for macOS (part 4 §8.1): its own payload, the - # system's frameworks only (tools/luban_os_release.sh checks). - - name: Build the Luban OS app (luban_os_release, macOS) - env: - MACOSX_DEPLOYMENT_TARGET: '14.0' - run: | - mcpp build -p luban-os --profile dist - bash tools/luban_os_release.sh - - - name: Upload the Luban OS app (macOS) - uses: actions/upload-artifact@v4 - with: - name: luban-os-macosx-arm64 - path: | - build/luban-os-*-macosx-arm64.tar.gz - build/luban-os-*-macosx-arm64.tar.gz.sha256 - - build-windows: - runs-on: windows-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install bootstrap xlings (pinned ${{ env.BOOTSTRAP_XLINGS_VERSION }}) - shell: pwsh - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: ${{ env.BOOTSTRAP_XLINGS_VERSION }} - run: | - # Same failure mode as the POSIX bootstrap, same three fixes: `irm | - # iex` reports whether iex ran, not whether irm fetched anything, - # so a reset connection installs nothing and the step goes green. - # `-ErrorAction Stop` on the call, NOT $ErrorActionPreference: the - # preference would stay set for `& $script` below and turn any - # non-terminating error the installer writes into an abort -- a - # behaviour change well outside what this fixes. - $script = "$env:TEMP\xlings-quick-install.ps1" - for ($i = 1; $i -le 3; $i++) { - try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 -ErrorAction Stop ` - -Uri https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 ` - -OutFile $script; break } - catch { Write-Host "bootstrap download failed (attempt $i/3); retrying"; Start-Sleep 10 } - } - if (-not (Test-Path $script) -or (Get-Item $script).Length -eq 0) { - Write-Host "::error::could not download quick_install.ps1"; exit 1 - } - & $script - # Whatever the installer thought it did, the binary is either here or - # it is not. - if (-not (Test-Path "$env:USERPROFILE\.xlings\bin\xlings.exe")) { - Write-Host "::error::bootstrap reported success but installed no xlings"; exit 1 - } - "XLINGS_HOME=$env:USERPROFILE\.xlings" >> $env:GITHUB_ENV - "$env:USERPROFILE\.xlings\subos\current\bin" >> $env:GITHUB_PATH - "$env:USERPROFILE\.xlings\bin" >> $env:GITHUB_PATH - - - name: Cache xlings package payloads - uses: actions/cache@v4 - with: - path: ~\.xlings\data\xpkgs - key: xlings-deps-${{ runner.os }}-${{ hashFiles('.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - xlings-deps-${{ runner.os }}- - - - name: Cache mcpp sandbox and project packages - id: mcpp-cache - uses: actions/cache@v4 - with: - path: | - ~\.mcpp - !~\.mcpp/bmi - !~\.mcpp/build-cache - .mcpp - !.mcpp/bmi - !.mcpp/build-cache - # `mcpp-v3-`: the v1 keyspace holds entries saved by runs that - # built against a fallback-restored BMI set and failed. Those are - # poisoned at their exact key, so the guard below -- which only fires - # on an INEXACT restore -- can never reach them. Retiring the prefix - # is what discards them; the guard is what stops it recurring. - key: mcpp-v4-release-${{ runner.os }}-${{ hashFiles('mcpp.toml', '.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - mcpp-v4-release-${{ runner.os }}- - # An inexact restore brings BMIs built under a different dependency set. - # Mixing them with anything rebuilt here fails as `import 'std' has CRC - # mismatch`, which reads like a compiler bug. Only the BMIs go -- the - # payload store is dependency-independent and is what makes the ~800 MB - # cache worth restoring. - # Evidence for task #56, gathered where the failure actually happens. - # - # Four hypotheses were refuted on a developer machine (2026-08-06): two - # toolchains sharing one BMI store; the tar round trip destroying - # metadata (the .gcm set survives byte-for-byte, mtimes included); a full - # project rebuild against restored BMIs; and prune_registry running after - # the restore. None reproduces it locally, so the next honest step is to - # record what CI has that a laptop does not, rather than guess a fifth - # hypothesis. - # - # What to look for: two entries that disagree on driver_identity or - # sysroot for the same cpp_standard. That would mean the restored set is - # internally inconsistent, which is the answer. - # - # continue-on-error on purpose: a diagnostic must never be the reason a - # release is blocked. Set XLINGS_KEEP_RESTORED_BMIS=1 on a throwaway - # branch to skip the wipe below and let the failure happen with this - # record in hand. - - name: Record the restored BMI set (diagnostic for #56) - shell: bash - continue-on-error: true - run: | - echo "cache-hit: ${{ steps.mcpp-cache.outputs.cache-hit }}" - for d in ~/.mcpp/bmi .mcpp/bmi ~/.mcpp/build-cache .mcpp/build-cache; do - if [ -d "$d" ]; then - echo " $d: $(du -sh "$d" 2>/dev/null | cut -f1), $(find "$d" -name '*.gcm' 2>/dev/null | wc -l) .gcm" - else - echo " $d: absent" - fi - done - echo " --- build identity per BMI dir (first 40) ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | head -40 | - while read -r f; do - printf ' %-18s %s %s\n' \ - "$(basename "$(dirname "$f")")" \ - "$(grep -o '"cpp_standard": *"[^"]*"' "$f" | cut -d'"' -f4)" \ - "$(grep -o '"driver_identity": *"[^"]*"' "$f" | cut -d'"' -f4)" - done - echo " --- distinct driver identities ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | - xargs -r grep -ho '"driver_identity": *"[^"]*"' | sort -u - - - name: Drop restored BMIs - if: env.XLINGS_KEEP_RESTORED_BMIS != '1' - shell: bash - run: | - rm -rf ~/.mcpp/bmi ~/.mcpp/build-cache .mcpp/bmi .mcpp/build-cache - - - - name: Prepare bootstrap package index - shell: pwsh - run: | - $indexDir = Join-Path $env:USERPROFILE ".xlings\data\xim-pkgindex" - Remove-Item -Recurse -Force $indexDir -ErrorAction SilentlyContinue - New-Item -ItemType Directory -Force (Split-Path $indexDir) | Out-Null - git clone https://github.com/openxlings/xim-pkgindex.git $indexDir - git -C $indexDir checkout --quiet $env:XIM_PKGINDEX_REF - if (-not (Test-Path (Join-Path $indexDir "pkgs"))) { - throw "bootstrap package index missing pkgs/" - } - - - name: Install build dependencies (mcpp) - shell: pwsh - run: | - Set-Location $env:GITHUB_WORKSPACE - xlings config --mirror GLOBAL - xlings install -y - mcpp --version - mcpp self version - - - name: Reinitialize mcpp registry (GLOBAL) - shell: pwsh - run: | - Remove-Item -Recurse -Force "$env:USERPROFILE\.mcpp\registry" -ErrorAction SilentlyContinue - Remove-Item -Recurse -Force "$env:GITHUB_WORKSPACE\.mcpp\registry" -ErrorAction SilentlyContinue - mcpp self config --mirror GLOBAL - - # Force the index refresh, then ASSERT what landed. - # - # The prune above drops the resolved indexes, and mcpp's own - # re-fetch happily uses whatever it finds in the restored cache -- - # so a freshly published dependency reads as "not found" against an - # index that demonstrably has it. Measured 2026-08-06: - # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the - # published artifact, the rolling pointer and releases/latest all - # carried the entry. - # - # `mcpp index update` is the refresh; `mcpp index status` prints the - # revision each index landed on, which is the one datum that makes a - # stale-index failure readable instead of surfacing two layers away - # as `@ not found`. - mcpp index update || true - mcpp index status || true - # PER PLATFORM, not a whole-file grep. - # - # A grep for `["0.0.53"]` passes on a file where the entry exists - # only under `xpm.linux` -- which is exactly the bug this assert was - # written to catch and did not: both bumps edited the linux section - # alone, linux CI went green, and macOS/Windows failed against a - # file that literally contains the string. Slice the section for - # THIS platform first. - - # The dist-profile artifact gate (no PDB beside the exe, size <= 80 MB) - # lives inside tools/windows_release.ps1, the only build step for the - # artifact in this job -- the checks necessarily execute here and are - # not duplicated in the workflow. - - name: Build (windows_release) - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tools\windows_release.ps1" - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: xlings-windows-x86_64 - path: build/xlings-*-windows-x86_64.zip - - # The Luban OS app for Windows (part 4 §8.2): its own payload. - - name: Build the Luban OS app (luban_os_release, Windows) - shell: pwsh - run: | - mcpp build -p luban-os --profile dist - if ($LASTEXITCODE -ne 0) { throw "mcpp build -p luban-os failed" } - & "$env:GITHUB_WORKSPACE\tools\luban_os_release.ps1" - - - name: Upload the Luban OS app (Windows) - uses: actions/upload-artifact@v4 - with: - name: luban-os-windows-x86_64 - path: | - build/luban-os-*-windows-x86_64.zip - build/luban-os-*-windows-x86_64.zip.sha256 - - prepare-candidates: - needs: [build-linux, build-linux-aarch64, build-macos, build-windows] - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - path: artifacts - - name: Generate source-bound sidecars - run: bash tools/gen_release_sidecars.sh artifacts sidecars - - uses: actions/upload-artifact@v4 - with: - name: xlings-release-sidecars - path: sidecars/*.sha256 - - candidate-linux-x86_64: - needs: [prepare-candidates] - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: {name: xlings-linux-x86_64, path: candidate} - - uses: actions/download-artifact@v4 - with: {name: xlings-release-sidecars, path: sidecars} - - run: bash tests/candidate-install/smoke.sh candidate/*.tar.gz sidecars/xlings-*-linux-x86_64.tar.gz.sha256 - - candidate-linux-aarch64: - needs: [prepare-candidates] - runs-on: ubuntu-24.04-arm - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: {name: xlings-linux-aarch64, path: candidate} - - uses: actions/download-artifact@v4 - with: {name: xlings-release-sidecars, path: sidecars} - - run: bash tests/candidate-install/smoke.sh candidate/*.tar.gz sidecars/xlings-*-linux-aarch64.tar.gz.sha256 - - candidate-macos-arm64: - needs: [prepare-candidates] - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: {name: xlings-macosx-arm64, path: candidate} - - uses: actions/download-artifact@v4 - with: {name: xlings-release-sidecars, path: sidecars} - - run: bash tests/candidate-install/smoke.sh candidate/*.tar.gz sidecars/xlings-*-macosx-arm64.tar.gz.sha256 - - candidate-windows-x86_64: - needs: [prepare-candidates] - runs-on: windows-2022 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: {name: xlings-windows-x86_64, path: candidate} - - uses: actions/download-artifact@v4 - with: {name: xlings-release-sidecars, path: sidecars} - - shell: pwsh - run: powershell -File tests/candidate-install/smoke.ps1 (Get-ChildItem candidate/*.zip).FullName (Get-ChildItem sidecars/xlings-*-windows-x86_64.zip.sha256).FullName - - create-release: - needs: [candidate-linux-x86_64, candidate-linux-aarch64, candidate-macos-arm64, candidate-windows-x86_64] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Determine version - id: version - run: | - if [[ -n "${{ inputs.version }}" ]]; then - echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - else - VER=$(sed -n 's/.*VERSION = "\([^"]*\)".*/\1/p' src/core/config.cppm | head -1) - if [[ -z "$VER" ]]; then - echo "Failed to read VERSION from src/core/config.cppm." >&2 - echo "Either fix the source path or pass an explicit version via workflow_dispatch input." >&2 - exit 1 - fi - echo "version=$VER" >> "$GITHUB_OUTPUT" - fi - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: List artifacts - run: find artifacts -type f - - - name: Create Release - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ steps.version.outputs.version }} - name: ${{ steps.version.outputs.version }} - draft: false - prerelease: false - files: | - artifacts/xlings-linux-x86_64/xlings-*-linux-x86_64.tar.gz - artifacts/luban-os-linux-x86_64/luban-os-*-linux-x86_64.tar.gz - artifacts/luban-os-linux-x86_64/xdesk-*-linux-x86_64.tar.gz - artifacts/luban-os-macosx-arm64/luban-os-*-macosx-arm64.tar.gz - artifacts/luban-os-windows-x86_64/luban-os-*-windows-x86_64.zip - artifacts/xlings-linux-aarch64/xlings-*-linux-aarch64.tar.gz - artifacts/xlings-macosx-arm64/xlings-*-macosx-arm64.tar.gz - artifacts/xlings-windows-x86_64/xlings-*-windows-x86_64.zip - artifacts/xlings-release-sidecars/*.sha256 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Build + publish the xim package index as a versioned resource artifact - # (Y-asset) to xlings-res/xim-index on GitHub (+ GitCode). Best-effort: each - # target is skipped when its token secret is absent, and the job never blocks - # the release. See .agents/docs/2026-06-22-index-as-resource-impl-plan.md. - publish-index: - needs: [create-release] - runs-on: ubuntu-latest - # secrets aren't available in step if: — surface them as env, gate on env. - env: - XLINGS_RES_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Determine version - id: version - run: | - if [[ -n "${{ inputs.version }}" ]]; then - echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - else - VER=$(sed -n 's/.*VERSION = "\([^"]*\)".*/\1/p' src/core/config.cppm | head -1) - echo "version=$VER" >> "$GITHUB_OUTPUT" - fi - - - name: Build index artifacts (main + default sub-indexes) - run: | - mkdir -p build/index - VER="${{ steps.version.outputs.version }}" - # main index - XLINGS_RELEASE_MIRROR=GLOBAL bash tools/build_xim_index_artifact.sh \ - --version "$VER" --out build/index - # default sub-indexes (URLs mirror the main index's xim-indexrepos.lua) - XLINGS_RELEASE_PKGINDEX_URL=https://github.com/openxlings/xim-pkgindex-awesome.git \ - bash tools/build_xim_index_artifact.sh --name awesome --version "$VER" --out build/index - XLINGS_RELEASE_PKGINDEX_URL=https://github.com/openxlings/xim-pkgindex-scode.git \ - bash tools/build_xim_index_artifact.sh --name scode --version "$VER" --out build/index - XLINGS_RELEASE_PKGINDEX_URL=https://github.com/d2learn/xim-pkgindex-d2x.git \ - bash tools/build_xim_index_artifact.sh --name d2x --version "$VER" --out build/index - - - name: Publish to GitHub (xlings-res/xim-index) - if: ${{ env.XLINGS_RES_TOKEN != '' }} - env: - GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - run: | - VER="${{ steps.version.outputs.version }}" - bash tools/publish_xim_index.sh --version "$VER" --dir build/index --skip-gitcode - for n in awesome scode d2x; do - bash tools/publish_xim_index.sh --version "$VER" --name "$n" --dir build/index --skip-gitcode - done - - # GitCode publish uses the vendored tools/gtc + GITCODE_TOKEN. Non-blocking. - - name: Publish to GitCode (xlings-res/xim-index) - if: ${{ env.GITCODE_TOKEN != '' }} - run: | - chmod +x tools/gtc - export PATH="$PWD/tools:$PATH" - VER="${{ steps.version.outputs.version }}" - bash tools/publish_xim_index.sh --version "$VER" --dir build/index --skip-github \ - || echo "gitcode main publish failed (non-blocking)" - for n in awesome scode d2x; do - bash tools/publish_xim_index.sh --version "$VER" --name "$n" --dir build/index --skip-github \ - || echo "gitcode $n publish failed (non-blocking)" - done - - # Push the rolling pointers as repo files (overwriteable, served raw) to - # both GitHub and GitCode. This is what clients read to find the latest - # artifact; repo files (unlike gitcode release assets) can be updated. - - name: Push index pointers (raw repo files, gh + gtc) - if: ${{ env.XLINGS_RES_TOKEN != '' || env.GITCODE_TOKEN != '' }} - env: - XLINGS_RES_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - # #476: recorded at the pointer top level so a client routed to an - # older index snapshot can still learn a newer client exists. Without - # it, that client reads the old snapshot's own xlings recipe, is told - # it is current, and can never leave the snapshot it was routed to. - XLINGS_CLIENT_LATEST: ${{ steps.version.outputs.version }} - run: bash tools/push_index_pointers.sh build/index - - # Mirror the xlings binaries to xlings-res/xlings on GitHub + GitCode so - # XLINGS_RES downloads resolve for all platforms (CN binary path is - # GitCode-only). Per-file retry: a multi-file gtc upload can 502 and drop - # later files (e.g. windows/macos missing). Best-effort, non-blocking. - mirror-binaries: - needs: [create-release] - runs-on: ubuntu-latest - # Hard backstop: normal runtime is 2-5 min. Without this, a stalled - # GitCode upload/verify pinned the job for 40+ min (v0.4.65). The script - # itself bounds every network call; this catches anything it can't. - # - # KNOWN LIMITATION — GitCode big assets: a GitHub-hosted runner CANNOT - # upload assets larger than ~8 MiB to GitCode (cross-border runner->OBS - # sustains only ~15-30 KB/s and times out; proven in probe PR #371). This - # job therefore mirrors everything to GitHub + the small GitCode assets, - # and is fail-fast (non-blocking) on the big GitCode ones. A maintainer - # finishes the GitCode mirror AFTER release by running, from any CN - # environment: `tools/mirror-latest.sh xlings`. Correctness does not depend - # on it — CN clients fall back to the GitHub asset URLs. Runbook: - # .agents/docs/2026-07-15-gitcode-large-asset-mirror-runbook.md - timeout-minutes: 15 - env: - XLINGS_RES_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Determine version - id: version - run: | - if [[ -n "${{ inputs.version }}" ]]; then - echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - else - echo "version=$(sed -n 's/.*VERSION = "\([^"]*\)".*/\1/p' src/core/config.cppm | head -1)" >> "$GITHUB_OUTPUT" - fi - # See bump-index for why `|| echo` was replaced: on 2026.7.27.0 this - # step created the GitCode release, uploaded no assets, and reported - # success. Every CN download 404'd. - - name: Mirror binaries to xlings-res/xlings (gh + gtc) - id: mirror - if: ${{ env.XLINGS_RES_TOKEN != '' }} - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - run: | - chmod +x tools/gtc tools/mirror_xlings_res.sh tools/mirror_res.sh - export PATH="$PWD/tools:$PATH" - # Each project on its own: a big asset a runner cannot upload to - # GitCode (expected; tools/mirror-latest.sh finishes it from a CN - # machine) used to end the step before luban-os and xdesk had even - # their GitHub release, which only this job can create. - rc=0 - bash tools/mirror_xlings_res.sh "${{ steps.version.outputs.version }}" || rc=1 - # The Luban OS app and xdesk beside it (xlings-res/luban-os, xdesk). - bash tools/mirror_res.sh luban-os "${{ steps.version.outputs.version }}" || rc=1 - bash tools/mirror_res.sh xdesk "${{ steps.version.outputs.version }}" || rc=1 - exit $rc - - - name: Report mirror outcome - if: ${{ always() && env.XLINGS_RES_TOKEN != '' }} - run: | - VER="${{ steps.version.outputs.version }}" - if [[ "${{ steps.mirror.outcome }}" == "success" ]]; then - echo "✅ binary mirror: \`xlings-res/xlings@$VER\`" >> "$GITHUB_STEP_SUMMARY" - else - { - echo "### ❌ binary mirror FAILED" - echo - echo "CN downloads for \`$VER\` may 404, and \`bump-index\` runs after" - echo "this job — check whether it also reported a failure." - echo - echo "Recover from any CN environment:" - echo - echo '```' - echo "tools/mirror-latest.sh xlings" - echo '```' - echo - echo "Verify with GET (HEAD returns 401 on GitCode):" - echo - echo '```' - echo "curl -sL -o /dev/null -w '%{http_code}\\n' \\" - echo " https://gitcode.com/xlings-res/xlings/releases/download/$VER/xlings-$VER-linux-x86_64.tar.gz" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - fi - - # Bump the xlings entry in the official index (openxlings/xim-pkgindex) and - # open a PR — a maintainer merges it. On merge, the index repo republishes - # its artifact and gitee-sync mirrors it. - # - # needs mirror-binaries, NOT because the index is on the critical path for - # availability (it isn't — the res mirror + index artifact cover that), but - # because bump_index.sh cannot do its job until the mirror exists: - # version-check.py hashes each released asset off xlings-res before writing - # the entry. Running the two in parallel — which is what the 2026.7.27.0 - # release did — leaves it nothing to hash, so it edits nothing, and `latest` - # stayed at 0.4.69 while the job reported success. No user could - # `self update` until it was re-run by hand. - bump-index: - needs: [create-release, mirror-binaries] - runs-on: ubuntu-latest - env: - XIM_PKGINDEX_TOKEN: ${{ secrets.XIM_PKGINDEX_TOKEN }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Determine version - id: version - run: | - if [[ -n "${{ inputs.version }}" ]]; then - echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - else - echo "version=$(sed -n 's/.*VERSION = "\([^"]*\)".*/\1/p' src/core/config.cppm | head -1)" >> "$GITHUB_OUTPUT" - fi - # continue-on-error rather than `|| echo`: still non-blocking for the - # release, but the step is marked failed and annotated instead of - # printing a line into a log nobody reads. `|| echo` is how a job that - # did nothing reported 7/7 success. - - name: Open index bump PR (xim-pkgindex) - id: bump - if: ${{ env.XIM_PKGINDEX_TOKEN != '' }} - continue-on-error: true - env: - PKGINDEX_TOKEN: ${{ secrets.XIM_PKGINDEX_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - chmod +x tools/bump_index.sh - bash tools/bump_index.sh xlings "${{ steps.version.outputs.version }}" - - - name: Report bump outcome - if: ${{ always() && env.XIM_PKGINDEX_TOKEN != '' }} - run: | - VER="${{ steps.version.outputs.version }}" - if [[ "${{ steps.bump.outcome }}" == "success" ]]; then - echo "✅ index bump: \`latest\` tracks \`$VER\`" >> "$GITHUB_STEP_SUMMARY" - else - { - echo "### ❌ index bump FAILED — users cannot \`self update\`" - echo - echo "\`latest\` in xim-pkgindex may still point at the previous" - echo "version. Recover from a clone of the index repo:" - echo - echo '```' - echo "python3 .github/scripts/version-check.py --workspace \"\$PWD\" --apply --only xlings" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/xlings-ci-aarch64.yml b/.github/workflows/xlings-ci-aarch64.yml deleted file mode 100644 index 57d58ea5b..000000000 --- a/.github/workflows/xlings-ci-aarch64.yml +++ /dev/null @@ -1,405 +0,0 @@ -name: xlings-ci-aarch64 - -# aarch64 / Android closed-loop CI for xlings. -# -# xlings is built by mcpp, so an aarch64 xlings is produced exactly like an -# aarch64 mcpp: cross-compile with `mcpp build --target aarch64-linux-musl` -# using the cross toolchain from the xlings ecosystem -# (xim:aarch64-linux-musl-gcc -> xlings-res), then run under qemu-aarch64. -# -# The aarch64 cross support lives in mcpp itself, so we self-host a fresh -# cross-capable mcpp from source (mcpp main) rather than relying on the pinned -# bootstrap mcpp. The artifact is a fully static musl aarch64 ELF — runnable -# natively in Termux on an Android phone (no bionic dependency). - -# Release-train integration branches (release/**) run the same matrix as -# main: PRs land on the integration branch first and only reach main once -# the whole train is green, so CI must gate them there, not at merge time. -on: - push: - branches: [ main, 'release/**' ] - pull_request: - branches: [ main, 'release/**' ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - # An xim-pkgindex branch whose packages are not published yet (a PR's, - # while both PRs are in flight; the repository variable is unset when the - # index PR merges): what CI installs its tools from (tools/ci/xlings_tools.sh) - # and what the tool table is held to (tools/lint_tools_provenance.sh). - XLINGS_TOOLS_INDEX_REF: ${{ vars.XLINGS_INDEX_UNDER_TEST || 'main' }} - XLINGS_PROVENANCE_INDEX_REF: ${{ vars.XLINGS_INDEX_UNDER_TEST || 'main' }} - # A bootstrap pin is not neutral setup: the client that lays down - # `subos/default` decides the runtime binding, so this line has to name a - # client whose binding the index can satisfy. - # - # It went v2026.8.10.1 -> v2026.8.27.1 while the index offered glibc 2.44.2, - # and back again when 2.44.2 was withdrawn (xim-pkgindex#700: three - # consumers pinned the binding name and none of them moved together). - # v2026.8.10.1 said `glibc@2.44` and the index offered it, so it worked -- - # but it worked because a constant happened to match, not because anything - # connected the two. - # - # 2026.8.27.5 is where it stops being luck. Its binding comes from the - # index, and resolution honours what the subos DECLARES over what the index - # calls newest, so the two cannot drift apart: - # - # declares glibc@X -> installs glibc@X, whatever `latest` moves to - # - # Measured across four client versions against an index carrying both 2.44 - # and 2.44.2: every client whose binding is a compiled-in CONSTANT installs - # one version while declaring another; every client that reads the index -- - # 2026.8.27.4 and .5 -- stays consistent. This pin was the last constant. - # - # ⚠️ Do not move it back below 2026.8.27.2 without re-running that probe: - # this is the pin that decides whether a packaging revision can ever be - # published for a runtime package again. - BOOTSTRAP_XLINGS_VERSION: v2026.8.27.5 - -jobs: - native-contracts: - name: native aarch64 compatibility contracts - needs: cross-build-and-run - runs-on: ubuntu-24.04-arm - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: xlings-linux-aarch64-candidate - path: candidate - - name: Run native contracts from the cross-built candidate - run: | - bash tests/e2e/prepare_fixture_index.sh - package_root=$(mktemp -d) - tar -xzf candidate/*.tar.gz -C "$package_root" - bin=$(find "$package_root" -path '*/bin/xlings' -type f | head -1) - test -x "$bin" - "$bin" --version - XLINGS_BIN="$bin" bash tests/e2e/aarch64_compat_contract_test.sh - - name: Candidate cold-home and aarch64 fail-closed lifecycle - run: | - bash tests/candidate-install/smoke.sh \ - candidate/*.tar.gz candidate/*.tar.gz.sha256 - - # Luban images on aarch64 (the aarch64 design §4): the candidate exports - # tiny as an ISO (UEFI El Torito, hybrid GPT), a drive and a qcow2 -- all - # in-process -- and boots each under qemu-system-aarch64 (virt, edk2), then - # `luban try`. The arm runner has no KVM: every boot is emulated. - luban-images: - name: luban images (aarch64, UEFI) - needs: cross-build-and-run - runs-on: ubuntu-24.04-arm - timeout-minutes: 90 - env: - XDEV_LANE: linux-aarch64-images - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: xlings-linux-aarch64-candidate - path: candidate - - name: Tools from the index (qemu-system, edk2-firmware, bwrap, e2fsprogs, limine) - run: | - bash tools/ci/xlings_tools.sh bwrap qemu-system edk2-firmware e2fsprogs limine - sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true - if [[ -e /dev/kvm ]]; then sudo chmod 666 /dev/kvm; fi - - name: Export and boot - run: | - package_root=$(mktemp -d) - tar -xzf candidate/*.tar.gz -C "$package_root" - bin=$(find "$package_root" -path '*/bin/xlings' -type f | head -1) - XLINGS_BIN="$bin" bash tests/e2e/luban_image_test.sh - - name: Attach the boots - if: always() - uses: actions/upload-artifact@v4 - with: - name: luban-images-aarch64 - path: | - /tmp/xlings-e2e-*/luban_image/*.log - /tmp/xlings-e2e-*/luban_image/*.ppm - if-no-files-found: ignore - - cross-build-and-run: - name: cross-build xlings aarch64 (host x86_64) + qemu run - runs-on: ubuntu-24.04 - timeout-minutes: 70 - steps: - - name: Checkout xlings - uses: actions/checkout@v4 - - - name: The runner's own tools (T0; nothing installed) - run: | - for t in curl git cc make jq tar xz python3; do command -v "$t" >/dev/null || { echo "::error::the runner image has no $t"; exit 1; }; done - - - name: Install bootstrap xlings - env: - XLINGS_NON_INTERACTIVE: 1 - XLINGS_VERSION: ${{ env.BOOTSTRAP_XLINGS_VERSION }} - run: | - # Retried, piped through a file, and asserted -- three fixes for one - # failure mode. `curl … | bash` reports the exit status of BASH, and - # bash fed an empty stdin exits 0, so a network reset installs - # nothing and the step goes GREEN. Measured on the 2026.9.4.1 - # release: `curl: (35) Recv failure: Connection reset by peer` here, - # step reported success, and the run died two steps later with - # `xlings: command not found` (exit 127) pointing at the wrong - # thing entirely. - for attempt in 1 2 3; do - curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 20 \ - https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ - -o /tmp/xlings-quick-install.sh && break - echo "bootstrap download failed (attempt $attempt/3); retrying" - sleep 10 - done - test -s /tmp/xlings-quick-install.sh \ - || { echo "::error::could not download quick_install.sh"; exit 1; } - bash /tmp/xlings-quick-install.sh - # The assertion that makes a silent no-op impossible: whatever the - # installer thought it did, the binary either exists here or it does - # not. - test -x "$HOME/.xlings/bin/xlings" \ - || { echo "::error::bootstrap reported success but installed no xlings"; exit 1; } - echo "XLINGS_HOME=$HOME/.xlings" >> "$GITHUB_ENV" - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: qemu from the ecosystem, not from apt - run: | - # This job cross-builds a release candidate and then RUNS it, which - # needs USER-mode qemu. It used to come from - # `apt-get install qemu-user-static`, and on 2026-08-27 that failed - # on every run: the runner image ships Microsoft apt sources that - # answer 403, so `apt-get update` failed the whole step over a - # repository this job installs nothing from. An ecosystem that - # supplies its own toolchain should not have a test that cannot run - # because a distribution repository is unhappy. - # - # xim:qemu-user-aarch64 is the same binary from upstream - # multiarch/qemu-user-static, statically linked (zero DT_NEEDED), so - # it brings no closure of its own. - xlings install xim:qemu-user-aarch64 -y - qemu-aarch64-static --version | head -1 - - # NO payload cache here. - # - # The reason is sound on its own: caching `~/.xlings/data/xpkgs` (the - # PAYLOADS) without `~/.xlings/.xlings.json` (the xvm registry that records - # what is installed) leaves the runner with payload directories that no - # version is registered for. `xlings install` then treats a present payload - # directory as already installed and skips the install AND its config() - # hook. Either cache both halves or neither; caching both would restore - # registrations pointing at payloads a different `.xlings.json` would have - # changed, so neither is the safe choice. - # - # BUT: dropping this cache did NOT fix the red cell, so it was not the - # cause. Four changes were committed here as fixes -- pinning mcpp, pinning - # the package index, dropping the from-source mcpp build, and dropping this - # cache -- and ALL FOUR left the failure identical: - # - # build.mcpp compiling / build.mcpp running - # error: posix_spawnp('.../xpkg@0.0.54/build.mcpp.bin') failed (error 2) - # - # Each was an inference from outside the runner. None was measured there. - # The `if: failure()` diagnostic at the end of this job exists to end that: - # it prints what the host toolchain actually produced, which is the one - # thing none of the four diagnoses ever looked at. - - name: Bootstrap mcpp + GLOBAL mirror - run: | - xlings config --mirror GLOBAL - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - # This job deliberately takes the LATEST mcpp, not the pin -- it - # exists to catch cross-build breakage against the current client. - xlings install mcpp -y - mcpp --version - - - name: Cross-build release candidate -> aarch64 static, run under qemu - run: | - # No MCPP_BIN: drive the cross-build with the mcpp this job INSTALLED, - # not one built from source here. - # - # This step used to clone mcpp and build it, on the premise that a - # released mcpp could not cross-compile. That premise is false: the - # released client cross-compiles fine. Removing the source build was - # still right, but it did NOT fix the red cell -- see the note above the - # bootstrap step for the four diagnoses that all failed. - # - # Do not "fix" this cell by pinning mcpp. This job deliberately takes the - # LATEST mcpp -- it exists to catch cross-build breakage against the - # current client, and pinning would quietly retire the canary. If a - # future released mcpp cannot cross-compile, this cell going red is the - # intended signal. - # - # Measured locally against released mcpp 2026.8.8.4, both with a warm - # sandbox and with a wiped one (fresh MCPP_HOME, cold build.mcpp - # compile): both succeed and produce - # - # ELF 64-bit LSB executable, ARM aarch64, statically linked - # - # So the failure is environmental to the runner, not in mcpp, xlings, - # the index, or the dependency versions. That is what the diagnostic - # below is for. - MCPP_TARGET=aarch64-linux-musl \ - SKIP_RUN_VERIFY=1 SKIP_NETWORK_VERIFY=1 \ - bash tools/linux_release.sh - archive=$(find build -maxdepth 1 -name 'xlings-*-linux-aarch64.tar.gz' | head -1) - test -f "$archive" - sha256sum "$archive" > "$archive.sha256" - package_root=$(mktemp -d) - tar -xzf "$archive" -C "$package_root" - bin=$(find "$package_root" -path '*/bin/xlings' -type f | head -1) - echo "== file =="; file "$bin" - file "$bin" | grep -q "ARM aarch64" || { echo "NOT aarch64"; exit 1; } - file "$bin" | grep -q "statically linked" || { echo "NOT static"; exit 1; } - diagnostic_dir=build/aarch64-run-diagnostics - mkdir -p "$diagnostic_dir" - expected_version=$(sed -n 's/.*VERSION = "\([^"]*\)".*/\1/p' src/core/config.cppm | head -1) - test -n "$expected_version" - printf 'xlings %s\n' "$expected_version" > "$diagnostic_dir/expected-version.txt" - git rev-parse HEAD > "$diagnostic_dir/candidate-commit.txt" - qemu_shim=$(command -v qemu-aarch64-static) - { - type -a qemu-aarch64-static - printf 'shim=%s\n' "$qemu_shim" - readlink -f "$qemu_shim" - file -L "$qemu_shim" - if grep -aqF 'xlings:multicall-shim:abi=1:caps=handoff:f615c0de' "$qemu_shim"; then - echo 'shim carries multicall marker v1' - else - echo 'shim has no multicall marker v1 (legacy dispatch may apply)' - fi - } > "$diagnostic_dir/qemu-routing.txt" - # Record only this provider's paths and active version, never the - # complete home configuration or environment (which can hold tokens). - python3 - "$diagnostic_dir" <<'PY' - import json, os, pathlib, sys - destination = pathlib.Path(sys.argv[1]) - try: - home = pathlib.Path(os.environ['XLINGS_HOME']) - config = json.loads((home / '.xlings.json').read_text()) - scope = home / 'subos' / 'current' / '.xlings.json' - workspace = json.loads(scope.read_text())['workspace'] - active = workspace['qemu-aarch64-static'] - if isinstance(active, dict): - active = active['active'] - target = config['versions']['qemu-aarch64-static'] - release = target['versions'][active] - filename = release.get('filename') or target.get('filename') or 'qemu-aarch64-static' - provider = pathlib.Path(release['path']) / filename - if release.get('alias'): - raise ValueError('provider has an alias; direct diagnostic is unavailable') - if not provider.is_absolute() or not provider.is_file(): - raise ValueError('registered provider is not an absolute existing file') - (destination / 'qemu-provider.txt').write_text(str(provider) + '\n') - (destination / 'qemu-provider.json').write_text(json.dumps({ - 'version': active, 'path': str(provider), 'scope': str(scope) - }) + '\n') - except Exception as error: - (destination / 'qemu-provider-error.txt').write_text(str(error) + '\n') - PY - echo "== qemu xlings --version ==" - set +e - "$qemu_shim" "$bin" --version > "$diagnostic_dir/stdout.txt" 2> "$diagnostic_dir/stderr.txt" - run_status=$? - set -e - printf '%s\n' "$run_status" > "$diagnostic_dir/status.txt" - cat "$diagnostic_dir/stdout.txt" "$diagnostic_dir/stderr.txt" - if [ "$run_status" -ne 0 ] || ! cmp -s "$diagnostic_dir/expected-version.txt" "$diagnostic_dir/stdout.txt"; then - # Diagnostic reruns never turn a failed admission into success. - # Preserve both shim and direct-provider traces to distinguish - # native dispatch from the emulator and the guest startup path. - set +e - cat "$diagnostic_dir/qemu-routing.txt" - cat "$diagnostic_dir"/qemu-provider*.txt "$diagnostic_dir"/qemu-provider.json 2>/dev/null - timeout 20s "$qemu_shim" --version > "$diagnostic_dir/shim-version.txt" 2>&1 - printf '%s\n' "$?" > "$diagnostic_dir/shim-version-status.txt" - timeout 20s "$qemu_shim" -strace "$bin" --version > "$diagnostic_dir/shim-trace-stdout.txt" 2> "$diagnostic_dir/shim-trace-stderr.txt" - printf '%s\n' "$?" > "$diagnostic_dir/shim-trace-status.txt" - if [ -s "$diagnostic_dir/qemu-provider.txt" ]; then - qemu_provider=$(cat "$diagnostic_dir/qemu-provider.txt") - file "$qemu_provider" - sha256sum "$qemu_provider" "$bin" - timeout 20s "$qemu_provider" --version > "$diagnostic_dir/provider-version.txt" 2>&1 - printf '%s\n' "$?" > "$diagnostic_dir/provider-version-status.txt" - timeout 20s "$qemu_provider" -strace "$bin" --version > "$diagnostic_dir/provider-trace-stdout.txt" 2> "$diagnostic_dir/provider-trace-stderr.txt" - printf '%s\n' "$?" > "$diagnostic_dir/provider-trace-status.txt" - fi - tail -n 100 "$diagnostic_dir"/*trace-stderr.txt - echo "::error::ARM candidate --version failed: status=$run_status, expected xlings $expected_version" - if [ "$run_status" -eq 0 ]; then exit 1; else exit "$run_status"; fi - fi - - # Runs ONLY on failure, as its own step. - # - # A previous diagnostic here never executed: it hung off the build command - # with `||`, and under `bash -e` the failing build aborted the step before - # the diagnostic could print. A whole CI round produced output that could - # not print. Hence `if: failure()` and a separate step. - # - # What it is for: `mcpp` reports the build.mcpp failure as - # - # posix_spawnp('.../xpkg@0.0.54/build.mcpp.bin') failed (error 2) - # - # which reads as "the binary is missing". It usually is not. `execve` - # returns ENOENT when a binary's PT_INTERP -- its dynamic loader -- does - # not exist, and names the BINARY, not the missing loader. The xlings gcc - # rewrites PT_INTERP to an absolute path inside whichever home configured - # its specs, so a loader path that is valid on the packaging machine can - # be absent on a runner. This step prints that path and whether it exists. - - name: Why the host build script could not run (diagnostic) - if: failure() - run: | - # `set +e` is load-bearing. GitHub runs this with `bash -e`, and the - # FIRST version of this diagnostic aborted at `"$bin" --help` (exit - # 127 -- the very failure being diagnosed), so the half that inspects - # the toolchain never printed. That is the same trap that killed the - # diagnostic before it, one level down. - set +e - echo "=== build.mcpp artifacts ===" - find target/.build-mcpp -maxdepth 3 \( -name 'build.mcpp.bin' -o -name '*.cache' \) -printf '%p\t%s bytes\n' 2>/dev/null || echo "(no target/.build-mcpp)" - - bin=$(find target/.build-mcpp -name 'build.mcpp.bin' 2>/dev/null | head -1) - if [ -n "$bin" ]; then - echo "=== PT_INTERP of $bin ===" - interp=$(readelf -p .interp "$bin" 2>/dev/null | sed -n 's/.*\] //p' | head -1) - echo "interp = ${interp:-}" - if [ -n "$interp" ]; then - if [ -e "$interp" ]; then - echo "interp EXISTS -> ENOENT was NOT the loader" - else - echo "interp MISSING -> this is the ENOENT. The loader, not the binary." - fi - fi - echo "=== can it run? ===" - "$bin" --help >/dev/null 2>&1; echo "exit=$?" - else - echo "build.mcpp.bin was never produced -> the host COMPILE failed, not the exec" - fi - - echo "=== host gcc as the sandbox resolved it ===" - mcpp self env || true - for root in "$HOME/.mcpp/registry/data/xpkgs" "$HOME/.xlings/data/xpkgs"; do - echo "--- $root ---" - ls "$root" 2>/dev/null | grep -E 'gcc|glibc' || echo "(none)" - for specs in "$root"/xim-x-gcc/*/lib/gcc/*/*/specs; do - [ -f "$specs" ] || continue - echo "specs: $specs" - grep -aoE '/[^ }%]*ld-linux-x86-64\.so\.2' "$specs" | sort -u | while read -r p; do - [ -e "$p" ] && echo " OK $p" || echo " MISSING $p" - done - done - done - - - name: Upload aarch64 candidate - if: always() - uses: actions/upload-artifact@v4 - with: - name: xlings-linux-aarch64-candidate - path: | - build/xlings-*-linux-aarch64.tar.gz - build/xlings-*-linux-aarch64.tar.gz.sha256 - build/aarch64-run-diagnostics/** diff --git a/.github/workflows/xlings-ci-fresh-install.yml b/.github/workflows/xlings-ci-fresh-install.yml deleted file mode 100644 index 2d7802f58..000000000 --- a/.github/workflows/xlings-ci-fresh-install.yml +++ /dev/null @@ -1,217 +0,0 @@ -name: xlings-ci-fresh-install - -# Fresh-install CI — validates the RELEASED xlings on a clean machine. -# -# Every other workflow here builds xlings from the PR and tests that binary -# against a warm, cached environment. This one tests the path a real first-time -# user takes: land on a bare host, run quick_install, use the tool. That path -# has failure modes the rest of the matrix structurally cannot see — a broken -# release artifact, a quick_install regression, an index that cannot resolve on -# a cold home, a shim that does not resolve, a version switch that silently -# no-ops, or a binary that will not start on an old glibc. -# -# Modelled on mcpp-community/mcpp's ci-fresh-install.yml. -# -# The suites live in tests/fresh-install/ rather than inline here: the CentOS 7 -# leg runs them under `docker run`, where a mounted script beats escaping a long -# heredoc through two shell layers, and it means a failure reproduces locally -# with one command: -# -# HOME=$(mktemp -d) bash tests/fresh-install/smoke.sh gcc - -# Deliberately NOT on `pull_request`. It ran on PRs while it was being -# validated (openxlings/xlings#446, four runs across all four platforms) and -# that trigger is now removed, for two reasons: -# -# 1. It tests the *released* binary, so on a PR it validates the workflow -# rather than the change — near-zero signal per run, at four platforms -# and eleven cold toolchain installs of cost. -# 2. The defects it currently reports live in xim-pkgindex, not here -# (openxlings/xim-pkgindex#442, #443, #444). Gating unrelated PRs on -# another repository's release state would train people to ignore it. -# -# Run it by hand from the Actions tab when you want an answer now. -on: - workflow_dispatch: - inputs: - index_ref: - # Recipe bugs are only ever caught on a real install, so a recipe fix - # cannot be validated until it is already published. This runs the - # whole suite, on every platform, against an unmerged xim-pkgindex - # branch — replacing only pkgs/ in the fetched index. - description: 'xim-pkgindex branch/tag to test against (blank = the published index)' - required: false - default: '' - - # `release: published` is deliberately NOT used. release.yml creates the - # release with GITHUB_TOKEN, and GitHub suppresses workflow triggers from - # GITHUB_TOKEN-generated events, so it would never fire from the pipeline. - # workflow_run is a platform-generated event, exempt from that suppression, - # and needs no cross-repo PAT. - workflow_run: - workflows: [release] - types: [completed] - - # Daily, to catch breakage that arrives from outside the repo: a runner image - # update, an index change, a package host going away. - schedule: - - cron: '0 6 * * *' - -concurrency: - group: xlings-ci-fresh-install-${{ github.ref }} - cancel-in-progress: true - -env: - XLINGS_NON_INTERACTIVE: '1' - # Blank on every trigger except a manual run that named a candidate index. - XIM_PKGINDEX_REF: ${{ inputs.index_ref }} - # Runners reach github.com but not the gitee/gitcode endpoints, which need - # auth — force GLOBAL rather than letting region detection guess. - XLINGS_RELEASE_MIRROR: GLOBAL - GIT_TERMINAL_PROMPT: '0' - -jobs: - # ──────────────────────────────────────────────────────────────────── - # The suite installs whatever `latest` resolves to, on purpose — that is - # the entire reason it exists. A pinned xlings version would turn it into a - # test of a snapshot nobody installs, and it would go stale silently, one - # release at a time, with every cell still green. - # - # The rule is written down in AGENTS.md and the contributing skill; this is - # what makes it a guarantee. It needs no runner image, no network and no - # build, so it is cheap enough to gate the rest. - # ──────────────────────────────────────────────────────────────────── - no-version-pin: - name: fresh-install tracks latest - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - uses: actions/checkout@v4 - - run: bash tests/fresh-install/no_xlings_version_pin_check.sh - - # ──────────────────────────────────────────────────────────────────── - # Linux — the reference leg. All three suites. - # ──────────────────────────────────────────────────────────────────── - linux-fresh: - name: Linux fresh (${{ matrix.suite }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - # Each cell does its OWN fresh install — no shared warm state, which is - # the entire point. They run in parallel, so wall-clock is one - # bootstrap plus one toolchain, not the sum. - suite: [core, gcc, llvm] - steps: - - uses: actions/checkout@v4 - - # Deliberately no actions/cache anywhere in this workflow: a warm cache - # would defeat the thing being tested. - - name: Run ${{ matrix.suite }} suite - run: bash tests/fresh-install/smoke.sh ${{ matrix.suite }} - - # Luban (the console, the window, archives, machines, an upgrade, other - # hosts) has a matrix of its own: xlings-ci-luban-fresh.yml. - - # ──────────────────────────────────────────────────────────────────── - # CentOS 7 — the old-glibc proof (glibc 2.17, EOL). - # - # A `container:` job cannot work here: actions/checkout, actions/cache and - # every other JS action need Node 20, which needs glibc 2.28. So the JS - # actions run on the ubuntu host, the repo is bind-mounted in, and only the - # smoke script executes inside the container. - # - # This leg is load-bearing, not decorative. The xlings Linux binary is - # static-musl so it *should* start on 2.17, and gcc/llvm pull in a - # self-contained xim:glibc@2.39 rather than the host's — but whether that - # whole stack works on a 2.17 host is otherwise unproven. - # ──────────────────────────────────────────────────────────────────── - centos7-fresh: - name: CentOS 7 fresh (${{ matrix.suite }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - suite: [core, gcc, llvm] - steps: - - uses: actions/checkout@v4 - - - name: Run ${{ matrix.suite }} suite in centos:7 - run: | - # If docker.io/centos:7 is ever pulled from Docker Hub, the drop-in - # replacement is quay.io/centos/centos:centos7 (same contents). - docker run --rm \ - -v "$GITHUB_WORKSPACE:/w" -w /w \ - -e HOME=/root \ - -e XLINGS_NON_INTERACTIVE=1 \ - -e XLINGS_RELEASE_MIRROR=GLOBAL \ - -e GIT_TERMINAL_PROMPT=0 \ - -e XIM_PKGINDEX_REF="$XIM_PKGINDEX_REF" \ - centos:7 \ - bash -euo pipefail -c ' - # CentOS 7 is EOL and mirrorlist.centos.org no longer resolves, - # so every yum call 404s until the repos point at the vault. - sed -i -e "s|^mirrorlist=|#mirrorlist=|g" \ - -e "s|^#\?baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g" \ - /etc/yum.repos.d/CentOS-*.repo - - # curl and tar are hard requirements of quick_install.sh; it also - # locates the extracted directory with a glob, so findutils/which - # are installed explicitly rather than assumed present. - yum -y install curl tar gzip xz git which findutils ca-certificates # host-dep-ok: a bare distribution, prepared to install xlings - - # The stock CA bundle predates the roots the GitHub asset CDN - # serves; without this, downloads fail at TLS handshake. - update-ca-trust force-enable - update-ca-trust extract - - echo "host glibc: $(ldd --version | head -1)" - bash /w/tests/fresh-install/smoke.sh ${{ matrix.suite }} - ' - - # ──────────────────────────────────────────────────────────────────── - # Windows. - # - # The gcc cell asserts group *registration*, not group switching: the index - # ships exactly one Windows gcc (15.1.0, via mingw64), so there is nothing to - # switch between. Real group switching is covered on Linux and CentOS 7. - # ──────────────────────────────────────────────────────────────────── - windows-fresh: - name: Windows fresh (${{ matrix.suite }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: windows-latest - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - suite: [core, gcc, llvm] - steps: - - uses: actions/checkout@v4 - - - name: Run ${{ matrix.suite }} suite - shell: pwsh - run: pwsh -File tests/fresh-install/smoke.ps1 -Suite ${{ matrix.suite }} - - # ──────────────────────────────────────────────────────────────────── - # macOS — no gcc cell: the package index ships no macOS gcc. - # ──────────────────────────────────────────────────────────────────── - macos-fresh: - name: macOS fresh (${{ matrix.suite }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - # macos-14 is the support floor — xlings ships minos=14.0 binaries, so a - # fresh install passing here is the continuous proof of that floor. - runs-on: macos-14 - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - suite: [core, llvm] - steps: - - uses: actions/checkout@v4 - - - name: Run ${{ matrix.suite }} suite - run: bash tests/fresh-install/smoke.sh ${{ matrix.suite }} diff --git a/.github/workflows/xlings-ci-host-matrix.yml b/.github/workflows/xlings-ci-host-matrix.yml deleted file mode 100644 index e248ca708..000000000 --- a/.github/workflows/xlings-ci-host-matrix.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: xlings-ci-host-matrix - -# The host matrix (Luban OS design part 2 §5): the same scenarios -# (tests/host-matrix/scenarios) on the hosts users have -- AppArmor's user -# namespace restriction on and off, SELinux, other kernels, other userlands -- -# each run as an ORDINARY user without sudo, where a one-time setup is asked -# for the way a person meets it. Every incident of 2026.10.10 came from a CI -# host cleaner than a user's machine; this is the layer that is not. -# -# K legs (kernel / LSM): the runner images themselves, and other -# distributions' cloud images under KVM (tests/host-matrix/vm.sh). -# U legs (userland): distribution containers, on a host whose restriction is -# lifted -- what they test is the distribution, not the runner's AppArmor. -# -# smoke: the PR's own build on the two legs most likely to break -# (xlings-ci-linux.yml calls it); full: every leg, nightly on the published -# release and on demand; another repository (xim-pkgindex) calls it with its -# index checkout. -on: - workflow_call: - inputs: - legs: { type: string, default: smoke, description: "smoke | full" } - artifact: { type: string, default: "", description: "an artifact of this run holding release.tar.gz (x86_64)" } - version: { type: string, default: "", description: "a published release (default: the latest)" } - index_repository: { type: string, default: "" } - index_ref: { type: string, default: "" } - workflow_dispatch: - inputs: - legs: { type: choice, options: [smoke, full], default: full } - version: { type: string, default: "" } - run: { type: string, default: "", description: "an xlings-ci-linux run whose xlings-linux-x86_64 is the build under test (x86_64 legs)" } - index_repository: { type: string, default: "" } - index_ref: { type: string, default: "" } - schedule: - - cron: "30 6 * * *" - -permissions: - contents: read - -# The index branch a cross-repository change is tested against before it is -# published (a repository variable, unset once the index PR merges): CI's -# tools come from it (tools/ci/xlings_tools.sh). -env: - XLINGS_TOOLS_INDEX_REF: ${{ vars.XLINGS_INDEX_UNDER_TEST || 'main' }} - -jobs: - plan: - runs-on: ubuntu-latest - outputs: - legs: ${{ steps.plan.outputs.legs }} - version: ${{ steps.plan.outputs.version }} - steps: - - id: plan - env: - LEGS: ${{ inputs.legs || 'full' }} - VERSION: ${{ inputs.version }} - ARTIFACT: ${{ inputs.artifact || inputs.run }} - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - smoke='[ - {"leg":"k-ubuntu-2404-restricted","kind":"runner","runner":"ubuntu-24.04","arch":"x86_64"}, - {"leg":"u-fedora","kind":"container","runner":"ubuntu-24.04","arch":"x86_64","image":"fedora:latest","setup":"bash xlings/tests/host-matrix/bare.sh"}]' - full='[ - {"leg":"k-ubuntu-2404-restricted","kind":"runner","runner":"ubuntu-24.04","arch":"x86_64"}, - {"leg":"k-ubuntu-2404-open","kind":"runner","runner":"ubuntu-24.04","arch":"x86_64","sysctl":"0"}, - {"leg":"k-ubuntu-2204","kind":"runner","runner":"ubuntu-22.04","arch":"x86_64"}, - {"leg":"k-ubuntu-2404-arm","kind":"runner","runner":"ubuntu-24.04-arm","arch":"aarch64"}, - {"leg":"k-fedora","kind":"vm","runner":"ubuntu-24.04","arch":"x86_64","distro":"fedora"}, - {"leg":"k-debian-12","kind":"vm","runner":"ubuntu-24.04","arch":"x86_64","distro":"debian-12"}, - {"leg":"k-arch","kind":"vm","runner":"ubuntu-24.04","arch":"x86_64","distro":"arch"}, - {"leg":"u-fedora","kind":"container","runner":"ubuntu-24.04","arch":"x86_64","image":"fedora:latest","setup":"bash xlings/tests/host-matrix/bare.sh"}, - {"leg":"u-arch","kind":"container","runner":"ubuntu-24.04","arch":"x86_64","image":"archlinux:latest","setup":"bash xlings/tests/host-matrix/bare.sh"}, - {"leg":"u-tumbleweed","kind":"container","runner":"ubuntu-24.04","arch":"x86_64","image":"opensuse/tumbleweed:latest","setup":"bash xlings/tests/host-matrix/bare.sh"}, - {"leg":"u-debian-testing","kind":"container","runner":"ubuntu-24.04","arch":"x86_64","image":"debian:testing","setup":"bash xlings/tests/host-matrix/bare.sh"}, - {"leg":"u-debian-12","kind":"container","runner":"ubuntu-24.04","arch":"x86_64","image":"debian:12","setup":"bash xlings/tests/host-matrix/bare.sh"}, - {"leg":"u-ubuntu-2004","kind":"container","runner":"ubuntu-24.04","arch":"x86_64","image":"ubuntu:20.04","setup":"bash xlings/tests/host-matrix/bare.sh"}]' - if [[ "$LEGS" == smoke ]]; then legs="$smoke"; else legs="$full"; fi - # A run's artifact is an x86_64 build: those legs only. - [[ -n "$ARTIFACT" ]] && legs="$(echo "$legs" | jq -c '[.[] | select(.arch == "x86_64")]')" - # The PR's artifact is an x86_64 build: smoke legs only. - echo "legs=$(echo "$legs" | jq -c .)" >> "$GITHUB_OUTPUT" - if [[ -z "$ARTIFACT" ]]; then - v="${VERSION:-$(gh release view -R openxlings/xlings --json tagName --jq .tagName)}" - echo "version=${v#v}" >> "$GITHUB_OUTPUT" - fi - - leg: - needs: plan - name: ${{ matrix.leg }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 75 - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(needs.plan.outputs.legs) }} - steps: - - uses: actions/checkout@v4 - with: - repository: openxlings/xlings - ref: ${{ github.repository == 'openxlings/xlings' && github.sha || 'main' }} - path: xlings - - name: The build under test (this run's artifact) - if: inputs.artifact != '' - uses: actions/download-artifact@v4 - with: - name: ${{ inputs.artifact }} - path: build - - name: The build under test (another run's artifact) - if: inputs.run != '' - uses: actions/download-artifact@v4 - with: - name: xlings-linux-x86_64 - path: build - run-id: ${{ inputs.run }} - repository: openxlings/xlings - github-token: ${{ github.token }} - - name: The build under test (a release) - if: inputs.artifact == '' && inputs.run == '' - env: - GH_TOKEN: ${{ github.token }} - V: ${{ needs.plan.outputs.version }} - run: | - mkdir -p build - gh release download "v$V" -R openxlings/xlings -p "xlings-$V-linux-${{ matrix.arch }}.tar.gz" -O build/release.tar.gz - - name: The index under test - if: inputs.index_repository != '' - uses: actions/checkout@v4 - with: - repository: ${{ inputs.index_repository }} - ref: ${{ inputs.index_ref }} - path: index - - name: Run (${{ matrix.kind }}) - env: - KIND: ${{ matrix.kind }} - SYSCTL: ${{ matrix.sysctl }} - IMAGE: ${{ matrix.image }} - SETUP: ${{ matrix.setup }} - DISTRO: ${{ matrix.distro }} - run: | - set -euo pipefail - tar=build/release.tar.gz - idx=(); [[ -d index ]] && idx=(--index index) - echo "apparmor_restrict_unprivileged_userns=$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null || echo absent)" - case "$KIND" in - runner) - [[ -n "$SYSCTL" ]] && sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns="$SYSCTL" - sudo bash xlings/tests/host-matrix/run.sh --tarball "$tar" --evidence evidence "${idx[@]}" ;; - container) - # The container tests the distribution's userland: the - # runner's AppArmor restriction is the K legs' subject. - sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 || true - docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined \ - --security-opt systempaths=unconfined --cap-add SYS_ADMIN \ - -v "$PWD:/w" -w /w "$IMAGE" bash -c "$SETUP && bash xlings/tests/host-matrix/run.sh --tarball /w/$tar --evidence /w/evidence ${idx[*]:+--index /w/index}" ;; - vm) - # qemu from the index (tools/ci/xlings_tools.sh); the VM's seed is - # served over its network and its disk is a snapshot: no - # cloud-image-utils, no qemu-img. - bash xlings/tools/ci/xlings_tools.sh qemu-system - sudo chmod 666 /dev/kvm - bash xlings/tests/host-matrix/vm.sh "$DISTRO" --tarball "$tar" --evidence evidence "${idx[@]}" ;; - esac - - name: Summary - if: always() - run: | - { echo "### ${{ matrix.leg }}"; cat evidence/summary.md 2>/dev/null || echo "(no summary)"; } >> "$GITHUB_STEP_SUMMARY" - - uses: actions/upload-artifact@v4 - if: always() - with: - name: host-matrix-${{ matrix.leg }} - path: evidence/ - if-no-files-found: ignore diff --git a/.github/workflows/xlings-ci-linux.yml b/.github/workflows/xlings-ci-linux.yml deleted file mode 100644 index d3bcfd478..000000000 --- a/.github/workflows/xlings-ci-linux.yml +++ /dev/null @@ -1,1043 +0,0 @@ -name: xlings-ci-linux - -# Release-train integration branches (release/**) run the same matrix as -# main: PRs land on the integration branch first and only reach main once -# the whole train is green, so CI must gate them there, not at merge time. -on: - push: - branches: [main, master, 'release/**'] - pull_request: - branches: [main, master, 'release/**'] - -# A newer push to the same PR supersedes the run in flight; main and release -# branches keep every run (each one is a commit that landed). -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - # An xim-pkgindex branch whose packages are not published yet (a PR's, - # while both PRs are in flight; the repository variable is unset when the - # index PR merges): what CI installs its tools from (tools/ci/xlings_tools.sh) - # and what the tool table is held to (tools/lint_tools_provenance.sh). - XLINGS_TOOLS_INDEX_REF: ${{ vars.XLINGS_INDEX_UNDER_TEST || 'main' }} - XLINGS_PROVENANCE_INDEX_REF: ${{ vars.XLINGS_INDEX_UNDER_TEST || 'main' }} - GIT_TERMINAL_PROMPT: 0 - # CI runs on github.com; force GLOBAL mirror so xim doesn't try gitee - # endpoints (which need auth and aren't reachable in CI). - XLINGS_RELEASE_MIRROR: GLOBAL - # Pinned bootstrap xlings used to install mcpp. - # - # ⚠️ The "known-good xlings + package index pair" this used to claim is only - # half real: XIM_PKGINDEX_REF is DECLARED in seven workflows and actually - # checked out in three, two of which are the Windows legs. On linux/macos - # the index this job resolves against is whatever is current, not the pin -- - # so treat that variable as documentation, not as a lock, until it is - # either honoured everywhere or removed. - BOOTSTRAP_XLINGS_VERSION: v2026.8.27.5 - # Must contain the mcpp version pinned in .xlings.json (2026.10.5.3). - XIM_PKGINDEX_REF: ebf1fbb3417ec513b923279044433887b3af8850 - -# The shape of a round (tests/README.md, "Parallel runs"): -# -# plan ─┬─ release ─────────────┬─ e2e, isolation-fix, host-matrix, arch-package, root, filesystems, -# │ (the one release │ plan-distro → distro → wsl-import -# │ build) └─ (acceptance takes it when it is ready) -# ├─ build ─┬─ unit (i/n) ... tests/lanes.toml [unit]: n machines, each -# │ │ building and running whole families of tests -# │ └─ acceptance ... checks, then the static gates on the release -# ├─ desktop-payloads ....... the app's and xdesk's release payloads -# └─ build-asan ─ asan (i/n) main and `ci:asan`: [asan], instrumented -# report (every lane's runs, one requirement map) -# -# Built once, reused: `build` makes the development build every lane runs on, -# from the last round's state (only what changed is rebuilt), and the lanes -# take it as it is; a test shard builds only the families it runs. Nothing -# waits for what it does not use: the release build is not behind the tests. -jobs: - # Seconds, no build: the repository's rules, and each lane's matrix from - # tests/lanes.toml. A broken rule stops the round before any machine builds. - plan: - runs-on: ubuntu-24.04 - outputs: - unit: ${{ steps.lanes.outputs.unit }} - asan: ${{ steps.lanes.outputs.asan }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - # Runs HERE, not in xlings-ci-fresh-install.yml, because that workflow - # deliberately has no `pull_request` trigger — a rule enforced only after - # merge is a rule that gets merged. Milliseconds, no build, no network. - - name: fresh-install must track the published latest - run: bash tests/fresh-install/no_xlings_version_pin_check.sh - - # Repository rules, every tools/lint_*.sh: a subos is deleted in one - # place (user data); system headers only in modules/platform; platform - # selection is `if constexpr`; package layers import downward only. - # Milliseconds, no build. A new lint joins by being a tools/lint_*.sh. - - name: repository lints (tools/lint_*.sh) - run: | - rc=0 - for lint in tools/lint_*.sh; do - echo "::group::$lint"; bash "$lint" || rc=1; echo "::endgroup::" - done - exit $rc - - # Shards are weighed by measured durations (main's latest report), taken - # once here so every shard of the round divides the same way. - - name: Measured durations, from main's history - uses: actions/cache/restore@v4 - with: - path: target/xdev/history - key: xdev-history-v1-linux-${{ github.sha }} - restore-keys: xdev-history-v1-linux- - - - name: Attach them for the shards - if: hashFiles('target/xdev/history/timings.json') != '' - uses: actions/upload-artifact@v4 - with: - name: linux-shard-timings - path: target/xdev/history/timings.json - - - name: Lanes (tests/lanes.toml) - id: lanes - run: | - for lane in unit asan; do - echo "$lane=$(python3 tools/ci/lane_matrix.py "$lane")" | tee -a "$GITHUB_OUTPUT" - done - - # The ONE release build of this commit on Linux x86_64, and nothing else: - # every lane that runs the release (e2e, distro, the host matrix, the - # Windows WSL 2 lane, acceptance) starts the moment it is attached. - release: - needs: plan - runs-on: ubuntu-24.04 - outputs: - release_ready: ${{ steps.candidate.outcome == 'success' }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Linux toolchain - uses: ./.github/actions/linux-toolchain - with: - mcpp-cache: mcpp-v4-ci-linux-release- - - - # The candidate and the static xdev the downstream lanes run, from the - # last round's release build when there is one (only what changed is - # rebuilt; a published release is built from nothing, release.yml). - - name: The release candidate - uses: ./.github/actions/linux-build - with: - profile: release - state: produce - attach: 'false' - - - name: Prepare release artifact - run: | - tarball=$(ls build/xlings-*-linux-x86_64.tar.gz 2>/dev/null | head -1) - [[ -z "$tarball" ]] && { echo "No release tarball found"; exit 1; } - cp "$tarball" build/release.tar.gz - - - name: Candidate cold-home lifecycle - id: candidate - run: | - sha256sum build/release.tar.gz > build/release.tar.gz.sha256 - bash tests/candidate-install/smoke.sh \ - build/release.tar.gz build/release.tar.gz.sha256 - - - name: Attach artifact - uses: actions/upload-artifact@v4 - if: success() - with: - name: xlings-linux-x86_64 - path: | - build/release.tar.gz - build/xlings-*-linux-x86_64.tar.gz - - - name: Attach xdev - uses: actions/upload-artifact@v4 - if: success() - with: - name: xdev-linux-build - path: target/xdev/runner/bin/xdev - - - name: Diagnose a failed mcpp build - if: failure() - run: bash tools/ci/diagnose_mcpp.sh - - # The development build every test lane runs on, once per round: from the - # last round's state, only what changed since is rebuilt - # (.github/actions/linux-build); the lanes that need it take it as it is. - build: - needs: plan - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Linux toolchain - uses: ./.github/actions/linux-toolchain - with: - sandbox: 'false' - - - name: The development build - uses: ./.github/actions/linux-build - with: - state: produce - - - name: Diagnose a failed mcpp build - if: failure() - run: bash tools/ci/diagnose_mcpp.sh - - # The Luban OS app's and xdesk's release payloads, as release.yml makes - # them (a glibc program, the system's loader, no RPATH, its NEEDED - # checked): a release must not be the first to find the packaging broken. - # Its own optimized build, beside the others. - desktop-payloads: - needs: plan - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Linux toolchain - uses: ./.github/actions/linux-toolchain - with: - mcpp-cache: mcpp-v4-ci-linux-desktop- - sandbox: 'false' - - - name: Luban OS and xdesk release payloads - run: | - bash tools/ci/xlings_tools.sh patchelf - mcpp build -p luban-os --profile dist - bash tools/luban_os_release.sh - mcpp build -p xdesk-app --profile dist - bash tools/xdesk_release.sh - - - name: Diagnose a failed mcpp build - if: failure() - run: bash tools/ci/diagnose_mcpp.sh - - # The same for the instrumented build, where the asan lane runs. - build-asan: - needs: plan - if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:asan') - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Linux toolchain - uses: ./.github/actions/linux-toolchain - with: - mcpp-cache: mcpp-v4-ci-linux-asan- - sandbox: 'false' - room: 'true' - - - name: The instrumented build - uses: ./.github/actions/linux-build - with: - profile: asan - state: produce - - - name: Diagnose a failed mcpp build - if: failure() - run: bash tools/ci/diagnose_mcpp.sh - - # The C++ tests, over tests/lanes.toml [unit].shards machines. Each takes - # the round's build, builds its share of the test binaries a family per - # mcpp call, and runs them [unit].jobs at a time. - unit: - needs: [plan, build] - name: unit (${{ matrix.shard }}) - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - shard: ${{ fromJSON(needs.plan.outputs.unit) }} - env: - # The capabilities this lane promises (tests/README.md): a test that - # needs one of them and finds it missing FAILS instead of skipping. - XDEV_LANE: linux-unit - XDEV_LANE_CAPS: linux,posix,pty,xlings-bin,userns,bwrap,sandbox,pasta,landlock,network - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Linux toolchain - uses: ./.github/actions/linux-toolchain - with: - save: 'false' - - - - name: The round's development build - uses: ./.github/actions/linux-build - with: - state: consume - - - name: Measured durations, as the plan job took them - continue-on-error: true - uses: actions/download-artifact@v4 - with: - name: linux-shard-timings - path: ${{ runner.temp }}/timings - - - name: Tests (shard ${{ matrix.shard }}) - run: | - shard='${{ matrix.shard }}' - timings=() - [[ -f "$RUNNER_TEMP/timings/timings.json" ]] && timings=(--timings "$RUNNER_TEMP/timings/timings.json") - "$XDEV" test --lane-config unit --shard "$shard" "${timings[@]}" --out "target/xdev/linux-unit-${shard%%/*}" - - - name: Attach the run - uses: actions/upload-artifact@v4 - if: always() - with: - name: xdev-linux-unit-${{ strategy.job-index }} - path: target/xdev/ - if-no-files-found: ignore - - # The Luban OS window as its tests saw it (every view, three sizes, - # English and Chinese, a desktop and the Host key's menu): for people. - - name: Attach the Luban OS screenshots - uses: actions/upload-artifact@v4 - if: always() && hashFiles('target/xtest-artifacts/luban-os/**') != '' - with: - name: luban-os-screenshots - path: target/xtest-artifacts/luban-os/ - - - name: Diagnose a failed mcpp build - if: failure() - run: bash tools/ci/diagnose_mcpp.sh - - # What runs on a development build and needs no release (xdev's own tests, - # the contract scripts, openkal beside this toolchain, the lints as - # evidence), then the static gates on the release build, fetched as soon as - # the release job attaches it. - acceptance: - needs: build - runs-on: ubuntu-24.04 - permissions: - actions: read - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Linux toolchain - uses: ./.github/actions/linux-toolchain - with: - save: 'false' - - - - name: The round's development build - uses: ./.github/actions/linux-build - with: - state: consume - - - name: The static gates' programs - run: | - # Built now, run on the release build once it is attached. - for test in e2e/test_domain_source e2e/test_domain_producer e2e/test_subos_perf e2e/test_home_perf \ - unit/test_root_generation_perf e2e/test_luban; do - mcpp test --no-run "$test" - done - - - name: xdev's own tests - run: | - mkdir -p target/xdev/linux-xdev - printf '{"name":"linux-xdev","platform":"linux","run":"%s:%s","probes":{}}\n' "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" > target/xdev/linux-xdev/lane.json - # XDEV_BIN: the CLI cases drive the xdev just built. - XDEV_BIN="$XDEV" \ - XTEST_META_OUT="$GITHUB_WORKSPACE/target/xdev/linux-xdev/meta.ndjson" \ - XTEST_RESULTS_OUT="$GITHUB_WORKSPACE/target/xdev/linux-xdev/cases.ndjson" \ - mcpp test -p xdev - - - name: Multiplatform contract scripts - env: - XDEV_LANE: linux-contracts - run: | - "$XDEV" test --no-mcpp --suite contract-scripts --out target/xdev/linux-contracts - - # openkal beside this toolchain's runtimes (design part 3 §4.3): glibc, - # and the static musl target the release uses. - - name: openkal coexistence (OPENKAL-COEXIST) - run: | - cd tools/openkal-coexistence - for target in "" "--target x86_64-linux-musl"; do - mcpp build $target - bin="$(find target -path '*/bin/openkal-coexistence' -type f -newer mcpp.toml | xargs ls -t | head -1)" - out="$("$bin")" - echo "$out" - grep -qx 'libstdc++ says: hello' <<<"$out" - grep -qx 'openkal says: hello' <<<"$out" - grep -q '^kal_version=' <<<"$out" - done - - # The same repository lints as the plan job, through xdev this time, - # so what they cover (LAYOUT-DEPS ...) is evidence in the requirement - # report and not only a green step. - - name: Repository lints (requirement evidence) - env: - XDEV_LANE: linux-lint - run: | - "$XDEV" test --no-mcpp --suite lint --out target/xdev/linux-lint - - - name: The release build of this commit - env: - GH_TOKEN: ${{ github.token }} - run: | - bash tools/ci/await_artifact.sh xlings-linux-x86_64 release candidate - tar -xzf candidate/release.tar.gz -C candidate - candidate="$(find "$GITHUB_WORKSPACE/candidate" -path '*/bin/xlings' -type f | head -1)" - test -x "$candidate" - echo "XLINGS_BIN=$candidate" >> "$GITHUB_ENV" - - - name: Static domain lifecycle, checked system source and image export - id: static_domain - continue-on-error: true - env: - XDEV_LANE: linux-static-domain - run: | - status=0 - "$XDEV" test e2e/test_domain_source --no-build --require-metadata \ - --out target/xdev/linux-static-domain || status=$? - "$XDEV" test e2e/test_domain_producer --no-build --require-metadata \ - --out target/xdev/linux-static-domain-producer || status=$? - python3 - <<'PY' - import json - from pathlib import Path - results = [json.loads(line) for directory in ('linux-static-domain', 'linux-static-domain-producer') - for line in Path(f'target/xdev/{directory}/cases.ndjson').read_text().splitlines()] - for case in ('DomainSourceProducer.ReusesOnlyCheckedReadonlyBytesAndExportsAnOwnedImage', - 'RootExport.OwnsBorrowedClosureAndOmitsOtherScopeVersions', - 'DomainProducer.InstallsAtLogicalPrefixInsideANamespaceAndPreservesHostData'): - checked = [row for row in results if row.get('test') == case] - assert len(checked) == 1 and checked[0]['status'] == 'pass', f'{case} must actually pass' - PY - exit "$status" - - - name: Static SubOS performance budgets - id: static_perf - continue-on-error: true - env: - XDEV_LANE: linux-static-perf - run: | - # Timing budgets are asserted here, where each case runs alone -- - # and after what the steps before wrote has reached the disk: a - # build's writeback under a 5 ms budget is the runner measured, not - # xlings (median 5.14 ms once, right after a release payload build). - sync - export XLINGS_PERF_BUDGETS=1 - status=0 - "$XDEV" test e2e/test_subos_perf --no-build --require-metadata \ - --out target/xdev/linux-static-perf || status=$? - "$XDEV" test unit/test_root_generation_perf --no-build --require-metadata \ - --out target/xdev/linux-static-generation-perf || status=$? - # A home of 5,000 registrations and 10 projects: one package's - # use / install / remove / doctor within their budgets. - "$XDEV" test e2e/test_home_perf --no-build --require-metadata \ - --out target/xdev/linux-static-home-perf || status=$? - python3 - <<'PY' - import json - from pathlib import Path - for directory, count in (('linux-static-perf', 4), ('linux-static-generation-perf', 3), - ('linux-static-home-perf', 1)): - results = [json.loads(line) for line in Path(f'target/xdev/{directory}/cases.ndjson').read_text().splitlines()] - assert len(results) == count and all(row['status'] == 'pass' for row in results), results - PY - exit "$status" - - # Luban OS as a first-time user meets it: the quick start makes a real - # system (downloaded, set up), its console opens and runs what is typed; - # an exported system is imported and has what it carried. With the - # release client: a development build cannot enter a root. - - name: Luban OS first use, with the release client - id: first_use - env: - XDEV_LANE: linux-luban-os-first-use - XDEV_LANE_CAPS: linux,posix,pty,xlings-bin,static-xlings,userns,bwrap,sandbox,network - run: | - "$XDEV" test e2e/test_luban --no-build --require-metadata --out target/xdev/linux-luban-os-first-use - - - name: Attach the runs - uses: actions/upload-artifact@v4 - if: always() - with: - name: xdev-linux-acceptance - path: target/xdev/ - if-no-files-found: ignore - - - name: Diagnose a failed mcpp build - if: failure() - run: bash tools/ci/diagnose_mcpp.sh - - - name: Require every static acceptance gate - if: always() - env: - DOMAIN_OUTCOME: ${{ steps.static_domain.outcome }} - PERF_OUTCOME: ${{ steps.static_perf.outcome }} - run: | - for outcome in "$DOMAIN_OUTCOME" "$PERF_OUTCOME"; do - [[ "$outcome" == success ]] || { echo "::error::static acceptance is incomplete or failed"; exit 1; } - done - - # The shell e2e suite, against the release build. - e2e: - needs: release - if: ${{ !cancelled() && needs.release.outputs.release_ready == 'true' }} - runs-on: ubuntu-24.04 - env: - XDEV_LANE: linux-e2e - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: The runner's own tools (T0; nothing installed) - run: | - for t in curl git cc make jq tar xz python3; do command -v "$t" >/dev/null || { echo "::error::the runner image has no $t"; exit 1; }; done - - - name: Fetch the release artifact and xdev - uses: actions/download-artifact@v4 - with: - name: xlings-linux-x86_64 - path: build - - - name: Fetch xdev - uses: actions/download-artifact@v4 - with: - name: xdev-linux-build - path: xdev-artifact - - - name: Prepare fixture index repo - run: | - bash tests/e2e/prepare_fixture_index.sh - - - name: Binary under test - run: | - mkdir -p build/extracted - tar -xzf build/release.tar.gz -C build/extracted - bin="$(find "$GITHUB_WORKSPACE/build/extracted" -path '*/bin/xlings' -type f | head -1)" - [[ -x "$bin" ]] || { echo "::error::no bin/xlings in the release artifact"; exit 1; } - echo "XLINGS_BIN=$bin" >> "$GITHUB_ENV" - xdev="$(find "$GITHUB_WORKSPACE/xdev-artifact" -name xdev -type f | head -1)" - chmod +x "$xdev" - echo "XDEV=$xdev" >> "$GITHUB_ENV" - - - name: "E2E-01: Bootstrap Home (portable + installed)" - run: | - bash tests/e2e/bootstrap_home_test.sh - - - name: "E2E suite (E2E-02..E2E-42, timed)" - run: | - "$XDEV" test --no-mcpp --suite e2e-shell --tarball build/release.tar.gz \ - --out target/xdev/linux-e2e - - - name: Attach the e2e run - uses: actions/upload-artifact@v4 - if: always() - with: - name: xdev-linux-e2e - path: target/xdev/ - if-no-files-found: ignore - - # The root repair for AppArmor-restricted user namespaces, on a runner - # left exactly as the image ships it (#640's comment). It installs - # /usr/lib/xlings/bwrap and an AppArmor profile with sudo, which is why it - # has a runner of its own. - isolation-fix: - needs: release - if: ${{ !cancelled() && needs.release.outputs.release_ready == 'true' }} - runs-on: ubuntu-24.04 - env: - XDEV_LANE: linux-isolation-fix - XDEV_LANE_CAPS: linux,xlings-bin,sudo,network - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Fetch the release artifact - uses: actions/download-artifact@v4 - with: - name: xlings-linux-x86_64 - path: build - - - name: Fetch xdev - uses: actions/download-artifact@v4 - with: - name: xdev-linux-build - path: xdev-artifact - - - name: Binary under test - run: | - mkdir -p build/extracted - tar -xzf build/release.tar.gz -C build/extracted - echo "XLINGS_BIN=$(find "$GITHUB_WORKSPACE/build/extracted" -path '*/bin/xlings' -type f | head -1)" >> "$GITHUB_ENV" - xdev="$(find "$GITHUB_WORKSPACE/xdev-artifact" -name xdev -type f | head -1)" - chmod +x "$xdev" - echo "XDEV=$xdev" >> "$GITHUB_ENV" - - - name: self doctor --isolation --fix - run: | - "$XDEV" test --no-mcpp --suite isolation-fix --require-metadata --out target/xdev/linux-isolation-fix - - - name: Attach the run - uses: actions/upload-artifact@v4 - if: always() - with: - name: xdev-linux-isolation-fix - path: target/xdev/ - if-no-files-found: ignore - - # The host matrix's smoke legs on this build (Luban OS design part 2 §5): - # Ubuntu 24.04 with its user-namespace restriction as shipped, and a - # Fedora userland -- each as an ordinary user without sudo. Every leg: - # nightly and on demand (xlings-ci-host-matrix.yml). - host-matrix: - needs: release - if: ${{ !cancelled() && needs.release.outputs.release_ready == 'true' }} - uses: ./.github/workflows/xlings-ci-host-matrix.yml - with: - # The label `host-matrix-full` runs every x86_64 leg on this build. - legs: ${{ contains(github.event.pull_request.labels.*.name, 'host-matrix-full') && 'full' || 'smoke' }} - artifact: xlings-linux-x86_64 - - # xlings as the root user (design 2026-06-21-root-privilege-identity): the - # release build in a bare ubuntu:24.04 root container with no sudo, so the - # run exercises the root / sudo identity layer itself (P0-1: no sudo - # dependency; P0-2: sudo chown-back). It was a workflow of its own that - # built xlings and ran the unit suite a second time to get here. - root: - needs: release - if: ${{ !cancelled() && needs.release.outputs.release_ready == 'true' }} - runs-on: ubuntu-24.04 - container: - image: ubuntu:24.04 # default user is root (uid 0); no sudo preinstalled - steps: - - name: Checkout - uses: actions/checkout@v4 - - # Minimal deps for the e2e harness — deliberately NOT installing sudo, - # so the pure-root scenario proves xlings never depends on it. - - name: Install minimal deps (no sudo) - run: | - apt-get update -qq - apt-get install -y curl ca-certificates tar findutils perl git # host-dep-ok: a bare container, prepared to install xlings - echo "sudo intentionally NOT installed:"; command -v sudo || echo " (absent — good)" - - - name: Fetch the release artifact - uses: actions/download-artifact@v4 - with: - name: xlings-linux-x86_64 - path: build - - - name: Confirm running as root - run: | - id - [ "$(id -u)" -eq 0 ] || { echo "expected uid 0"; exit 1; } - - - name: "ROOT-01: root usability (pure root + simulated sudo)" - run: | - bash tests/e2e/root_usability_test.sh build/release.tar.gz - - # A home where people put one (design 2026-10-15-filesystem-capabilities): - # on NFSv3, without renameat2's flags (mcpp-community/mcpp#809), what a - # package places is still published and never over what is there; on FAT, - # without symbolic links, the home is refused at its making with what to do. - filesystems: - needs: release - if: ${{ !cancelled() && needs.release.outputs.release_ready == 'true' }} - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Fetch the release artifact - uses: actions/download-artifact@v4 - with: - name: xlings-linux-x86_64 - path: build - - - name: An NFSv3 export of this runner and a FAT image, mounted - run: | - sudo apt-get update -qq - sudo apt-get install -y nfs-kernel-server dosfstools # host-dep-ok: the filesystems under test are the runner's - sudo mkdir -p /srv/nfs /mnt/nfs /mnt/fat - sudo chown "$(id -u):$(id -g)" /srv/nfs - echo "/srv/nfs 127.0.0.1(rw,sync,no_subtree_check,all_squash,anonuid=$(id -u),anongid=$(id -g))" | sudo tee /etc/exports - sudo exportfs -ra - sudo systemctl restart nfs-kernel-server - sudo mount -t nfs -o vers=3,nolock,local_lock=all 127.0.0.1:/srv/nfs /mnt/nfs - truncate -s 64M "$RUNNER_TEMP/fat.img" - mkfs.vfat "$RUNNER_TEMP/fat.img" - sudo mount -o loop,uid="$(id -u)",gid="$(id -g)" "$RUNNER_TEMP/fat.img" /mnt/fat - grep -E " /mnt/(nfs|fat) " /proc/mounts - - - name: "NFSv3: install, place, remove" - run: bash tests/e2e/nfs_home_test.sh build/release.tar.gz /mnt/nfs - - - name: "FAT: refused at its making" - run: bash tests/e2e/fat_home_test.sh build/release.tar.gz /mnt/fat - - # Deployment S as a distribution ships it (design part 2 §5): the AUR - # recipe built from THIS build's release artifact, installed with pacman, - # used by two users. Replaces xlings-ci-archlinux.yml, which ran only when - # config/aur changed and built a 0.4.14 download. - arch-package: - needs: release - if: ${{ !cancelled() && needs.release.outputs.release_ready == 'true' }} - runs-on: ubuntu-24.04 - container: - image: archlinux:latest - env: - XDEV_LANE: linux-arch-package - XDEV_LANE_CAPS: linux,root,network - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Fetch the release artifact - uses: actions/download-artifact@v4 - with: - name: xlings-linux-x86_64 - path: build - - - name: Fetch xdev - uses: actions/download-artifact@v4 - with: - name: xdev-linux-build - path: xdev-artifact - - - name: Package, install, use - run: | - xdev="$(find "$GITHUB_WORKSPACE/xdev-artifact" -name xdev -type f | head -1)" - chmod +x "$xdev" - "$xdev" test --no-mcpp --suite aur --tarball build/release.tar.gz \ - --out target/xdev/linux-arch-package - - - name: Attach the run - uses: actions/upload-artifact@v4 - if: always() - with: - name: xdev-linux-arch-package - path: target/xdev/ - if-no-files-found: ignore - - # The root projection (SubOS design part 2): Luban roots from the published - # editions -- entered in bwrap, exported to docker, booted under qemu with - # their own kernel -- against THIS build's release binary (static: an - # exported image's xlings runs with no host under it). - plan-distro: - needs: release - if: ${{ !cancelled() && needs.release.outputs.release_ready == 'true' }} - runs-on: ubuntu-24.04 - outputs: - matrix: ${{ steps.plan.outputs.matrix }} - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: xdev-linux-build - path: xdev-artifact - - name: Restore measured main-branch shard weights - uses: actions/cache/restore@v4 - with: - path: target/xdev/history - key: xdev-history-v1-linux-${{ github.sha }} - restore-keys: xdev-history-v1-linux- - - name: Plan root scenarios with the local selector - id: plan - run: | - xdev="$(find "$GITHUB_WORKSPACE/xdev-artifact" -name xdev -type f | head -1)" - test -n "$xdev" - chmod +x "$xdev" - weights=() - if [[ -f target/xdev/history/timings.json ]]; then weights+=(--timings target/xdev/history/timings.json); fi - matrix="$("$xdev" ci plan --no-mcpp --suite distro --platform linux --lane main --shards "$(python3 tools/ci/lane_matrix.py --count distro)" --require-metadata "${weights[@]}")" - echo "matrix=$matrix" >> "$GITHUB_OUTPUT" - - distro: - needs: [release, plan-distro] - if: ${{ !cancelled() && needs.release.outputs.release_ready == 'true' && needs.plan-distro.result == 'success' }} - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.plan-distro.outputs.matrix) }} - runs-on: ubuntu-24.04 - env: - XDEV_LANE: linux-distro-${{ strategy.job-index }} - XDEV_LANE_CAPS: linux,xlings-bin,bwrap,userns,network,docker,qemu,sudo - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Fetch the release artifact - uses: actions/download-artifact@v4 - with: - name: xlings-linux-x86_64 - path: build - - - name: Fetch xdev - uses: actions/download-artifact@v4 - with: - name: xdev-linux-build - path: xdev-artifact - - - name: Lane capabilities (bwrap, qemu, kvm) - run: | - # Every tool from the index (tools/ci/xlings_tools.sh): the images - # themselves are written in-process (no xorriso, no qemu-img). - bash tools/ci/xlings_tools.sh bwrap qemu-system e2fsprogs edk2-firmware limine - sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true - # KVM when the runner has it: a boot in seconds instead of a minute. - if [[ -e /dev/kvm ]]; then sudo chmod 666 /dev/kvm; fi - - - name: Binary under test - run: | - mkdir -p build/extracted - tar -xzf build/release.tar.gz -C build/extracted - echo "XLINGS_BIN=$(find "$GITHUB_WORKSPACE/build/extracted" -path '*/bin/xlings' -type f | head -1)" >> "$GITHUB_ENV" - xdev="$(find "$GITHUB_WORKSPACE/xdev-artifact" -name xdev -type f | head -1)" - chmod +x "$xdev" - echo "XDEV=$xdev" >> "$GITHUB_ENV" - - - name: Roots, images, boots - run: | - "$XDEV" test --no-mcpp --suite distro --lane main --shard '${{ matrix.shard }}' \ - --require-metadata --out target/xdev/linux-distro-${{ strategy.job-index }} - - # The luban-tiny tarball the image scenario exported, for wsl --import. - - name: Attach the root tarball - uses: actions/upload-artifact@v4 - if: always() - with: - name: luban-tiny-tar - path: /tmp/xlings-e2e-*/rootfs_image/tiny.tar.gz - if-no-files-found: ignore - - - name: Attach the run - uses: actions/upload-artifact@v4 - if: always() - with: - name: xdev-linux-distro-${{ strategy.job-index }} - path: target/xdev/ - if-no-files-found: ignore - - # EXPORT-WSL (design part 2 §13): the tarball a Linux build exported, - # imported by Windows. GitHub's Windows runners offer WSL1 only, through a - # third-party action; per the design's decision this lane REPORTS and does - # not block (a WSL1 failure says nothing about WSL2): a pass or a skip - # with the reason, in the same report as everything else. - wsl-import: - needs: distro - if: always() - runs-on: windows-2022 - steps: - - name: Fetch the root tarball - uses: actions/download-artifact@v4 - continue-on-error: true - with: - name: luban-tiny-tar - path: root - - - name: WSL - id: setup - continue-on-error: true - uses: Vampire/setup-wsl@v5 - with: - distribution: Alpine - wsl-version: 1 - - - name: wsl --import, run - id: run - continue-on-error: true - shell: pwsh - run: | - $tar = Get-ChildItem -Recurse root -Filter tiny.tar.gz | Select-Object -First 1 - if (-not $tar) { throw "no tarball from the distro job" } - New-Item -ItemType Directory -Force C:\luban | Out-Null - wsl --import luban C:\luban $tar.FullName --version 1 - if ($LASTEXITCODE -ne 0) { throw "wsl --import failed" } - $out = wsl -d luban -- /bin/sh -c '. /etc/os-release; echo "os=$ID/$VARIANT_ID"; xlings --version' - $out - if (-not ($out -match "os=luban/tiny")) { throw "not luban tiny: $out" } - if (-not ($out -match "^xlings ")) { throw "no xlings: $out" } - - - name: Record - if: always() - shell: pwsh - run: | - $dir = "target/xdev/windows-wsl" - New-Item -ItemType Directory -Force $dir | Out-Null - $name = "wsl: subos export --tar imports with wsl --import (WSL1)" - ('{"name":"windows-wsl","platform":"windows","run":"' + $env:GITHUB_RUN_ID + ':' + $env:GITHUB_RUN_ATTEMPT + '","probes":{}}') | Set-Content "$dir/lane.json" - ('{"test":"' + $name + '","area":"scenario","covers":["EXPORT-WSL"],"requires":[],"proves":"flow"}') | Set-Content "$dir/meta.ndjson" - if ("${{ steps.run.outcome }}" -eq "success") { $st = "pass"; $msg = "" } - elseif ("${{ steps.setup.outcome }}" -ne "success") { $st = "skip"; $msg = "WSL1 could not be set up on this runner" } - else { $st = "skip"; $msg = "best effort on WSL1 (design part 2 §17.1): the import or the run failed; see the job log" } - ('{"test":"' + $name + '","status":"' + $st + '","ms":0,"message":"' + $msg + '"}') | Set-Content "$dir/scripts.ndjson" - - - name: Attach the run - if: always() - uses: actions/upload-artifact@v4 - with: - name: xdev-linux-wsl - path: target/xdev/ - - # One report for the Linux lanes, in the step summary. - report: - needs: [release, build, unit, acceptance, asan, e2e, isolation-fix, arch-package, distro, wsl-import] - if: always() - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Fetch the runs - uses: actions/download-artifact@v4 - with: - pattern: xdev-linux-* - path: runs - - - name: Restore main-branch execution history - uses: actions/cache/restore@v4 - with: - path: target/xdev/history - key: xdev-history-v1-linux-${{ github.sha }} - restore-keys: xdev-history-v1-linux- - - - name: Report - run: | - xdev="$(find runs -name xdev -type f | head -1)" - [[ -n "$xdev" ]] || { echo "no xdev binary: the build job failed before building it"; exit 1; } - chmod +x "$xdev" - args=() - for d in $(find runs -name lane.json -exec dirname {} \;); do args+=(--in "$d"); done - # --fail-uncovered: a `required` ID in tests/requirements.toml with - # no test, or a test naming an ID the file does not declare, fails. - history=() - if [[ -f target/xdev/history/trend.json ]]; then history+=(--trend target/xdev/history/trend.json); fi - mkdir -p target/xdev/current-report - status=0 - "$xdev" report "${args[@]}" "${history[@]}" --summary \ - --requirements tests/requirements.toml --fail-uncovered \ - --write target/xdev/current-report --timings-out target/xdev/current-report/timings.json || status=$? - python3 - <<'PY' || status=1 - import json - from pathlib import Path - report = json.loads(Path('target/xdev/current-report/report.json').read_text()) - # Windows quoting has its own mandatory Windows gate; WSL1 is the - # design's explicit best-effort lane and remains visible as such. - elsewhere = {'SUBOS-CMD-WIN-QUOTING', 'EXPORT-WSL'} - missing = [name for name, evidence in report['requirements'].items() - if name not in elsewhere and not evidence['passed_by']] - assert not missing, f'required behavior lacks passing execution evidence: {missing}' - PY - if [[ -f target/xdev/current-report/trend.json && -f target/xdev/current-report/timings.json ]]; then - mkdir -p target/xdev/history - cp target/xdev/current-report/{trend,timings}.json target/xdev/history/ - fi - exit "$status" - - - name: Attach reports and retained trends - if: always() - uses: actions/upload-artifact@v4 - with: - name: xdev-linux-report - path: target/xdev/current-report - retention-days: 90 - if-no-files-found: ignore - - - name: Retain main-branch trends for subsequent plans - if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main' && hashFiles('target/xdev/history/trend.json') != '' - uses: actions/cache/save@v4 - with: - path: target/xdev/history - key: xdev-history-v1-linux-${{ github.sha }} - - # The C++ tests instrumented (mcpp.toml `[profile.asan]`, issue #433: a - # use-after-free reached three releases undetected because nothing ran the - # tests instrumented), over tests/lanes.toml [asan].shards machines: the - # instrumented build is most of the time, and a shard builds only its - # families. Off the PR critical path: every push to main and release/**, - # and a PR labelled `ci:asan`. A HARD gate: a failure on main is a - # regression to revert or fix at once. - # - # Its own mcpp cache key prefix: BMIs compiled with - # `-fsanitize=address,undefined` are not interchangeable with the dev - # profile's (the "CRC mismatch" failure mode), so no restore can hand one - # profile's BMIs to the other. - asan: - needs: [plan, build-asan] - if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:asan') - name: asan (${{ matrix.shard }}) - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - shard: ${{ fromJSON(needs.plan.outputs.asan) }} - env: - XDEV_LANE: linux-asan - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Linux toolchain - uses: ./.github/actions/linux-toolchain - with: - save: 'false' - mcpp-cache: mcpp-v4-ci-linux-asan- - sandbox: 'false' - room: 'true' - - - name: The round's instrumented build - uses: ./.github/actions/linux-build - with: - profile: asan - state: consume - - - name: Measured durations, as the plan job took them - continue-on-error: true - uses: actions/download-artifact@v4 - with: - name: linux-shard-timings - path: ${{ runner.temp }}/timings - - - name: Tests (asan, shard ${{ matrix.shard }}) - run: | - shard='${{ matrix.shard }}' - timings=() - [[ -f "$RUNNER_TEMP/timings/timings.json" ]] && timings=(--timings "$RUNNER_TEMP/timings/timings.json") - "$XDEV" test --lane-config asan --shard "$shard" "${timings[@]}" --out "target/xdev/linux-asan-${shard%%/*}" - - - name: Attach the run - uses: actions/upload-artifact@v4 - if: always() - with: - name: xdev-linux-asan-${{ strategy.job-index }} - path: target/xdev/ - if-no-files-found: ignore - - - name: Diagnose a failed mcpp build - if: failure() - run: bash tools/ci/diagnose_mcpp.sh - - # E2E-00: a fresh mcpp home builds xlings for the musl target. The PR - # build jobs and the release build prove the same build with the same - # mcpp, so it runs where the instrumented build runs: on main, once. - - name: "E2E-00: mcpp builds xlings from source (fresh mcpp home)" - if: github.event_name == 'push' && startsWith(matrix.shard, '1/') - run: | - bash tests/e2e/prepare_fixture_index.sh - bash tests/e2e/mcpp_build_xlings_test.sh - - - name: Sanitizer verdict - if: always() - run: | - if [ "${{ job.status }}" = "success" ]; then - echo "### asan ${{ matrix.shard }}: PASS" >> "$GITHUB_STEP_SUMMARY" - else - echo "### asan ${{ matrix.shard }}: FAIL" >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/xlings-ci-luban-fresh.yml b/.github/workflows/xlings-ci-luban-fresh.yml deleted file mode 100644 index fa3f06d23..000000000 --- a/.github/workflows/xlings-ci-luban-fresh.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: xlings-ci-luban-fresh - -# Luban as a person meets it, from a fresh install of the RELEASED xlings -# (what `latest` is; never a pinned version): every edition made and used on -# the hosts people have, the window, archives, machines, an environment that -# outlives an upgrade, and what macOS and Windows say. The suites are -# tests/fresh-install/luban.sh (bash) and smoke.ps1 (Windows). It tests the -# published experience; the code in the tree is xlings-ci-linux*.yml's. - -on: - workflow_dispatch: - inputs: - index_ref: - description: 'xim-pkgindex branch/tag to test against (blank = the published index)' - required: false - default: '' - # After a release, as xlings-ci-fresh-install does (release.yml's own - # GITHUB_TOKEN cannot trigger workflows; workflow_run can). - workflow_run: - workflows: [release] - types: [completed] - schedule: - - cron: '30 6 * * *' - -concurrency: - group: xlings-ci-luban-fresh-${{ github.ref }} - cancel-in-progress: true - -env: - XLINGS_NON_INTERACTIVE: '1' - XIM_PKGINDEX_REF: ${{ inputs.index_ref }} - XLINGS_RELEASE_MIRROR: GLOBAL - GIT_TERMINAL_PROMPT: '0' - -jobs: - luban: - name: ${{ matrix.suite }} (${{ matrix.os }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ${{ matrix.os }} - timeout-minutes: 120 - strategy: - fail-fast: false - matrix: - include: - # nano, tiny and core made and used: x86_64, aarch64, an older host. - - { suite: luban-console, os: ubuntu-24.04 } - - { suite: luban-console, os: ubuntu-24.04-arm } - - { suite: luban-console, os: ubuntu-22.04 } - # the window: the quick start, the desktop editions (sandbox; KVM machine) - - { suite: luban-window, os: ubuntu-24.04 } - # .luban, a directory, a tar - - { suite: luban-archive, os: ubuntu-24.04 } - - { suite: luban-archive, os: ubuntu-24.04-arm } - # the environment as a machine, and an image booted (KVM) - - { suite: luban-machine, os: ubuntu-24.04 } - # made by the release before, used after `xlings self update` - - { suite: luban-upgrade, os: ubuntu-24.04 } - # a home used for a while (default on an older glibc, its tools, a - # system of the old layout), then the new client - - { suite: luban-lived-in, os: ubuntu-24.04 } - # what a computer that cannot run Luban systems says - - { suite: luban-host, os: macos-14 } - - { suite: luban-host, os: windows-latest } - steps: - - uses: actions/checkout@v4 - # Deliberately no cache: a fresh install is the thing tested. - - name: KVM - if: runner.os == 'Linux' - run: if [ -e /dev/kvm ]; then sudo chmod 666 /dev/kvm; ls -l /dev/kvm; else echo "no /dev/kvm on this runner"; fi - - name: Run ${{ matrix.suite }} - if: runner.os != 'Windows' - env: - LUBAN_SCREENSHOTS: ${{ github.workspace }}/luban-fresh - GH_TOKEN: ${{ github.token }} - run: bash tests/fresh-install/smoke.sh ${{ matrix.suite }} - - name: Run ${{ matrix.suite }} - if: runner.os == 'Windows' - shell: pwsh - run: pwsh -File tests/fresh-install/smoke.ps1 -Suite ${{ matrix.suite }} - - uses: actions/upload-artifact@v4 - if: always() - with: - name: luban-fresh-${{ matrix.suite }}-${{ matrix.os }} - path: luban-fresh/ - if-no-files-found: ignore diff --git a/.github/workflows/xlings-ci-macos.yml b/.github/workflows/xlings-ci-macos.yml deleted file mode 100644 index ea1629582..000000000 --- a/.github/workflows/xlings-ci-macos.yml +++ /dev/null @@ -1,458 +0,0 @@ -name: xlings-ci-macos - -# Release-train integration branches (release/**) run the same matrix as -# main: PRs land on the integration branch first and only reach main once -# the whole train is green, so CI must gate them there, not at merge time. -on: - push: - branches: [main, master, 'release/**'] - pull_request: - branches: [main, master, 'release/**'] - -# A newer push to the same PR supersedes the run in flight; main and release -# branches keep every run (each one is a commit that landed). -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - GIT_TERMINAL_PROMPT: 0 - MACOSX_DEPLOYMENT_TARGET: "14.0" - # CI runs on github.com; force GLOBAL mirror so xim doesn't try gitee - # endpoints (which need auth and aren't reachable in CI). - XLINGS_RELEASE_MIRROR: GLOBAL - # Pinned bootstrap xlings used to install mcpp. - # - # ⚠️ The "known-good xlings + package index pair" this used to claim is only - # half real: XIM_PKGINDEX_REF is DECLARED in seven workflows and actually - # checked out in three, two of which are the Windows legs. On linux/macos - # the index this job resolves against is whatever is current, not the pin -- - # so treat that variable as documentation, not as a lock, until it is - # either honoured everywhere or removed. - BOOTSTRAP_XLINGS_VERSION: v2026.8.27.5 - # Must contain the mcpp version pinned in .xlings.json (2026.10.5.3). - XIM_PKGINDEX_REF: ebf1fbb3417ec513b923279044433887b3af8850 - -jobs: - build-and-test: - runs-on: macos-15 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - # ── Phase 1: Configure Environment via xlings ───────────────── - - - name: Install bootstrap xlings (pinned ${{ env.BOOTSTRAP_XLINGS_VERSION }}) - env: - XLINGS_NON_INTERACTIVE: 1 - XLINGS_VERSION: ${{ env.BOOTSTRAP_XLINGS_VERSION }} - run: | - # Retried, piped through a file, and asserted -- three fixes for one - # failure mode. `curl … | bash` reports the exit status of BASH, and - # bash fed an empty stdin exits 0, so a network reset installs - # nothing and the step goes GREEN. Measured on the 2026.9.4.1 - # release: `curl: (35) Recv failure: Connection reset by peer` here, - # step reported success, and the run died two steps later with - # `xlings: command not found` (exit 127) pointing at the wrong - # thing entirely. - for attempt in 1 2 3; do - curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 20 \ - https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ - -o /tmp/xlings-quick-install.sh && break - echo "bootstrap download failed (attempt $attempt/3); retrying" - sleep 10 - done - test -s /tmp/xlings-quick-install.sh \ - || { echo "::error::could not download quick_install.sh"; exit 1; } - bash /tmp/xlings-quick-install.sh - # The assertion that makes a silent no-op impossible: whatever the - # installer thought it did, the binary either exists here or it does - # not. - test -x "$HOME/.xlings/bin/xlings" \ - || { echo "::error::bootstrap reported success but installed no xlings"; exit 1; } - echo "XLINGS_HOME=$HOME/.xlings" >> "$GITHUB_ENV" - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Cache xlings package payloads - uses: actions/cache@v4 - with: - path: ~/.xlings/data/xpkgs - key: xlings-deps-${{ runner.os }}-${{ hashFiles('.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - xlings-deps-${{ runner.os }}- - - - name: Cache mcpp sandbox and project packages - id: mcpp-cache - uses: actions/cache@v4 - with: - path: | - ~/.mcpp - !~/.mcpp/bmi - !~/.mcpp/build-cache - .mcpp - !.mcpp/bmi - !.mcpp/build-cache - # BMIs are NOT cached, and are deleted after every restore. That - # is the fix, and it took three prefix retirements to find. - # - # A restored BMI set never works. `import 'std' has CRC mismatch` - # follows, which reads like a compiler bug. What hid it is that the - # old guard wiped the BMIs on an INEXACT restore -- so retiring the - # key prefix forced an inexact restore, fired the guard, and produced - # one clean run. Each retirement therefore looked like a fix and was - # an accident; the next run got an exact hit, skipped the guard, and - # failed again. Read straight off two consecutive runs of THIS - # workflow: the one that passed shows "Drop stale BMIs" as `success`, - # the one that failed shows it `skipped`. - # - # So the wipe is unconditional now, and the BMI directories are - # excluded from the cache as well -- the exclusion is an optimisation - # (do not store bytes we delete on arrival), the unconditional wipe is - # the guarantee, and it cannot depend on exclude-pattern semantics. - # - # What the cache is FOR is `registry/data/xpkgs`: ~800 MB of payloads - # that are toolchain-independent and survive a round trip fine. - # - # The key still names the workflow that produced it. mcpp.toml - # declares both the dev toolchain (gcc@16.1.0) and the release target - # (gcc@15.1.0-musl), so a CI build and a release build hash the same - # -- sharing one key across workflows is wrong on its own terms even - # though it was not what caused this. - key: mcpp-v4-ci-macos-${{ runner.os }}-dt110-${{ hashFiles('mcpp.toml', '.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - mcpp-v4-ci-macos-${{ runner.os }}-dt110- - # An inexact restore (the `restore-keys` fallback) brings BMIs built - # under a different dependency set. Mixing them with anything rebuilt - # here fails as `import 'std' has CRC mismatch` — which reads like a - # compiler bug and is a cache restored for the wrong inputs. Measured on - # PR #480, where `mcpplibs.xpkg` went 0.0.47 → 0.0.48: the fallback - # cache's tinyhttps/cmdline BMIs no longer matched the `std` the fresh - # xpkg build produced. - # - # Only the BMIs go. The payload store under registry/data/xpkgs is - # dependency-independent and is what makes the ~800 MB cache worth - # restoring at all. - # Evidence for task #56, gathered where the failure actually happens. - # - # Four hypotheses were refuted on a developer machine (2026-08-06): two - # toolchains sharing one BMI store; the tar round trip destroying - # metadata (the .gcm set survives byte-for-byte, mtimes included); a full - # project rebuild against restored BMIs; and prune_registry running after - # the restore. None reproduces it locally, so the next honest step is to - # record what CI has that a laptop does not, rather than guess a fifth - # hypothesis. - # - # What to look for: two entries that disagree on driver_identity or - # sysroot for the same cpp_standard. That would mean the restored set is - # internally inconsistent, which is the answer. - # - # continue-on-error on purpose: a diagnostic must never be the reason a - # release is blocked. Set XLINGS_KEEP_RESTORED_BMIS=1 on a throwaway - # branch to skip the wipe below and let the failure happen with this - # record in hand. - - name: Record the restored BMI set (diagnostic for #56) - shell: bash - continue-on-error: true - run: | - echo "cache-hit: ${{ steps.mcpp-cache.outputs.cache-hit }}" - for d in ~/.mcpp/bmi .mcpp/bmi ~/.mcpp/build-cache .mcpp/build-cache; do - if [ -d "$d" ]; then - echo " $d: $(du -sh "$d" 2>/dev/null | cut -f1), $(find "$d" -name '*.gcm' 2>/dev/null | wc -l) .gcm" - else - echo " $d: absent" - fi - done - echo " --- build identity per BMI dir (first 40) ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | head -40 | - while read -r f; do - printf ' %-18s %s %s\n' \ - "$(basename "$(dirname "$f")")" \ - "$(grep -o '"cpp_standard": *"[^"]*"' "$f" | cut -d'"' -f4)" \ - "$(grep -o '"driver_identity": *"[^"]*"' "$f" | cut -d'"' -f4)" - done - echo " --- distinct driver identities ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | - xargs -r grep -ho '"driver_identity": *"[^"]*"' | sort -u - - - name: Drop restored BMIs - if: env.XLINGS_KEEP_RESTORED_BMIS != '1' - shell: bash - run: | - rm -rf ~/.mcpp/bmi ~/.mcpp/build-cache .mcpp/bmi .mcpp/build-cache - - - name: Prepare bootstrap package index - run: | - rm -rf "$XLINGS_HOME/data/xim-pkgindex" - bash tests/e2e/prepare_fixture_index.sh "$XLINGS_HOME/data/xim-pkgindex" - - - name: Install build dependencies (mcpp) - run: | - cd "$GITHUB_WORKSPACE" - xlings config --mirror GLOBAL - # Refresh the index before resolving anything out of it. - # - # Without this the run resolves against whatever index snapshot the - # bootstrap shipped with, which is not the one this PR needs: a - # freshly published `mcpplibs.xpkg@0.0.50` reported as "not found" - # while older entries from the same index downloaded fine. The - # failure names the package, so it reads as an unpublished - # dependency rather than as an index nobody refreshed. - xlings update || true - xlings install -y - - # Installing is not switching, and this cache makes the difference - # visible. `xlings install` lays the payload down; the shim keeps - # resolving whatever version was already active -- the client says so - # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp - # restored from a cache that line is the ONLY sign that the mcpp - # about to run is the old one. Measured 2026-08-06: .xlings.json - # pinned 2026.8.6.1 while the run used the cached build, whose index - # snapshot predated the dependency this PR needs -- surfacing as - # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably - # had it. - # The pin comes from .xlings.json at runtime, not a literal -- a - # literal here is a value that changes, and the next mcpp bump would - # leave every workflow switching to the previous one. - pin="$(python3 -c 'import json;print(json.load(open(".xlings.json"))["workspace"]["mcpp"])' 2>/dev/null || true)" - [ -n "$pin" ] && xlings use mcpp "$pin" || true - - mcpp self version - - - name: Reinitialize mcpp registry (GLOBAL) - run: | - # Drop the resolved indexes and mirror config so they are - # re-resolved against GLOBAL, but KEEP registry/data/xpkgs -- the - # downloaded and extracted package payloads. - # - # Those payloads are sha256-pinned by their recipe, so they are - # mirror-independent: keeping them cannot leak a CN artifact into a - # GLOBAL build. Wiping them made every run re-fetch every transitive - # source from its upstream host, so CI failed wholesale whenever one - # of those hosts was unreachable -- e.g. compat.bzip2 (a transitive - # dep of compat.libarchive, and absent from mcpp.lock, which only - # pins direct deps) fetched from sourceware.org. It also made the - # ~800 MB mcpp cache restored one step earlier almost worthless. - prune_registry() { - [ -d "$1" ] || return 0 - find "$1" -mindepth 1 -maxdepth 1 ! -name data ! -name subos -exec rm -rf {} + # the subos links into the payloads, and packages into it - [ -d "$1/data" ] || return 0 - find "$1/data" -mindepth 1 -maxdepth 1 ! -name xpkgs -exec rm -rf {} + - } - prune_registry "$HOME/.mcpp/registry" - prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" - mcpp self config --mirror GLOBAL - - # Force the index refresh, then ASSERT what landed. - # - # The prune above drops the resolved indexes, and mcpp's own - # re-fetch happily uses whatever it finds in the restored cache -- - # so a freshly published dependency reads as "not found" against an - # index that demonstrably has it. Measured 2026-08-06: - # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the - # published artifact, the rolling pointer and releases/latest all - # carried the entry. - # - # `mcpp index update` is the refresh; `mcpp index status` prints the - # revision each index landed on, which is the one datum that makes a - # stale-index failure readable instead of surfacing two layers away - # as `@ not found`. - mcpp index update || true - mcpp index status || true - # PER PLATFORM, not a whole-file grep. - # - # A grep for `["0.0.53"]` passes on a file where the entry exists - # only under `xpm.linux` -- which is exactly the bug this assert was - # written to catch and did not: both bumps edited the linux section - # alone, linux CI went green, and macOS/Windows failed against a - # file that literally contains the string. Slice the section for - # THIS platform first. - - - name: Prepare unit fixture index repo - run: | - bash tests/e2e/prepare_fixture_index.sh ../xim-pkgindex - - # ── Phase 1.5: Unit Tests ──────────────────────────────────── - - - name: Build and run unit tests - env: - XDEV_LANE: macos-unit - XDEV_LANE_CAPS: macos,posix,pty,xlings-bin,sandbox - XDEV_SUMMARY: "1" - run: | - mcpp build - mcpp build -p luban-tool - # The Luban OS app (a window): built on every platform, its headless - # tests (SDL's offscreen driver) find it beside bin/xlings. - mcpp build -p luban-os - mcpp build -p xdev - # A member's binary lands under the root's target/ or the member's - # own, depending on the mcpp version. - xdev="$(find target apps/xdev -name xdev -type f -perm -111 2>/dev/null | head -1)" - [[ -n "$xdev" ]] || { echo "::error::mcpp build -p xdev produced no xdev"; find . -name 'xdev*' -not -path './.git/*' | head; exit 1; } - xdev="$GITHUB_WORKSPACE/$xdev" - export XDEV_BIN="$xdev" - mkdir -p target/xdev/macos-xdev - printf '{"name":"macos-xdev","platform":"macos","run":"%s:%s","probes":{}}\n' "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" > target/xdev/macos-xdev/lane.json - XTEST_META_OUT="$GITHUB_WORKSPACE/target/xdev/macos-xdev/meta.ndjson" \ - XTEST_RESULTS_OUT="$GITHUB_WORKSPACE/target/xdev/macos-xdev/cases.ndjson" \ - mcpp test -p xdev - mcpp test --no-run --message-format json - "$xdev" test --no-build --out target/xdev/macos-unit - - # A separate step, `if: failure()`, and not an `|| { ... }` on one - # command. - # - # The diagnostics used to hang off `mcpp test || { ... }` with - # `mcpp build` running unguarded above it. The shell is `bash -e`, so a - # failing `mcpp build` aborted the step before the block was ever - # reached: the diagnostic existed, never ran, and the failure looked - # exactly the same as if it had. Measured 2026-08-06 -- a whole CI round - # spent adding index-content output that could not print. - - name: Diagnose a failed mcpp build - if: failure() - run: | - # What index does this runner actually hold? `mcpplibs.xpkg@ - # not found` was diagnosed repeatedly from the outside -- index - # commit, published artifact, rolling pointer, releases/latest, - # publish lag, the mcpp version pin, install-vs-use, and the - # lockfile hash were each checked and each was correct. All of that - # is inference about a file only the runner can see. - for idx in "$HOME/.mcpp/registry/data/mcpplibs/pkgs/x/xpkg.lua" \ - "$GITHUB_WORKSPACE/.mcpp/registry/data/mcpplibs/pkgs/x/xpkg.lua"; do - if [ -f "$idx" ]; then - echo "--- $idx" - grep -oE '\["[0-9]+\.[0-9]+\.[0-9]+"\]' "$idx" | head -8 - else - echo "--- $idx : ABSENT" - fi - done - for root in "$HOME/.mcpp/registry" "$GITHUB_WORKSPACE/.mcpp/registry"; do - [ -d "$root" ] || continue - echo "--- $root" - ls -1 "$root" 2>/dev/null | head -8 - ls -1 "$root/data" 2>/dev/null | head -8 - ls -1 "$root/index-snapshots" 2>/dev/null | head -8 - done - mcpp --version 2>&1 | head -2 || true - mcpp self version 2>&1 | head -3 || true - xlings --version 2>&1 | head -2 || true - - - name: "Assert macOS min-version support (minos + static libc++)" - run: | - # Multiple fingerprint dirs can exist (build + test fingerprints - # differ); pick the newest binary, same as the release stage-2 fix. - BIN=$(ls -t $(find target -type f -path '*/bin/xlings') 2>/dev/null | head -1) - test -n "$BIN" || { echo "xlings binary not found"; exit 1; } - echo "all candidates:"; find target -type f -path '*/bin/xlings' | head -5 - echo "selected: $BIN" - echo "=== LC_BUILD_VERSION ===" - otool -l "$BIN" | grep -A4 LC_BUILD_VERSION | head -6 - otool -l "$BIN" | grep -A4 LC_BUILD_VERSION | grep -q "minos ${MACOSX_DEPLOYMENT_TARGET}" \ - || { echo "FAIL: expected minos ${MACOSX_DEPLOYMENT_TARGET}"; exit 1; } - echo "=== dylib deps ===" - otool -L "$BIN" - # mcpp 0.0.50 (static libc++ via -Wl,-load_hidden) is the index - # latest — hard assertion. - if otool -L "$BIN" | grep -q "libc++"; then - echo "FAIL: still linked against system libc++"; exit 1 - fi - echo "OK: static libc++" - - - name: Prepare fixture index repo - run: | - bash tests/e2e/prepare_fixture_index.sh - - # ── Phase 2: Build via Release Script ─────────────────────── - - - name: Build (macos_release) - run: | - chmod +x ./tools/macos_release.sh - SKIP_NETWORK_VERIFY=1 ./tools/macos_release.sh - - - name: Platform core contract - # 2026-09-12 robustness/usability round: bash e2e contracts that - # don't need a whole subos rebuild to be meaningful on macOS. - run: | - bash tests/e2e/notice_once_test.sh - bash tests/e2e/remove_force_contract_test.sh - bash tests/e2e/doctor_cross_subos_fix_test.sh - bash tests/e2e/broken_home_isolation_test.sh - bash tests/e2e/sysroot_refresh_pinning_subos_test.sh - - - name: Fresh XLINGS_HOME first install - run: bash tests/e2e/fresh_xlings_home_install_test.sh - - - name: "E2E-01: Bootstrap Home (portable + installed)" - run: | - bash tests/e2e/bootstrap_home_test.sh - - - name: Prepare release artifact - run: | - tarball=$(ls build/xlings-*-macosx-arm64.tar.gz 2>/dev/null | head -1) - [[ -z "$tarball" ]] && { echo "No release tarball found"; exit 1; } - cp "$tarball" build/release.tar.gz - - - name: Candidate cold-home lifecycle - run: | - shasum -a 256 build/release.tar.gz > build/release.tar.gz.sha256 - bash tests/candidate-install/smoke.sh \ - build/release.tar.gz build/release.tar.gz.sha256 - - - name: "E2E-02: Release Self Install" - run: | - bash tests/e2e/release_self_install_test.sh build/release.tar.gz - - - name: "E2E-02a: Release Packaged Index" - run: | - bash tests/e2e/release_packaged_index_test.sh build/release.tar.gz - - - name: "E2E-03: Quick Install Smoke" - env: - XLINGS_NON_INTERACTIVE: 1 - run: | - bash tests/scripts/test_quick_install.sh - - - name: "E2E-04: Release Subos + d2x Reuse + RPATH" - run: | - bash tests/e2e/release_subos_smoke_test.sh build/release.tar.gz - - - name: "E2E-05: Fixed Project Scenarios" - run: | - bash tests/e2e/project_e2e_test.sh - - - name: "E2E-06: Script-type package install/export/uninstall" - run: | - bash tests/e2e/script_type_install_test.sh - - - name: "E2E-07: Remove preserves other versions (multi-version regression)" - run: | - bash tests/e2e/remove_multi_version_test.sh - - - name: "E2E-08: TUI UTF-8 output regression" - run: | - bash tests/e2e/tui_utf8_test.sh - - - name: "E2E-09: GitHub mirror fallback wiring (HTTP / git / index)" - run: | - bash tests/e2e/mirror_fallback_test.sh - - - name: "E2E-10: remove xim:xlings — single-version guard" - run: | - bash tests/e2e/remove_self_guard_test.sh - - - name: "E2E-11: build/runtime deps split (kind=Build skip-activate + build_dep API)" - run: | - bash tests/e2e/build_deps_split_test.sh - - - name: "E2E-12: shim owner anchoring (owner -> env -> default)" - run: | - bash tests/e2e/shim_owner_anchoring_test.sh - - - name: Attach artifact - uses: actions/upload-artifact@v4 - if: success() - with: - name: xlings-macosx-arm64 - path: build/release.tar.gz diff --git a/.github/workflows/xlings-ci-windows.yml b/.github/workflows/xlings-ci-windows.yml deleted file mode 100644 index 36791311d..000000000 --- a/.github/workflows/xlings-ci-windows.yml +++ /dev/null @@ -1,541 +0,0 @@ -name: xlings-ci-windows - -# Release-train integration branches (release/**) run the same matrix as -# main: PRs land on the integration branch first and only reach main once -# the whole train is green, so CI must gate them there, not at merge time. -on: - push: - branches: [main, master, 'release/**'] - pull_request: - branches: [main, master, 'release/**'] - -# A newer push to the same PR supersedes the run in flight; main and release -# branches keep every run (each one is a commit that landed). -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - GIT_TERMINAL_PROMPT: 0 - # CI runs on github.com; force GLOBAL mirror so xim doesn't try gitee - # endpoints (which need auth and aren't reachable in CI). - XLINGS_RELEASE_MIRROR: GLOBAL - # Pinned bootstrap xlings used to install mcpp. - # - # ⚠️ The "known-good xlings + package index pair" this used to claim is only - # half real: XIM_PKGINDEX_REF is DECLARED in seven workflows and actually - # checked out in three, two of which are the Windows legs. On linux/macos - # the index this job resolves against is whatever is current, not the pin -- - # so treat that variable as documentation, not as a lock, until it is - # either honoured everywhere or removed. - BOOTSTRAP_XLINGS_VERSION: v2026.8.27.5 - # Must contain the mcpp version pinned in .xlings.json (2026.10.5.3). - XIM_PKGINDEX_REF: ebf1fbb3417ec513b923279044433887b3af8850 - -jobs: - build-and-test: - runs-on: windows-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - # ── Phase 1: Configure Environment via xlings ───────────────── - - - name: Install bootstrap xlings (pinned ${{ env.BOOTSTRAP_XLINGS_VERSION }}) - shell: pwsh - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: ${{ env.BOOTSTRAP_XLINGS_VERSION }} - run: | - # Same failure mode as the POSIX bootstrap, same three fixes: `irm | - # iex` reports whether iex ran, not whether irm fetched anything, - # so a reset connection installs nothing and the step goes green. - # `-ErrorAction Stop` on the call, NOT $ErrorActionPreference: the - # preference would stay set for `& $script` below and turn any - # non-terminating error the installer writes into an abort -- a - # behaviour change well outside what this fixes. - $script = "$env:TEMP\xlings-quick-install.ps1" - for ($i = 1; $i -le 3; $i++) { - try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 -ErrorAction Stop ` - -Uri https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 ` - -OutFile $script; break } - catch { Write-Host "bootstrap download failed (attempt $i/3); retrying"; Start-Sleep 10 } - } - if (-not (Test-Path $script) -or (Get-Item $script).Length -eq 0) { - Write-Host "::error::could not download quick_install.ps1"; exit 1 - } - & $script - # Whatever the installer thought it did, the binary is either here or - # it is not. - if (-not (Test-Path "$env:USERPROFILE\.xlings\bin\xlings.exe")) { - Write-Host "::error::bootstrap reported success but installed no xlings"; exit 1 - } - "XLINGS_HOME=$env:USERPROFILE\.xlings" >> $env:GITHUB_ENV - "$env:USERPROFILE\.xlings\subos\current\bin" >> $env:GITHUB_PATH - "$env:USERPROFILE\.xlings\bin" >> $env:GITHUB_PATH - - - name: Cache xlings package payloads - uses: actions/cache@v4 - with: - path: ~\.xlings\data\xpkgs - key: xlings-deps-${{ runner.os }}-${{ hashFiles('.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - xlings-deps-${{ runner.os }}- - - - name: Cache mcpp sandbox and project packages - id: mcpp-cache - uses: actions/cache@v4 - with: - path: | - ~\.mcpp - !~\.mcpp/bmi - !~\.mcpp/build-cache - .mcpp - !.mcpp/bmi - !.mcpp/build-cache - # BMIs are NOT cached, and are deleted after every restore. That - # is the fix, and it took three prefix retirements to find. - # - # A restored BMI set never works. `import 'std' has CRC mismatch` - # follows, which reads like a compiler bug. What hid it is that the - # old guard wiped the BMIs on an INEXACT restore -- so retiring the - # key prefix forced an inexact restore, fired the guard, and produced - # one clean run. Each retirement therefore looked like a fix and was - # an accident; the next run got an exact hit, skipped the guard, and - # failed again. Read straight off two consecutive runs of THIS - # workflow: the one that passed shows "Drop stale BMIs" as `success`, - # the one that failed shows it `skipped`. - # - # So the wipe is unconditional now, and the BMI directories are - # excluded from the cache as well -- the exclusion is an optimisation - # (do not store bytes we delete on arrival), the unconditional wipe is - # the guarantee, and it cannot depend on exclude-pattern semantics. - # - # What the cache is FOR is `registry/data/xpkgs`: ~800 MB of payloads - # that are toolchain-independent and survive a round trip fine. - # - # The key still names the workflow that produced it. mcpp.toml - # declares both the dev toolchain (gcc@16.1.0) and the release target - # (gcc@15.1.0-musl), so a CI build and a release build hash the same - # -- sharing one key across workflows is wrong on its own terms even - # though it was not what caused this. - key: mcpp-v4-ci-windows-${{ runner.os }}-${{ hashFiles('mcpp.toml', '.xlings.json') }}-${{ env.BOOTSTRAP_XLINGS_VERSION }} - restore-keys: | - mcpp-v4-ci-windows-${{ runner.os }}- - # An inexact restore (the `restore-keys` fallback) brings BMIs built - # under a different dependency set. Mixing them with anything rebuilt - # here fails as `import 'std' has CRC mismatch` — which reads like a - # compiler bug and is a cache restored for the wrong inputs. Measured on - # PR #480, where `mcpplibs.xpkg` went 0.0.47 → 0.0.48: the fallback - # cache's tinyhttps/cmdline BMIs no longer matched the `std` the fresh - # xpkg build produced. - # - # Only the BMIs go. The payload store under registry/data/xpkgs is - # dependency-independent and is what makes the ~800 MB cache worth - # restoring at all. - # Evidence for task #56, gathered where the failure actually happens. - # - # Four hypotheses were refuted on a developer machine (2026-08-06): two - # toolchains sharing one BMI store; the tar round trip destroying - # metadata (the .gcm set survives byte-for-byte, mtimes included); a full - # project rebuild against restored BMIs; and prune_registry running after - # the restore. None reproduces it locally, so the next honest step is to - # record what CI has that a laptop does not, rather than guess a fifth - # hypothesis. - # - # What to look for: two entries that disagree on driver_identity or - # sysroot for the same cpp_standard. That would mean the restored set is - # internally inconsistent, which is the answer. - # - # continue-on-error on purpose: a diagnostic must never be the reason a - # release is blocked. Set XLINGS_KEEP_RESTORED_BMIS=1 on a throwaway - # branch to skip the wipe below and let the failure happen with this - # record in hand. - - name: Record the restored BMI set (diagnostic for #56) - shell: bash - continue-on-error: true - run: | - echo "cache-hit: ${{ steps.mcpp-cache.outputs.cache-hit }}" - for d in ~/.mcpp/bmi .mcpp/bmi ~/.mcpp/build-cache .mcpp/build-cache; do - if [ -d "$d" ]; then - echo " $d: $(du -sh "$d" 2>/dev/null | cut -f1), $(find "$d" -name '*.gcm' 2>/dev/null | wc -l) .gcm" - else - echo " $d: absent" - fi - done - echo " --- build identity per BMI dir (first 40) ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | head -40 | - while read -r f; do - printf ' %-18s %s %s\n' \ - "$(basename "$(dirname "$f")")" \ - "$(grep -o '"cpp_standard": *"[^"]*"' "$f" | cut -d'"' -f4)" \ - "$(grep -o '"driver_identity": *"[^"]*"' "$f" | cut -d'"' -f4)" - done - echo " --- distinct driver identities ---" - find ~/.mcpp/bmi .mcpp/bmi -name std-module.json 2>/dev/null | - xargs -r grep -ho '"driver_identity": *"[^"]*"' | sort -u - - - name: Drop restored BMIs - if: env.XLINGS_KEEP_RESTORED_BMIS != '1' - shell: bash - run: | - rm -rf ~/.mcpp/bmi ~/.mcpp/build-cache .mcpp/bmi .mcpp/build-cache - - - name: Prepare bootstrap package index - shell: pwsh - run: | - $indexDir = Join-Path $env:USERPROFILE ".xlings\data\xim-pkgindex" - Remove-Item -Recurse -Force $indexDir -ErrorAction SilentlyContinue - New-Item -ItemType Directory -Force (Split-Path $indexDir) | Out-Null - git clone https://github.com/openxlings/xim-pkgindex.git $indexDir - git -C $indexDir checkout --quiet $env:XIM_PKGINDEX_REF - if (-not (Test-Path (Join-Path $indexDir "pkgs"))) { - throw "bootstrap package index missing pkgs/" - } - - - name: Install build dependencies (mcpp) - shell: pwsh - run: | - Set-Location $env:GITHUB_WORKSPACE - xlings config --mirror GLOBAL - # Refresh the index before resolving out of it. Without this the run - # resolves against the snapshot the bootstrap shipped with, and a - # freshly published dependency reads as "not found" while older - # entries from the same index download fine. - xlings update - $global:LASTEXITCODE = 0 - xlings install -y - - # Installing is not switching. `xlings install` lays the payload - # down; the shim keeps resolving whatever version was already - # active, and with ~/.mcpp restored from a cache that is the old - # build -- whose index snapshot can predate the dependency this PR - # needs. Measured 2026-08-06 as `mcpplibs.xpkg@0.0.53 not found` - # against an index that demonstrably had it. - # The pin comes from .xlings.json at runtime, not a literal. - $pin = (Get-Content .xlings.json | ConvertFrom-Json).workspace.mcpp - if ($pin) { xlings use mcpp "$pin" } - $global:LASTEXITCODE = 0 - - mcpp --version - $global:LASTEXITCODE = 0 - mcpp self version - - - name: Reinitialize mcpp registry (GLOBAL) - shell: pwsh - run: | - # See xlings-ci-linux.yml for the rationale: keep registry/data/xpkgs - # (sha256-pinned payloads) so a single unreachable upstream source - # host cannot fail the whole run, and so the mcpp cache restored one - # step earlier is actually worth something. - function Prune-Registry($root) { - if (-not (Test-Path $root)) { return } - Get-ChildItem -Force $root | Where-Object { $_.Name -ne 'data' } | - Remove-Item -Recurse -Force -ErrorAction SilentlyContinue - $data = Join-Path $root 'data' - if (Test-Path $data) { - Get-ChildItem -Force $data | Where-Object { $_.Name -ne 'xpkgs' } | - Remove-Item -Recurse -Force -ErrorAction SilentlyContinue - } - } - Prune-Registry "$env:USERPROFILE\.mcpp\registry" - Prune-Registry "$env:GITHUB_WORKSPACE\.mcpp\registry" - mcpp self config --mirror GLOBAL - - # Force the index refresh, then assert what landed -- see the note - # in xlings-ci-linux.yml. Calling update without checking leaves - # "refreshed" and "still stale" producing identical output. - mcpp index update - $global:LASTEXITCODE = 0 - mcpp index status - $global:LASTEXITCODE = 0 - - - name: Prepare unit fixture index repo - shell: bash - run: | - bash tests/e2e/prepare_fixture_index.sh ../xim-pkgindex - - # ── Phase 1.5: Unit Tests ──────────────────────────────────── - - # `continue-on-error`, with the verdict re-asserted at the end of the - # job. - # - # This step both BUILDS and tests, and a failure here skipped the 15 - # steps after it -- every E2E, on the one platform whose defects the E2E - # suite exists to catch. Observed on main: a timing-sensitive unit test - # flaked (the same commit was green on its PR branch) and the Windows - # leg produced no end-to-end signal at all for a Windows-only fix. - # - # A red job with full signal beats a red job with none. The `Unit test - # verdict` step below is what keeps it red. - - name: Build and run unit tests - id: unit - continue-on-error: true - shell: pwsh - env: - XDEV_LANE: windows-unit - XDEV_LANE_CAPS: windows,xlings-bin,sandbox - XDEV_SUMMARY: "1" - run: | - mcpp build - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - mcpp build -p luban-tool - # The Luban OS app (a window): built on every platform, its headless - # tests (SDL's offscreen driver) find it beside bin/xlings. - mcpp build -p luban-os - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - mcpp build -p xdev - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - # A member's binary lands under the root's target\ or the member's - # own, depending on the mcpp version. - $xdev = Get-ChildItem target, apps\xdev -Recurse -Filter xdev.exe -ErrorAction SilentlyContinue | Select-Object -First 1 - if (-not $xdev) { Write-Host "::error::mcpp build -p xdev produced no xdev.exe under target/"; exit 1 } - $env:XDEV_BIN = $xdev.FullName - New-Item -ItemType Directory -Force target/xdev/windows-xdev | Out-Null - ('{"name":"windows-xdev","platform":"windows","run":"' + $env:GITHUB_RUN_ID + ':' + $env:GITHUB_RUN_ATTEMPT + '","probes":{}}') | Set-Content target/xdev/windows-xdev/lane.json - $env:XTEST_META_OUT = "$env:GITHUB_WORKSPACE/target/xdev/windows-xdev/meta.ndjson" - $env:XTEST_RESULTS_OUT = "$env:GITHUB_WORKSPACE/target/xdev/windows-xdev/cases.ndjson" - mcpp test -p xdev - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Remove-Item Env:XTEST_META_OUT, Env:XTEST_RESULTS_OUT - mcpp test --no-run --message-format json - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & $xdev.FullName test --no-build --out target/xdev/windows-unit - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - $quoting = @(Get-Content target/xdev/windows-unit/cases.ndjson | ForEach-Object { $_ | ConvertFrom-Json } | - Where-Object test -eq 'SubosCmdContract.ExitCodeHomeAndCmdQuotingOnWindows') - if ($quoting.Count -ne 1 -or $quoting[0].status -ne 'pass') { - throw 'SUBOS-CMD-WIN-QUOTING must have a passing Windows execution' - } - - # If the BUILD failed there is nothing to run E2E against, and every - # step below would fail for a reason that has nothing to do with it. - # Distinguished from "the unit tests failed", which the E2E suite is - # perfectly able to run past. - - name: Stop if the build itself failed - if: steps.unit.outcome == 'failure' - shell: pwsh - run: | - if (-not (Test-Path "target")) { - Write-Host "::error::build produced no target/ -- skipping E2E" - exit 1 - } - Write-Host "unit tests failed but the build succeeded; E2E continues" - - - name: Prepare fixture index repo - shell: bash - run: | - bash tests/e2e/prepare_fixture_index.sh - - # ── Phase 2: Build via Release Script ─────────────────────── - - - name: Build (windows_release) - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tools\windows_release.ps1" - - - name: "Regression: directory outside the ANSI code page (mcpp#693)" - shell: pwsh - run: '& "$env:GITHUB_WORKSPACE\tests\e2e\ansi_code_page_directory_test.ps1"' - - - name: Fresh XLINGS_HOME first install - shell: pwsh - run: '& "$env:GITHUB_WORKSPACE\tests\e2e\fresh_xlings_home_install_test.ps1"' - - - name: "E2E-01: Bootstrap Home (portable + installed)" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\bootstrap_home_test.ps1" - - - name: Prepare release artifact - shell: pwsh - run: | - $zip = Get-ChildItem "build\xlings-*-windows-x86_64.zip" | Select-Object -First 1 - if (-not $zip) { throw "No release zip found" } - Copy-Item $zip.FullName "build\release.zip" - - - name: Candidate cold-home lifecycle - shell: pwsh - run: | - $hash = (Get-FileHash -Algorithm SHA256 "build\release.zip").Hash.ToLowerInvariant() - "$hash release.zip" | Set-Content "build\release.zip.sha256" - & "$env:GITHUB_WORKSPACE\tests\candidate-install\smoke.ps1" -Archive "build\release.zip" -Sidecar "build\release.zip.sha256" - - # ── Phase 3: E2E Tests ───────────────────────────────────── - - # A shim runs the entry binary's code after every upgrade (#615): on - # Windows shims are hard links, and replacing the entry detached them. - # Uses the pinned bootstrap xlings as the pre-fix "legacy" build. - - name: "E2E-120w: Shim identity across upgrades" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\windows_shim_identity_test.ps1" -Archive "build\release.zip" -LegacyBinary "$env:USERPROFILE\.xlings\bin\xlings.exe" - - - name: "E2E-02: Release Self Install" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\release_self_install_test.ps1" "build\release.zip" - - - name: "E2E-02a: Release Packaged Index" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\release_packaged_index_test.ps1" "build\release.zip" - - - name: "E2E-03: Quick Install Smoke" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\scripts\test_quick_install.ps1" - - - name: "E2E-04: Release Subos + d2x" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\release_subos_smoke_test.ps1" "build\release.zip" - - - name: "E2E-08: SubOS on the wsl2 carrier (or refused with its route)" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\windows_carrier_wsl2_test.ps1" "build\release.zip" - - - name: "E2E-05: Project Shim Mirror" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\project_shim_mirror_test.ps1" - - - name: "E2E-06: Shim Project Context" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\shim_project_context_test.ps1" - - - name: "E2E-07: Sub-Index Search + pkgindex-build" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\sub_index_search_test.ps1" "build\release.zip" - - - name: Attach artifact - uses: actions/upload-artifact@v4 - if: success() - with: - name: xlings-windows-x86_64 - path: build/release.zip - - # The Luban OS app built above, for the WSL 2 lane's window tests. - - name: Attach the Luban OS app - if: success() - shell: pwsh - run: | - $app = Get-ChildItem target -Recurse -Filter luban-os.exe -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -match '[\\/]bin[\\/]luban-os[\\/]luban-os\.exe$' } | Select-Object -First 1 - if (-not $app) { Write-Host "::error::no luban-os.exe under target/"; exit 1 } - New-Item -ItemType Directory -Force build/app | Out-Null - Copy-Item $app.FullName build/app/luban-os.exe - - uses: actions/upload-artifact@v4 - if: success() - with: - name: luban-os-windows-ci - path: build/app/luban-os.exe - - # The unit step is `continue-on-error` so it cannot skip the E2E suite; - # this is where its result becomes the job's. Without it a unit failure - # would report success, which is the opposite defect and a worse one. - - name: Unit test verdict - if: always() && steps.unit.outcome == 'failure' - shell: pwsh - run: | - Write-Host "::error::unit tests failed (see the 'Build and run unit tests' step)" - exit 1 - - # Luban OS on Windows, for real (part 4 §11.3): a Luban system in the Luban - # host machine on the runner's WSL 2. The hosted runner has nested - # virtualization, which WSL 2 needs, but GitHub does not promise it: a - # failure to set WSL 2 up is the environment's and is reported as such - # (its own step), apart from a failure of the code under test. - luban-wsl2: - needs: build-and-test - runs-on: windows-2025 - # A green lane takes about 15 minutes. A hung `wsl --import` on the runner - # was not ended by its step's own bound (it held the job for 40 minutes): - # the job's bound is what ends it. - timeout-minutes: 40 - permissions: - actions: read - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Fetch the release under test - uses: actions/download-artifact@v4 - with: - name: xlings-windows-x86_64 - path: build - - - name: Fetch the Luban OS app - uses: actions/download-artifact@v4 - with: - name: luban-os-windows-ci - path: build/app - - # The host machine's xlings and luban are the Linux build of the same - # commit (xlings-ci-linux.yml's release artifact), as a Windows home - # gets the Linux build of its own release. - - name: The Linux build of this commit, for the Luban host machine - shell: bash - env: - GH_TOKEN: ${{ github.token }} - SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: | - set -u - for i in $(seq 1 80); do - id=$(gh run list -R "$GITHUB_REPOSITORY" -w xlings-ci-linux.yml -c "$SHA" --json databaseId -q '.[0].databaseId' 2>/dev/null || true) - if [ -n "$id" ] && gh run download "$id" -R "$GITHUB_REPOSITORY" -n xlings-linux-x86_64 -D guest 2>/dev/null; then break; fi - sleep 60 - done - [ -f guest/release.tar.gz ] || { echo "::error::no Linux release build of $SHA appeared in xlings-ci-linux.yml"; exit 1; } - # Only its two programs: a Windows file system takes no Linux links. - mkdir -p guest/x && tar -xzf guest/release.tar.gz -C guest/x --wildcards '*/bin/xlings' '*/bin/luban' - bin=$(find "$PWD/guest/x" -path '*/bin/xlings' -type f | head -1) - [ -n "$bin" ] && [ -f "$(dirname "$bin")/luban" ] || { echo "::error::the Linux build has no bin/xlings and bin/luban"; exit 1; } - echo "XLINGS_CARRIER_GUEST_XLINGS=$(cygpath -w "$bin")" >> "$GITHUB_ENV" - - # The runner's WSL is the environment, not the code under test: its - # import has hung on a hosted runner (minutes on `wsl --import`, nothing - # after). Bounded, tried twice, and said to be the environment's when - # neither attempt sets it up. - - name: WSL 2 (the runner's environment) - id: wsl - continue-on-error: true - timeout-minutes: 8 - uses: Vampire/setup-wsl@v7 - with: - distribution: Alpine-3.22 - wsl-version: 2 - - - name: WSL 2 (the runner's environment, again) - id: wsl_again - if: steps.wsl.outcome != 'success' - continue-on-error: true - timeout-minutes: 8 - uses: Vampire/setup-wsl@v7 - with: - distribution: Alpine-3.22 - wsl-version: 2 - - - name: The runner's WSL 2 is there - if: steps.wsl.outcome != 'success' && steps.wsl_again.outcome != 'success' - shell: pwsh - run: | - Write-Host "::error::the runner's WSL 2 could not be set up twice (the environment, not xlings): see the two setup steps" - exit 1 - - - name: What the runner's WSL is - shell: pwsh - run: | - $env:WSL_UTF8 = '1' - wsl.exe --status - wsl.exe --version - - - name: "Luban OS on WSL 2: status, a system, a command, a machine as reported" - shell: pwsh - run: | - & "$env:GITHUB_WORKSPACE\tests\e2e\windows_luban_wsl2_test.ps1" "build\release.zip" "build\app\luban-os.exe" diff --git a/tests/host-matrix/bare.sh b/tests/host-matrix/bare.sh index 2563d0471..ad889e341 100755 --- a/tests/host-matrix/bare.sh +++ b/tests/host-matrix/bare.sh @@ -10,7 +10,9 @@ if command -v apt-get >/dev/null; then as_root env DEBIAN_FRONTEND=noninteractive apt-get -qq update >/dev/null as_root env DEBIAN_FRONTEND=noninteractive apt-get -qq install -y curl ca-certificates sudo util-linux passwd tar gzip procps >/dev/null # host-dep-ok: a bare distribution, prepared to install xlings elif command -v dnf >/dev/null; then - as_root dnf -y -q install curl sudo util-linux shadow-utils tar gzip findutils procps-ng >/dev/null # host-dep-ok: a bare distribution, prepared to install xlings + as_root dnf -y -q install --allowerasing curl sudo util-linux shadow-utils tar gzip findutils procps-ng >/dev/null # host-dep-ok: a bare distribution, prepared to install xlings +elif command -v apk >/dev/null; then + as_root apk add -q bash curl ca-certificates sudo util-linux shadow tar gzip findutils procps >/dev/null # host-dep-ok: a bare distribution, prepared to install xlings elif command -v pacman >/dev/null; then as_root pacman -Sy --noconfirm --needed curl sudo util-linux tar gzip findutils procps-ng >/dev/null # host-dep-ok: a bare distribution, prepared to install xlings elif command -v zypper >/dev/null; then diff --git a/tests/host-matrix/vm.sh b/tests/host-matrix/vm.sh index 821173b10..42a372a77 100644 --- a/tests/host-matrix/vm.sh +++ b/tests/host-matrix/vm.sh @@ -55,6 +55,11 @@ case "$distro" in image=Arch-Linux-x86_64-cloudimg.qcow2 sums="$base/$image.SHA256"; sumtool=sha256sum prepare='sudo bash hm-in/suite/bare.sh' ;; + rocky-9) + base=https://dl.rockylinux.org/pub/rocky/9/images/x86_64 + image=Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 + sums="$base/$image.CHECKSUM"; sumtool=sha256sum + prepare='sudo bash hm-in/suite/bare.sh' ;; *) echo "unknown distro: $distro" >&2; exit 2 ;; esac From fafe2b64a64225da6f34df138cae94a6b9a738a4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 16 Oct 2026 04:20:16 +0900 Subject: [PATCH 2/6] probe: round 2 -- O1 with the real vim payload, O3 on a mount of its own, synthetic bind cost, O6 a machine; O4 legs measured (disabled) --- .github/probe/inside.sh | 64 ++++++++++++++++++------------------- .github/workflows/probe.yml | 2 ++ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/probe/inside.sh b/.github/probe/inside.sh index 3361e14ec..aa6feaaf9 100755 --- a/.github/probe/inside.sh +++ b/.github/probe/inside.sh @@ -4,9 +4,9 @@ # Ubuntu runner. Prints observations; never asserts. set -uo pipefail XV="${XV:-2026.10.15.4}" +HERE="$(cd "$(dirname "$0")" && pwd)" T="$RUNNER_TEMP/p"; mkdir -p "$T/logs"; cd "$T" S="${GITHUB_STEP_SUMMARY:-/dev/null}" -HERE="$(cd "$(dirname "$0")" && pwd)" say() { echo "$*"; echo "$*" >> "$S"; } block() { local title="$1"; shift; local out rc @@ -48,6 +48,8 @@ rows = [l.rstrip("\n").split("\t") for l in open(os.path.join(g, "links.tsv")) i kinds = collections.Counter(); dirs = [] for rel, target in rows[:]: t = target if os.path.isabs(target) else os.path.join(g, os.path.dirname(rel), target) + if t.startswith("/xlings/") and not os.path.exists(t): + t = os.path.expanduser("~/.xlings/domains/xlings/private") + t[len("/xlings"):] if os.path.isdir(t): kinds["dir"] += 1; dirs.append((rel, target)) elif os.path.exists(t): @@ -67,30 +69,27 @@ done say "### O2: bwrap start-up cost per bind" BW="$(command -v bwrap || ls "$HOME"/.xlings/data/xpkgs/xim-x-bwrap/*/bin/bwrap 2>/dev/null | head -1)" say "bwrap: $BW ($("$BW" --version 2>&1))" -DIRS="$(cat "$RUNNER_TEMP"/dirs-pdesk.txt "$RUNNER_TEMP"/dirs-pcore.txt 2>/dev/null | sort -u)" -block "bwrap time vs number of --ro-bind" python3 - "$BW" < len(pool): break +for n in [0, 10, 50, 100, 200, 500]: args = list(base) - for i, d in enumerate(pool[:n]): - args += ["--dir", f"/mnt/b{i}", "--ro-bind", d, f"/mnt/b{i}"] + for i in range(n): + args += ["--dir", f"/mnt/b{i}", "--ro-bind", f"{bench}/d{i}", f"/mnt/b{i}"] args += ["--", "/bin/true"] ts = [] for _ in range(15): t0 = time.perf_counter(); r = subprocess.run(args, capture_output=True); ts.append((time.perf_counter() - t0) * 1000) if r.returncode: print(" n=%d failed: %s" % (n, r.stderr.decode()[:200])); break - print(f" binds={n:5d} median={statistics.median(ts):7.1f} ms max={max(ts):7.1f} ms") + print(f" binds={n:4d} median={statistics.median(ts):7.1f} ms max={max(ts):7.1f} ms") PY say "### O1: vim and terminfo" G="$(gen_of pcore)" -VIM="$(readlink -f "$G/usr/bin/vim" 2>/dev/null)" +block "what /usr/bin/vim is in the generation" bash -c "ls -la '$G/usr/bin/vim'; readlink -f '$G/usr/bin/vim'" +VIM="$(ls -d "$HOME"/.xlings/data/xpkgs/xim-x-vim/*/bin/vim "$HOME"/.xlings/domains/xlings/private/data/xpkgs/xim-x-vim/*/bin/vim 2>/dev/null | head -1)" block "vim payload" bash -c "echo '$VIM'; file '$VIM'; '$VIM' --version | head -3; '$VIM' --version | grep -iE 'terminfo|tgetent|tlib' ; strings '$VIM' | grep -E '^/[a-z/]*terminfo' | sort -u | head" block "terminfo dirs present on the runner" bash -c "ls -d /etc/terminfo /lib/terminfo /usr/share/terminfo /usr/lib/terminfo 2>&1" printf 'hello\n' > "$T/t.txt" @@ -111,34 +110,33 @@ cat > "$T/o3.sh" <<'SH' set -u T="$1" echo "caps: $(grep CapEff /proc/self/status)" -cat /usr/bin/which-gen -# 1. stack: bind the new generation directly over /usr -if mount --bind "$T/genB/usr" /usr 2>&1; then echo "stack bind: ok -> $(cat /usr/bin/which-gen)"; else echo "stack bind: failed"; fi -umount /usr 2>/dev/null && echo "unmount top: ok -> $(cat /usr/bin/which-gen)" -# 2. stage + move +echo "mounted: $(cat /mnt/usr/bin/which-gen)" +if mount --bind "$T/genB/usr" /mnt/usr 2>&1; then echo "stack bind: ok -> $(cat /mnt/usr/bin/which-gen)"; umount /mnt/usr 2>&1 && echo "unmount top: ok -> $(cat /mnt/usr/bin/which-gen)"; else echo "stack bind: failed"; fi mkdir -p /mnt/stage -if mount --bind "$T/genB/usr" /mnt/stage 2>&1 && mount --move /mnt/stage /usr 2>&1; then echo "stage+move: ok -> $(cat /usr/bin/which-gen)"; else echo "stage+move: failed"; fi -# 3. read-only remount of the new one -mount -o remount,bind,ro /usr 2>&1 && echo "remount ro: ok" || echo "remount ro: failed" -# 4. lazy detach of what is underneath, then a reader keeps its file -exec 9&1 && echo "lazy umount: ok; held fd still reads: $(cat <&9); now /usr has: $(cat /usr/bin/which-gen 2>&1)" -# 5. drop the capability for a child (what session-init would do before the shell) +if mount --bind "$T/genB/usr" /mnt/stage 2>&1 && mount --move /mnt/stage /mnt/usr 2>&1; then echo "stage+move: ok -> $(cat /mnt/usr/bin/which-gen)"; else echo "stage+move: failed"; fi +mount -o remount,bind,ro /mnt/usr 2>&1 && echo "remount ro: ok" || echo "remount ro: failed" +exec 9&1 && echo "lazy umount top: ok; held fd reads: $(cat <&9); /mnt/usr now: $(cat /mnt/usr/bin/which-gen 2>&1)" python3 - <<'PY' import ctypes, os libc = ctypes.CDLL(None, use_errno=True) -PR_CAPBSET_DROP = 24 -for cap in range(0, 41): - libc.prctl(PR_CAPBSET_DROP, cap, 0, 0, 0) +for cap in range(0, 41): libc.prctl(24, cap, 0, 0, 0) +libc.prctl(47, 4, 0, 0, 0) # PR_CAP_AMBIENT, CLEAR_ALL pid = os.fork() if pid == 0: - os.execvp("sh", ["sh", "-c", "grep -E 'CapEff|CapBnd' /proc/self/status; mount --bind /tmp /mnt 2>&1 || echo 'child mount: refused (expected)'"]) + os.execvp("sh", ["sh", "-c", "grep -E 'CapEff|CapBnd' /proc/self/status; mount --bind /tmp /mnt/stage 2>&1 || echo 'child mount: refused'"]) os.waitpid(pid, 0) PY SH block "O3 without --cap-add (as xlings starts bwrap today)" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --ro-bind / / --dev /dev --proc /proc \ - --tmpfs /mnt --bind "$T" "$T" --ro-bind "$T/genA/usr" /usr -- sh "$T/o3.sh" "$T" + --tmpfs /mnt --bind "$T" "$T" --dir /mnt/usr --ro-bind "$T/genA/usr" /mnt/usr -- sh "$T/o3.sh" "$T" block "O3 with --cap-add CAP_SYS_ADMIN" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_SYS_ADMIN --ro-bind / / --dev /dev --proc /proc \ - --tmpfs /mnt --bind "$T" "$T" --ro-bind "$T/genA/usr" /usr -- sh "$T/o3.sh" "$T" -block "O3 /usr that came from the parent namespace (locked), with CAP_SYS_ADMIN" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_SYS_ADMIN \ - --bind / / --dev /dev --proc /proc --tmpfs /mnt -- sh -c "mount --bind $T/genB/usr /usr && echo 'stack over host /usr: ok' ; umount -l /usr && echo 'umount stacked: ok'; mkdir -p /mnt/s && mount --bind $T/genB/usr /mnt/s && mount --move /mnt/s /usr && echo 'move onto host /usr: ok' || echo 'move onto host /usr: failed'" + --tmpfs /mnt --bind "$T" "$T" --dir /mnt/usr --ro-bind "$T/genA/usr" /mnt/usr -- sh "$T/o3.sh" "$T" +block "O3 a mount from the parent namespace (locked: /opt), with CAP_SYS_ADMIN" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_SYS_ADMIN \ + --bind / / --dev /dev --proc /proc --tmpfs /mnt -- /bin/sh -c "mkdir -p /mnt/s; mount --bind $T/genB/usr /opt && echo 'stack over a locked mount: ok' && /usr/bin/umount /opt && echo 'umount the stacked one: ok'; /usr/bin/umount -l / 2>&1 | head -1; mount --bind $T/genB/usr /mnt/s && mount --move /mnt/s /opt && echo 'move onto a locked mount: ok' || echo 'move onto a locked mount: failed'" + +say "### O6: a machine, and what /usr/bin/vim resolves to there" +sudo chmod 666 /dev/kvm 2>/dev/null || true +block "luban config pcore mode machine" timeout 300 "$HOME/.xlings/bin/luban" config pcore mode machine +LINES_=60 block "luban run pcore (machine) -- vim --version; active subos" timeout 900 env XLINGS_NON_INTERACTIVE=1 "$HOME/.xlings/bin/luban" run pcore -y -- /bin/sh -c 'vim --version | head -2; echo "XLINGS_ACTIVE_SUBOS=$XLINGS_ACTIVE_SUBOS"; xlings subos list 2>&1 | head -5; id; xlings install -y xim:jq 2>&1 | tail -2' + diff --git a/.github/workflows/probe.yml b/.github/workflows/probe.yml index 5d03a73f8..141820e0a 100644 --- a/.github/workflows/probe.yml +++ b/.github/workflows/probe.yml @@ -30,6 +30,7 @@ jobs: # O4: distributions as user spaces (containers), as the host matrix runs # them, with the scenarios that make and enter a system. container: + if: false name: O4 ${{ matrix.image }} (${{ matrix.runner }}) runs-on: ${{ matrix.runner }} timeout-minutes: 90 @@ -80,6 +81,7 @@ jobs: # O4: Rocky 9 as a kernel and SELinux host (KVM). vm-rocky: + if: false name: O4 rocky-9 (KVM) runs-on: ubuntu-24.04 timeout-minutes: 90 From 212555a04de2be54383d72a1bf1571acc4834074 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 16 Oct 2026 04:29:00 +0900 Subject: [PATCH 3/6] probe: round 3 -- dropping capabilities after session-init, a machine's active subos and a regular user's install, a desktop's /usr/share --- .github/probe/inside3.sh | 77 +++++++++++++++++++++++++++++++++++++ .github/workflows/probe.yml | 4 +- 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100755 .github/probe/inside3.sh diff --git a/.github/probe/inside3.sh b/.github/probe/inside3.sh new file mode 100755 index 000000000..1ba2ca661 --- /dev/null +++ b/.github/probe/inside3.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Round 3: O3b (dropping capabilities for the user's processes after +# session-init keeps CAP_SYS_ADMIN), O6 (a machine: what /usr/bin/vim +# resolves to, which subos is "this one", a regular user's install), O7 (a +# desktop system's /usr/share: launcher entries, cursor theme, terminfo). +# Prints observations; never asserts. +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +XV="${XV:-2026.10.15.4}" +T="$RUNNER_TEMP/p"; mkdir -p "$T/logs"; cd "$T" +S="${GITHUB_STEP_SUMMARY:-/dev/null}" +say() { echo "$*"; echo "$*" >> "$S"; } +block() { + local title="$1"; shift; local out rc + out="$("$@" 2>&1)"; rc=$? + echo "== $title (exit $rc)"; echo "$out" + { echo; echo "**$title** (exit $rc)"; echo '```'; echo "$out" | tail -n "${LINES_:-40}"; echo '```'; } >> "$S" +} +sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 +curl -fsSL -o xl.tgz "https://github.com/openxlings/xlings/releases/download/v$XV/xlings-$XV-linux-x86_64.tar.gz" +mkdir -p inst && tar -xzf xl.tgz -C inst +(cd inst/*/ && ./bin/xlings self install logs/install.log 2>&1 +export PATH="$HOME/.xlings/bin:$PATH" +LB="$HOME/.xlings/bin/luban" +BW="$(ls "$HOME"/.xlings/data/xpkgs/xim-x-bwrap/*/bin/bwrap 2>/dev/null | head -1)" +[[ -x "$BW" ]] || { xlings install -y xim:bwrap >/dev/null 2>&1; BW="$(ls "$HOME"/.xlings/data/xpkgs/xim-x-bwrap/*/bin/bwrap | head -1)"; } + +say "### O3b: session-init keeps CAP_SYS_ADMIN; the user's processes must not" +cat > "$T/drop.py" <<'PY' +import ctypes, os +libc = ctypes.CDLL(None, use_errno=True) +def status(tag): + caps = [l.strip() for l in open("/proc/self/status") if l.startswith(("CapEff", "CapPrm", "CapBnd", "CapAmb"))] + print(tag, " ".join(caps)) +status("session-init:") +ok = os.system("mount --bind /tmp /mnt/a 2>/dev/null && umount /mnt/a") == 0 +print("session-init can mount:", ok) +dropped = all(libc.prctl(24, c, 0, 0, 0) == 0 for c in range(0, 41)) # PR_CAPBSET_DROP +print("bounding set dropped:", dropped, "errno", ctypes.get_errno()) +libc.prctl(47, 4, 0, 0, 0) # PR_CAP_AMBIENT_CLEAR_ALL +SECBIT_NOROOT, SECBIT_NOROOT_LOCKED = 1 << 0, 1 << 1 +print("securebits NOROOT:", libc.prctl(28, SECBIT_NOROOT | SECBIT_NOROOT_LOCKED, 0, 0, 0) == 0, "errno", ctypes.get_errno()) # PR_SET_SECUREBITS +pid = os.fork() +if pid == 0: + os.execvp("sh", ["sh", "-c", "grep -E 'CapEff|CapPrm|CapBnd' /proc/self/status | tr '\\n' ' '; echo; mount --bind /tmp /mnt/a 2>&1 && echo 'child mount: ALLOWED' || echo 'child mount: refused'"]) +os.waitpid(pid, 0) +PY +block "O3b with CAP_SYS_ADMIN only" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_SYS_ADMIN \ + --ro-bind / / --dev /dev --proc /proc --tmpfs /mnt --dir /mnt/a --bind "$T" "$T" -- python3 "$T/drop.py" +block "O3b with CAP_SYS_ADMIN and CAP_SETPCAP" "$BW" --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_SYS_ADMIN --cap-add CAP_SETPCAP \ + --ro-bind / / --dev /dev --proc /proc --tmpfs /mnt --dir /mnt/a --bind "$T" "$T" -- python3 "$T/drop.py" + +say "### making core and desktop" +LINES_=8 block "luban new pcore --edition core -y" timeout 2400 env XLINGS_NON_INTERACTIVE=1 "$LB" new pcore --edition core -y +LINES_=8 block "luban new pdesk --edition desktop -y" timeout 2400 env XLINGS_NON_INTERACTIVE=1 "$LB" new pdesk --edition desktop -y + +say "### O7: a desktop system's /usr/share" +LINES_=80 block "pdesk (sandbox): /usr/share, XDG_DATA_DIRS, launcher entries, cursor themes, terminfo" timeout 300 "$LB" run pdesk -- /bin/sh -c ' +echo "XDG_DATA_DIRS=$XDG_DATA_DIRS"; echo "--- /usr/share:"; ls /usr/share +echo "--- applications dirs:"; for d in $(echo "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" | tr ":" " ") "$HOME/.local/share"; do ls -d "$d/applications" 2>/dev/null && ls "$d/applications" | head; done +echo "--- .desktop files anywhere in /xlings payloads:"; find /xlings/data/xpkgs -name "*.desktop" 2>/dev/null | head -20 +echo "--- cursor themes:"; ls -d /usr/share/icons/*/cursors ~/.icons/*/cursors 2>/dev/null; find /xlings/data/xpkgs -type d -name cursors 2>/dev/null | head +echo "--- terminfo:"; ls -d /usr/share/terminfo /etc/terminfo 2>/dev/null; find /xlings/data/xpkgs -maxdepth 5 -type d -name terminfo 2>/dev/null | head +echo "--- foot:"; command -v foot; ls -la "$(command -v foot)" 2>/dev/null' +block "pdesk session files (what starts, its environment)" bash -c "G=\$(readlink -f \$(find / -path '*/subos/pdesk/root' 2>/dev/null | head -1)); echo \$G; cat \$(dirname \$(dirname \$G))/rootfs/etc/luban/* 2>/dev/null | head -20; find \$(dirname \$(dirname \$G))/rootfs/etc -maxdepth 3 -name '*session*' -o -maxdepth 3 -name '*xdesk*' 2>/dev/null | head; ls \$G/usr/share/luban 2>/dev/null; cat \$G/usr/share/luban/settings.json 2>/dev/null | head -30" + +say "### O6: a machine" +sudo chmod 666 /dev/kvm 2>/dev/null || true +block "luban config pcore --mode machine" timeout 300 "$LB" config pcore --mode machine +LINES_=60 block "machine: vim, the active subos, a regular user's install" timeout 1500 env XLINGS_NON_INTERACTIVE=1 "$LB" run -y pcore -- /bin/sh -c ' +echo "kernel: $(uname -r)"; echo "XLINGS_ACTIVE_SUBOS=$XLINGS_ACTIVE_SUBOS"; ls -la /usr/bin/vim +vim --version 2>&1 | head -3 +xlings subos list 2>&1 | head -6 +id +xlings install -y xim:jq 2>&1 | tail -3' +LINES_=60 block "machine as its login user: vim and install" timeout 900 "$LB" run -y pcore --user root -- /bin/sh -c 'adduser -D probeuser 2>/dev/null || useradd -m probeuser; su probeuser -c "id; vim --version 2>&1 | head -2; xlings install -y xim:jq 2>&1 | tail -3"' +"$LB" stop pcore >/dev/null 2>&1 || true diff --git a/.github/workflows/probe.yml b/.github/workflows/probe.yml index 141820e0a..d98d7fd2d 100644 --- a/.github/workflows/probe.yml +++ b/.github/workflows/probe.yml @@ -14,13 +14,13 @@ env: jobs: inside: - name: O1 O2 O3 (ubuntu-24.04, a core and a desktop system) + name: O3b O6 O7 (ubuntu-24.04, core and desktop; a machine) runs-on: ubuntu-24.04 timeout-minutes: 120 steps: - uses: actions/checkout@v4 - name: probe - run: bash .github/probe/inside.sh + run: bash .github/probe/inside3.sh - uses: actions/upload-artifact@v4 if: always() with: From 6d8ce8bdd516b1b81569dd48ec492d21d8aad818 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 16 Oct 2026 05:03:43 +0900 Subject: [PATCH 4/6] probe: round 4 -- console font at display scales, trays, cursor themes, virtiofs, a machine's current subos --- .github/probe/font.sh | 75 ++++++++++++++ .github/probe/misc.sh | 62 ++++++++++++ .github/probe/probe-round1-3.yml.txt | 113 +++++++++++++++++++++ .github/workflows/probe.yml | 145 +++++++++++++-------------- 4 files changed, 318 insertions(+), 77 deletions(-) create mode 100755 .github/probe/font.sh create mode 100755 .github/probe/misc.sh create mode 100644 .github/probe/probe-round1-3.yml.txt diff --git a/.github/probe/font.sh b/.github/probe/font.sh new file mode 100755 index 000000000..a30e1e01e --- /dev/null +++ b/.github/probe/font.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# A6: the console's glyphs and cells at display scales 1, 1.5 and 2 (X11 on +# Xvfb, SDL_VIDEO_X11_SCALING_FACTOR), and with a proportional font standing +# in for the monospace one. Prints observations; never asserts. +set -uo pipefail +XV="${XV:-2026.10.15.4}" +T="$RUNNER_TEMP/p"; mkdir -p "$T/shots"; cd "$T" +S="${GITHUB_STEP_SUMMARY:-/dev/null}" +say() { echo "$*"; echo "$*" >> "$S"; } +sudo apt-get -qq update >/dev/null; sudo apt-get -qq install -y xvfb fonts-dejavu-core fonts-noto-cjk imagemagick >/dev/null +curl -fsSL -o xl.tgz "https://github.com/openxlings/xlings/releases/download/v$XV/xlings-$XV-linux-x86_64.tar.gz" +mkdir -p inst && tar -xzf xl.tgz -C inst && (cd inst/*/ && ./bin/xlings self install install.log 2>&1 +export PATH="$HOME/.xlings/bin:$PATH" +xlings install -y xim:luban-os > luban-os.log 2>&1 +APP="$(ls "$HOME"/.xlings/data/xpkgs/xim-x-luban-os/*/bin/luban-os 2>/dev/null | head -1)" +[[ -x "$APP" ]] || APP="$(command -v luban-os)" +say "luban-os: $APP ($("$APP" --version 2>&1))" +Xvfb :99 -screen 0 2600x1700x24 >/dev/null 2>&1 & sleep 2 +export DISPLAY=:99 + +script() { # + cat </dev/null) cols" +key Return +see 20s quick brown +frames 5 +screenshot $1 +quit +EOF +} +run() { # + local name="$1"; shift + script "$T/shots/$name.bmp" > "$T/$name.script" + env "$@" SDL_VIDEODRIVER=x11 timeout 120 "$APP" --script "$T/$name.script" --console-exec /bin/bash --norc --noprofile -i > "$T/$name.log" 2>&1 + say "$name: exit $? $(tail -1 "$T/$name.log")" + convert "$T/shots/$name.bmp" "$T/shots/$name.png" 2>/dev/null +} +run scale-1 SDL_VIDEO_X11_SCALING_FACTOR=1 PS1='$ ' +run scale-1.5 SDL_VIDEO_X11_SCALING_FACTOR=1.5 PS1='$ ' +run scale-2 SDL_VIDEO_X11_SCALING_FACTOR=2 PS1='$ ' +run proportional-1 SDL_VIDEO_X11_SCALING_FACTOR=1 PS1='$ ' LUBAN_OS_MONO_FONT=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf + +# The interface at scale 2 too: the quick start. +printf 'wait quick-start\nframes 5\nscreenshot %s\nquit\n' "$T/shots/quick-start-scale-2.bmp" > "$T/qs.script" +SDL_VIDEO_X11_SCALING_FACTOR=2 SDL_VIDEODRIVER=x11 timeout 60 "$APP" --script "$T/qs.script" --view quick-start > "$T/qs.log" 2>&1 +convert "$T/shots/quick-start-scale-2.bmp" "$T/shots/quick-start-scale-2.png" 2>/dev/null + +# Cell geometry, measured: the columns where ink starts on the first text row. +python3 - "$T/shots" <<'PY' | tee -a "$GITHUB_STEP_SUMMARY" +import glob, os, sys, subprocess +for png in sorted(glob.glob(os.path.join(sys.argv[1], "*.png"))): + out = subprocess.run(["convert", png, "-colorspace", "gray", "-threshold", "50%", "txt:-"], capture_output=True, text=True).stdout + ink = {} + for line in out.splitlines()[1:]: + try: + xy, rest = line.split(":", 1) + x, y = map(int, xy.split(",")) + except ValueError: + continue + if "#FFFFFF" in rest or "white" in rest: + ink.setdefault(y, set()).add(x) + rows = sorted(y for y, xs in ink.items() if len(xs) > 20) + # text rows: runs of consecutive y with ink + runs, start, prev = [], None, None + for y in rows: + if start is None: start = prev = y + elif y == prev + 1: prev = y + else: runs.append((start, prev)); start = prev = y + if start is not None: runs.append((start, prev)) + heights = [b - a + 1 for a, b in runs[:6]] + pitch = [runs[i + 1][0] - runs[i][0] for i in range(min(5, len(runs) - 1))] + print(f"- {os.path.basename(png)}: ink heights {heights} · row pitch {pitch}") +PY diff --git a/.github/probe/misc.sh b/.github/probe/misc.sh new file mode 100755 index 000000000..33e5d2d58 --- /dev/null +++ b/.github/probe/misc.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# O9 cursor themes, O10 virtiofs, O11 a core system as a machine (vim and +# the current subos). Prints observations; never asserts. +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +XV="${XV:-2026.10.15.4}" +T="$RUNNER_TEMP/p"; mkdir -p "$T/logs"; cd "$T" +S="${GITHUB_STEP_SUMMARY:-/dev/null}" +say() { echo "$*"; echo "$*" >> "$S"; } +block() { + local title="$1"; shift; local out rc + out="$("$@" 2>&1)"; rc=$? + echo "== $title (exit $rc)"; echo "$out" + { echo; echo "**$title** (exit $rc)"; echo '```'; echo "$out" | tail -n "${LINES_:-40}"; echo '```'; } >> "$S" +} +sudo apt-get -qq update >/dev/null + +say "### O9: cursor themes" +sudo apt-get -qq install -y adwaita-icon-theme breeze-cursor-theme >/dev/null 2>&1 +block "sizes, files, nominal sizes, licences" python3 - <<'PY' +import os, struct, glob +def nominal(path): + try: + b = open(path, "rb").read() + if b[:4] != b"Xcur": return [] + n = struct.unpack_from("/dev/null 2>&1 +VFS="$(command -v virtiofsd || ls /usr/libexec/virtiofsd /usr/lib/qemu/virtiofsd 2>/dev/null | head -1)" +mkdir -p "$T/share" +block "virtiofsd unprivileged (--sandbox none)" bash -c "'$VFS' --version 2>&1 | head -1; ('$VFS' --socket-path='$T/vfs.sock' --shared-dir='$T/share' --sandbox none & echo \$! > '$T/vfs.pid'); sleep 2; ls -la '$T/vfs.sock' && echo 'listening as $(id -un)'; kill \$(cat '$T/vfs.pid') 2>/dev/null" +block "virtiofsd unprivileged (--sandbox namespace)" bash -c "('$VFS' --socket-path='$T/vfs2.sock' --shared-dir='$T/share' --sandbox namespace & echo \$! > '$T/vfs2.pid'); sleep 2; ls -la '$T/vfs2.sock' && echo listening; kill \$(cat '$T/vfs2.pid') 2>/dev/null" +curl -fsSL -o xl.tgz "https://github.com/openxlings/xlings/releases/download/v$XV/xlings-$XV-linux-x86_64.tar.gz" +mkdir -p inst && tar -xzf xl.tgz -C inst && (cd inst/*/ && ./bin/xlings self install logs/install.log 2>&1 +export PATH="$HOME/.xlings/bin:$PATH" +xlings install -y xim:linux-kernel-virt > logs/kernel.log 2>&1 +K="$(ls -d "$HOME"/.xlings/data/xpkgs/xim-x-linux-kernel-virt/* 2>/dev/null | head -1)" +block "linux-kernel-virt: virtio-fs and FUSE in its config" bash -c "ls '$K' '$K'/boot 2>/dev/null | head; cfg=\$(ls '$K'/boot/config* '$K'/config* 2>/dev/null | head -1); if [ -n \"\$cfg\" ]; then grep -E 'CONFIG_(VIRTIO_FS|FUSE_FS|DAX)=' \"\$cfg\"; else img=\$(ls '$K'/boot/vmlinuz* | head -1); echo \"no config file; searching \$img\"; strings \"\$img\" | grep -m3 -E 'virtiofs|virtio_fs'; fi" + +say "### O11: a core system as a machine" +sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 +sudo chmod 666 /dev/kvm 2>/dev/null || true +LB="$HOME/.xlings/bin/luban" +LINES_=6 block "luban new pcore --edition core -y" timeout 2400 env XLINGS_NON_INTERACTIVE=1 "$LB" new pcore --edition core -y +block "luban config pcore --mode machine" "$LB" config pcore --mode machine +LINES_=80 block "luban enter pcore (machine): vim, current subos" python3 "$HERE/pty_run.py" --out "$T/logs/machine.bin" --wait 600 --tail 10 \ + --step '(Proceed|\[Y/n\]|y/N)|y\r' \ + --step '[#$] $|uname -r; echo ACTIVE=$XLINGS_ACTIVE_SUBOS; ls -la /usr/bin/vim; vim --version | head -2; xlings subos list | head -5; cat /etc/xlings/root.json 2>&1; echo PROBE-DONE\r' \ + --step 'PROBE-DONE|poweroff\r' \ + -- env TERM=xterm-256color "$LB" enter pcore diff --git a/.github/probe/probe-round1-3.yml.txt b/.github/probe/probe-round1-3.yml.txt new file mode 100644 index 000000000..d98d7fd2d --- /dev/null +++ b/.github/probe/probe-round1-3.yml.txt @@ -0,0 +1,113 @@ +# [temp][do not merge] Probe for +# .agents/docs/2026-10-16-luban-inside-experience-and-ecosystem-design.md +# (O1 vim and terminfo, O2 generation directory entries and bind cost, +# O3 mount --move in a user namespace, O4 more host distributions). +# Every script prints what it observes and never asserts. +name: probe + +on: + pull_request: + workflow_dispatch: + +env: + XV: 2026.10.15.4 + +jobs: + inside: + name: O3b O6 O7 (ubuntu-24.04, core and desktop; a machine) + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - name: probe + run: bash .github/probe/inside3.sh + - uses: actions/upload-artifact@v4 + if: always() + with: + name: inside + path: ${{ runner.temp }}/p/logs + + # O4: distributions as user spaces (containers), as the host matrix runs + # them, with the scenarios that make and enter a system. + container: + if: false + name: O4 ${{ matrix.image }} (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - { runner: ubuntu-24.04, arch: x86_64, image: "rockylinux:9" } + - { runner: ubuntu-24.04, arch: x86_64, image: "almalinux:9" } + - { runner: ubuntu-24.04, arch: x86_64, image: "quay.io/centos/centos:stream10" } + - { runner: ubuntu-24.04, arch: x86_64, image: "amazonlinux:2023" } + - { runner: ubuntu-24.04, arch: x86_64, image: "alpine:3.20" } + - { runner: ubuntu-24.04, arch: x86_64, image: "debian:13" } + - { runner: ubuntu-24.04, arch: x86_64, image: "opensuse/leap:15.6" } + - { runner: ubuntu-24.04-arm, arch: aarch64, image: "debian:12" } + - { runner: ubuntu-24.04-arm, arch: aarch64, image: "fedora:latest" } + - { runner: ubuntu-24.04-arm, arch: aarch64, image: "alpine:3.20" } + steps: + - uses: actions/checkout@v4 + with: + path: xlings + - name: release + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p build + gh release download "v$XV" -R openxlings/xlings -p "xlings-$XV-linux-${{ matrix.arch }}.tar.gz" -O build/release.tar.gz + - name: run (container) + continue-on-error: true + env: + IMAGE: ${{ matrix.image }} + run: | + sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 || true + docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined \ + --security-opt systempaths=unconfined --cap-add SYS_ADMIN \ + -v "$PWD:/w" -w /w "$IMAGE" sh -c 'command -v bash >/dev/null || apk add -q bash; bash -c "bash xlings/tests/host-matrix/bare.sh && bash xlings/tests/host-matrix/run.sh --tarball /w/build/release.tar.gz --evidence /w/evidence --scenarios \"install edition terminal\""' + - name: summary + if: always() + run: | + { echo "### ${{ matrix.image }} (${{ matrix.arch }})"; cat evidence/summary.md 2>/dev/null || echo "(no summary)"; } >> "$GITHUB_STEP_SUMMARY" + for f in evidence/*.ndjson; do [ -f "$f" ] || continue; echo "#### $f"; grep -E '"(fail|error)"' "$f" | head -5; done >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: o4-${{ strategy.job-index }} + path: evidence/ + if-no-files-found: ignore + + # O4: Rocky 9 as a kernel and SELinux host (KVM). + vm-rocky: + if: false + name: O4 rocky-9 (KVM) + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + with: + path: xlings + - name: release + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p build + gh release download "v$XV" -R openxlings/xlings -p "xlings-$XV-linux-x86_64.tar.gz" -O build/release.tar.gz + - name: run (vm) + continue-on-error: true + run: | + bash xlings/tools/ci/xlings_tools.sh qemu-system + sudo chmod 666 /dev/kvm + bash xlings/tests/host-matrix/vm.sh rocky-9 --tarball build/release.tar.gz --evidence evidence --scenarios "install edition terminal" + - name: summary + if: always() + run: | + { echo "### rocky-9 (KVM)"; cat evidence/summary.md 2>/dev/null || echo "(no summary)"; } >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: o4-rocky-vm + path: evidence/ + if-no-files-found: ignore diff --git a/.github/workflows/probe.yml b/.github/workflows/probe.yml index d98d7fd2d..2c58a0b49 100644 --- a/.github/workflows/probe.yml +++ b/.github/workflows/probe.yml @@ -1,7 +1,8 @@ # [temp][do not merge] Probe for # .agents/docs/2026-10-16-luban-inside-experience-and-ecosystem-design.md -# (O1 vim and terminfo, O2 generation directory entries and bind cost, -# O3 mount --move in a user namespace, O4 more host distributions). +# Round 4: A6 (the console's font at display scales), O12 (trays), O9 +# (cursor themes), O10 (virtiofs), O11 (a machine's current subos). +# Rounds 1-3 are kept in .github/probe/probe-round1-3.yml.txt. # Every script prints what it observes and never asserts. name: probe @@ -11,103 +12,93 @@ on: env: XV: 2026.10.15.4 + SDL: release-3.4.2 jobs: - inside: - name: O3b O6 O7 (ubuntu-24.04, core and desktop; a machine) + font: + name: A6 console font at display scales runs-on: ubuntu-24.04 - timeout-minutes: 120 + timeout-minutes: 40 steps: - uses: actions/checkout@v4 - name: probe - run: bash .github/probe/inside3.sh + run: bash .github/probe/font.sh - uses: actions/upload-artifact@v4 if: always() with: - name: inside - path: ${{ runner.temp }}/p/logs + name: font-shots + path: ${{ runner.temp }}/p/shots - # O4: distributions as user spaces (containers), as the host matrix runs - # them, with the scenarios that make and enter a system. - container: - if: false - name: O4 ${{ matrix.image }} (${{ matrix.runner }}) - runs-on: ${{ matrix.runner }} + misc: + name: O9 cursors, O10 virtiofs, O11 machine + runs-on: ubuntu-24.04 timeout-minutes: 90 - strategy: - fail-fast: false - matrix: - include: - - { runner: ubuntu-24.04, arch: x86_64, image: "rockylinux:9" } - - { runner: ubuntu-24.04, arch: x86_64, image: "almalinux:9" } - - { runner: ubuntu-24.04, arch: x86_64, image: "quay.io/centos/centos:stream10" } - - { runner: ubuntu-24.04, arch: x86_64, image: "amazonlinux:2023" } - - { runner: ubuntu-24.04, arch: x86_64, image: "alpine:3.20" } - - { runner: ubuntu-24.04, arch: x86_64, image: "debian:13" } - - { runner: ubuntu-24.04, arch: x86_64, image: "opensuse/leap:15.6" } - - { runner: ubuntu-24.04-arm, arch: aarch64, image: "debian:12" } - - { runner: ubuntu-24.04-arm, arch: aarch64, image: "fedora:latest" } - - { runner: ubuntu-24.04-arm, arch: aarch64, image: "alpine:3.20" } steps: - uses: actions/checkout@v4 - with: - path: xlings - - name: release - env: - GH_TOKEN: ${{ github.token }} - run: | - mkdir -p build - gh release download "v$XV" -R openxlings/xlings -p "xlings-$XV-linux-${{ matrix.arch }}.tar.gz" -O build/release.tar.gz - - name: run (container) - continue-on-error: true - env: - IMAGE: ${{ matrix.image }} - run: | - sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 || true - docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined \ - --security-opt systempaths=unconfined --cap-add SYS_ADMIN \ - -v "$PWD:/w" -w /w "$IMAGE" sh -c 'command -v bash >/dev/null || apk add -q bash; bash -c "bash xlings/tests/host-matrix/bare.sh && bash xlings/tests/host-matrix/run.sh --tarball /w/build/release.tar.gz --evidence /w/evidence --scenarios \"install edition terminal\""' - - name: summary - if: always() - run: | - { echo "### ${{ matrix.image }} (${{ matrix.arch }})"; cat evidence/summary.md 2>/dev/null || echo "(no summary)"; } >> "$GITHUB_STEP_SUMMARY" - for f in evidence/*.ndjson; do [ -f "$f" ] || continue; echo "#### $f"; grep -E '"(fail|error)"' "$f" | head -5; done >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true + - name: probe + run: bash .github/probe/misc.sh - uses: actions/upload-artifact@v4 if: always() with: - name: o4-${{ strategy.job-index }} - path: evidence/ - if-no-files-found: ignore + name: misc-logs + path: ${{ runner.temp }}/p/logs - # O4: Rocky 9 as a kernel and SELinux host (KVM). - vm-rocky: - if: false - name: O4 rocky-9 (KVM) - runs-on: ubuntu-24.04 - timeout-minutes: 90 + # O12: SDL 3.4.2's own testtray on each platform. + tray: + name: O12 tray (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-15, ubuntu-24.04] steps: - - uses: actions/checkout@v4 - with: - path: xlings - - name: release - env: - GH_TOKEN: ${{ github.token }} + - name: build SDL's testtray + shell: bash run: | - mkdir -p build - gh release download "v$XV" -R openxlings/xlings -p "xlings-$XV-linux-x86_64.tar.gz" -O build/release.tar.gz - - name: run (vm) - continue-on-error: true + git clone -q --depth 1 -b "$SDL" https://github.com/libsdl-org/SDL.git sdl + cmake -S sdl -B build -DSDL_TESTS=ON -DSDL_INSTALL_TESTS=OFF -DCMAKE_BUILD_TYPE=Release > cmake.log 2>&1 || tail -30 cmake.log + cmake --build build --config Release --target testtray -j 4 > build.log 2>&1 || tail -30 build.log + find build -name 'testtray*' -type f | head + - name: run (Windows) and capture the screen + if: runner.os == 'Windows' + shell: pwsh run: | - bash xlings/tools/ci/xlings_tools.sh qemu-system - sudo chmod 666 /dev/kvm - bash xlings/tests/host-matrix/vm.sh rocky-9 --tarball build/release.tar.gz --evidence evidence --scenarios "install edition terminal" - - name: summary - if: always() + $exe = (Get-ChildItem -Recurse build -Filter testtray.exe | Select-Object -First 1).FullName + Push-Location (Split-Path $exe); $p = Start-Process -FilePath $exe -PassThru; Pop-Location + Start-Sleep 6 + Add-Type -AssemblyName System.Windows.Forms, System.Drawing + $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds + $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height + [System.Drawing.Graphics]::FromImage($bmp).CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size) + New-Item -ItemType Directory -Force shots | Out-Null; $bmp.Save("shots/windows.png") + "process alive: $(-not $p.HasExited)" | Tee-Object -Append $env:GITHUB_STEP_SUMMARY + Stop-Process $p -ErrorAction SilentlyContinue + - name: run (macOS) and capture the screen + if: runner.os == 'macOS' + shell: bash + run: | + exe="$(find build -name testtray -type f -perm -u+x | head -1)" + (cd "$(dirname "$exe")" && ./testtray > "$GITHUB_WORKSPACE/tray.log" 2>&1 &) ; sleep 6 + mkdir -p shots; screencapture -x shots/macos.png + echo "process alive: $(pgrep -x testtray >/dev/null && echo yes || echo no)" >> "$GITHUB_STEP_SUMMARY" + tail -5 tray.log >> "$GITHUB_STEP_SUMMARY" || true + - name: run (Linux) without and with appindicator + if: runner.os == 'Linux' + shell: bash run: | - { echo "### rocky-9 (KVM)"; cat evidence/summary.md 2>/dev/null || echo "(no summary)"; } >> "$GITHUB_STEP_SUMMARY" + sudo apt-get -qq update >/dev/null; sudo apt-get -qq install -y xvfb >/dev/null + Xvfb :99 >/dev/null 2>&1 & sleep 2; export DISPLAY=:99 + exe="$(find build -name testtray -type f -perm -u+x | head -1)" + try() { (cd "$(dirname "$exe")" && SDL_LOGGING=all=debug timeout 6 ./testtray) > "tray-$1.log" 2>&1; echo "**$1** (exit $?)"; echo '```'; grep -iE 'tray|gtk|appindicator|error|could not|fail' "tray-$1.log" | head -10; echo '```'; } + { echo "ldd: $(ldd "$exe" | grep -iE 'gtk|appindicator|dbus' || echo none)"; try without; } >> "$GITHUB_STEP_SUMMARY" + sudo apt-get -qq install -y libgtk-3-0 libayatana-appindicator3-1 >/dev/null + try with-gtk-and-appindicator >> "$GITHUB_STEP_SUMMARY" - uses: actions/upload-artifact@v4 if: always() with: - name: o4-rocky-vm - path: evidence/ + name: tray-${{ matrix.os }} + path: | + shots/ + tray*.log if-no-files-found: ignore From 5ad02e753994316fc556174af8709e63392ee07b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 16 Oct 2026 05:09:12 +0900 Subject: [PATCH 5/6] probe: round 5 -- tray on Windows (static) and Linux, virtio-fs in the machine kernel, a machine's current subos --- .github/probe/misc.sh | 64 ++++++++++++++++--------------------- .github/probe/pty_run.py | 4 +-- .github/workflows/probe.yml | 9 ++++-- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/.github/probe/misc.sh b/.github/probe/misc.sh index 33e5d2d58..2f600d3b5 100755 --- a/.github/probe/misc.sh +++ b/.github/probe/misc.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# O9 cursor themes, O10 virtiofs, O11 a core system as a machine (vim and -# the current subos). Prints observations; never asserts. +# Round 5: O10 (virtio-fs in the machine kernel), O11 (a core system as a +# machine: vim and the current subos). Prints observations; never asserts. set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" XV="${XV:-2026.10.15.4}" @@ -13,41 +13,33 @@ block() { echo "== $title (exit $rc)"; echo "$out" { echo; echo "**$title** (exit $rc)"; echo '```'; echo "$out" | tail -n "${LINES_:-40}"; echo '```'; } >> "$S" } -sudo apt-get -qq update >/dev/null - -say "### O9: cursor themes" -sudo apt-get -qq install -y adwaita-icon-theme breeze-cursor-theme >/dev/null 2>&1 -block "sizes, files, nominal sizes, licences" python3 - <<'PY' -import os, struct, glob -def nominal(path): - try: - b = open(path, "rb").read() - if b[:4] != b"Xcur": return [] - n = struct.unpack_from("/dev/null 2>&1 -VFS="$(command -v virtiofsd || ls /usr/libexec/virtiofsd /usr/lib/qemu/virtiofsd 2>/dev/null | head -1)" -mkdir -p "$T/share" -block "virtiofsd unprivileged (--sandbox none)" bash -c "'$VFS' --version 2>&1 | head -1; ('$VFS' --socket-path='$T/vfs.sock' --shared-dir='$T/share' --sandbox none & echo \$! > '$T/vfs.pid'); sleep 2; ls -la '$T/vfs.sock' && echo 'listening as $(id -un)'; kill \$(cat '$T/vfs.pid') 2>/dev/null" -block "virtiofsd unprivileged (--sandbox namespace)" bash -c "('$VFS' --socket-path='$T/vfs2.sock' --shared-dir='$T/share' --sandbox namespace & echo \$! > '$T/vfs2.pid'); sleep 2; ls -la '$T/vfs2.sock' && echo listening; kill \$(cat '$T/vfs2.pid') 2>/dev/null" curl -fsSL -o xl.tgz "https://github.com/openxlings/xlings/releases/download/v$XV/xlings-$XV-linux-x86_64.tar.gz" mkdir -p inst && tar -xzf xl.tgz -C inst && (cd inst/*/ && ./bin/xlings self install logs/install.log 2>&1 export PATH="$HOME/.xlings/bin:$PATH" + +say "### O10: virtio-fs in the machine kernel" xlings install -y xim:linux-kernel-virt > logs/kernel.log 2>&1 K="$(ls -d "$HOME"/.xlings/data/xpkgs/xim-x-linux-kernel-virt/* 2>/dev/null | head -1)" -block "linux-kernel-virt: virtio-fs and FUSE in its config" bash -c "ls '$K' '$K'/boot 2>/dev/null | head; cfg=\$(ls '$K'/boot/config* '$K'/config* 2>/dev/null | head -1); if [ -n \"\$cfg\" ]; then grep -E 'CONFIG_(VIRTIO_FS|FUSE_FS|DAX)=' \"\$cfg\"; else img=\$(ls '$K'/boot/vmlinuz* | head -1); echo \"no config file; searching \$img\"; strings \"\$img\" | grep -m3 -E 'virtiofs|virtio_fs'; fi" +cat > "$T/kernel.sh" <<'SH' +K="$1" +find "$K" -maxdepth 4 | head -20 +for f in $(find "$K" -name 'config*' -o -name '.config' | head -2); do echo "config: $f"; grep -E 'CONFIG_(VIRTIO_FS|FUSE_FS|DAX)=' "$f"; done +echo "modules:"; find "$K" -name 'virtiofs.ko*' -o -name 'fuse.ko*' | head +for f in $(find "$K" -name 'modules.builtin' | head -1); do echo "builtin: $f"; grep -E 'virtiofs|/fuse' "$f"; done +img="$(find "$K" -name 'vmlinuz*' | head -1)"; echo "image: $img" +if [ -n "$img" ]; then + out="$(mktemp)" + # The image is compressed: the extract-vmlinux way (gzip / zstd / xz) + for magic in '\037\213\010' '(\265/\375' '\3757zXZ\000'; do + pos=$(LC_ALL=C grep -aobP "$(printf "$magic" | od -An -tx1 | sed 's/ /\\x/g')" "$img" | head -1 | cut -d: -f1) + [ -n "$pos" ] || continue + tail -c +$((pos + 1)) "$img" | (gzip -dc 2>/dev/null || zstd -dc 2>/dev/null || xz -dc 2>/dev/null) > "$out" 2>/dev/null + [ -s "$out" ] && break + done + if [ -s "$out" ]; then strings "$out" | grep -m5 -E 'virtiofs|virtio_fs|fuse' || echo "no virtiofs/fuse strings in the decompressed image"; else echo "could not decompress"; fi +fi +SH +block "linux-kernel-virt: virtio-fs" bash "$T/kernel.sh" "$K" say "### O11: a core system as a machine" sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 @@ -55,8 +47,8 @@ sudo chmod 666 /dev/kvm 2>/dev/null || true LB="$HOME/.xlings/bin/luban" LINES_=6 block "luban new pcore --edition core -y" timeout 2400 env XLINGS_NON_INTERACTIVE=1 "$LB" new pcore --edition core -y block "luban config pcore --mode machine" "$LB" config pcore --mode machine -LINES_=80 block "luban enter pcore (machine): vim, current subos" python3 "$HERE/pty_run.py" --out "$T/logs/machine.bin" --wait 600 --tail 10 \ - --step '(Proceed|\[Y/n\]|y/N)|y\r' \ - --step '[#$] $|uname -r; echo ACTIVE=$XLINGS_ACTIVE_SUBOS; ls -la /usr/bin/vim; vim --version | head -2; xlings subos list | head -5; cat /etc/xlings/root.json 2>&1; echo PROBE-DONE\r' \ - --step 'PROBE-DONE|poweroff\r' \ +LINES_=80 block "luban enter pcore (machine): vim, current subos" python3 "$HERE/pty_run.py" --out "$T/logs/machine.bin" --wait 900 --tail 15 \ + --step '(Proceed|\[Y/n\]|y/N|[#$] $) ::: y\r' \ + --step '[#$] $ ::: uname -r; echo ACTIVE=$XLINGS_ACTIVE_SUBOS; ls -la /usr/bin/vim; vim --version | head -2; xlings subos list | head -5; cat /etc/xlings/root.json; echo PROBE-DONE\r' \ + --step 'PROBE-DONE ::: poweroff\r' \ -- env TERM=xterm-256color "$LB" enter pcore diff --git a/.github/probe/pty_run.py b/.github/probe/pty_run.py index 3b7536192..db408cfdc 100755 --- a/.github/probe/pty_run.py +++ b/.github/probe/pty_run.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Run a command on a pty, type a script into it, and record every byte. - pty_run.py --out [--cols 120 --rows 40] [--step "|"]... -- + pty_run.py --out [--cols 120 --rows 40] [--step " ::: "]... -- Each step waits (up to --wait seconds) until the output so far matches the regex, then types the text (\\r and \\x1b escapes are honoured). Prints a @@ -42,7 +42,7 @@ def pump(timeout): alive = True for step in a.step: - pat, _, text = step.partition("|") + pat, _, text = step.partition(" ::: ") text = text.encode().decode("unicode_escape").encode("latin-1") deadline = time.time() + a.wait start = len(buf) diff --git a/.github/workflows/probe.yml b/.github/workflows/probe.yml index 2c58a0b49..23dffc49f 100644 --- a/.github/workflows/probe.yml +++ b/.github/workflows/probe.yml @@ -1,5 +1,6 @@ # [temp][do not merge] Probe for # .agents/docs/2026-10-16-luban-inside-experience-and-ecosystem-design.md +# Round 5: tray on Windows (static) and Linux (X11 dev), O10 kernel, O11 machine. # Round 4: A6 (the console's font at display scales), O12 (trays), O9 # (cursor themes), O10 (virtiofs), O11 (a machine's current subos). # Rounds 1-3 are kept in .github/probe/probe-round1-3.yml.txt. @@ -16,6 +17,7 @@ env: jobs: font: + if: false name: A6 console font at display scales runs-on: ubuntu-24.04 timeout-minutes: 40 @@ -30,7 +32,7 @@ jobs: path: ${{ runner.temp }}/p/shots misc: - name: O9 cursors, O10 virtiofs, O11 machine + name: O10 kernel virtio-fs, O11 machine runs-on: ubuntu-24.04 timeout-minutes: 90 steps: @@ -51,13 +53,14 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest, macos-15, ubuntu-24.04] + os: [windows-latest, ubuntu-24.04] steps: - name: build SDL's testtray shell: bash run: | + if [ "$RUNNER_OS" = Linux ]; then sudo apt-get -qq update >/dev/null; sudo apt-get -qq install -y libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxfixes-dev libxss-dev >/dev/null; fi git clone -q --depth 1 -b "$SDL" https://github.com/libsdl-org/SDL.git sdl - cmake -S sdl -B build -DSDL_TESTS=ON -DSDL_INSTALL_TESTS=OFF -DCMAKE_BUILD_TYPE=Release > cmake.log 2>&1 || tail -30 cmake.log + cmake -S sdl -B build -DSDL_SHARED=OFF -DSDL_STATIC=ON -DSDL_TESTS=ON -DSDL_INSTALL_TESTS=OFF -DCMAKE_BUILD_TYPE=Release > cmake.log 2>&1 || tail -30 cmake.log cmake --build build --config Release --target testtray -j 4 > build.log 2>&1 || tail -30 build.log find build -name 'testtray*' -type f | head - name: run (Windows) and capture the screen From 6427e4cea5551a8c74d871aa546a7cff50b6fdbe Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 16 Oct 2026 05:20:05 +0900 Subject: [PATCH 6/6] probe: round 6 -- the Linux tray (XTEST off) --- .github/workflows/probe.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/probe.yml b/.github/workflows/probe.yml index 23dffc49f..a173c2ad7 100644 --- a/.github/workflows/probe.yml +++ b/.github/workflows/probe.yml @@ -32,6 +32,7 @@ jobs: path: ${{ runner.temp }}/p/shots misc: + if: false name: O10 kernel virtio-fs, O11 machine runs-on: ubuntu-24.04 timeout-minutes: 90 @@ -53,14 +54,14 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest, ubuntu-24.04] + os: [ubuntu-24.04] steps: - name: build SDL's testtray shell: bash run: | if [ "$RUNNER_OS" = Linux ]; then sudo apt-get -qq update >/dev/null; sudo apt-get -qq install -y libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxfixes-dev libxss-dev >/dev/null; fi git clone -q --depth 1 -b "$SDL" https://github.com/libsdl-org/SDL.git sdl - cmake -S sdl -B build -DSDL_SHARED=OFF -DSDL_STATIC=ON -DSDL_TESTS=ON -DSDL_INSTALL_TESTS=OFF -DCMAKE_BUILD_TYPE=Release > cmake.log 2>&1 || tail -30 cmake.log + cmake -S sdl -B build -DSDL_SHARED=OFF -DSDL_STATIC=ON -DSDL_X11_XTEST=OFF -DSDL_TESTS=ON -DSDL_INSTALL_TESTS=OFF -DCMAKE_BUILD_TYPE=Release > cmake.log 2>&1 || tail -30 cmake.log cmake --build build --config Release --target testtray -j 4 > build.log 2>&1 || tail -30 build.log find build -name 'testtray*' -type f | head - name: run (Windows) and capture the screen