fix: a bare dependency's wire address takes BOTH halves from its descriptor (0.0.109)#286
Merged
Merged
Conversation
Sunrisepeak
force-pushed
the
fix/bare-name-wire-address
branch
4 times, most recently
from
July 26, 2026 07:12
ffdba04 to
297cd0d
Compare
…riptor (0.0.109) main went 6-of-7 workflows red without a line of mcpp changing. The PR run for the very same commit was green 80 minutes earlier; in between, mcpp-index #120 migrated all 48 descriptors to SPEC-001 short names. The identity gate deliberately accepts a `compat` descriptor for a bare/default- namespace request — that is how `gtest = "1.15.2"` resolves to compat.gtest at all. But the wire address then took its NAME from that descriptor and its NAMESPACE from the request, emitting `mcpplibs:gtest`, which no index is keyed by. It only ever worked because the literal `package.name` used to read `compat.gtest`, so the hardcoded `compat.<short>` retry caught it. The short-name migration removed that coincidence and 34 of the index's 48 packages became uninstallable by bare name. So the main path was always broken and a legacy retry had been carrying it. This is a latent defect coming due, not a new regression. Evidence, from one CI job (same index, same xlings, same process): compat:zlib@1.3.2 OK <- [dependencies.compat] zlib compat:libarchive@3.8.7 OK mcpplibs:ftxui@6.1.9 FAIL <- bare [dependencies] ftxui compat.ftxui@6.1.9 FAIL Fix: `mcpp::manifest::xpkg_wire_address` derives ns and name together from the descriptor the gate accepted, and prepare.cppm uses it. Blast radius, checked against every descriptor in the live index: the 8 mcpplibs packages address exactly as before, the 6 under other namespaces are unreachable by bare name anyway, and the 34 compat ones — all currently broken — start working. A descriptor that cannot be read keeps the historical derivation byte for byte, so an unsynced index still fails where it used to. The compat retry now tries `compat:<short>` before the legacy `compat.<short>`, so the no-descriptor path works against both index generations. Two more failures in the same batch, same trigger: * e2e 163 built its fixture by sed'ing the literal `name = "chriskohlhoff.asio"` out of the real registry descriptor. #120 renamed it, the rewrite silently stopped matching, and the fixture carried the wrong identity — a hermetic- looking test that had upstream's exact bytes compiled into it. Rewrites now match by shape and hard-fail if they miss. * bootstrap-mcpp ran `xlings install mcpp -y` with no pin. `.xlings.json` fed only the cache key, and restore-keys prefix-matches, so jobs ran 0.0.107 and 0.0.102 against an index whose floor is 0.0.108. Warm caches hid the E0006; a cold one would not have. It now installs the pin and addresses that version by path rather than through a shim that can point elsewhere. Verification: * 9 unit cases over xpkg_wire_address covering every row of the blast-radius table, including the ones that must NOT change. * New e2e 165 reproduces the production bug hermetically: a bare dep served by a compat descriptor from a local fixture index. Run against a binary built without the fix it reports the exact production symptom (mcpplibs:widget then compat.widget); with the fix it passes. * End-to-end against the live migrated index: a bare `eigen = "5.0.1"` now resolves in one shot as `compat:eigen@5.0.1`, no fallback. Design + plan: .agents/docs/2026-07-26-bare-name-wire-address-{design,implementation-plan}.md
Sunrisepeak
force-pushed
the
fix/bare-name-wire-address
branch
from
July 26, 2026 07:17
297cd0d to
abc494b
Compare
Sunrisepeak
added a commit
that referenced
this pull request
Jul 26, 2026
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 — `<ver>/mcpp` on Linux, `<ver>/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.
Sunrisepeak
added a commit
that referenced
this pull request
Jul 26, 2026
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 — `<ver>/mcpp` on Linux, `<ver>/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.
Sunrisepeak
added a commit
that referenced
this pull request
Jul 26, 2026
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 — `<ver>/mcpp` on Linux, `<ver>/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.
Sunrisepeak
added a commit
that referenced
this pull request
Jul 26, 2026
ci: one pinned-mcpp bootstrap, not six drifting copies
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 — `<ver>/mcpp` on Linux, `<ver>/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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the 2026-07-25 breakage that left
mainwith 6 of 7 workflows red, plus the two other failures that landed in the same batch.What happened
Nothing in mcpp changed. The PR run for the very same commit was green 80 minutes earlier; in between, mcpp-index#120 migrated all 48 descriptors to SPEC-001 short names (
name = "compat.gtest"→name = "gtest").Root cause
The identity gate deliberately accepts a
compatdescriptor for a bare/default-namespace request — that is the only reasongtest = "1.15.2"resolves to compat.gtest at all:But the wire address then took its name from that descriptor and its namespace from the request:
mcpplibs:gtestis not a key any index has. It only ever worked because the literalpackage.nameused to readcompat.gtest, so the hardcodedcompat.<short>retry caught it. The migration removed that coincidence.The main path was always broken and a legacy retry had been carrying it. A latent defect coming due, not a new regression.
Evidence — one CI job, same index, same xlings, same process
Qualified spelling works; bare spelling does not. The correct address is
compat:ftxui@6.1.9and neither attempt was it.The fix
mcpp::manifest::xpkg_wire_addressderives namespace and name together from the descriptor the gate accepted. The rule: the descriptor the identity gate accepted supplies both halves of the address.Blast radius, checked against every descriptor in the live index:
namespace = "mcpplibs"namespace = "compat"mcpplibs:X→compat:X— all 34 currently brokenOnly the packages that are already broken change. A descriptor that cannot be read keeps the historical derivation byte for byte, so an unsynced index still fails exactly where it used to.
The compat retry now tries
compat:<short>before the legacycompat.<short>, so the no-descriptor path works against both index generations.Second instance of the same defect
mcpp new --templatebuilt its target the same way (<ns>.<short>@<ver>, a re-derived FQN) and is equally dead post-migration. Every case in e2e 69 runs off a pre-seeded install cache, so that path had zero coverage — the same shape of blind spot as the index side. Fixed with the same function, plus a sub-case that exercises it.The other two failures in the batch
e2e 163 built its fixture by sed'ing the literal
name = "chriskohlhoff.asio"out of the real registry descriptor. #120 renamed it, the rewrite silently stopped matching, and the fixture carried the wrong identity — a hermetic-looking test with upstream's exact bytes compiled into it. Rewrites now match by shape and hard-fail if they miss.Unpinned CI bootstraps. Three places bootstrapped mcpp with a bare
xlings install mcpp -y, which means "newest in whatever index copy this runner happens to have"..xlings.jsonfed only the cache key, andrestore-keysprefix-matches, so jobs ran 0.0.107, 0.0.102 and 0.0.105 against an index whose floor is 0.0.108. Warm caches hid theE0006; the moment one went cold, the floor check made every descriptor read return nothing and each dependency fell back to its legacy derived address. All three now install the pin from.xlings.json, address that version by its own path rather than through a shim that can point elsewhere, and assert the parsed version matches exactly.CI's sandbox xlings was 39 minor versions stale. The index's short-name migration put two packages named
luain one repo —compat:luaandmcpplibs.capi:lua, both pulled in transitively bymcpplibs.xpkg. Before xlings 0.4.69 (xlings#381) a repo keyed its table by the barepackage.name, so one of the two was simply unreachable, and which one depended on the machine: CI failed oncompat:luaon Windows andmcpplibs.capi:luaon Linux.release.ymlandcross-build-test.ymlhad already moved to 0.4.69 — which is exactly whycross-build-teststayed green while everybootstrap-mcppjob failed. The two composite actions were left behind at 0.4.30. Both are now on 0.4.69.The version also had to enter the cache lineage, not just the key. mcpp vendors xlings into
~/.mcpp/registry/binonce atself initand never revisits it (acquire_xlings_binaryreturns early when the file exists), so with the version only in the key,restore-keyswould hand a 0.4.30 sandbox to a 0.4.69 bootstrap — and the sandbox copy is the one that actually resolves dependencies. This costs one cold run.The bootstrap now also prints both the system and sandbox xlings versions. That divergence is invisible today, and it is what made this the hardest part of the diagnosis.
Why the index side was green
Every mcpp-index example spells the dependency
[dependencies.compat]+ a path index — the one form that was never broken. Real consumers, including mcpp's ownmcpp.toml, write it bare. Verification matrix and consumption matrix were not the same shape, so 8 green checks meant nothing. The new e2e builds its own index so it cannot drift, and asserts on the address mcpp sends, not merely on whether the install happened to work.Verification
xpkg_wire_addresscovering every row of the blast-radius table, including the ones that must not change.mcpplibs:widgetthencompat.widget; with the fix it passes.eigen = "5.0.1"now resolves in one shot ascompat:eigen@5.0.1, no fallback.Also surfaces the attempted wire addresses in the install-failure diagnostic — diagnosing this incident required
MCPP_VERBOSE=1to discover what mcpp had actually asked for, while the error named only the dependency, which is the one thing nobody doubts.Design + plan:
.agents/docs/2026-07-26-bare-name-wire-address-{design,implementation-plan}.mdFollow-ups (not in this PR)
Users hit the
luacollision too, silently. mcpp never refreshes the xlings it vendored into the sandbox and never checks whether it is new enough for the index it is talking to. Any sandbox created before 0.4.69 fails the same way — one arbitrary package "not found". Worth a version check with a real diagnostic.ci-fresh-install.ymlpinsMCPP_PIN: '0.0.103'with a comment saying to bump it at each release; it is five releases behind. Bumping with the.xlings.jsonpin post-release.The index URL is spelled
mcpp-community/mcpp-index.gitin 4 places andmcpplibs/mcpp-index.gitin 6; only GitHub's rename redirect keeps the former working. Leftover from the index: mcpplibs 索引 URL 仍指向已迁移的旧组织,且被 xlings 判定为自定义索引而强制走 git #267 org migration.mcpp-index: add a bare-name + remote-index example to close the blind spot.
mcpp-index:
chriskohlhoff.asio.lua:41-48still saysnameMUST be fully qualified; line 49 is nowname = "asio".imgui/ffmpegexist under bothmcpplibsandcompat; a bare request picks one by candidate-filename order. Pre-existing ambiguity, same family as descriptor spec 与安装目标口径不一致:package.name 写成 split 形式(namespace + 短名)的索引包无法被任何消费端安装,且 lint 静默放行 #278 — deserves its own issue.