Skip to content

Commit 4d8d080

Browse files
committed
fix(pm): derive mangling from authored modules
1 parent 1ef3112 commit 4d8d080

3 files changed

Lines changed: 124 additions & 37 deletions

File tree

src/build/prepare.cppm

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3294,25 +3294,11 @@ prepare_build(bool print_fingerprint,
32943294
}
32953295
};
32963296

3297-
// Stage a dep's source files into a fresh directory, rewriting their
3298-
// module / import declarations against `rename`. Used by the multi-
3299-
// version mangling fallback (Level 1) so two cross-major copies of
3300-
// the same package can coexist with distinct module names.
3301-
//
3302-
// Headers (referenced via `[build].include_dirs`) are NOT staged —
3303-
// those keep pointing at the original install dir via absolutized
3304-
// include paths.
3305-
auto stage_with_rewrite = [](const std::filesystem::path& srcRoot,
3306-
const std::filesystem::path& dstRoot,
3307-
const mcpp::manifest::Manifest& depManifest,
3308-
const std::map<std::string, std::string>& rename)
3309-
-> std::expected<void, std::string>
3297+
auto package_source_files = [](
3298+
const std::filesystem::path& srcRoot,
3299+
const mcpp::manifest::Manifest& depManifest)
3300+
-> std::expected<std::set<std::filesystem::path>, std::string>
33103301
{
3311-
std::error_code ec;
3312-
std::filesystem::create_directories(dstRoot, ec);
3313-
if (ec) return std::unexpected(std::format(
3314-
"stage: cannot create '{}': {}", dstRoot.string(), ec.message()));
3315-
33163302
// Resolve the source globs against the original root, falling
33173303
// back to the convention default if the manifest didn't set any.
33183304
std::vector<std::string> globs = depManifest.modules.sources;
@@ -3338,8 +3324,32 @@ prepare_build(bool print_fingerprint,
33383324
"stage: no source files found under '{}' (globs={})",
33393325
srcRoot.string(), globs.size()));
33403326
}
3327+
return sourceFiles;
3328+
};
33413329

