Skip to content

Commit 9016c0b

Browse files
fix: an indirect import is found whatever module map its importer has (GCC)
A GCC module map now names the BMI directory as its repository ($root) and every path below it, so a BMI built with GCC's own mapper and one built with a map record their imports at the same paths. The dyndep step reads $root. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent bfa9b8f commit 9016c0b

6 files changed

Lines changed: 115 additions & 6 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
`app.wasm`, the edge's implicit output was missing, and `mcpp pack` of a
1515
`wasm32-emscripten` program (mcpp-plugins' `dist-web`) found no stem family.
1616
A WebAssembly link is in place, as a PE link is.
17+
- **A module's indirect import is found whatever map its importer has (GCC).**
18+
A unit that reaches another package's module is compiled with a module map,
19+
and a unit that imports only the root's modules with GCC's own mapper. A BMI
20+
records the paths of its imports as its compile's mapper spelt them, and the
21+
map named no repository, so a unit with a map that imported a root module
22+
compiled without one looked for that module's import beside the build
23+
directory (`failed to read compiled module`, `Bad import dependency`). The
24+
map now names the BMI directory as its repository (`$root gcm.cache`) and
25+
every path below it, as GCC's own mapper does; the dyndep step reads the
26+
same form.
1727

1828
## [2026.10.26.1] - 2026-10-26
1929

‎modules/dyndep/src/dyndep.cppm‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ struct DyndepOptions {
6363
// The BMI path `name` takes under `opts`: the module map's, or the flat one.
6464
std::string bmi_path_for(std::string_view name, const DyndepOptions& opts);
6565

66-
// Parse a module map: `<module name> <BMI path>` per line.
66+
// Parse a module map: `<module name> <BMI path>` per line. A `$root <dir>`
67+
// line (GCC's repository) puts every relative path after it below `<dir>`.
6768
std::map<std::string, std::string, std::less<>> parse_module_map(std::string_view body);
6869

6970
// Parse a single .ddi JSON body to a UnitInfo. Returns unexpected on JSON error.
@@ -206,14 +207,21 @@ std::string bmi_path_for(std::string_view name, const DyndepOptions& opts) {
206207

207208
std::map<std::string, std::string, std::less<>> parse_module_map(std::string_view body) {
208209
std::map<std::string, std::string, std::less<>> out;
210+
std::string_view root;
209211
while (!body.empty()) {
210212
auto nl = body.find('\n');
211213
auto line = body.substr(0, nl);
212214
body.remove_prefix(nl == std::string_view::npos ? body.size() : nl + 1);
213215
while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.remove_suffix(1);
214216
auto sp = line.find(' ');
215217
if (sp == std::string_view::npos || sp == 0) continue;
216-
out.emplace(std::string(line.substr(0, sp)), std::string(line.substr(sp + 1)));
218+
const auto name = line.substr(0, sp);
219+
const auto path = line.substr(sp + 1);
220+
if (name == "$root") { root = path; continue; }
221+
if (root.empty() || path.starts_with('/') || std::filesystem::path(path).is_absolute())
222+
out.emplace(std::string(name), std::string(path));
223+
else
224+
out.emplace(std::string(name), std::string(root) + "/" + std::string(path));
217225
}
218226
return out;
219227
}

‎src/build/plan.cppm‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3553,8 +3553,20 @@ make_plan(const mcpp::manifest::Manifest& manifest,
35533553
for (auto const* std_name : {"std", "std.compat"})
35543554
entries.emplace(std_name, std::string(traits.bmiDir) + "/" + basename(std_name));
35553555

3556+
// GCC's map names the BMI directory as its repository (`$root`)
3557+
// and every path below it, as GCC's own mapper does for a unit
3558+
// given no map. A BMI records the paths of the modules it imports
3559+
// as its compile's mapper spelt them, and an importer finds an
3560+
// INDIRECT import at that recorded path below its own repository:
3561+
// a root unit compiled without a map, imported by one compiled with
3562+
// a repository other than the BMI directory, would send that
3563+
// importer to `<name>.gcm` beside the build directory.
35563564
ModuleScope scope;
3557-
for (auto const& [name, path] : entries) scope.content += name + " " + path + "\n";
3565+
const std::string root = std::string(traits.bmiDir) + "/";
3566+
if (gcc) scope.content = "$root " + std::string(traits.bmiDir) + "\n";
3567+
for (auto const& [name, path] : entries)
3568+
scope.content += name + " "
3569+
+ (gcc && path.starts_with(root) ? path.substr(root.size()) : path) + "\n";
35583570
if (!gcc && importsQualified) {
35593571
// `-fmodule-file=<name>=<path>` (clang) or `/reference
35603572
// <name>=<path>` (MSVC) for each module the unit imports that
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env bash
2+
# requires:
3+
# 940 -- an indirect import is found whatever module map its importer has.
4+
#
5+
# A unit of the root package is given a module map only when it reaches a
6+
# module of another package (pack drive and selection design 2026-10-01, B1);
7+
# a unit that imports only the root's modules is compiled with the compiler's
8+
# own mapper. GCC records in a BMI the path of each module that BMI imports, as
9+
# its compile's mapper spelt it, and an importer finds an INDIRECT import at
10+
# that recorded path below its own repository. Until 2026.10.26.2 a map named
11+
# no repository, so a unit with a map that imported a root module compiled
12+
# without one looked for that module's import beside the build directory:
13+
#
14+
# mcppide.platform.os: error: failed to read compiled module: No such file
15+
# note: compiled module file is 'mcppide.platform.os.gcm'
16+
#
17+
# Criteria:
18+
# A. `app.os` (imports nothing), `app.process` (imports app.os; no map),
19+
# `app.jobs` (imports app.process and a path dependency's module; a map):
20+
# the build succeeds and the program runs.
21+
# B. The reverse: `app.view` (no map) imports `app.jobs` (a map), whose BMI
22+
# records its own imports; the build succeeds.
23+
set -e
24+
25+
TMP=$(mktemp -d)
26+
trap 'rm -rf "$TMP"' EXIT
27+
fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; }
28+
29+
cd "$TMP"
30+
mkdir -p dep/src app/src
31+
cat > dep/mcpp.toml <<'TOML'
32+
[package]
33+
name = "dep"
34+
version = "0.1.0"
35+
36+
[targets.dep]
37+
kind = "lib"
38+
TOML
39+
printf 'export module dep.lib;\nexport int dep_value() { return 2; }\n' > dep/src/lib.cppm
40+
41+
cat > app/mcpp.toml <<'TOML'
42+
[package]
43+
name = "app"
44+
version = "0.1.0"
45+
46+
[dependencies]
47+
dep = { path = "../dep" }
48+
TOML
49+
printf 'export module app.os;\nexport inline constexpr int os_value = 1;\n' > app/src/os.cppm
50+
printf 'export module app.process;\nimport app.os;\nexport int process_value() { return os_value; }\n' > app/src/process.cppm
51+
printf 'export module app.jobs;\nimport app.process;\nimport dep.lib;\nexport int jobs_value() { return process_value() + dep_value(); }\n' > app/src/jobs.cppm
52+
printf 'export module app.view;\nimport app.jobs;\nexport int view_value() { return jobs_value() * 10; }\n' > app/src/view.cppm
53+
printf '#include <cstdio>\nimport app.view;\nint main() { std::printf("%%d\\n", view_value()); }\n' > app/src/main.cpp
54+
55+
cd app
56+
"$MCPP" build > build.log 2>&1 || fail "a unit with a map could not read a root BMI's import" build.log
57+
echo "ok: A, B the build succeeds"
58+
out=$("$MCPP" run 2>run.log | tr -d '\r' | tail -1)
59+
[ "$out" = "30" ] || fail "the program printed '$out', expected 30" run.log
60+
echo "ok: the program runs"
61+
62+
echo "PASS: 940 an indirect import is found whatever map its importer has"

‎tests/unit/test_dyndep.cpp‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,17 @@ TEST(VerifyUnitExpectations, ExpectNoneMatchesEmptyUnit) {
203203
auto err = mcpp::dyndep::verify_unit_expectations(u, std::nullopt, {});
204204
EXPECT_FALSE(err.has_value());
205205
}
206+
207+
// GCC's map names its repository with `$root`, and every relative path after
208+
// it lies below that directory: the dyndep step must name the BMI the compiler
209+
// reads, or ninja orders the compile before the BMI exists.
210+
TEST(Dyndep, ParseModuleMapRootedAtTheBmiDirectory) {
211+
auto m = parse_module_map("$root gcm.cache\na a.gcm\ndep.m dep/dep.m.gcm\nstd std.gcm\n");
212+
EXPECT_EQ(m.size(), 3u);
213+
EXPECT_FALSE(m.contains("$root"));
214+
EXPECT_EQ(m.at("a"), "gcm.cache/a.gcm");
215+
EXPECT_EQ(m.at("dep.m"), "gcm.cache/dep/dep.m.gcm");
216+
// Without `$root` (clang's and MSVC's maps) a path is taken as written.
217+
auto plain = parse_module_map("m pcm.cache/core/m.pcm\n");
218+
EXPECT_EQ(plain.at("m"), "pcm.cache/core/m.pcm");
219+
}

‎tests/unit/test_module_address.cpp‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,12 @@ TEST(ModuleAddress, MemberBmisAreQualifiedAndTheMapIsTheUnitsReach) {
199199
ASSERT_TRUE(gcc) << err;
200200
auto const& iface = gcc->core.at("core.cppm");
201201
EXPECT_EQ(iface.bmiFile, "core/corelib.gcm");
202-
EXPECT_NE(iface.mapContent.find("corelib gcm.cache/core/corelib.gcm\n"), std::string::npos);
203-
EXPECT_NE(iface.mapContent.find("m gcm.cache/core/m.gcm\n"), std::string::npos);
204-
EXPECT_NE(iface.mapContent.find("std gcm.cache/std.gcm\n"), std::string::npos);
202+
// GCC's map is rooted at the BMI directory, as its default mapper is, so
203+
// a BMI records its imports at the same paths whichever mapper made it.
204+
EXPECT_TRUE(iface.mapContent.starts_with("$root gcm.cache\n")) << iface.mapContent;
205+
EXPECT_NE(iface.mapContent.find("corelib core/corelib.gcm\n"), std::string::npos);
206+
EXPECT_NE(iface.mapContent.find("m core/m.gcm\n"), std::string::npos);
207+
EXPECT_NE(iface.mapContent.find("std std.gcm\n"), std::string::npos);
205208
EXPECT_EQ(iface.mapContent.find("tool"), std::string::npos)
206209
<< "the map names a module the unit does not reach:\n" << iface.mapContent;
207210
ASSERT_FALSE(iface.flags.empty());

0 commit comments

Comments
 (0)