Skip to content

fix: #278 package identity convergence — index side + dependency side (0.0.105)#279

Merged
Sunrisepeak merged 7 commits into
mainfrom
fix/issue278-package-identity-convergence
Jul 25, 2026
Merged

fix: #278 package identity convergence — index side + dependency side (0.0.105)#279
Sunrisepeak merged 7 commits into
mainfrom
fix/issue278-package-identity-convergence

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Fixes #278

The incident

mcpp-index changed chriskohlhoff.asio's name from "chriskohlhoff.asio" to "asio" (namespace untouched). Lint went green; all three platform workspace jobs died after 20–58 minutes with E_NOT_FOUND. The descriptor parsed, passed mcpp's own identity gate, and no consumer spelling could install it.

Root cause — one split, two sides

Index side. Identity normalization treats (ns="a", name="b") and (ns="a", name="a.b") as equivalent; install-target construction hard-assumes the latter. Verified against libxpkg: build_index is called with no namespace, so the index is a flat key space keyed by the literal package.name, looked up with exact entries.find(). mcpp addresses packages as <ns>.<short>. The two never meet.

Dependency side. The bare-name ladder's discovery rung was specified as "match by name across the precedence path" but implemented as "try two more filenames" — and a total miss fell through to candidates.front() silently, so mcpp continued with a namespace it had invented and the user met the failure much later, wrapped around that invented name.

