Date: 2026-05-26 Status: Proposed
当用户代码有编译错误时,默认 mcpp build 输出应聚焦在用户真正需要修复的
编译器诊断上:
- 不显示 ninja 进度信息,如
[1/9] OBJ obj/main.o - 不把
env LD_LIBRARY_PATH=...这种运行时环境前缀暴露在失败命令行里 - 不重复打印同一段 ninja / compiler 输出
--verbose仍保留足够的构建细节,便于定位 mcpp 自身问题
复现目录:
cd /home/speak/test/mcpp/helloworld
mcpp build样例里 src/main.cpp 当前有明确语法错误:
int main(int argc, char* argv[]) {
hw::
return 0;
}当前普通 mcpp build 的关键输出:
ninja: Entering directory `/home/speak/test/mcpp/helloworld/target/...'
[1/2] OBJ obj/main.o
FAILED: obj/main.o
env LD_LIBRARY_PATH='/home/speak/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/lib:...'${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} /home/speak/.mcpp/.../clang++ ...
/home/speak/test/mcpp/helloworld/src/main.cpp:8:5: error: expected unqualified-id
8 | return 0;
| ^
1 error generated.
ninja: build stopped: subcommand failed.
error: auto-installing default toolchain gcc@15.1.0-musl failed: ...
为单独观察完整 backend 失败路径,使用已有 /home/speak/.mcpp 配置运行:
MCPP_HOME=/home/speak/.mcpp mcpp build --no-cache可以稳定看到同一段 ninja 输出被打印两次:第一次由 backend 直接 fputs,
第二次被塞入 BuildError.message 后由 CLI 的 ui::error() 再打印。
src/build/ninja_backend.cppm
NinjaBackend::build()生成build.ninja,然后执行ninja -C <outputDir> 2>&1。- 非 verbose 模式也会在失败时输出完整 ninja 捕获文本:
if (opts.verbose || !ok) {
std::fputs(out.c_str(), stdout);
}- 失败时又把同一份
out放入BuildError.message:
return std::unexpected(BuildError{
std::format("ninja failed (exit non-zero):\n{}", out),
plan.outputDir / "build.ninja"});src/cli.cppm
run_build_plan()收到失败后再次打印:
mcpp::ui::error(r.error().message);这就是重复输出的直接原因。
src/cli.cppm::try_fast_build() 在 build cache 命中时直接运行旧
build.ninja。如果 ninja 非零退出,它会先打印输出,再返回 nullopt:
if (!ok) {
if (!verbose) std::fputs(out.c_str(), stdout);
return std::nullopt;
}随后 cmd_build() 进入完整 prepare_build()。在当前复现环境中,PATH 下的
mcpp 使用的 MCPP_HOME 没有默认 toolchain,所以用户代码编译错误后又追加了
一次默认 toolchain 自动安装失败提示,导致错误主题混杂。
src/toolchain/probe.cppm
detect()填充Toolchain::compilerRuntimeDirscompiler_env_prefix()把 runtime dirs 转成 shell 前缀
return mcpp::platform::linux_::build_ld_library_path_prefix(dirs);src/platform/linux.cppm
return std::format("env LD_LIBRARY_PATH={}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}} ",
mcpp::platform::shell::quote(joined));src/build/flags.cppm
f.toolEnv = mcpp::toolchain::compiler_env_prefix(plan.toolchain);src/build/ninja_backend.cppm
toolenv = env LD_LIBRARY_PATH='...'${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
rule cxx_object
command = $toolenv $cxx $cxxflags -c $in -o $outninja 在命令失败时会打印失败 command,因此这个 toolenv 会直接暴露给用户。
默认构建命令改为安静模式:
ninja --quiet -C <outputDir>作用:
- 隐藏
ninja: Entering directory ... - 隐藏
[x/y] DESCRIPTION ... - 保留失败时的必要 stderr/stdout
已在复现目录手动验证:ninja --quiet -C ... 不再显示 [x/y] 进度,但仍会
显示失败 command 和 compiler diagnostic。因此它只能解决第一类噪声,不能单独
解决 LD_LIBRARY_PATH 和重复输出。
--verbose 模式保持当前行为:不加 --quiet,继续追加 -v。
把运行时库路径从 build.ninja 规则里移出,改为启动 ninja 子进程时注入环境。
建议新增跨平台 API:
namespace mcpp::platform::env {
struct ScopedEnv {
ScopedEnv(std::string key, std::optional<std::string> value);
~ScopedEnv();
};
std::string path_list_separator();
std::string runtime_library_path_key();
std::string prepend_path_list(std::string_view key,
std::span<const std::filesystem::path> dirs);
} // namespace mcpp::platform::env平台策略:
| 平台 | 变量 | 说明 |
|---|---|---|
| Linux | LD_LIBRARY_PATH |
当前实际需要,替代 toolenv |
| macOS | 空 | 不设置 DYLD_LIBRARY_PATH;它会影响 ninja 自身和系统 framework 的 dyld 解析,依赖 toolchain/rpath |
| Windows | PATH |
如果私有工具链 DLL 需要搜索路径,按 ; prepend;当前可先空实现 |
NinjaBackend::build() 中:
- 从
plan.toolchain.compilerRuntimeDirs计算 scoped env - 在
process::capture(ninjaCmd)前创建ScopedEnv build.ninja不再写toolenv变量- 所有规则从:
command = $toolenv $cxx $cxxflags -c $in -o $out改为:
command = $cxx $cxxflags -c $in -o $out这样失败时 ninja 最多打印 compiler 命令,不会再出现
env LD_LIBRARY_PATH=...。同时 clang-scan-deps、clang++、llvm-ar 等
ninja 子命令都会继承同一份环境,不需要每条 rule 单独拼 shell 前缀。
后续如果要进一步消除全局环境 mutation,可把 ScopedEnv 升级为
process::run({ .argv, .cwd, .env }),POSIX 走 fork/execve,Windows 走
CreateProcessW environment block。但第一阶段用 RAII env 已能满足当前目标,
改动面更小。
需要规定:ninja 输出只能由一个层级负责展示。
推荐方案:
struct BuildError {
std::string summary; // "build failed"
std::string diagnosticOutput; // 过滤后的 compiler/ninja 输出
std::optional<std::filesystem::path> where;
};调用关系:
NinjaBackend::build()只捕获并返回,不直接fputsrun_build_plan()负责打印一次- 默认模式打印:
error: build failed
<filtered compiler diagnostics>
- verbose 模式打印完整 ninja 输出,但仍只打印一次
如果暂时不想扩展 BuildError,最小改法是:
NinjaBackend::build()失败时不再fputs(out)BuildError.message保留一份输出
但这个最小改法仍会把整段输出挂在 error: 后面,可读性不如结构化字段。
--quiet 解决进度行,但失败输出仍会包含:
FAILED: obj/main.o
<compiler command>
ninja: build stopped: subcommand failed.
建议增加 filter_ninja_output(out, flags, mode):
默认模式移除:
ninja: Entering directory ...ninja: build stopped: subcommand failed.FAILED: ...- 已知工具命令行:以当前
cxxBinary、ccBinary、arBinary、scanDepsPath开头的行 - 旧 build.ninja 中残留的
env LD_LIBRARY_PATH=... <tool>命令行 - ninja 进度行:
^\[[0-9]+/[0-9]+\]
默认模式保留:
- compiler warning/error diagnostic
- 源码路径、行列号和 caret
- linker diagnostic
- mcpp 自己的 dyndep / manifest diagnostic
--verbose 模式不过滤。
目标默认输出示例:
Compiling helloworld v0.1.0 (.)
error: build failed
/home/speak/test/mcpp/helloworld/src/main.cpp:8:5: error: expected unqualified-id
8 | return 0;
| ^
1 error generated.
try_fast_build() 当前在失败时“先打印失败,再回退完整构建”。这会导致:
- 同一编译错误可能先由 fast-path 打印一次,再由完整构建打印一次
- 旧
build.ninja的 toolchain 和当前 config 不一致时,用户会看到两个不同主题的错误
建议把返回值从 std::optional<int> 改成 tri-state:
enum class FastBuildKind { NotApplicable, Success, BuildFailed, StaleOrInvalid };
struct FastBuildResult {
FastBuildKind kind;
int exitCode = 0;
std::string output;
};策略:
- cache 不存在、fingerprint 不匹配、
build.ninja缺失:NotApplicable - ninja 成功:
Success - ninja 报
loading build.ninja/unknown target/ manifest 结构明显不匹配:StaleOrInvalid,静默回到完整 prepare - 普通 compile/link 失败:
BuildFailed,直接按同一套过滤和单次打印逻辑返回, 不再触发自动安装默认 toolchain
这样用户代码错误不会被后续 toolchain resolve/install 错误污染。
-
先去重输出
- 调整
NinjaBackend::build()和run_build_plan()的打印职责 - 增加 syntax-error e2e,断言同一 compiler error 只出现一次
- 调整
-
移动 toolenv 到 scoped env
- 增加
platform::env::ScopedEnv emit_ninja_string()删除toolenv变量和$toolenv前缀NinjaBackend::build()和try_fast_build()启动 ninja 前设置运行时库路径- 断言生成的
build.ninja不包含LD_LIBRARY_PATH
- 增加
-
安静化 ninja 默认输出
- 非 verbose 加
--quiet - 增加
filter_ninja_output() - 断言默认失败输出不包含
[1/、FAILED:、ninja: build stopped
- 非 verbose 加
-
修正 fast-path 失败
- 将
try_fast_build()改为 tri-state - 编译失败直接返回失败,不进入完整 prepare
- stale/invalid ninja 才允许静默回退
- 将
新增或调整 E2E:
bash tests/e2e/05_errors.sh新增专门用例,例如 tests/e2e/48_build_error_output.sh:
- 创建项目并写入语法错误
- 运行
mcpp build - 断言输出包含源码诊断:
src/main.cpp:*: error: - 断言输出不包含:
LD_LIBRARY_PATH=[1/FAILED:ninja: Entering directoryninja: build stopped
- 断言
expected unqualified-id只出现一次 - 运行
mcpp build --verbose,断言 verbose 仍保留 ninja/command 细节 - 检查生成的
build.ninja不包含toolenv和LD_LIBRARY_PATH
相关单元测试:
filter_ninja_output():覆盖 progress、FAILED、command、compiler diagnostic 混合文本ScopedEnv:覆盖变量不存在、变量已存在、prepend 后析构恢复
本地验证命令:
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 bash tests/e2e/05_errors.sh
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 bash tests/e2e/48_build_error_output.sh
mcpp buildScopedEnv会短暂修改当前进程环境。mcpp CLI 当前是单构建流程,风险可控; 如果未来 backend 并发运行多个 ninja,应升级为ProcessOptions.env。- 过滤 command line 不能误删 compiler diagnostic。规则应只删除“已知工具路径开头” 的整行,不做宽泛字符串匹配。
--verbose必须不丢信息,否则会降低 mcpp 自身问题的可诊断性。- fast-path 回退判断要保守:无法确认是 stale/invalid 时,按普通构建失败处理并 打印一次,避免再次污染错误输出。