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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### 变更(命名统一,全部旧拼写永久兼容)
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.93"
version = "0.0.94"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
55 changes: 35 additions & 20 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> gated;
for (auto& [f, globs] : bc.featureSources)
for (auto& g : globs) gated.insert(g);
auto drop = [&](std::vector<std::string>& 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<std::string> gated;
for (auto& [f, globs] : bc.featureSources)
for (auto& g : globs) gated.insert(g);
auto drop = [&](std::vector<std::string>& v) {
std::erase_if(v, [&](const std::string& s) { return gated.contains(s); });
};
drop(bc.sources);
drop(pkg.manifest.modules.sources);
}
std::set<std::string> activeSet(active.begin(), active.end());
auto add = [](std::vector<std::string>& 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);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
97 changes: 97 additions & 0 deletions tests/e2e/100_feature_sources_test_mode.sh
Original file line number Diff line number Diff line change
@@ -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 <cJSON.h>
#include <cJSON_Utils.h>
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 <cJSON.h>
#include <cJSON_Utils.h>
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 <cJSON.h>
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"
Loading