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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,67 @@
- [x] **B2** 本地跑 163 验证四个 sub-case(step 4 依赖 xlings >= 0.4.69,按现有逻辑
自动 skip 则接受)。

### P3 — 失效面 C:bootstrap pin
### P3 — 失效面 C:pin 与版本漂移(实施中范围扩大)

- [x] **C1** `.github/actions/bootstrap-mcpp/action.yml`:unix + windows 两段
`xlings install mcpp -y` → 读 `.xlings.json` 的 pin 装指定版本。
- [x] **C2** 装完断言 `mcpp --version` == pin,不等则 fail(不再静默漂移)。
- [x] **C2** 装完断言解析出的版本**精确等于** pin(子串匹配会让 `0.0.10` 被
`0.0.109` 满足),不等则 fail。
- [x] **C3** 按「版本目录」定位二进制而非硬编码内部布局:Linux 是
`~/.xlings/data/xpkgs/xim-x-mcpp/<v>/mcpp`,Windows 是
`.../subos/default/data/xpkgs/xim-x-mcpp/<v>/bin/mcpp.exe`,根与层级都不同。
**教训**:只凭 Windows 一条日志推断跨平台路径,白烧一轮 CI。
- [x] **C4** `.xlings.json` 用 `cd` 之前捕获的 `REPO_DIR` 绝对路径读取——Windows
leg 解压后 `cd "$WORK"` 且从不切回;不用 `GITHUB_WORKSPACE`(git-bash 下
是反斜杠 Windows 路径)。
- [x] **C5** `setup-macos-llvm` 同样是未 pin 的 bootstrap 点(冷启动拿到 0.0.105
< 索引 floor 0.0.108 → 描述符全读不到 → 依赖回落 legacy 地址失败)。同法 pin。
- [x] **C6** bootstrap 打印 system / sandbox 两个 xlings 版本。沙箱那份才是真正
解析依赖的,而它从不更新,此前完全不可见。

### P3b — 沙箱 xlings 版本(实测后追加,非推断)

- [x] **C7** 实测 CI 沙箱 xlings = **0.4.30**(先加零风险诊断打印,再动手)。
索引短名迁移后同一 repo 有两个 `name = "lua"`(`compat:lua` 与
`mcpplibs.capi:lua`,经 `mcpplibs.xpkg` 传递同时需要),0.4.69 前 xlings 按
裸 name 建表 → 必有一个够不到,**够不到哪个取决于机器**。
- [x] **C8** `bootstrap-mcpp` / `setup-macos-llvm` 的 xlings 0.4.30 → **0.4.69**,
与 `release.yml`/`cross-build-test.yml`(早已 0.4.69)对齐;
`bootstrap-macos.yml`(dispatch-only)一并跟上。
- [x] **C9** xlings 版本进入**缓存血统**而非仅 key:`restore-keys` 是前缀匹配,
只放进 key 的话 0.4.69 的 bootstrap 仍会恢复 0.4.30 的沙箱。代价=一次冷启动。

### P3c — 全仓 bootstrap 收敛(release 失败后追加)

0.0.109 的 release 在 `build + upload (linux/x86_64)` 挂掉,同一个症状:裸
`xlings install mcpp` 拿到 **0.0.105** < 索引 floor 0.0.108 → 描述符全读不到 →
依赖回落 legacy 地址 → 死在 `mcpplibs.cmdline@0.0.1`。P3 只修了两个 composite
action,`release.yml` 自己还有 4 处内联 bootstrap。

- [x] **C10** 全仓盘点:裸 `xlings install mcpp` 共 **6+ 处**(release.yml ×4、
cross-build-test.yml ×2、两个 action、ci-aarch64-fresh-install、ci-linux-e2e),
各自还有细微漂移(Windows 那处用 `find | head -1` 取到任意版本)。
- [x] **C11** 抽 `.github/tools/install_pinned_mcpp.sh` 作为**唯一实现**:读 pin →
装 → 按版本目录定位 → 精确版本断言 → stdout 只输出路径、诊断走 stderr。
六处调用它,而不是写第七份拷贝。
- [x] **C12** 修 `set -eo pipefail` 下的雷:`find ~/.xlings ... 2>/dev/null` 遇到
不可读目录返回非零(`2>/dev/null` 只藏消息不藏状态)→ 整条管道非零 → 脚本
在能报出任何有用信息之前就被 errexit 杀掉。全部 `|| true` + 显式 `return 0`。
**这个雷在 P3 写进两个 action 的版本里同样存在**,只是循环提前 return 才没炸。
- [x] **C13** Windows release leg 同样在 `cd "$WORK"` 后不回仓库 → 用 cd 前捕获的
`REPO_DIR="$(pwd)"`,不用 `GITHUB_WORKSPACE`(git-bash 下是反斜杠路径)。
- [ ] **C14**(follow-up,不在本次)`ci-aarch64-fresh-install` 的裸安装是**故意的**
(验证用户 fresh-install 路径);`ci-linux-e2e` 的 hermetic job 目前是绿的,
连跑六轮后为风格一致去动绿 job 是坏判断。两处单独评估。

### P4 — 版本 + 收口

- [x] **D1** 版本 0.0.108 → 0.0.109,两处同步。
- [x] **D2** 本地全量单测。
- [ ] **D3** 本地 e2e(至少 100 / 163 / 165 / 78 / 79 + 全量能跑的部分)。
- [ ] **D4** 单 PR(附版本)→ 7 个 workflow 全绿。
- [x] **D3** 本地全量 e2e:155 passed / 唯一非预期失败是本机 g++ shim 环境问题(22)。
**负向对照**用 `git checkout main -- <files>` 重建(`git stash` 做不了——
修复已在 commit 里时它只回退未提交部分,测试会假通过):165 与 69-L8 都失败。
- [x] **D4** 单 PR #286(0.0.109)→ 15 个 check 全绿。
- [ ] **D5** bypass squash 合入 main。

