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

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions .agents/docs/2026-07-17-mcpp-feature-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# mcpp 通用构建能力需求清单(G1–G9)

> 日期:2026-07-17
> 视角:每条都是对媒体/加密/数值计算类库通用的机制,不是给 ffmpeg/opencv 开特例。
> 背景:mcpp-index 收录大型 C/C++ 库(ffmpeg、opencv、openssl、dav1d、x264、BLAS 族)的 POC 过程中沉淀的通用能力缺口。
> 对应设计方案:[2026-07-17-asm-sources-and-general-build-capabilities-design.md](2026-07-17-asm-sources-and-general-build-capabilities-design.md)

## 先回答:要支持 nasm 构建吗?

**要,建议做成一等公民(`.asm` 直接进 sources)**,理由四条:

1. 手写汇编是 FFmpeg/dav1d/x264/OpenSSL/BLAS 整个生态的常态,mcpp 定位"大型 C/C++ 库源码构建"迟早绕不开;
2. 声明式进 sources 自动获得指纹/增量/并行,不用每个包自己写 build.mcpp 管缓存重跑;
3. nasm 本身是交叉汇编器,原生支持后 cross 零特判(build.mcpp 路线在 cross 下还要先解决跳过问题);
4. Cargo 没有原生汇编(逼出了 cc crate 生态),Zig 有 `addAssemblyFile` —— mcpp 该站 Zig 这边。

## 通用能力清单(G1–G9)

### 声明式构建能力(高杠杆)

- **G1 原生汇编源**:`.S`(GAS,现有 cc 驱动就能编,覆盖 ARM 汇编,成本≈加后缀路由)+ `.asm`(NASM,`-f` 格式按目标三元组推导,覆盖 x86 汇编)★本清单最高杠杆
- **G4 per-glob flags**:`[build.flags."**/*.avx2.cpp"] cxxflags=["-mavx2"]` —— 两大用例:SIMD 多档 dispatch TU、三方代码告警隔离(现在只能全包 `-w`,连自己的封装层都被静默)
- **G5 mcpp.toml 的 features gate sources**:index 描述符能做(compat.gtest),mcpp.toml 不能——两边不对称;而"feature = 一组源文件 + 一个 define"正是 vendored 大库最高频形态(imgcodecs 17 种编解码器、ffmpeg codec 集合、highgui 后端)

### build.mcpp 补全(通用构建期逻辑)

- **G2 运行依赖包的 build.mcpp**(实测 0.0.93 静默跳过;作用域照 Cargo:flags 只进该包自身 TU,link 指令传播到终链)——没有它 build.mcpp 只对根项目有意义
- **G3 build.mcpp 环境契约**:cross 下运行 + 暴露 TARGET/HOST/活跃 features/PROFILE/OUT_DIR(对应 Cargo 的环境变量族)——不暴露 features,build 程序无法按 codec 集合筛汇编列表

### 供给与修缮

- **G7 xlings 打包 nasm**(≥2.16;缺失硬失败不降级)——**已完成:最新 xlings 已支持 nasm**
- **G8a** glob 不跟目录软链接、**G8b** 相对 `-I` 对 `.cppm` 单元不生效(两个 POC 实测的一致性问题)
- **G6 generated_files 进 mcpp.toml**(与 index 对齐,小项)
- **G9 跨项目构建产物缓存**(生态级远期;缓存键含工具链指纹,与"源码构建"哲学不矛盾)

## 排期建议

G7(已完成)→ **G1**(最高杠杆)→ **G5、G4**(声明式三连,vendored 大库形态就此完整)→ **G2+G3** → G8 顺手 → G6/G9 远期。

