Skip to content

Commit 01c800c

Browse files
committed
fix: [build].defines is a BuildInputs member, and [build] stops swallowing unknown keys (2026.7.28.1)
Review follow-up on #297. The original fix routed `[build].defines` to the P1689 scan correctly — the mechanism was right and the fold sites already mirrored the #229 conditional-merge funnel. Two things were left open. 1. `defines` lived on BuildConfig, not BuildInputs. The cfg axis carries `ConditionalConfig::inputs`, a BuildInputs — that is the whole point of the type (#258): "membership here is the answer, and it is a type rather than a hand-kept list, so it cannot drift." A BuildConfig- only field is therefore inexpressible under `[target.'cfg(...)'.build]`, and since the conditional reader only reads keys it knows, a platform-only macro vanished without a word — #296's own failure mode, one section over. The fold's comment claimed conditional defines landed; they could not. `defines` is moved to BuildInputs, append() carries it, the conditional reader reads it, and the emptiness gate counts it (a section with only `defines` was being dropped before it was ever evaluated). 2. The root cause was never "defines wasn't folded" — it was that `[build]` read its keys with a bare if-chain and silently discarded everything else. That is #131's footgun, which `[targets.<name>]` already guards against with exactly the comment that describes #296: "a `[targets.x] cxxflags` typo on an older mcpp just vanished." `[build]` and `[target.<pred>.build]` now get the same treatment — a schema warning, an error under --strict. Verified against all 116 mcpp.toml in the repo and every heredoc manifest in tests/: zero false positives. (`build.default_*` in the e2e fixtures is the GLOBAL config.toml, parsed by config.cppm, and is untouched by this.) Also: the xpkg descriptor listed `defines` as a did-you-mean redirect to `flags`. The two manifest surfaces are two spellings of one schema, so a key accepted in an mcpp.toml must not be an unknown-key error in a descriptor — `defines` is now a real key there and in `target_cfg`, and only the singular misspelling still redirects. Tests — the new call sites had no coverage at all, and two thirds of the existing fold sites were untested: - conditional `defines` parse, defines-only section, append() merge, both unknown-key guards, and a manifest exercising every supported `[build]` key so the guard can't fire on one (129 -> 136 unit tests) - e2e 167 rewritten: toolchain-neutral (a LOCAL module, not `import std`, so it runs on Windows where #296 was reported), #error guards as the assertion, plus NAME=value, a value containing a space (the -D quoting path from #234), the cfg axis, a path DEPENDENCY's own defines, and non-propagation to the consumer - e2e 167 no longer masks a failing `mcpp run` behind `tail`'s exit status - e2e 93 asserts `defines` parses as a known descriptor key Version bumped to 2026.7.28.1 (check_version_pins.sh green; the bootstrap MCPP_PIN stays at the released 2026.7.27.1).
1 parent 8a9aedb commit 01c800c

11 files changed

Lines changed: 493 additions & 70 deletions

File tree

docs/05-mcpp-toml.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,33 @@ then only guaranteed to run on the build machine's version and above). A lower
183183
floor (11–13) requires a self-built libc++ archive (already verified to work, a
184184
data-level switch, available on request).
185185

186-
`defines` desugars each entry to `-D<x>` on both the C and C++ compile channels and reaches every TU in the package — including module interface units — so it affects the P1689 module scan. For macros that should only affect a single binary's entry source, use `[targets.<name>].defines` instead.
186+
`defines` takes **bare** macro names (no `-D`) and desugars each entry to `-D<x>` on
187+
both the C and C++ compile channels. It reaches every TU in the package — module
188+
interface units included — so it also reaches the P1689 module scan, which is what
189+
makes a macro-guarded `import` resolvable. Assembly units pick it up too. It is a
190+
build input like any other, so `[target.'cfg(...)'.build]` can carry it:
191+
192+
```toml
193+
[build]
194+
defines = ["APP_NAME=\"demo\""]
195+
196+
[target.'cfg(windows)'.build]
197+
defines = ["USE_WIN32", "WINVER=0x0A00"]
198+
```
199+
200+
Picking the right axis:
201+
202+
| You want the macro on… | Use |
203+
|---|---|
204+
| every TU of this package | `[build].defines` (here) |
205+
| one binary's own entry source only | `[targets.<name>].defines` |
206+
| a specific set of files | `[build].flags` with a `glob` + `defines` |
207+
| every TU **and** every consumer's TUs | `[features.<name>].defines` (an interface contribution) |
208+
209+
`[build].defines` is private to the package: it does not propagate to consumers.
210+
211+
Unsupported keys under `[build]` are reported as a warning (an error under
212+
`--strict`) rather than silently ignored.
187213

