Skip to content

Commit 3d3e2dc

Browse files
committed
feat(build): 汇编源一等公民 — .S/.s/.asm 进 sources,NASM 按目标推导 -f
设计: .agents/docs/2026-07-17-asm-sources-and-general-build-capabilities-design.md (PR-A) - .S/.s (GAS) 走 C 驱动的新 asm_object 规则(asm 安全旗标子集,无 -std/-O, PIC/调试/工具链定位旗标保留);.asm (NASM) 走 nasm_object 规则,-f 由 Triple::nasm_format() 从目标三元组推导(elf64/win64/macho64/...,交叉零特判), %include 经 -MD/-MQ 进 ninja deps 通道 - 默认 sources glob 扩为 src/**/*.{cppm,cpp,cc,c,S,s,asm};汇编对象名带全扩展 限定(foo.asm.o),同 stem 的 .c/.asm 兄弟不再撞对象 - 扩展名路由四处同改并单测锁住:scanner 跳模块扫描、plan 链接收集 (is_implementation_source)、ninja pick_rule、cdb(.asm 不进 compile_commands,clangd 读不懂;.S 以 cc + asm 旗标进) - nasm 惰性解析(仅当计划含 .asm 单元):PATH → mcpp 沙箱 → xlings install, 版本 floor ≥2.16,不可得即硬失败,绝不静默跳过;非 x86 目标遇 .asm、 MSVC 工具链遇 .S 均硬报错并给出门控指引 - G8a: 源 glob 跟随目录软链接(递归栈祖先链防环 + 文件级 canonical 去重); 顺带修 path_matches_glob 用 fs::relative 会解析软链致匹配失效的问题 (改词法 lexically_relative) - e2e 104(GAS 全链路+增量+build/test 双路径)/105(NASM/-f 推导/%include 依赖追踪/CDB 排除),run_all.sh 增 nasm capability;单测 35/35 过, e2e 106 过(3 失败均为既有环境性:22/54/62)
1 parent 450c24b commit 3d3e2dc

19 files changed

Lines changed: 1125 additions & 38 deletions

.agents/docs/2026-07-17-asm-sources-and-general-build-capabilities-design.md

Lines changed: 397 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# mcpp 通用构建能力需求清单(G1–G9)
2+
3+
> 日期:2026-07-17
4+
> 视角:每条都是对媒体/加密/数值计算类库通用的机制,不是给 ffmpeg/opencv 开特例。
5+
> 背景:mcpp-index 收录大型 C/C++ 库(ffmpeg、opencv、openssl、dav1d、x264、BLAS 族)的 POC 过程中沉淀的通用能力缺口。
6+
> 对应设计方案:[2026-07-17-asm-sources-and-general-build-capabilities-design.md](2026-07-17-asm-sources-and-general-build-capabilities-design.md)
7+
8+
## 先回答:要支持 nasm 构建吗?
9+
10+
**要,建议做成一等公民(`.asm` 直接进 sources)**,理由四条:
11+
12+
1. 手写汇编是 FFmpeg/dav1d/x264/OpenSSL/BLAS 整个生态的常态,mcpp 定位"大型 C/C++ 库源码构建"迟早绕不开;
13+
2. 声明式进 sources 自动获得指纹/增量/并行,不用每个包自己写 build.mcpp 管缓存重跑;
14+
3. nasm 本身是交叉汇编器,原生支持后 cross 零特判(build.mcpp 路线在 cross 下还要先解决跳过问题);
15+
4. Cargo 没有原生汇编(逼出了 cc crate 生态),Zig 有 `addAssemblyFile` —— mcpp 该站 Zig 这边。
16+
17+
## 通用能力清单(G1–G9)
18+
19+
### 声明式构建能力(高杠杆)
20+
21+
- **G1 原生汇编源**:`.S`(GAS,现有 cc 驱动就能编,覆盖 ARM 汇编,成本≈加后缀路由)+ `.asm`(NASM,`-f` 格式按目标三元组推导,覆盖 x86 汇编)★本清单最高杠杆
22+
- **G4 per-glob flags**:`[build.flags."**/*.avx2.cpp"] cxxflags=["-mavx2"]` —— 两大用例:SIMD 多档 dispatch TU、三方代码告警隔离(现在只能全包 `-w`,连自己的封装层都被静默)
23+
- **G5 mcpp.toml 的 features gate sources**:index 描述符能做(compat.gtest),mcpp.toml 不能——两边不对称;而"feature = 一组源文件 + 一个 define"正是 vendored 大库最高频形态(imgcodecs 17 种编解码器、ffmpeg codec 集合、highgui 后端)
24+
25+
### build.mcpp 补全(通用构建期逻辑)
26+
27+
- **G2 运行依赖包的 build.mcpp**(实测 0.0.93 静默跳过;作用域照 Cargo:flags 只进该包自身 TU,link 指令传播到终链)——没有它 build.mcpp 只对根项目有意义
28+
- **G3 build.mcpp 环境契约**:cross 下运行 + 暴露 TARGET/HOST/活跃 features/PROFILE/OUT_DIR(对应 Cargo 的环境变量族)——不暴露 features,build 程序无法按 codec 集合筛汇编列表
29+
30+
### 供给与修缮
31+
32+
- **G7 xlings 打包 nasm**(≥2.16;缺失硬失败不降级)——**已完成:最新 xlings 已支持 nasm**
33+
- **G8a** glob 不跟目录软链接、**G8b** 相对 `-I``.cppm` 单元不生效(两个 POC 实测的一致性问题)
34+
- **G6 generated_files 进 mcpp.toml**(与 index 对齐,小项)
35+
- **G9 跨项目构建产物缓存**(生态级远期;缓存键含工具链指纹,与"源码构建"哲学不矛盾)
36+
37+
## 排期建议
38+
39+
G7(已完成)→ **G1**(最高杠杆)→ **G5、G4**(声明式三连,vendored 大库形态就此完整)→ **G2+G3** → G8 顺手 → G6/G9 远期。
40+
41+
映射到收录项目:F1+ 的 ffmpeg 汇编加速硬依赖 G7 + (G1 或 G2+G3),G1 路线最短;P5 的 OpenCV SIMD dispatch 硬依赖 G4 或 G2+G3;而 F0/F1(纯 C)和 P0–P4 零阻塞,随时可开工。