3342-
for (auto const& f : sourceFiles) {
3330+
// Stage a dep's source files into a fresh directory, rewriting their
3331+
// module / import declarations against `rename`. Used by the multi-
3332+
// version mangling fallback (Level 1) so two cross-major copies of
3333+
// the same package can coexist with distinct module names.
3334+
//
3335+
// Headers (referenced via `[build].include_dirs`) are NOT staged —
3336+
// those keep pointing at the original install dir via absolutized
3337+
// include paths.
3338+
auto stage_with_rewrite = [&](const std::filesystem::path& srcRoot,
3339+
const std::filesystem::path& dstRoot,
3340+
const mcpp::manifest::Manifest& depManifest,
3341+
const std::map<std::string, std::string>& rename)
3342+
-> std::expected<void, std::string>
3343+
{
3344+
std::error_code ec;
3345+
std::filesystem::create_directories(dstRoot, ec);
3346+
if (ec) return std::unexpected(std::format(
3347+
"stage: cannot create '{}': {}", dstRoot.string(), ec.message()));
3348+
3349+
auto sources = package_source_files(srcRoot, depManifest);
3350+
if (!sources) return std::unexpected(sources.error());
3351+
3352+
for (auto const& f : *sources) {
33433353
auto rel = std::filesystem::relative(f, srcRoot, ec);
33443354
if (ec) return std::unexpected(std::format(
33453355
"stage: cannot relativize '{}': {}", f.string(), ec.message()));
@@ -3361,6 +3371,29 @@ prepare_build(bool print_fingerprint,
33613371
return {};
33623372
};
33633373

3374+
auto declared_modules_for = [&](const std::filesystem::path& srcRoot,
3375+
const mcpp::manifest::Manifest& depManifest)
3376+
-> std::expected<std::vector<std::string>, std::string>
3377+
{
3378+
auto sources = package_source_files(srcRoot, depManifest);
3379+
if (!sources) return std::unexpected(sources.error());
3380+
std::vector<std::string> modules;
3381+
for (auto const& file : *sources) {
3382+
std::ifstream is(file);
3383+
if (!is) return std::unexpected(std::format(
3384+
"mangle: cannot read '{}'", file.string()));
3385+
std::stringstream buf; buf << is.rdbuf();
3386+
for (auto& name : mcpp::pm::declared_module_roots(buf.str())) {
3387+
if (std::ranges::find(modules, name) == modules.end())
3388+
modules.push_back(std::move(name));
3389+
}
3390+
}
3391+
if (modules.empty()) return std::unexpected(std::format(
3392+
"mangle: package '{}' declares no named C++ module to rewrite",
3393+
depManifest.package.name));
3394+
return modules;
3395+
};
3396+
33643397
// Stage 2a — feature-activated optional dependencies. Defined as local
33653398
// lambdas (NOT file-scope functions): keeping their std::map instantiations
33663399
// inside this implementation unit avoids polluting the exported module BMI,
@@ -3612,14 +3645,22 @@ prepare_build(bool print_fingerprint,
36123645
spec.version));
36133646
}
36143647

3615-
// Module names in the source files use the dep's full
3616-
// [package].name (e.g. "mcpplibs.cmdline"), not the
3617-
// namespaced-subtable shortName. Use that for the
3618-
// rename key so the rewriter actually matches what the
3619-
// .cppm sources declare.
3620-
const std::string moduleName = secondaryManifest.package.name;
3621-
std::string mangled =
3622-
mcpp::pm::mangle_name(moduleName, spec.version);
3648+
// Module names are authored API and are not required to
3649+
// mirror package identity. Discover every provided module
3650+
// root from the secondary's source text, then rewrite the
3651+
// same map in both the secondary and its consumer.
3652+
auto moduleNames = declared_modules_for(
3653+
secondaryRoot, secondaryManifest);
3654+
if (!moduleNames) return std::unexpected(moduleNames.error());
3655+
std::map<std::string, std::string> rename;
3656+
for (auto const& module : *moduleNames) {
3657+
rename.emplace(module,
3658+
mcpp::pm::mangle_name(module, spec.version));
3659+
}
3660+
const auto& moduleName = moduleNames->front();
3661+
const auto& mangledModule = rename.at(moduleName);
3662+
const std::string mangledPackage = mcpp::pm::mangle_name(
3663+
key.shortName, spec.version);
36233664

36243665
// Stage layout:
36253666
// <root>/target/.mangled/<consumerPkg>/<dep>__<version>/ ← rewritten secondary source
@@ -3629,10 +3670,9 @@ prepare_build(bool print_fingerprint,
36293670
auto stageBase = *root / "target" / ".mangled"
36303671
/ consumerManifest.package.name;
36313672
auto secStage = stageBase
3632-
/ std::format("{}__{}", moduleName, spec.version);
3673+
/ std::format("{}__{}", key.shortName, spec.version);
36333674
auto consumerStage = stageBase / "__self__";
36343675

3635-
std::map<std::string, std::string> rename{ {moduleName, mangled} };
36363676
if (auto r = stage_with_rewrite(secondaryRoot, secStage,
36373677
secondaryManifest, rename); !r)
36383678
return std::unexpected(r.error());
@@ -3649,11 +3689,10 @@ prepare_build(bool print_fingerprint,
36493689
// exact (ns, mangled) pair dedup cleanly. The original
36503690
// primary entry (it->second) is untouched.
36513691
auto stagedManifest = secondaryManifest;
3652-
// Update [package].name to the mangled module name so
3653-
// the modgraph validator (which checks "exported module
3654-
// must be prefixed by package name") accepts the
3655-
// rewritten sources.
3656-
stagedManifest.package.name = mangled;
3692+
// Give the staged package a distinct atomic identity too;
3693+
// authored module names remain independent and are carried
3694+
// exclusively by the rename map above.
3695+
stagedManifest.package.name = mangledPackage;
36573696
if (stagedManifest.package.namespace_.empty()) {
36583697
stagedManifest.package.namespace_ = key.ns.empty()
36593698
? std::string(mcpp::pm::kDefaultNamespace) : key.ns;
@@ -3673,7 +3712,7 @@ prepare_build(bool print_fingerprint,
36733712
std::make_unique<mcpp::manifest::Manifest>(std::move(stagedManifest)));
36743713
dep_cache_identities.push_back({
36753714
.indexName = cache_index_name(key.ns),
3676-
.packageName = mangled,
3715+
.packageName = mangledPackage,
36773716
.version = spec.version,
36783717
.sourceKind = "version",
36793718
});
@@ -3682,7 +3721,7 @@ prepare_build(bool print_fingerprint,
36823721
recordDependencyEdge(item.consumerDepIndex, depPackageIndex, spec);
36833722
auto linkFlagsAdded = propagateLinkFlags(secStage, *dep_manifests.back());
36843723

3685-
ResolvedKey mangledKey{key.ns, mangled};
3724+
ResolvedKey mangledKey{key.ns, mangledPackage};
36863725
resolved[mangledKey] = ResolvedRecord{
36873726
.version = spec.version,
36883727
.constraint = item.originalConstraint,
@@ -3695,7 +3734,8 @@ prepare_build(bool print_fingerprint,
36953734

36963735
mcpp::ui::info("Mangled",
36973736
std::format("{} v{} ↔ v{} → {} (cross-major fallback)",
3698-
moduleName, it->second.version, spec.version, mangled));
3737+
moduleName, it->second.version, spec.version,
3738+
mangledModule));
36993739
continue;
37003740
}
37013741

src/pm/mangle.cppm

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export namespace mcpp::pm {
3434
// confuse partition-style readings later).
3535
std::string mangle_name(std::string_view base, std::string_view version);
3636

37+
// Primary module roots declared by source text, in first-seen order. Partition
38+
// declarations return their owning root; imports, global module fragments and
39+
// bare partition declarations are ignored.
40+
std::vector<std::string> declared_module_roots(std::string_view source);
41+
3742
// Rewrite a single .cppm file's module / import declarations:
3843
// * `(export )?module N;` → `(export )?module rename[N];`
3944
// * `(export )?module N:P;` → `(export )?module rename[N]:P;`
@@ -106,6 +111,36 @@ read_name(std::string_view s, std::size_t i)
106111

107112
} // namespace
108113

114+
std::vector<std::string> declared_module_roots(std::string_view source) {
115+
std::vector<std::string> roots;
116+
std::size_t lineStart = 0;
117+
while (lineStart < source.size()) {
118+
auto eol = source.find('\n', lineStart);
119+
if (eol == std::string_view::npos) eol = source.size();
120+
auto line = source.substr(lineStart, eol - lineStart);
121+
122+
auto cur = skip_ws(line, 0);
123+
if (auto p = consume_keyword(line, cur, "export");
124+
p != std::string::npos) {
125+
cur = skip_ws(line, p);
126+
}
127+
auto afterModule = consume_keyword(line, cur, "module");
128+
if (afterModule != std::string::npos) {
129+
cur = skip_ws(line, afterModule);
130+
if (cur < line.size() && line[cur] != ';' && line[cur] != ':') {
131+
auto [nameEnd, name] = read_name(line, cur);
132+
if (nameEnd != std::string::npos
133+
&& std::ranges::find(roots, name) == roots.end()) {
134+
roots.emplace_back(name);
135+
}
136+
}
137+
}
138+
if (eol == source.size()) break;
139+
lineStart = eol + 1;
140+
}
141+
return roots;
142+
}
143+
109144
std::string rewrite_module_decls(
110145
std::string_view source,
111146
const std::map<std::string, std::string>& rename)

tests/unit/test_mangle.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ TEST(Mangle, NameFormat) {
1111
EXPECT_EQ(mangle_name("a", "0"), "a__v0__mcpp");
1212
}
1313

14+
TEST(Mangle, DiscoversAuthoredModuleRootsWithoutPackageNameAssumptions) {
15+
auto roots = declared_module_roots(
16+
"module;\n"
17+
"export module mcpplibs.cmdline;\n"
18+
"export module mcpplibs.cmdline:options;\n"
19+
"module mcpplibs.cmdline:impl;\n"
20+
"import unrelated;\n"
21+
"export module vendor.extra;\n");
22+
EXPECT_EQ(roots, (std::vector<std::string>{
23+
"mcpplibs.cmdline", "vendor.extra"}));
24+
}
25+
1426
TEST(Mangle, RewriteEmpty) {
1527
std::map<std::string, std::string> table;
1628
EXPECT_EQ(rewrite_module_decls("", table), "");

0 commit comments

Comments
 (0)