Evaluate generator expressions in test and install BUILD_DEPENDS

The `BUILD_DEPENDS` options of `add_test`, `discover_tests` and `install`,
and the `CMAKE_TEST_BUILD_DEPENDS` directory property, took their entries
literally, so a dependency could not be computed per configuration or made
conditional, unlike the test `COMMAND`, from whose generator expressions
the test's target dependencies are already taken.

Evaluate an entry that contains a generator expression, as the test
`COMMAND` is evaluated: targets the expression references become target
dependencies, an evaluated value naming an artifact of such a target is
built by that dependency, any other evaluated value is a target or file
as before, and an entry evaluating to an empty string is ignored.  The
directory property is evaluated as a whole, so that an expression may
produce a list.  `install()` rules are configuration-independent, so an
`install(... BUILD_DEPENDS)` entry is evaluated for each configuration
and the rule depends on the union of the results.  Entries without
generator expressions are resolved exactly as before.

Closes: #28223
This commit is contained in:
Roger Leigh authored and Brad King committed 2026-10-30 16:39:48 -04:00
1 parent 7047fa76f6
commit 192199fae0
18 files changed
+286 -17

No files matched your search

+12
View File
@@ -101,6 +101,18 @@ directory the test is created in.
directory if the file exists there, and otherwise with respect to the
current binary directory.
* .. versionchanged:: 4.6
Entries may be specified using
:manual:`generator expressions <cmake-generator-expressions(7)>`.
An entry containing a generator expression is evaluated for the
configuration. Targets it references, as with ``$<TARGET_FILE:tgt>``,
are target dependencies, as they are when referenced in the test
``COMMAND``, and an evaluated value naming an artifact of such a target
is built by that target dependency. Any other evaluated value is a
target or file, as above. An entry that evaluates to an empty string is
ignored.
``COMMAND_EXPAND_LISTS``
.. versionadded:: 3.16
+5
View File
@@ -117,6 +117,11 @@ the provided regular expression and replacement strings.
Tests with missing build dependencies will not be run.
.. versionchanged:: 4.6
Entries may be specified using
:manual:`generator expressions <cmake-generator-expressions(7)>`,
evaluated as for the ``BUILD_DEPENDS`` option of :command:`add_test`.
CTest executes the discovery step to obtain the list of tests and then runs
each discovered test using the command-line produced by ``COMMAND`` together
with ``TEST_ARGS``. The pass/fail behavior of each discovered test follows
+11
View File
@@ -156,6 +156,17 @@ signatures that specify them. The common options are:
binary directory. If this option is used multiple times in a single call,
its list of dependencies accumulates.
.. versionchanged:: 4.6
Entries may be specified using
:manual:`generator expressions <cmake-generator-expressions(7)>`.
An entry containing a generator expression is evaluated for each build
configuration, and the rule depends on the union of the results.
Targets it references, as with ``$<TARGET_FILE:tgt>``, are target
dependencies, and an evaluated value naming an artifact of such a target
is built by that target dependency. Any other evaluated value is a
target or file, as above. An entry that evaluates to an empty string is
ignored.
.. versionadded:: 3.1
Command signatures that install files may print messages during
installation. Use the :variable:`CMAKE_INSTALL_MESSAGE` variable
@@ -15,3 +15,12 @@ there, and otherwise with respect to the current binary directory.
Projects may append to this property directly with :command:`set_property` or
:command:`set_directory_properties` to extend ``test_prep/all`` with
directory-scoped build dependencies.
.. versionchanged:: 4.6
The property value may contain
:manual:`generator expressions <cmake-generator-expressions(7)>`.
A value containing a generator expression is evaluated as a whole for each
configuration, and the result is the list of dependencies. Targets
referenced by the expression, as with ``$<TARGET_FILE:tgt>``, are target
dependencies, and an evaluated entry naming an artifact of such a target is
built by that target dependency. Empty entries are ignored.
+9
View File
@@ -0,0 +1,9 @@
build-depends-genex
-------------------
* The ``BUILD_DEPENDS`` options of the :command:`add_test`,
:command:`discover_tests` and :command:`install` commands, and the
:prop_dir:`CMAKE_TEST_BUILD_DEPENDS` directory property, now support
:manual:`generator expressions <cmake-generator-expressions(7)>`.
Targets referenced by an expression, such as ``$<TARGET_FILE:tgt>``,
become dependencies, and entries evaluating to an empty string are ignored.
+61 -10
View File
@@ -3,14 +3,20 @@
#include "cmInstallGenerator.h"
#include <algorithm>
#include <memory>
#include <set>
#include <sstream>
#include <utility>
#include <cm/string_view>
#include <cmext/algorithm>
#include "cmDiagnostics.h"
#include "cmGeneratorExpression.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmList.h"
#include "cmListFileCache.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
@@ -288,11 +294,16 @@ bool cmInstallGenerator::ResolveBuildDepends(
return gg->FindArtifactOwningTarget(file, config);
});
};
// Resolve a name to a target or a file.
auto const resolve = [this, lg, gg, &signature, &entries,
&isArtifact](std::string const& name) -> bool {
// Resolve a name to a target or a file. Names evaluated from generator
// expressions are added once, since they are evaluated for each
// configuration.
auto const resolve = [this, lg, gg, &signature, &entries, &isArtifact](
std::string const& name, std::string const& entry,
bool evaluated) -> bool {
if (cmGeneratorTarget const* target = lg->FindGeneratorTargetToUse(name)) {
entries.Targets.push_back(target);
if (!evaluated || !cm::contains(entries.Targets, target)) {
entries.Targets.push_back(target);
}
return true;
}
// A relative path is interpreted with respect to the current source
@@ -301,21 +312,61 @@ bool cmInstallGenerator::ResolveBuildDepends(
std::string file = lg->GetRealFileDependency(name);
if (gg->IsGeneratedByBuild(file) || isArtifact(file) ||
cmSystemTools::FileExists(file)) {
entries.Files.push_back(std::move(file));
if (!evaluated || !cm::contains(entries.Files, file)) {
entries.Files.push_back(std::move(file));
}
return true;
}
lg->IssueMessage(
MessageType::FATAL_ERROR,
cmStrCat("install(", signature, ") BUILD_DEPENDS entry \"", name,
"\" is neither a target nor a file that exists or is "
"generated by the build."),
evaluated
? cmStrCat("install(", signature, ") BUILD_DEPENDS entry \"", entry,
"\" evaluates to \"", name,
"\", which is neither a target nor a file that exists or "
"is generated by the build.")
: cmStrCat("install(", signature, ") BUILD_DEPENDS entry \"", name,
"\" is neither a target nor a file that exists or is "
"generated by the build."),
this->GetBacktrace());
return false;
};
cmGeneratorExpression ge(*lg->GetCMakeInstance(), this->GetBacktrace());
for (std::string const& name : names) {
if (!resolve(name)) {
return false;
if (cmGeneratorExpression::Find(name) == std::string::npos) {
if (!resolve(name, name, false)) {
return false;
}
continue;
}
// An entry containing generator expressions is evaluated for each
// configuration, and the rule depends on the union of the results.
// Targets an entry references are dependencies. An evaluated value
// naming an artifact of such a target is built by that target
// dependency, and any other value is a target or file, like an entry
// without generator expressions.
std::unique_ptr<cmCompiledGeneratorExpression> cge = ge.Parse(name);
for (std::string const& config : configs) {
cmList const evaluated{ cge->Evaluate(lg, config) };
std::set<cmGeneratorTarget const*> referenced;
for (cmGeneratorTarget const* target : cge->GetTargets()) {
if (target && !target->IsImported()) {
referenced.insert(target);
if (!cm::contains(entries.Targets, target)) {
entries.Targets.push_back(target);
}
}
}
for (std::string const& value : evaluated) {
if (value.empty() ||
(cmSystemTools::FileIsFullPath(value) &&
referenced.count(gg->FindArtifactOwningTarget(value, config)))) {
continue;
}
if (!resolve(value, name, true)) {
return false;
}
}
}
}
return true;
+12 -5
View File
@@ -269,16 +269,23 @@ bool cmLocalGenerator::GetDirectoryTestPrepTarget(DirectoryTestPrepTarget& out,
out.Dependencies = discovery->second;
}
cmList buildDepends;
std::vector<std::string> buildDepends;
if (directoryTestPrepDependencies) {
buildDepends.assign(
cmList{ *directoryTestPrepDependencies, cmList::EmptyElements::Yes });
if (cmGeneratorExpression::Find(*directoryTestPrepDependencies) !=
std::string::npos) {
// Evaluate the value as a whole, so that a generator expression may
// produce a list. The evaluated list is the list of dependencies.
buildDepends.push_back(*directoryTestPrepDependencies);
} else {
cmList const list{ *directoryTestPrepDependencies,
cmList::EmptyElements::Yes };
buildDepends.assign(list.begin(), list.end());
}
}
cmTestGenerator::BuildDependencies deps;
cmTestGenerator::EvaluateBuildDependencies(
this, config, this->Makefile->GetBacktrace(), "directory_test_prep", {},
std::vector<std::string>(buildDepends.begin(), buildDepends.end()), deps,
this->Makefile);
buildDepends, deps, this->Makefile);
for (cmGeneratorTarget* target : deps.Targets) {
out.Dependencies.push_back({ target->GetName(), target });
}
+26 -1
View File
@@ -150,7 +150,32 @@ bool cmTestGenerator::EvaluateBuildDependencies(
if (depName.empty()) {
continue;
}
addBuildDependency(depName);
if (cmGeneratorExpression::Find(depName) == std::string::npos) {
addBuildDependency(depName);
continue;
}
// An entry containing generator expressions is evaluated for the
// configuration. Targets it references are dependencies, as are those
// referenced by the test command. An evaluated value naming an artifact
// of such a target is built by that target dependency, and any other
// value is a target or file, like an entry without generator expressions.
auto parsed = ge.Parse(depName);
cmList const evaluated{ parsed->Evaluate(lg, config) };
std::set<cmGeneratorTarget*> referenced;
for (cmGeneratorTarget* dep : parsed->GetTargets()) {
if (dep && !dep->IsImported()) {
dependencies.insert(dep);
referenced.insert(dep);
}
}
for (std::string const& value : evaluated) {
if (value.empty() ||
(cmSystemTools::FileIsFullPath(value) &&
referenced.count(gg->FindArtifactOwningTarget(value, config)))) {
continue;
}
addBuildDependency(value);
}
}
for (cmGeneratorTarget* gt : dependencies) {
@@ -74,3 +74,31 @@ install(CODE "
)
add_library(unit_tests STATIC neverbuild.c)
# BUILD_DEPENDS entries may contain generator expressions, evaluated for each
# configuration. A target referenced by an expression is a dependency, so the
# artifact the expression evaluates to is prepared by building it. An entry
# evaluating to a target name names that target, and one evaluating to
# nothing adds no dependency.
add_library(depends_genex STATIC EXCLUDE_FROM_ALL lib.c)
set_property(TARGET depends_genex
PROPERTY ARCHIVE_OUTPUT_DIRECTORY "$<1:${artifact_dir}>")
set(genex_payload "${CMAKE_CURRENT_BINARY_DIR}/genex-payload.txt")
add_custom_target(generate_genex_payload
COMMAND ${CMAKE_COMMAND} -E touch "${genex_payload}"
BYPRODUCTS "${genex_payload}"
)
install(CODE "
if(NOT EXISTS \"$<TARGET_FILE:depends_genex>\")
message(FATAL_ERROR \"Preparation did not build depends_genex\")
endif()
if(NOT EXISTS \"${genex_payload}\")
message(FATAL_ERROR \"Preparation did not generate genex-payload.txt\")
endif()
"
COMPONENT Payload
BUILD_DEPENDS
"$<TARGET_FILE:depends_genex>"
"$<$<BOOL:ON>:generate_genex_payload>"
"$<$<BOOL:OFF>:${CMAKE_CURRENT_BINARY_DIR}/never-generated.txt>"
)
@@ -0,0 +1,16 @@
# Under a multi-configuration generator, preparing the Debug configuration
# also builds what the entry evaluated for Release names. A single
# configuration generator evaluates the entries for its one configuration.
set(debug_built "${RunCMake_TEST_BINARY_DIR}/union-debug-built.txt")
set(release_built "${RunCMake_TEST_BINARY_DIR}/union-release-built.txt")
if(NOT EXISTS "${debug_built}")
set(RunCMake_TEST_FAILED
"Preparation did not build the Debug dependency union_debug.")
elseif(RunCMake_GENERATOR_IS_MULTI_CONFIG AND NOT EXISTS "${release_built}")
set(RunCMake_TEST_FAILED "Preparation for Debug did not build the \
Release dependency, so the rule does not depend on the union of the \
entries evaluated for each configuration.")
elseif(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG AND EXISTS "${release_built}")
set(RunCMake_TEST_FAILED
"Preparation built the Release dependency, but only Debug is configured.")
endif()
@@ -0,0 +1,20 @@
# An install(... BUILD_DEPENDS) entry is evaluated for each configuration, and
# the rule depends on the union of the results, so preparing for any one
# configuration also prepares what the other configurations' entries name.
# Each dependency records that it was built.
set(union_debug_built "${CMAKE_CURRENT_BINARY_DIR}/union-debug-built.txt")
add_custom_target(union_debug
COMMAND ${CMAKE_COMMAND} -E touch "${union_debug_built}"
BYPRODUCTS "${union_debug_built}"
)
set(union_release_built "${CMAKE_CURRENT_BINARY_DIR}/union-release-built.txt")
add_custom_target(union_release
COMMAND ${CMAKE_COMMAND} -E touch "${union_release_built}"
BYPRODUCTS "${union_release_built}"
)
install(CODE "message(STATUS \"Installing Union\")"
COMPONENT Union
BUILD_DEPENDS
"$<$<CONFIG:Debug>:union_debug>"
"$<$<CONFIG:Release>:${union_release_built}>"
)
@@ -179,6 +179,16 @@ endfunction()
run_configured_install_prep(ConfigurationsDebug Debug)
run_configured_install_prep(ConfigurationsRelease Release)
# The rule depends on the union of the entries evaluated for each
# configuration, which BuildDependsUnion-prep-check.cmake verifies.
block()
configure_install_prep(BuildDependsUnion Debug)
run_cmake_command(BuildDependsUnion-prep ${CMAKE_COMMAND}
--build . --config Debug --target install_prep/Union)
run_cmake_command(BuildDependsUnion-install ${CMAKE_COMMAND}
--install . --config Debug --component Union)
endblock()
if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
block()
configure_install_prep(ConfigurationsMulti Debug)
@@ -0,0 +1,53 @@
project(TestDependencyBuildDependsGenex C)
enable_testing()
# Targets reached only through generator expressions in build dependencies.
# Each records that it was built, so the tests below fail unless building
# test_prep/all built it.
foreach(name IN ITEMS genex_test genex_discover genex_directory)
add_executable(${name} EXCLUDE_FROM_ALL ../add_test/main.c)
add_custom_command(TARGET ${name} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E touch
"${CMAKE_CURRENT_BINARY_DIR}/${name}-built.txt"
VERBATIM)
endforeach()
add_custom_target(genex_utility
COMMAND "${CMAKE_COMMAND}" -E touch
"${CMAKE_CURRENT_BINARY_DIR}/genex_utility-built.txt"
VERBATIM)
# An entry that evaluates to nothing adds no dependency. Were it taken
# literally, the tests would not run for want of this file.
set(never "${CMAKE_CURRENT_BINARY_DIR}/never-built.txt")
# A target referenced by an expression is a dependency, and the artifact the
# expression evaluates to is built by it. An entry evaluating to a target
# name is a target dependency.
add_test(NAME GenexBuildDepends
COMMAND "${CMAKE_COMMAND}" -E cat
"${CMAKE_CURRENT_BINARY_DIR}/genex_test-built.txt"
"${CMAKE_CURRENT_BINARY_DIR}/genex_utility-built.txt"
BUILD_DEPENDS
"$<TARGET_FILE:genex_test>"
"$<$<BOOL:OFF>:${never}>"
"$<$<BOOL:ON>:genex_utility>")
add_executable(fake_discovery fake_discovery.c)
discover_tests(COMMAND fake_discovery
DISCOVERY_ARGS --list_tests
DISCOVERY_MATCH "^([^,]+),([^,]+)$"
TEST_NAME "GENEX.\\1"
TEST_ARGS "\\1"
BUILD_DEPENDS
"$<TARGET_FILE:genex_discover>"
"$<$<BOOL:OFF>:${never}>")
add_test(NAME GenexDiscoverBuilt
COMMAND "${CMAKE_COMMAND}" -E cat
"${CMAKE_CURRENT_BINARY_DIR}/genex_discover-built.txt")
# The directory property is evaluated as a whole.
set_property(DIRECTORY APPEND PROPERTY CMAKE_TEST_BUILD_DEPENDS
"$<TARGET_FILE:genex_directory>" "$<$<BOOL:OFF>:${never}>")
add_test(NAME GenexDirectoryBuilt
COMMAND "${CMAKE_COMMAND}" -E cat
"${CMAKE_CURRENT_BINARY_DIR}/genex_directory-built.txt")
@@ -241,7 +241,7 @@ if(RunCMake_GENERATOR MATCHES "Ninja|FASTBuild|Makefiles")
# Verify successful behavior
block()
foreach(case IN ITEMS DiscoverTestsUtility DiscoverTestsCommands
DiscoverTestsGeneratedFile DirectoryGeneratedFile)
DiscoverTestsGeneratedFile DirectoryGeneratedFile BuildDependsGenex)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${case}-build)
run_cmake(${case})
set(RunCMake_TEST_NO_CLEAN 1)
@@ -0,0 +1,9 @@
^CMake Error at FILES-BUILD_DEPENDS-genex-missing\.cmake:[0-9]+ \(install\):
install\(FILES\) BUILD_DEPENDS entry "\$<1:missing_target>" evaluates to
"missing_target", which is neither a target nor a file that exists or is
generated by the build\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
CMake Generate step failed\. Build files cannot be regenerated correctly\.$
@@ -0,0 +1,2 @@
install(FILES empty.c DESTINATION .
BUILD_DEPENDS "$<$<BOOL:OFF>:missing_off>" "$<1:missing_target>")
@@ -136,6 +136,7 @@ if (CMake_TEST_install_VARIANT STREQUAL "Files")
run_cmake(DIRECTORY-PATTERN-BUILD_DEPENDS)
run_cmake(SCRIPT-BUILD_DEPENDS-missing-target)
run_cmake(FILES-BUILD_DEPENDS-missing-target)
run_cmake(FILES-BUILD_DEPENDS-genex-missing)
run_cmake(PROGRAMS-BUILD_DEPENDS-missing-target)
run_cmake(DIRECTORY-BUILD_DEPENDS-missing-target)
if(RunCMake_GENERATOR MATCHES "Ninja|Make|FASTBuild")