docs/05-mcpp-toml.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ version = "0.1.0"
1515
```
1616

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

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

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

193205
```toml
@@ -718,7 +730,7 @@ mcpp build --target x86_64-linux-musl
718730

719731
| Item | Default | Notes |
720732
|---|---|---|
721-
| Source files | `src/**/*.{cppm,cpp,cc,c}` | Scanned recursively and automatically |
733+
| Source files | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | Scanned recursively and automatically |
722734
| Entry point | `src/main.cpp` | If this file exists, a `bin` target is inferred |
723735
| Library root | `src/<pkg-tail>.cppm` | Override with `[lib].path` |
724736
| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++26` / `c++2c` / `c++latest` / `c++fly` (experimental playground) |

docs/zh/05-mcpp-toml.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ version = "0.1.0"
1515
```
1616

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

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

636636
| 项目 | 默认值 | 说明 |
637637
|---|---|---|
638-
| 源文件 | `src/**/*.{cppm,cpp,cc,c}` | 自动递归扫描 |
638+
| 源文件 | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | 自动递归扫描 |
639639
| 入口 | `src/main.cpp` | 有这个文件就推断为 `bin` 目标 |
640640
| 库根 | `src/<pkg-tail>.cppm` | 可用 `[lib].path` 覆盖 |
641641
| C++ 标准 | `c++23` |`[package].standard` 配置; 支持 `c++26` / `c++2c` |

src/build/compile_commands.cppm

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,19 @@ std::string emit_compile_commands(const BuildPlan& plan, const CompileFlags& fla
123123
nlohmann::json entries = nlohmann::json::array();
124124

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

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

src/build/flags.cppm

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

324+
// GAS assembly (.S/.s via the C driver): the asm-safe subset — no -std
325+
// (C-only) and no -O (meaningless), but PIC stays (.S sources gate on
326+
// __PIC__), -g is fine, and the toolchain-location flags must come along
327+
// (hermetic link model: never fall back to a host `as`). MSVC dialect has
328+
// no GAS path — prepare hard-errors before these flags are consumed.
329+
f.as = std::format("{}{}{}{}{}",
330+
prof.debug ? " -g" : "", pic_flag,
331+
compile_toolchain_flags, b_flag, include_flags);
332+
333+
// NASM (.asm): fixed GNU-ish spelling of its own — include dirs are
334+
// re-spelt with -I regardless of dialect (nasm ≥2.14 inserts a missing
335+
// path separator itself); DWARF debug info exists on ELF only.
336+
if (!plan.nasmPath.empty()) {
337+
std::string nasm_includes;
338+
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
339+
auto abs = inc.is_absolute() ? inc : (plan.projectRoot / inc);
340+
nasm_includes += " -I" + escape_path(abs);
341+
}
342+
std::string nasm_debug;
343+
if (prof.debug && plan.nasmFormat.starts_with("elf"))
344+
nasm_debug = " -g -F dwarf";
345+
f.nasm = nasm_debug + nasm_includes;
346+
}
347+
322348
// Link flags
323349
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
324350
f.linkage = plan.manifest.buildConfig.linkage;

