Skip to content

sandbox xlings is vendored once and never refreshed — an in-place mcpp upgrade keeps resolving deps with the old one #289

Description

@Sunrisepeak

The sandbox's xlings is copied once at self init and never revisited, so upgrading mcpp in place leaves the old one running dependency resolution. mcpp already knows which version it wants — it prints it — but never compares.

The gap, in three facts

1. mcpp knows the version it expects. src/xlings.cppm:39:

namespace pinned {
    // Keep in lock-step with the XLINGS_VERSION pins in release.yml /
    // cross-build-test.yml / ci-linux-e2e.yml (the xlings actually bundled
    // into releases). Printed by `mcpp self env`.
    inline constexpr std::string_view kXlingsVersion   = "0.4.69";
}

mcpp self env shows it:

MCPP_HOME           = /home/speak/.mcpp
xlings binary       = /home/speak/.mcpp/registry/bin/xlings
xlings pinned       = 0.4.69          <-- known

2. That constant is consumed in exactly one place — the diagnostic print. grep -rn kXlingsPinnedVersion src/ returns two hits: the definition and std::println in config.cppm:698. Nothing compares it to what is actually on disk, and doctor.cppm does not check the xlings version at all.

3. The acquisition point returns early. src/fallback/xlings_binary.cppm:22:

acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false) {
    if (std::filesystem::exists(destBin)) return destBin;   // <-- never revisited

It has one caller (config.cppm:634). So the copy made the first time ~/.mcpp/registry was created is the copy that stays, whatever mcpp version is running on top of it.

Why it matters now

MCPP_HOME defaults to the shared user sandbox, not the install directory — a freshly installed 0.0.109 still resolves ~/.mcpp/registry/bin/xlings, i.e. whatever was there before:

$ <fresh 0.0.109 install>/bin/mcpp self env
MCPP_HOME     = /home/speak/.mcpp
xlings binary = /home/speak/.mcpp/registry/bin/xlings

mcpp-index's SPEC-001 short-name migration put two packages literally named lua in one index repo — compat:lua and mcpplibs.capi:lua, both pulled in transitively via mcpplibs.xpkg. Before xlings 0.4.69 (openxlings/xlings#381) a repo keyed its table by the bare package.name, so one of the two is unreachable — and which one depends on iteration order. In CI this showed up as Windows failing on compat:lua while Linux failed on mcpplibs.capi:lua, with no message anywhere saying the xlings doing the resolving was too old:

error: xlings install_packages failed (exit 1) for 'lua@5.4.7' ...

CI was measured at 0.4.30 in the sandbox while the system xlings had already moved on — 39 minor versions of drift, invisible.

Who is affected: only sandboxes created before 0.4.69 and then upgraded in place. Release tarballs ship their own bundled xlings (<install>/registry/bin/xlings, 0.4.69 in 0.0.109), so a clean install is fine. Verified: fresh XLINGS_HOME → xlings install mcpp@0.0.109 → bundled xlings reports 0.4.69.

Proposed direction

Every mcpp release already ships a version-bound xlings at a stable relative path:

<install>/mcpp                    wrapper
<install>/bin/mcpp                real binary
<install>/registry/bin/xlings     bundled, version-bound

So at runtime mcpp can compare the sandbox copy against kXlingsPinnedVersion and, on mismatch, refresh it from its own bundled copy. This is not mcpp guessing at the user's environment — it is mcpp restoring an invariant it already declares and already knows the correct value for.

Sequencing suggestion, so the safe half can land without waiting on the design of the second:

  1. Detect + diagnose. Compare the sandbox xlings version to kXlingsPinnedVersion. On mismatch, emit an actionable error/warning naming both versions and pointing at the remedy (mcpp self init --force works today, it is just undiscoverable). Add the same check to mcpp doctor. Pure information, no writes.
  2. Refresh from the bundled copy. On mismatch, copy <install>/registry/bin/xlings over the sandbox copy. Open questions worth settling deliberately rather than in passing:
    • Is the binary alone sufficient, or does a 0.4.30 → 0.4.69 jump need the sandbox's registry/ runtime data reinitialised too? A binary/runtime mismatch inside the sandbox could be worse than a stale-but-consistent one.
    • Should it be silent, announced, or gated on a flag? Overwriting something in a user's ~/.mcpp without a word is the part I would not want to get wrong by default.
    • Downgrade direction: if the sandbox has a newer xlings than the running mcpp pins, overwriting is a regression.

Adjacent, same shape

kXlingsVersion's own comment says to keep it "in lock-step with the XLINGS_VERSION pins in release.yml / cross-build-test.yml / ci-linux-e2e.yml" — by hand, enforced nowhere. That list is already incomplete: .github/actions/bootstrap-mcpp and .github/actions/setup-macos-llvm also pin an xlings version (both were still on 0.4.30 until #287 today, which is what let CI's sandbox rot). A check that the constant and the workflow pins agree would stop the next silent divergence.

Context: found while shipping 0.0.109 (#286 / #287 / #288). Recorded as F1 in .agents/docs/2026-07-26-bare-name-wire-address-implementation-plan.md, which also lists five smaller release-ops findings from the same batch.

Activity

  1. speak-agent commented on Aug 8, 2026

    @speak-agent
    Member

    结论:本 issue 描述的三条事实今天都已不成立,关闭。残留的两个新缺口移交 #397。

    核验基线 main@80291ca / v2026.8.8.4。逐条对照本文的「三个事实」:

    事实 1「mcpp 知道自己期望的 xlings 版本」—— 仍然成立,只是数值漂了:src/xlings.cppm:47 现在是 kXlingsVersion = "2026.8.8.1"(本文写的 0.4.69)。

    事实 2「该常量只有一个消费点(诊断打印),没有任何东西拿它和磁盘上的实际版本比较,doctor 也不检查」—— 已不成立。 现有 4 个消费点:

    • src/config.cppm:36 — 别名
    • src/config.cppm:603-604 — 传进 acquire_xlings_binary 做比较
    • src/config.cppm:668 — 打印
    • src/doctor.cppm:405-418 — vendored_xlings_version() + version_is_older() 比对并告警

    事实 3「获取点 if (exists(destBin)) return destBin; 永不复查」—— 已不成立。 src/fallback/xlings_binary.cppm:55-92:

    if (std::filesystem::exists(destBin)) {
        auto have = vendored_xlings_version(destBin);
        if (pinnedVersion.empty() || have.empty() || !version_is_older(have, pinnedVersion))
            return destBin;
        auto candidate = candidate_source_version();
        if (candidate.empty() || !version_is_older(have, candidate)) { /* Note, keep */ }
        else { std::filesystem::remove(destBin, rec); /* 重新获取 */ }
    }

    替换前先给候选源定价这一步是有来历的 —— 注释 :66-69 记着第一版直接删了重装,把 2026.8.2.1 换成了系统的 0.4.51。

    落地于 fdad165(PR #378,2026.8.8.2)。实测(隔离 MCPP_HOME + 伪 xlings):落后且有更新源 ⇒ Updating vendored xlings 0.4.51 -> …,文件被替换;落后但源更旧 ⇒ 打 Note 保留;沙箱更新 ⇒ 静默不动,无降级。

    「Adjacent, same shape」那半条(pin 靠注释维持 lock-step,无人强制)也做了,而且比原提议强:.github/tools/check_version_pins.sh,由 .github/workflows/ci-linux.yml:56 在 bootstrap 之前运行。本地实跑 exit 0:OK: xlings pins all at 2026.8.8.1。它还顺手管住了 mcpp 自身版本的四处一致性。


    移交 #397 的两条(本轮核验新发现,不是本文报的)

    C-5 · Windows 上这套机制整体空转。 src/fallback/xlings_binary.cppm:160 把 2>/dev/null 硬编码进一条经 cmd.exe /c 执行的命令 ⇒ 返回空串 ⇒ acquire 退回旧的 early return、doctor 退化成 warn。本文点名的受害平台(Windows CI 上的 compat:lua)恰恰是修复不生效的那个平台。 mcpp::platform::null_redirect 就在 src/platform/common.cppm:37-41,一行改动。

    C-6 · 本文自己提的第 2 步(从自带副本刷新)没有实现。 candidate_source_version()(:214-224)只看 MCPP_VENDORED_XLINGS 和 which xlings;而 release wrapper(release.yml:351-354)不导出前者,src/home.cppm:92-99 又把 data/xpkgs/ 下的 mcpp 取消自包含资格。于是在「机器上的 xlings 也很旧」这个核心前提下,一份与 pin 逐字一致的 xlings 就躺在 <install>/registry/bin/xlings(实测 2026.8.8.4 包内为 xlings 2026.8.8.1)却不在候选链里。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions