Skip to content

Commit c2f0cc0

Browse files
authored
feat(targets): per-target entry-scoped flags + required_features; test --profile; 0.0.55 (#131)
Resolves #131 — per-target build config without breaking compile-once. Per-target defines/cxxflags/cflags (entry-scoped), required_features gate, mcpp test --profile/--features, unsupported-key warnings, docs decision guide. Tests: unit + e2e 70-73. Design: .agents/docs/2026-06-18-per-target-build-config-design.md
1 parent 47d026f commit c2f0cc0

17 files changed

Lines changed: 736 additions & 10 deletions
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Per-Target 构建配置设计:配置发散归编译单元,目标只携本地标志
2+
3+
> 2026-06-18 · 状态: 已实施(0.0.55,①②③④⑤;⑥ 明确不做) · 代码锚点基于 main@47d026f (v0.0.54)
4+
>
5+
> **实施结果**:`[targets.<name>]` 新增 `defines`/`cxxflags`/`cflags`(入口作用域,④)与
6+
> `required_features`(门禁,⑤);不支持键发 warning/`--strict` error(②);`mcpp test`
7+
> `--profile`/`--features`/`--strict`(①);docs/05(及 zh)新增决策指引(③)。
8+
> 单测 `tests/unit/test_manifest.cpp` 覆盖 ②④;e2e `57/58/59` 覆盖 ④⑤①。
9+
> 验证:`mcpp build` 自举通过;`mcpp test` 18/18;新 e2e 三项 + 多目标/workspace/静态/共享/
10+
> 包级 flags 回归集全过。
11+
> 关联: [GitHub Issue #131](https://github.com/mcpp-community/mcpp/issues/131)(`[targets.<name>]` 求 cxxflags 覆盖)
12+
> `.agents/docs/2026-06-04-manifest-schema-ownership.md`(语法封闭·词汇开放)
13+
> `.agents/docs/2026-05-30-package-owned-build-flags-plan.md`(per-unit flags 管线由此建立)
14+
15+
## 0. 问题
16+
17+
Issue #131 请求 `[targets.<name>]` 支持按目标覆盖 `cxxflags`,类比 xmake `target:add("cxflags")` /
18+
CMake `target_compile_options` / Cargo `[profile.*.package.*]`。两个动机示例:
19+
20+
- **场景 A**:`[targets.server] cxxflags=["-DBUILD_SERVER=1"]` / `[targets.client] cxxflags=["-DBUILD_CLIENT=1"]`
21+
—— 一个工程出两个二进制,各带各的宏。
22+
- **场景 B**:`[targets.test_contracts] cxxflags=["-fcontract-evaluation-semantic=observe"]`
23+
—— 测试目标要在不同求值语义下编译。
24+
25+
本设计的结论:**#131 不应按字面("通用 per-target cxxflags over 共享编译池")实现**。该字面诉求
26+
与 mcpp 的 compile-once 模型冲突,且需求已被现有三轴(workspace / features / profile)基本覆盖;
27+
真正缺的是两处小改 + 文档,外加两个可选的、严格受限的便利原语。
28+
29+
## 1. 设计判据(判定法)
30+
31+
> **配置发散在"编译单元(包)"边界,不在"链接单元(target)"边界。**
32+
> target 只能携带它**独占、且不影响共享镜像一致性**的本地标志;凡是会沿模块图传染、
33+
> 或影响 ABI/链接一致性的设置,禁止 per-target。
34+
35+
依据:
36+
37+
- **target 是链接期概念**(哪些 `.o` 链在一起);**编译 flags 是编译期概念**(一个 `.o` 怎么编出来)。
38+
#131 的别扭本质是把编译期配置挂到链接期实体上、还跨越共享编译池——阻抗失配。
39+
- **Cargo 用规则编码了同一条边界**:`[profile.*.package.*]` 允许 `opt-level`/`codegen-units`
40+
这类**本地优化**,却**明令禁止** `panic`/`lto`/`rpath` 这类必须全局一致的项,且任意 `rustflags`
41+
至今只在 nightly `-Z profile-rustflags`(#10271,未稳定)。Cargo `[[bin]]`**不支持** per-bin
42+
features/flags(只有 `required-features` 门禁)。#7916 是 feature resolver v2 的 `dev_dep`
43+
追踪(已闭),属 feature 并集机制,**不是** per-target features。
44+
- 与本仓 `manifest-schema-ownership` 的铁律 C 一致:**包级旋钮统一收敛进 features**,糖键入核需
45+
领域中立 + 1:1 脱糖。per-target 任意 flags 不满足此约束。
46+
47+
## 2. 现状代码盘点(main@47d026f)
48+
49+
| 机制 | 代码锚点 | 现状 |
50+
|---|---|---|
51+
| `Target` 结构(name/kind/main/soname) | `src/manifest.cppm:55-60` | **无任何 flags 字段** |
52+
| `[targets.<name>]` 解析(只读 kind/main) | `src/manifest.cppm:560-579` | 其余键**静默忽略** |
53+
| `[build].cxxflags` 解析(全局一份) | `src/manifest.cppm:895-896` / `BuildConfig:104` | 仅项目级 |
54+
| compile-once:每源一 CompileUnit | `src/build/plan.cppm:295-322` | 共享源**编一次**,被多 target 共享 |
55+
| per-unit flags 管线(已建) | `CompileUnit{packageCflags,packageCxxflags}` `plan.cppm:23-24`;ninja 边变量 `unit_cxxflags` `ninja_backend.cppm:494,541-545,588-592` | #131 可复用 |
56+
| 入口 main 按 target 单独建 CU(**④ 挂载点**) | `src/build/plan.cppm:495-509`(`main_cu`,已灌 `packageCxxflags`) | 入口天然 target 独占 |
57+
| target 独占性:仅入口 main | `plan.cppm:336-348`(entryFilesAcrossTargets);`:427-441`(非入口对象链进每个 target) | 非入口源**不可** per-target 独占 |
58+
| features:additive、按包全局、只出宏 | `src/build/prepare.cppm:2078-2152`(`-DMCPP_FEATURE_*` 推入 `buildConfig.cxxflags` `:2097-2102`) | 共享 lib **编一份** |
59+
| profile:整构建模式(含 cxxflags 逃生舱) | `BuildConfig` 优化旋钮;`prepare.cppm` 合入 active profile | build 可选,见缺口 |
60+
| `mcpp build` 解析 `--profile/--features` | `src/cli/cmd_build.cppm:29-31`;`BuildOverrides` `prepare.cppm:281-287` | OK |
61+
| `mcpp test` **不解析任何 flag** | `cmd_test` `src/cli/cmd_build.cppm:66-71`;`run_tests(passthrough)` `src/build/execute.cppm:406` | **缺口①** |
62+
| fingerprint 含 compile-flags hash | `src/toolchain/fingerprint.cppm:7` | per-target flags 自动覆盖 |
63+
64+
**为什么共享源做不到 per-target 不同 flag**:一个 `.cppm` 编出**一个 `.o` + 一个 BMI**,被多个
65+
target 链接。要让它按 target 带不同宏,就得把编译节点**复制成多份** `obj/<variant>/`,且 BMI 按
66+
`(模块,变体)` 索引——变体会沿 import 图**传染**整片子图。这正是 compile-once 要避免的成本,也是
67+
本设计拒绝 ⑥ 的原因。
68+
69+
## 3. 需求拆解(把 #131 还原成三类真实需求)
70+
71+
1. **变体二进制**(场景 A):宏若只碰各 app **自己的代码** → workspace 拆包;若必须穿透共享 core
72+
→ 那是 core 的互斥变体,features(并集)给不了同构建两版,需分次构建或运行期分发。
73+
2. **测试换模式**(场景 B):求值语义/sanitizer 是**整构建模式**,须到达被测库 → profile,而非 target。
74+
3. **目标独占源的本地标志**:仅作用于该 target 入口/独占源(`-Wno-x``-DVERSION=`)→ 唯一真正
75+
适合 per-target flags 的窄缝(入口独占,无 compile-once 冲突)。
76+
77+
## 4. 方案
78+
79+
### [P0·小] `mcpp test` 接受 `--profile` / `--features`
80+
81+
- **解决**:场景 B 的规范解是 profile,但 `mcpp test` 今天丢弃所有 flag(`cmd_build.cppm:66-71`),
82+
profile 机器无法用于测试构建。
83+
- **改动**:`cmd_test` 照搬 `cmd_build` 的 overrides 解析(`cmd_build.cppm:29-31`);
84+
`run_tests`(`execute.cppm:406`)增收 `BuildOverrides` 并透传至 `prepare_build`(已有 `includeDevDeps=true` 路径)。
85+
- **语义正确性**:profile 整构建统一 → observe/sanitizer **到达被测库**;复用 fingerprint;不破 compile-once。
86+
- **用法**:`mcpp test --profile contracts`,其中 `[profile.contracts] cxxflags=["-fcontracts","-fcontract-evaluation-semantic=observe"]`
87+
88+
### [P0·小] `[targets.<name>]` 不支持键给显式提示
89+
90+
- **解决**:今天写 `[targets.x] cxxflags=[...]` 被静默丢弃(`manifest.cppm:560-579`),零反馈——
91+
正是 #131 的 footgun。
92+
- **改动**:target 解析识别 `cxxflags`/`cflags`/`defines`/`features`/`required_features` 等键。
93+
- 若实施 ④/⑤ → 对应键变为受限支持;
94+
- 其余未支持键 → warning(`--strict` 下报错,沿用现有 strict 通道),提示指向正确机制
95+
(workspace / features / profile)。
96+
97+
### [P1·小] docs 增"Per-target / per-binary 构建配置"决策指引
98+
99+
-`docs/05-mcpp-toml.md` 增一节(紧接 §2.2 targets),给三轴决策:
100+
- 多二进制不同配置(宏在各自代码)→ **workspace member**(docs/06);
101+
- 共享 lib 的可选能力 → **features**(§2.8,additive、按包、编一份);
102+
- 整构建模式 → **profile + `--profile`**(§2.9,build 与 ① 后的 test);
103+
- 并说明**为何不是 per-target cxxflags**(compile-once / BMI 一致性 / 与 Cargo `[[bin]]` 同款限制)。
104+
105+
### [P2·中] 严格"入口/独占源作用域"的 per-target `cxxflags` / `defines`
106+
107+
- **解决**:场景 A 中"宏只碰各自 main"的便利——免拆 workspace。入口源 target 独占 →
108+
编一次只进一个 target → **无共享、无 compile-once 冲突**
109+
- **挂载点(现成)**:`plan.cppm:495-509` 已按 target 构造 `main_cu` 并灌 `packageCxxflags`
110+
改动 = `Target``cxxflags`/`defines`(`manifest.cppm:55-60` + 解析 `:560-579`);
111+
构造 `main_cu` 时把 `t.cxxflags`(及 `defines` 脱糖成 `-D`)**追加**`main_cu.packageCxxflags`;
112+
ninja 边下发与 fingerprint **零额外改动**(复用 `unit_cxxflags` + compile-flags hash)。
113+
- **铁律护栏**:
114+
- 只作用于该 target **独占源**(今日 = 入口 main);
115+
- 若标志会落到**被多 target 共享的对象****报错**,绝不静默半生效;
116+
- `defines` 设为一等键(可校验、跨工具链可移植);`cxxflags` 为裸标志逃生舱。
117+
- 文档明写"不穿透共享模块"。
118+
- **局限(诚实标注)**:今日独占源仅入口 main;要覆盖"server 专属 helper 模块"需再上
119+
`[targets.X] sources=[...]`(per-target 源归属),规模更大,**本设计不含**
120+
121+
### [P2·小-中] `[targets.<name>] required_features = [...]`(借 Cargo 门禁)
122+
123+
- **是什么**:目标门禁——仅当所列 feature 全部激活时才构建该 target,否则跳过(不报错)。
124+
**只门控,不激活**(feature 仍靠 `--features`/`default`)。
125+
- **解决**:① 可选工具/example/平台专属二进制(缩小默认构建面);② 场景 A 的**互斥变体**——
126+
`mcpp build --features server` 只出 server,且 `server` 宏(按包全局)**穿透共享 core**,
127+
client 被门禁挡掉;互斥变体分次构建是正确模型。
128+
- **不解决**:同构建内两 target 不同配置(features 并集,撞 compile-once)。
129+
- **改动**:`Target``requiredFeatures`(`manifest.cppm:55-60` + 解析);
130+
link-unit 循环(`plan.cppm:464`)按**激活 feature 集**(`prepare.cppm:2078-2152` 算出)过滤 target;
131+
**不碰 CompileUnit/fingerprint**——纯链接期选择,零变体成本。
132+
- **与 ④ 互补**:④ = 一次构建多 bin、各自 main 上本地标志(到不了共享);
133+
⑤ = 互斥变体分次构建、配置穿透共享 core。
134+
135+
### [❌·大] 通用 per-target cxxflags over 共享池 / 变体分区 —— 不做
136+
137+
-#131 字面诉求。代价:BMI`(模块,变体)` 翻倍 + 沿 import 图传染整片子图。
138+
- 需求已被 workspace(场景 A)+ features(共享可选开关)+ profile(整构建模式)+ ④/⑤ 覆盖。
139+
- 与判据(§1)及 schema-ownership 铁律 C 冲突。**除非出现 workspace+feature 都解不掉的硬实例,否则不上。**
140+
141+
## 5. 实施清单
142+
143+
最小闭环 = **① + ② + ③**(全小改),即可让 #131 两场景都有规范、正确、今日可用的答案,且消灭静默坑。
144+
④/⑤ 视产品是否要给"单目录多二进制"再加便利,可后续独立 PR。
145+
146+
- [ ] **** `run_tests` 增收 `BuildOverrides`(`execute.cppm:406`);`cmd_test` 解析 `--profile/--features/--strict`(`cmd_build.cppm:66-71`)并透传
147+
- [ ] **** e2e:`mcpp test --profile <observe>` 下被测库以该 profile 编译(对照默认 enforce)
148+
- [ ] **** target 解析识别未支持键并 warning(`--strict` 报错),文案指向 workspace/features/profile(`manifest.cppm:560-579`)
149+
- [ ] **** 单测:`test_manifest` 覆盖 `[targets.x] cxxflags=[...]` 触发提示
150+
- [ ] **** `docs/05-mcpp-toml.md` 新增决策节;`docs/zh` 同步
151+
- [ ] ****(可选)`Target{cxxflags,defines}` + 解析;`main_cu` 追加 target flags(`plan.cppm:495-509`);共享源冲突时报错
152+
- [ ] ****(可选)e2e:两 bin 各带不同 `-D`,各自 main `#ifdef` 断言;验证共享对象不受影响
153+
- [ ] ****(可选)`Target{requiredFeatures}` + 解析;link-unit 按激活集过滤(`plan.cppm:464`)
154+
- [ ] ****(可选)e2e:`--features X` 只出对应 target
155+
156+
## 6. 验证
157+
158+
```bash
159+
mcpp test # 单测 + e2e 全过
160+
mcpp build --profile contracts # ① 前置:build 侧 profile 可用
161+
mcpp test --profile contracts # ① 后:测试在 observe 下编(被测库受影响)
162+
```
163+
164+
## 7. 设计原则沉淀
165+
166+
- **配置发散归编译单元(包),不归链接单元(target)**;target 只携独占、不影响共享一致性的本地标志。
167+
- **可发散的(本地优化/独占标志)与必须一致的(ABI/链接/共享 BMI)划清界**——照抄 Cargo
168+
`[profile.*.package.*]` 允许 opt-level 却禁 panic/lto/rpath 的规矩。
169+
- **包级变体收敛进 features**(additive、并集、按包编一份);跨构建选择用 `required_features` 门禁;
170+
整构建模式用 profile。三者皆不触碰 compile-once。

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.55] — 2026-06-18
7+
8+
### 新增
9+
10+
- `[targets.<name>]` 新增按目标的键 `defines` / `cxxflags` / `cflags`,作用于该目标
11+
**独占的入口源**(它的 `main`)。用于二进制入口私有的标志(如 `-DBUILD_SERVER=1`
12+
局部告警抑制),不影响共享模块/实现对象(compile-once 模型不变)。需要穿透共享代码的
13+
差异请用 workspace member 或 `[features]`(#131)。
14+
- `[targets.<name>]` 新增 `required_features`:仅当列出的 feature 全部激活时才构建该目标,
15+
否则静默跳过。是构建选择门禁,不激活 feature。
16+
- `mcpp test` 现在接受 `--profile` / `--features` / `--strict`,让被测代码与测试二进制
17+
在所选 profile/feature 下编译(适合 sanitizer、契约求值语义等整次构建模式)。
18+
19+
### 变更
20+
21+
- `[targets.<name>]` 下的不支持键不再被静默丢弃,而是产生 warning(`--strict` 下为 error),
22+
并指引到正确的机制(workspace / features / profile)。
23+
- 文档 `docs/05-mcpp-toml.md`(及 `docs/zh`)新增"构建配置该放哪"的决策指引。
24+
设计记录见 `.agents/docs/2026-06-18-per-target-build-config-design.md`
25+
626
## [0.0.54] — 2026-06-10
727

828
### 修复

docs/05-mcpp-toml.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,54 @@ downstream programs can load the library via its standard ABI name through
8181
`DT_NEEDED` or `dlopen()`. This field only applies to `kind = "shared"`, and the
8282
value must be a filename basename.
8383

84+
#### Per-target keys
85+
86+
```toml
87+
[targets.server]
88+
kind = "bin"
89+
main = "src/server.cpp"
90+
defines = ["BUILD_SERVER=1", "PORT=8080"] # -D macros, applied to this target's entry only
91+
cxxflags = ["-Wno-deprecated-declarations"] # extra C++ flags for this target's entry (no -std=...)
92+
cflags = ["-DPURE_C"] # extra C flags for this target's entry
93+
94+
[targets.gui]
95+
kind = "bin"
96+
main = "src/gui.cpp"
97+
required_features = ["gui"] # only built when feature `gui` is active
98+
```
99+
100+
| Key | Meaning |
101+
|---|---|
102+
| `defines` | Preprocessor macros (`name` or `name=value`); desugar to `-D<x>` on both the C and C++ entry compile. |
103+
| `cxxflags` / `cflags` | Extra compile flags for this target. Do **not** put `-std=...` here — use `[package].standard`. |
104+
| `required_features` | The target is emitted only when **every** listed feature is active in the build; otherwise it is silently skipped. A gate only — it does not activate features (use `--features` / `[features].default`). |
105+
106+
> **Scope (important):** `defines` / `cxxflags` / `cflags` on a target apply **only to that
107+
> target's exclusive entry source** (its `main`) — never to shared module/impl objects, which
108+
> are compiled once and linked into every target (mcpp's compile-once model). They are the right
109+
> tool when the flag only needs to affect a single binary's (or test's) own entry — for example a
110+
> per-test contract evaluation semantic (`-fcontract-evaluation-semantic=observe`) for a test whose
111+
> `main` exercises the violation, a feature macro the entry alone reads, or a local warning
112+
> suppression. If a flag must reach **shared** code, it does not belong here — split into a
113+
> [workspace](06-workspace.md) member or use `[features]`, or for a whole-build mode use a
114+
> `[profile.*]` (`mcpp test --profile <name>` builds the whole test image, code-under-test
115+
> included, under that profile).
116+
>
117+
> Unsupported keys under `[targets.<name>]` are reported as a warning (an error under `--strict`).
118+
119+
**Choosing where build configuration goes** — when more than one binary must differ:
120+
121+
| You want | Use |
122+
|---|---|
123+
| Different macros/flags on a binary's **own entry** | per-target `defines` / `cxxflags` (above) |
124+
| Two products that differ in code they **share** | split into [workspace](06-workspace.md) members, each with its own `[build]` flags over a shared `lib` |
125+
| To **select a variant** of a shared library (e.g. a backend) | `[features]` on that library (§2.8) — additive, reaches the library's own compile |
126+
| A **whole-build mode** (sanitizers, contract semantics, opt level) | `[profile.<name>]` (§2.9) + `--profile`; also honored by `mcpp test --profile <name>` |
127+
128+
mcpp deliberately does not compile a shared source two different ways within one build: a source
129+
maps to one object (and one BMI for modules), so divergence that must reach shared code belongs at
130+
the package/feature boundary, not on an individual target.
131+
84132
### 2.3 `[build]` — Build Configuration
85133

86134
```toml
@@ -258,7 +306,8 @@ cxxflags = ["-fno-plt"]
258306
ldflags = []
259307
```
260308

261-
- Selection: `mcpp build --profile <name>`, defaulting to `release`.
309+
- Selection: `mcpp build --profile <name>` (and `mcpp test --profile <name>`, which builds the
310+
code-under-test plus the test binaries under that profile), defaulting to `release`.
262311
- Built-in profiles: `release` (-O2) / `dev`, `debug` (-O0 -g) / `dist` (-O3 + strip;
263312
**LTO is not enabled by default**). `[profile.<built-in name>]` can override a
264313
built-in definition wholesale.

0 commit comments

Comments
 (0)