diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 4f6562f..58d17d5 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -10,6 +10,7 @@ export module mcpp.build.flags; import std; import mcpp.build.plan; +import mcpp.toolchain.clang; import mcpp.toolchain.detect; import mcpp.toolchain.registry; @@ -129,13 +130,18 @@ CompileFlags compute_flags(const BuildPlan& plan) { if (isClang && !plan.stdBmiPath.empty()) { std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan)); } + std::string std_compat_module_flag; + if (isClang && !plan.stdCompatBmiPath.empty()) { + auto compatDst = mcpp::toolchain::clang::staged_std_compat_bmi_path(plan.outputDir); + std_compat_module_flag = " -fmodule-file=std.compat=" + escape_path(compatDst); + } auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); std::string prebuilt_module_flag; if (traits.needsPrebuiltModulePath) { prebuilt_module_flag = std::format(" -fprebuilt-module-path={}", traits.bmiDir); } - f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}{}", module_flag, std_module_flag, - prebuilt_module_flag, + f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}{}{}", module_flag, std_module_flag, + std_compat_module_flag, prebuilt_module_flag, opt_flag, pic_flag, sysroot_flag, b_flag, include_flags, user_cxxflags); f.cc = std::format("-std={}{}{}{}{}{}{}", c_std, opt_flag, pic_flag, sysroot_flag, b_flag, include_flags, user_cflags); diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 1a7c1aa..de886b2 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -245,11 +245,6 @@ std::string emit_ninja_string(const BuildPlan& plan) { // Scan rule: produce P1689 .ddi for one TU. // GCC: built-in -fdeps-format=p1689r5 flags during preprocessing. // Clang: external clang-scan-deps tool with -format=p1689. - // Note: restat is intentionally NOT used here. The downstream - // cxx_dyndep and cxx_module rules already have restat = 1 and - // BMI preservation logic, which is sufficient to prevent - // cascading rebuilds when only implementation (not interface) - // changes. append("rule cxx_scan\n"); if (plan.scanDepsPath.empty()) { // GCC path: compiler-integrated P1689 scanning. @@ -285,6 +280,19 @@ std::string emit_ninja_string(const BuildPlan& plan) { escape_ninja_path(plan.stdObjectPath))); } + bool has_std_compat = !plan.stdCompatBmiPath.empty() && !plan.stdCompatObjectPath.empty(); + auto compat_bmi_dst = std::filesystem::path("pcm.cache") / "std.compat.pcm"; + auto compat_o_dst = std::filesystem::path("obj") / "std.compat.o"; + if (has_std_compat) { + // std.compat.pcm depends on std.pcm — ensure std.pcm is staged first + // so clang can resolve the transitive dependency when loading std.compat.pcm. + append(std::format("build {} : cp_bmi {} | {}\n", escape_ninja_path(compat_bmi_dst), + escape_ninja_path(plan.stdCompatBmiPath), + escape_ninja_path(std_bmi_dst))); + append(std::format("build {} : cp_bmi {}\n\n", escape_ninja_path(compat_o_dst), + escape_ninja_path(plan.stdCompatObjectPath))); + } + auto bmi_path = [&traits](std::string_view name) { std::string s(traits.bmiDir); s += '/'; @@ -378,11 +386,18 @@ std::string emit_ninja_string(const BuildPlan& plan) { // .c files don't `import` modules; skip BMI implicit inputs. if (rule != "c_object") { for (auto& imp : cu.imports) { - if (imp == "std" || imp == "std.compat") { + if (imp == "std") { if (has_std_artifacts) implicit += " " + escape_ninja_path(std_bmi_dst); continue; } + if (imp == "std.compat") { + if (has_std_compat) + implicit += " " + escape_ninja_path(compat_bmi_dst); + else if (has_std_artifacts) + implicit += " " + escape_ninja_path(std_bmi_dst); + continue; + } implicit += " " + bmi_path(imp); } } @@ -419,6 +434,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { case LinkUnit::TestBinary: if (has_std_artifacts) ins += " " + escape_ninja_path(std_o_dst); + if (has_std_compat) + ins += " " + escape_ninja_path(compat_o_dst); rule = "cxx_link"; break; case LinkUnit::StaticLibrary: @@ -427,6 +444,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { case LinkUnit::SharedLibrary: if (has_std_artifacts) ins += " " + escape_ninja_path(std_o_dst); + if (has_std_compat) + ins += " " + escape_ninja_path(compat_o_dst); rule = "cxx_shared"; break; } diff --git a/src/build/plan.cppm b/src/build/plan.cppm index f38d233..2e83573 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -37,6 +37,8 @@ struct BuildPlan { std::filesystem::path outputDir; // target/// std::filesystem::path stdBmiPath; // absolute path to prebuilt std.gcm std::filesystem::path stdObjectPath; // absolute path to prebuilt std.o + std::filesystem::path stdCompatBmiPath; // absolute path to prebuilt std.compat.pcm + std::filesystem::path stdCompatObjectPath; // absolute path to prebuilt std.compat.o std::filesystem::path scanDepsPath; // clang-scan-deps binary (Clang only) std::vector compileUnits; // topologically sorted diff --git a/src/cli.cppm b/src/cli.cppm index 35b56cf..2b536a3 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1965,11 +1965,15 @@ prepare_build(bool print_fingerprint, // Pre-build std module only when the source graph actually imports it. std::filesystem::path stdBmiPath; std::filesystem::path stdObjectPath; + std::filesystem::path stdCompatBmiPath; + std::filesystem::path stdCompatObjectPath; if (needsStdModule) { auto sm = mcpp::toolchain::ensure_built(*tc, fp.hex); if (!sm) return std::unexpected(sm.error().message); stdBmiPath = sm->bmiPath; stdObjectPath = sm->objectPath; + stdCompatBmiPath = sm->compatBmiPath; + stdCompatObjectPath = sm->compatObjectPath; } if (print_fingerprint) { @@ -1990,6 +1994,8 @@ prepare_build(bool print_fingerprint, ctx.stdObject = stdObjectPath; ctx.plan = mcpp::build::make_plan(*m, *tc, fp, scan.graph, report.topoOrder, *root, ctx.outputDir, stdBmiPath, stdObjectPath); + ctx.plan.stdCompatBmiPath = stdCompatBmiPath; + ctx.plan.stdCompatObjectPath = stdCompatObjectPath; // Clang: discover clang-scan-deps for P1689 dyndep scanning. if (mcpp::toolchain::is_clang(*tc)) { diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 4271a49..23cf617 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -26,6 +26,19 @@ std::vector std_module_build_commands(const Toolchain& tc, const std::filesystem::path& bmiPath, std::string_view sysrootFlag); +std::optional find_libcxx_std_compat_source( + const std::filesystem::path& cxx_binary, + const std::string& envPrefix); + +std::filesystem::path std_compat_bmi_path(const std::filesystem::path& cacheDir); +std::filesystem::path staged_std_compat_bmi_path(const std::filesystem::path& outputDir); + +std::vector std_compat_build_commands(const Toolchain& tc, + const std::filesystem::path& cacheDir, + const std::filesystem::path& bmiPath, + const std::filesystem::path& stdBmiPath, + std::string_view sysrootFlag); + std::filesystem::path archive_tool(const Toolchain& tc); // Locate clang-scan-deps in the same bin/ directory as clang++. @@ -125,6 +138,11 @@ void enrich_toolchain(Toolchain& tc, const std::string& envPrefix) { tc.stdModuleSource = *p; tc.hasImportStd = true; } + if (tc.hasImportStd) { + if (auto p = find_libcxx_std_compat_source(tc.binaryPath, envPrefix)) { + tc.stdCompatSource = *p; + } + } } std::filesystem::path std_bmi_path(const std::filesystem::path& cacheDir) { @@ -173,4 +191,57 @@ std::optional find_scan_deps(const Toolchain& tc) { return std::nullopt; } +std::optional find_libcxx_std_compat_source( + const std::filesystem::path& cxx_binary, + const std::string& envPrefix) +{ + // Same search strategy as find_libcxx_std_module_source but for std.compat + auto root = cxx_binary.parent_path().parent_path(); + auto p = root / "share" / "libc++" / "v1" / "std.compat.cppm"; + if (std::filesystem::exists(p)) return p; + return std::nullopt; +} + +std::filesystem::path std_compat_bmi_path(const std::filesystem::path& cacheDir) { + return cacheDir / "pcm.cache" / "std.compat.pcm"; +} + +std::filesystem::path staged_std_compat_bmi_path(const std::filesystem::path& outputDir) { + return outputDir / "pcm.cache" / "std.compat.pcm"; +} + +std::vector std_compat_build_commands(const Toolchain& tc, + const std::filesystem::path& cacheDir, + const std::filesystem::path& bmiPath, + const std::filesystem::path& stdBmiPath, + std::string_view sysrootFlag) +{ + auto relBmi = std::filesystem::relative(bmiPath, cacheDir).string(); + auto relStdBmi = std::filesystem::relative(stdBmiPath, cacheDir).string(); + // std.compat depends on std, so we need -fmodule-file=std= + // Note: the path after = must NOT be shell-quoted separately; the + // entire -fmodule-file flag is a single token to the compiler. + return { + std::format("cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} " + "-fmodule-file=std={} " + "--precompile {} -o {} 2>&1", + mcpp::xlings::shq(cacheDir.string()), + mcpp::toolchain::compiler_env_prefix(tc), + mcpp::xlings::shq(tc.binaryPath.string()), + sysrootFlag, + relStdBmi, + mcpp::xlings::shq(tc.stdCompatSource.string()), + mcpp::xlings::shq(relBmi)), + std::format("cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} " + "-fmodule-file=std={} " + "{} -c -o std.compat.o 2>&1", + mcpp::xlings::shq(cacheDir.string()), + mcpp::toolchain::compiler_env_prefix(tc), + mcpp::xlings::shq(tc.binaryPath.string()), + sysrootFlag, + relStdBmi, + mcpp::xlings::shq(relBmi)) + }; +} + } // namespace mcpp::toolchain::clang diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index 0a01488..cecfacc 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -17,6 +17,7 @@ struct Toolchain { std::string stdlibId; // "libstdc++" std::string stdlibVersion; std::filesystem::path stdModuleSource; // bits/std.cc / std.cppm + std::filesystem::path stdCompatSource; // bits/std_compat.cc / std.compat.cppm std::filesystem::path sysroot; // -print-sysroot output (or empty) std::vector compilerRuntimeDirs; // LD_LIBRARY_PATH for private tools std::vector linkRuntimeDirs; // -L/-rpath dirs for produced binaries diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 0d9c029..042f4d0 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -34,6 +34,8 @@ struct StdModule { std::filesystem::path cacheDir; // // std::filesystem::path bmiPath; // /gcm.cache/std.gcm std::filesystem::path objectPath; // /std.o + std::filesystem::path compatBmiPath; // /pcm.cache/std.compat.pcm + std::filesystem::path compatObjectPath; // /std.compat.o }; struct StdModError { std::string message; }; @@ -98,39 +100,54 @@ std::expected ensure_built( : mcpp::toolchain::gcc::std_bmi_path(sm.cacheDir); sm.objectPath = sm.cacheDir / "std.o"; - if (std::filesystem::exists(sm.bmiPath) && std::filesystem::exists(sm.objectPath)) { - return sm; - } - - std::error_code ec; - std::filesystem::create_directories(sm.bmiPath.parent_path(), ec); - if (ec) return std::unexpected(StdModError{ - std::format("cannot create '{}': {}", sm.bmiPath.parent_path().string(), ec.message())}); - std::string sysroot_flag; if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); } - std::string out; - - if (is_clang(tc)) { - for (auto& cmd : mcpp::toolchain::clang::std_module_build_commands( - tc, sm.cacheDir, sm.bmiPath, sysroot_flag)) { + bool std_cached = std::filesystem::exists(sm.bmiPath) && std::filesystem::exists(sm.objectPath); + + if (!std_cached) { + std::error_code ec; + std::filesystem::create_directories(sm.bmiPath.parent_path(), ec); + if (ec) return std::unexpected(StdModError{ + std::format("cannot create '{}': {}", sm.bmiPath.parent_path().string(), ec.message())}); + + std::string out; + + if (is_clang(tc)) { + for (auto& cmd : mcpp::toolchain::clang::std_module_build_commands( + tc, sm.cacheDir, sm.bmiPath, sysroot_flag)) { + if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); + else out += *r; + } + } else { + auto cmd = mcpp::toolchain::gcc::std_module_build_command( + tc, sm.cacheDir, sysroot_flag); if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); else out += *r; } - } else { - auto cmd = mcpp::toolchain::gcc::std_module_build_command( - tc, sm.cacheDir, sysroot_flag); - if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); - else out += *r; + + if (!std::filesystem::exists(sm.bmiPath)) { + return std::unexpected(StdModError{ + std::format("expected BMI at '{}' but it wasn't produced; output:\n{}", + sm.bmiPath.string(), out)}); + } } - if (!std::filesystem::exists(sm.bmiPath)) { - return std::unexpected(StdModError{ - std::format("expected BMI at '{}' but it wasn't produced; output:\n{}", - sm.bmiPath.string(), out)}); + // Build std.compat after std (std.compat depends on std, Clang only) + if (is_clang(tc) && !tc.stdCompatSource.empty()) { + auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); + if (!std::filesystem::exists(compatBmi)) { + std::string out; + for (auto& cmd : mcpp::toolchain::clang::std_compat_build_commands( + tc, sm.cacheDir, compatBmi, sm.bmiPath, sysroot_flag)) { + if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); + else out += *r; + } + } + sm.compatBmiPath = compatBmi; + sm.compatObjectPath = sm.cacheDir / "std.compat.o"; } return sm; diff --git a/tests/e2e/41_llvm_std_compat.sh b/tests/e2e/41_llvm_std_compat.sh new file mode 100755 index 0000000..9747c38 --- /dev/null +++ b/tests/e2e/41_llvm_std_compat.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# 41_llvm_std_compat.sh — build a project that uses import std.compat with Clang. +set -e + +LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" +if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then + echo "SKIP: xlings llvm@20.1.7 is not installed" + exit 0 +fi +if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.compat.cppm" ]]; then + echo "SKIP: xlings llvm@20.1.7 has no libc++ std.compat.cppm" + exit 0 +fi + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > mcpp.toml <<'EOF' +[package] +name = "compat_test" +version = "0.1.0" +[toolchain] +linux = "llvm@20.1.7" +EOF + +cat > src/main.cpp <<'EOF' +import std.compat; + +int main() { + // std.compat provides C stdlib functions like printf + printf("compat %d\n", 42); + return 0; +} +EOF + +"$MCPP" build --no-cache > "$TMP/build.log" 2>&1 || { + cat "$TMP/build.log" + echo "FAIL: std.compat build failed" + exit 1 +} + +binary=$(find target -type f -path '*/bin/compat_test' | head -1) +[[ -n "$binary" && -x "$binary" ]] || { + echo "FAIL: compat_test binary missing" + exit 1 +} + +out=$("$binary") +[[ "$out" == "compat 42" ]] || { + echo "FAIL: wrong output: $out" + exit 1 +} + +echo "OK"