Skip to content

Commit e243856

Browse files
authored
fix(build): feature sources 在 mcpp test 下不编译 (0.0.94) (#218)
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 均通过。
1 parent e949ccf commit e243856

5 files changed

Lines changed: 156 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.94] — 2026-07-15
7+
8+
### 修复
9+
10+
- **依赖包被激活 feature 的 `sources``mcpp test` 下不编译**`prepare_build()`
11+
把 feature 源集解析(drop + add)**整段**门在 `!includeDevDeps`,而 `mcpp test`
12+
`includeDevDeps = true` → 激活 feature 的 sources **从不被加回**构建图。
13+
descriptor 若把某个 glob ****写在 `features` 下(xpkg 的 `features.X.sources`
14+
只落进 `featureSources`、从不进 base `sources`),该包在 `mcpp build` 下正常、
15+
`mcpp test` 下必然链接失败(`undefined reference`)。
16+
- 命中面:`compat.cjson``utils`(`cJSONUtils_*`)、`compat.eigen`
17+
`eigen_blas`(`dgemm_`)、`compat.spdlog``compiled`
18+
- **`eigen_blas``dgemm_` 一直被记为「把 feature 编出的依赖目标链进 test
19+
二进制是 follow-up」——定性是错的**:不是链接问题,是**源集解析**问题。
20+
- 修复:**drop 仍只在 build 模式做**(`mcpp test` 需要保留完整源面,让 dev-dep
21+
轨的 per-test main 检测看得见 `gtest_main.cc` 并逐 test 剪枝——见
22+
`tests/e2e/79_gtest_regular_dep_feature_main.sh`);**add 改为两模式都做**,
23+
**去重**,使 gtest 那种 base/feature 双列的 glob 不会进两次。
24+
- 回归测试:`tests/e2e/100_feature_sources_test_mode.sh`(cjson `utils`
25+
`mcpp test` 下必须编译并链接;`mcpp build` 仍正常;不请求 feature 时 gated
26+
源仍被排除)。
27+
628
## [0.0.93] — 2026-07-15
729

