Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
31 changes: 25 additions & 6 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 += '/';
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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:
Expand All @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ struct BuildPlan {
std::filesystem::path outputDir; // target/<triple>/<fp>/
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<CompileUnit> compileUnits; // topologically sorted
Expand Down
6 changes: 6 additions & 0 deletions src/cli.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)) {
Expand Down
71 changes: 71 additions & 0 deletions src/toolchain/clang.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ std::vector<std::string> std_module_build_commands(const Toolchain& tc,
const std::filesystem::path& bmiPath,
std::string_view sysrootFlag);

std::optional<std::filesystem::path> 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::string> 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++.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -173,4 +191,57 @@ std::optional<std::filesystem::path> find_scan_deps(const Toolchain& tc) {
return std::nullopt;
}

std::optional<std::filesystem::path> 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::string> 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=<std.pcm>
// 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
1 change: 1 addition & 0 deletions src/toolchain/model.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::filesystem::path> compilerRuntimeDirs; // LD_LIBRARY_PATH for private tools
std::vector<std::filesystem::path> linkRuntimeDirs; // -L/-rpath dirs for produced binaries
Expand Down
63 changes: 40 additions & 23 deletions src/toolchain/stdmod.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ struct StdModule {
std::filesystem::path cacheDir; // <cache_root>/<fp>/
std::filesystem::path bmiPath; // <cacheDir>/gcm.cache/std.gcm
std::filesystem::path objectPath; // <cacheDir>/std.o
std::filesystem::path compatBmiPath; // <cacheDir>/pcm.cache/std.compat.pcm
std::filesystem::path compatObjectPath; // <cacheDir>/std.compat.o
};

struct StdModError { std::string message; };
Expand Down Expand Up @@ -98,39 +100,54 @@ std::expected<StdModule, StdModError> 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;
Expand Down
59 changes: 59 additions & 0 deletions tests/e2e/41_llvm_std_compat.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading