diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 196a0f85..fac22859 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -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 @@ -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 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 closure = {dep.packageIndex}; + std::vector 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 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(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, diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index ad2f93a1..afdea5a7 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -422,14 +422,12 @@ static std::expected step13_link_forms(PrepareState& state, B } static std::expected 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); diff --git a/tests/e2e/880_shared_images_use_their_packages_link_configuration.sh b/tests/e2e/880_shared_images_use_their_packages_link_configuration.sh new file mode 100755 index 00000000..fc53fe3c --- /dev/null +++ b/tests/e2e/880_shared_images_use_their_packages_link_configuration.sh @@ -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 +#include +int helper_value(void) { + DWORD length = 0; + GetProfilesDirectoryW(NULL, &length); + return length != 0; +} +#else +#include +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 +#include +__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 +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" < "$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" diff --git a/tests/unit/test_shared_link_usage.cpp b/tests/unit/test_shared_link_usage.cpp new file mode 100644 index 00000000..cc6749cc --- /dev/null +++ b/tests/unit/test_shared_link_usage.cpp @@ -0,0 +1,96 @@ +#include + +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 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(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{owner.root / "owner-lib"}); + EXPECT_EQ(group.runtimeLibraryDirs, + std::vector{owner.root / "owner-runtime"}); + EXPECT_EQ(group.linkIntent.libraries, std::vector{"helper-runtime-lib"}); + EXPECT_EQ(group.runtimeDlopenLibs, std::vector{"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; + } + } + } +}