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
465 changes: 465 additions & 0 deletions .agents/docs/2026-05-15-clang-parity-and-toolchain-abstraction.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .agents/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# 开发/方案文档目录
53 changes: 28 additions & 25 deletions src/bmi_cache.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// Layout (per docs/26-bmi-cache.md):
// $MCPP_HOME/bmi/<fingerprint>/deps/<indexName>/<pkgName>@<version>/
// gcm.cache/<module>.gcm
// {gcm,pcm}.cache/<module>.{gcm,pcm}
// obj/<file>.m.o + <file>.o
// manifest.txt (sentinel + file list)
//
Expand Down Expand Up @@ -31,20 +31,22 @@ struct CacheKey {
std::string indexName; // "mcpplibs" / "xim" / ...
std::string packageName; // "mcpplibs.cmdline"
std::string version; // "0.0.1"
std::string bmiDirName = "gcm.cache"; // "gcm.cache" | "pcm.cache"
std::string manifestTag = "gcm"; // "gcm" | "pcm"

std::filesystem::path dir() const {
return mcppHome / "bmi" / fingerprint / "deps"
/ indexName / std::format("{}@{}", packageName, version);
}

std::filesystem::path manifestFile() const { return dir() / "manifest.txt"; }
std::filesystem::path gcmDir() const { return dir() / "gcm.cache"; }
std::filesystem::path bmiDir() const { return dir() / bmiDirName; }
std::filesystem::path objDir() const { return dir() / "obj"; }
};

// File names (basename only) that belong to one dep package's cache entry.
struct DepArtifacts {
std::vector<std::string> gcmFiles; // basenames in gcm.cache/
std::vector<std::string> bmiFiles; // basenames in bmiDir/
std::vector<std::string> objFiles; // basenames in obj/
};

Expand All @@ -55,14 +57,14 @@ bool is_cached(const CacheKey& key);
std::expected<DepArtifacts, std::string>
read_manifest(const CacheKey& key);

// Copy missing cached files into projectTarget/{gcm.cache,obj}. Existing
// project outputs are left untouched: GCC BMIs may differ byte-for-byte between
// Copy missing cached files into projectTarget/{bmiDirName,obj}. Existing
// project outputs are left untouched: BMIs may differ byte-for-byte between
// equivalent builds, and overwriting them would dirty downstream modules.
std::expected<DepArtifacts, std::string>
stage_into(const CacheKey& key,
const std::filesystem::path& projectTargetDir);

