Skip to content

Commit f9e5a62

Browse files
committed
feat: std.compat module support + cxx_scan restat + E2E test
std.compat: Clang's libc++ ships std.compat.cppm alongside std.cppm. Add discovery, precompilation, staging, and -fmodule-file flag for std.compat. GCC's bits/std.cc implicitly covers std.compat so no GCC changes needed. cxx_scan restat: re-implement backup-compare-restore pattern on scan rule. Previous CI failure was proven to be a cache-restore fluke. When .ddi content is unchanged, old file is restored (preserving mtime) + restat = 1 prevents downstream cascade. E2E: add 41_llvm_std_compat.sh testing import std.compat with Clang.
1 parent 34db876 commit f9e5a62

8 files changed

Lines changed: 234 additions & 31 deletions

File tree

src/build/flags.cppm

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export module mcpp.build.flags;
1010

1111
import std;
1212
import mcpp.build.plan;
13+
import mcpp.toolchain.clang;
1314
import mcpp.toolchain.detect;
1415
import mcpp.toolchain.registry;
1516

@@ -129,13 +130,18 @@ CompileFlags compute_flags(const BuildPlan& plan) {
129130
if (isClang && !plan.stdBmiPath.empty()) {
130131
std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan));
131132
}
133+
std::string std_compat_module_flag;
134+
if (isClang && !plan.stdCompatBmiPath.empty()) {
135+
auto compatDst = mcpp::toolchain::clang::staged_std_compat_bmi_path(plan.outputDir);
136+
std_compat_module_flag = " -fmodule-file=std.compat=" + escape_path(compatDst);
137+
}
132138
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
133139
std::string prebuilt_module_flag;
134140
if (traits.needsPrebuiltModulePath) {
135141
prebuilt_module_flag = std::format(" -fprebuilt-module-path={}", traits.bmiDir);
136142
}
137-
f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}{}", module_flag, std_module_flag,
138-
prebuilt_module_flag,
143+
f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}{}{}", module_flag, std_module_flag,
144+
std_compat_module_flag, prebuilt_module_flag,
139145
opt_flag, pic_flag, sysroot_flag, b_flag, include_flags, user_cxxflags);
140146
f.cc = std::format("-std={}{}{}{}{}{}{}", c_std, opt_flag, pic_flag, sysroot_flag, b_flag,
141147
include_flags, user_cflags);