映射到收录项目:F1+ 的 ffmpeg 汇编加速硬依赖 G7 + (G1 或 G2+G3),G1 路线最短;P5 的 OpenCV SIMD dispatch 硬依赖 G4 或 G2+G3;而 F0/F1(纯 C)和 P0–P4 零阻塞,随时可开工。
18 changes: 15 additions & 3 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ version = "0.1.0"
```

mcpp infers automatically:
- Source files: `src/**/*.{cppm,cpp,cc,c}`
- Source files: `src/**/*.{cppm,cpp,cc,c,S,s,asm}`
- Entry point: `src/main.cpp` → produces the `hello` binary
- Standard: C++23
- Modules: scans `export module ...` declarations and builds the dependency graph automatically
Expand Down Expand Up @@ -134,7 +134,7 @@ the package/feature boundary, not on an individual target.

```toml
[build]
sources = ["src/**/*.cppm", "src/**/*.cpp"] # Source globs (default: src/**/*.{cppm,cpp,cc,c})
sources = ["src/**/*.cppm", "src/**/*.cpp"] # Source globs (default: src/**/*.{cppm,cpp,cc,c,S,s,asm})
include_dirs = ["include", "third_party/include"] # Header search paths
c_standard = "c11" # Standard for C source files (default c11)
cflags = ["-DFOO=1"] # Extra C compile flags
Expand Down Expand Up @@ -188,6 +188,18 @@ sources = [
]
```

**Assembly sources** (mcpp 0.0.95+): `.S`/`.s` (GAS — preprocessed by the C
driver, covers ARM and AT&T-syntax x86) and `.asm` (NASM — Intel-syntax x86)
are first-class sources: default-globbed, fingerprinted, built incrementally
in parallel, and linked like any other object. The NASM output format is
derived from the target triple (`elf64`/`win64`/`macho64`/... — cross builds
just work), and `nasm` itself is resolved lazily only when `.asm` units exist:
`PATH` first, then the mcpp sandbox, then `xlings install nasm`; if none
yields nasm ≥ 2.16 the build **fails hard** (assembly is never silently
skipped). Limits: `.asm` targets x86 only (hard error elsewhere — gate the
files off other targets), `.S` is unavailable on the MSVC toolchain, and
`.asm` means NASM syntax (MASM sources should be `!`-excluded).

### 2.4 `[lib]` — Library Root Module Convention

```toml
Expand Down Expand Up @@ -718,7 +730,7 @@ mcpp build --target x86_64-linux-musl

| Item | Default | Notes |
|---|---|---|
| Source files | `src/**/*.{cppm,cpp,cc,c}` | Scanned recursively and automatically |
| Source files | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | Scanned recursively and automatically |
| Entry point | `src/main.cpp` | If this file exists, a `bin` target is inferred |
| Library root | `src/<pkg-tail>.cppm` | Override with `[lib].path` |
| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++26` / `c++2c` / `c++latest` / `c++fly` (experimental playground) |
Expand Down
6 changes: 3 additions & 3 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ version = "0.1.0"
```

mcpp 自动推断:
- 源文件: `src/**/*.{cppm,cpp,cc,c}`
- 源文件: `src/**/*.{cppm,cpp,cc,c,S,s,asm}`
- 入口: `src/main.cpp` → 生成 `hello` 二进制
- 标准: C++23
- 模块: 扫描 `export module ...` 声明自动建立依赖图
Expand Down Expand Up @@ -129,7 +129,7 @@ mcpp 刻意不在一次构建里把同一个共享源编译成两份:一个源

```toml
[build]
sources = ["src/**/*.cppm", "src/**/*.cpp"] # 源文件 glob(默认: src/**/*.{cppm,cpp,cc,c})
sources = ["src/**/*.cppm", "src/**/*.cpp"] # 源文件 glob(默认: src/**/*.{cppm,cpp,cc,c,S,s,asm})
include_dirs = ["include", "third_party/include"] # 头文件搜索路径
c_standard = "c11" # C 源文件的标准(默认 c11)
cflags = ["-DFOO=1"] # 额外 C 编译参数
Expand Down Expand Up @@ -635,7 +635,7 @@ mcpp build --target x86_64-linux-musl

| 项目 | 默认值 | 说明 |
|---|---|---|
| 源文件 | `src/**/*.{cppm,cpp,cc,c}` | 自动递归扫描 |
| 源文件 | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | 自动递归扫描 |
| 入口 | `src/main.cpp` | 有这个文件就推断为 `bin` 目标 |
| 库根 | `src/<pkg-tail>.cppm` | 可用 `[lib].path` 覆盖 |
| C++ 标准 | `c++23` | 用 `[package].standard` 配置; 支持 `c++26` / `c++2c` |
Expand Down
15 changes: 12 additions & 3 deletions src/build/compile_commands.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,19 @@ std::string emit_compile_commands(const BuildPlan& plan, const CompileFlags& fla
nlohmann::json entries = nlohmann::json::array();

for (auto& cu : plan.compileUnits) {
// Pick compiler + flags based on source type.
const bool isCSource = is_c_source(cu.source);
// NASM units carry a command line no CDB consumer (clangd, …) can
// interpret — a bogus entry actively harms LSP diagnostics, so they
// are excluded from the CDB entirely.
if (cu.source.extension() == ".asm") continue;
// Pick compiler + flags based on source type. GAS units (.S/.s) ride
// the C driver with the asm-safe flag string, mirroring build.ninja.
const auto ext = cu.source.extension();
const bool isGasSource = ext == ".S" || ext == ".s";
const bool isCSource = is_c_source(cu.source) || isGasSource;
const auto& compiler = isCSource ? flags.ccBinary : flags.cxxBinary;
const auto& flagStr = isCSource ? flags.cc : flags.cxx;
const auto& flagStr = isGasSource ? flags.as
: isCSource ? flags.cc
: flags.cxx;

auto output_path = (plan.outputDir / cu.object).string();

Expand Down
26 changes: 26 additions & 0 deletions src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export namespace mcpp::build {
struct CompileFlags {
std::string cxx; // full cxxflags string
std::string cc; // full cflags string
std::string as; // asm-safe subset for .S/.s via the C driver
std::string nasm; // NASM global flags (.asm; own spelling)
std::string ld; // ldflags string
std::filesystem::path cxxBinary; // g++ / clang++ / cl.exe
std::filesystem::path ccBinary; // gcc / clang (derived; cl.exe = same)
Expand Down Expand Up @@ -319,6 +321,30 @@ CompileFlags compute_flags(const BuildPlan& plan) {
: std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag,
compile_toolchain_flags, b_flag, include_flags);

// GAS assembly (.S/.s via the C driver): the asm-safe subset — no -std
// (C-only) and no -O (meaningless), but PIC stays (.S sources gate on
// __PIC__), -g is fine, and the toolchain-location flags must come along
// (hermetic link model: never fall back to a host `as`). MSVC dialect has
// no GAS path — prepare hard-errors before these flags are consumed.
f.as = std::format("{}{}{}{}{}",
prof.debug ? " -g" : "", pic_flag,
compile_toolchain_flags, b_flag, include_flags);

// NASM (.asm): fixed GNU-ish spelling of its own — include dirs are
// re-spelt with -I regardless of dialect (nasm ≥2.14 inserts a missing
// path separator itself); DWARF debug info exists on ELF only.
if (!plan.nasmPath.empty()) {
std::string nasm_includes;
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
auto abs = inc.is_absolute() ? inc : (plan.projectRoot / inc);
nasm_includes += " -I" + escape_path(abs);
}
std::string nasm_debug;
if (prof.debug && plan.nasmFormat.starts_with("elf"))
nasm_debug = " -g -F dwarf";
f.nasm = nasm_debug + nasm_includes;
}

// Link flags
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
f.linkage = plan.manifest.buildConfig.linkage;
Expand Down
96 changes: 83 additions & 13 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,35 @@ bool is_c_source(const std::filesystem::path& src) {
return ext == ".c" || ext == ".m";
}

bool is_gas_source(const std::filesystem::path& src) {
auto ext = src.extension();
return ext == ".S" || ext == ".s";
}

bool is_nasm_source(const std::filesystem::path& src) {
return src.extension() == ".asm";
}

// TUs the P1689 module scan must skip: C-family and assembly units cannot
// contain `import`/`module` declarations, and feeding them to the scanner
// would route them through the C++ frontend.
bool is_scan_exempt(const std::filesystem::path& src) {
return is_c_source(src) || is_gas_source(src) || is_nasm_source(src);
}

// Per-unit flags an assembler can take: the -D/-U/-I subset of the unit's C
// flags (feature defines land there). NASM shares the GNU -D/-U/-I spelling
// (and ≥2.14 inserts a missing -I path separator itself), so one filter
// serves both asm rules.
std::vector<std::string> asm_unit_flags(const CompileUnit& cu) {
std::vector<std::string> out;
for (auto& f : cu.packageCflags) {
if (f.starts_with("-D") || f.starts_with("-U") || f.starts_with("-I"))
out.push_back(f);
}
return out;
}

std::string ltrim_copy(std::string_view s) {
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front())))
s.remove_prefix(1);
Expand Down Expand Up @@ -275,20 +304,29 @@ std::string emit_ninja_string(const BuildPlan& plan) {
// All compile/link flags are computed once via flags.cppm.
auto flags = compute_flags(plan);

bool need_c_rule = false;
bool need_c_rule = false, need_asm_rule = false, need_nasm_rule = false;
for (auto& cu : plan.compileUnits) {
if (is_c_source(cu.source)) {
need_c_rule = true;
break;
}
if (is_c_source(cu.source)) need_c_rule = true;
else if (is_gas_source(cu.source)) need_asm_rule = true;
else if (is_nasm_source(cu.source)) need_nasm_rule = true;
}

append(std::format("cxx = {}\n", escape_ninja_path(flags.cxxBinary)));
append(std::format("cxxflags = {}\n", flags.cxx));
if (need_c_rule) {
if (need_c_rule || need_asm_rule) { // asm_object drives the C compiler too
append(std::format("cc = {}\n", escape_ninja_path(flags.ccBinary)));
}
if (need_c_rule) {
append(std::format("cflags = {}\n", flags.cc));
}
if (need_asm_rule) {
append(std::format("asmflags ={}\n", flags.as));
}
if (need_nasm_rule) {
append(std::format("nasm = {}\n", escape_ninja_path(plan.nasmPath)));
append(std::format("nasmfmt = {}\n", plan.nasmFormat));
append(std::format("nasmflags ={}\n", flags.nasm));
}
append(std::format("ldflags ={}\n", flags.ld));

// `ar` for cxx_archive.
Expand Down Expand Up @@ -406,6 +444,28 @@ std::string emit_ninja_string(const BuildPlan& plan) {
append("\n");
}

if (need_asm_rule) {
// GAS assembly (.S/.s) through the C driver: it preprocesses .S (cpp)
// and assembles both, dispatching by extension. $asmflags is the
// asm-safe flag subset (no -std / no -O — see flags.cppm).
append("rule asm_object\n");
append(std::format(
" command = $cc $local_includes $asmflags $unit_asmflags {}\n",
compile_tail));
append(" description = AS $out\n\n");
}

if (need_nasm_rule) {
// NASM (.asm): its own fixed flag spelling — deliberately outside
// CommandDialect. -MD/-MQ feed ninja's header tracking for %include.
append("rule nasm_object\n");
append(" command = $nasm -f $nasmfmt $local_includes $nasmflags "
"$unit_asmflags -MD $out.d -MQ $out -o $out $in\n");
append(" deps = gcc\n");
append(" depfile = $out.d\n");
append(" description = NASM $out\n\n");
}

// Link/archive/shared: driver-style (g++/clang++ are the linker) vs the
// msvc dialect's separate link.exe/lib.exe. The msvc commands go through
// response files — object lists exceed cmd.exe's 8191-char limit fast.
Expand Down Expand Up @@ -533,6 +593,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
return "cxx_module";
if (ext == ".c" || ext == ".m")
return "c_object";
if (ext == ".S" || ext == ".s")
return "asm_object";
if (ext == ".asm")
return "nasm_object";
return "cxx_object";
};

Expand All @@ -549,7 +613,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
std::vector<std::string> ddi_paths;
ddi_paths.reserve(plan.compileUnits.size());
for (auto& cu : plan.compileUnits) {
if (is_c_source(cu.source))
if (is_scan_exempt(cu.source))
continue;
auto ddi = (cu.object.parent_path() / cu.source.filename()).string() + ".ddi";
ddi_paths.push_back(ddi);
Expand Down Expand Up @@ -580,7 +644,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
}();
std::map<std::string, std::string> ddi_expect;
for (auto& cu : plan.compileUnits) {
if (is_c_source(cu.source)) continue;
if (is_scan_exempt(cu.source)) continue;
if (!cu.scanOverridden && !verifyAll) continue;
auto ddi = (cu.object.parent_path() / cu.source.filename()).string() + ".ddi";
std::string exp;
Expand Down Expand Up @@ -618,7 +682,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
out_line += " | " + bmi_path(*cu.providesModule);
}
out_line += std::format(" : {} {}", rule, escape_ninja_path(cu.source));
if (rule != "c_object") {
if (!is_scan_exempt(cu.source)) {
auto ddi = (cu.object.parent_path() / cu.source.filename()).string() + ".ddi";
auto it = ddi_to_dd.find(ddi);
if (it != ddi_to_dd.end()) {
Expand All @@ -637,7 +701,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
}
if (auto includes = local_include_flags(cu); !includes.empty())
out_line += " local_includes =" + includes + "\n";
if (is_c_source(cu.source)) {
if (is_gas_source(cu.source) || is_nasm_source(cu.source)) {
if (auto flags = join_flags(asm_unit_flags(cu)); !flags.empty())
out_line += " unit_asmflags =" + flags + "\n";
} else if (is_c_source(cu.source)) {
if (auto flags = join_flags(cu.packageCflags); !flags.empty())
out_line += " unit_cflags =" + flags + "\n";
} else {
Expand All @@ -653,8 +720,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
std::string rule = pick_rule(cu.source);

std::string implicit;
// .c files don't `import` modules; skip BMI implicit inputs.
if (rule != "c_object") {
// C/asm files don't `import` modules; skip BMI implicit inputs.
if (!is_scan_exempt(cu.source)) {
for (auto& imp : cu.imports) {
if (imp == "std") {
if (has_std_artifacts)
Expand Down Expand Up @@ -684,7 +751,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
out_line += "\n";
if (auto includes = local_include_flags(cu); !includes.empty())
out_line += " local_includes =" + includes + "\n";
if (is_c_source(cu.source)) {
if (is_gas_source(cu.source) || is_nasm_source(cu.source)) {
if (auto flags = join_flags(asm_unit_flags(cu)); !flags.empty())
out_line += " unit_asmflags =" + flags + "\n";
} else if (is_c_source(cu.source)) {
if (auto flags = join_flags(cu.packageCflags); !flags.empty())
out_line += " unit_cflags =" + flags + "\n";
} else {
Expand Down
Loading
Loading