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
Original file line number Diff line number Diff line change
@@ -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)。
Expand Down
220 changes: 220 additions & 0 deletions .agents/docs/2026-07-19-v0.0.99-feature-forwarding-238-230-design.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/workflows/ci-linux-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/cross-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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@<pin>" 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}" \
Expand Down Expand Up @@ -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}" \
Expand Down
18 changes: 9 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}" \
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
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.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-*`。
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.98"
version = "0.0.99"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
9 changes: 8 additions & 1 deletion src/build/build_program.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,14 @@ std::expected<void, std::string> 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);
Expand Down
95 changes: 78 additions & 17 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>& 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<std::string>& parentActive,
std::string_view parentName)
-> std::expected<void, std::string> {
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<std::string> 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<std::string> rootReq = parse_feature_request(overrides.features);
mergeActiveFeatureDeps(*m, rootReq);
// #243: the root's active features may forward features to its direct deps.
std::vector<std::string> 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, {}});
}
}

Expand Down Expand Up @@ -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});
}
}

Expand Down
21 changes: 20 additions & 1 deletion src/manifest/toml.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,18 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
};
for (auto& [fname, fval] : *features_table) {
std::vector<std::string> implied;
std::vector<std::string> 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<std::string> defs;
read_str_array(ft, "defines", defs);
if (!defs.empty()) m.buildConfig.featureDefines[fname] = std::move(defs);
Expand All @@ -232,7 +238,20 @@ std::expected<Manifest, ManifestError> 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<std::string> 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);
}
}

Expand Down
Loading
Loading