src/build/ninja_backend.cppm

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,33 @@ std::string emit_ninja_string(const BuildPlan& plan) {
253253
append("rule cxx_scan\n");
254254
if (plan.scanDepsPath.empty()) {
255255
// GCC path: compiler-integrated P1689 scanning.
256-
append(" command = $toolenv $cxx $cxxflags -fmodules "
256+
// Backup-compare-restore preserves .ddi mtime when content unchanged.
257+
append(" command = "
258+
"if [ -f \"$out\" ]; then cp -p \"$out\" \"$out.bak\"; fi && "
259+
"$toolenv $cxx $cxxflags -fmodules "
257260
"-fdeps-format=p1689r5 "
258261
"-fdeps-file=$out -fdeps-target=$compile_target "
259-
"-M -MM -MF $out.dep -E $in -o $compile_target\n");
262+
"-M -MM -MF $out.dep -E $in -o $compile_target && "
263+
"if [ -f \"$out.bak\" ] && cmp -s \"$out\" \"$out.bak\"; then "
264+
"mv \"$out.bak\" \"$out\"; "
265+
"else "
266+
"rm -f \"$out.bak\"; "
267+
"fi\n");
260268
} else {
261269
// Clang path: clang-scan-deps produces P1689 JSON to stdout.
262-
append(" command = $toolenv $scan_deps -format=p1689 -- "
263-
"$cxx $cxxflags -c $in -o $compile_target > $out\n");
270+
// Backup-compare-restore preserves .ddi mtime when content unchanged.
271+
append(" command = "
272+
"if [ -f \"$out\" ]; then cp -p \"$out\" \"$out.bak\"; fi && "
273+
"$toolenv $scan_deps -format=p1689 -- "
274+
"$cxx $cxxflags -c $in -o $compile_target > $out && "
275+
"if [ -f \"$out.bak\" ] && cmp -s \"$out\" \"$out.bak\"; then "
276+
"mv \"$out.bak\" \"$out\"; "
277+
"else "
278+
"rm -f \"$out.bak\"; "
279+
"fi\n");
264280
}
265-
append(" description = SCAN $out\n\n");
281+
append(" description = SCAN $out\n");
282+
append(" restat = 1\n\n");
266283

267284
// Aggregate .ddi files into a Ninja dyndep file.
268285
append(std::format(
@@ -285,6 +302,19 @@ std::string emit_ninja_string(const BuildPlan& plan) {
285302
escape_ninja_path(plan.stdObjectPath)));
286303
}
287304

305+
bool has_std_compat = !plan.stdCompatBmiPath.empty() && !plan.stdCompatObjectPath.empty();
306+
auto compat_bmi_dst = std::filesystem::path("pcm.cache") / "std.compat.pcm";
307+
auto compat_o_dst = std::filesystem::path("obj") / "std.compat.o";
308+
if (has_std_compat) {
309+
// std.compat.pcm depends on std.pcm — ensure std.pcm is staged first
310+
// so clang can resolve the transitive dependency when loading std.compat.pcm.
311+
append(std::format("build {} : cp_bmi {} | {}\n", escape_ninja_path(compat_bmi_dst),
312+
escape_ninja_path(plan.stdCompatBmiPath),
313+
escape_ninja_path(std_bmi_dst)));
314+
append(std::format("build {} : cp_bmi {}\n\n", escape_ninja_path(compat_o_dst),
315+
escape_ninja_path(plan.stdCompatObjectPath)));
316+
}
317+
288318
auto bmi_path = [&traits](std::string_view name) {
289319
std::string s(traits.bmiDir);
290320
s += '/';
@@ -378,11 +408,18 @@ std::string emit_ninja_string(const BuildPlan& plan) {
378408
// .c files don't `import` modules; skip BMI implicit inputs.
379409
if (rule != "c_object") {
380410
for (auto& imp : cu.imports) {
381-
if (imp == "std" || imp == "std.compat") {
411+
if (imp == "std") {
382412
if (has_std_artifacts)
383413
implicit += " " + escape_ninja_path(std_bmi_dst);
384414
continue;
385415
}
416+
if (imp == "std.compat") {
417+
if (has_std_compat)
418+
implicit += " " + escape_ninja_path(compat_bmi_dst);
419+
else if (has_std_artifacts)
420+
implicit += " " + escape_ninja_path(std_bmi_dst);
421+
continue;
422+
}
386423
implicit += " " + bmi_path(imp);
387424
}
388425
}
@@ -419,6 +456,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
419456
case LinkUnit::TestBinary:
420457
if (has_std_artifacts)
421458
ins += " " + escape_ninja_path(std_o_dst);
459+
if (has_std_compat)
460+
ins += " " + escape_ninja_path(compat_o_dst);
422461
rule = "cxx_link";
423462
break;
424463
case LinkUnit::StaticLibrary:
@@ -427,6 +466,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
427466
case LinkUnit::SharedLibrary:
428467
if (has_std_artifacts)
429468
ins += " " + escape_ninja_path(std_o_dst);
469+
if (has_std_compat)
470+
ins += " " + escape_ninja_path(compat_o_dst);
430471
rule = "cxx_shared";
431472
break;
432473
}

src/build/plan.cppm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ struct BuildPlan {
3737
std::filesystem::path outputDir; // target/<triple>/<fp>/
3838
std::filesystem::path stdBmiPath; // absolute path to prebuilt std.gcm
3939
std::filesystem::path stdObjectPath; // absolute path to prebuilt std.o
40+
std::filesystem::path stdCompatBmiPath; // absolute path to prebuilt std.compat.pcm
41+
std::filesystem::path stdCompatObjectPath; // absolute path to prebuilt std.compat.o
4042
std::filesystem::path scanDepsPath; // clang-scan-deps binary (Clang only)
4143

4244
std::vector<CompileUnit> compileUnits; // topologically sorted

src/cli.cppm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,11 +1965,15 @@ prepare_build(bool print_fingerprint,
19651965
// Pre-build std module only when the source graph actually imports it.
19661966
std::filesystem::path stdBmiPath;
19671967
std::filesystem::path stdObjectPath;
1968+
std::filesystem::path stdCompatBmiPath;
1969+
std::filesystem::path stdCompatObjectPath;
19681970
if (needsStdModule) {
19691971
auto sm = mcpp::toolchain::ensure_built(*tc, fp.hex);
19701972
if (!sm) return std::unexpected(sm.error().message);
19711973
stdBmiPath = sm->bmiPath;
19721974
stdObjectPath = sm->objectPath;
1975+
stdCompatBmiPath = sm->compatBmiPath;
1976+
stdCompatObjectPath = sm->compatObjectPath;
19731977
}
19741978

19751979
if (print_fingerprint) {
@@ -1990,6 +1994,8 @@ prepare_build(bool print_fingerprint,
19901994
ctx.stdObject = stdObjectPath;
19911995
ctx.plan = mcpp::build::make_plan(*m, *tc, fp, scan.graph, report.topoOrder,
19921996
*root, ctx.outputDir, stdBmiPath, stdObjectPath);
1997+
ctx.plan.stdCompatBmiPath = stdCompatBmiPath;
1998+
ctx.plan.stdCompatObjectPath = stdCompatObjectPath;
19931999

19942000
// Clang: discover clang-scan-deps for P1689 dyndep scanning.
19952001
if (mcpp::toolchain::is_clang(*tc)) {

src/toolchain/clang.cppm

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ std::vector<std::string> std_module_build_commands(const Toolchain& tc,
2626
const std::filesystem::path& bmiPath,
2727
std::string_view sysrootFlag);
2828

29+
std::optional<std::filesystem::path> find_libcxx_std_compat_source(
30+
const std::filesystem::path& cxx_binary,
31+
const std::string& envPrefix);
32+
33+
std::filesystem::path std_compat_bmi_path(const std::filesystem::path& cacheDir);
34+
std::filesystem::path staged_std_compat_bmi_path(const std::filesystem::path& outputDir);
35+
36+
std::vector<std::string> std_compat_build_commands(const Toolchain& tc,
37+
const std::filesystem::path& cacheDir,
38+
const std::filesystem::path& bmiPath,
39+
const std::filesystem::path& stdBmiPath,
40+
std::string_view sysrootFlag);
41+
2942
std::filesystem::path archive_tool(const Toolchain& tc);
3043

3144
// Locate clang-scan-deps in the same bin/ directory as clang++.
@@ -125,6 +138,11 @@ void enrich_toolchain(Toolchain& tc, const std::string& envPrefix) {
125138
tc.stdModuleSource = *p;
126139
tc.hasImportStd = true;
127140
}
141+
if (tc.hasImportStd) {
142+
if (auto p = find_libcxx_std_compat_source(tc.binaryPath, envPrefix)) {
143+
tc.stdCompatSource = *p;
144+
}
145+
}
128146
}
129147

130148
std::filesystem::path std_bmi_path(const std::filesystem::path& cacheDir) {
@@ -173,4 +191,57 @@ std::optional<std::filesystem::path> find_scan_deps(const Toolchain& tc) {
173191
return std::nullopt;
174192
}
175193

194+
std::optional<std::filesystem::path> find_libcxx_std_compat_source(
195+
const std::filesystem::path& cxx_binary,
196+
const std::string& envPrefix)
197+
{
198+
// Same search strategy as find_libcxx_std_module_source but for std.compat
199+
auto root = cxx_binary.parent_path().parent_path();
200+
auto p = root / "share" / "libc++" / "v1" / "std.compat.cppm";
201+
if (std::filesystem::exists(p)) return p;
202+
return std::nullopt;
203+
}
204+
205+
std::filesystem::path std_compat_bmi_path(const std::filesystem::path& cacheDir) {
206+
return cacheDir / "pcm.cache" / "std.compat.pcm";
207+
}
208+
209+
std::filesystem::path staged_std_compat_bmi_path(const std::filesystem::path& outputDir) {
210+
return outputDir / "pcm.cache" / "std.compat.pcm";
211+
}
212+
213+
std::vector<std::string> std_compat_build_commands(const Toolchain& tc,
214+
const std::filesystem::path& cacheDir,
215+
const std::filesystem::path& bmiPath,
216+
const std::filesystem::path& stdBmiPath,
217+
std::string_view sysrootFlag)
218+
{
219+
auto relBmi = std::filesystem::relative(bmiPath, cacheDir).string();
220+
auto relStdBmi = std::filesystem::relative(stdBmiPath, cacheDir).string();
221+
// std.compat depends on std, so we need -fmodule-file=std=<std.pcm>
222+
// Note: the path after = must NOT be shell-quoted separately; the
223+
// entire -fmodule-file flag is a single token to the compiler.
224+
return {
225+
std::format("cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} "
226+
"-fmodule-file=std={} "
227+
"--precompile {} -o {} 2>&1",
228+
mcpp::xlings::shq(cacheDir.string()),
229+
mcpp::toolchain::compiler_env_prefix(tc),
230+
mcpp::xlings::shq(tc.binaryPath.string()),
231+
sysrootFlag,
232+
relStdBmi,
233+
mcpp::xlings::shq(tc.stdCompatSource.string()),
234+
mcpp::xlings::shq(relBmi)),
235+
std::format("cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} "
236+
"-fmodule-file=std={} "
237+
"{} -c -o std.compat.o 2>&1",
238+
mcpp::xlings::shq(cacheDir.string()),
239+
mcpp::toolchain::compiler_env_prefix(tc),
240+
mcpp::xlings::shq(tc.binaryPath.string()),
241+
sysrootFlag,
242+
relStdBmi,
243+
mcpp::xlings::shq(relBmi))
244+
};
245+
}
246+
176247
} // namespace mcpp::toolchain::clang

src/toolchain/model.cppm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct Toolchain {
1717
std::string stdlibId; // "libstdc++"
1818
std::string stdlibVersion;
1919
std::filesystem::path stdModuleSource; // bits/std.cc / std.cppm
20+
std::filesystem::path stdCompatSource; // bits/std_compat.cc / std.compat.cppm
2021
std::filesystem::path sysroot; // -print-sysroot output (or empty)
2122
std::vector<std::filesystem::path> compilerRuntimeDirs; // LD_LIBRARY_PATH for private tools
2223
std::vector<std::filesystem::path> linkRuntimeDirs; // -L/-rpath dirs for produced binaries

src/toolchain/stdmod.cppm

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ struct StdModule {
3434
std::filesystem::path cacheDir; // <cache_root>/<fp>/
3535
std::filesystem::path bmiPath; // <cacheDir>/gcm.cache/std.gcm
3636
std::filesystem::path objectPath; // <cacheDir>/std.o
37+
std::filesystem::path compatBmiPath; // <cacheDir>/pcm.cache/std.compat.pcm
38+
std::filesystem::path compatObjectPath; // <cacheDir>/std.compat.o
3739
};
3840

3941
struct StdModError { std::string message; };
@@ -98,39 +100,54 @@ std::expected<StdModule, StdModError> ensure_built(
98100
: mcpp::toolchain::gcc::std_bmi_path(sm.cacheDir);
99101
sm.objectPath = sm.cacheDir / "std.o";
100102

101-
if (std::filesystem::exists(sm.bmiPath) && std::filesystem::exists(sm.objectPath)) {
102-
return sm;
103-
}
104-
105-
std::error_code ec;
106-
std::filesystem::create_directories(sm.bmiPath.parent_path(), ec);
107-
if (ec) return std::unexpected(StdModError{
108-
std::format("cannot create '{}': {}", sm.bmiPath.parent_path().string(), ec.message())});
109-
110103
std::string sysroot_flag;
111104
if (!tc.sysroot.empty()) {
112105
sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string());
113106
}
114107

115-
std::string out;
116-
117-
if (is_clang(tc)) {
118-
for (auto& cmd : mcpp::toolchain::clang::std_module_build_commands(
119-
tc, sm.cacheDir, sm.bmiPath, sysroot_flag)) {
108+
bool std_cached = std::filesystem::exists(sm.bmiPath) && std::filesystem::exists(sm.objectPath);
109+
110+
if (!std_cached) {
111+
std::error_code ec;
112+
std::filesystem::create_directories(sm.bmiPath.parent_path(), ec);
113+
if (ec) return std::unexpected(StdModError{
114+
std::format("cannot create '{}': {}", sm.bmiPath.parent_path().string(), ec.message())});
115+
116+
std::string out;
117+
118+
if (is_clang(tc)) {
119+
for (auto& cmd : mcpp::toolchain::clang::std_module_build_commands(
120+
tc, sm.cacheDir, sm.bmiPath, sysroot_flag)) {
121+
if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error());
122+
else out += *r;
123+
}
124+
} else {
125+
auto cmd = mcpp::toolchain::gcc::std_module_build_command(
126+
tc, sm.cacheDir, sysroot_flag);
120127
if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error());
121128
else out += *r;
122129
}
123-
} else {
124-
auto cmd = mcpp::toolchain::gcc::std_module_build_command(
125-
tc, sm.cacheDir, sysroot_flag);
126-
if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error());
127-
else out += *r;
130+
131+
if (!std::filesystem::exists(sm.bmiPath)) {
132+
return std::unexpected(StdModError{
133+
std::format("expected BMI at '{}' but it wasn't produced; output:\n{}",
134+
sm.bmiPath.string(), out)});
135+
}
128136
}
129137

130-
if (!std::filesystem::exists(sm.bmiPath)) {
131-
return std::unexpected(StdModError{
132-
std::format("expected BMI at '{}' but it wasn't produced; output:\n{}",
133-
sm.bmiPath.string(), out)});
138+
// Build std.compat after std (std.compat depends on std, Clang only)
139+
if (is_clang(tc) && !tc.stdCompatSource.empty()) {
140+
auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir);
141+
if (!std::filesystem::exists(compatBmi)) {
142+
std::string out;
143+
for (auto& cmd : mcpp::toolchain::clang::std_compat_build_commands(
144+
tc, sm.cacheDir, compatBmi, sm.bmiPath, sysroot_flag)) {
145+
if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error());
146+
else out += *r;
147+
}
148+
}
149+
sm.compatBmiPath = compatBmi;
150+
sm.compatObjectPath = sm.cacheDir / "std.compat.o";
134151
}
135152

136153
return sm;

0 commit comments

Comments
 (0)