Both contracts existed only as prose comments (prepare.cppm:1714, :1456, and the gate's "the caller derives the namespace"). They are now predicates.

What this changes

INV-NAME (index side)package.name must BE the fully-qualified name whenever package.namespace is declared. One predicate, two call sites: mcpp xpkg parse (index CI, seconds) and the install path (catches descriptors that never passed lint; zero extra I/O — the descriptor is already in memory). Lint and runtime cannot drift.

The predicate is deliberately narrow. Written as the general "literal name != derived fqname" comparison it would flag the working compat-alias path — a bare gtest request derives gtest while the literal is compat.gtest — and break every bare dependency. CompatAliasIsClean locks that.

INV-RESOLVE (dependency side) — a bare name resolves in exactly three places: mcpplibs, compat, no-namespace upstream. Everything else must be written out.

Global short-name search was considered and rejected: it makes resolution depend on which indices happen to be present, so adding an index could silently retarget an existing dependency; same-short-name collisions would be settled by an index ordering that has no total order across user [indices]. Reproducibility beats convenience.

The index-wide scan survives only as a did-you-mean on the already-failed path — result goes into error text, never back into resolution (three constraints documented at Fetcher::scan_fqns_with_short_name).

Generator fixmcpp emit xpkg wrote a bare name and no namespace at all, so filing a package into a namespaced index meant hand-adding namespace = "<org>", and that edit is what created the split form. aimol.tensorvia-cpu was born exactly this way (still carries the "AUTO-GENERATED, do not edit by hand" banner). Prescribed in the 2026-06-26 design §4.5; never landed until now.

⚠️ Breaking

Bare names no longer reach third-party namespaces. Migration needs no docs — the failure prints the two lines to write:

error: dependency 'asio': no package found under the namespaces mcpp searched
  tried: (mcpplibs, asio), (no namespace, asio)
  a package with this name exists under another namespace:
    chriskohlhoff.asio
  bare names only resolve to the `mcpplibs` / `compat` namespaces. write it out:
    [dependencies]
    "chriskohlhoff.asio" = "1.38.1"
  or:
    [dependencies.chriskohlhoff]
    asio = "1.38.1"

Verified: the suggested spelling downloads and compiles asio end-to-end.

Scope correction found during implementation

The design's "2 violations in 62 descriptors" figure covered only the mcpplibs index. Running the predicate over the full registry (200+) surfaced 30 split-form descriptors — and all 28 extra ones are legitimate: in xlings-native indices (xim-pkgindex, -scode) package.namespace is an install-dir category (config, scode, awesome), the index is keyed by the bare name, and the split spelling is correct there. All 30 lack an mcpp segment and never reach mcpp's dependency path.

Hence mcpp xpkg parse --allow-split-name for xlings-native trees. No xim repo runs mcpp xpkg parse today (checked their .github/), and mcpp-index's CI does — so enforce-by-default is right.

Verification

  • Unit — 8 new XpkgNameForm cases incl. the CompatAliasIsClean regression lock; 37/37 unit binaries green.
  • e2e 161 — lint rejects split form with the exact fix line, --json carries it, --allow-split-name opts out, runtime fail-fast fires before any download.
  • e2e 162 — bare third-party name fails with did-you-mean; both documented spellings resolve the same package; bare gtestcompat.gtest still works.
  • Full-index regression — all 49/49 mcpplibs descriptors parse clean, 0 INV-NAME violations (zero false positives).
  • Live check — bare gtest builds; "chriskohlhoff.asio" downloads + compiles.

Ecosystem

mcpp-index already carries both fixes upstream (chriskohlhoff.asio and aimol.tensorvia-cpu are FQN; #116's check_package_name.lua landed) — that guard is now redundant with mcpp xpkg parse but harmless.

Design: .agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md
Plan: .agents/docs/2026-07-25-issue278-implementation-plan.md

Design/proposal doc for entering the embedded Linux ecosystem, restructured
as decisions-first (with rationale) then supporting detail.

Positioning: mcpp brings its own matched-libc cross compiler (latest GCC) and
consumes an external device sysroot — it is NOT a distro builder and never
adapts per-vendor SDK. The single core change is opening the sysroot seam (L2);
the CLibMode::Sysroot mechanism and the hermetic allow-list already accept
tc.sysroot, so it's a minimal insertion. pkg-config and qemu cross test-run are
xlings provisioning + thin wiring.

Locks the maintainer decision register (16 decisions + rationale), incl.:
- C++ source-only; binary interop C-ABI only + wrapper; vendor C++-only libs via
  a user-built C-bridge .so (built in vendor ABI, pure-C boundary)
- glibc baseline pinned low when building payloads (GCC version x glibc decoupled);
  payloads use the latest GCC (16), GCC 15 is only the import-std capability floor
- dropped: L4 backend mode, sysroot pinning/mgmt, .pc synthesis, and the
  "compiler doesn't support C++23" diagnostic (config II makes that case
  architecturally impossible)

Beachhead: Raspberry Pi aarch64. Net work: P0 sysroot seam + matched-libc
low-glibc GCC 16 payloads (+ one glibc-floor validation); P1 pkg-config + qemu
test-run.

Supersedes the earlier issue-276 analysis note (renamed to a proper design doc).
… (0.0.105)

mcpp-index changed `chriskohlhoff.asio`'s `name` from "chriskohlhoff.asio" to
"asio" (namespace untouched). Lint went green; all three platform workspace
jobs died after 20-58 minutes with E_NOT_FOUND. The descriptor parsed, passed
mcpp's own identity gate, and no consumer spelling could install it.

Root cause is a split in how package identity is spoken on the two sides:

  index side  — identity normalization treats (ns="a", name="b") and
                (ns="a", name="a.b") as equivalent, while install-target
                construction hard-assumes the latter. The index is a FLAT key
                space keyed by the LITERAL package.name (libxpkg: build_index
                is called with no namespace, lookup is exact entries.find),
                so the two never meet.

  dependency side — the bare-name candidate ladder's discovery rung was
                specified as "match by name across the precedence path" but
                implemented as "try two more filenames", and a total miss fell
                through to candidates.front() SILENTLY, so mcpp continued with
                a namespace it had invented.

Both contracts existed only as prose comments. They are now predicates.

INV-NAME (index side): package.name must BE the fully-qualified name whenever
package.namespace is declared. One predicate, two call sites — `mcpp xpkg
parse` (index CI, seconds) and the install path (catches descriptors that
never passed through lint, zero extra I/O since the descriptor is already in
memory). The predicate is deliberately narrow: written as the general
"literal != derived fqname" comparison it would flag the working compat-alias
path and break every bare dependency. CompatAliasIsClean locks that.

INV-RESOLVE (dependency side): a bare name resolves in exactly three places —
mcpplibs, compat, and no-namespace upstream packages. Everything else must be
written out. Global short-name search is rejected: it makes resolution depend
on which indices happen to be present, so adding an index could silently
retarget an existing dependency.

BREAKING: bare names no longer reach third-party namespaces. The index-wide
scan survives only as a did-you-mean on the already-failed path — result goes
into error text, never back into resolution.

Also fixes the generator that produced these descriptors: `mcpp emit xpkg`
wrote a bare name and no namespace at all, so filing a package into a
namespaced index meant hand-adding `namespace = "<org>"` — and that edit is
what created the split form. aimol.tensorvia-cpu was born exactly this way.
Prescribed in 2026-06-26 design §4.5; never landed until now.

Design: .agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md

Fixes #278
A custom git index is cloned by xlings during install, so at candidate-
selection time its descriptors may legitimately not be on disk yet.
"Not found" is not conclusive there, and turning it into a hard error
would reject a package that materializes a moment later. Builtin and
local path indices are both readable at selection time, so they keep
the strict rule.
mcpp.toml and src/toolchain/fingerprint.cppm are two independent version
sources; bumping only the manifest left `mcpp --version` reporting the
old number, which 01_help_and_version.sh cross-checks against mcpp.toml.
@Sunrisepeak
Sunrisepeak merged commit bac3519 into main Jul 25, 2026
15 checks passed
@Sunrisepeak
Sunrisepeak deleted the fix/issue278-package-identity-convergence branch July 25, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant