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.

17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [0.0.101] — 2026-07-20

> #253:feature 模型两缺口收口——per-feature per-glob `flags` + per-OS `features` 语义锁定,解锁 compat.opencv `dnn` off-linux 腿并消除 feature-off 构建的死 glob 告警。设计见 `.agents/docs/2026-07-20-issue-253-feature-flags-and-per-os-features-design.md`。

### 新增

- **`features.<name>.flags`**(#253):feature 级 per-glob 编译旗标,与 `[build].flags` 共用同一 entry 文法(`glob` 必填 + `cflags`/`cxxflags`/`asmflags`/`defines`),xpkg 与 mcpp.toml 双文法一数据模型(共享解析 helper)。激活时折入既有 globFlags 单一漏斗(scanner 匹配/per-TU 落旗标/死 glob 告警/fingerprint 四个下游零改动),追加在 base 规则之后、feature 按名序,"last flag wins" 使 feature 规则可覆盖 base;折入点在 `includeDevDeps` 门外(0.0.94 双路径不变量)。**私有 per-TU、不传播**(与接口开关 `defines` 的语义分界)。feature off 时规则不存在 → opencv mlas 一类"结构性必死 glob"的告警自然消失;feature on 而 glob 空仍告警,且文案点名归属 feature(`features.<f>.flags glob '...' matched no source file`)。TOML 侧 `[[features.<name>.flags]]` AOT 拼写同步进 #227 封闭文法 allowlist(`features.*.flags` 通配段)。
- **per-OS `features` 语义锁定**(#253):`mcpp.<os>` 段的文本拼接 additive overlay 本就覆盖 `features` 键——同名 feature 逐子键 append(中性段在前、OS 段在后),per-OS 段可注册 OS-only feature;现以单测(逐 OS `osOverride`)+ e2e 锁死并写入文档,配合 `features.<f>.flags` 构成 opencv `dnn` 的 common/delta 形态(中性段放跨平台公共载荷,per-OS 段放 mlas x86 / NEON 差集与其旗标)。

### 修复

- **依赖包 per-glob flags 此前不入 per-package fingerprint**:`canonical_package_build_metadata` 只序列化 cflags/cxxflags/ldflags/genfiles,globFlags 靠"descriptor 随版本冻结"间接成立;feature 折入使该向量随构建变化,现与根侧同款全量有序序列化(`globflags:/gc:/gxx:/gas:/gd:`)。

### 备注

- e2e 新增 146(feature flags 四象限 + 死 glob 告警消除/点名 + 私有不传播对照)/147(per-OS features 端到端,宿主段生效、非宿主段投毒不可见);单测新增 xpkg/TOML 解析与 per-OS 合并锁定。host/target 轴缺陷(per-OS 拼接键=宿主常量,交叉编译选段错误)另开 issue 跟踪,不入本版。

## [0.0.100] — 2026-07-19

> 大型源码直编包(ffmpeg/opencv 级,数千 TU)全平台化批次:#247/#248/#249 平台三修 + 增量构建修复 + build.mcpp 指令面补全(P1)。设计见 `.agents/docs/2026-07-19-large-source-pkg-platform-fixes-and-buildmcpp-generation-design.md`。
Expand Down
25 changes: 20 additions & 5 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,8 @@ feature carry package-owned preprocessor `defines`, feature-gated source globs
(`sources`, mcpp 0.0.95+ — the globs leave the default build and compile only when
the feature is active, exactly like an index descriptor's `features.<f>.sources`;
the highest-frequency shape for vendored libraries: *feature = a source set + a
define*), and/or capability `requires` / `provides` (see §2.8.1) alongside its
implied features:
define*), feature-gated per-glob compile flags (`flags`, mcpp 0.0.101+), and/or
capability `requires` / `provides` (see §2.8.1) alongside its implied features:

```toml
[features]
Expand All @@ -426,16 +426,31 @@ extra = []
mpl2only = { defines = ["EIGEN_MPL2_ONLY"] }
# Table form: a define + an implied feature.
fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] }
# Table form: feature-gated sources + per-glob flags that co-locate with them.
simd = { sources = ["src/simd/**"], flags = [
{ glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] }
```

- `defines` are **bare** macro names (no `-D`); each desugars to `-D<x>` on the
package's own compile when the feature is active — exactly like `[targets.*]
defines`. They are restricted by convention to the package's **own** namespaced
macros: a feature does **not** inject free-form `cflags`/`ldflags`, which would
break the additive feature-union model. Link flags come from a provider
dependency (§2.8.1), not from a feature.
macros: a feature does **not** inject free-form package-wide `cflags`/`ldflags`,
which would break the additive feature-union model. Link flags come from a
provider dependency (§2.8.1), not from a feature.
- The automatic `-DMCPP_FEATURE_<NAME>` is still defined for every active feature,
so `defines` are additive to it.
- `flags` (mcpp 0.0.101+) is the same ordered array-of-inline-tables grammar as
`[build].flags` (§2.3: `glob` required, plus `cflags`/`cxxflags`/`asmflags`/
`defines`; the `[[features.<name>.flags]]` array-of-tables spelling is accepted
too, like `[[build.flags]]`). When the feature is active the entries
are appended **after** the base `[build].flags`, features in name order, so a
feature rule wins over a broader base rule via "last flag wins"; when it is
inactive the entries do not exist (no dead-glob warning). This is how a
feature's group-specific flags co-locate with its `sources` instead of living
as base rules whose globs go dead on feature-off builds. Unlike `defines`,
feature `flags` are **private per-TU build flags** — they never propagate to
consumers (same contract as `[build].flags`), so they stay inside the additive
model: scoped by glob, deterministic order, no cross-package effect.

### 2.8.1 `provides` / `requires` — Capabilities (backend selection)

Expand Down
18 changes: 15 additions & 3 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,8 @@ extra = []
之外,携带包自有的预处理 `defines`、feature 门控的源 glob(`sources`,mcpp
0.0.95+——列出的 glob 离开默认构建,仅当 feature 激活时才编译,与 index 描述符的
`features.<f>.sources` 完全对等;这正是 vendored 大库最高频的形态:*feature =
一组源文件 + 一个 define*),以及 capability 的 `requires` / `provides`
(见 §2.8.1):
一组源文件 + 一个 define*)、feature 门控的 per-glob 编译旗标(`flags`,mcpp
0.0.101+),以及 capability 的 `requires` / `provides`(见 §2.8.1):

```toml
[features]
Expand All @@ -389,13 +389,25 @@ extra = []
mpl2only = { defines = ["EIGEN_MPL2_ONLY"] }
# 表形式:宏 + 一个隐含 feature。
fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] }
# 表形式:feature 门控源 + 与其同居的 per-glob 旗标。
simd = { sources = ["src/simd/**"], flags = [
{ glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] }
```

- `defines` 为**裸**宏名(不带 `-D`);feature 激活时每个脱糖为 `-D<x>`,加到该包
自己的编译上——与 `[targets.*] defines` 完全一致。按约定仅限包**自有**的带命名
空间宏:feature **不**注入自由的 `cflags`/`ldflags`,否则会破坏加性的 feature
空间宏:feature **不**注入自由的包级 `cflags`/`ldflags`,否则会破坏加性的 feature
并集模型。链接旗标来自 provider 依赖(§2.8.1),而非 feature。
- 每个激活的 feature 仍会得到自动的 `-DMCPP_FEATURE_<NAME>`,`defines` 与之叠加。
- `flags`(mcpp 0.0.101+)与 `[build].flags`(§2.3)共用同一有序 inline-table 数组
文法(`glob` 必填,加 `cflags`/`cxxflags`/`asmflags`/`defines`;与
`[[build.flags]]` 一样也接受 `[[features.<name>.flags]]` 拼写)。feature 激活时
条目追加在 base `[build].flags` **之后**(feature 按名
序),"last flag wins" 使 feature 规则可覆盖更宽的 base 规则;未激活时条目根本
不存在(不会有死 glob 告警)。这让 feature 的组内专属旗标与其 `sources` 同居,
而不必写成 base 规则、在 feature-off 构建里留下必死的 glob。与 `defines` 不同,
feature `flags` 是**私有 per-TU 构建旗标**——永不传播给消费者(与 `[build].flags`
同契约),因此不破坏加性模型:glob 限定作用面、顺序确定、无跨包效应。

### 2.8.1 `provides` / `requires` —— 能力(后端选择)

Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "0.0.100"
version = "0.0.101"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
34 changes: 34 additions & 0 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,20 @@ std::string canonical_package_build_metadata(
s += " ldflag:";
s += flag;
}
// Per-glob flags — same full ordered serialization as the root-side
// block above. Until #253 dependency globFlags were unfingerprinted
// (held only by "descriptor frozen per version" + "feature toggles
// always change cflags via -DMCPP_FEATURE_*"); feature-folded entries
// make the vector build-variant, so fingerprint it directly.
// featureOrigin is diagnostic-only and deliberately NOT serialized
// (the active feature set is already in cflags above).
for (auto const& gf : pkg.manifest.buildConfig.globFlags) {
s += " globflags:"; s += gf.glob;
for (auto const& f : gf.cflags) { s += " gc:"; s += f; }
for (auto const& f : gf.cxxflags) { s += " gxx:"; s += f; }
for (auto const& f : gf.asmflags) { s += " gas:"; s += f; }
for (auto const& f : gf.defines) { s += " gd:"; s += f; }
}
if (pkg.usageResolved) {
for (auto const& dir : pkg.privateBuild.includeDirs) {
s += " private_include:";
Expand Down Expand Up @@ -2972,6 +2986,26 @@ prepare_build(bool print_fingerprint,
}
}
}
// #253: per-feature per-glob flags — fold each ACTIVE feature's
// entries into the base globFlags funnel. Everything downstream
// (scanner glob match, per-TU flag landing, zero-hit warning,
// fingerprint serialization) consumes the ONE vector unchanged.
// Appended AFTER base entries, features in map (= name) order, so
// application order is deterministic and a feature rule wins over
// a broader base rule via "last flag wins". An inactive feature
// contributes nothing — its dead globs no longer exist to warn
// about. Deliberately OUTSIDE any includeDevDeps gate: like the
// sources ADD above, `mcpp build` and `mcpp test` must agree
// (0.0.94 dual-path invariant). featureOrigin tags the entry so
// the scanner's zero-hit warning can name the owning feature.
for (auto& [f, entries] : bc.featureFlags) {
if (std::ranges::find(active, f) == active.end()) continue;
for (auto const& gf : entries) {
auto tagged = gf;
tagged.featureOrigin = f;
bc.globFlags.push_back(std::move(tagged));
}
}
};
if (!packages.empty()) {
auto rootReq = parse_feature_request(overrides.features);
Expand Down
124 changes: 90 additions & 34 deletions src/manifest/toml.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,71 @@ bool is_array_of_tables(const t::Value& v) {
return true;
}

// #253: shared parser for the per-glob flags array shape
// `[{ glob = "...", cflags/cxxflags/asmflags/defines = [...] }, ...]` —
// one entry grammar for `[build].flags` and `[features].<name>.flags`.
// `ctxLabel` names the anchoring key in error messages. Entries append to
// `dst` in declaration order (order is the override semantics). Returns an
// error message, or nullopt on success.
std::optional<std::string> parse_glob_flags_value(
const t::Value& fv, std::string_view ctxLabel, std::vector<GlobFlags>& dst)
{
if (!fv.is_array()) {
return std::format(
"{} must be an array of inline tables "
"(flags = [{{ glob = \"...\", cxxflags = [...] }}, ...])", ctxLabel);
}
for (auto& ev : fv.as_array()) {
if (!ev.is_table()) {
return std::format(
"{} entries must be inline tables with a `glob` key", ctxLabel);
}
auto& et = ev.as_table();
GlobFlags gf;
for (auto& [k, v] : et) {
auto read_list = [&](std::vector<std::string>& out) -> bool {
if (!v.is_array()) return false;
for (auto& s : v.as_array())
if (s.is_string()) out.push_back(s.as_string());
return true;
};
bool ok = false;
if (k == "glob") { ok = v.is_string(); if (ok) gf.glob = v.as_string(); }
else if (k == "cflags") ok = read_list(gf.cflags);
else if (k == "cxxflags") ok = read_list(gf.cxxflags);
else if (k == "asmflags") ok = read_list(gf.asmflags);
else if (k == "defines") ok = read_list(gf.defines);
if (!ok) {
return std::format(
"{}: invalid key '{}' (expected glob = \"...\" "
"plus cflags/cxxflags/asmflags/defines arrays)", ctxLabel, k);
}
}
if (gf.glob.empty()) {
return std::format("{} entry is missing its `glob` key", ctxLabel);
}
dst.push_back(std::move(gf));
}
return std::nullopt;
}

// Allowlist entries are dotted paths whose segments may be the wildcard `*`,
// matching exactly one path segment — needed for #253's `features.<name>.flags`,
// whose middle segment (the feature name) is author-chosen.
bool aot_path_matches(std::string_view pattern, std::string_view path) {
while (true) {
auto pDot = pattern.find('.');
auto sDot = path.find('.');
auto pSeg = pattern.substr(0, pDot);
auto sSeg = path.substr(0, sDot);
if (pSeg != "*" && pSeg != sSeg) return false;
if (pDot == std::string_view::npos || sDot == std::string_view::npos)
return pDot == std::string_view::npos && sDot == std::string_view::npos;
pattern.remove_prefix(pDot + 1);
path.remove_prefix(sDot + 1);
}
}

std::optional<std::string> find_disallowed_array_of_tables(
const t::Table& tbl, const std::string& prefix,
std::span<const std::string_view> allowlist)
Expand All @@ -60,7 +125,7 @@ std::optional<std::string> find_disallowed_array_of_tables(
std::string path = prefix.empty() ? k : std::format("{}.{}", prefix, k);
if (is_array_of_tables(v)) {
bool allowed = false;
for (auto a : allowlist) if (a == path) { allowed = true; break; }
for (auto a : allowlist) if (aot_path_matches(a, path)) { allowed = true; break; }
if (!allowed) return path;
} else if (v.is_table()) {
if (auto found = find_disallowed_array_of_tables(v.as_table(), path, allowlist))
Expand All @@ -82,11 +147,15 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
// Closed-grammar guard: reject any array-of-tables whose dotted path
// isn't explicitly allowlisted, BEFORE any section is read. See
// find_disallowed_array_of_tables above.
static constexpr std::string_view kAllowedArraysOfTables[] = { "build.flags" };
static constexpr std::string_view kAllowedArraysOfTables[] = {
"build.flags",
"features.*.flags", // #253 — the middle segment is the feature name
};
if (auto badPath = find_disallowed_array_of_tables(doc->root(), "", kAllowedArraysOfTables)) {
return std::unexpected(error(origin, std::format(
"[[{}]] (array-of-tables) is not allowed for section '{}'; "
"array-of-tables syntax is only supported for [[build.flags]]",
"array-of-tables syntax is only supported for [[build.flags]] "
"and [[features.<name>.flags]]",
*badPath, *badPath)));
}

Expand Down Expand Up @@ -241,6 +310,21 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
read_str_array(ft, "provides", provs);
if (!reqs.empty()) m.featureRequires[fname] = std::move(reqs);
if (!provs.empty()) m.featureProvides[fname] = std::move(provs);
// #253: per-feature per-glob compile flags — same entry grammar
// as [build].flags (shared parse_glob_flags_value), gated by
// this feature and folded in AFTER base globFlags at activation
// so feature rules win via "last flag wins". Both spellings
// reach here: the inline array and [[features.X.flags]] AOT
// (allowlisted via the features.*.flags pattern, mirroring
// #227's build.flags decision — libs/toml builds one shape).
if (auto it = ft.find(std::string("flags")); it != ft.end()) {
if (auto err = parse_glob_flags_value(
it->second,
std::format("[features].{}.flags", fname),
m.buildConfig.featureFlags[fname])) {
return std::unexpected(error(origin, *err));
}
}
}
// #243: split `dep/feat` tokens out of `implies` into featureForwards
// (raw depKey shares the `dependencies`/`featureDeps` keyspace); the
Expand Down Expand Up @@ -782,37 +866,9 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
"(flags = [{ glob = \"...\", cxxflags = [...] }, ...]) "
"or an array of tables ([[build.flags]] glob = \"...\")"));
}
for (auto& ev : fv->as_array()) {
if (!ev.is_table()) {
return std::unexpected(error(origin,
"[build].flags entries must be inline tables with a `glob` key"));
}
auto& et = ev.as_table();
GlobFlags gf;
for (auto& [k, v] : et) {
auto read_list = [&](std::vector<std::string>& out) -> bool {
if (!v.is_array()) return false;
for (auto& s : v.as_array())
if (s.is_string()) out.push_back(s.as_string());
return true;
};
bool ok = false;
if (k == "glob") { ok = v.is_string(); if (ok) gf.glob = v.as_string(); }
else if (k == "cflags") ok = read_list(gf.cflags);
else if (k == "cxxflags") ok = read_list(gf.cxxflags);
else if (k == "asmflags") ok = read_list(gf.asmflags);
else if (k == "defines") ok = read_list(gf.defines);
if (!ok) {
return std::unexpected(error(origin, std::format(
"[build].flags: invalid key '{}' (expected glob = \"...\" "
"plus cflags/cxxflags/asmflags/defines arrays)", k)));
}
}
if (gf.glob.empty()) {
return std::unexpected(error(origin,
"[build].flags entry is missing its `glob` key"));
}
m.buildConfig.globFlags.push_back(std::move(gf));
if (auto err = parse_glob_flags_value(
*fv, "[build].flags", m.buildConfig.globFlags)) {
return std::unexpected(error(origin, *err));
}
}
if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v;
Expand Down
Loading
Loading