From 75e2a9dda78daeb97499ce736295d1e60256c585 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 15 Jul 2026 22:29:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(build):=20feature=20sources=20=E5=9C=A8=20`?= =?UTF-8?q?mcpp=20test`=20=E4=B8=8B=E4=B8=8D=E7=BC=96=E8=AF=91=20(0.0.94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prepare_build() 把 feature 源集解析(drop + add)整段门在 `!includeDevDeps`, `mcpp test` 走 includeDevDeps=true → 激活 feature 的 sources 从不被加回构建图。 xpkg 的 `features.X.sources` 只落进 featureSources、从不进 base sources,故 「只在 features 下声明」的包 build 正常、test 必然 undefined reference。 命中 compat.cjson `utils`、compat.eigen `eigen_blas`(dgemm_)、compat.spdlog `compiled`。eigen_blas 的 dgemm_ 一直被记为「链进 test 二进制是 follow-up」, 定性是错的:不是链接问题,是源集解析问题。 drop 仍只在 build 模式做(test 模式需保留完整源面供 dev-dep 轨 per-test main 检测剪枝,见 e2e 79);add 改为两模式都做 + 去重(gtest 双列 glob 不会进两次)。 回归测试 e2e 100:已验证在未修复的 0.0.92 上以目标错误失败、修复后通过; e2e 78/79(#168 gtest 回归靶子)与单测 35/35 均通过。 --- CHANGELOG.md | 22 +++++ mcpp.toml | 2 +- src/build/prepare.cppm | 55 +++++++----- src/toolchain/fingerprint.cppm | 2 +- tests/e2e/100_feature_sources_test_mode.sh | 97 ++++++++++++++++++++++ 5 files changed, 156 insertions(+), 22 deletions(-) create mode 100755 tests/e2e/100_feature_sources_test_mode.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0e285..8f62185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,28 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.94] — 2026-07-15 + +### 修复 + +- **依赖包被激活 feature 的 `sources` 在 `mcpp test` 下不编译**。`prepare_build()` + 把 feature 源集解析(drop + add)**整段**门在 `!includeDevDeps`,而 `mcpp test` + 走 `includeDevDeps = true` → 激活 feature 的 sources **从不被加回**构建图。 + descriptor 若把某个 glob **只**写在 `features` 下(xpkg 的 `features.X.sources` + 只落进 `featureSources`、从不进 base `sources`),该包在 `mcpp build` 下正常、 + 在 `mcpp test` 下必然链接失败(`undefined reference`)。 + - 命中面:`compat.cjson` 的 `utils`(`cJSONUtils_*`)、`compat.eigen` 的 + `eigen_blas`(`dgemm_`)、`compat.spdlog` 的 `compiled`。 + - **`eigen_blas` 的 `dgemm_` 一直被记为「把 feature 编出的依赖目标链进 test + 二进制是 follow-up」——定性是错的**:不是链接问题,是**源集解析**问题。 + - 修复:**drop 仍只在 build 模式做**(`mcpp test` 需要保留完整源面,让 dev-dep + 轨的 per-test main 检测看得见 `gtest_main.cc` 并逐 test 剪枝——见 + `tests/e2e/79_gtest_regular_dep_feature_main.sh`);**add 改为两模式都做**, + 并**去重**,使 gtest 那种 base/feature 双列的 glob 不会进两次。 + - 回归测试:`tests/e2e/100_feature_sources_test_mode.sh`(cjson `utils` 在 + `mcpp test` 下必须编译并链接;`mcpp build` 仍正常;不请求 feature 时 gated + 源仍被排除)。 + ## [0.0.93] — 2026-07-15 ### 变更(命名统一,全部旧拼写永久兼容) diff --git a/mcpp.toml b/mcpp.toml index 52a51a5..13ff08f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.93" +version = "0.0.94" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 5e21c1b..00538df 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -2479,32 +2479,47 @@ prepare_build(bool print_fingerprint, } } // Feature-gated sources (e.g. gtest's gtest_main.cc behind "main"): - // drop EVERY feature-listed glob from the default build, then re-add - // only the ones whose feature is active. Runs even when no feature is - // active, so a gated source is excluded by default. + // drop EVERY feature-listed glob from the default build, then add + // back only the ones whose feature is active. Runs even when no + // feature is active, so a gated source is excluded by default. // - // ONLY in build mode (!includeDevDeps). `mcpp test` (includeDevDeps) - // keeps the full surface so the dev-dependency track's per-test main - // detection (run_tests / make_plan) still sees gtest_main.cc and - // prunes it per test — the two tracks stay decoupled. Combined with - // the descriptor keeping gtest_main.cc in base `sources` too, this - // means test mode is unaffected. + // The DROP is build-mode only (!includeDevDeps). `mcpp test` + // (includeDevDeps) keeps the full surface so the dev-dependency + // track's per-test main detection (run_tests / make_plan) still sees + // gtest_main.cc and prunes it per test — the two tracks stay + // decoupled; gtest's descriptor keeps gtest_main.cc in base `sources` + // too, so skipping the drop leaves it visible. + // + // The ADD runs in BOTH modes. A descriptor may list a glob ONLY under + // `features` and never in base `sources` (xpkg's `features.X.sources` + // lands in featureSources alone — compat.spdlog's `compiled`, + // compat.cjson's `utils`, compat.eigen's `eigen_blas`). Gating the add + // on !includeDevDeps meant those sources were never compiled under + // `mcpp test` → link-time `undefined reference` (the eigen_blas + // `dgemm_` failure, long misread as a linking follow-up: it was + // source-set resolution, not linking). Add is dedup'd so gtest's + // doubly-listed gtest_main.cc cannot land twice. auto& bc = pkg.manifest.buildConfig; - if (!includeDevDeps && !bc.featureSources.empty()) { - std::set gated; - for (auto& [f, globs] : bc.featureSources) - for (auto& g : globs) gated.insert(g); - auto drop = [&](std::vector& v) { - std::erase_if(v, [&](const std::string& s) { return gated.contains(s); }); - }; - drop(bc.sources); - drop(pkg.manifest.modules.sources); + if (!bc.featureSources.empty()) { + if (!includeDevDeps) { + std::set gated; + for (auto& [f, globs] : bc.featureSources) + for (auto& g : globs) gated.insert(g); + auto drop = [&](std::vector& v) { + std::erase_if(v, [&](const std::string& s) { return gated.contains(s); }); + }; + drop(bc.sources); + drop(pkg.manifest.modules.sources); + } std::set activeSet(active.begin(), active.end()); + auto add = [](std::vector& v, const std::string& g) { + if (std::ranges::find(v, g) == v.end()) v.push_back(g); + }; for (auto& [f, globs] : bc.featureSources) { if (!activeSet.contains(f)) continue; for (auto& g : globs) { - bc.sources.push_back(g); - pkg.manifest.modules.sources.push_back(g); + add(bc.sources, g); + add(pkg.manifest.modules.sources, g); } } } diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 9c947fd..44e594a 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.93"; +inline constexpr std::string_view MCPP_VERSION = "0.0.94"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/100_feature_sources_test_mode.sh b/tests/e2e/100_feature_sources_test_mode.sh new file mode 100755 index 0000000..223736d --- /dev/null +++ b/tests/e2e/100_feature_sources_test_mode.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# requires: +# 100_feature_sources_test_mode.sh — a dependency's feature-gated `sources` must +# be compiled under `mcpp test`, not just `mcpp build`. +# +# Regression: prepare_build() gated the whole feature-source resolution (drop + +# add) on `!includeDevDeps`, so `mcpp test` never added an active feature's +# sources. Descriptors that list a glob ONLY under `features` (xpkg's +# `features.X.sources` never lands in base `sources`) therefore compiled fine +# under `mcpp build` but failed to link under `mcpp test` with `undefined +# reference` — compat.cjson's `utils`, compat.spdlog's `compiled`, and +# compat.eigen's `eigen_blas` all hit this. The drop stays build-mode only (see +# 79_gtest_regular_dep_feature_main.sh); the add now runs in both modes. +# +# compat.cjson is the vehicle: plain C, small, and `utils` (cJSON_Utils.c) is +# feature-gated with no other dependency. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +"$MCPP" new app > /dev/null +cd app +rm -f src/main.cpp +mkdir -p tests + +cat > mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +cjson = { version = "1.7.19", features = ["utils"] } +EOF + +# Calls into BOTH the base sources (cJSON.c) and the feature-gated ones +# (cJSON_Utils.c) — a link failure on either fails the test. +cat > tests/t.cpp <<'EOF' +#include +#include +int main() { + cJSON* root = cJSON_Parse("{\"a\":1}"); + if (!root) return 1; + char* p = cJSONUtils_FindPointerFromObjectTo(root, root); + int ok = (p != 0); + cJSON_Delete(root); + return ok ? 0 : 1; +} +EOF + +# (1) THE REGRESSION: `mcpp test` must compile the feature's sources and link. +"$MCPP" test > /dev/null 2>&1 || { + echo "FAIL: feature-gated sources not compiled under \`mcpp test\`" + "$MCPP" test 2>&1 | tail -5 + exit 1 +} +nj=$(find target -name build.ninja | xargs ls -t 2>/dev/null | head -1) +grep -q 'cJSON_Utils' "$nj" || { + echo "FAIL: cJSON_Utils.c absent from the test-mode build graph"; exit 1 +} + +# (2) `mcpp build` keeps working (the path that was always correct). +cat > src/main.cpp <<'EOF' +#include +#include +int main() { + cJSON* root = cJSON_Parse("{\"a\":1}"); + char* p = cJSONUtils_FindPointerFromObjectTo(root, root); + (void)p; cJSON_Delete(root); return 0; +} +EOF +"$MCPP" build > /dev/null || { echo "FAIL: build with feature sources failed"; exit 1; } +nj=$(find target -name build.ninja | xargs ls -t 2>/dev/null | head -1) +grep -q 'cJSON_Utils' "$nj" || { echo "FAIL: cJSON_Utils.c absent from build graph"; exit 1; } + +# (3) The drop still works: without the feature, the gated source stays OUT of a +# `mcpp build` graph (guards against "add everything unconditionally"). +cat > mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +cjson = "1.7.19" +EOF +cat > src/main.cpp <<'EOF' +#include +int main() { cJSON* r = cJSON_Parse("{\"a\":1}"); cJSON_Delete(r); return 0; } +EOF +"$MCPP" build > /dev/null || { echo "FAIL: default (no feature) build failed"; exit 1; } +nj=$(find target -name build.ninja | xargs ls -t 2>/dev/null | head -1) +if grep -q 'cJSON_Utils' "$nj"; then + echo "FAIL: cJSON_Utils.c compiled without the \`utils\` feature"; exit 1 +fi + +echo "OK"