830
### 变更(命名统一,全部旧拼写永久兼容)

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 = "0.0.93"
3+
version = "0.0.94"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/prepare.cppm

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2479,32 +2479,47 @@ prepare_build(bool print_fingerprint,
24792479
}
24802480
}
24812481
// Feature-gated sources (e.g. gtest's gtest_main.cc behind "main"):
2482-
// drop EVERY feature-listed glob from the default build, then re-add
2483-
// only the ones whose feature is active. Runs even when no feature is
2484-
// active, so a gated source is excluded by default.
2482+
// drop EVERY feature-listed glob from the default build, then add
2483+
// back only the ones whose feature is active. Runs even when no
2484+
// feature is active, so a gated source is excluded by default.
24852485
//
2486-
// ONLY in build mode (!includeDevDeps). `mcpp test` (includeDevDeps)
2487-
// keeps the full surface so the dev-dependency track's per-test main
2488-
// detection (run_tests / make_plan) still sees gtest_main.cc and
2489-
// prunes it per test — the two tracks stay decoupled. Combined with
2490-
// the descriptor keeping gtest_main.cc in base `sources` too, this
2491-
// means test mode is unaffected.
2486+
// The DROP is build-mode only (!includeDevDeps). `mcpp test`
2487+
// (includeDevDeps) keeps the full surface so the dev-dependency
2488+
// track's per-test main detection (run_tests / make_plan) still sees
2489+
// gtest_main.cc and prunes it per test — the two tracks stay
2490+
// decoupled; gtest's descriptor keeps gtest_main.cc in base `sources`
2491+
// too, so skipping the drop leaves it visible.
2492+
//
2493+
// The ADD runs in BOTH modes. A descriptor may list a glob ONLY under
2494+
// `features` and never in base `sources` (xpkg's `features.X.sources`
2495+
// lands in featureSources alone — compat.spdlog's `compiled`,
2496+
// compat.cjson's `utils`, compat.eigen's `eigen_blas`). Gating the add
2497+
// on !includeDevDeps meant those sources were never compiled under
2498+
// `mcpp test` → link-time `undefined reference` (the eigen_blas
2499+
// `dgemm_` failure, long misread as a linking follow-up: it was
2500+
// source-set resolution, not linking). Add is dedup'd so gtest's
2501+
// doubly-listed gtest_main.cc cannot land twice.
24922502
auto& bc = pkg.manifest.buildConfig;
2493-
if (!includeDevDeps && !bc.featureSources.empty()) {
2494-
std::set<std::string> gated;
2495-
for (auto& [f, globs] : bc.featureSources)
2496-
for (auto& g : globs) gated.insert(g);
2497-
auto drop = [&](std::vector<std::string>& v) {
2498-
std::erase_if(v, [&](const std::string& s) { return gated.contains(s); });
2499-
};
2500-
drop(bc.sources);
2501-
drop(pkg.manifest.modules.sources);
2503+
if (!bc.featureSources.empty()) {
2504+
if (!includeDevDeps) {
2505+
std::set<std::string> gated;
2506+
for (auto& [f, globs] : bc.featureSources)
2507+
for (auto& g : globs) gated.insert(g);
2508+
auto drop = [&](std::vector<std::string>& v) {
2509+
std::erase_if(v, [&](const std::string& s) { return gated.contains(s); });
2510+
};
2511+
drop(bc.sources);
2512+
drop(pkg.manifest.modules.sources);
2513+
}
25022514
std::set<std::string> activeSet(active.begin(), active.end());
2515+
auto add = [](std::vector<std::string>& v, const std::string& g) {
2516+
if (std::ranges::find(v, g) == v.end()) v.push_back(g);
2517+
};
25032518
for (auto& [f, globs] : bc.featureSources) {
25042519
if (!activeSet.contains(f)) continue;
25052520
for (auto& g : globs) {
2506-
bc.sources.push_back(g);
2507-
pkg.manifest.modules.sources.push_back(g);
2521+
add(bc.sources, g);
2522+
add(pkg.manifest.modules.sources, g);
25082523
}
25092524
}
25102525
}

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 = "0.0.93";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.94";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env bash
2+
# requires:
3+
# 100_feature_sources_test_mode.sh — a dependency's feature-gated `sources` must
4+
# be compiled under `mcpp test`, not just `mcpp build`.
5+
#
6+
# Regression: prepare_build() gated the whole feature-source resolution (drop +
7+
# add) on `!includeDevDeps`, so `mcpp test` never added an active feature's
8+
# sources. Descriptors that list a glob ONLY under `features` (xpkg's
9+
# `features.X.sources` never lands in base `sources`) therefore compiled fine
10+
# under `mcpp build` but failed to link under `mcpp test` with `undefined
11+
# reference` — compat.cjson's `utils`, compat.spdlog's `compiled`, and
12+
# compat.eigen's `eigen_blas` all hit this. The drop stays build-mode only (see
13+
# 79_gtest_regular_dep_feature_main.sh); the add now runs in both modes.
14+
#
15+
# compat.cjson is the vehicle: plain C, small, and `utils` (cJSON_Utils.c) is
16+
# feature-gated with no other dependency.
17+
set -e
18+
19+
TMP=$(mktemp -d)
20+
trap "rm -rf $TMP" EXIT
21+
22+
cd "$TMP"
23+
"$MCPP" new app > /dev/null
24+
cd app
25+
rm -f src/main.cpp
26+
mkdir -p tests
27+
28+
cat > mcpp.toml <<'EOF'
29+
[package]
30+
name = "app"
31+
version = "0.1.0"
32+
33+
[dependencies]
34+
cjson = { version = "1.7.19", features = ["utils"] }
35+
EOF
36+
37+
# Calls into BOTH the base sources (cJSON.c) and the feature-gated ones
38+
# (cJSON_Utils.c) — a link failure on either fails the test.
39+
cat > tests/t.cpp <<'EOF'
40+
#include <cJSON.h>
41+
#include <cJSON_Utils.h>
42+
int main() {
43+
cJSON* root = cJSON_Parse("{\"a\":1}");
44+
if (!root) return 1;
45+
char* p = cJSONUtils_FindPointerFromObjectTo(root, root);
46+
int ok = (p != 0);
47+
cJSON_Delete(root);
48+
return ok ? 0 : 1;
49+
}
50+
EOF
51+
52+
# (1) THE REGRESSION: `mcpp test` must compile the feature's sources and link.
53+
"$MCPP" test > /dev/null 2>&1 || {
54+
echo "FAIL: feature-gated sources not compiled under \`mcpp test\`"
55+
"$MCPP" test 2>&1 | tail -5
56+
exit 1
57+
}
58+
nj=$(find target -name build.ninja | xargs ls -t 2>/dev/null | head -1)
59+
grep -q 'cJSON_Utils' "$nj" || {
60+
echo "FAIL: cJSON_Utils.c absent from the test-mode build graph"; exit 1
61+
}
62+
63+
# (2) `mcpp build` keeps working (the path that was always correct).
64+
cat > src/main.cpp <<'EOF'
65+
#include <cJSON.h>
66+
#include <cJSON_Utils.h>
67+
int main() {
68+
cJSON* root = cJSON_Parse("{\"a\":1}");
69+
char* p = cJSONUtils_FindPointerFromObjectTo(root, root);
70+
(void)p; cJSON_Delete(root); return 0;
71+
}
72+
EOF
73+
"$MCPP" build > /dev/null || { echo "FAIL: build with feature sources failed"; exit 1; }
74+
nj=$(find target -name build.ninja | xargs ls -t 2>/dev/null | head -1)
75+
grep -q 'cJSON_Utils' "$nj" || { echo "FAIL: cJSON_Utils.c absent from build graph"; exit 1; }
76+
77+
# (3) The drop still works: without the feature, the gated source stays OUT of a
78+
# `mcpp build` graph (guards against "add everything unconditionally").
79+
cat > mcpp.toml <<'EOF'
80+
[package]
81+
name = "app"
82+
version = "0.1.0"
83+
84+
[dependencies]
85+
cjson = "1.7.19"
86+
EOF
87+
cat > src/main.cpp <<'EOF'
88+
#include <cJSON.h>
89+
int main() { cJSON* r = cJSON_Parse("{\"a\":1}"); cJSON_Delete(r); return 0; }
90+
EOF
91+
"$MCPP" build > /dev/null || { echo "FAIL: default (no feature) build failed"; exit 1; }
92+
nj=$(find target -name build.ninja | xargs ls -t 2>/dev/null | head -1)
93+
if grep -q 'cJSON_Utils' "$nj"; then
94+
echo "FAIL: cJSON_Utils.c compiled without the \`utils\` feature"; exit 1
95+
fi
96+
97+
echo "OK"

0 commit comments

Comments
 (0)