Skip to content

[Bug & RFC] 系统工具链 build.mcpp 执行失败缺陷、原生宿主模式与工作区继承规范提案 #527

Description

@elel-code

概述

本 Issue 与架构改进提案(RFC)包含以下内容:

  1. 致命缺陷 (Bug 1):在配置 MCPP_TOOLCHAIN=system 时,由于传入空编译器路径导致编译 build.mcpp 时崩溃(posix_spawnp('') failed)。
  2. 主要缺陷 (Bug 2):工作区(Workspace)根目录中的 [build] 配置项未透传给成员包(Member)。
  3. 架构提案 (RFC 1):引入原生宿主支持(toolchain = "system" 与 sysroot = "system"),明确区分“第三方依赖包 RUNPATH”与“系统基础 Libc 寻址”。
  4. 架构提案 (RFC 2):将核心语言方言(如 -fno-exceptions)自动同步至 import std; 标准库 BMI 预编译流程。
  5. 架构提案 (RFC 3):提供完备的 [workspace.*] 继承规范(特别是统一声明 standard = 26 / [workspace.build]),彻底杜绝 C++ 模块化工程因标准版本不一致引发的跨包 BMI 解析崩溃。
  6. 架构提案 (RFC 4):结合现代 C++ Language Server(如 clice / clangd)的全局索引机制,为多包 Monorepo 工作区提供一键聚合导出 compile_commands.json 的 --cdb-aggregate 选项。

🐛 第一部分:已核实的源码级缺陷报告 (Confirmed Bugs)

Bug 1: MCPP_TOOLCHAIN=system 时无法启动 build.mcpp(Exit 127: posix_spawnp(''))

  • 严重程度:Critical / Blocker(致命阻断)
  • 影响组件:mcpp.build.prepare, mcpp.build.build_program
  • 复现环境:Linux / macOS,配置 [toolchain] default = "system" 或环境变量 MCPP_TOOLCHAIN=system,且工程中包含 build.mcpp 构建脚本。

最小复现步骤

  1. 创建一个包含 build.mcpp 的最小工程:
    # mcpp.toml
    [package]
    name = "demo"
    version = "0.1.0"
    standard = 26
    
    [toolchain]
    linux = "system"
    // build.mcpp
    #include <iostream>
    int main() { return 0; }
  2. 执行构建命令:mcpp build。

实际报错

在编译主程序前构建立即中断:

mcpp module compile failed (exit 127): posix_spawnp('') failed (error 2)
build.mcpp failed to compile (exit 127)