src/build/ninja_backend.cppm

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,35 @@ bool is_c_source(const std::filesystem::path& src) {
151151
return ext == ".c" || ext == ".m";
152152
}
153153

154+
bool is_gas_source(const std::filesystem::path& src) {
155+
auto ext = src.extension();
156+
return ext == ".S" || ext == ".s";
157+
}
158+
159+
bool is_nasm_source(const std::filesystem::path& src) {
160+
return src.extension() == ".asm";
161+
}
162+
163+
// TUs the P1689 module scan must skip: C-family and assembly units cannot
164+
// contain `import`/`module` declarations, and feeding them to the scanner
165+
// would route them through the C++ frontend.
166+
bool is_scan_exempt(const std::filesystem::path& src) {
167+
return is_c_source(src) || is_gas_source(src) || is_nasm_source(src);
168+
}
169+
170+
// Per-unit flags an assembler can take: the -D/-U/-I subset of the unit's C
171+
// flags (feature defines land there). NASM shares the GNU -D/-U/-I spelling
172+
// (and ≥2.14 inserts a missing -I path separator itself), so one filter
173+
// serves both asm rules.
174+
std::vector<std::string> asm_unit_flags(const CompileUnit& cu) {
175+
std::vector<std::string> out;
176+
for (auto& f : cu.packageCflags) {
177+
if (f.starts_with("-D") || f.starts_with("-U") || f.starts_with("-I"))
178+
out.push_back(f);
179+
}
180+
return out;
181+
}
182+
154183
std::string ltrim_copy(std::string_view s) {
155184
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front())))
156185
s.remove_prefix(1);
@@ -275,20 +304,29 @@ std::string emit_ninja_string(const BuildPlan& plan) {
275304
// All compile/link flags are computed once via flags.cppm.
276305
auto flags = compute_flags(plan);
277306

278-
bool need_c_rule = false;
307+
bool need_c_rule = false, need_asm_rule = false, need_nasm_rule = false;
279308
for (auto& cu : plan.compileUnits) {
280-
if (is_c_source(cu.source)) {
281-
need_c_rule = true;
282-
break;
283-
}
309+
if (is_c_source(cu.source)) need_c_rule = true;
310+
else if (is_gas_source(cu.source)) need_asm_rule = true;
311+
else if (is_nasm_source(cu.source)) need_nasm_rule = true;
284312
}
285313

286314
append(std::format("cxx = {}\n", escape_ninja_path(flags.cxxBinary)));
287315
append(std::format("cxxflags = {}\n", flags.cxx));
288-
if (need_c_rule) {
316+
if (need_c_rule || need_asm_rule) { // asm_object drives the C compiler too
289317
append(std::format("cc = {}\n", escape_ninja_path(flags.ccBinary)));
318+
}
319+
if (need_c_rule) {
290320
append(std::format("cflags = {}\n", flags.cc));
291321
}
322+
if (need_asm_rule) {
323+
append(std::format("asmflags ={}\n", flags.as));
324+
}
325+
if (need_nasm_rule) {
326+
append(std::format("nasm = {}\n", escape_ninja_path(plan.nasmPath)));
327+
append(std::format("nasmfmt = {}\n", plan.nasmFormat));
328+
append(std::format("nasmflags ={}\n", flags.nasm));
329+
}
292330
append(std::format("ldflags ={}\n", flags.ld));
293331

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

447+
if (need_asm_rule) {
448+
// GAS assembly (.S/.s) through the C driver: it preprocesses .S (cpp)
449+
// and assembles both, dispatching by extension. $asmflags is the
450+
// asm-safe flag subset (no -std / no -O — see flags.cppm).
451+
append("rule asm_object\n");
452+
append(std::format(
453+
" command = $cc $local_includes $asmflags $unit_asmflags {}\n",
454+
compile_tail));
455+
append(" description = AS $out\n\n");
456+
}
457+
458+
if (need_nasm_rule) {
459+
// NASM (.asm): its own fixed flag spelling — deliberately outside
460+
// CommandDialect. -MD/-MQ feed ninja's header tracking for %include.
461+
append("rule nasm_object\n");
462+
append(" command = $nasm -f $nasmfmt $local_includes $nasmflags "
463+
"$unit_asmflags -MD $out.d -MQ $out -o $out $in\n");
464+
append(" deps = gcc\n");
465+
append(" depfile = $out.d\n");
466+
append(" description = NASM $out\n\n");
467+
}
468+
409469
// Link/archive/shared: driver-style (g++/clang++ are the linker) vs the
410470
// msvc dialect's separate link.exe/lib.exe. The msvc commands go through
411471
// response files — object lists exceed cmd.exe's 8191-char limit fast.
@@ -533,6 +593,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
533593
return "cxx_module";
534594
if (ext == ".c" || ext == ".m")
535595
return "c_object";
596+
if (ext == ".S" || ext == ".s")
597+
return "asm_object";
598+
if (ext == ".asm")
599+
return "nasm_object";
536600
return "cxx_object";
537601
};
538602

@@ -549,7 +613,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
549613
std::vector<std::string> ddi_paths;
550614
ddi_paths.reserve(plan.compileUnits.size());
551615
for (auto& cu : plan.compileUnits) {
552-
if (is_c_source(cu.source))
616+
if (is_scan_exempt(cu.source))
553617
continue;
554618
auto ddi = (cu.object.parent_path() / cu.source.filename()).string() + ".ddi";
555619
ddi_paths.push_back(ddi);
@@ -580,7 +644,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
580644
}();
581645
std::map<std::string, std::string> ddi_expect;
582646
for (auto& cu : plan.compileUnits) {
583-
if (is_c_source(cu.source)) continue;
647+
if (is_scan_exempt(cu.source)) continue;
584648
if (!cu.scanOverridden && !verifyAll) continue;
585649
auto ddi = (cu.object.parent_path() / cu.source.filename()).string() + ".ddi";
586650
std::string exp;
@@ -618,7 +682,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
618682
out_line += " | " + bmi_path(*cu.providesModule);
619683
}
620684
out_line += std::format(" : {} {}", rule, escape_ninja_path(cu.source));
621-
if (rule != "c_object") {
685+
if (!is_scan_exempt(cu.source)) {
622686
auto ddi = (cu.object.parent_path() / cu.source.filename()).string() + ".ddi";
623687
auto it = ddi_to_dd.find(ddi);
624688
if (it != ddi_to_dd.end()) {
@@ -637,7 +701,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
637701
}
638702
if (auto includes = local_include_flags(cu); !includes.empty())
639703
out_line += " local_includes =" + includes + "\n";
640-
if (is_c_source(cu.source)) {
704+
if (is_gas_source(cu.source) || is_nasm_source(cu.source)) {
705+
if (auto flags = join_flags(asm_unit_flags(cu)); !flags.empty())
706+
out_line += " unit_asmflags =" + flags + "\n";
707+
} else if (is_c_source(cu.source)) {
641708
if (auto flags = join_flags(cu.packageCflags); !flags.empty())
642709
out_line += " unit_cflags =" + flags + "\n";
643710
} else {
@@ -653,8 +720,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
653720
std::string rule = pick_rule(cu.source);
654721

655722
std::string implicit;
656-
// .c files don't `import` modules; skip BMI implicit inputs.
657-
if (rule != "c_object") {
723+
// C/asm files don't `import` modules; skip BMI implicit inputs.
724+
if (!is_scan_exempt(cu.source)) {
658725
for (auto& imp : cu.imports) {
659726
if (imp == "std") {
660727
if (has_std_artifacts)
@@ -684,7 +751,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
684751
out_line += "\n";
685752
if (auto includes = local_include_flags(cu); !includes.empty())
686753
out_line += " local_includes =" + includes + "\n";
687-
if (is_c_source(cu.source)) {
754+
if (is_gas_source(cu.source) || is_nasm_source(cu.source)) {
755+
if (auto flags = join_flags(asm_unit_flags(cu)); !flags.empty())
756+
out_line += " unit_asmflags =" + flags + "\n";
757+
} else if (is_c_source(cu.source)) {
688758
if (auto flags = join_flags(cu.packageCflags); !flags.empty())
689759
out_line += " unit_cflags =" + flags + "\n";
690760
} else {

0 commit comments

Comments
 (0)