diff --git a/README.zh-CN.md b/README.zh-CN.md index 5c0fdb1f..e44c3ae7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,11 +8,11 @@ [](https://en.cppreference.com/w/cpp/23) [](https://en.cppreference.com/w/cpp/language/modules) [](LICENSE) +[](https://github.com/mcpp-community/mcpp/actions/workflows/ci.yml) | [文档](docs/zh/) · [快速开始](docs/zh/01-getting-started.md) · [mcpp.toml 指南](docs/zh/04-mcpp-toml.md) · [示例项目](docs/zh/03-examples.md) · [工具链管理](docs/zh/20-toolchains.md) | |:---:| | [包索引 mcpp-index](https://mcpplibs.github.io/mcpp-index/) · [模块化库 mcpplibs](https://github.com/mcpplibs) · [社区论坛](https://forum.d2learn.org/category/20) · [Issues](https://github.com/mcpp-community/mcpp/issues) · [Releases](https://github.com/mcpp-community/mcpp/releases) | -| [](https://github.com/mcpp-community/mcpp/actions/workflows/ci.yml) | | 支持的插件 · [mcpp-language-server(mcppls)](https://github.com/Sunrisepeak/mcpp-language-server) —— C++20/23 模块语言服务器,面向 VS Code、Zed、CLion、Neovim、AI Agent(MCP)与 CI |
diff --git a/src/build/plan.cppm b/src/build/plan.cppm
index 1e068831..329c912f 100644
--- a/src/build/plan.cppm
+++ b/src/build/plan.cppm
@@ -2247,6 +2247,9 @@ make_plan(const mcpp::manifest::Manifest& manifest,
for (std::size_t i = 1; i < packages.size(); ++i) {
auto const& p = packages[i];
auto qname = qualified_package_name(p.manifest);
+ // Host-only packages are built by their own sub-build. Clearing their
+ // source globs does not remove the declared shared targets.
+ if (p.buildTimeOnly) continue;
// A DISTRIBUTION PACKAGE's shared target is already built — that is what
// the package IS. Creating a link unit for it made ninja fail outright
// with `multiple rules generate bin/libmathkit.dll`: this loop declared
@@ -2870,9 +2873,11 @@ make_plan(const mcpp::manifest::Manifest& manifest,
}
}
}
+ // An artifact is an independent program of its package, even when
+ // that package also provides a shared image to other consumers.
for (auto const& cu : plan.compileUnits) {
if (!closure.contains(cu.packageName)) continue;
- if (sharedDepPackages.contains(cu.packageName)) continue;
+ if (cu.packageName != owner && sharedDepPackages.contains(cu.packageName)) continue;
if (mcpp::links_unconditionally(cu.kind)) lu.objects.push_back(cu.object);
}
if (!r.target.main.empty()) {
@@ -2920,13 +2925,20 @@ make_plan(const mcpp::manifest::Manifest& manifest,
}
for (auto const& cu : plan.compileUnits) {
if (!closure.contains(cu.packageName)) continue;
- if (sharedDepPackages.contains(cu.packageName)) continue;
+ if (cu.packageName != owner && sharedDepPackages.contains(cu.packageName)) continue;
if (!is_implementation_source(cu.kind)) continue;
if (lu.entryMain && cu.source == *lu.entryMain) continue;
if (entryFilesAcrossTargets.contains(cu.source)) continue;
lu.objects.push_back(cu.object);
}
- append_shared_deps_for_linked_objects(lu);
+ // Only this program's closure contributes shared links. Walking every
+ // compile unit also picked up consumers of its sibling shared image
+ // and made the artifact depend on that image instead of its own code.
+ for (auto i : seen)
+ if (i == r.packageIndex
+ || (!sharedDepPackages.contains(qualified_package_name(packages[i].manifest))
+ && !placedInImage.contains(qualified_package_name(packages[i].manifest))))
+ append_direct_shared_deps(lu, i);
// In a workspace plan the plan's own line pools the dependencies'
// flags and not a member's, so the program links with its closure's
// line, as a member's program does, in a group that places nothing.
@@ -3087,9 +3099,12 @@ make_plan(const mcpp::manifest::Manifest& manifest,
"'{}', which target '{}' of this build also produces",
t.name, owner, lu.output.generic_string(), other.targetName));
+ // A sibling shared target is another image of this member's
+ // objects, not a dependency of its executable. Only another
+ // package's shared target supplies its implementation by link.
for (auto const& cu : plan.compileUnits) {
if (!closure.contains(cu.packageName)) continue;
- if (sharedDepPackages.contains(cu.packageName)) continue;
+ if (cu.packageName != owner && sharedDepPackages.contains(cu.packageName)) continue;
if (mcpp::links_unconditionally(cu.kind)) lu.objects.push_back(cu.object);
}
if (!t.main.empty() && lu.kind != LinkUnit::StaticLibrary) {
@@ -3147,7 +3162,7 @@ make_plan(const mcpp::manifest::Manifest& manifest,
const bool entryDefinesMain = lu.entryMain && source_defines_main(*lu.entryMain);
for (auto const& cu : plan.compileUnits) {
if (!closure.contains(cu.packageName)) continue;
- if (sharedDepPackages.contains(cu.packageName)) continue;
+ if (cu.packageName != owner && sharedDepPackages.contains(cu.packageName)) continue;
if (!is_implementation_source(cu.kind)) continue;
if (lu.entryMain && cu.source == *lu.entryMain) continue;
if (entryFilesAcrossTargets.contains(cu.source)) continue;
@@ -3159,11 +3174,12 @@ make_plan(const mcpp::manifest::Manifest& manifest,
}
if (lu.kind != LinkUnit::StaticLibrary) {
const auto before = lu.implicitInputs.size();
- // The shared libraries the closure's packages link, and no
- // other member's.
+ // The member still links its declared shared dependencies
+ // even when it also produces a shared target of its own.
for (auto i : closureIdx)
- if (!sharedDepPackages.contains(qualified_package_name(packages[i].manifest))
- && !placedInImage.contains(qualified_package_name(packages[i].manifest)))
+ if (i == mi
+ || (!sharedDepPackages.contains(qualified_package_name(packages[i].manifest))
+ && !placedInImage.contains(qualified_package_name(packages[i].manifest))))
append_direct_shared_deps(lu, i);
// The graph-built shared libraries this unit loads are placed
// beside it.
diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp
index 74b147d3..36821e99 100644
--- a/src/build/prepare/features.cpp
+++ b/src/build/prepare/features.cpp
@@ -1319,6 +1319,7 @@ step6_host_module_registration(PrepareState& state) {
// its sources would turn that gap into an undefined
// reference a long way from here.
if (viaTarget[d] || !viaAny[d]) continue;
+ state.packages[d].buildTimeOnly = true;
auto& dm = state.packages[d].manifest;
dm.buildConfig.sources.clear();
dm.buildConfig.featureSources.clear();
diff --git a/src/modgraph/scanner.cppm b/src/modgraph/scanner.cppm
index 275f6863..5cad6fe9 100644
--- a/src/modgraph/scanner.cppm
+++ b/src/modgraph/scanner.cppm
@@ -148,6 +148,9 @@ struct PackageRoot {
UsageRequirements publicUsage;
UsageRequirements linkUsage;
bool usageResolved = false;
+ // Reached only through build-time edges: resolved for host tools/rules,
+ // but neither its sources nor its products belong to the target plan.
+ bool buildTimeOnly = false;
// A selected workspace member that the plan builds (workspace design
// 2026-09-29 §15), and the directory below `bin/` its products are placed
// in (empty: `bin/` itself). False on the plan's root and on every
diff --git a/tests/e2e/00_fixture_path_hygiene.sh b/tests/e2e/00_fixture_path_hygiene.sh
index 1795f2f5..d1351b08 100755
--- a/tests/e2e/00_fixture_path_hygiene.sh
+++ b/tests/e2e/00_fixture_path_hygiene.sh
@@ -25,6 +25,20 @@
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
+source "$HERE/_host_path.sh"
+
+# CI passes MCPP_HOME in native Windows spelling, not just Git Bash spelling.
+# Both must produce paths safe to interpolate into TOML basic strings.
+case "$(uname -s)" in
+ MINGW* | MSYS* | CYGWIN*)
+ [[ "$(host_path 'C:\Users\runner admin\.mcpp/registry')" == 'C:/Users/runner admin/.mcpp/registry' ]] || {
+ echo "FAIL: host_path did not normalize a native Windows absolute path"
+ exit 1
+ }
+ [[ "$(host_path 'C:/Users/runner admin/.mcpp/registry')" == 'C:/Users/runner admin/.mcpp/registry' ]] || exit 1
+ ;;
+esac
+[[ "$(host_path '../relative directory')" == '../relative directory' ]] || exit 1
# A TOML `path` key: either at the start of a line, or inside an inline table
# (`{ path = "..." }`). Anchoring this way is what keeps shell assignments such
diff --git a/tests/e2e/880_a_members_executable_keeps_its_own_implementation.sh b/tests/e2e/880_a_members_executable_keeps_its_own_implementation.sh
new file mode 100755
index 00000000..7e79fb25
--- /dev/null
+++ b/tests/e2e/880_a_members_executable_keeps_its_own_implementation.sh
@@ -0,0 +1,216 @@
+#!/usr/bin/env bash
+# requires:
+# 880 -- mcpp#760: a member with shared and bin targets builds an independent
+# executable from its own implementation, not from its sibling shared library.
+#
+# A module interface, a separate implementation unit and a static dependency
+# must all reach the executable. Another member still consumes the shared
+# target. Workspace selection, shipped artifacts and host tools keep this rule.
+set -e
+source "$(dirname "$0")/_host_path.sh"
+
+TMP=$(mktemp -d)
+trap 'rm -rf "$TMP"' EXIT
+fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; }
+MCPP="${MCPP:-mcpp}"
+
+# Share the installed tool payloads, but keep the build and package caches
+# private. The fixture has no registry package dependencies; the default host
+# toolchain is resolved through the existing registry, as in the E2E runner.
+REGISTRY_HOST=$(host_path "${MCPP_HOME:-$HOME/.mcpp}/registry")
+export MCPP_HOME="$TMP/mcpp-home"
+mkdir -p "$MCPP_HOME" "$TMP/ws"
+cat > "$MCPP_HOME/config.toml" <