From 520001e839675f183c5250681952f1601cee6ca5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 26 Jul 2026 18:58:16 +0800 Subject: [PATCH] ci: one pinned-mcpp bootstrap, not six drifting copies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.0.109 release died in `build + upload (linux/x86_64)` with the symptom 0.0.109 was released to fix: error: index requires mcpp >= 0.0.108 but this is mcpp 0.0.105 [E0006] error: xlings install_packages failed (exit 1) for 'mcpplibs.cmdline@0.0.1' A bare `xlings install mcpp -y` resolves "newest in whatever index copy this runner happens to have", not the pin in .xlings.json. When that lands below the index floor, the floor check makes EVERY descriptor read return nothing, so every dependency falls back to its legacy derived address — and the error names neither the version nor the floor as the cause. #286 pinned two composite actions. It missed that release.yml carries four inline bootstraps of its own, cross-build-test.yml two more, and each had drifted: the Windows one used `find ~/.xlings -name mcpp.exe | head -1`, which returns whichever version the directory walk reaches first. So: one implementation. .github/tools/install_pinned_mcpp.sh reads the pin, installs it, locates the binary inside that version's own directory (the layout underneath differs per platform — `/mcpp` on Linux, `/bin/mcpp.exe` on Windows — so only the version directory is treated as the contract), asserts the parsed version matches exactly, and prints just the path. Six call sites use it, including the two from #286, so there is no seventh copy to drift. Two bugs found while consolidating, both of which #286 had shipped: * Under `set -eo pipefail` a `find ~/.xlings ... 2>/dev/null` over a tree with one unreadable directory exits non-zero — the redirect hides the message, not the status. That killed the script before it could report anything. Every find is now `|| true` with an explicit `return 0`, since a miss is a normal outcome the caller handles. #286's copies had the same landmine; it only stayed quiet because the loop returned early before reaching the fallback. * An exact version comparison instead of a substring match, which would let a pin of 0.0.10 be satisfied by a 0.0.109 binary. The Windows release leg also never returns to the workspace after `cd "$WORK"`, so it reads .xlings.json through a REPO_DIR captured beforehand rather than GITHUB_WORKSPACE, which git-bash sees as a backslash path. Left alone deliberately: ci-aarch64-fresh-install's bare install is the point of that workflow (it verifies what a user's fresh install resolves), and ci-linux-e2e's hermetic bootstrap is green — reshaping a passing job for stylistic consistency after six CI rounds is not a good trade. No source change; 0.0.109 is unreleased, so the version stays. --- ...e-name-wire-address-implementation-plan.md | 57 ++++++++- .github/actions/bootstrap-mcpp/action.yml | 114 +----------------- .github/actions/setup-macos-llvm/action.yml | 43 +------ .github/tools/install_pinned_mcpp.sh | 92 ++++++++++++++ .github/workflows/cross-build-test.yml | 20 ++- .github/workflows/release.yml | 42 ++++--- 6 files changed, 194 insertions(+), 174 deletions(-) create mode 100755 .github/tools/install_pinned_mcpp.sh diff --git a/.agents/docs/2026-07-26-bare-name-wire-address-implementation-plan.md b/.agents/docs/2026-07-26-bare-name-wire-address-implementation-plan.md index 56e0189c..56fb19a8 100644 --- a/.agents/docs/2026-07-26-bare-name-wire-address-implementation-plan.md +++ b/.agents/docs/2026-07-26-bare-name-wire-address-implementation-plan.md @@ -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//mcpp`,Windows 是 + `.../subos/default/data/xpkgs/xim-x-mcpp//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 -- ` 重建(`git stash` 做不了—— + 修复已在 commit 里时它只回退未提交部分,测试会假通过):165 与 69-L8 都失败。 +- [x] **D4** 单 PR #286(0.0.109)→ 15 个 check 全绿。 - [ ] **D5** bypass squash 合入 main。 ### P5 — xlings 全生态验证 diff --git a/.github/actions/bootstrap-mcpp/action.yml b/.github/actions/bootstrap-mcpp/action.yml index 393c8f7a..009ce3d7 100644 --- a/.github/actions/bootstrap-mcpp/action.yml +++ b/.github/actions/bootstrap-mcpp/action.yml @@ -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//` instead of assuming - # the layout underneath: it is `/mcpp` on Linux and `/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 @@ -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 diff --git a/.github/actions/setup-macos-llvm/action.yml b/.github/actions/setup-macos-llvm/action.yml index b8d0cbe9..60bb0cc6 100644 --- a/.github/actions/setup-macos-llvm/action.yml +++ b/.github/actions/setup-macos-llvm/action.yml @@ -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" diff --git a/.github/tools/install_pinned_mcpp.sh b/.github/tools/install_pinned_mcpp.sh new file mode 100755 index 00000000..f3d2723c --- /dev/null +++ b/.github/tools/install_pinned_mcpp.sh @@ -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" diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml index be5c73f1..38773400 100644 --- a/.github/workflows/cross-build-test.yml +++ b/.github/workflows/cross-build-test.yml @@ -120,9 +120,15 @@ jobs: find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true xlings config --mirror GLOBAL 2>/dev/null || true xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - xlings install mcpp -y + # MCPP_BOOT is what actually runs the bootstrap build below, so it — + # not just MCPP — has to be the pinned binary. It used to be the shim + # in subos/default/bin, which resolves to whatever version xvm has + # selected; pinning only MCPP would have looked right and changed + # nothing. + MCPP_BOOT=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") + echo "MCPP=$MCPP_BOOT" >> "$GITHUB_ENV" echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - echo "MCPP_BOOT=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" + echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" - name: Self-host build (bootstrap mcpp -> fresh host mcpp) run: | @@ -247,9 +253,15 @@ jobs: find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true xlings config --mirror GLOBAL 2>/dev/null || true xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - xlings install mcpp -y + # MCPP_BOOT is what actually runs the bootstrap build below, so it — + # not just MCPP — has to be the pinned binary. It used to be the shim + # in subos/default/bin, which resolves to whatever version xvm has + # selected; pinning only MCPP would have looked right and changed + # nothing. + MCPP_BOOT=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") + echo "MCPP=$MCPP_BOOT" >> "$GITHUB_ENV" echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - echo "MCPP_BOOT=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" + echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" - name: Self-host build (fresh host mcpp) run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bda4c578..9507f471 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -107,10 +107,10 @@ jobs: fi export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings --version - xlings install mcpp -y - MCPP="$HOME/.xlings/subos/default/bin/mcpp" - test -x "$MCPP" - "$MCPP" --version + # Pinned to .xlings.json — a bare `xlings install mcpp` resolves + # "newest in this runner's index copy" and put 0.0.105 (below the + # index floor) into this job. See .github/tools/install_pinned_mcpp.sh. + MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") echo "MCPP=$MCPP" >> "$GITHUB_ENV" echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" @@ -296,18 +296,22 @@ jobs: run: | xlings config --mirror GLOBAL 2>/dev/null || true xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - xlings install mcpp -y - mcpp --version + MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + "$MCPP" --version - name: Cross-build mcpp -> aarch64-linux-musl (this release's source) run: | export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - mcpp self config --mirror GLOBAL 2>/dev/null || true + # "$MCPP", not a bare `mcpp`: the bare form runs the PATH shim, which + # resolves to whatever version xvm has selected — so pinning the + # bootstrap would have looked correct and changed nothing here. + "$MCPP" self config --mirror GLOBAL 2>/dev/null || true # The published bootstrap mcpp predates aarch64 cross-build support # (the feature landed after the last release), so it resolves the # x86_64 host musl-gcc for an aarch64 target. Two-stage instead: build # THIS release's x86_64 mcpp first, then cross-build aarch64 with it. - mcpp build --target x86_64-linux-musl + "$MCPP" build --target x86_64-linux-musl FRESH=$(find target/x86_64-linux-musl -type f -name mcpp | head -1) test -n "$FRESH" "$FRESH" build --target aarch64-linux-musl @@ -429,10 +433,10 @@ jobs: fi export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings --version - xlings install mcpp -y - MCPP="$HOME/.xlings/subos/default/bin/mcpp" - test -x "$MCPP" - "$MCPP" --version + # Pinned to .xlings.json — a bare `xlings install mcpp` resolves + # "newest in this runner's index copy" and put 0.0.105 (below the + # index floor) into this job. See .github/tools/install_pinned_mcpp.sh. + MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") echo "MCPP=$MCPP" >> "$GITHUB_ENV" echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" @@ -601,6 +605,10 @@ jobs: XLINGS_NON_INTERACTIVE: '1' XLINGS_VERSION: '0.4.69' run: | + # Captured before the `cd` below, in POSIX form: this step never + # returns to the workspace, and GITHUB_WORKSPACE is a backslash + # Windows path that git-bash tools mangle. + REPO_DIR="$(pwd)" WORK=$(mktemp -d) zipfile="xlings-${XLINGS_VERSION}-windows-x86_64.zip" curl -fsSL -o "${WORK}/${zipfile}" \ @@ -611,13 +619,9 @@ jobs: export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" echo "$USERPROFILE/.xlings/subos/default/bin" >> "$GITHUB_PATH" xlings.exe --version - xlings.exe install mcpp -y - MCPP=$(find "$USERPROFILE/.xlings" -name "mcpp.exe" -path "*/bin/*" 2>/dev/null | head -1) - if [ -z "$MCPP" ]; then - MCPP=$(find "$USERPROFILE/.xlings" -name "mcpp" -path "*/bin/*" 2>/dev/null | head -1) - fi - test -n "$MCPP" || { echo "FAIL: mcpp not found after xlings install"; exit 1; } - "$MCPP" --version + # Pinned + version-scoped lookup. The old `find | head -1` returned + # whichever version the directory walk reached first. + MCPP=$(bash "$REPO_DIR/.github/tools/install_pinned_mcpp.sh" "$REPO_DIR") echo "MCPP=$MCPP" >> "$GITHUB_ENV" XLINGS_BIN=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") echo "XLINGS_BIN=$XLINGS_BIN" >> "$GITHUB_ENV"