188214
Do not configure the C++ standard via `build.cxxflags = ["-std=..."]`. Instead use:
189215

docs/zh/05-mcpp-toml.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,31 @@ cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftP
167167
系统 libc++(产物只保证在构建机同版本及以上运行)。更低 floor(11–13)
168168
需自建 libc++ 归档(已验证可行,数据级切换,按需提供)。
169169

170-
`defines` 把每个条目脱糖为 `-D<x>`,同时作用于 C 和 C++ 编译通道,并覆盖包内每个 TU(包括模块接口单元),因此会影响 P1689 模块扫描。若只想让某个二进制入口源读到宏,请改用 `[targets.<name>].defines`
170+
`defines` 接受****宏名(不带 `-D`),把每个条目脱糖为 `-D<x>`,同时作用于 C 和
171+
C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入 P1689 模块扫描
172+
—— 这正是被宏保护的 `import` 能被解析的前提。汇编单元同样能拿到。它是普通的构建
173+
输入,所以 `[target.'cfg(...)'.build]` 也能承载它:
174+
175+
```toml
176+
[build]
177+
defines = ["APP_NAME=\"demo\""]
178+
179+
[target.'cfg(windows)'.build]
180+
defines = ["USE_WIN32", "WINVER=0x0A00"]
181+
```
182+
183+
选择合适的轴:
184+
185+
| 想让宏作用于… ||
186+
|---|---|
187+
| 本包的每个 TU | `[build].defines`(本节) |
188+
| 仅某个二进制自己的入口源 | `[targets.<name>].defines` |
189+
| 指定的一批文件 | `[build].flags``glob` + `defines` |
190+
| 本包每个 TU **以及**消费者的 TU | `[features.<name>].defines`(接口贡献) |
191+
192+
`[build].defines` 是包私有的:不会传播给消费者。
193+
194+
`[build]` 下不支持的键会作为警告报出(`--strict` 下为错误),而不是被静默忽略。
171195

172196
C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用:
173197

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.7.27.1"
3+
version = "2026.7.28.1"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/prepare.cppm

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -443,10 +443,19 @@ void merge_conditional_build_inputs(mcpp::manifest::Manifest& m,
443443
}
444444