### P5 — xlings 全生态验证
Expand Down
114 changes: 6 additions & 108 deletions .github/actions/bootstrap-mcpp/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,70 +77,10 @@ runs:
export PATH="$HOME/.xlings/subos/default/bin:$PATH"
echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH"
xlings --version
# xim:mcpp — pinned, NOT bare `xlings install mcpp`.
#
# A bare install is idempotent, so on a cache hit it keeps whatever
# version the cache already holds. `.xlings.json` only fed the cache
# KEY, and `restore-keys` prefix-matches, so a stale sandbox got
# restored and the pin was never applied: jobs ran 0.0.107 and 0.0.102
# against an index whose floor was 0.0.108 (E0006). Warm caches hid it
# — a cold one would have been a hard failure.
# Absolute, POSIX-form path captured before any `cd`: the Windows leg
# cds into the unpack dir and never returns, so a relative
# `.xlings.json` resolves to nothing there — and GITHUB_WORKSPACE is a
# backslash Windows path that git-bash tools mangle.
XJSON="$REPO_DIR/.xlings.json"
MCPP_PIN=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' "$XJSON" \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
test -n "$MCPP_PIN" || { echo "FAIL: no mcpp pin in $XJSON"; exit 1; }
echo "bootstrap pin: mcpp@${MCPP_PIN}"
# Address the pinned version through ITS OWN version directory rather
# than the shim in subos/default/bin (which is a symlink to xlings and
# resolves to whatever version is currently selected — exactly the
# drift this pin exists to prevent).
#
# Locate it by searching inside `xim-x-mcpp/<pin>/` instead of assuming
# the layout underneath: it is `<ver>/mcpp` on Linux and `<ver>/bin/
# mcpp.exe` on Windows, and the roots differ too. Anything found under
# that directory IS the pinned version, so this keeps the guarantee
# without hardcoding a shape that varies per platform.
find_pinned_mcpp() {
local d r
for d in "$HOME/.xlings/data/xpkgs/xim-x-mcpp/${MCPP_PIN}" \
"$HOME/.xlings/subos/default/data/xpkgs/xim-x-mcpp/${MCPP_PIN}"; do
[ -d "$d" ] || continue
r=$(find "$d" -type f -name mcpp 2>/dev/null | head -1)
[ -n "$r" ] && { echo "$r"; return 0; }
done
# Last resort: the roots above are the two layouts seen in practice,
# but the VERSION DIRECTORY is the actual contract — find it wherever
# xlings put it rather than fail on a layout we have not met yet.
find "$HOME/.xlings" -type d -path "*xim-x-mcpp/${MCPP_PIN}" \
-exec find {} -type f -name mcpp ';' 2>/dev/null | head -1
}
xlings install "mcpp@${MCPP_PIN}" -y || true
MCPP=$(find_pinned_mcpp)
if [ -z "$MCPP" ]; then
# A restored ~/.xlings can carry a xim-pkgindex clone that predates
# this release, and the refresh marker can read fresh while the clone
# is behind. Refresh once and retry before calling it a failure.
echo "pinned install did not land; refreshing the index and retrying"
xlings update || true
xlings install "mcpp@${MCPP_PIN}" -y || true
MCPP=$(find_pinned_mcpp)
fi
test -n "$MCPP" || {
echo "FAIL: no mcpp binary under xim-x-mcpp/${MCPP_PIN}"
ls -d "$HOME"/.xlings/data/xpkgs/xim-x-mcpp/* \
"$HOME"/.xlings/subos/default/data/xpkgs/xim-x-mcpp/* 2>/dev/null
exit 1; }
"$MCPP" --version
# Compare the parsed version exactly: a substring match would let pin
# 0.0.10 be satisfied by a 0.0.109 binary.
MCPP_GOT=$("$MCPP" --version 2>/dev/null | head -1 \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
[ "$MCPP_GOT" = "$MCPP_PIN" ] || {
echo "FAIL: mcpp at $MCPP reports '$MCPP_GOT', pin is '$MCPP_PIN'"; exit 1; }
# xim:mcpp — pinned, NOT bare `xlings install mcpp`. One shared
# implementation for every bootstrap point in the repo; see
# .github/tools/install_pinned_mcpp.sh for why.
MCPP=$(bash "$REPO_DIR/.github/tools/install_pinned_mcpp.sh" "$REPO_DIR")
# The SANDBOX xlings is a separate copy: mcpp vendors it into
# ~/.mcpp/registry/bin at `self init` and never revisits it
# (acquire_xlings_binary returns early when the file exists), while
Expand Down Expand Up @@ -174,50 +114,8 @@ runs:
export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH"
echo "$USERPROFILE/.xlings/subos/default/bin" >> "$GITHUB_PATH"
xlings.exe --version
# Pinned install — see the unix leg for why a bare `install mcpp` let
# the version drift behind the index floor unnoticed. The old `find |
# head -1` here made it worse: with several versions in the sandbox it
# returned whichever the directory walk hit first.
# Absolute, POSIX-form path captured before any `cd`: the Windows leg
# cds into the unpack dir and never returns, so a relative
# `.xlings.json` resolves to nothing there — and GITHUB_WORKSPACE is a
# backslash Windows path that git-bash tools mangle.
XJSON="$REPO_DIR/.xlings.json"
MCPP_PIN=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' "$XJSON" \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
test -n "$MCPP_PIN" || { echo "FAIL: no mcpp pin in $XJSON"; exit 1; }
echo "bootstrap pin: mcpp@${MCPP_PIN}"
# Search inside the pinned version's own directory rather than assuming
# the layout under it — see the unix leg.
find_pinned_mcpp() {
local d r
for d in "$USERPROFILE/.xlings/subos/default/data/xpkgs/xim-x-mcpp/${MCPP_PIN}" \
"$USERPROFILE/.xlings/data/xpkgs/xim-x-mcpp/${MCPP_PIN}"; do
[ -d "$d" ] || continue
r=$(find "$d" -type f -name "mcpp.exe" 2>/dev/null | head -1)
[ -n "$r" ] && { echo "$r"; return 0; }
done
find "$USERPROFILE/.xlings" -type d -path "*xim-x-mcpp/${MCPP_PIN}" \
-exec find {} -type f -name "mcpp.exe" ';' 2>/dev/null | head -1
}
xlings.exe install "mcpp@${MCPP_PIN}" -y || true
MCPP=$(find_pinned_mcpp)
if [ -z "$MCPP" ]; then
echo "pinned install did not land; refreshing the index and retrying"
xlings.exe update || true
xlings.exe install "mcpp@${MCPP_PIN}" -y || true
MCPP=$(find_pinned_mcpp)
fi
test -n "$MCPP" || {
echo "FAIL: no mcpp.exe under xim-x-mcpp/${MCPP_PIN}"
ls -d "$USERPROFILE"/.xlings/subos/default/data/xpkgs/xim-x-mcpp/* \
"$USERPROFILE"/.xlings/data/xpkgs/xim-x-mcpp/* 2>/dev/null
exit 1; }
"$MCPP" --version
MCPP_GOT=$("$MCPP" --version 2>/dev/null | head -1 \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
[ "$MCPP_GOT" = "$MCPP_PIN" ] || {
echo "FAIL: mcpp at $MCPP reports '$MCPP_GOT', pin is '$MCPP_PIN'"; exit 1; }
# Pinned install — shared implementation, see the unix leg.
MCPP=$(bash "$REPO_DIR/.github/tools/install_pinned_mcpp.sh" "$REPO_DIR")
# See the unix leg: the sandbox keeps its own vendored xlings copy.
echo "system xlings: $(xlings.exe --version 2>/dev/null | head -1)"
if [ -x "$USERPROFILE/.mcpp/registry/bin/xlings.exe" ]; then
Expand Down
43 changes: 4 additions & 39 deletions .github/actions/setup-macos-llvm/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,45 +61,10 @@ runs:
- name: Bootstrap mcpp via xlings
shell: bash
run: |
# Pinned to .xlings.json, same as .github/actions/bootstrap-mcpp.
# A bare `xlings install mcpp` takes "newest in whatever index copy
# this runner happens to have", which on a cold cache resolved 0.0.105
# — below the index's own floor (0.0.108). The bootstrap then could not
# read ANY descriptor (the floor check makes the read return nothing),
# so every dependency fell back to its legacy derived address and the
# build died on `mcpplibs.cmdline@0.0.1`.
MCPP_PIN=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' "$GITHUB_WORKSPACE/.xlings.json" \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
test -n "$MCPP_PIN" || { echo "FAIL: no mcpp pin in .xlings.json"; exit 1; }
echo "bootstrap pin: mcpp@${MCPP_PIN}"
find_pinned_mcpp() {
local d r
for d in "$HOME/.xlings/data/xpkgs/xim-x-mcpp/${MCPP_PIN}" \
"$HOME/.xlings/subos/default/data/xpkgs/xim-x-mcpp/${MCPP_PIN}"; do
[ -d "$d" ] || continue
r=$(find "$d" -type f -name mcpp 2>/dev/null | head -1)
[ -n "$r" ] && { echo "$r"; return 0; }
done
find "$HOME/.xlings" -type d -path "*xim-x-mcpp/${MCPP_PIN}" \
-exec find {} -type f -name mcpp ';' 2>/dev/null | head -1
}
xlings install "mcpp@${MCPP_PIN}" -y || true
MCPP=$(find_pinned_mcpp)
if [ -z "$MCPP" ]; then
echo "pinned install did not land; refreshing the index and retrying"
xlings update || true
xlings install "mcpp@${MCPP_PIN}" -y || true
MCPP=$(find_pinned_mcpp)
fi
test -n "$MCPP" || {
echo "FAIL: no mcpp binary under xim-x-mcpp/${MCPP_PIN}"
ls -d "$HOME"/.xlings/data/xpkgs/xim-x-mcpp/* 2>/dev/null
exit 1; }
"$MCPP" --version
MCPP_GOT=$("$MCPP" --version 2>/dev/null | head -1 \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
[ "$MCPP_GOT" = "$MCPP_PIN" ] || {
echo "FAIL: mcpp at $MCPP reports '$MCPP_GOT', pin is '$MCPP_PIN'"; exit 1; }
# Pinned to .xlings.json, shared implementation. A bare
# `xlings install mcpp` resolved 0.0.105 here on a cold cache — below
# the index floor, which makes every descriptor read return nothing.
MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE")
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV"

Expand Down
92 changes: 92 additions & 0 deletions .github/tools/install_pinned_mcpp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# install_pinned_mcpp.sh — install the bootstrap mcpp that .xlings.json pins,
# and print the resolved binary's path on stdout.
#
# WHY THIS EXISTS. Six places bootstrapped mcpp with `xlings install mcpp -y`.
# Passing the package name is what breaks it: with an explicit name xlings
# resolves "newest in this runner's index copy" and the workspace pin never
# applies. On 2026-07-26 CI jobs were running 0.0.102, 0.0.105 and 0.0.107
# against an index whose floor is 0.0.108. Warm caches hid it; the moment one
# went cold the floor check made EVERY descriptor read return nothing, every
# dependency fell back to its legacy derived address, and the build died on
# `mcpplibs.cmdline@0.0.1` — an error naming neither the version nor the floor.
#
# WHAT THIS DOES NOT DO. It does not parse the pin to decide what to install,
# does not refresh the index, and does not go looking for the binary on disk.
# `.xlings.json` is xlings' own file and xlings already does all three:
#
# $ xlings install # no package name
# [xlings] 'mcpp@0.0.108' not in current index; refreshing index...
# Packages to install (1):
# ◆ xim:mcpp@0.0.108
#
# An earlier version of this script re-implemented every one of those steps by
# hand — grep the pin, `xlings update` on miss, `find` the install directory.
# That is the same mistake the fix it supports is about: re-deriving what the
# owner of the information will tell you. It also cost two bugs (a hardcoded
# per-platform layout, and a `find` that exits non-zero on one unreadable
# directory under `set -eo pipefail`).
#
# `-u` activates the version just installed, so the shim resolves to it; that
# is the piece a plain `install` leaves alone, and the reason CI could install
# one version and then run another.
#
# Usage: MCPP=$(bash .github/tools/install_pinned_mcpp.sh)
# MCPP=$(bash .github/tools/install_pinned_mcpp.sh /path/to/repo)
#
# stdout is ONLY the binary path; all diagnostics go to stderr.
set -euo pipefail

REPO_DIR="${1:-$(pwd)}"
[ -f "$REPO_DIR/.xlings.json" ] || {
echo "FAIL: no .xlings.json at $REPO_DIR" >&2; exit 1; }

case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) XL_HOME="${USERPROFILE:-$HOME}"; EXE=".exe" ;;
*) XL_HOME="$HOME"; EXE="" ;;
esac

# The workspace pin is read from the CWD, so run from the repo regardless of
# where the caller happens to be (the Windows legs cd into an unpack dir and
# never return).
cd "$REPO_DIR"

# Address xlings by path, not through PATH. The callers export
# `$USERPROFILE/.xlings/subos/default/bin` into PATH before invoking this, and
# `xlings.exe --version` works in that shell — but this script runs as a child
# bash, and MSYS re-derives PATH from the Windows environment on startup, which
# drops that mixed-separator entry (`C:\Users\x/.xlings/...`) and leaves
# `xlings.exe: command not found`. This is the same well-known location every
# caller already hardcodes as XLINGS_BIN, so it adds no new layout knowledge.
XL="$XL_HOME/.xlings/subos/default/bin/xlings${EXE}"
[ -x "$XL" ] || XL=$(command -v "xlings${EXE}" 2>/dev/null || true)
[ -n "$XL" ] && [ -x "$XL" ] || {
echo "FAIL: no xlings at $XL_HOME/.xlings/subos/default/bin nor on PATH" >&2
exit 1; }

"$XL" install -y -u -g >&2

MCPP="$XL_HOME/.xlings/subos/default/bin/mcpp${EXE}"
[ -x "$MCPP" ] || { echo "FAIL: no mcpp at $MCPP after install" >&2; exit 1; }

# Assert the outcome rather than trust it. This is the ONLY place the pin is
# parsed, and only to check what xlings actually activated — a guard against
# the silent drift this script exists to end, not a second resolution path.
#
# The `|| true` on both captures is load-bearing under `set -eo pipefail`: a
# `| grep` that matches nothing exits 1, which would kill the script silently —
# CI would get a bare non-zero and no diagnostic, exactly the failure mode this
# whole change is meant to stop. A miss has to reach the check below, which can
# name what it expected and what it found.
PIN=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' "$REPO_DIR/.xlings.json" \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
GOT=$("$MCPP" --version 2>/dev/null | head -1 \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
# Exact comparison: a substring match would let a pin of 0.0.10 be satisfied by
# a 0.0.109 binary.
[ -n "$PIN" ] && [ "$GOT" = "$PIN" ] || {
echo "FAIL: .xlings.json pins '${PIN:-?}' but the active mcpp is '${GOT:-?}'" >&2
exit 1; }
echo "bootstrap mcpp: $MCPP ($GOT)" >&2

echo "$MCPP"
Loading
Loading