Skip to content

Commit 7582cd9

Browse files
committed
feat(manifest): 声明式清单能力 — features.sources / generated_files / cfg 条件 sources / per-glob flags
设计: .agents/docs/2026-07-17-asm-sources-and-general-build-capabilities-design.md (PR-B) - G5: mcpp.toml [features] 表形式增 sources 键 → featureSources(与 xpkg 逐字对等);顺修 gate 语义真缺口:drop 只按字符串删 glob,默认 glob 仍罩住 被门控文件——未激活 feature 的 glob 现在变成 `!` 排除,gate 真正生效 (build 模式;test 模式保持 0.0.94 的解耦决策不变,e2e 100/79 回归全过) - G6: mcpp.toml [generated_files] 节 → generatedFiles(路径越根解析期报错); 根 manifest 补 materialize 调用(此前只对依赖做);自研 TOML 解析器补 """/''' 多行字符串(TOML 1.0:首换行修剪、行尾反斜杠、闭合粘连引号) - G1b: ConditionalConfig 增 sources,[target.'cfg(...)'.build] sources 按解析 后 target 求值 APPEND(含 modules.sources 镜像);cfg 合并提炼为 merge_conditional_build 并补依赖侧求值(此前 conditionalConfigs 对依赖 完全不求值,descriptor 写了也静默无效);xpkg 增 target_cfg 键 - G4: [build] flags = [{ glob, cflags/cxxflags/asmflags/defines }] 有序数组 (TOML 表是 sorted map 丢声明序,数组形式才能承载"顺序即语义");xpkg 增 对称 flags 键;scanner 命中附加(声明序 append,后者胜)、零命中 warning、 SourceUnit/CompileUnit 增 packageAsmflags 通道接 PR-A 的 unit_asmflags; 全序列折入 canonical_compile_flags 指纹 - G8b 已定位并修复:用户 cflags/cxxflags 里的相对 -I 按 ninja cwd(outputDir) 解析而非项目根(.cpp/.cppm 同病)——scanner 单点绝对化 + plan 入口单元同步 - 单测:toml/xpkg 对称性契约测试、多行字符串、per-glob 顺序语义、相对 -I; e2e 106(featureSources 四象限)/107(generated_files)/108(cfg sources, host-aware)/109(per-glob flags 顺序覆盖+零命中告警);全量 110 过, 3 失败均既有环境性(22/54/62);docs/05 中英同步(含 PR-A 汇编段补中文)
1 parent d440a0b commit 7582cd9

18 files changed

Lines changed: 1069 additions & 15 deletions

docs/05-mcpp-toml.md

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,46 @@ sources = [
188188
]
189189
```
190190

191+
**Per-glob flags** (mcpp 0.0.95+): `[build] flags` is an ordered array of
192+
inline tables attaching extra compile flags to exactly the sources a glob
193+
matches — the tool for SIMD dispatch TUs and vendored-code warning isolation:
194+
195+
```toml
196+
[build]
197+
flags = [
198+
{ glob = "third_party/**", cflags = ["-w"], cxxflags = ["-w"] },
199+
{ glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"], defines = ["HAVE_AVX2"] },
200+
{ glob = "src/x86/**/*.asm", asmflags = ["-DPREFIX"] },
201+
]
202+
```
203+
204+
Keys per entry: `glob` (package-root-relative, required) plus `cflags` /
205+
`cxxflags` / `asmflags` / `defines` (no `ldflags` — linking has no per-TU
206+
scope). Declaration order is application order: a later entry's flags land
207+
later on the command line, so with GNU "last flag wins" a narrower glob
208+
placed after a broader one overrides it. All matching entries apply. These
209+
are private build flags — they never propagate to consumers. A glob that
210+
matches no source file prints a warning (a typo'd glob must not silently do
211+
nothing).
212+
213+
**Generated files** (mcpp 0.0.95+): `[generated_files]` maps a relative path
214+
to file contents (TOML multiline strings supported). Entries are written
215+
into the project tree before source globs expand — the same mechanism index
216+
descriptors use to synthesize module wrappers — and the content enters the
217+
fingerprint, so editing it rebuilds:
218+
219+
```toml
220+
[generated_files]
221+
"src/gen/wrap.cppm" = """
222+
module;
223+
#include <vendored.h>
224+
export module wrap;
225+
"""
226+
```
227+
228+
Paths must stay inside the project root (`..` / absolute paths are parse
229+
errors).
230+
191231
**Assembly sources** (mcpp 0.0.95+): `.S`/`.s` (GAS — preprocessed by the C
192232
driver, covers ARM and AT&T-syntax x86) and `.asm` (NASM — Intel-syntax x86)
193233
are first-class sources: default-globbed, fingerprinted, built incrementally
@@ -319,7 +359,9 @@ there is no ambiguity. Use the bare form for a single OS/family; use `cfg(...)`
319359
when you need arch/env conditions or combinators.
320360

321361
- **Keys**: `dependencies` / `dev-dependencies` / `build-dependencies`, and
322-
`build` with `cflags` / `cxxflags` / `ldflags`.
362+
`build` with `cflags` / `cxxflags` / `ldflags` / `sources` (mcpp 0.0.95+ —
363+
conditional source globs, e.g. gating `src/x86/**/*.asm` behind
364+
`cfg(arch = "x86_64")`; `!`-exclusion globs work here too).
323365
- **Evaluated against the resolved target** — the `--target` triple for a cross
324366
build, otherwise the host. So a native Linux build never even *downloads* a
325367
`[target.windows]` dependency.
@@ -352,8 +394,12 @@ extra = []
352394
#### Table form — a feature that contributes more than implied features
353395

354396
A `[features]` entry may be written as a **table** instead of an array, letting the
355-
feature carry package-owned preprocessor `defines` and/or capability `requires` /
356-
`provides` (see §2.8.1) alongside its implied features:
397+
feature carry package-owned preprocessor `defines`, feature-gated source globs
398+
(`sources`, mcpp 0.0.95+ — the globs leave the default build and compile only when
399+
the feature is active, exactly like an index descriptor's `features.<f>.sources`;
400+
the highest-frequency shape for vendored libraries: *feature = a source set + a
401+
define*), and/or capability `requires` / `provides` (see §2.8.1) alongside its
402+
implied features:
357403

358404
```toml
359405
[features]

docs/zh/05-mcpp-toml.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,49 @@ sources = [
175175
]
176176
```
177177

178+
**per-glob 旗标**(mcpp 0.0.95+):`[build] flags`**有序**的内联表数组,把额外
179+
编译旗标只附加到 glob 命中的源文件——SIMD 多档 dispatch TU 与三方代码告警隔离的
180+
正解:
181+
182+
```toml
183+
[build]
184+
flags = [
185+
{ glob = "third_party/**", cflags = ["-w"], cxxflags = ["-w"] },
186+
{ glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"], defines = ["HAVE_AVX2"] },
187+
{ glob = "src/x86/**/*.asm", asmflags = ["-DPREFIX"] },
188+
]
189+
```
190+
191+
每条目键:`glob`(相对包根,必填)+ `cflags` / `cxxflags` / `asmflags` /
192+
`defines`(没有 `ldflags`——链接没有 per-TU 作用域)。声明顺序即应用顺序:靠后
193+
条目的旗标排在命令行更后,配合 GNU "后旗标胜",窄 glob 放在宽 glob 之后即可覆盖。
194+
所有命中条目都生效;这些是私有构建旗标,不会传播给消费者。glob 零命中会打印
195+
warning(打错的 glob 不允许静默无效)。
196+
197+
**生成文件**(mcpp 0.0.95+):`[generated_files]` 把相对路径映射到文件内容(支持
198+
TOML 多行字符串)。条目在源 glob 展开之前写入工程树——与 index 描述符合成模块
199+
包装文件是同一机制——内容进指纹,改内容即重建:
200+
201+
```toml
202+
[generated_files]
203+
"src/gen/wrap.cppm" = """
204+
module;
205+
#include <vendored.h>
206+
export module wrap;
207+
"""
208+
```
209+
210+
路径必须留在工程根之内(`..` / 绝对路径是解析错误)。
211+
212+
**汇编源**(mcpp 0.0.95+):`.S`/`.s`(GAS——由 C 驱动器预处理,覆盖 ARM 与
213+
AT&T 语法 x86)和 `.asm`(NASM——Intel 语法 x86)是一等源文件:默认 glob 收录、
214+
进指纹、增量并行构建、像任何对象一样链接。NASM 的输出格式由目标三元组推导
215+
(`elf64`/`win64`/`macho64`/...——交叉构建零特判);`nasm` 仅在存在 `.asm` 单元时
216+
惰性解析:先 `PATH`,再 mcpp 沙箱,再 `xlings install nasm`;找不到 ≥2.16 的
217+
nasm 则**硬失败**(汇编绝不静默跳过)。限制:`.asm` 仅限 x86 目标(其他目标硬
218+
报错——用条件 sources 门控)、MSVC 工具链不支持 `.S``.asm` 即 NASM 语法
219+
(MASM 源请用 `!` 排除)。
220+
178221
### 2.4 `[lib]` — 库根模块约定
179222

180223
```toml
@@ -290,7 +333,9 @@ cxxflags = ["-march=x86-64-v2"]
290333
OS/家族用裸形式;需要 arch/env 条件或组合子时用 `cfg(...)`
291334

292335
- **可放的键**:`dependencies` / `dev-dependencies` / `build-dependencies`,以及
293-
`build` 下的 `cflags` / `cxxflags` / `ldflags`
336+
`build` 下的 `cflags` / `cxxflags` / `ldflags` / `sources`(mcpp 0.0.95+——
337+
条件源 glob,如把 `src/x86/**/*.asm` 门控在 `cfg(arch = "x86_64")` 之后;
338+
`!` 排除 glob 同样可用)。
294339
- **按解析后的目标求值**——交叉构建时是 `--target` 三元组,否则是 host。所以原生
295340
Linux 构建**根本不会下载** `[target.windows]` 的依赖。
296341
- **优先级**:精确三元组表压过 `cfg`/别名表;多个命中的谓词表,其旗标会拼接。
@@ -317,7 +362,10 @@ extra = []
317362
#### 表形式 —— 让 feature 贡献的不止是隐含 feature
318363

319364
`[features]` 的条目除了写成数组,还可写成****,从而让该 feature 在隐含 feature
320-
之外,携带包自有的预处理 `defines`,以及 capability 的 `requires` / `provides`
365+
之外,携带包自有的预处理 `defines`、feature 门控的源 glob(`sources`,mcpp
366+
0.0.95+——列出的 glob 离开默认构建,仅当 feature 激活时才编译,与 index 描述符的
367+
`features.<f>.sources` 完全对等;这正是 vendored 大库最高频的形态:*feature =
368+
一组源文件 + 一个 define*),以及 capability 的 `requires` / `provides`
321369
(见 §2.8.1):
322370

323371
```toml

src/build/ninja_backend.cppm

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,15 @@ bool is_scan_exempt(const std::filesystem::path& src) {
170170
// Per-unit flags an assembler can take: the -D/-U/-I subset of the unit's C
171171
// flags (feature defines land there). NASM shares the GNU -D/-U/-I spelling
172172
// (and ≥2.14 inserts a missing -I path separator itself), so one filter
173-
// serves both asm rules.
173+
// serves both asm rules. Explicit per-glob asmflags (G4) append after the
174+
// filtered subset — author-directed flags win.
174175
std::vector<std::string> asm_unit_flags(const CompileUnit& cu) {
175176
std::vector<std::string> out;
176177
for (auto& f : cu.packageCflags) {
177178
if (f.starts_with("-D") || f.starts_with("-U") || f.starts_with("-I"))
178179
out.push_back(f);
179180
}
181+
out.insert(out.end(), cu.packageAsmflags.begin(), cu.packageAsmflags.end());
180182
return out;
181183
}
182184

src/build/plan.cppm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ struct CompileUnit {
2424
std::vector<std::filesystem::path> localIncludeDirs;
2525
std::vector<std::string> packageCflags;
2626
std::vector<std::string> packageCxxflags;
27+
std::vector<std::string> packageAsmflags; // per-glob asmflags (G4)
2728
std::optional<std::string> providesModule; // logical name, if .cppm export
2829
std::vector<std::string> imports; // logical names imported
2930
// Unit came from a scan_overrides declaration — plan-vs-ddi
@@ -445,6 +446,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
445446
cu.localIncludeDirs = u.localIncludeDirs;
446447
cu.packageCflags = u.packageCflags;
447448
cu.packageCxxflags = u.packageCxxflags;
449+
cu.packageAsmflags = u.packageAsmflags;
448450
const auto fname = object_filename_for(u.path, objExt);
449451
if (basenameCount[fname] > 1) {
450452
// Use <sanitized-pkg>/<parent-dir-name> as prefix to handle
@@ -700,6 +702,10 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
700702
main_cu.packageCflags = manifest.buildConfig.cflags;
701703
main_cu.packageCxxflags = manifest.buildConfig.cxxflags;
702704
}
705+
// Root-relative -I flags → absolute (G8b), mirroring the scanner's
706+
// treatment of every scanned unit.
707+
mcpp::modgraph::absolutize_include_flags(projectRoot, main_cu.packageCflags);
708+
mcpp::modgraph::absolutize_include_flags(projectRoot, main_cu.packageCxxflags);
703709

704710
// We didn't scan main.cpp earlier (it's not in scanner output unless globbed in).
705711
// Best-effort: scan its imports here.

src/build/prepare.cppm

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,15 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) {
225225
s += " ldflag:";
226226
s += flag;
227227
}
228+
// Per-glob flags (G4): full ordered serialization — glob + every list —
229+
// so editing any entry (or reordering) re-fingerprints the output dir.
230+
for (auto const& gf : m.buildConfig.globFlags) {
231+
s += " globflags:"; s += gf.glob;
232+
for (auto const& f : gf.cflags) { s += " gc:"; s += f; }
233+
for (auto const& f : gf.cxxflags) { s += " gxx:"; s += f; }
234+
for (auto const& f : gf.asmflags) { s += " gas:"; s += f; }
235+
for (auto const& f : gf.defines) { s += " gd:"; s += f; }
236+
}
228237
return s;
229238
}
230239

@@ -327,6 +336,31 @@ materialize_generated_files(const std::filesystem::path& root,
327336
return {};
328337
}
329338

339+
// L1 cfg merge for ONE manifest (root or dependency): append the matching
340+
// conditional cflags/cxxflags/ldflags and sources (G1b) to its buildConfig.
341+
// Sources also update the legacy modules.sources mirror — the scanner walks
342+
// that. Conditional dependency maps are root-only and handled at the root
343+
// call site; a dependency's conditional configs otherwise evaluate the same
344+
// way (descriptor `target_cfg` must not be silently inert).
345+
void merge_conditional_build(mcpp::manifest::Manifest& m,
346+
const cfgpred::Ctx& ctx,
347+
std::string_view targetTriple)
348+
{
349+
for (auto const& cc : m.conditionalConfigs) {
350+
if (!cfgpred::matches(cc.predicate, ctx, targetTriple)) continue;
351+
m.buildConfig.cflags.insert(m.buildConfig.cflags.end(),
352+
cc.cflags.begin(), cc.cflags.end());
353+
m.buildConfig.cxxflags.insert(m.buildConfig.cxxflags.end(),
354+
cc.cxxflags.begin(), cc.cxxflags.end());
355+
m.buildConfig.ldflags.insert(m.buildConfig.ldflags.end(),
356+
cc.ldflags.begin(), cc.ldflags.end());
357+
for (auto const& s : cc.sources) {
358+
m.buildConfig.sources.push_back(s);
359+
m.modules.sources.push_back(s);
360+
}
361+
}
362+
}
363+
330364
bool is_std_module(std::string_view name) {
331365
return name == "std" || name == "std.compat";
332366
}
@@ -717,18 +751,14 @@ prepare_build(bool print_fingerprint,
717751
// flags append to buildConfig, mirroring the [profile] merge above.
718752
if (!m->conditionalConfigs.empty()) {
719753
auto cc_ctx = cfgpred::context_for(overrides.target_triple);
754+
merge_conditional_build(*m, cc_ctx, overrides.target_triple);
720755
for (auto const& cc : m->conditionalConfigs) {
721756
if (!cfgpred::matches(cc.predicate, cc_ctx, overrides.target_triple))
722757
continue;
723-
m->buildConfig.cflags.insert(m->buildConfig.cflags.end(),
724-
cc.cflags.begin(), cc.cflags.end());
725-
m->buildConfig.cxxflags.insert(m->buildConfig.cxxflags.end(),
726-
cc.cxxflags.begin(), cc.cxxflags.end());
727-
m->buildConfig.ldflags.insert(m->buildConfig.ldflags.end(),
728-
cc.ldflags.begin(), cc.ldflags.end());
729758
// Conditional dependencies (Phase 1b): merge into the manifest maps
730759
// before dependency resolution so they resolve like any dep. insert()
731760
// keeps an existing unconditional entry (no silent override).
761+
// Root-only — a dependency's own conditional deps are out of scope.
732762
m->dependencies.insert(cc.dependencies.begin(), cc.dependencies.end());
733763
m->devDependencies.insert(cc.devDependencies.begin(), cc.devDependencies.end());
734764
m->buildDependencies.insert(cc.buildDependencies.begin(), cc.buildDependencies.end());
@@ -962,6 +992,16 @@ prepare_build(bool print_fingerprint,
962992
// when its source/inputs/env change. Leaf-only: it cannot gate the top-level
963993
// dependency graph. Skipped under a cross --target (host program, host run).
964994
// See .agents/docs/2026-06-30-l3-build-mcpp-implementation-design.md.
995+
// Root [generated_files]: materialize before build.mcpp and the modgraph
996+
// scan so synthesized sources are globbed like any on-disk file. (The
997+
// per-dependency call sits in the dep resolution loop below; the root
998+
// manifest needs its own.)
999+
if (!m->buildConfig.generatedFiles.empty()) {
1000+
if (auto r = materialize_generated_files(*root, *m); !r) {
1001+
return std::unexpected(r.error());
1002+
}
1003+
}
1004+
9651005
if (auto bp = mcpp::build::run_build_program(
9661006
*m, *root, explicit_compiler, *tc, m->cppStandard.canonical,
9671007
/*isCross=*/!overrides.target_triple.empty());
@@ -1563,6 +1603,15 @@ prepare_build(bool print_fingerprint,
15631603
"dependency '{}': {}", depName, r.error()));
15641604
}
15651605

1606+
// Dependency-side L1 cfg merge (flags + sources): a descriptor's
1607+
// `target_cfg` / a dep mcpp.toml's [target.'cfg(...)'.build] must
1608+
// evaluate here too — before its globs expand.
1609+
if (!manifest->conditionalConfigs.empty()) {
1610+
merge_conditional_build(*manifest,
1611+
cfgpred::context_for(overrides.target_triple),
1612+
overrides.target_triple);
1613+
}
1614+
15661615
return std::pair{effRoot, std::move(*manifest)};
15671616
};
15681617

@@ -2502,14 +2551,27 @@ prepare_build(bool print_fingerprint,
25022551
auto& bc = pkg.manifest.buildConfig;
25032552
if (!bc.featureSources.empty()) {
25042553
if (!includeDevDeps) {
2505-
std::set<std::string> gated;
2554+
// glob → owned by at least one ACTIVE feature?
2555+
std::set<std::string> activeNow(active.begin(), active.end());
2556+
std::map<std::string, bool> gated;
25062557
for (auto& [f, globs] : bc.featureSources)
2507-
for (auto& g : globs) gated.insert(g);
2558+
for (auto& g : globs)
2559+
gated[g] = gated[g] || activeNow.contains(f);
25082560
auto drop = [&](std::vector<std::string>& v) {
25092561
std::erase_if(v, [&](const std::string& s) { return gated.contains(s); });
25102562
};
25112563
drop(bc.sources);
25122564
drop(pkg.manifest.modules.sources);
2565+
// Dropping the glob STRING is not enough: files it matches
2566+
// may still be covered by a broader base glob (the default
2567+
// src/** — the mcpp.toml G5 case). An inactive gate becomes
2568+
// a `!` exclusion so the gate actually gates; active gates
2569+
// are re-added below.
2570+
for (auto& [g, isActive] : gated) {
2571+
if (isActive || g.starts_with("!")) continue;
2572+
bc.sources.push_back("!" + g);
2573+
pkg.manifest.modules.sources.push_back("!" + g);
2574+
}
25132575
}
25142576
std::set<std::string> activeSet(active.begin(), active.end());
25152577
auto add = [](std::vector<std::string>& v, const std::string& g) {

0 commit comments

Comments
 (0)