445445
// Desugar `[build].defines` into `-D<x>` on both C and C++ flag channels.
446-
// This must run AFTER conditional `[target.'cfg(...)'.build]` sections are
447-
// merged (so conditional defines land too) and BEFORE the manifest is
448-
// snapshotted into packages[] / fingerprinted. Idempotent: clearing the
449-
// `defines` vector after folding makes repeated calls harmless.
446+
//
447+
// ORDER (both halves are load-bearing): this must run AFTER
448+
// merge_conditional_build_inputs — `defines` is a BuildInputs member, so a
449+
// matching `[target.'cfg(...)'.build] defines` has been appended by then and
450+
// folds in the same pass, landing after the unconditional entries so GNU
451+
// last-wins gives the conditional rule precedence — and BEFORE the manifest is
452+
// snapshotted into packages[] / fingerprinted, because that snapshot (not the
453+
// manifest) is what the P1689 scan, the compile edges and compute_fingerprint
454+
// actually read.
455+
//
456+
// Idempotent: clearing the vector after folding makes repeated calls harmless.
457+
// Both `cflags` and `cxxflags` get the macro; assembly units pick it up for
458+
// free via the -D/-U/-I subset the ninja backend filters out of packageCflags.
450459
void fold_build_defines_into_flags(mcpp::manifest::BuildConfig& bc) {
451460
for (auto const& d : bc.defines) {
452461
bc.cflags.push_back("-D" + d);

src/manifest/toml.cppm

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,38 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
889889
if (val.is_string()) m.xlings.envs[k] = val.as_string();
890890
if (auto v = doc->get_string("build.macos_deployment_target"))
891891
m.buildConfig.macosDeploymentTarget = *v;
892+
893+
// Surface unsupported [build] keys instead of silently dropping them.
894+
// #296 is #131's footgun one section over: `[build] defines` on an mcpp
895+
// that did not know the key simply vanished, and the build then failed
896+
// much later with a module-graph divergence naming neither the key nor
897+
// the manifest. An unknown key in a section this central is always a typo
898+
// or a version mismatch, never an intentional no-op — so say so. Same
899+
// policy as [targets.<name>] above: a warning, an error under --strict.
900+
//
901+
// MUST stay in sync with the `doc->get_*("build.<key>")` reads above.
902+
static constexpr std::string_view kKnownBuildKeys[] = {
903+
"allow_host_libs", "c_standard", "cflags", "cxxflags",
904+
"default-profile", "defines", "dialect_cxxflags", "flags",
905+
"include_dirs", "include_dirs_after", "ldflags",
906+
"macos_deployment_target", "profile", "sources", "static_stdlib",
907+
"target",
908+
};
909+
if (auto* bt = doc->get_table("build")) {
910+
for (auto& [key, _] : *bt) {
911+
bool known = false;
912+
for (auto k : kKnownBuildKeys) if (key == k) { known = true; break; }
913+
if (!known) {
914+
m.schemaWarnings.push_back(std::format(
915+
"[build] has unsupported key '{}' (ignored). Supported keys: "
916+
"sources, cflags, cxxflags, ldflags, defines, flags, "
917+
"include_dirs, include_dirs_after, dialect_cxxflags, "
918+
"c_standard, target, static_stdlib, allow_host_libs, "
919+
"profile, macos_deployment_target.", key));
920+
}
921+
}
922+
}
923+
892924
for (auto const& flag : m.buildConfig.cxxflags) {
893925
if (starts_with_std_flag(flag)) {
894926
return std::unexpected(error(origin,
@@ -992,6 +1024,12 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
9921024
read_list("cxxflags", cc.inputs.cxxflags);
9931025
read_list("ldflags", cc.inputs.ldflags);
9941026
read_list("sources", cc.inputs.sources);
1027+
// #296: package-level macros are a build input like any other,
1028+
// so the cfg axis carries them too — a platform-only macro
1029+
// (`[target.'cfg(windows)'.build] defines = ["USE_WIN32"]`)
1030+
// must reach the scan and the compile exactly like an
1031+
// unconditional one.
1032+
read_list("defines", cc.inputs.defines);
9951033
// #258: per-glob flags and include dirs, through the SAME entry
9961034
// grammar `[build].flags` uses — a conditional section is just
9971035
// a set of build inputs, so it reads the same way.
@@ -1004,6 +1042,31 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
10041042
cc.inputs.globFlags))
10051043
return std::unexpected(error(origin, *err));
10061044
}
1045+
// The conditional axis carries BuildInputs and nothing else, so
1046+
// its vocabulary is exactly that struct's members — a key
1047+
// outside it (`static_stdlib`, `target`, a profile knob) is not
1048+
// conditionable and would otherwise vanish without a word, the
1049+
// #296 failure mode. MUST stay in sync with the reads above and
1050+
// with types.cppm's BuildInputs.
1051+
static constexpr std::string_view kKnownConditionalBuildKeys[] = {
1052+
"cflags", "cxxflags", "defines", "flags",
1053+
"include_dirs", "include_dirs_after", "ldflags", "sources",
1054+
};
1055+
for (auto& [key, _] : bt) {
1056+
bool known = false;
1057+
for (auto k : kKnownConditionalBuildKeys)
1058+
if (key == k) { known = true; break; }
1059+
if (!known) {
1060+
m.schemaWarnings.push_back(std::format(
1061+
"[target.{}.build] has unsupported key '{}' (ignored). "
1062+
"A conditional section may only contribute build INPUTS: "
1063+
"sources, cflags, cxxflags, ldflags, defines, flags, "
1064+
"include_dirs, include_dirs_after. Selection knobs "
1065+
"(target, linkage, static_stdlib) and profile settings "
1066+
"are resolved before the predicate is evaluated and "
1067+
"cannot be conditioned.", triple, key));
1068+
}
1069+
}
10071070
}
10081071
// [target.<predicate>.{dependencies,dev-dependencies,build-dependencies}]
10091072
// parsed via the shared table-based loader (same selectors/namespaces
@@ -1020,6 +1083,7 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
10201083
if (auto r = read_deps("build-dependencies", cc.buildDependencies); !r) return std::unexpected(r.error());
10211084
if (!cc.inputs.cflags.empty() || !cc.inputs.cxxflags.empty()
10221085
|| !cc.inputs.ldflags.empty() || !cc.inputs.sources.empty()
1086+
|| !cc.inputs.defines.empty()
10231087
|| !cc.inputs.globFlags.empty() || !cc.inputs.includeDirs.empty()
10241088
|| !cc.inputs.includeDirsAfter.empty()
10251089
|| !cc.dependencies.empty() || !cc.devDependencies.empty()

src/manifest/types.cppm

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,17 @@ struct BuildInputs {
171171
std::vector<std::string> cflags;
172172
std::vector<std::string> cxxflags;
173173
std::vector<std::string> ldflags;
174+
// #296: package-level preprocessor macros. Unlike per-target `defines`
175+
// (which only reach the binary's own entry TU), these reach EVERY TU in
176+
// the package — module interface units included — so they participate in
177+
// the P1689 module scan, which is what makes a macro-guarded `import`
178+
// resolvable. Desugared to `-D<x>` on both the C and C++ channels
179+
// (fold_build_defines_into_flags in prepare.cppm) after the conditional
180+
// merge and before the manifest is snapshotted into packages[] /
181+
// fingerprinted. A member HERE rather than on BuildConfig so the cfg axis
182+
// can carry it: `[target.'cfg(windows)'.build] defines = [...]` must work,
183+
// and membership of this type is what guarantees it (see above).
184+
std::vector<std::string> defines;
174185
std::vector<GlobFlags> globFlags; // flags = [...] (ordered)
175186
std::vector<std::filesystem::path> includeDirs; // relative to package root
176187
// #249: emitted as -idirafter (searched after the toolchain's system dirs)
@@ -185,6 +196,7 @@ inline void append(BuildInputs& dst, const BuildInputs& src) {
185196
dst.cflags.insert(dst.cflags.end(), src.cflags.begin(), src.cflags.end());
186197
dst.cxxflags.insert(dst.cxxflags.end(), src.cxxflags.begin(), src.cxxflags.end());
187198
dst.ldflags.insert(dst.ldflags.end(), src.ldflags.begin(), src.ldflags.end());
199+
dst.defines.insert(dst.defines.end(), src.defines.begin(), src.defines.end());
188200
dst.globFlags.insert(dst.globFlags.end(),
189201
src.globFlags.begin(), src.globFlags.end());
190202
dst.includeDirs.insert(dst.includeDirs.end(),
@@ -203,13 +215,6 @@ inline void append(BuildInputs& dst, const BuildInputs& src) {
203215
// is read in ~150 places, and a BuildConfig genuinely IS a set of build
204216
// inputs plus the selection axis and resolved policy scalars.
205217
struct BuildConfig : BuildInputs {
206-
// Package-level preprocessor defines. Unlike per-target `defines` (which
207-
// only affect the binary's entry TU), these reach every TU in this
208-
// package — including module interface units — so they participate in
209-
// the P1689 module scan. Desugared to `-D<x>` on both C and C++ compiles
210-
// before the manifest is snapshot into BuildPlan / fingerprint.
211-
std::vector<std::string> defines;
212-
213218
// feature name → extra source globs gated by that feature. A glob listed
214219
// here is EXCLUDED from the default build and only compiled/linked when the
215220
// feature is active for this package (resolved in prepare_build). Lets a

src/manifest/xpkg.cppm

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ namespace mcpp::manifest {
202202
// Kept as one array so the did-you-mean suggester speaks the same vocabulary
203203
// the parser accepts.
204204
inline constexpr std::string_view kKnownXpkgKeys[] = {
205-
"cflags", "c_standard", "cxxflags", "deps", "features", "flags",
205+
"cflags", "c_standard", "cxxflags", "defines", "deps", "features", "flags",
206206
"generated_files", "import_std", "include_dirs", "include_dirs_after",
207207
"language", "ldflags",
208208
"linux", "macosx", "modules", "provides", "runtime", "scan_overrides",
@@ -215,8 +215,10 @@ inline constexpr std::pair<std::string_view, std::string_view> kXpkgKeyAliases[]
215215
{ "dependencies", "deps" },
216216
{ "dependency", "deps" },
217217
{ "requires", "deps" }, // cargo muscle memory
218-
{ "define", "flags" },
219-
{ "defines", "flags" },
218+
// #296: `defines` is a real key now (package-level bare macros). Only the
219+
// singular misspelling needs redirecting, and it points at `defines` — the
220+
// old redirect to `flags` predates the key existing.
221+
{ "define", "defines" },
220222
{ "feature", "features" },
221223
{ "source", "sources" },
222224
{ "target", "targets" },
@@ -1261,6 +1263,7 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
12611263
: sub == "cxxflags" ? &cc.inputs.cxxflags
12621264
: sub == "ldflags" ? &cc.inputs.ldflags
12631265
: sub == "sources" ? &cc.inputs.sources
1266+
: sub == "defines" ? &cc.inputs.defines
12641267
: nullptr;
12651268
// Unknown sub-keys stay a HARD ERROR here. The shared
12661269
// BuildInputs parser must not be read as licence to relax
@@ -1273,8 +1276,8 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
12731276
if (!dst && !pathDst) {
12741277
return std::unexpected(ManifestError{
12751278
std::format("unknown target_cfg key '{}' (expected "
1276-
"cflags/cxxflags/ldflags/sources/flags/"
1277-
"include_dirs/include_dirs_after)", sub),
1279+
"cflags/cxxflags/ldflags/sources/defines/"
1280+
"flags/include_dirs/include_dirs_after)", sub),
12781281
m.sourcePath, 0, 0});
12791282
}
12801283
if (!cur.consume('=') || !cur.consume('{')) {
@@ -1298,6 +1301,7 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
12981301
cur.consume('}');
12991302
if (!cc.inputs.cflags.empty() || !cc.inputs.cxxflags.empty()
13001303
|| !cc.inputs.ldflags.empty() || !cc.inputs.sources.empty()
1304+
|| !cc.inputs.defines.empty()
13011305
|| !cc.inputs.globFlags.empty()
13021306
|| !cc.inputs.includeDirs.empty()
13031307
|| !cc.inputs.includeDirsAfter.empty())
@@ -1517,19 +1521,28 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
15171521
}
15181522
cur.consume('}');
15191523
}
1520-
else if (key == "cflags" || key == "cxxflags" || key == "ldflags") {
1524+
else if (key == "cflags" || key == "cxxflags" || key == "ldflags"
1525+
|| key == "defines") {
15211526
// `{ "-Dfoo", "-Wall", ... }` — appended to the per-rule baseline
15221527
// by ninja_backend. cflags goes to the C rule (.c files), cxxflags
15231528
// to C++ rule (.cpp/.cc/.cxx/.cppm), ldflags to link commands.
1529+
//
1530+
// #296: `defines` carries BARE macro names (`{ "USE_FOO", "N=1" }`,
1531+
// no `-D`) and desugars onto BOTH compile channels at prepare time,
1532+
// which is what lets it reach the P1689 scan. Same key, same
1533+
// meaning as `[build].defines` in an mcpp.toml — the two manifest
1534+
// surfaces are two spellings of one schema, so a key that exists in
1535+
// one must not be a did-you-mean error in the other.
15241536
if (!cur.consume('{')) {
15251537
return std::unexpected(ManifestError{
15261538
std::format("expected '{{' after `{} =`", key),
15271539
m.sourcePath, 0, 0});
15281540
}
15291541
cur.skip_ws_and_comments();
1530-
auto& target = (key == "cflags")
1531-
? m.buildConfig.cflags
1532-
: (key == "cxxflags" ? m.buildConfig.cxxflags : m.buildConfig.ldflags);
1542+
auto& target = (key == "cflags") ? m.buildConfig.cflags
1543+
: (key == "cxxflags") ? m.buildConfig.cxxflags
1544+
: (key == "defines") ? m.buildConfig.defines
1545+
: m.buildConfig.ldflags;
15331546
while (!cur.eof() && cur.peek() != '}') {
15341547
auto s = cur.read_string();
15351548
if (key == "cxxflags" && starts_with_std_flag(s)) {

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "2026.7.27.1";
21+
inline constexpr std::string_view MCPP_VERSION = "2026.7.28.1";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;

0 commit comments

Comments
 (0)