feat(deps): multi-version mangling — Level 1 fallback#19
Merged
Sunrisepeak merged 1 commit intomainfrom May 9, 2026
Merged
Conversation
… coexistence When SemVer merge fails (typically two non-overlapping pins like =0.0.1 ⨯ =0.0.2 across two consumers of the same package), instead of hard-erroring the resolver now stages the secondary version's source under `target/.mangled/<consumer>/...` and rewrites its module declarations to a mangled name (`<X>__v<M>_<m>_<p>__mcpp`). The direct consumer's source is staged + rewritten too so its `import X;` points at the mangled secondary BMI. Both BMIs coexist in the build graph at distinct module names; C++23 module attachment isolates their internal symbols, so no namespace mangling is needed. Touches: - `src/pm/mangle.cppm` (new) — pure helper: name format + a regex-free source rewriter that handles `(export )?module X;`, `(export )?module X:Y;`, `(export )?import X;`, `import X:Y;`. Bare partition imports (`import :Y;`) and the global module fragment (`module ;`) are left intact. 11 new unit tests in `tests/unit/test_mangle.cpp`. - `src/cli.cppm` — resolver conflict branch: when `try_merge_semver` fails, run mangling instead of returning. WorkItem gains a `consumerDepIndex` field so the fallback can locate the consumer manifest for staging. Resolved record uses the mangled module name as `[package].name` so the modgraph validator's "module must be prefixed by package name" check passes. - `src/build/plan.cppm` + `ninja_backend.cppm` — when two compile units share a basename across packages (e.g. cmdline 0.0.1 and staged-mangled 0.0.2 both have `parse.cppm`), the colliding object paths get a per-package subdirectory (`obj/<sanitized-pkg>/<file>`). `.ddi` placement follows. Single-package builds keep the original `obj/<file>` layout so cache hashes don't churn. - `tests/e2e/33_multi_version_mangling.sh` (new) — Y-shape graph: libA pinned to cmdline 0.0.1, libB pinned to cmdline 0.0.2; the build succeeds with both BMIs (`mcpplibs.cmdline.gcm` + `mcpplibs.cmdline__v0_0_2__mcpp.gcm`) on disk and the runtime output matches. - `tests/e2e/32_semver_merge.sh` — drop the previously-irreconcilable case; that's now Level 1's territory and is covered by test 33. MVP scope (clear errors out of bounds): - Conflicting consumer must be a dep, not the main package. - Secondary version must be a leaf (no own transitive deps). Both surfaces are documented in the error message so users know which limit they hit and have a workaround (pin one version). CHANGELOG 0.0.3 entry: covers all three resolution levels (transitive walker / SemVer merge / mangling) and the obj-path-namespacing side-fix to the build backend.
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.
Summary
Layer 1 of the 0.0.3 dep-resolution work. Wraps up the three-step strategy on
mainalong with PR #17 (transitive walker) and PR #18 (SemVer merge).When the SemVer merger can't find a single version satisfying every consumer (typically
=0.0.1⨯=0.0.2or any genuine cross-major split), the resolver no longer hard-errors. Instead it stages the secondary version's source undertarget/.mangled/<consumer>/..., rewrites(export )?module X;/(export )?import X;to a mangled name (<X>__v<M>_<m>_<p>__mcpp), and the direct consumer is staged + rewritten too so itsimportpoints at the mangled BMI. Both BMIs coexist in the build graph at distinct module names; C++23 module attachment handles ABI isolation, so no namespace mangling is needed.Pieces
src/pm/mangle.cppm— pure helper (name format + regex-free source rewriter), 11 unit testscli.cppmresolver conflict branch — invokes mangling ontry_merge_semverfailure; tracksconsumerDepIndexper WorkItem; renames the staged secondary's[package].nameso the modgraph validator's prefix check passesbuild/plan.cppm+ninja_backend.cppm— basename-collision detection: when two compile units across packages share a filename (e.g. cmdline 0.0.1 and staged-mangled 0.0.2 both haveparse.cppm), colliding objects getobj/<sanitized-pkg>/<file>;.ddiplacement follows. Single-package builds keep the original layouttests/e2e/33_multi_version_mangling.sh— Y-shape: libA→cmdline 0.0.1, libB→cmdline 0.0.2; build succeeds with both BMIs on disktests/e2e/32_semver_merge.sh— irreconcilable case dropped (now handled by Level 1)MVP scope
Both limits surface as clear error messages with the workaround (pin one version explicitly). Recursive mangling for deeper graphs is a follow-up.
Test plan
tests/unit/test_mangle.cpp— 11 tests for name format + rewritertests/e2e/33_multi_version_mangling.sh(new)tests/e2e/32_semver_merge.sh(updated)tests/e2e/31_transitive_deps.shstill green