@@ -32,6 +32,7 @@ import mcpp.config;
3232import mcpp.fetcher;
3333import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm
3434import mcpp.pm.commands; // PR-R5: cmd_add / cmd_remove / cmd_update live here now
35+ import mcpp.pm.mangle; // Level 1 multi-version fallback (cross-major coexistence)
3536import mcpp.ui;
3637import mcpp.bmi_cache;
3738import mcpp.dyndep;
@@ -1062,11 +1063,15 @@ prepare_build(bool print_fingerprint,
10621063 };
10631064 std::map<ResolvedKey, ResolvedRecord> resolved;
10641065
1066+ // Sentinel for "the consumer is the main package" (no dep_manifests entry).
1067+ constexpr std::size_t kMainConsumer = static_cast <std::size_t >(-1 );
1068+
10651069 struct WorkItem {
10661070 std::string name; // dep map key as written
10671071 mcpp::manifest::DependencySpec spec; // copy (we may mutate version)
10681072 std::string requestedBy; // who asked for it
10691073 std::string originalConstraint; // spec.version BEFORE pinning (for SemVer merge)
1074+ std::size_t consumerDepIndex; // dep_manifests slot of who pushed this child; kMainConsumer for main
10701075 };
10711076 std::deque<WorkItem> worklist;
10721077
@@ -1212,15 +1217,76 @@ prepare_build(bool print_fingerprint,
12121217 }
12131218 };
12141219
1220+ // Stage a dep's source files into a fresh directory, rewriting their
1221+ // module / import declarations against `rename`. Used by the multi-
1222+ // version mangling fallback (Level 1) so two cross-major copies of
1223+ // the same package can coexist with distinct module names.
1224+ //
1225+ // Headers (referenced via `[build].include_dirs`) are NOT staged —
1226+ // those keep pointing at the original install dir via absolutized
1227+ // include paths.
1228+ auto stage_with_rewrite = [](const std::filesystem::path& srcRoot,
1229+ const std::filesystem::path& dstRoot,
1230+ const mcpp::manifest::Manifest& depManifest,
1231+ const std::map<std::string, std::string>& rename)
1232+ -> std::expected<void , std::string>
1233+ {
1234+ std::error_code ec;
1235+ std::filesystem::create_directories (dstRoot, ec);
1236+ if (ec) return std::unexpected (std::format (
1237+ " stage: cannot create '{}': {}" , dstRoot.string (), ec.message ()));
1238+
1239+ // Resolve the source globs against the original root, falling
1240+ // back to the convention default if the manifest didn't set any.
1241+ std::vector<std::string> globs = depManifest.modules .sources ;
1242+ if (globs.empty ()) {
1243+ globs = { " src/**/*.cppm" , " src/**/*.cpp" ,
1244+ " src/**/*.cc" , " src/**/*.c" };
1245+ }
1246+ std::set<std::filesystem::path> sourceFiles;
1247+ for (auto const & g : globs) {
1248+ for (auto & p : mcpp::modgraph::expand_glob (srcRoot, g)) {
1249+ sourceFiles.insert (p);
1250+ }
1251+ }
1252+ if (sourceFiles.empty ()) {
1253+ return std::unexpected (std::format (
1254+ " stage: no source files found under '{}' (globs={})" ,
1255+ srcRoot.string (), globs.size ()));
1256+ }
1257+
1258+ for (auto const & f : sourceFiles) {
1259+ auto rel = std::filesystem::relative (f, srcRoot, ec);
1260+ if (ec) return std::unexpected (std::format (
1261+ " stage: cannot relativize '{}': {}" , f.string (), ec.message ()));
1262+ auto dst = dstRoot / rel;
1263+ std::filesystem::create_directories (dst.parent_path (), ec);
1264+
1265+ std::ifstream is (f);
1266+ if (!is) return std::unexpected (std::format (
1267+ " stage: cannot read '{}'" , f.string ()));
1268+ std::stringstream buf; buf << is.rdbuf ();
1269+ std::string content = buf.str ();
1270+
1271+ std::string out = mcpp::pm::rewrite_module_decls (content, rename);
1272+ std::ofstream os (dst);
1273+ if (!os) return std::unexpected (std::format (
1274+ " stage: cannot write '{}'" , dst.string ()));
1275+ os << out;
1276+ }
1277+ return {};
1278+ };
1279+
12151280 // Seed the worklist from the main manifest. Dev-deps only when the
12161281 // caller wants them; they're never propagated transitively.
12171282 const std::string mainPkgLabel = m->package .name ;
12181283 for (auto & [n, s] : m->dependencies ) {
1219- worklist.push_back ({n, s, mainPkgLabel, s.version });
1284+ worklist.push_back ({n, s, mainPkgLabel, s.version , kMainConsumer });
12201285 }
12211286 if (includeDevDeps) {
12221287 for (auto & [n, s] : m->devDependencies ) {
1223- worklist.push_back ({n, s, mainPkgLabel + " (dev-dep)" , s.version });
1288+ worklist.push_back ({n, s, mainPkgLabel + " (dev-dep)" ,
1289+ s.version , kMainConsumer });
12241290 }
12251291 }
12261292
@@ -1274,21 +1340,124 @@ prepare_build(bool print_fingerprint,
12741340 item.originalConstraint ,
12751341 fetcher);
12761342 if (!merged) {
1277- return std::unexpected (std::format (
1278- " dependency '{}{}{}' has irreconcilable versions in "
1279- " the transitive graph:\n "
1280- " '{}' (constraint '{}') requested by '{}'\n "
1281- " '{}' (constraint '{}') requested by '{}'\n "
1282- " SemVer merge: {}\n "
1283- " C++ modules require a single global version of each "
1284- " package; pick a version compatible with both "
1285- " consumers, or ask one upstream to widen its dep "
1286- " range. (cross-major fallback via multi-version "
1287- " mangling is planned in a follow-up PR)" ,
1288- key.ns , key.ns .empty () ? " " : " ." , key.shortName ,
1289- it->second .version , it->second .constraint , it->second .requestedBy ,
1290- spec.version , item.originalConstraint , item.requestedBy ,
1291- merged.error ()));
1343+ // Level 1 fallback: multi-version mangling. Two
1344+ // versions can't be reconciled by SemVer, but they
1345+ // can coexist in the same build if we mangle the
1346+ // secondary copy's module name and rewrite the one
1347+ // consumer that asked for it. The primary keeps its
1348+ // authored module name so consumers that don't care
1349+ // about the secondary see no churn.
1350+ //
1351+ // MVP scope (these limits surface as clear errors):
1352+ // * The conflicting consumer must be a dep, not
1353+ // the main package — main-package mangling
1354+ // would mean rewriting user-authored sources,
1355+ // which is too surprising for a fallback path.
1356+ // * The secondary version must be a leaf (no own
1357+ // transitive deps) — recursive mangling is
1358+ // deferred to a follow-up.
1359+ if (item.consumerDepIndex == kMainConsumer ) {
1360+ return std::unexpected (std::format (
1361+ " dependency '{}{}{}' has irreconcilable versions:\n "
1362+ " '{}' (constraint '{}') requested by '{}'\n "
1363+ " '{}' (constraint '{}') requested by '{}'\n "
1364+ " SemVer merge: {}\n "
1365+ " Multi-version mangling can't help here — the conflict "
1366+ " involves the main package directly. Pin one version "
1367+ " explicitly in your mcpp.toml." ,
1368+ key.ns , key.ns .empty () ? " " : " ." , key.shortName ,
1369+ it->second .version , it->second .constraint , it->second .requestedBy ,
1370+ spec.version , item.originalConstraint , item.requestedBy ,
1371+ merged.error ()));
1372+ }
1373+
1374+ auto loaded = loadVersionDep (name, spec.version );
1375+ if (!loaded) return std::unexpected (loaded.error ());
1376+ auto & [secondaryRoot, secondaryManifest] = *loaded;
1377+
1378+ if (!secondaryManifest.dependencies .empty ()) {
1379+ return std::unexpected (std::format (
1380+ " dependency '{}{}{}' has irreconcilable versions:\n "
1381+ " '{}' requested by '{}'\n "
1382+ " '{}' requested by '{}'\n "
1383+ " Multi-version mangling fallback only handles leaf "
1384+ " secondaries in 0.0.3 — but the secondary v{} declares "
1385+ " its own dependencies, which would need recursive "
1386+ " mangling. Pin one version explicitly, or wait for "
1387+ " the recursive-mangling extension." ,
1388+ key.ns , key.ns .empty () ? " " : " ." , key.shortName ,
1389+ it->second .version , it->second .requestedBy ,
1390+ spec.version , item.requestedBy ,
1391+ spec.version ));
1392+ }
1393+
1394+ // Module names in the source files use the dep's full
1395+ // [package].name (e.g. "mcpplibs.cmdline"), not the
1396+ // namespaced-subtable shortName. Use that for the
1397+ // rename key so the rewriter actually matches what the
1398+ // .cppm sources declare.
1399+ const std::string moduleName = secondaryManifest.package .name ;
1400+ std::string mangled =
1401+ mcpp::pm::mangle_name (moduleName, spec.version );
1402+
1403+ // Stage layout:
1404+ // <root>/target/.mangled/<consumerPkg>/<dep>__<version>/ ← rewritten secondary source
1405+ // <root>/target/.mangled/<consumerPkg>/__self__/ ← rewritten consumer source
1406+ auto & consumerManifest = *dep_manifests[item.consumerDepIndex ];
1407+ auto consumerRoot = packages[item.consumerDepIndex + 1 ].root ;
1408+ auto stageBase = *root / " target" / " .mangled"
1409+ / consumerManifest.package .name ;
1410+ auto secStage = stageBase
1411+ / std::format (" {}__{}" , moduleName, spec.version );
1412+ auto consumerStage = stageBase / " __self__" ;
1413+
1414+ std::map<std::string, std::string> rename{ {moduleName, mangled} };
1415+ if (auto r = stage_with_rewrite (secondaryRoot, secStage,
1416+ secondaryManifest, rename); !r)
1417+ return std::unexpected (r.error ());
1418+ if (auto r = stage_with_rewrite (consumerRoot, consumerStage,
1419+ consumerManifest, rename); !r)
1420+ return std::unexpected (r.error ());
1421+
1422+ // Re-anchor the consumer's PackageRoot at its staged copy
1423+ // so the modgraph scanner picks up the rewritten imports.
1424+ packages[item.consumerDepIndex + 1 ].root = consumerStage;
1425+
1426+ // Record the staged secondary as a brand-new dep entry
1427+ // under its mangled name, so future encounters of this
1428+ // exact (ns, mangled) pair dedup cleanly. The original
1429+ // primary entry (it->second) is untouched.
1430+ auto stagedManifest = secondaryManifest;
1431+ // Update [package].name to the mangled module name so
1432+ // the modgraph validator (which checks "exported module
1433+ // must be prefixed by package name") accepts the
1434+ // rewritten sources.
1435+ stagedManifest.package .name = mangled;
1436+ // Absolutize secondary's include_dirs against its original
1437+ // install root so the staged copy still finds headers.
1438+ for (auto & inc : stagedManifest.buildConfig .includeDirs ) {
1439+ if (inc.is_relative ()) inc = secondaryRoot / inc;
1440+ }
1441+
1442+ dep_manifests.push_back (
1443+ std::make_unique<mcpp::manifest::Manifest>(std::move (stagedManifest)));
1444+ packages.push_back ({secStage, *dep_manifests.back ()});
1445+ auto added = propagateIncludeDirs (secStage, *dep_manifests.back ());
1446+
1447+ ResolvedKey mangledKey{key.ns , mangled};
1448+ resolved[mangledKey] = ResolvedRecord{
1449+ .version = spec.version ,
1450+ .constraint = item.originalConstraint ,
1451+ .requestedBy = item.requestedBy ,
1452+ .source = " version" ,
1453+ .depIndex = dep_manifests.size () - 1 ,
1454+ .includeDirsAdded = std::move (added),
1455+ };
1456+
1457+ mcpp::ui::info (" Mangled" ,
1458+ std::format (" {} v{} ↔ v{} → {} (cross-major fallback)" ,
1459+ moduleName, it->second .version , spec.version , mangled));
1460+ continue ;
12921461 }
12931462
12941463 // Combine the constraint strings so future merges AND with
@@ -1365,7 +1534,8 @@ prepare_build(bool print_fingerprint,
13651534 for (auto & [child_name, child_spec] :
13661535 dep_manifests[it->second .depIndex ]->dependencies ) {
13671536 worklist.push_back ({child_name, child_spec, newLabel,
1368- child_spec.version });
1537+ child_spec.version ,
1538+ it->second .depIndex });
13691539 }
13701540 continue ;
13711541 }
@@ -1514,9 +1684,10 @@ prepare_build(bool print_fingerprint,
15141684 key.ns .empty () ? " " : " ." ,
15151685 key.shortName ,
15161686 sourceKind == " version" ? spec.version : sourceKind);
1687+ const std::size_t selfIdx = dep_manifests.size () - 1 ;
15171688 for (auto & [child_name, child_spec] : dep_manifests.back ()->dependencies ) {
15181689 worklist.push_back ({child_name, child_spec, thisDepLabel,
1519- child_spec.version });
1690+ child_spec.version , selfIdx });
15201691 }
15211692 }
15221693
0 commit comments