Skip to content

Commit c7c1c3e

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 c7c1c3e

8 files changed

Lines changed: 212 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: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,6 @@ std::string emit_ninja_string(const BuildPlan& plan) {
245245
// Scan rule: produce P1689 .ddi for one TU.
246246
// GCC: built-in -fdeps-format=p1689r5 flags during preprocessing.
247247
// Clang: external clang-scan-deps tool with -format=p1689.
248-
// Note: restat is intentionally NOT used here. The downstream
249-
// cxx_dyndep and cxx_module rules already have restat = 1 and
250-
// BMI preservation logic, which is sufficient to prevent
251-
// cascading rebuilds when only implementation (not interface)
252-
// changes.
253248
append("rule cxx_scan\n");
254249
if (plan.scanDepsPath.empty()) {
255250
// GCC path: compiler-integrated P1689 scanning.
@@ -285,6 +280,19 @@ std::string emit_ninja_string(const BuildPlan& plan) {
285280
escape_ninja_path(plan.stdObjectPath)));
286281
}
287282

283+
bool has_std_compat = !plan.stdCompatBmiPath.empty() && !plan.stdCompatObjectPath.empty();
284+
auto compat_bmi_dst = std::filesystem::path("pcm.cache") / "std.compat.pcm";
285+
auto compat_o_dst = std::filesystem::path("obj") / "std.compat.o";
286+
if (has_std_compat) {
287+
// std.compat.pcm depends on std.pcm — ensure std.pcm is staged first
288+
// so clang can resolve the transitive dependency when loading std.compat.pcm.
289+
append(std::format("build {} : cp_bmi {} | {}\n", escape_ninja_path(compat_bmi_dst),
290+
escape_ninja_path(plan.stdCompatBmiPath),
291+
escape_ninja_path(std_bmi_dst)));
292+
append(std::format("build {} : cp_bmi {}\n\n", escape_ninja_path(compat_o_dst),
293+
escape_ninja_path(plan.stdCompatObjectPath)));
294+
}
295+
288296
auto bmi_path = [&traits](std::string_view name) {
289297
std::string s(traits.bmiDir);
290298
s += '/';
@@ -378,11 +386,18 @@ std::string emit_ninja_string(const BuildPlan& plan) {
378386
// .c files don't `import` modules; skip BMI implicit inputs.
379387
if (rule != "c_object") {
380388
for (auto& imp : cu.imports) {
381-
if (imp == "std" || imp == "std.compat") {
389+
if (imp == "std") {
382390
if (has_std_artifacts)
383391
implicit += " " + escape_ninja_path(std_bmi_dst);
384392
continue;
385393
}
394+
if (imp == "std.compat") {
395+
if (has_std_compat)
396+
implicit += " " + escape_ninja_path(compat_bmi_dst);
397+
else if (has_std_artifacts)
398+
implicit += " " + escape_ninja_path(std_bmi_dst);
399+
continue;
400+
}
386401
implicit += " " + bmi_path(imp);
387402
}
388403
}
@@ -419,6 +434,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
419434
case LinkUnit::TestBinary:
420435
if (has_std_artifacts)
421436
ins += " " + escape_ninja_path(std_o_dst);
437+
if (has_std_compat)
438+
ins += " " + escape_ninja_path(compat_o_dst);
422439
rule = "cxx_link";
423440
break;
424441
case LinkUnit::StaticLibrary:
@@ -427,6 +444,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
427444
case LinkUnit::SharedLibrary:
428445
if (has_std_artifacts)
429446
ins += " " + escape_ninja_path(std_o_dst);
447+
if (has_std_compat)
448+
ins += " " + escape_ninja_path(compat_o_dst);
430449
rule = "cxx_shared";
431450
break;
432451
}

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;

tests/e2e/41_llvm_std_compat.sh

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
# 41_llvm_std_compat.sh — build a project that uses import std.compat with Clang.
3+
set -e
4+
5+
LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7"
6+
if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then
7+
echo "SKIP: xlings llvm@20.1.7 is not installed"
8+
exit 0
9+
fi
10+
if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.compat.cppm" ]]; then
11+
echo "SKIP: xlings llvm@20.1.7 has no libc++ std.compat.cppm"
12+
exit 0
13+
fi
14+
15+
TMP=$(mktemp -d)
16+
trap "rm -rf $TMP" EXIT
17+
export MCPP_HOME="$TMP/mcpp-home"
18+
source "$(dirname "$0")/_inherit_toolchain.sh"
19+
20+
mkdir -p "$TMP/proj/src"
21+
cd "$TMP/proj"
22+
23+
cat > mcpp.toml <<'EOF'
24+
[package]
25+
name = "compat_test"
26+
version = "0.1.0"
27+
[toolchain]
28+
linux = "llvm@20.1.7"
29+
EOF
30+
31+
cat > src/main.cpp <<'EOF'
32+
import std.compat;
33+
34+
int main() {
35+
// std.compat provides C stdlib functions like printf
36+
printf("compat %d\n", 42);
37+
return 0;
38+
}
39+
EOF
40+
41+
"$MCPP" build --no-cache > "$TMP/build.log" 2>&1 || {
42+
cat "$TMP/build.log"
43+
echo "FAIL: std.compat build failed"
44+
exit 1
45+
}
46+
47+
binary=$(find target -type f -path '*/bin/compat_test' | head -1)
48+
[[ -n "$binary" && -x "$binary" ]] || {
49+
echo "FAIL: compat_test binary missing"
50+
exit 1
51+
}
52+
53+
out=$("$binary")
54+
[[ "$out" == "compat 42" ]] || {
55+
echo "FAIL: wrong output: $out"
56+
exit 1
57+
}
58+
59+
echo "OK"

0 commit comments

Comments
 (0)