install(TARGETS): Add option to install runtime deps built as other targets

Add a `RUNTIME_DEPENDENCY_TARGETS` option for `install(TARGETS)`
to automatically install the necessary runtime shared libraries.

Closes: #23505
This commit is contained in:
Stepanov Igor
2026-09-22 10:40:52 -04:00
committed by Brad King
parent 216cc54531
commit 53019cd2e6
9 changed files with 369 additions and 17 deletions
+34
View File
@@ -157,6 +157,7 @@ Signatures
install(TARGETS <target>... [EXPORT <export-name>]
[RUNTIME_DEPENDENCIES <arg>...|RUNTIME_DEPENDENCY_SET <set-name>]
[RUNTIME_DEPENDENCY_TARGETS]
[<artifact-option>...]
[<artifact-kind> <artifact-option>...]...
[INCLUDES DESTINATION [<dir> ...]]
@@ -477,6 +478,36 @@ Signatures
The ``RUNTIME_DEPENDENCIES`` and ``RUNTIME_DEPENDENCY_SET`` keywords are
mutually exclusive.
``RUNTIME_DEPENDENCY_TARGETS``
.. versionadded:: 4.5
This option installs runtime artifacts of non-imported ``SHARED``
library targets from the same build that appear on the computed
link lines of the listed ``<target>...`` and, recursively, of those
shared libraries. The ``RUNTIME``, ``LIBRARY``, and ``FRAMEWORK``
destinations and other artifact options from this call apply to
those artifacts (``RUNTIME`` on DLL platforms, ``LIBRARY``
otherwise, and ``FRAMEWORK`` for macOS frameworks).
Unlike ``RUNTIME_DEPENDENCIES``, this option does not call
:command:`file(GET_RUNTIME_DEPENDENCIES)` and does not install files
that are not CMake targets of the current build.
This option may not be used with ``EXPORT``. The call may specify
only runtime artifact groups (``RUNTIME``, ``LIBRARY``,
``FRAMEWORK``, and ``BUNDLE``).
.. note::
Static, interface, and object libraries on a listed target's
link line are traversed only to discover shared libraries; they
are not themselves installed by this option. Executables and
module libraries are not installed transitively. Imported
targets are skipped. Each :command:`install(TARGETS)` call with
this option installs the collected shared libraries into the
destinations of that call, even if another call already installs
the same target.
:ref:`Interface Libraries` may be listed among the targets to install.
They install no artifacts but will be included in an associated ``EXPORT``.
If :ref:`Object Libraries` are listed but given no destination for their
@@ -1180,6 +1211,9 @@ Signatures
Targets built within the build tree will never be installed as runtime
dependencies, nor will their own dependencies, unless the targets themselves
are installed with :command:`install(TARGETS)`.
Use the ``RUNTIME_DEPENDENCY_TARGETS`` option of
:command:`install(TARGETS)` to install runtime artifacts of
non-imported shared libraries from the same build.
The generated install script calls :command:`file(GET_RUNTIME_DEPENDENCIES)`
on the build-tree files to calculate the runtime dependencies. The build-tree
@@ -0,0 +1,10 @@
install-TARGETS-RUNTIME_DEPENDENCY_TARGETS
------------------------------------------
* The :command:`install(TARGETS)` command gained a
``RUNTIME_DEPENDENCY_TARGETS`` option to install runtime artifacts
of non-imported shared libraries from the same build that the named
targets link to. Unlike ``RUNTIME_DEPENDENCIES``, this uses CMake
target graph information rather than
:command:`file(GET_RUNTIME_DEPENDENCIES)`. The option cannot be
used with ``EXPORT`` and may only install runtime artifacts.
+210 -17
View File
@@ -19,12 +19,14 @@
#include "cmArgumentParser.h"
#include "cmArgumentParserTypes.h"
#include "cmCMakePath.h"
#include "cmComputeLinkInformation.h"
#include "cmDiagnosticContext.h"
#include "cmDiagnostics.h"
#include "cmExecutionStatus.h"
#include "cmExperimental.h"
#include "cmExportSet.h"
#include "cmGeneratorExpression.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmInstallAndroidMKExportGenerator.h"
#include "cmInstallCMakeConfigExportGenerator.h"
@@ -44,6 +46,7 @@
#include "cmInstallScriptGenerator.h"
#include "cmInstallTargetGenerator.h"
#include "cmList.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmPackageInfoArguments.h"
@@ -60,8 +63,16 @@
#include "cmUnreachable.h"
#include "cmValue.h"
class cmListFileBacktrace;
namespace {
enum class RuntimeOnly
{
No,
Yes,
};
struct InstallCategoryFlags
{
bool Archive = false;
@@ -121,6 +132,57 @@ struct InstallContext
}
};
void CollectEvaluatedTransitiveSharedModuleTargets(
cmLocalGenerator& lg, std::vector<std::string> const& rootNames,
std::set<cmTarget*>& out, std::string const& config)
{
std::set<cmGeneratorTarget const*> visited;
std::vector<cmGeneratorTarget const*> stack;
std::set<std::string> const rootNameSet(rootNames.begin(), rootNames.end());
auto consider = [&](cmGeneratorTarget const* dep) {
if (!dep || dep->IsImported()) {
return;
}
if (!visited.insert(dep).second) {
return;
}
cm::TargetType type = dep->GetType();
if (type == cm::TargetType::SHARED_LIBRARY) {
if (rootNameSet.find(dep->GetName()) == rootNameSet.end()) {
out.insert(dep->Target);
}
stack.push_back(dep);
}
};
auto enqueue = [&](cmGeneratorTarget const* current) {
cmComputeLinkInformation* linkInfo = current->GetLinkInformation(config);
std::set<cmGeneratorTarget const*> const& sharedTargets =
linkInfo->GetSharedLibrariesLinked();
for (cmGeneratorTarget const* dep : sharedTargets) {
consider(dep);
}
};
for (std::string const& name : rootNames) {
cmGeneratorTarget* root = lg.FindGeneratorTargetToUse(name);
if (!root) {
root = lg.GetGlobalGenerator()->FindGeneratorTarget(name);
}
if (!root) {
continue;
}
visited.insert(root);
enqueue(root);
}
while (!stack.empty()) {
cmGeneratorTarget const* current = stack.back();
stack.pop_back();
enqueue(current);
}
}
struct RuntimeDependenciesArgs
{
ArgumentParser::MaybeEmpty<std::vector<std::string>> Directories;
@@ -417,7 +479,8 @@ void RegisterInstallComponents(cmMakefile* makefile, InstallContext const& ctx,
bool CreateInstallGeneratorsForTarget(
Helper& helper, cmTarget& target, InstallContext const& ctx,
InstallCategoryFlags& flags, std::string& error,
cm::optional<std::vector<std::string>> configurations = {})
cm::optional<std::vector<std::string>> configurations = {},
RuntimeOnly runtimeOnly = RuntimeOnly::No)
{
cmInstallCommandArguments const& archiveArgs = *ctx.ArchiveArgs;
cmInstallCommandArguments const& libraryArgs = *ctx.LibraryArgs;
@@ -514,27 +577,32 @@ bool CreateInstallGeneratorsForTarget(
// cygwin. Currently no other platform is a DLL platform.
if (target.IsDLLPlatform()) {
// When in namelink only mode skip all libraries on Windows.
if (namelinkMode == cmInstallTargetGenerator::NamelinkModeOnly) {
if (runtimeOnly == RuntimeOnly::No &&
namelinkMode == cmInstallTargetGenerator::NamelinkModeOnly) {
namelinkOnly = true;
return addTargetExport();
}
// This is a DLL platform.
if (!archiveArgs.GetDestination().empty()) {
if (runtimeOnly == RuntimeOnly::No &&
!archiveArgs.GetDestination().empty()) {
// The import library uses the ARCHIVE properties.
archiveGenerator = CreateInstallTargetGenerator(
target, archiveArgs, true, helper.CaptureContext(), false, false,
configurations);
artifactsSpecified = true;
}
if (!runtimeArgs.GetDestination().empty()) {
if (runtimeOnly == RuntimeOnly::Yes ||
!runtimeArgs.GetDestination().empty()) {
// The DLL uses the RUNTIME properties.
runtimeGenerator = CreateInstallTargetGenerator(
target, runtimeArgs, false, helper.CaptureContext(), false, false,
target, runtimeArgs, false, helper.CaptureContext(),
helper.GetRuntimeDestination(&runtimeArgs), false, false,
configurations);
artifactsSpecified = true;
}
if (!archiveGenerator && !runtimeGenerator) {
if (runtimeOnly == RuntimeOnly::No && !archiveGenerator &&
!runtimeGenerator) {
archiveGenerator = CreateInstallTargetGenerator(
target, archiveArgs, true, helper.CaptureContext(),
helper.GetArchiveDestination(nullptr), false, false,
@@ -553,7 +621,8 @@ bool CreateInstallGeneratorsForTarget(
// INSTALL properties. Otherwise, use the LIBRARY properties.
if (target.IsFrameworkOnApple()) {
// When in namelink only mode skip frameworks.
if (namelinkMode == cmInstallTargetGenerator::NamelinkModeOnly) {
if (runtimeOnly == RuntimeOnly::No &&
namelinkMode == cmInstallTargetGenerator::NamelinkModeOnly) {
namelinkOnly = true;
return addTargetExport();
}
@@ -574,7 +643,8 @@ bool CreateInstallGeneratorsForTarget(
if (!libraryArgs.GetDestination().empty()) {
artifactsSpecified = true;
}
if (namelinkMode != cmInstallTargetGenerator::NamelinkModeOnly) {
if (runtimeOnly == RuntimeOnly::Yes ||
namelinkMode != cmInstallTargetGenerator::NamelinkModeOnly) {
libraryGenerator = CreateInstallTargetGenerator(
target, libraryArgs, false, helper.CaptureContext(),
helper.GetLibraryDestination(&libraryArgs), false, false,
@@ -582,7 +652,8 @@ bool CreateInstallGeneratorsForTarget(
libraryGenerator->SetNamelinkMode(
cmInstallTargetGenerator::NamelinkModeSkip);
}
if (namelinkMode != cmInstallTargetGenerator::NamelinkModeSkip) {
if (runtimeOnly == RuntimeOnly::No &&
namelinkMode != cmInstallTargetGenerator::NamelinkModeSkip) {
namelinkGenerator = CreateInstallTargetGenerator(
target, libraryArgs, false, helper.CaptureContext(),
helper.GetLibraryDestination(&libraryArgs), false, true,
@@ -590,10 +661,11 @@ bool CreateInstallGeneratorsForTarget(
namelinkGenerator->SetNamelinkMode(
cmInstallTargetGenerator::NamelinkModeOnly);
}
namelinkOnly =
namelinkOnly = runtimeOnly == RuntimeOnly::No &&
(namelinkMode == cmInstallTargetGenerator::NamelinkModeOnly);
if (target.GetMakefile()->PlatformSupportsAppleTextStubs() &&
if (runtimeOnly == RuntimeOnly::No &&
target.GetMakefile()->PlatformSupportsAppleTextStubs() &&
target.IsSharedLibraryWithExports()) {
// Apple .tbd files use the ARCHIVE properties
if (!archiveArgs.GetDestination().empty()) {
@@ -625,6 +697,9 @@ bool CreateInstallGeneratorsForTarget(
}
} break;
case cm::TargetType::STATIC_LIBRARY: {
if (runtimeOnly == RuntimeOnly::Yes) {
break;
}
// If it is marked with FRAMEWORK property use the FRAMEWORK set of
// INSTALL properties. Otherwise, use the LIBRARY properties.
if (target.IsFrameworkOnApple()) {
@@ -671,14 +746,20 @@ bool CreateInstallGeneratorsForTarget(
target, libraryArgs, false, helper.CaptureContext(), moduleDest, false,
false, configurations);
libraryGenerator->SetNamelinkMode(namelinkMode);
namelinkOnly =
libraryGenerator->SetNamelinkMode(
runtimeOnly == RuntimeOnly::Yes
? cmInstallTargetGenerator::NamelinkModeNone
: namelinkMode);
namelinkOnly = runtimeOnly == RuntimeOnly::No &&
(namelinkMode == cmInstallTargetGenerator::NamelinkModeOnly);
if (runtimeDependencySet) {
runtimeDependencySet->AddModule(libraryGenerator.get());
}
} break;
case cm::TargetType::OBJECT_LIBRARY: {
if (runtimeOnly == RuntimeOnly::Yes) {
break;
}
// Objects use OBJECT properties.
if (!objectArgs.GetDestination().empty()) {
// Verify that we know where the objects are to install them.
@@ -699,6 +780,9 @@ bool CreateInstallGeneratorsForTarget(
}
} break;
case cm::TargetType::EXECUTABLE: {
if (runtimeOnly == RuntimeOnly::Yes) {
break;
}
if (target.IsAppBundleOnApple()) {
// Application bundles use the BUNDLE properties.
if (!bundleArgs.GetDestination().empty()) {
@@ -760,7 +844,8 @@ bool CreateInstallGeneratorsForTarget(
// FRAMEWORK. For other target types or on other platforms, they are not
// installed automatically and so we need to create install files
// generators for them.
bool createInstallGeneratorsForTargetFileSets = true;
bool createInstallGeneratorsForTargetFileSets =
runtimeOnly == RuntimeOnly::No;
if (target.IsFrameworkOnApple()) {
createInstallGeneratorsForTargetFileSets = false;
@@ -836,7 +921,7 @@ bool CreateInstallGeneratorsForTarget(
}
}
if (!namelinkOnly) {
if (runtimeOnly == RuntimeOnly::No && !namelinkOnly) {
for (std::size_t i = 0; i < fileSetArgs.size(); i++) {
fileSetGenerators.push_back(CreateInstallFileSetGenerator(
helper, target, fileSetArgs[i], configurations));
@@ -844,7 +929,8 @@ bool CreateInstallGeneratorsForTarget(
}
}
if (!cxxModuleBmiArgs.GetDestination().empty()) {
if (runtimeOnly == RuntimeOnly::No &&
!cxxModuleBmiArgs.GetDestination().empty()) {
cxxModuleBmiGenerator = cm::make_unique<cmInstallCxxModuleBmiGenerator>(
target.GetName(), helper.GetCxxModulesBmiDestination(&cxxModuleBmiArgs),
cxxModuleBmiArgs.GetPermissions(),
@@ -857,7 +943,12 @@ bool CreateInstallGeneratorsForTarget(
}
// Add this install rule to an export if one was specified.
if (!addTargetExport()) {
// Transitive runtime artifacts from RUNTIME_DEPENDENCY_TARGETS are
// included; skip if this call created no runtime install generators.
bool const hasRuntimeArtifact = libraryGenerator || runtimeGenerator ||
frameworkGenerator || bundleGenerator;
if ((runtimeOnly == RuntimeOnly::No || hasRuntimeArtifact) &&
!addTargetExport()) {
return false;
}
@@ -893,6 +984,63 @@ bool CreateInstallGeneratorsForTarget(
return true;
}
bool InstallTargetsGroupedByConfig(
Helper& helper, InstallContext const& ctx, InstallCategoryFlags& flags,
cmLocalGenerator& lg,
std::map<std::string, std::set<cmTarget*>> const& byConfig,
std::vector<std::string> const& configs)
{
std::map<cmTarget*, std::size_t> hits;
for (std::pair<std::string const, std::set<cmTarget*>> const& e : byConfig) {
for (cmTarget* t : e.second) {
++hits[t];
}
}
std::set<cmTarget*> common;
std::size_t const n = configs.size();
for (std::pair<cmTarget* const, std::size_t> const& h : hits) {
if (h.second == n) {
common.insert(h.first);
}
}
auto installDep =
[&](cmTarget& dep,
cm::optional<std::vector<std::string>> configurations) -> bool {
std::string error;
if (!CreateInstallGeneratorsForTarget(helper, dep, ctx, flags, error,
configurations, RuntimeOnly::Yes)) {
lg.IssueMessage(MessageType::FATAL_ERROR, error);
return false;
}
return true;
};
for (cmTarget* dep : common) {
if (!installDep(*dep, cm::nullopt)) {
return false;
}
}
std::map<cmTarget*, std::vector<std::string>> configSpecific;
for (std::string const& config : configs) {
auto it = byConfig.find(config);
if (it == byConfig.end()) {
continue;
}
for (cmTarget* dep : it->second) {
if (common.find(dep) == common.end()) {
configSpecific[dep].push_back(config);
}
}
}
return std::all_of(
configSpecific.begin(), configSpecific.end(),
[&](std::pair<cmTarget* const, std::vector<std::string>> const& e) {
return installDep(*e.first, e.second);
});
}
void CheckAbsoluteDestination(Helper& helper, std::string const& destination)
{
// Check for an absolute destination.
@@ -1045,6 +1193,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>>
runtimeDependenciesArgVector;
std::string runtimeDependencySetArg;
bool installTransitiveDependencies = false;
std::vector<std::string> unknownArgs;
cmInstallCommandArguments genericArgs(helper.DefaultComponentName,
*helper.Makefile);
@@ -1052,6 +1201,8 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
genericArgs.Bind("EXPORT"_s, exports);
genericArgs.Bind("RUNTIME_DEPENDENCIES"_s, runtimeDependenciesArgVector);
genericArgs.Bind("RUNTIME_DEPENDENCY_SET"_s, runtimeDependencySetArg);
genericArgs.Bind("RUNTIME_DEPENDENCY_TARGETS"_s,
installTransitiveDependencies);
genericArgs.Parse(genericArgVector, &unknownArgs);
bool success = genericArgs.Finalize();
@@ -1224,6 +1375,14 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
return false;
}
if (installTransitiveDependencies) {
if (!exports.empty()) {
status.SetError("TARGETS RUNTIME_DEPENDENCY_TARGETS cannot be used "
"with EXPORT.");
return false;
}
}
cmInstallRuntimeDependencySet* runtimeDependencySet = nullptr;
if (runtimeDependenciesArgVector) {
if (!runtimeDependencySetArg.empty()) {
@@ -1348,6 +1507,40 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
ctx->RuntimeDependencySet = runtimeDependencySet;
ctx->BindGenericArguments();
if (installTransitiveDependencies) {
std::vector<std::string> rootNames;
rootNames.reserve(targets.size());
for (cmTarget const* t : targets) {
rootNames.push_back(t->GetName());
}
helper.Makefile->AddGeneratorAction(
[ctx, rootNames](cmLocalGenerator& lg, cmListFileBacktrace const&) {
std::vector<std::string> configs =
lg.GetMakefile()->GetGeneratorConfigs(
cmMakefile::IncludeEmptyConfig);
std::map<std::string, std::set<cmTarget*>> transitiveTargetsByConfig;
for (std::string const& config : configs) {
CollectEvaluatedTransitiveSharedModuleTargets(
lg, rootNames, transitiveTargetsByConfig[config], config);
}
cmExecutionStatus genStatus(*lg.GetMakefile());
Helper genHelper(genStatus);
InstallCategoryFlags flags;
if (!InstallTargetsGroupedByConfig(genHelper, *ctx, flags, lg,
transitiveTargetsByConfig,
configs)) {
return;
}
RegisterInstallComponents(lg.GetMakefile(), *ctx, flags);
},
cmMakefile::GeneratorActionWhen::AfterGeneratorTargets);
}
InstallCategoryFlags flags;
flags.FileSet.resize(ctx->FileSetArgs.size(), false);
@@ -191,6 +191,8 @@ elseif (CMake_TEST_install_VARIANT STREQUAL "Targets")
run_install_test(TARGETS-Defaults)
run_install_test(TARGETS-Module-Destination)
run_install_test(TARGETS-SymbolicComponent)
run_install_test(TARGETS-RUNTIME_DEPENDENCY_TARGETS)
run_cmake(TARGETS-RUNTIME_DEPENDENCY_TARGETS-EXPORT)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
run_install_test(TARGETS-NAMELINK-No-Tweak)
@@ -0,0 +1,4 @@
^CMake Error at TARGETS-RUNTIME_DEPENDENCY_TARGETS-EXPORT\.cmake:[0-9]+ \(install\):
install TARGETS RUNTIME_DEPENDENCY_TARGETS cannot be used with EXPORT\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)$
@@ -0,0 +1,6 @@
enable_language(C)
add_executable(exe main.c)
install(TARGETS exe RUNTIME_DEPENDENCY_TARGETS
EXPORT e
RUNTIME DESTINATION bin)
@@ -0,0 +1,73 @@
if(WIN32)
set(_check_files
[[bin]]
[[bin/first]]
[[bin/first/exe_1\.exe]]
[[bin/first/(lib)?shared_1\.dll]]
[[bin/first/(lib)?shared_2\.dll]]
[[bin/first/(lib)?shared_3\.dll]]
[[bin/second]]
[[bin/second/exe_2\.exe]]
[[bin/second/(lib)?shared_1\.dll]]
[[bin/second/(lib)?shared_2\.dll]]
[[bin/second/(lib)?shared_3\.dll]])
elseif(MSYS)
set(_check_files
[[bin]]
[[bin/first]]
[[bin/first/exe_1\.exe]]
[[bin/first/msys-shared_1\.dll]]
[[bin/first/msys-shared_2\.dll]]
[[bin/first/msys-shared_3\.dll]]
[[bin/second]]
[[bin/second/exe_2\.exe]]
[[bin/second/msys-shared_1\.dll]]
[[bin/second/msys-shared_2\.dll]]
[[bin/second/msys-shared_3\.dll]])
elseif(CYGWIN)
set(_check_files
[[bin]]
[[bin/first]]
[[bin/first/cygshared_1\.dll]]
[[bin/first/cygshared_2\.dll]]
[[bin/first/cygshared_3\.dll]]
[[bin/first/exe_1\.exe]]
[[bin/second]]
[[bin/second/cygshared_1\.dll]]
[[bin/second/cygshared_2\.dll]]
[[bin/second/cygshared_3\.dll]]
[[bin/second/exe_2\.exe]])
elseif(AIX)
set(_check_files
[[bin]]
[[bin/first]]
[[bin/first/exe_1]]
[[bin/second]]
[[bin/second/exe_2]]
[[lib]]
[[lib/first]]
[[lib/first/libshared_1\.a]]
[[lib/first/libshared_2\.a]]
[[lib/first/libshared_3\.a]]
[[lib/second]]
[[lib/second/libshared_1\.a]]
[[lib/second/libshared_2\.a]]
[[lib/second/libshared_3\.a]])
else()
set(_check_files
[[bin]]
[[bin/first]]
[[bin/first/exe_1]]
[[bin/second]]
[[bin/second/exe_2]]
[[lib]]
[[lib/first]]
[[lib/first/libshared_1\.(dylib|so)]]
[[lib/first/libshared_2\.(dylib|so)]]
[[lib/first/libshared_3\.(dylib|so)]]
[[lib/second]]
[[lib/second/libshared_1\.(dylib|so)]]
[[lib/second/libshared_2\.(dylib|so)]]
[[lib/second/libshared_3\.(dylib|so)]])
endif()
check_installed("^${_check_files}$")
@@ -0,0 +1,29 @@
enable_language(C)
add_executable(exe_1 main.c)
add_library(static STATIC obj1.c)
add_library(shared_1 SHARED obj5.c)
add_library(shared_2 SHARED obj3.c)
add_library(shared_3 SHARED obj4.c)
add_executable(exe_2 main.c)
add_library(iface INTERFACE)
target_link_libraries(exe_1 PRIVATE static shared_1)
target_link_libraries(static PRIVATE shared_2)
target_link_libraries(shared_1 PRIVATE shared_3)
target_link_libraries(exe_2 PRIVATE iface)
target_link_libraries(iface INTERFACE static shared_1)
install(
TARGETS exe_1 RUNTIME_DEPENDENCY_TARGETS
COMPONENT exc
LIBRARY DESTINATION lib/first
RUNTIME DESTINATION bin/first)
install(
TARGETS exe_2 RUNTIME_DEPENDENCY_TARGETS
COMPONENT exc
LIBRARY DESTINATION lib/second
RUNTIME DESTINATION bin/second)