// Copy fresh build outputs from projectTarget/{gcm.cache,obj} → cache dir
// Copy fresh build outputs from projectTarget/{bmiDirName,obj} → cache dir
// and write manifest.txt last (atomic-ish sentinel).
std::expected<void, std::string>
populate_from(const CacheKey& key,
Expand Down Expand Up @@ -91,9 +93,9 @@ bool copy_one(const std::filesystem::path& from,
return !ec;
}

std::string serialize_manifest(const DepArtifacts& a) {
std::string serialize_manifest(std::string_view tag, const DepArtifacts& a) {
std::string out = "# Auto-generated by mcpp bmi_cache. Do not edit.\n";
for (auto& g : a.gcmFiles) out += std::format("gcm: {}\n", g);
for (auto& g : a.bmiFiles) out += std::format("{}: {}\n", tag, g);
for (auto& o : a.objFiles) out += std::format("obj: {}\n", o);
return out;
}
Expand All @@ -107,7 +109,8 @@ parse_manifest(const std::filesystem::path& p) {
while (std::getline(is, line)) {
while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back();
if (line.empty() || line[0] == '#') continue;
if (line.starts_with("gcm: ")) a.gcmFiles.push_back(line.substr(5));
if (line.starts_with("gcm: ")) a.bmiFiles.push_back(line.substr(5));
else if (line.starts_with("pcm: ")) a.bmiFiles.push_back(line.substr(5));
else if (line.starts_with("obj: ")) a.objFiles.push_back(line.substr(5));
}
return a;
Expand All @@ -121,8 +124,8 @@ bool is_cached(const CacheKey& key) {
auto arts = parse_manifest(mf);
if (!arts) return false;
// Verify every listed file actually exists on disk.
for (auto& g : arts->gcmFiles) {
if (!std::filesystem::exists(key.gcmDir() / g)) return false;
for (auto& g : arts->bmiFiles) {
if (!std::filesystem::exists(key.bmiDir() / g)) return false;
}
for (auto& o : arts->objFiles) {
if (!std::filesystem::exists(key.objDir() / o)) return false;
Expand All @@ -142,23 +145,23 @@ stage_into(const CacheKey& key,
auto arts = parse_manifest(key.manifestFile());
if (!arts) return std::unexpected(arts.error());

auto projectGcm = projectTargetDir / "gcm.cache";
auto projectBmi = projectTargetDir / key.bmiDirName;
auto projectObj = projectTargetDir / "obj";
std::error_code ec;
std::filesystem::create_directories(projectGcm, ec);
std::filesystem::create_directories(projectBmi, ec);
std::filesystem::create_directories(projectObj, ec);

for (auto& g : arts->gcmFiles) {
auto from = key.gcmDir() / g;
auto to = projectGcm / g;
for (auto& g : arts->bmiFiles) {
auto from = key.bmiDir() / g;
auto to = projectBmi / g;
if (std::filesystem::exists(to, ec)) {
ec.clear();
continue;
}
ec.clear();
if (!copy_one(from, to, ec)) {
return std::unexpected(std::format(
"stage gcm '{}': {}", g, ec.message()));
"stage bmi '{}': {}", g, ec.message()));
}
touch_now(to);
}
Expand Down Expand Up @@ -221,25 +224,25 @@ populate_from(const CacheKey& key,
~LockGuard() { release_lock(fd); }
} guard{ lockFd };

auto cacheGcm = key.gcmDir();
auto cacheBmi = key.bmiDir();
auto cacheObj = key.objDir();
std::error_code ec;
std::filesystem::create_directories(cacheGcm, ec);
std::filesystem::create_directories(cacheBmi, ec);
std::filesystem::create_directories(cacheObj, ec);

auto projectGcm = projectTargetDir / "gcm.cache";
auto projectBmi = projectTargetDir / key.bmiDirName;
auto projectObj = projectTargetDir / "obj";

for (auto& g : arts.gcmFiles) {
auto from = projectGcm / g;
auto to = cacheGcm / g;
for (auto& g : arts.bmiFiles) {
auto from = projectBmi / g;
auto to = cacheBmi / g;
if (!std::filesystem::exists(from)) {
return std::unexpected(std::format(
"expected build output missing: {}", from.string()));
}
if (!copy_one(from, to, ec)) {
return std::unexpected(std::format(
"populate gcm '{}': {}", g, ec.message()));
"populate bmi '{}': {}", g, ec.message()));
}
}
for (auto& o : arts.objFiles) {
Expand All @@ -260,7 +263,7 @@ populate_from(const CacheKey& key,
tmp += ".tmp";
{
std::ofstream os(tmp);
os << serialize_manifest(arts);
os << serialize_manifest(key.manifestTag, arts);
}
std::filesystem::rename(tmp, key.manifestFile(), ec);
if (ec) {
Expand Down
8 changes: 7 additions & 1 deletion src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ CompileFlags compute_flags(const BuildPlan& plan) {
if (isClang && !plan.stdBmiPath.empty()) {
std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan));
}
f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}", module_flag, std_module_flag,
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,
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
74 changes: 53 additions & 21 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,13 @@ bool is_c_source(const std::filesystem::path& src) {
} // namespace

std::string emit_ninja_string(const BuildPlan& plan) {
bool dyndep = dyndep_mode_enabled()
&& mcpp::toolchain::is_gcc(plan.toolchain);
// dyndep requires P1689 scanning capability:
// GCC: built-in -fdeps-format=p1689r5
// Clang: external clang-scan-deps tool (same P1689 output format)
bool has_scanner = mcpp::toolchain::is_gcc(plan.toolchain)
|| !plan.scanDepsPath.empty();
bool dyndep = dyndep_mode_enabled() && has_scanner;
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
std::string out;
auto append = [&](std::string s) { out += std::move(s); };

Expand Down Expand Up @@ -162,6 +167,9 @@ std::string emit_ninja_string(const BuildPlan& plan) {
}
if (dyndep) {
append(std::format("mcpp = {}\n", escape_ninja_path(mcpp_exe_path())));
if (!plan.scanDepsPath.empty()) {
append(std::format("scan_deps = {}\n", escape_ninja_path(plan.scanDepsPath)));
}
}
append("\n");

Expand All @@ -170,10 +178,12 @@ std::string emit_ninja_string(const BuildPlan& plan) {
append(" description = STAGE $out\n\n");

// P1: per-file dyndep rule. Converts one .ddi → .dd independently.
append("rule cxx_dyndep\n");
append(" command = $mcpp dyndep --single --output $out $in\n");
append(" description = DYNDEP $out\n");
append(" restat = 1\n\n");
append(std::format(
"rule cxx_dyndep\n"
" command = $mcpp dyndep --single --bmi-dir {} --bmi-ext {} --output $out $in\n"
" description = DYNDEP $out\n"
" restat = 1\n\n",
traits.bmiDir, traits.bmiExt));

// P2: cxx_module preserves BMI timestamps when interface is unchanged.
// GCC always updates the .gcm timestamp even if content is identical.
Expand All @@ -184,18 +194,20 @@ std::string emit_ninja_string(const BuildPlan& plan) {
//
// $bmi_out is set per build edge to the BMI path (gcm.cache/<module>.gcm).
// If $bmi_out is empty (no module provided), we just compile normally.
std::string module_output_flag = traits.needsExplicitModuleOutput
? " -fmodule-output=$bmi_out" : "";
append("rule cxx_module\n");
append(" command = "
append(std::format(" command = "
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then "
"cp -p \"$bmi_out\" \"$bmi_out.bak\"; "
"fi && "
"$toolenv $cxx $cxxflags -c $in -o $out && "
"$toolenv $cxx $cxxflags{} -c $in -o $out && "
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && "
"cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then "
"mv \"$bmi_out.bak\" \"$bmi_out\"; "
"else "
"rm -f \"$bmi_out.bak\"; "
"fi\n");
"fi\n", module_output_flag));
append(" description = MOD $out\n");
if (dyndep)
append(" restat = 1\n");
Expand Down Expand Up @@ -231,18 +243,31 @@ std::string emit_ninja_string(const BuildPlan& plan) {

if (dyndep) {
// Scan rule: produce P1689 .ddi for one TU.
// -E -M -MM -MF gives us the dep file; -fdeps-* gives us the .ddi.
// GCC: built-in -fdeps-format=p1689r5 flags during preprocessing.
// Clang: external clang-scan-deps tool with -format=p1689.
append("rule cxx_scan\n");
append(" command = $toolenv $cxx $cxxflags -fdeps-format=p1689r5 "
"-fdeps-file=$out -fdeps-target=$compile_target "
"-M -MM -MF $out.dep -E $in -o $compile_target\n");
if (plan.scanDepsPath.empty()) {
// GCC path: compiler-integrated P1689 scanning.
append(" command = $toolenv $cxx $cxxflags -fmodules "
"-fdeps-format=p1689r5 "
"-fdeps-file=$out -fdeps-target=$compile_target "
"-M -MM -MF $out.dep -E $in -o $compile_target\n");
} else {
// Clang path: clang-scan-deps produces P1689 JSON to stdout,
// then we redirect to $out. The -- separator passes the full
// compile command so clang-scan-deps knows the flags/sysroot.
append(" command = $toolenv $scan_deps -format=p1689 -- "
"$cxx $cxxflags -c $in -o $compile_target > $out\n");
}
append(" description = SCAN $out\n\n");

// Aggregate .ddi files into a Ninja dyndep file.
append("rule cxx_collect\n");
append(" command = $mcpp dyndep --output $out $in\n");
append(" description = COLLECT $out\n");
append(" restat = 1\n\n");
append(std::format(
"rule cxx_collect\n"
" command = $mcpp dyndep --bmi-dir {} --bmi-ext {} --output $out $in\n"
" description = COLLECT $out\n"
" restat = 1\n\n",
traits.bmiDir, traits.bmiExt));
}

// Stage prebuilt std artifacts into the compiler-specific BMI cache.
Expand All @@ -257,11 +282,12 @@ std::string emit_ninja_string(const BuildPlan& plan) {
escape_ninja_path(plan.stdObjectPath)));
}

auto bmi_path = [](std::string_view name) {
std::string s = "gcm.cache/";
auto bmi_path = [&traits](std::string_view name) {
std::string s(traits.bmiDir);
s += '/';
for (char c : name)
s.push_back(c == ':' ? '-' : c);
s += ".gcm";
s += traits.bmiExt;
return s;
};

Expand Down Expand Up @@ -360,12 +386,18 @@ std::string emit_ninja_string(const BuildPlan& plan) {

std::string out_line = "build " + escape_ninja_path(cu.object);
if (cu.providesModule) {
out_line += " " + bmi_path(*cu.providesModule);
// Use implicit output (|) so $out only contains the .o file.
// GCC writes BMI implicitly; Clang uses -fmodule-output=$bmi_out.
out_line += " | " + bmi_path(*cu.providesModule);
}
out_line += std::format(" : {} {}", rule, escape_ninja_path(cu.source));
if (!implicit.empty())
out_line += " |" + implicit;
out_line += "\n";
// Clang needs $bmi_out to emit -fmodule-output=$bmi_out
if (cu.providesModule) {
out_line += " bmi_out = " + bmi_path(*cu.providesModule) + "\n";
}
append(std::move(out_line));
}
append("\n");
Expand Down
1 change: 1 addition & 0 deletions src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ 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 scanDepsPath; // clang-scan-deps binary (Clang only)

std::vector<CompileUnit> compileUnits; // topologically sorted
std::vector<LinkUnit> linkUnits;
Expand Down
Loading
Loading