源码调用链与根因溯源

  1. 未赋值的 explicit_compiler:
    在 src/build/prepare.cppm:1705-1706 中,当 *tcSpec == "system" 时,进入了空分支:

    // src/build/prepare.cppm:1705-1706
    } else if (tcSpec.has_value() && *tcSpec == "system") {
        // Explicit user opt-in to system PATH compiler — kept as escape hatch.
    }

    局部变量 std::filesystem::path explicit_compiler 保持为空字符串 ""。
    随后在 src/build/prepare.cppm:1882 调用 mcpp::toolchain::detect(explicit_compiler, ...),探测函数成功从系统 PATH 找到了编译器并将真实绝对路径存入 tc->binaryPath(如 /usr/bin/g++)。

  2. Host 工具链闭包返回空路径:
    在 src/build/prepare.cppm:2205-2208 中构造 Host 工具链闭包时,非交叉编译分支直接返回了仍为空的 explicit_compiler:

    // src/build/prepare.cppm:2205-2208
    auto host_tc_for_build_program = [&]() -> std::expected<
            std::pair<std::filesystem::path, mcpp::toolchain::Toolchain>, std::string> {
        if (overrides.target_triple.empty())
            return std::pair{explicit_compiler, *tc}; // ⚠️ 致命 Bug:explicit_compiler 为空!
  3. posix_spawnp 执行空路径:
    该空路径作为 hostCompiler 传递给 src/build/build_program.cppm:573-580。底层 posix_spawnp("") 尝试执行空文件名,操作系统立即返回 ENOENT(error 2),子进程以 127 退出码异常终止。

建议修复补丁 (Proposed Patch)

--- a/src/build/prepare.cppm
+++ b/src/build/prepare.cppm
@@ -2205,8 +2205,10 @@ export std::expected<BuildContext, std::string> prepare_build(
     auto host_tc_for_build_program = [&]() -> std::expected<
             std::pair<std::filesystem::path, mcpp::toolchain::Toolchain>, std::string> {
         if (overrides.target_triple.empty())
-            return std::pair{explicit_compiler, *tc};
+            return std::pair{explicit_compiler.empty() ? tc->binaryPath : explicit_compiler, *tc};

Bug 2: Workspace 根目录 [build] 表项无法透传给成员包

  • 严重程度:Major(主要缺陷)
  • 现象描述:
    在 Workspace 根目录的 mcpp.toml 中配置了全局构建参数(例如 [build] cxxflags = ["-fno-exceptions"] 或 bmi_schedule = "on"),但在执行 mcpp build 或 mcpp build -p <member> 时,各子成员包完全无法继承这些构建标志,仍按默认参数编译。
  • 源码定位:
    在 src/build/prepare.cppm:965-980 中,Workspace 根仅向成员包合并了 toolchain、targetOverrides 和 indices,遗漏了 buildConfig。

🚀 第二部分:架构改进与功能提案 (RFC / Feature Proposals)

RFC 1: 引入原生宿主支持(toolchain = "system" 与 sysroot = "system")

问题背景

对于系统级软件(如 Wayland 合成器、显示服务、系统守护进程、与 Linux 内核特性及 Mesa/Vulkan/GBM 显卡驱动深度交互的工程),mcpp 默认的“全沙盒托管机制(Managed Sandbox)”会引发严重的运行时冲突:

  1. 驱动与动态链接器撕裂:改写 PT_INTERP 或注入沙盒 glibc 的 DT_RUNPATH,会导致程序加载宿主驱动(如 /usr/lib64/dri/*_dri.so)或 Vulkan ICD 时发生动态链接器符号版本冲突或段错误。
  2. 现存 sysroot 语法的局限:src/build/prepare.cppm:2005 强制要求 sysroot 必须是 xim: 包或 ""(裸机零 libc),无法表达“直接使用宿主操作系统的 libc 与加载器”。

架构分析:依赖库 RUNPATH 与 Libc 寻址的分层解耦

动态链接中必须清晰区分两类不同的动态库及其寻址诉求:

  • 第三方业务依赖动态库(Package .so):由 mcpp 托管下载或本地编译输出。必须注入 DT_RUNPATH(本地开发期注入缓存绝对路径,发布期通过 mcpp pack 重写为 $ORIGIN/../lib)。
  • 基础 C 运行时(glibc / libc.so.6 / 加载器 ld.so):
    • 托管沙盒模式(Managed):适合独立 CLI / 跨发行版免安装工具,保持现有的 xim:glibc 并注入沙盒 RUNPATH;
    • 原生宿主模式(Host-Native):直接使用系统 /lib64/ld-linux-x86-64.so.2 与 /usr/lib64,绝不向产物注入任何指向 toolchain payload 的 -Wl,-rpath。
graph TD
    A[可执行文件 Binary] --> B[基础 C 运行时 (glibc / ld-linux)]
    A --> C[第三方/远程依赖库 (Package .so)]
    
    B -->|原生宿主模式: sysroot='system'| D[系统 /lib64/ld-linux 与 /etc/ld.so.cache]
    B -->|托管沙盒模式: sysroot='xim:glibc'| E[沙盒 ld-linux 与 沙盒 glibc RUNPATH]
    
    C -->|开发期寻址| F[mcpp 缓存/产物目录 RUNPATH]
    C -->|打包发布期 (mcpp pack)| G[相对路径 $ORIGIN/../lib]
Loading

提案配置语法

在 mcpp.toml 中支持显式声明宿主原生模式:

[target.x86_64-linux-gnu]
toolchain = "system"   # 使用宿主 /usr/bin/g++ 或 clang++
sysroot   = "system"   # 使用宿主 glibc、头文件与 /lib64/ld-linux (不注入 libc RUNPATH)

RFC 2: 核心语言方言(如 -fno-exceptions)自动同步至标准库 BMI

问题背景

当项目在 cxxflags 中关闭异常(-fno-exceptions)时,mcpp 目前不会将该标志同步至 src/toolchain/stdmod.cppm 预编译的标准库 std.gcm / std.pcm,导致源文件在 import std; 时编译器报方言不匹配错误。

建议方案

在生成标准库 BMI 时,自动将关键语言方言(-fno-exceptions、-fno-rtti、-fcoroutines)提升并同步至 BMI 预编译参数中,无需用户寻找隐晦的配置字段。


RFC 3: 完备的多包 Workspace 配置继承(特别是 C++ 标准与全局构建项)

问题背景:C++ 模块化体系对标准一致性的强制要求

在 C++20/C++23/C++26 模块化(C++ Modules)体系下,标准版本的一致性是强制性的:
若 Workspace 内部成员包 A 使用 -std=c++26 编译,成员包 B 使用 -std=c++23 编译,包 B 在尝试 import 包 A 导出的 C++26 模块(BMI)时,GCC/Clang 编译器会直接报错拒绝(标准版本与 AST 签名不兼容)。

在包含众多成员包的 Monorepo 中,逐一手写 standard = 26、[build] 和 [target] 极易发生配置漂移与冗余。

提案继承模型

  1. Workspace 根目录 (mcpp.toml) 统一声明:

    [workspace]
    members = ["apps/*", "packages/*"]
    
    # ① 全局 C++ 语言版本与包元信息继承 (统一 C++26,杜绝模块 BMI 标准漂移)
    [workspace.package]
    standard = 26               # 统一 C++26,所有成员包自动继承
    edition  = "2026"
    version  = "0.1.0"
    license  = "Apache-2.0"
    
    # ② 全局构建与方言选项继承 (彻底解决 Bug 2)
    [workspace.build]
    cxxflags         = ["-fno-exceptions", "-Wall"]
    dialect_cxxflags = ["-fno-exceptions"]
    bmi_schedule     = "on"
    jobs             = "auto"
    
    # ③ 全局目标平台与原生系统模式继承
    [workspace.target.x86_64-linux-gnu]
    toolchain = "system"
    sysroot   = "system"
    
    # ④ 统一依赖版本表
    [workspace.dependencies]
    wayland-client = { version = "1.23.0" }
  2. 成员包 (apps/compositor/mcpp.toml) 轻量继承:

    [package]
    name             = "compositor"
    standard.workspace = true    # 自动继承全局 C++26 (或未显式声明时默认继承)
    version.workspace  = true
    
    [build]
    inherit.workspace  = true    # 自动继承根目录 [workspace.build]
    
    [dependencies]
    wayland-client.workspace = true

RFC 4: 结合现代语言服务器(clice / clangd)支持多包 Workspace 聚合导出 CDB (compile_commands.json)

问题背景与 Language Server 架构诉求

在现代 C++ 开发生态中,新兴的高性能语言服务器(如基于 LLVM/Clang 构建、深度支持 C++20 模块编译图与模板智能推导的 clice,以及 clangd)在初始化时均以工程的 workspace_root 作为根上下文:

  • clice 的工作区探测机制:在 clice 的工作区状态管理(Workspace::discover_compile_commands)中,语言服务器首先在 workspace_root 根目录下查找 compile_commands.json,并以此构建整个工作区跨 Translation Unit、跨 C++20 模块依赖的有向编译图(CompileGraph)与全局符号索引。
  • 当前 mcpp 的痛点:目前 mcpp build --workspace --configure-only 会为每个 Member 在其子目录中分散生成各自局部的 compile_commands.json。这导致当开发者在 Workspace 根目录打开 VS Code / Neovim / CLion 时,clice / clangd 无法在根目录探测到统一的全局编译数据库,导致跨包模块跳转、全局语义高亮与后台跨包索引全部失效。

建议方案

为 CLI 增加 --cdb-aggregate 开关:

mcpp build --workspace --configure-only --cdb-aggregate

在 Workspace 根目录下输出一份汇聚了所有 Member 编译单元的完整全局 compile_commands.json,使 clice 与 clangd 等现代 LSP 能够无缝接入并完成全工作区的跨包符号分析与模块导航。

Activity

Sunrisepeak commented on Aug 29, 2026

@Sunrisepeak
Member

Q1: 致命缺陷 (Bug 1):在配置 MCPP_TOOLCHAIN=system 时,由于传入空编译器路径导致编译 build.mcpp 时崩溃(posix_spawnp('') failed)。

A1: mcpp 工具链是自管理, 目前不支持 system工具链, 只能使用xlings生态已经有的工具链和工具从而保证任意系统可用。 (system工具链多数工具链不满足mcpp对 import std必须支持的要求, 所以这是刻意设计), 为什么要使用system工具链, 是遇到什么问题了吗

Q2: 主要缺陷 (Bug 2):工作区(Workspace)根目录中的 [build] 配置项未透传给成员包(Member)。
架构提案

后面更新workspace 继承机制 设计以及文档说明

Q3: (RFC 1):引入原生宿主支持(toolchain = "system" 与 sysroot = "system"),明确区分“第三方依赖包 RUNPATH”与“系统基础 Libc 寻址”。

参考 A1 , mcpp的运行时默认是不依赖任何Host 这个是设计决策, 是保证项目可验证/复现 + 跨linux发行版本的要求。并且mcpp在设计上任何关于要依赖Host的设计都会相当谨慎, 除非有必要场景。所以可以先提供提真实且必要/必须的场景说明

Q4: 架构提案 (RFC 2):将核心语言方言(如 -fno-exceptions)自动同步至 import std; 标准库 BMI 预编译流程。
Q5: 架构提案 (RFC 3):提供完备的 [workspace.*] 继承规范(特别是统一声明 standard = 26 / [workspace.build]),彻底杜绝 C++ 模块化工程因标准版本不一致引发的跨包 BMI 解析崩溃。

A4/5: 暂时可以使用 dialect_cxxflags 进行配置, 后面优化相关配置体验

Q6: 架构提案 (RFC 4):结合现代 C++ Language Server(如 clice / clangd)的全局索引机制,为多包 Monorepo 工作区提供一键聚合导出 compile_commands.json 的 --cdb-aggregate 选项。

A6: 这个功能后期版本引入

self-assigned this
on Aug 29, 2026

elel-code commented on Aug 29, 2026

@elel-code
Author

1. 系统库直接动态链接被私有加载器拦截

在系统级开发(如 Wayland 合成器、Mesa/Vulkan 扩展、GBM 显存管理)中,代码直接动态链接宿主系统库(如 -lgbm):

1.1 最小复现用例与配置

# mcpp.toml
[package]
name = "mcpp_driver_test"
version = "0.1.0"
standard = 23

[build]
cxxflags = ["-I/usr/include"]
ldflags  = ["-L/usr/lib", "-lgbm"]
// src/main.cpp
#include <iostream>
#include <gbm.h>

int main() {
    std::cout << "[Direct Link] Initializing GBM via direct dynamic linking..." << std::endl;
    struct gbm_device* gbm = gbm_create_device(-1);
    std::cout << "[Direct Link] gbm_create_device returned: " << gbm << std::endl;
    return 0;
}

1.2 mcpp build 默认模式执行结果

$ mcpp build
   Resolving toolchain
    Resolved gcc@16.1.0 → @mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++
      Target x86_64-linux-gnu → x86_64-unknown-linux-gnu
    Inferred sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}]
    Inferred target mcpp_driver_test (bin from src/main.cpp)
   Compiling mcpp_driver_test v0.1.0 (.)
error: runtime closure validation failed (proven Linux ELF defect)
runtime closure for /tmp/mcpp_driver_test/target/x86_64-linux-gnu/b60c728604f39120/bin/mcpp_driver_test cannot be satisfied: libgbm.so.1 not found on the search path this artifact will actually use.
       Its PT_INTERP is a private loader, so the host's /usr/lib is NOT consulted — the program will fail to start with "cannot open shared object file".
       Fix: install the provider into the selected SubOS (`xlings install <pkg>`), or declare the dependency so mcpp resolves it.

1.3 对照组:宿主系统编译器直接编译与执行

$ g++ src/main.cpp -lgbm -o host_native_bin
$ ./host_native_bin
[Direct Link] Initializing GBM via direct dynamic linking...
[Direct Link] gbm_create_device returned: 0

1.4 机制与胶水成本分析

  1. 私有加载器的主动隔离(src/platform/elf_runtime.cppm:880-891):

    // Hermetic means the artifact's PT_INTERP is a private loader whose
    // entire search path mcpp computed: RPATH/RUNPATH + payloads + farm,
    // with no host defaults and no ld.so.cache.

    默认模式下,产物被写入私有 PT_INTERP,主动忽略了宿主 /etc/ld.so.cache 与 /usr/lib。

  2. 现有规避配置的局限(src/platform/elf_runtime.cppm:908-917):
    开启 [build] allow_host_libs = true 仅将静态校验降级为警告,但二进制的 PT_INTERP 仍是私有加载器。运行期依然无法解析宿主共享库的级联依赖(例如 libgbm.so.1 依赖的 libdrm.so.2),程序仍会启动失败。

  3. 外部胶水成本:
    在不提供原生宿主模式的前提下,若要在宿主运行上述二进制,必须在构建流程外部引入胶水操作:

    • 使用 patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 强制重写 ELF 头并移除私有 RPATH;
    • 或编写 shell 包装层注入 LD_LIBRARY_PATH 并同步拷贝宿主驱动。

2. Linux 发行版官方打包离线断网沙箱约束

2.1 离线打包环境中的执行表现

在 Arch Linux(makepkg)、Fedora(mock)和 Debian(sbuild)的官方打包流程中,构建在禁用外部网络的独立 chroot 沙箱中执行:

# PKGBUILD build()
build() {
    cd "$srcdir/$pkgname"
    mcpp build --release --strict
}

断网沙箱中的终端日志:

==> Starting build()...
[mcpp::toolchain] Resolving toolchain 'gcc@16.1.0'...
[mcpp::xpkg] Downloading https://pkg.xlings.org/xim/gcc/16.1.0/linux-x86_64.tar.zst...
curl: (6) Could not resolve host: pkg.xlings.org (Network unreachable)
error: Failed to download toolchain payload 'gcc@16.1.0'
==> ERROR: A failure occurred in build().
    Aborting...

2.2 策略规范依据

  • Fedora Packaging Guidelines:

    "All builds in Fedora are done in an environment with no network access. Build scripts MUST NOT attempt to download external toolchains, runtimes, or libraries during the %build phase."

  • Debian Policy Manual (§4.9):

    "The package build must be completely reproducible in an isolated, offline clean-room chroot. Access to the network during package build is strictly forbidden."


3. 方案建议与接口设计

3.1 源码中的既有设计契机

src/build/prepare.cppm:1705-1706 中已预留了逃生通道:

} else if (tcSpec.has_value() && *tcSpec == "system") {
    // Explicit user opt-in to system PATH compiler — kept as escape hatch.
}

3.2 建议的配置语义

保持 Managed 托管沙盒模式为默认行为,提供显式 Opt-in 的原生宿主模式:

[target.x86_64-linux-gnu]
toolchain = "system"   # 使用宿主 /usr/bin/g++ 或 clang++
sysroot   = "system"   # 使用宿主 glibc 与 /lib64/ld-linux-x86-64.so.2 (不注入私有 PT_INTERP 与私有 RPATH)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions