From 7fd8b4633e2c2893c8aaca2b488e94beab30b968 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 19 Jul 2026 08:13:32 +0800 Subject: [PATCH 1/6] =?UTF-8?q?docs(design):=200.0.99=20batch=20=E2=80=94?= =?UTF-8?q?=20#243=20forwarding=20+=20#238=20xlings=20bump=20+=20#230=20wi?= =?UTF-8?q?ndows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconciles the #243 forwarding design onto the actual 0.0.98 funnel (DependencyEdge + aggregatedRequest, landed by 0cca9e0) instead of the superseded requestedFeaturesByPkg accumulator, and folds in the #238 (vendored xlings 0.4.67) and #230 (build.mcpp .exe) plans. --- ...-19-issue-243-feature-forwarding-design.md | 2 + ....0.99-feature-forwarding-238-230-design.md | 220 ++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 .agents/docs/2026-07-19-v0.0.99-feature-forwarding-238-230-design.md diff --git a/.agents/docs/2026-07-19-issue-243-feature-forwarding-design.md b/.agents/docs/2026-07-19-issue-243-feature-forwarding-design.md index 7d58e87b..a8e86ceb 100644 --- a/.agents/docs/2026-07-19-issue-243-feature-forwarding-design.md +++ b/.agents/docs/2026-07-19-issue-243-feature-forwarding-design.md @@ -1,5 +1,7 @@ # Issue #243 —— feature 依赖转发(`dep/feat`)设计 +> **⚠️ 实现路径已校正(见 [2026-07-19-v0.0.99-feature-forwarding-238-230-design.md](2026-07-19-v0.0.99-feature-forwarding-238-230-design.md))**:本文 §5 提出的 `requestedFeaturesByPkg` 累加器,在 0.0.98 已由 `0cca9e0` 的 `DependencyEdge` + `aggregatedRequest` 单一漏斗实现;#243 只需**把转发注入这个既有漏斗**(把转发 feature 追加进子依赖 push 前的 `spec.features`),不再新建平行结构。§1–§4、§6–§8 的语义分析仍然有效。 +> > 日期:2026-07-19 > 基线:mcpp **0.0.98**(HEAD;`MCPP_VERSION = "0.0.98"` @ `src/toolchain/fingerprint.cppm:21`) > 范围:GitHub issue **#243** —— manifest `[features]` 的 (1) feature 条件依赖 与 (2) 依赖 feature **转发**(Cargo `dep/feat` 语法)。阻塞用例:opencv 模块包的 `opencv.dnn` 接口(一个 feature 既要拉入一个依赖、又要打开该依赖的某个 feature)。 diff --git a/.agents/docs/2026-07-19-v0.0.99-feature-forwarding-238-230-design.md b/.agents/docs/2026-07-19-v0.0.99-feature-forwarding-238-230-design.md new file mode 100644 index 00000000..a5ce2a1a --- /dev/null +++ b/.agents/docs/2026-07-19-v0.0.99-feature-forwarding-238-230-design.md @@ -0,0 +1,220 @@ +# v0.0.99 批次设计 —— #243 feature 转发 + #238 xlings 升级 + #230 windows 复验 + +> 日期:2026-07-19 +> 基线:mcpp **0.0.98**(HEAD;`MCPP_VERSION` @ `src/toolchain/fingerprint.cppm:21`) +> 目标版本:**0.0.99** +> 覆盖 issue:**#243**(manifest `[features]` 的 dep/feat 转发)、**#238**(≥2 项目级 index_repo 时 xlings `install_packages` 静默失败)、**#230**([windows] `mcpp test --workspace` 退出 127)。 +> 前序设计:[2026-07-19-issue-243-feature-forwarding-design.md](2026-07-19-issue-243-feature-forwarding-design.md)(#243 语义与用例分析)。本文 **§1.3 校正**其 §5 的实现路径:0.0.98 已通过 `0cca9e0` 落地 `DependencyEdge` + `aggregatedRequest` 单一漏斗,原设计假想的 `requestedFeaturesByPkg` 累加器**已经存在**,#243 只需注入这个既有漏斗,不再新建平行结构。 +> 上游总账:[2026-07-19-issues-230-243-batch-ledger-and-architecture-assessment.md](2026-07-19-issues-230-243-batch-ledger-and-architecture-assessment.md)。 +> 所有 file:line 锚点均在 0.0.98 源码上核实。 + +--- + +## 0. 三个 issue 的现状核实(结论先行) + +| issue | 类型 | 0.0.98 现状(核实) | 0.0.99 需做 | +|---|---|---|---| +| **#243** | enhancement | 转发(`dep/feat`)**完全不存在**;条件依赖(`[feature-deps]` / xpkg `features.x.deps`)已存在 | **新增转发**:数据模型 `featureForwards` + 两文法解析 + 注入 `dependencyEdges` 漏斗 | +| **#238** | bug | 根因在 xlings(`install_packages` 多仓解析),mcpp 侧诊断已落地(`00b66c5`);**xlings#374 已在上游修复并发布 0.4.67**(`cf9b60d56`) | **升级 vendored xlings 0.4.62 → 0.4.67**,生态真实验证多仓安装 | +| **#230** | bug(win) | scanner symlink 逃逸 + 窄串转换抛异常→`__fastfail`→裸 127,**代码已修**(`df985df`/#231,在 HEAD)+ `main.cpp` 兜底 catch | **复验** + 补 `build.mcpp` 产物 windows `.exe` 后缀(逃逸修复后 workspace build-mcpp 成员在 windows 才会被走到的次生面) | + +三者共版本 0.0.99 一次 PR/release,是 #230–#243 批次的收尾(参见 batch ledger §3 "剩余开口工作")。 + +--- + +## 1. #243 —— feature 依赖转发(Cargo `dep/feat` 平价) + +### 1.1 语义与用例 + +opencv 模块包(源码直编形态)要提供 `import opencv.dnn;`:一个 feature `dnn` 既要拉入 `compat.opencv`(条件依赖,已支持),又要**打开该依赖的 `dnn` feature**(转发,新增),否则 `dnn` 的 +309 TU 要么对所有消费者永远全量拉、要么无法提供。 + +期望:`opencv = { version="…", features=["dnn"] }`(消费端)→ 模块包启用 `dnn.cppm` 源 + 依赖的 `compat.opencv` **以 `features=["dnn"]` 参与构建**。转发必须**传递**(链式转发继续)且在 **`mcpp build` 与 `mcpp test` 双路径**上都成立。 + +### 1.2 面向用户语法(Cargo 平价) + +`[features]` 数组(及表格形 `implies`)里,**含 `/` 的 token = 转发 `/`**,不含 `/` = 本地 implied feature。与 Cargo `[features] F = ["dep/feat", ...]` 完全一致: + +```toml +[features] +# dnn 激活时:本地拉 dnn.cppm 源集,并把 compat.opencv 的 dnn feature 打开 +dnn = { sources = ["src/dnn.cppm"], implies = ["compat.opencv/dnn"] } +# 数组简写等价:dnn = ["compat.opencv/dnn"] +# 表格形亦可用专用键(自解释、等价):dnn = { forward = ["compat.opencv/dnn"] } + +[dependencies] +compat.opencv = { version = "0.0.3", default-features = false } +``` + +`depKey` 与 `[dependencies]` / `[feature-deps]` **同键空间**(`resolve_dependency_selector(...).stableMapKey`,实为原始 selector 串;`compat.opencv` 保持 `compat.opencv`)。转发是**加法**:只增开依赖的 feature,不改依赖是否被拉入(那是 `[feature-deps]` 的职责),不受 `default-features` 开关影响(§1.5)。 + +> **不加 `deps` 内联**:条件依赖的 canonical TOML 面仍是独立 `[feature-deps.]` 段(需要完整 DependencySpec);转发只需两个字符串,内联进 `[features]` 是自然的。二者正交、可组合(§1.1 用例即二者同用)。 + +### 1.3 数据模型(唯一新增) + +`src/manifest/types.cppm`(紧邻 `featuresMap` / `featureDeps`,同族)新增: + +```cpp +// #243: dep/feat 转发(Cargo 平价)。featureName → [(depStableKey, depFeature)]。 +// 本包 feature F 激活时,把 depFeature 注入依赖 depStableKey 的请求集。 +// depStableKey 与 dependencies / featureDeps 同键空间(stableMapKey == 原始 selector 串)。 +std::map>> featureForwards; +``` + +**不新增** `DependencySpec` 字段、**不新增** TOML 段。 + +### 1.4 共享切分 helper(两文法唯一切分点) + +`src/pm/dependency_selector.cppm`(两文法均已 `import` 之)加一个纯 inline 函数: + +```cpp +// "compat.opencv/dnn" → {depKey:"compat.opencv", depFeature:"dnn"}。无 '/' 或空段 → nullopt(按本地 implied 处理)。 +inline std::optional> +split_feature_forward_token(std::string_view token); +``` + +按**首个** `/` 切分(feature 名不含 `/`)。两解析器把 implies token 逐个过它:命中→ `featureForwards`,否则→ `featuresMap`(implies)。 + +- **TOML**(`toml.cppm:196-237`):array 简写与表格 `implies` 收进 `implied` 后统一 partition;新增表格键 `forward`(值为 `dep/feat` token 数组)也走同一 helper。 +- **xpkg**(`xpkg.cppm:1108-1195`):feature 表 `}` 收尾后对 `featuresMap[fname]` 做同一 partition(与 TOML 对称)。`deps` 分支不变。 + +### 1.5 传播算法 —— 注入既有 `aggregatedRequest` 漏斗(核心) + +0.0.98 的 `prepare.cppm` 已把 per-package 请求 feature 集收敛为唯一漏斗: + +- `DependencyEdge{ consumerPackageIndex, dependencyPackageIndex, requestedFeatures, defaultFeatures }`(`:1849-1863`),由 `recordDependencyEdge(consumerDepIndex, depIdx, spec)` 从 `spec.features` 填充(`:1948-1975`)。 +- `aggregatedRequest(depPkgIndex)`(`:2858-2871`):对某 dep,union 所有入边的 `requestedFeatures`(菱形安全,Cargo 语义)。 +- 两个消费点都读它:激活 `apply(packages[i], req, depDefaultFeatures)`(`:2872-2890`)与 dep build.mcpp 的 `bpEnv.features`(`:2910-2918`)。 +- 解析期 `mergeActiveFeatureDeps(pm, spec.features, spec.defaultFeatures)`(`:2646`)用**同一** `spec.features` 拉条件依赖。 + +**转发的唯一注入点:在把某包 P 的子依赖 push 进 worklist 之前,按 P 的激活 feature 集,把 P 转发给该子依赖的 feature 追加进子依赖的 `spec.features`。** 一处注入同时覆盖两个消费点: + +1. **解析期**:子依赖被处理时 `mergeActiveFeatureDeps(*dep_manifest, spec.features, …)` 用到的 `spec.features` 已含转发 feature → 该子依赖的 `[feature-deps.]` 也随之展开(转发能触发被转发包自身的条件依赖)。 +2. **激活期**:`recordDependencyEdge(…, spec)` 把含转发 feature 的 `spec.features` 记到 P→D 边上 → `aggregatedRequest(D)` union 到它 → `apply(D)` 激活该 feature(发 `-DMCPP_FEATURE_`、`featureDefines`、`featureSources` drop/add)。**`apply()` 内部一字不改。** + +**注入 helper**(`prepare.cppm`,近其它 feature lambda): + +```cpp +// P 的激活 feature 集 parentActive 决定哪些转发生效;把 P 转发给 childKey 的 feature +// 并入 childSpec.features(去重)。在 child push 进 worklist 前调用。 +auto injectForwards = [](const Manifest& parent, const vector& parentActive, + const string& childKey, DependencySpec& childSpec) { + if (parent.featureForwards.empty()) return; + for (auto& f : parentActive) + if (auto it = parent.featureForwards.find(f); it != parent.featureForwards.end()) + for (auto& [depKey, depFeat] : it->second) + if (depKey == childKey && + find(childSpec.features.begin(), childSpec.features.end(), depFeat) + == childSpec.features.end()) + childSpec.features.push_back(depFeat); +}; +``` + +**两处 push 点各加一次注入(push 的是增广后的 spec 拷贝,不改存储的 manifest):** + +- **root**(`:2172-2183`):`rootActive = feature_closure(*m, rootReq, true)`;对 `m->dependencies` 每个子依赖 `injectForwards(*m, rootActive, n, reqCopy)` 后 push。 +- **dep**(`:2686-2689`):`depActive = feature_closure(*dep_manifests.back(), spec.features, spec.defaultFeatures)`(与 `:2646` 的 `mergeActiveFeatureDeps` 同闭包);对其子依赖同样注入后 push。 + +**传递性**:root→mid→leaf 的链,mid 处理时其 `spec.features`(来自 root 对 mid 的边,已含 root 的转发)决定 `depActive`,再把 mid 对 leaf 的转发注入 leaf 的边 → 沿 BFS 前向边天然递归。**终止性**:feature 集单调增且有限;`feature_closure` 的 `seen` 挡 implies 环。 + +**菱形局限(已知,与既有一致)**:若 D 有多个消费者且**晚到**的转发边在 D 已 resolve 之后才出现,晚到的转发不会重新触发 D 的条件依赖展开——这与 0.0.98 `aggregatedRequest` 注释所述"解析期按边、激活期 union"的同一历史边界一致(激活期仍会 union 到,`apply` 正确;只有"晚到转发再触发 D 的 `[feature-deps]`"这一步受限)。opencv.dnn 是 root→opencv→protobuf 的树链,不触及此边界。 + +### 1.6 与 #242(`default-features = false`)组合 + +转发是加法、default-features 是减法,二者在 `feature_closure` 入口并集、正交: + +- 转发注入到的是 `spec.features`(显式请求),**不受** `default-features` 开关影响。 +- `compat.opencv = { default-features = false }` + 转发 `compat.opencv/dnn` → opencv 的 `requested = {dnn}`,default seed 被跳过 → **只带 dnn(+其 implies),不带 opencv 默认 feature 集**。正是"精简依赖"想要的。 + +### 1.7 校验(复用漏斗红利) + +- **转发未定义的 dep feature**(`opencv/nonesuch`):转发 feature 经边进 `aggregatedRequest`,被**既有**"dependency does not declare requested feature"门(`:2875-2883`)校验 → strict 报错 / warn。无需新增校验路径。 +- **转发到未声明的 dep**(depKey 不在 `dependencies`∪`featureDeps`):新增轻量校验——在注入处,若某激活 feature 的转发 depKey 不在该包(已折入 feature-deps 的)`dependencies` 中,strict 报错 / 非 strict warn(与既有 feature 校验同模式)。feature 未激活则不校验(惰性)。 + +### 1.8 测试 + +**单测**(`tests/unit/test_manifest.cpp`,并列既有 `FeatureDepsTomlSection` / `FeatureDepsAndImplies`): + +- `TEST(Manifest, FeatureForwardTomlParse)`:`F = ["local", "compat.opencv/dnn"]` → `featuresMap["F"]=={"local"}` 且 `featureForwards["F"]=={{"compat.opencv","dnn"}}`。 +- `TEST(Manifest, FeatureForwardTableForms)`:表格 `implies` 混入 + 专用 `forward` 键都分流到 `featureForwards`。 +- `TEST(SynthesizeFromXpkgLua, FeatureForwardParse)`:xpkg `implies={"dep/x"}` → `featureForwards`。 + +**e2e**(host-aware,`tests/e2e/128_feature_forwarding.sh`,并入 `run_all.sh`;模板同 `126_default_features_opt_out.sh`): + +- 夹具:`leaf`(feature `extra` 带 `defines=["LEAF_EXTRA=1"]`,不在 default);`mid` 依赖 `leaf`(`default-features=false`)且 `[features] withextra = ["leaf/extra"]`;`app` 依赖 `mid`。 + - `app` 请求 `mid` 的 `withextra` → leaf 以 extra 编译,`LEAF_EXTRA` / `MCPP_FEATURE_EXTRA` 在 leaf 的 `compile_commands.json` 在场(转发经 root→mid→leaf 传递)。 + - `app` 不请求 withextra → leaf 无 extra,`LEAF_EXTRA` **不**在场。 + - **双路径**:同断言在 `mcpp test` 复跑(锁 0.0.97 双路径不变量)。 + +--- + +## 2. #238 —— 升级 vendored xlings 0.4.62 → 0.4.67 + +### 2.1 根因归属(核实) + +`install_packages` 在 ≥2 项目级 `index_repos` 时静默 exit 1,**根因在 xlings**(不在 mcpp)。已开 **openxlings/xlings#374**,该 issue **已 closed**:上游修复 `cf9b60d56` "fix(xim): surface multi-repo install failures + best-effort catalog (#374)",发布于 **xlings 0.4.67**(`v0.4.66...v0.4.67` 恰好一条该 commit)。mcpp 侧诊断(`00b66c5`,把裸 `exit 1` 变成可操作错误)已在 0.0.98。 + +### 2.2 改动 + +把 release/构建/e2e 三处 workflow 的 vendored xlings pin 从 `0.4.62` 升到 `0.4.67`: + +- `.github/workflows/release.yml`(多处 `XLINGS_VERSION: '0.4.62'`、aarch64 bundle URL `xlings-0.4.62-linux-aarch64`)。 +- `.github/workflows/cross-build-test.yml`(2 处)。 +- `.github/workflows/ci-linux-e2e.yml`(`quick_install.sh v0.4.62`)。 + +`src/xlings.cppm:36 kXlingsVersion` 是 xlings **self-install** 的最低 pin,与多仓解析无关,可一并同步到 `0.4.67`(保守:核对其消费点后再定;不影响 #238 修复,因修复来自 bundle 的 xlings 二进制行为)。 + +### 2.3 生态真实验证(§4) + +release 后按发布闭环:mirror xlings-res → xim-pkgindex → **构造 ≥2 index_repo 的沙箱真装一个未缓存包**(如 `compat:compat.imgui`),断言 exit 0(0.4.62 下为静默 exit 1);再 bump bootstrap pin。 + +--- + +## 3. #230 —— windows workspace 退出 127 + +### 3.1 根因与既有修复(核实) + +`mcpp test --workspace` 在 windows 于成员 glob 扫描期,`recursive_directory_iterator(follow_directory_symlink)`(#220 `d440a0b` 引入)顺 `#224` 建的 `.mcpp/.xlings/data/` symlink 逃逸进 vendored `xim-pkgindex`,其中一个 **CJK 文件名**触发 MSVC `lexically_relative().generic_string()` 宽→窄转换抛 `std::system_error`;0.0.95 未捕获 → `std::terminate` → `__fastfail(0xC0000409)` → git-bash 报裸 **127**。 + +**代码已修**(在 HEAD): +- `scanner.cppm`:`is_excluded_walk_dir` prune `.mcpp`(`:291-300`,两处 walk 调用 `:438-441`/`:531-532`)+ 窄串转换 try/catch never-throw(`:135-146`)。 +- `src/main.cpp`:顶层 catch,未捕获异常→`error: internal: unhandled exception` + exit 70(不再裸 127)。 +来源 `df985df`(#231),0.0.98 已含。 + +### 3.2 0.0.99 补:`build.mcpp` 产物 windows `.exe` 后缀 + +scanner 逃逸修复后,workspace 的 `build-mcpp` 成员(带 `build.mcpp`)在 windows 才会被真正走到——此时暴露一个次生 windows 面: + +`src/build/build_program.cppm:539` 硬编码产物名 `build.mcpp.bin`(无 `.exe`),`:591` 经 `capture_exec` 运行;windows 上 `capture_exec` 走 cmd.exe(`_popen`,argv[0] 原样不加引号),**cmd 无法按 `.bin` 扩展名执行 PE**(不在 PATHEXT)。 + +**修复**:产物名按平台取后缀(与 `plan.cppm:201` 用 `mcpp::platform::exe_suffix` 同惯例): + +```cpp +fs::path bin = bdir / (mcpp::platform::is_windows ? "build.mcpp.exe" : "build.mcpp.bin"); +``` + +非 windows 字节不变(仍 `build.mcpp.bin`);windows 产出并按 `.exe` 名执行。此改动零 schema、零跨平台行为回归(`is_windows` 是 constexpr)。既有 e2e `89_build_mcpp.sh` / `92_build_mcpp_import.sh` 在 linux 覆盖(exe_suffix 为空,无行为变化);windows 侧由 CI(ci-windows)+ mcpp-index workspace-windows 复验。 + +### 3.3 复验与关闭 + +0.0.99 release 后,mcpp-index 的 workspace(windows)CI pin 升到 0.0.99(从临时钉回的 0.0.94),全绿则关闭 #230。 + +--- + +## 4. 版本 / PR / 发布 / 生态验证计划 + +**单 PR,逐 commit 绿(每 commit `mcpp build` self-host + `mcpp test` 全绿):** + +1. `feat(manifest)`:`featureForwards` 数据模型 + `split_feature_forward_token` + 两文法解析 + 解析单测。 +2. `feat(build)`:转发注入 `dependencyEdges` 漏斗(root/dep 两 push 点)+ 未声明-dep 校验 + e2e 128 并入 `run_all.sh`。 +3. `fix(build.mcpp)`:windows `.exe` 后缀(#230 次生面)。 +4. `chore(deps)`:vendored xlings 0.4.62 → 0.4.67(#238);workflow pins。 +5. `chore(release)`:版本 `0.0.99`(`fingerprint.cppm` + `mcpp.toml` + CHANGELOG,仅末尾改版本号)。 + +**发布闭环**(memory `release-publish-pipeline`):release→四平台产物→mirror xlings-res(gh+gtc 双端)→xim-pkgindex PR→**xlings install 真装 0.0.99**→bump bootstrap pin(`.xlings.json` / workspace)。 + +**生态真实验证(必做,三 issue 各一条闭环):** +- #243:在真实索引拉一个带转发的包(或本地夹具),`import` 被转发 feature 提供的模块接口,编译+运行通过。 +- #238:≥2 index_repo 沙箱真装未缓存包,exit 0。 +- #230:mcpp-index workspace(windows)CI 在 0.0.99 全绿。 + +**架构验收**:manifest 文法 / xpkg 描述符 / feature 模型三处对"转发"收敛为**一数据模型 `featureForwards` 多文法**;per-package 请求 feature 集仍是唯一漏斗 `aggregatedRequest`(转发只注入,不新建平行结构)。 From d40d91b2815cad1e0ad05a6dfc9055a549c71ed7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 19 Jul 2026 08:13:32 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(manifest):=20#243=20feature=20dep/feat?= =?UTF-8?q?=20forwarding=20=E2=80=94=20data=20model=20+=20two=20grammars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Manifest::featureForwards (featureName -> [(depStableKey, depFeature)]) and one shared split point split_feature_forward_token in dependency_selector. A [features] implied token containing '/' (or the dedicated table-form 'forward' key) forwards a feature to a dependency, Cargo-parity. Both the TOML and xpkg-descriptor grammars partition '/' tokens out of implies into featureForwards (raw depKey == stableMapKey, same keyspace as dependencies/featureDeps). Unit tests for both surfaces. --- src/manifest/toml.cppm | 21 +++++++- src/manifest/types.cppm | 12 +++++ src/manifest/xpkg.cppm | 15 ++++++ src/pm/dependency_selector.cppm | 19 +++++++ tests/unit/test_manifest.cpp | 96 +++++++++++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 1 deletion(-) diff --git a/src/manifest/toml.cppm b/src/manifest/toml.cppm index a24b4dd7..1d4848f3 100644 --- a/src/manifest/toml.cppm +++ b/src/manifest/toml.cppm @@ -210,12 +210,18 @@ std::expected parse_string(std::string_view content, }; for (auto& [fname, fval] : *features_table) { std::vector implied; + std::vector forwardTokens; if (fval.is_array()) { for (auto& v : fval.as_array()) if (v.is_string()) implied.push_back(v.as_string()); } else if (fval.is_table()) { auto& ft = fval.as_table(); read_str_array(ft, "implies", implied); + // #243: a feature may forward features to its dependencies + // (Cargo `dep/feat`). Two equivalent spellings, one data model: + // `dep/feat` tokens mixed into `implies` (Cargo parity), or a + // dedicated self-documenting `forward = ["dep/feat", ...]` key. + read_str_array(ft, "forward", forwardTokens); std::vector defs; read_str_array(ft, "defines", defs); if (!defs.empty()) m.buildConfig.featureDefines[fname] = std::move(defs); @@ -232,7 +238,20 @@ std::expected parse_string(std::string_view content, if (!reqs.empty()) m.featureRequires[fname] = std::move(reqs); if (!provs.empty()) m.featureProvides[fname] = std::move(provs); } - m.featuresMap[fname] = std::move(implied); + // #243: split `dep/feat` tokens out of `implies` into featureForwards + // (raw depKey shares the `dependencies`/`featureDeps` keyspace); the + // dedicated `forward` key is always forwards. Plain names stay implies. + std::vector localImplies; + for (auto& tok : implied) { + if (auto fwd = mcpp::pm::split_feature_forward_token(tok)) + m.featureForwards[fname].push_back(std::move(*fwd)); + else + localImplies.push_back(std::move(tok)); + } + for (auto& tok : forwardTokens) + if (auto fwd = mcpp::pm::split_feature_forward_token(tok)) + m.featureForwards[fname].push_back(std::move(*fwd)); + m.featuresMap[fname] = std::move(localImplies); } } diff --git a/src/manifest/types.cppm b/src/manifest/types.cppm index 19ac18fc..3c6e47b6 100644 --- a/src/manifest/types.cppm +++ b/src/manifest/types.cppm @@ -384,6 +384,18 @@ struct Manifest { // Each value is a full DependencySpec, so a feature-dep may itself request // features. See .agents/docs/2026-06-29-feature-optional-dependencies-s2-design.md. std::map> featureDeps; + // Feature System v2 #243 — dep/feat forwarding (Cargo parity). When this + // package's feature is active, each is injected + // into the request set for dependency , so a feature can open + // a feature OF a dependency (e.g. an opencv module package's `dnn` feature + // forwarding `compat.opencv/dnn`). shares the keyspace of + // `featuresMap`'s siblings `dependencies` / `featureDeps` (the raw selector + // string == resolve_dependency_selector(...).stableMapKey). Forwarding is + // additive: it only opens more of a dep's features, never pulls the dep in + // (that is featureDeps' job) and is unaffected by `default-features`. + // featureName → [(depStableKey, depFeature)]. + std::map>> + featureForwards; // Root-only: [capabilities] cap = "provider" pins (also fed by --cap). std::map capabilityPins; diff --git a/src/manifest/xpkg.cppm b/src/manifest/xpkg.cppm index 4eca32c3..8e55ce19 100644 --- a/src/manifest/xpkg.cppm +++ b/src/manifest/xpkg.cppm @@ -1189,6 +1189,21 @@ synthesize_from_xpkg_lua(std::string_view luaContent, cur.skip_ws_and_comments(); } cur.consume('}'); + // #243: partition `dep/feat` tokens collected under this + // feature's `implies` into featureForwards (Cargo parity). + // Mirrors the TOML surface — both grammars share the one split + // point split_feature_forward_token; the `deps` branch above is + // untouched (that is featureDeps, a different concern). + if (auto fmi = m.featuresMap.find(fname); fmi != m.featuresMap.end()) { + std::vector localImplies; + for (auto& tok : fmi->second) { + if (auto fwd = mcpp::pm::split_feature_forward_token(tok)) + m.featureForwards[fname].push_back(std::move(*fwd)); + else + localImplies.push_back(std::move(tok)); + } + fmi->second = std::move(localImplies); + } cur.skip_ws_and_comments(); } cur.consume('}'); diff --git a/src/pm/dependency_selector.cppm b/src/pm/dependency_selector.cppm index a6920a00..36b8091a 100644 --- a/src/pm/dependency_selector.cppm +++ b/src/pm/dependency_selector.cppm @@ -56,6 +56,25 @@ inline DependencySelector make_direct_dependency_selector( return out; } +// #243 Cargo dep/feat forwarding. Split a `[features]` implied-feature token: +// a token containing '/' means "when this feature is active, request +// from dependency " (Cargo `[features] F = ["dep/feat"]`). +// is returned verbatim so it lands in the same keyspace as the +// `dependencies` / `featureDeps` maps (both keyed by the raw selector string, +// == stableMapKey). Split on the FIRST '/' (feature names contain no '/'). +// A token with no '/', or with an empty dep/feature half, is a plain local +// implied feature → nullopt. +inline std::optional> +split_feature_forward_token(std::string_view token) +{ + auto slash = token.find('/'); + if (slash == std::string_view::npos) return std::nullopt; + auto depKey = token.substr(0, slash); + auto depFeat = token.substr(slash + 1); + if (depKey.empty() || depFeat.empty()) return std::nullopt; + return std::pair{std::string(depKey), std::string(depFeat)}; +} + inline DependencySelector resolve_dependency_selector( std::string_view selector, DependencySelectorMode) diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 68236c97..3a3e6d14 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -500,6 +500,102 @@ package = { EXPECT_TRUE(m->dependencies.empty()); } +// Feature System v2 #243: dep/feat forwarding (Cargo parity). A `[features]` +// implied-feature token containing '/' means "when this feature is active, +// request from dependency ". It is split out of the +// implies list into featureForwards; plain names stay implies. +TEST(Manifest, FeatureForwardTomlArrayAndImplies) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[targets.x] +kind = "lib" +[features] +default = [] +dnn = ["dnn-local", "compat.opencv/dnn"] +[dependencies] +compat.opencv = { version = "0.0.3", default-features = false } +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + + // The plain name stays an implied feature. + ASSERT_TRUE(m->featuresMap.contains("dnn")); + ASSERT_EQ(m->featuresMap["dnn"].size(), 1u); + EXPECT_EQ(m->featuresMap["dnn"][0], "dnn-local"); + + // The `dep/feat` token becomes a forward (depKey verbatim = stableMapKey). + ASSERT_TRUE(m->featureForwards.contains("dnn")); + ASSERT_EQ(m->featureForwards["dnn"].size(), 1u); + EXPECT_EQ(m->featureForwards["dnn"][0].first, "compat.opencv"); + EXPECT_EQ(m->featureForwards["dnn"][0].second, "dnn"); +} + +// The table form accepts forwards both mixed into `implies` and via the +// dedicated self-documenting `forward` key; both feed featureForwards. +TEST(Manifest, FeatureForwardTableForms) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[targets.x] +kind = "lib" +[features] +default = [] +a = { implies = ["base", "dep.one/x"] } +b = { forward = ["dep.two/y", "dep.two/z"] } +base = [] +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + + // `implies` keeps the plain name, forwards the slashed token. + ASSERT_EQ(m->featuresMap["a"].size(), 1u); + EXPECT_EQ(m->featuresMap["a"][0], "base"); + ASSERT_TRUE(m->featureForwards.contains("a")); + ASSERT_EQ(m->featureForwards["a"].size(), 1u); + EXPECT_EQ(m->featureForwards["a"][0].first, "dep.one"); + EXPECT_EQ(m->featureForwards["a"][0].second, "x"); + + // Dedicated `forward` key: all tokens are forwards; feature has no implies. + EXPECT_TRUE(m->featuresMap["b"].empty()); + ASSERT_TRUE(m->featureForwards.contains("b")); + ASSERT_EQ(m->featureForwards["b"].size(), 2u); + EXPECT_EQ(m->featureForwards["b"][0].first, "dep.two"); + EXPECT_EQ(m->featureForwards["b"][0].second, "y"); + EXPECT_EQ(m->featureForwards["b"][1].second, "z"); +} + +// The Lua descriptor surface splits `dep/feat` tokens the same way (one shared +// split point, two grammars). +TEST(SynthesizeFromXpkgLua, FeatureForwardParse) { + constexpr auto lua = R"( +package = { + spec = "1", + name = "opencv", + xpm = { linux = { ["0.0.3"] = { url = "u", sha256 = "h" } } }, + mcpp = { + sources = { "*/anchor.c" }, + targets = { ["opencv"] = { kind = "lib" } }, + features = { + ["dnn"] = { sources = { "*/dnn.cppm" }, implies = { "compat.opencv/dnn" } }, + }, + }, +} +)"; + auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "opencv", "0.0.3"); + ASSERT_TRUE(m.has_value()) << m.error().format(); + // The forward token left featuresMap for featureForwards. + EXPECT_TRUE(m->featuresMap["dnn"].empty()); + ASSERT_TRUE(m->featureForwards.contains("dnn")); + ASSERT_EQ(m->featureForwards["dnn"].size(), 1u); + EXPECT_EQ(m->featureForwards["dnn"][0].first, "compat.opencv"); + EXPECT_EQ(m->featureForwards["dnn"][0].second, "dnn"); + // sources still gated normally (untouched by the split). + ASSERT_TRUE(m->buildConfig.featureSources.contains("dnn")); +} + // mcpp#237: an unknown mcpp-segment key is collected (so the build path can // surface it) and the did-you-mean helper maps well-known confusables + // edit-distance typos back to the closed key vocabulary. From 698fefe42b6605b21a221ac0c4fcb71466f7eb81 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 19 Jul 2026 08:13:32 +0800 Subject: [PATCH 3/6] feat(build): #243 inject feature forwarding into the dependency-edge funnel When a resolved package's feature is active, inject the features it forwards to each child into that child's request BEFORE it is pushed onto the worklist. One injection covers both consumption points: resolution (mergeActiveFeatureDeps folds the forwarded feature's conditional deps) and activation (recordDependencyEdge -> aggregatedRequest -> apply emits its defines/sources). Transitive forwarding rides the BFS forward edge (root->mid->leaf); additive with #242 default-features=false. A forward to an undeclared dep is a strict error / non-strict warning; an undefined dep feature reuses the existing does-not-declare gate. e2e 128 covers the transitive tree on both mcpp build and mcpp test. --- src/build/prepare.cppm | 95 +++++++++++++++---- tests/e2e/128_feature_forwarding.sh | 139 ++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 17 deletions(-) create mode 100755 tests/e2e/128_feature_forwarding.sh diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index e647fbc2..7636a692 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -2154,31 +2154,79 @@ prepare_build(bool print_fingerprint, } }; + // #243: dep/feat forwarding. When a resolved package's feature F is active, + // it may forward features to its dependencies (Cargo `[features] F = + // ["dep/feat"]`). Injecting the forwarded feature into the child's request + // BEFORE the child is pushed onto the worklist makes BOTH consumption points + // observe it: resolution (mergeActiveFeatureDeps reads the child's + // spec.features) and activation (recordDependencyEdge stores spec.features on + // the P->D edge, which aggregatedRequest unions and apply() activates). + // Transitive forwarding rides the BFS forward edge (root -> mid -> leaf). + auto injectForwards = [](const mcpp::manifest::Manifest& parent, + const std::vector& parentActive, + const std::string& childKey, + mcpp::manifest::DependencySpec& childSpec) { + if (parent.featureForwards.empty()) return; + for (auto const& f : parentActive) { + auto it = parent.featureForwards.find(f); + if (it == parent.featureForwards.end()) continue; + for (auto const& [depKey, depFeat] : it->second) { + if (depKey != childKey) continue; + if (std::find(childSpec.features.begin(), childSpec.features.end(), + depFeat) == childSpec.features.end()) + childSpec.features.push_back(depFeat); + } + } + }; + // #243: a forward whose active feature targets a dependency that is not + // declared (in [dependencies], [dev-dependencies], or an active + // [feature-deps] already folded into `dependencies`) is a manifest bug — + // name it instead of silently dropping. Only active features' forwards are + // checked (lazy, like the unknown-requested-feature gate at ~2875). + auto validateForwards = [&](const mcpp::manifest::Manifest& parent, + const std::vector& parentActive, + std::string_view parentName) + -> std::expected { + for (auto const& f : parentActive) { + auto it = parent.featureForwards.find(f); + if (it == parent.featureForwards.end()) continue; + for (auto const& [depKey, depFeat] : it->second) { + if (parent.dependencies.contains(depKey) + || parent.devDependencies.contains(depKey)) continue; + auto msg = std::format( + "feature '{}' of '{}' forwards to dependency '{}' (as " + "'{}/{}') which is not declared in [dependencies] or " + "[feature-deps]", f, parentName, depKey, depKey, depFeat); + if (overrides.strict) return std::unexpected(msg); + std::println(stderr, "warning: {}", msg); + } + } + return {}; + }; + // Pull the root package's active feature-deps into its dependency set before // seeding, so `mcpp build --features X` resolves X's optional deps. - { - std::vector rootReq; - for (std::size_t p = 0; p < overrides.features.size();) { - auto c = overrides.features.find_first_of(", ", p); - auto tok = overrides.features.substr( - p, c == std::string::npos ? std::string::npos : c - p); - if (!tok.empty()) rootReq.push_back(tok); - if (c == std::string::npos) break; - p = c + 1; - } - mergeActiveFeatureDeps(*m, rootReq); - } + std::vector rootReq = parse_feature_request(overrides.features); + mergeActiveFeatureDeps(*m, rootReq); + // #243: the root's active features may forward features to its direct deps. + std::vector rootActive = feature_closure(*m, rootReq, true); + if (auto fe = validateForwards(*m, rootActive, m->package.name); !fe) + return std::unexpected(fe.error()); // Seed the worklist from the main manifest. Dev-deps only when the // caller wants them; they're never propagated transitively. const std::string mainPkgLabel = m->package.name; for (auto& [n, s] : m->dependencies) { - worklist.push_back({n, s, mainPkgLabel, s.version, kMainConsumer, {}}); + auto req = s; + injectForwards(*m, rootActive, n, req); + worklist.push_back({n, req, mainPkgLabel, req.version, kMainConsumer, {}}); } if (includeDevDeps) { for (auto& [n, s] : m->devDependencies) { - worklist.push_back({n, s, mainPkgLabel + " (dev-dep)", - s.version, kMainConsumer, {}}); + auto req = s; + injectForwards(*m, rootActive, n, req); + worklist.push_back({n, req, mainPkgLabel + " (dev-dep)", + req.version, kMainConsumer, {}}); } } @@ -2683,9 +2731,22 @@ prepare_build(bool print_fingerprint, key.shortName, sourceKind == "version" ? spec.version : sourceKind); const std::size_t selfIdx = dep_manifests.size() - 1; + // #243: forward this dep's active features to ITS children before they + // are pushed (transitive dep->dep forwarding rides the BFS forward + // edge). Uses the SAME closure inputs as mergeActiveFeatureDeps above + // (this edge's spec.features, already carrying any forward injected by + // this dep's own consumer, + defaultFeatures), so activation agrees + // with resolution. + auto depActive = feature_closure(*dep_manifests.back(), spec.features, + spec.defaultFeatures); + if (auto fe = validateForwards(*dep_manifests.back(), depActive, + dep_manifests.back()->package.name); !fe) + return std::unexpected(fe.error()); for (auto& [child_name, child_spec] : dep_manifests.back()->dependencies) { - worklist.push_back({child_name, child_spec, thisDepLabel, - child_spec.version, selfIdx, dep_root}); + auto childReq = child_spec; + injectForwards(*dep_manifests.back(), depActive, child_name, childReq); + worklist.push_back({child_name, childReq, thisDepLabel, + childReq.version, selfIdx, dep_root}); } } diff --git a/tests/e2e/128_feature_forwarding.sh b/tests/e2e/128_feature_forwarding.sh new file mode 100755 index 00000000..1bdeb773 --- /dev/null +++ b/tests/e2e/128_feature_forwarding.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# 128_feature_forwarding.sh — feature dep/feat forwarding (#243, Cargo parity). +# A package feature may open a feature OF a dependency: +# +# [features] +# withextra = ["leaf/extra"] # when `withextra` is active, request +# # `extra` from dependency `leaf` +# +# This is what unblocks a module package's optional module interface (the +# opencv.dnn case): a single feature both pulls a source set locally AND opens +# the heavy feature of the compat dependency, without forcing every consumer to +# pay for it. +# +# Graph: app -> mid -> leaf. `leaf` has a feature `extra` (NOT default) that +# carries a package-owned define LEAF_EXTRA. `mid` forwards `leaf/extra` behind +# its own `withextra` feature and pulls leaf with default-features = false (so +# leaf's default set never brings `extra` on its own). The forward must: +# * activate leaf's `extra` ONLY when the consumer turns on mid/withextra, +# * travel TRANSITIVELY (root app -> mid -> leaf), +# * hold on BOTH `mcpp build` and `mcpp test` (the 0.0.97 double-path +# invariant — activation must not diverge between the two). +# +# No `requires:` capability → runs on all three CI platforms. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +# ── leaf: a lib whose non-default feature `extra` carries a define ─────────── +mkdir -p leaf/include/leaf leaf/src +cat > leaf/mcpp.toml <<'EOF' +[package] +name = "leaf" +version = "0.1.0" + +[features] +default = [] +extra = { defines = ["LEAF_EXTRA=1"] } + +[build] +include_dirs = ["include"] + +[targets.leaf] +kind = "lib" +EOF +cat > leaf/include/leaf/leaf.hpp <<'EOF' +#pragma once +inline int leaf_mode() { +#ifdef LEAF_EXTRA + return 1; +#else + return 0; +#endif +} +EOF +cat > leaf/src/leaf.cppm <<'EOF' +export module leaf; +export int leaf_anchor() { return 0; } +EOF + +# ── mid: depends on leaf (default-features = false), forwards leaf/extra ───── +mkdir -p mid/src +cat > mid/mcpp.toml <<'EOF' +[package] +name = "mid" +version = "0.1.0" + +[features] +default = [] +withextra = ["leaf/extra"] + +[dependencies] +leaf = { path = "../leaf", default-features = false } + +[targets.mid] +kind = "lib" +EOF +cat > mid/src/mid.cppm <<'EOF' +export module mid; +export int mid_anchor() { return 0; } +EOF + +# helper: does leaf's compile command carry the extra-feature define? +leaf_has_extra() { # $1 = compile_commands.json path + grep -q 'LEAF_EXTRA' "$1" +} + +# ── Case A: app turns on mid/withextra → forward opens leaf/extra ──────────── +mkdir -p appon/src +cat > appon/mcpp.toml <<'EOF' +[package] +name = "appon" +version = "0.1.0" + +[dependencies] +mid = { path = "../mid", features = ["withextra"] } +EOF +cat > appon/src/main.cpp <<'EOF' +int main() { return 0; } +EOF + +( cd appon + "$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL(A): build failed"; exit 1; } + leaf_has_extra compile_commands.json || { + echo "FAIL(A): leaf/extra was NOT activated by the forward"; cat compile_commands.json; exit 1; } + # double-path: the same forward must hold under `mcpp test`. + "$MCPP" test > t.log 2>&1 || { cat t.log; echo "FAIL(A): test build failed"; exit 1; } + leaf_has_extra compile_commands.json || { + echo "FAIL(A/test): forward did not hold on the test path"; cat compile_commands.json; exit 1; } +) + +# ── Case B: app does NOT turn on withextra → leaf/extra stays off ──────────── +mkdir -p appoff/src +cat > appoff/mcpp.toml <<'EOF' +[package] +name = "appoff" +version = "0.1.0" + +[dependencies] +mid = { path = "../mid" } +EOF +cat > appoff/src/main.cpp <<'EOF' +int main() { return 0; } +EOF + +( cd appoff + "$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL(B): build failed"; exit 1; } + if leaf_has_extra compile_commands.json; then + echo "FAIL(B): leaf/extra leaked though no consumer requested mid/withextra" + cat compile_commands.json; exit 1 + fi + "$MCPP" test > t.log 2>&1 || { cat t.log; echo "FAIL(B): test build failed"; exit 1; } + if leaf_has_extra compile_commands.json; then + echo "FAIL(B/test): leaf/extra leaked on the test path"; cat compile_commands.json; exit 1 + fi +) + +echo "OK" From 43b2a44c1967abfdf99754f6cf07cacb90c2b9cb Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 19 Jul 2026 08:13:32 +0800 Subject: [PATCH 4/6] fix(build.mcpp): name the compiled program .exe on Windows (#230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.mcpp compiled to build.mcpp.bin, which Windows cmd.exe (the capture_exec shell there) cannot launch — .bin is not in PATHEXT. Name it build.mcpp.exe on Windows (is_windows is constexpr; non-Windows keeps .bin, bytes unchanged). Surfaces once a workspace build.mcpp member is reached on Windows, after the scanner symlink-escape crash fix (df985df) stops masking it as exit 127. --- src/build/build_program.cppm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index 6a6a32f5..ae36c182 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -536,7 +536,14 @@ std::expected run_build_program( } fs::create_directories(outDir, ec); // creates bdir too - fs::path bin = bdir / "build.mcpp.bin"; + // #230: on Windows the capture_exec shell is cmd.exe, which can only launch + // a PE by an executable extension — a bare `.bin` is not in PATHEXT and + // fails to run. Name the compiled program `.exe` there; other platforms keep + // `.bin` (cosmetic — bdir is separate from the `.mcpp` source). Surfaces + // once a workspace build.mcpp member is reached on Windows, after the + // scanner symlink-escape crash fix (df985df) stops masking it as exit 127. + fs::path bin = bdir / (mcpp::platform::is_windows + ? "build.mcpp.exe" : "build.mcpp.bin"); // ── Compile build.mcpp with the host toolchain ────────────────────────── std::string std_flag = "-std=" + std::string(cppStandard.empty() ? "c++23" : cppStandard); From a571255ba04ad55f7f3612b0a6e76fb348bb9e9b Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 19 Jul 2026 08:13:32 +0800 Subject: [PATCH 5/6] chore(deps): vendor xlings 0.4.67 for the multi-index_repo install fix (#238) xlings 0.4.67 carries openxlings/xlings#374 (fix(xim): surface multi-repo install failures + best-effort catalog, cf9b60d5), so a workspace member with >=2 inherited index_repos installs uncached packages instead of failing silently at exit 1. Bump release/cross-build/e2e pins 0.4.62 -> 0.4.67. --- .github/workflows/ci-linux-e2e.yml | 2 +- .github/workflows/cross-build-test.yml | 12 ++++++++---- .github/workflows/release.yml | 18 +++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml index 7e823b74..3703eed3 100644 --- a/.github/workflows/ci-linux-e2e.yml +++ b/.github/workflows/ci-linux-e2e.yml @@ -158,7 +158,7 @@ jobs: - name: Bootstrap xlings + released mcpp run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.62 + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.67 export PATH="$HOME/.xlings/subos/current/bin:$PATH" xlings update xlings install mcpp -y -g diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml index 088bf8b6..f24d7f70 100644 --- a/.github/workflows/cross-build-test.yml +++ b/.github/workflows/cross-build-test.yml @@ -92,13 +92,17 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - # 0.4.62 (current) — kept in lock-step with the xlings the release - # is built/bundled against (release.yml). A past 0.4.61 "download 404 + # 0.4.67 (current) — kept in lock-step with the xlings the release + # is built/bundled against (release.yml). Bumped 0.4.62 -> 0.4.67 for + # mcpp#238: xlings 0.4.67 carries the multi-index_repo install fix + # (openxlings/xlings#374, commit cf9b60d5) so a workspace member with + # >=2 inherited index_repos installs uncached packages instead of + # failing silently. A past 0.4.61 "download 404 # for mcpp@" was NOT a version bug — the xlings-res/mcpp GitHub # release assets were uploaded in a broken state (records present, # blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX # half is handled by the marker-clear below. - XLINGS_VERSION: '0.4.62' + XLINGS_VERSION: '0.4.67' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" curl -fsSL -o "/tmp/${tarball}" \ @@ -211,7 +215,7 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.62' + XLINGS_VERSION: '0.4.67' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" curl -fsSL -o "/tmp/${tarball}" \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0db2650d..9d2a4886 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,7 @@ jobs: # Pin xlings to a known-good version. The upstream install # script always grabs `latest` (no version override), so we # download + self-install manually to avoid broken releases. - XLINGS_VERSION: '0.4.62' + XLINGS_VERSION: '0.4.67' run: | if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" @@ -278,10 +278,10 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" - - name: Bootstrap mcpp via xlings (latest 0.4.62) + - name: Bootstrap mcpp via xlings (latest 0.4.67) env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.62' + XLINGS_VERSION: '0.4.67' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" curl -fsSL -o "/tmp/${tarball}" \ @@ -336,13 +336,13 @@ jobs: exec "$(dirname "$0")/bin/mcpp" "$@" LAUNCHER chmod +x "$STAGING/$WRAPPER/mcpp" - # Bundle the aarch64 xlings (0.4.62) so install.sh consumers on aarch64 + # Bundle the aarch64 xlings (0.4.67) so install.sh consumers on aarch64 # get an aarch64 xlings, not the x86_64 bootstrap one. - XLA="xlings-0.4.62-linux-aarch64.tar.gz" + XLA="xlings-0.4.67-linux-aarch64.tar.gz" if curl -fsSL -o "/tmp/$XLA" \ - "https://github.com/openxlings/xlings/releases/download/v0.4.62/$XLA"; then + "https://github.com/openxlings/xlings/releases/download/v0.4.67/$XLA"; then tar -xzf "/tmp/$XLA" -C /tmp - XLBIN=$(find /tmp/xlings-0.4.62-linux-aarch64 -path '*/bin/xlings' -type f | head -1) + XLBIN=$(find /tmp/xlings-0.4.67-linux-aarch64 -path '*/bin/xlings' -type f | head -1) if [ -n "$XLBIN" ]; then mkdir -p "$STAGING/$WRAPPER/registry/bin" cp "$XLBIN" "$STAGING/$WRAPPER/registry/bin/xlings" @@ -417,7 +417,7 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.62' + XLINGS_VERSION: '0.4.67' run: | if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then WORK=$(mktemp -d) @@ -599,7 +599,7 @@ jobs: shell: bash env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.62' + XLINGS_VERSION: '0.4.67' run: | WORK=$(mktemp -d) zipfile="xlings-${XLINGS_VERSION}-windows-x86_64.zip" From a18b436304a6dbff991015d93c9f3ba2649b6351 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 19 Jul 2026 08:13:32 +0800 Subject: [PATCH 6/6] chore(release): mcpp 0.0.98 -> 0.0.99 (#243 #238 #230) --- CHANGELOG.md | 17 +++++++++++++++++ mcpp.toml | 2 +- src/toolchain/fingerprint.cppm | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19b7cc72..df7af40c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.99] — 2026-07-19 + +> #230–#243 批次收尾:#243 feature 转发落地(0.0.98 只出了设计)+ #238 根因修复随 xlings 0.4.67 vendored 入包 + #230 windows build.mcpp 次生面补齐。设计见 `.agents/docs/2026-07-19-v0.0.99-feature-forwarding-238-230-design.md`。 + +### 新增 + +- **feature 依赖 feature 转发 `dep/feat`**(#243,Cargo 平价):`[features]` 里含 `/` 的 token(或表格形专用 `forward = ["dep/feat"]` 键)表示"本包该 feature 激活时,顺带打开依赖 `dep` 的 `feat` feature"——一个 feature 既能本地拉源、又能开依赖的重档 feature,解阻塞模块包的可选模块接口(opencv-m 的 `import opencv.dnn;`:`dnn` 一档同时拉 `dnn.cppm` 源集并把 `compat.opencv` 以 `features=["dnn"]` 参与构建,而非对所有消费者全量 +309 TU)。收敛为**一数据模型 `featureForwards` 两文法**(TOML 与 xpkg 描述符共享唯一切分点 `split_feature_forward_token`);转发**注入 0.0.98 既有的 `aggregatedRequest` 依赖边漏斗**(在子依赖 push 进 worklist 前把转发 feature 并入其请求集),一处注入同时覆盖解析(`mergeActiveFeatureDeps` 拉被转发 feature 的条件依赖)与激活(边图 union → `apply()` 发宏/源集)两个消费点;沿 BFS 前向边天然**传递**(root→mid→leaf)。与 #242 `default-features = false` 加性组合(转发进显式请求集、不受默认门控影响),`mcpp build`/`mcpp test` 双路径一致。转发到未声明依赖 strict 报错 / 非 strict 告警;转发未声明的依赖 feature 复用既有 "does not declare requested feature" 门。单测 3 例、e2e 128(含双路径)。 + +### 修复 + +- **≥2 项目级 index_repo 时 `install_packages` 静默失败**(#238,**根因修复上游落地**):vendored xlings 由 0.4.62 升至 **0.4.67**,携 openxlings/xlings#374 的多仓安装修复(`fix(xim): surface multi-repo install failures + best-effort catalog`,commit `cf9b60d5`)。此前 workspace 根级 `[indices]` 继承(#224)× default 重定向(R6)组合会给每个成员播下 ≥2 个 `index_repos`,任一未缓存包安装裸 `exit 1` 无 error 事件;0.0.98 已在 mcpp 侧把它变成可操作诊断,0.0.99 随 bundle 带上真正的解析修复。发布/交叉构建/e2e 三处 workflow pin 同步 0.4.67。 +- **windows 下依赖/成员 `build.mcpp` 产物名缺 `.exe` 无法执行**(#230 次生面):`build.mcpp` 编出的宿主程序此前恒名 `build.mcpp.bin`;windows 的 `capture_exec` 走 cmd.exe,`.bin` 不在 PATHEXT 故无法按名启动 PE。现按平台取后缀(windows=`build.mcpp.exe`,其余保持 `.bin`,`is_windows` 为 constexpr,非 windows 字节不变)。此面在 0.0.96 的 scanner symlink-逃逸崩溃(`df985df`,裸 127 的真凶)修复后才会被 workspace 的 build-mcpp 成员在 windows 走到。 + +### 备注 + +- #230 主因(scanner glob 顺 `.mcpp/.xlings` symlink 逃逸进 vendored 索引、CJK 文件名触发 MSVC 窄串转换抛异常→`__fastfail`→裸 127)已于 0.0.96 根治并在 0.0.98/0.0.99 在库;`src/main.cpp` 顶层 catch 兜底(未捕获异常→exit 70,不再裸 127)。0.0.99 补齐 build.mcpp 次生面后,mcpp-index 的 workspace(windows)CI 从临时钉回的 0.0.94 升到 0.0.99 复验全绿即关闭。 + ## [0.0.98] — 2026-07-19 > #230–#243 批次(单 PR 统一发布,逐 commit):#233 对象路径消歧的两个后续缺口(#239/#240,解阻塞 mcpplibs #79 opencv 收录)+ #237/#241/#242 根因级实现 + #238 mcpp 侧诊断(根因在 openxlings/xlings#374)+ #243 设计。总账 + 架构评估见 `.agents/docs/2026-07-19-issues-230-243-batch-ledger-and-architecture-assessment.md`;各设计文档见 `.agents/docs/2026-07-19-*`。 diff --git a/mcpp.toml b/mcpp.toml index 6791fbc4..af082296 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.98" +version = "0.0.99" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 02660b3b..3196c3fd 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.98"; +inline constexpr std::string_view MCPP_VERSION = "0.0.99"; struct FingerprintInputs { Toolchain toolchain;