Skip to content
Closed
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
96 changes: 67 additions & 29 deletions src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -2537,35 +2537,6 @@ make_plan(const mcpp::manifest::Manifest& manifest,
}
};

for (auto const& dep : sharedDepTargets) {
LinkUnit lu;
lu.targetName = dep.target.name;
lu.package = dep.packageName;
lu.kind = LinkUnit::SharedLibrary;
lu.dependencyOwned = true;
lu.output = dep.output;
lu.importLibrary = import_library_for(dep.target, naming);
if (msvcTarget && !dep.target.windowsAutoExport && !dep.target.exportPatterns.empty())
return std::unexpected(exports_without_discovery(dep.target, dep.packageName));
if (msvcTarget && dep.target.windowsAutoExport && !lu.importLibrary.empty())
lu.defFile = std::filesystem::path("bin") / (dep.target.name + ".def");
lu.soname = dep.target.soname;
lu.exportPatterns = dep.target.exportPatterns;
lu.runtimeAliases = runtime_aliases_for_target(dep.target, naming);
lu.loaderTagFlag = loader_tag_flag(lu.kind);
append_package_objects(lu, dep.packageName);
append_direct_shared_deps(lu, dep.packageIndex);
if (auto it = staticsByImagePackage.find(dep.packageIndex);
it != staticsByImagePackage.end()) {
for (auto staticIndex : it->second) {
append_package_objects(
lu, qualified_package_name(packages[staticIndex].manifest));
append_direct_shared_deps(lu, staticIndex);
}
}
plan.linkUnits.push_back(std::move(lu));
}

// THE PROGRAMS A CONSUMER SHIPS FROM ITS DEPENDENCIES (mcpp#711).
//
// An edge `x = { ..., artifacts = ["updater"] }` asks for the dependency's
Expand Down Expand Up @@ -2615,6 +2586,73 @@ make_plan(const mcpp::manifest::Manifest& manifest,
}
}
}
// Shared images are created before workspace members, but their link
// configuration belongs to their owning package. Multiple DLLs in one
// package share a link-only group without scheduling duplicate deployment.
std::map<std::size_t, int> sharedLinkGroups;
for (auto const& dep : sharedDepTargets) {
LinkUnit lu;
lu.targetName = dep.target.name;
lu.package = dep.packageName;
lu.kind = LinkUnit::SharedLibrary;
lu.dependencyOwned = true;
lu.output = dep.output;
lu.importLibrary = import_library_for(dep.target, naming);
if (msvcTarget && !dep.target.windowsAutoExport && !dep.target.exportPatterns.empty())
return std::unexpected(exports_without_discovery(dep.target, dep.packageName));
if (msvcTarget && dep.target.windowsAutoExport && !lu.importLibrary.empty())
lu.defFile = std::filesystem::path("bin") / (dep.target.name + ".def");
lu.soname = dep.target.soname;
lu.exportPatterns = dep.target.exportPatterns;
lu.runtimeAliases = runtime_aliases_for_target(dep.target, naming);
lu.loaderTagFlag = loader_tag_flag(lu.kind);
if (!sharedLinkGroups.contains(dep.packageIndex)) {
std::set<std::size_t> closure = {dep.packageIndex};
std::vector<std::size_t> pending = {dep.packageIndex};
while (!pending.empty()) {
const auto i = pending.back();
pending.pop_back();
if (auto edges = directPackageDeps.find(i); edges != directPackageDeps.end()) {
for (auto j : edges->second) {
if (artifactEdges.contains({i, j}) || packages[j].buildTimeOnly) continue;
if (closure.insert(j).second) pending.push_back(j);
}
}
}
BuildPlan::LinkGroup group;
group.linkOnly = true;
group.productDir = lu.output.parent_path();
// A virtual root holds workspace configuration; an ordinary
// consumer's private link flags must not enter dependency DLLs.
if (manifest.package.virtualRoot) group.ldflags = packages[0].linkUsage.ldflags;
group.ldflags.insert(group.ldflags.end(), packages[dep.packageIndex].linkUsage.ldflags.begin(),
packages[dep.packageIndex].linkUsage.ldflags.end());
std::vector<mcpp::modgraph::PackageRoot> closurePackages;
closurePackages.push_back(packages[dep.packageIndex]);
for (auto i : closure) {
if (i == dep.packageIndex) continue;
group.ldflags.insert(group.ldflags.end(), packages[i].linkUsage.ldflags.begin(),
packages[i].linkUsage.ldflags.end());
closurePackages.push_back(packages[i]);
}
derive_runtime(closurePackages, group.productDir, group);
sharedLinkGroups[dep.packageIndex] = static_cast<int>(plan.linkGroups.size());
plan.linkGroups.push_back(std::move(group));
}
lu.linkGroup = sharedLinkGroups.at(dep.packageIndex);
append_package_objects(lu, dep.packageName);
append_direct_shared_deps(lu, dep.packageIndex);
if (auto it = staticsByImagePackage.find(dep.packageIndex);
it != staticsByImagePackage.end()) {
for (auto staticIndex : it->second) {
append_package_objects(
lu, qualified_package_name(packages[staticIndex].manifest));
append_direct_shared_deps(lu, staticIndex);
}
}
plan.linkUnits.push_back(std::move(lu));
}

// Reached through a non-artifact edge from the root (its dependencies,
// dev- and build-dependencies included), versus reached only through an
// artifact edge. Only the second set is withheld from the root's images,
Expand Down
14 changes: 6 additions & 8 deletions src/build/prepare/plan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -422,14 +422,12 @@ static std::expected<void, std::string> step13_link_forms(PrepareState& state, B
}

static std::expected<void, std::string> step13_make_plan(PrepareState& state, BuildContext& ctx) {
// A workspace plan links each member's closure with that closure's own
// flags (workspace design 2026-09-29 §15): each package's link flags,
// its build program's included, with search paths made absolute against
// the package, as the root's pooled list holds them outside a workspace.
if (state.workspacePlan())
for (std::size_t i = 1; i < state.packages.size(); ++i)
state.packages[i].linkUsage.ldflags = normalized_dependency_ldflags(
state.packages[i].root, state.packages[i].manifest.buildConfig.ldflags);
// Each image reads its owner's link snapshot, including DLLs from ordinary
// path dependencies. Refresh all dependency snapshots after build scripts
// and resolve search paths against their declaring package.
for (std::size_t i = 1; i < state.packages.size(); ++i)
state.packages[i].linkUsage.ldflags = normalized_dependency_ldflags(
state.packages[i].root, state.packages[i].manifest.buildConfig.ldflags);
auto planResult = mcpp::build::make_plan(*state.m, *state.tc, state.fp, state.scan.graph, state.report.topoOrder,
state.packages, *state.root, ctx.outputDir,
state.stdBmiPath, state.stdObjectPath, state.storeRoots);
Expand Down
116 changes: 116 additions & 0 deletions tests/e2e/880_shared_images_use_their_packages_link_configuration.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# requires: unix-shell
# DLL 的脚本库与静态依赖的链接选项必须在该 DLL 自己的链接边生效。
set -euo pipefail
MCPP="${MCPP:-mcpp}"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
fail() { echo "FAIL: $1"; cat "$2"; exit 1; }
cd "$TMP"
mkdir -p owner/src helper/src app/src standalone/src
cat > mcpp.toml <<'EOF'
[workspace]
members = ["owner", "app"]
EOF
cat > owner/mcpp.toml <<'EOF'
[package]
name = "owner"
version = "0.1.0"
[build]
sources = ["src/*.c"]
[dependencies]
helper = { path = "../helper" }
[targets.owner]
kind = "shared"
EOF
cat > owner/build.mcpp <<'EOF'
import std;
import mcpp;
int main() {
if (std::string_view(mcpp::target_os()) == "windows") {
mcpp::link_lib("bcrypt");
} else {
mcpp::link_lib("m");
}
}
EOF
cat > helper/mcpp.toml <<'EOF'
[package]
name = "helper"
version = "0.1.0"
[build]
sources = ["src/*.c"]
EOF
cat > helper/build.mcpp <<'EOF'
import std;
import mcpp;
int main() {
if (std::string_view(mcpp::target_os()) == "windows") {
mcpp::link_lib("userenv");
mcpp::link_lib("delayimp");
mcpp::link_flag("-Wl,/DELAYLOAD:userenv.dll");
} else {
mcpp::link_lib("m");
}
}
EOF
cat > helper/src/helper.c <<'EOF'
#ifdef _WIN32
#include <windows.h>
#include <userenv.h>
int helper_value(void) {
DWORD length = 0;
GetProfilesDirectoryW(NULL, &length);
return length != 0;
}
#else
#include <math.h>
int helper_value(void) { volatile double x = 0.0; return cos(x) == 1.0; }
#endif
EOF
cat > owner/src/owner.c <<'EOF'
int helper_value(void);
#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
__declspec(dllexport) int owner_value(void) {
unsigned char byte;
return BCryptGenRandom(NULL, &byte, 1, BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0
&& helper_value() ? 42 : 0;
}
#else
#include <math.h>
int owner_value(void) {
volatile double x = 0.0;
return sin(x) == 0.0 && helper_value() ? 42 : 0;
}
#endif
EOF
for name in app standalone; do
cat > "$name/mcpp.toml" <<EOF
[package]
name = "$name"
version = "0.1.0"
[dependencies]
owner = { path = "../owner" }
[targets.$name]
kind = "bin"
main = "src/main.c"
EOF
printf 'int owner_value(void);\nint main(void) { return owner_value() == 42 ? 0 : 1; }\n' > "$name/src/main.c"
done
for mode in workspace member; do
args=(--workspace)
[ "$mode" != member ] || args=(-p app)
"$MCPP" build "${args[@]}" > "$mode.log" 2>&1 || fail "$mode lost the DLL's link configuration" "$mode.log"
exe=$(find target -type f \( -name app -o -name app.exe \) -path '*/bin/*' | head -1)
[ -n "$exe" ] || fail "$mode built no app" "$mode.log"
"$exe" || fail "$mode app could not use its shared library" "$mode.log"
done
"$MCPP" build -p owner > owner.log 2>&1 || fail "selecting only the DLL owner lost its flags" owner.log
cd standalone
"$MCPP" build > standalone.log 2>&1 || fail "a path dependency lost its DLL link configuration" standalone.log
exe=$(find target -type f \( -name standalone -o -name standalone.exe \) -path '*/bin/*' | head -1)
[ -n "$exe" ] || fail "the ordinary path consumer built no program" standalone.log
"$exe" || fail "the ordinary path consumer could not call the DLL" standalone.log
echo "ok: shared images use their owners' and static dependencies' link configuration"
96 changes: 96 additions & 0 deletions tests/unit/test_shared_link_usage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#include <gtest/gtest.h>

import std;
import mcpp.build.plan;
import mcpp.build.ninja;
import mcpp.manifest;
import mcpp.modgraph.scanner;
import mcpp.toolchain.model;

namespace {

mcpp::modgraph::PackageRoot package(const std::filesystem::path& root,
std::string name, std::string flag) {
mcpp::modgraph::PackageRoot p;
p.root = root / name;
p.manifest.package.name = std::move(name);
p.manifest.package.version = "0.1.0";
p.linkUsage.ldflags = {std::move(flag)};
return p;
}

} // namespace

// DLL 的链接配置属于它的包,不能从消费者或其它工作区成员借来。
TEST(SharedLinkUsage, OwnerAndDependencyFlagsReachEverySharedImage) {
using mcpp::manifest::DependencySpec;
using mcpp::manifest::Target;
for (const bool workspace : {false, true}) {
for (const auto triple : {"x86_64-linux-gnu", "x86_64-pc-windows-msvc"}) {
const auto root = std::filesystem::temp_directory_path() / "mcpp-shared-link-usage";
auto app = package(root, "app", "-lconsumer_only");
auto owner = package(root, "owner", "-lowner_only");
auto helper = package(root, "helper", "-lhelper_only");
auto unrelated = package(root, "unrelated", "-lunrelated_only");
owner.manifest.runtimeConfig.linkIntent.linkLibraryDirs = {"owner-lib"};
owner.manifest.runtimeConfig.linkIntent.runtimeSearchDirs = {"owner-runtime"};
owner.manifest.runtimeConfig.requirements.push_back({.kind = "soname", .value = "owner-plugin"});
owner.manifest.runtimeConfig.artifacts.push_back({.role = "plugin", .path = "owner-plugin"});
owner.manifest.runtimeConfig.provides = {"owner-capability"};
helper.manifest.runtimeConfig.linkIntent.libraries = {"helper-runtime-lib"};
unrelated.manifest.runtimeConfig.linkIntent.linkLibraryDirs = {"unrelated-lib"};
app.manifest.package.virtualRoot = workspace;
if (workspace) app.linkUsage.ldflags.clear();
app.manifest.dependencies["owner"] = DependencySpec{.path = "../owner"};
app.manifest.dependencies["unrelated"] = DependencySpec{.path = "../unrelated"};
owner.manifest.dependencies["helper"] = DependencySpec{.path = "../helper"};
owner.manifest.targets.push_back({.name = "owner_a", .kind = Target::SharedLibrary});
owner.manifest.targets.push_back({.name = "owner_b", .kind = Target::SharedLibrary});
owner.selectedMember = workspace;
owner.memberProducts = "owner";
std::vector<mcpp::modgraph::PackageRoot> packages = {app, owner, helper, unrelated};
mcpp::toolchain::Toolchain tc;
tc.compiler = mcpp::toolchain::CompilerId::Clang;
tc.targetTriple = triple;
const auto plan = mcpp::build::make_plan(app.manifest, tc, {}, {}, {}, packages,
app.root, root / "target", {}, {});
ASSERT_TRUE(plan.has_value()) << plan.error();
std::size_t measured = 0;
for (const auto& unit : plan->linkUnits) {
if (unit.package != "owner") continue;
++measured;
ASSERT_GE(unit.linkGroup, 0) << unit.targetName << " " << triple;
const auto& group = plan->linkGroups.at(static_cast<std::size_t>(unit.linkGroup));
EXPECT_TRUE(group.linkOnly);
EXPECT_TRUE(group.placements.empty());
EXPECT_NE(std::ranges::find(group.ldflags, "-lowner_only"), group.ldflags.end());
EXPECT_NE(std::ranges::find(group.ldflags, "-lhelper_only"), group.ldflags.end());
EXPECT_EQ(std::ranges::find(group.ldflags, "-lconsumer_only"), group.ldflags.end());
EXPECT_EQ(std::ranges::find(group.ldflags, "-lunrelated_only"), group.ldflags.end());
EXPECT_EQ(group.linkIntent.linkLibraryDirs,
std::vector<std::filesystem::path>{owner.root / "owner-lib"});
EXPECT_EQ(group.runtimeLibraryDirs,
std::vector<std::filesystem::path>{owner.root / "owner-runtime"});
EXPECT_EQ(group.linkIntent.libraries, std::vector<std::string>{"helper-runtime-lib"});
EXPECT_EQ(group.runtimeDlopenLibs, std::vector<std::string>{"owner-plugin"});
ASSERT_EQ(group.runtimeArtifacts.size(), 1u);
EXPECT_EQ(group.runtimeArtifacts.front().path, owner.root / "owner-plugin");
ASSERT_EQ(group.runtimeProviders.size(), 1u);
EXPECT_EQ(group.runtimeProviders.front().capability, "owner-capability");
}
EXPECT_EQ(measured, 2u);
const auto ninja = mcpp::build::emit_ninja_string(*plan);
for (const auto& unit : plan->linkUnits) {
if (unit.package != "owner") continue;
const auto start = ninja.find("build " + unit.output.generic_string());
ASSERT_NE(start, std::string::npos);
const auto next = ninja.find("\nbuild ", start + 1);
const auto edge = ninja.substr(start, next - start);
EXPECT_NE(edge.find("-lowner_only"), std::string::npos) << edge;
EXPECT_NE(edge.find("-lhelper_only"), std::string::npos) << edge;
EXPECT_EQ(edge.find("-lconsumer_only"), std::string::npos) << edge;
EXPECT_EQ(edge.find("-lunrelated_only"), std::string::npos) << edge;
}
}
}
}
Loading