Skip to content

Commit 015d929

Browse files
committed
fix(toolchain): keep -stdlib=libc++ where the std module has always put it
Placing it after the libc++ -isystem flags instead of right after -nostdinc++ changed the std module's command string, and that string is part of the std cache identity (std_build_commands feeds the cache directory name) — every user's std BMIs would have been invalidated for a reordering that buys nothing. Caught only because the first byte-equality check scanned the whole shared cache directory and compared stale entries alongside fresh ones; the check now wipes the std cache first so it compares like with like. Also moves the bundled mcpp module's compile out of build_program.cppm's anonymous namespace into mcpp.build.hostprogram. Growing that namespace reproduced PR#332's clang miscompilation exactly — Segmentation fault: 11 on every macOS build.mcpp e2e, in contract_env, which this change never touched.
1 parent cd0a848 commit 015d929

5 files changed

Lines changed: 227 additions & 187 deletions

File tree

.agents/docs/2026-08-02-host-compile-single-producer-design.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,17 @@ clang/libc++ 的 mcpp,崩得和真 bug 一样。每轮验证:
277277
|---|---|
278278
| build.mcpp 走 ninja / 生成第二张图 | §3:排序约束 + 一个 TU 无增量收益 |
279279
| 把 build.mcpp 塞进主构建图 | §3:循环依赖 |
280-
| 顺手统一**链接** | 主构建链接的是 target 产物、build.mcpp 链接的是宿主 helper,策略本就不同(`staticHostHelper`)。本方案只统一**编译**侧;链接侧留作独立评估 |
280+
| 统一**主构建的**链接侧 | 主构建链接的是 target 产物、build.mcpp 链接的是宿主 helper,策略本就不同(`staticHostHelper`)。`flags.cppm` 的链接装配保持原样 |
281281
| 给 build.mcpp 加多源文件 / C / 汇编支持 | 它是单 TU 程序,这是 L3 的设计选择,不在本方案范围 |
282282

283283
---
284284

285-
## 8. 实施顺序
285+
## 7.5 实施中相对本设计的两处偏差(已落地)
286+
287+
| 偏差 | 原因 |
288+
|---|---|
289+
| **补了 `host_link_tokens`**(§7 原写「链接侧不做」) | `build.mcpp`**一次驱动调用同时编译和链接**,`host_base_flags` 里本就含 `-fuse-ld=lld` / `--rtlib` / `-L` / `-rpath` / `--dynamic-linker`。不覆盖链接侧就根本迁不动它。§7 的排除项因此收窄为「不动**主构建的**链接装配」——`flags.cppm` 的链接侧确实一行没改 |
290+
| **`stdmod` 的 deployment target 仍由它自己追加** | 三处的 flag **顺序**不同:`flags.cppm` 是 dm→deployment→lm,`stdmod` 是 dm→lm→deployment。让生产者统一顺序会改变 `std_build_commands` 字符串 → 按 §6.1 会让每个用户的 std BMI 全量失效。取舍:共享**装配**(手写的 dm 块已删除),只把这一个 flag 的**位置**留在本地并写明原因。位置统一留作后续 —— 届时应与一次本就会改变 std 身份的变更搭车 |
286291

287292
|| 内容 | 规模 | 验收 |
288293
|---|---|---|---|

src/build/build_program.cppm

Lines changed: 1 addition & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import mcpp.platform.process;
1919
import mcpp.toolchain.cppfly; // std_flag (dialect- and c++fly-aware -std= spelling)
2020
import mcpp.toolchain.dialect; // CommandDialect — gnu vs cl.exe spellings
2121
import mcpp.toolchain.fingerprint; // hash_file / hash_string (FNV-1a, 16 hex)
22+
import mcpp.build.hostprogram; // bundled `mcpp` module compile (own module: see its header)
2223
import mcpp.toolchain.hostflags; // the shared host-compile flag producer
2324
import mcpp.toolchain.linkmodel; // shared C-library / clang-cfg-bypass model
2425
import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target/is_mingw_target
@@ -215,188 +216,6 @@ std::vector<std::string> host_base_flags(const mcpp::toolchain::Toolchain& tc,
215216
return f;
216217
}
217218

218-
// The bundled `mcpp` build module — a typed API over the stdout wire protocol
219-
// so build.mcpp can `import mcpp;` instead of `#include`. Its own I/O uses
220-
// C-level primitives in the global module fragment, so the module itself
221-
// needs no std BMI and stays buildable before one exists. (That was once also
222-
// a limit on build.mcpp; it no longer is — a build.mcpp may `import std;` and
223-
// the engine stages the same std module the main build uses.)
224-
// The functions mirror the directive set 1:1; they just print the
225-
// `mcpp:` lines the engine already parses. Embedded in the binary (not shipped as
226-
// a file) so it always matches this mcpp's protocol.
227-
// NOTE: the module declaration line uses a `@MODULE@` placeholder (substituted
228-
// with `export module` when written) so mcpp's own line-based module scanner does
229-
// not mistake this embedded string for build_program.cppm exporting a 2nd module.
230-
constexpr std::string_view kMcppModuleSource = R"CPP(module;
231-
#include <cstdio>
232-
#include <cstdlib>
233-
@MODULE@ mcpp;
234-
export namespace mcpp {
235-
inline void cxxflag(const char* flag) { std::printf("mcpp:cxxflag=%s\n", flag); }
236-
inline void cflag(const char* flag) { std::printf("mcpp:cflag=%s\n", flag); }
237-
inline void link_lib(const char* name) { std::printf("mcpp:link-lib=%s\n", name); }
238-
inline void link_search(const char* dir) { std::printf("mcpp:link-search=%s\n", dir); }
239-
inline void define(const char* name) { std::printf("mcpp:cfg=%s\n", name); }
240-
inline void generated(const char* path) { std::printf("mcpp:generated=%s\n", path); }
241-
inline void source(const char* path) { std::printf("mcpp:source=%s\n", path); }
242-
inline void include_dir(const char* dir) { std::printf("mcpp:include-dir=%s\n", dir); }
243-
inline void include_dir_after(const char* dir) { std::printf("mcpp:include-dir-after=%s\n", dir); }
244-
inline void rerun_if_changed(const char* path) { std::printf("mcpp:rerun-if-changed=%s\n", path); }
245-
inline void rerun_if_env_changed(const char* var) { std::printf("mcpp:rerun-if-env-changed=%s\n", var); }
246-
// ── environment contract (read side; values injected by the engine) ─────
247-
inline const char* env_or(const char* n) { const char* v = std::getenv(n); return v ? v : ""; }
248-
inline const char* target() { return env_or("MCPP_TARGET"); }
249-
inline const char* target_os() { return env_or("MCPP_TARGET_OS"); }
250-
inline const char* target_arch() { return env_or("MCPP_TARGET_ARCH"); }
251-
inline const char* target_env() { return env_or("MCPP_TARGET_ENV"); }
252-
inline const char* host() { return env_or("MCPP_HOST"); }
253-
inline const char* profile() { return env_or("MCPP_PROFILE"); }
254-
inline const char* out_dir() { return env_or("MCPP_OUT_DIR"); }
255-
inline const char* manifest_dir() { return env_or("MCPP_MANIFEST_DIR"); }
256-
inline bool has_feature(const char* name) {
257-
char buf[256] = "MCPP_FEATURE_";
258-
unsigned long o = 13;
259-
for (const char* p = name; *p && o + 1 < sizeof buf; ++p, ++o) {
260-
char c = *p;
261-
buf[o] = (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A')
262-
: ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_';
263-
}
264-
buf[o] = 0;
265-
return std::getenv(buf) != nullptr;
266-
}
267-
// mcpp#241: resolved install dir of a declared dependency (by its package
268-
// name), or "" if not found. Same sanitize as has_feature; wraps
269-
// MCPP_DEP_<SANITIZED_NAME>_DIR.
270-
inline const char* dep_dir(const char* name) {
271-
char buf[256] = "MCPP_DEP_";
272-
unsigned long o = 9;
273-
for (const char* p = name; *p && o + 5 < sizeof buf; ++p, ++o) {
274-
char c = *p;
275-
buf[o] = (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A')
276-
: ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_';
277-
}
278-
buf[o++] = '_'; buf[o++] = 'D'; buf[o++] = 'I'; buf[o++] = 'R'; buf[o] = 0;
279-
return env_or(buf);
280-
}
281-
}
282-
)CPP";
283-
284-
// Compile the bundled `mcpp` module into `bdir` and return the extra flags the
285-
// build.mcpp compile needs to import it (the object `mcpp.o` is linked alongside).
286-
// GCC : -fmodules → gcm.cache/mcpp.gcm + mcpp.o; build.mcpp compiles from
287-
// `bdir` (cwd) so GCC finds gcm.cache/mcpp.gcm.
288-
// Clang : --precompile → mcpp.pcm, then -c → mcpp.o; pass -fmodule-file=mcpp=<pcm>.
289-
// Does the source contain `import <name>;`?
290-
//
291-
// A plain substring search is not enough here: "import std" is a prefix of
292-
// "import std.compat", so the naive test reports both for a program that
293-
// only imports the latter, and mcpp would build a std BMI nobody asked for.
294-
// Match the whole module name and require the terminating `;`, tolerating
295-
// the whitespace the grammar allows. Occurrences inside comments or string
296-
// literals still match — over-detection costs one cached BMI lookup, never
297-
// a wrong build, and that is the same trade the `import mcpp` check has
298-
// always made.
299-
bool imports_module(std::string_view src, std::string_view name) {
300-
constexpr std::string_view kImport = "import";
301-
std::size_t pos = 0;
302-
while ((pos = src.find(kImport, pos)) != std::string_view::npos) {
303-
std::size_t i = pos + kImport.size();
304-
// `importfoo` is not an import.
305-
if (i >= src.size() || (src[i] != ' ' && src[i] != '\t')) { ++pos; continue; }
306-
while (i < src.size() && (src[i] == ' ' || src[i] == '\t')) ++i;
307-
if (src.compare(i, name.size(), name) == 0) {
308-
std::size_t j = i + name.size();
309-
while (j < src.size() && (src[j] == ' ' || src[j] == '\t')) ++j;
310-
if (j < src.size() && src[j] == ';') return true;
311-
}
312-
++pos;
313-
}
314-
return false;
315-
}
316-
317-
// What the bundled `mcpp` module contributes to the build.mcpp compile.
318-
struct McppModule {
319-
std::vector<std::string> useFlags; // how the consumer names the BMI
320-
fs::path object; // linked alongside build.mcpp
321-
};
322-
323-
std::expected<McppModule, std::string>
324-
build_mcpp_module(const fs::path& bdir, const fs::path& compiler,
325-
const std::vector<std::string>& base, const std::string& stdFlag,
326-
const mcpp::toolchain::Toolchain& tc,
327-
const std::vector<std::pair<std::string, std::string>>& env) {
328-
std::error_code ec;
329-
fs::path cppm = bdir / "mcpp.cppm";
330-
std::string moduleSrc(kMcppModuleSource);
331-
if (auto p = moduleSrc.find("@MODULE@"); p != std::string::npos)
332-
moduleSrc.replace(p, std::string_view("@MODULE@").size(), "export module");
333-
{ std::ofstream os(cppm, std::ios::trunc);
334-
os << moduleSrc;
335-
if (!os) return std::unexpected(std::string("could not write mcpp module source")); }
336-
337-
auto run = [&](std::vector<std::string> argv, const char* what)
338-
-> std::expected<void, std::string> {
339-
auto r = mcpp::platform::process::capture_exec(argv, env, bdir.string());
340-
if (r.exit_code != 0)
341-
return std::unexpected(std::format("mcpp module {} failed (exit {}):\n{}",
342-
what, r.exit_code, r.output));
343-
return {};
344-
};
345-
auto with_base = [&](std::vector<std::string> head) {
346-
for (auto& b : base) head.push_back(b);
347-
return head;
348-
};
349-
350-
// Dispatch on the SAME module table the main build uses (BmiTraits +
351-
// CommandDialect), not on a local is_clang/else. That is what makes a
352-
// toolchain family work here as soon as it works there — adding cl.exe
353-
// needed no new pipeline, only this row.
354-
const auto traits = mcpp::toolchain::bmi_traits(tc);
355-
const auto& dial = mcpp::toolchain::dialect_for(tc);
356-
McppModule out;
357-
358-
if (tc.compiler == mcpp::toolchain::CompilerId::MSVC) {
359-
// cl produces the .ifc and the .obj in one step.
360-
fs::path ifc = bdir / ("mcpp" + std::string(traits.bmiExt));
361-
out.object = bdir / ("mcpp" + std::string(dial.objExt));
362-
std::vector<std::string> argv{compiler.string()};
363-
for (auto f : dial.alwaysFlagsArgv) argv.emplace_back(f);
364-
argv.push_back(stdFlag);
365-
argv.push_back("/interface");
366-
for (auto f : dial.forceCxxLangArgv) argv.emplace_back(f);
367-
argv.push_back(dial.compileOnly == std::string_view("/c") ? "/c" : "-c");
368-
argv.push_back("mcpp.cppm");
369-
argv.push_back("/ifcOutput"); argv.push_back(ifc.string());
370-
argv.push_back(std::string(dial.outputObjPrefix) + out.object.string());
371-
if (auto r = run(with_base(std::move(argv)), "compile"); !r)
372-
return std::unexpected(r.error());
373-
out.useFlags = mcpp::toolchain::bmi_reference_tokens(" /reference mcpp=", ifc);
374-
return out;
375-
}
376-
377-
out.object = bdir / ("mcpp" + std::string(dial.objExt));
378-
if (mcpp::toolchain::is_clang(tc)) {
379-
fs::path pcm = bdir / ("mcpp" + std::string(traits.bmiExt));
380-
if (auto r = run(with_base({compiler.string(), stdFlag, "--precompile",
381-
"mcpp.cppm", "-o", pcm.string()}), "precompile"); !r)
382-
return std::unexpected(r.error());
383-
if (auto r = run(with_base({compiler.string(), stdFlag, "-c",
384-
pcm.string(), "-o", out.object.string()}), "object"); !r)
385-
return std::unexpected(r.error());
386-
out.useFlags = mcpp::toolchain::bmi_reference_tokens("-fmodule-file=mcpp=", pcm);
387-
return out;
388-
}
389-
390-
// GCC: BMIs are implicit under <cwd>/gcm.cache, so nothing to name.
391-
if (auto r = run(with_base({compiler.string(), stdFlag,
392-
std::string(mcpp::toolchain::bmi_traits(tc).compileModulesFlag).empty()
393-
? "-fmodules" : "-fmodules",
394-
"-c", "mcpp.cppm", "-o", out.object.string()}), "compile"); !r)
395-
return std::unexpected(r.error());
396-
out.useFlags = {"-fmodules"};
397-
return out;
398-
}
399-
400219
// ── Cache (line-based; one record per line, internal format) ───────────────
401220
// program <hash>
402221
// compiler <hash>

0 commit comments

Comments
 (0)