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
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue.svg)](https://en.cppreference.com/w/cpp/23)
[![Module](https://img.shields.io/badge/module-ok-green.svg)](https://en.cppreference.com/w/cpp/language/modules)
[![License](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](LICENSE)
[![ci](https://github.com/mcpp-community/mcpp/actions/workflows/ci.yml/badge.svg?branch=main)](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) |
| [![ci](https://github.com/mcpp-community/mcpp/actions/workflows/ci.yml/badge.svg?branch=main)](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 |

<p align="center">
Expand Down
34 changes: 25 additions & 9 deletions src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/build/prepare/features.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions src/modgraph/scanner.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions tests/e2e/00_fixture_path_hygiene.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
216 changes: 216 additions & 0 deletions tests/e2e/880_a_members_executable_keeps_its_own_implementation.sh
Original file line number Diff line number Diff line change
@@ -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" <<EOF
[xlings]
home = "$REGISTRY_HOST"
EOF

EXE=""
case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) EXE=".exe" ;; esac
bin_of() { find target -path '*/bin/*' -name "$1$EXE" -type f | head -1; }

cd "$TMP/ws"
cat > mcpp.toml <<'EOF'
[workspace]
members = ["dual", "client", "support"]
EOF
mkdir -p common/src dual/src client/src support/src
cat > support/mcpp.toml <<'EOF'
[package]
name = "support"
version = "0.1.0"

[targets.support]
kind = "shared"
EOF
printf 'export module t880_support;\nexport int delta() { return 1; }\n' > support/src/support.cppm
cat > common/mcpp.toml <<'EOF'
[package]
name = "common"
version = "0.1.0"

[targets.common]
kind = "lib"
EOF
printf 'export module t880_base;\nexport int base_value() { return 41; }\n' > common/src/base.cppm
cat > dual/mcpp.toml <<'EOF'
[package]
name = "dual"
version = "0.1.0"

[dependencies]
common = { path = "../common" }
support = { path = "../support" }

[targets.dual_dll]
kind = "shared"

[targets.dual]
kind = "bin"
main = "src/main.cpp"
EOF
printf 'export module t880_dual;\nexport int interface_value() { return 1; }\nexport int answer();\n' > dual/src/dual.cppm
printf 'module t880_dual;\nimport t880_base;\nimport t880_support;\nint answer() { return base_value() + delta(); }\n' > dual/src/impl.cpp
printf 'import t880_dual;\nint main() { return answer() == 42 && interface_value() == 1 ? 0 : 1; }\n' > dual/src/main.cpp
cat > client/mcpp.toml <<'EOF'
[package]
name = "client"
version = "0.1.0"

[dependencies]
dual = { path = "../dual" }

[targets.client]
kind = "bin"
main = "src/main.cpp"
EOF
cp dual/src/main.cpp client/src/main.cpp

"$MCPP" build --workspace > workspace.log 2>&1 || fail "the workspace did not build" workspace.log
dual=$(bin_of dual)
client=$(bin_of client)
[ -n "$dual" ] && [ -n "$client" ] || fail "a member's executable is missing" workspace.log
"$dual" || fail "the owner's executable did not run"
"$client" || fail "the shared-library consumer did not run"

# Remove every placement of the sibling library. A client that imports it must
# fail, while the owner's executable must still run: mere success with both
# products beside it would also accept the import-library workaround for #760.
mkdir hidden
n=0
while IFS= read -r library; do
n=$((n + 1))
mv "$library" "hidden/$n"
done < <(find target -path '*/bin/*' \( -name '*dual_dll.dll' -o -name '*dual_dll.so*' -o -name '*dual_dll.dylib' \))
[ "$n" -gt 0 ] || fail "the sibling shared library was not built" workspace.log
"$dual" || fail "the owner's executable depends on its sibling shared library"
if "$client" > client.log 2>&1; then
fail "the external consumer did not use the shared library" client.log
fi

"$MCPP" build -p dual > selected.log 2>&1 || fail "the selected member did not build" selected.log
"$(bin_of dual)" || fail "the selected member's executable did not run"

# Request the same executable as an artifact, rather than selecting its owner.
# The feature gate must be activated by the dependency that requests it.
cat >> dual/mcpp.toml <<'EOF'
required_features = ["exe"]

[features]
exe = []
EOF
mkdir -p shipped/src
cat > shipped/mcpp.toml <<'EOF'
[package]
name = "shipped"
version = "0.1.0"

[dependencies]
dual = { path = "../dual", artifacts = ["dual"], features = ["exe"] }
support = { path = "../support" }

[targets.shipped]
kind = "bin"
main = "src/main.cpp"
EOF
printf 'import t880_support;\nint main() { return delta() == 1 ? 0 : 1; }\n' > shipped/src/main.cpp
cat > mcpp.toml <<'EOF'
[workspace]
members = ["dual", "client", "support", "shipped"]
EOF
"$MCPP" build -p shipped > artifact.log 2>&1 || fail "the requested artifact did not build" artifact.log
artifact=$(find target -path "*/bin/shipped/dual$EXE" -type f | head -1)
[ -n "$artifact" ] || fail "the requested artifact was not placed beside its consumer" artifact.log
"$artifact" || fail "the requested artifact did not run"
"$(bin_of shipped)" || fail "the artifact's consumer did not run"
"$MCPP" build --workspace > artifact-workspace.log 2>&1 || fail "the workspace with an artifact request did not build" artifact-workspace.log
"$artifact" || fail "the workspace's requested artifact did not run"
"$dual" || fail "the workspace's owner executable did not run"
n=0
while IFS= read -r library; do
n=$((n + 1))
mv "$library" "hidden/artifact-$n"
done < <(find target -path '*/bin/*' \( -name '*dual_dll.dll' -o -name '*dual_dll.so*' -o -name '*dual_dll.dylib' \))
"$artifact" || fail "the requested artifact depends on its sibling shared library"

# A host tool is rooted at its own package and published separately. Use a
# static dependency here: external shared-tool runtime deployment is a separate
# contract, while this test checks independence from the tool's own sibling.
mkdir -p "$TMP/tooldual/src" "$TMP/toolapp/src"
cp dual/src/dual.cppm "$TMP/tooldual/src/dual.cppm"
cp dual/src/main.cpp "$TMP/tooldual/src/main.cpp"
printf 'module t880_dual;\nimport t880_base;\nint answer() { return base_value() + 1; }\n' > "$TMP/tooldual/src/impl.cpp"
cat > "$TMP/tooldual/mcpp.toml" <<'EOF'
[package]
name = "tooldual"
version = "0.1.0"

[dependencies]
common = { path = "../ws/common" }

[features]
exe = []

[targets.dual_dll]
kind = "shared"

[targets.dual]
kind = "bin"
main = "src/main.cpp"
required_features = ["exe"]
EOF
cat > "$TMP/toolapp/mcpp.toml" <<'EOF'
[package]
name = "toolapp"
version = "0.1.0"

[build-dependencies]
tooldual = { path = "../tooldual", tools = ["dual"], features = ["exe"] }
EOF
printf 'int main() { return 0; }\n' > "$TMP/toolapp/src/main.cpp"
cat > "$TMP/toolapp/build.mcpp" <<'EOF'
#include <cstdlib>
#include <string>
import mcpp;
int main() {
const char* tool = mcpp::dep_bin("tooldual", "dual");
if (!tool || !*tool) return 1;
return std::system((std::string("\"") + tool + "\"").c_str()) == 0 ? 0 : 1;
}
EOF
cd "$TMP/toolapp"
"$MCPP" build > tool.log 2>&1 || fail "the host tool did not build and run" tool.log
"$(bin_of toolapp)" || fail "the host tool's consumer did not run"
# A target-side edge must retain the same provider's shared product even when
# it is also requested as a build-time tool.
cat >> mcpp.toml <<'EOF'

[dependencies]
tooldual = { path = "../tooldual" }
EOF
cp "$TMP/tooldual/src/main.cpp" src/main.cpp
"$MCPP" build > dual-role.log 2>&1 || fail "the dual-role provider did not build" dual-role.log
"$(bin_of toolapp)" || fail "the dual-role provider's consumer did not run"
[ -n "$(find target -path '*/bin/*' \( -name '*dual_dll.dll' -o -name '*dual_dll.so*' -o -name '*dual_dll.dylib' \) -type f | head -1)" ] || fail "the dual-role provider's shared library is missing" dual-role.log

echo "PASS: 880_a_members_executable_keeps_its_own_implementation"
3 changes: 2 additions & 1 deletion tests/e2e/_host_path.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ host_path() {
# agree and Windows accepts `/` as a separator. Passing one to `cygpath -m`
# would silently ANCHOR it at the current directory — which is not where the
# manifest being written lives — so relative input is returned untouched.
# MCPP_HOME can arrive from CI as C:\Users\...; it is absolute too.
case "$1" in
/*) ;;
/* | [A-Za-z]:[\\/]*) ;;
*) printf '%s' "$1"; return 0 ;;
esac
case "$(uname -s)" in
Expand Down
Loading