1
0
mirror of https://github.com/Kitware/CMake.git synced 2025-10-14 02:08:27 +08:00

GenEx: Fix evaluation of $<CONFIG> on imported targets

The historic implementation of `$<CONFIG>` had some errors that could
result in multiple configurations matching. First, it always considered
the configuration of the consuming target, even if a consumed imported
target selected a different configuration. Second, it matched the entire
list of `MAP_IMPORTED_CONFIG_<CONFIG>` configurations, even if none of
those were actually selected. The latter in particular is redundant at
best, as we also consider the selected configuration of an imported
target, which is the correct configuration to match for imported
targets. Refactor the implementation so that only one configuration is
considered.

Fixes: #23660
Issue: #27022
This commit is contained in:
Matthew Woehlke
2025-07-30 12:02:18 -04:00
committed by Brad King
parent 9b36e49ad9
commit 8ac826a5f2
13 changed files with 325 additions and 24 deletions

View File

@@ -98,6 +98,7 @@ Policies Introduced by CMake 4.2
.. toctree::
:maxdepth: 1
CMP0199: $<CONFIG> only matches the configuration of the consumed target. </policy/CMP0199>
CMP0198: CMAKE_PARENT_LIST_FILE is not defined in CMakeLists.txt. </policy/CMP0198>
Policies Introduced by CMake 4.1

93
Help/policy/CMP0199.rst Normal file
View File

@@ -0,0 +1,93 @@
CMP0199
-------
.. versionadded:: 4.2
:genex:`$<CONFIG:cfgs>` only matches the configuration of the consumed target.
Historically, when a :genex:`$<CONFIG:cfgs>` generator expression appeared in
the properties of an imported target, it would match (that is, evaluate to
``1``) if any of the ``cfgs`` matched *any* of the following:
1. The selected configuration of the imported target being consumed.
2. The configuration of the consuming target.
3. *Any* of the configurations in the :prop_tgt:`MAP_IMPORTED_CONFIG_<CONFIG>`
of the imported target being consumed
(where ``<CONFIG>`` is the configuration of the consuming target),
*whether or not such configurations are valid for the imported target*.
This could result in expressions which are intended to be mutually exclusive
being concurrently evaluated. This would be especially problematic if the
value of a compile definition is intended to be determined by the
configuration, as this lack of exclusivity could result in redefinition.
CMake 4.2 and above prefer to consider *only* the configuration of the imported
target being consumed; that is, (1) in the above list.
This policy provides compatibility with projects that rely on the historical
behavior. The ``OLD`` behavior for this policy is to retain the historic
behavior as described above. The ``NEW`` behavior is to consider only the
configuration of the imported target being consumed.
.. note::
This policy only applies to generator expressions being evaluated as part of
the usage requirements of imported targets which are not imported from |CPS|
packages.
For non-imported targets, both the historic and ongoing behavior is to
consider only the configuration of the consuming target. (The selected
configuration of a non-imported target is always the active build
configuration, which is necessarily the same as the consuming target's
configuration.)
For targets imported from |CPS| packages, the ``NEW`` behavior is used,
regardless of the policy setting.
.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 4.2
.. |WARNS_OR_DOES_NOT_WARN| replace:: warns
.. include:: include/STANDARD_ADVICE.rst
.. include:: include/DEPRECATED.rst
Examples
^^^^^^^^
Consider the following imported libraries:
.. code-block:: cmake
add_library(test1 INTERFACE IMPORTED)
set_target_properties(test1 PROPERTIES
IMPORTED_CONFIGURATIONS "DEBUG"
INTERFACE_COMPILE_DEFINITIONS
"$<$<CONFIG:debug>:DEBUG>;$<$<CONFIG:release>:RELEASE>"
)
add_library(test2 INTERFACE IMPORTED)
set_target_properties(test2 PROPERTIES
IMPORTED_CONFIGURATIONS "TEST"
INTERFACE_COMPILE_DEFINITIONS
"$<$<CONFIG:debug>:DEBUG>;$<$<CONFIG:example>:EXAMPLE>;$<$<CONFIG:test>:TEST>"
MAP_IMPORTED_CONFIG_RELEASE "DEBUG;EXAMPLE;TEST"
)
Assume that the consuming project is built in the ``Release`` configuration.
Under the ``OLD`` policy, a consumer of ``test1`` would see both ``DEBUG``
and ``RELEASE`` defined; ``$<CONFIG:debug>`` evaluates to ``1`` because the
selected configuration of ``test1`` is ``DEBUG``, and ``$<CONFIG:release>``
evaluates to ``1`` because the consumer's configuration is ``Release``
(keeping in mind that configuration matching is case-insensitive). Likewise,
a consumer of ``test2`` would see all of ``DEBUG``, ``RELEASE`` and ``TEST``
defined; ``$<CONFIG:debug>``, ``$<CONFIG:example>`` and ``$<CONFIG:test>`` all
evaluate to ``1`` because all of these configurations appear in
``MAP_IMPORTED_CONFIG_RELEASE``.
Under the ``NEW`` policy, when ``test1`` is consumed, only ``$<CONFIG:debug>``
will evaluate to ``1``. Similarly, when ``test2`` is consumed, only
``$<CONFIG:test>`` will evaluate to ``1``. Both of these correspond to the
configuration of the consumed library that is actually selected by CMake.
.. |CPS| replace:: Common Package Specification

View File

@@ -0,0 +1,5 @@
config-fix-matching
-------------------
The :genex:`$<CONFIG:cfgs>` generator expression no longer matches multiple
configurations. See policy :policy:`CMP0199`.

View File

@@ -2226,9 +2226,11 @@ static const struct ConfigurationTestNode : public cmGeneratorExpressionNode
if (parameters.empty()) {
return configurationNode.Evaluate(parameters, context, content, nullptr);
}
static cmsys::RegularExpression configValidator("^[A-Za-z0-9_]*$");
context->HadContextSensitiveCondition = true;
// First, validate our arguments.
static cmsys::RegularExpression configValidator("^[A-Za-z0-9_]*$");
bool firstParam = true;
for (auto const& param : parameters) {
if (!configValidator.find(param)) {
@@ -2248,41 +2250,109 @@ static const struct ConfigurationTestNode : public cmGeneratorExpressionNode
context->LG->GetCMakeInstance()->IssueMessage(
MessageType::WARNING, e.str(), context->Backtrace);
}
firstParam = false;
if (context->Config.empty()) {
if (param.empty()) {
}
// Determine the context(s) in which the expression should be evaluated. If
// CMPxxxx is NEW, the context is exactly one of the imported target's
// selected configuration, if applicable, or the consuming target's
// configuration, otherwise.
//
// If CMPxxxx is OLD, we evaluate first in the context of the consuming
// target, then, if the consumed target is imported, we evaluate based on
// the mapped configurations (this logic is... problematic; see comment
// below), then finally based on the selected configuration of the imported
// target.
bool const targetIsImported =
(context->CurrentTarget && context->CurrentTarget->IsImported());
bool const oldPolicy = [&] {
if (!targetIsImported) {
// For non-imported targets, there is no behavior difference between
// the OLD and NEW policy.
return false;
}
cmTarget const* const t = context->CurrentTarget->Target;
if (t->GetOrigin() == cmTarget::Origin::Cps) {
// Generator expressions appearing on targets imported from CPS should
// always be evaluated according to the selected configuration of the
// imported target, i.e. the NEW policy.
return false;
}
switch (context->HeadTarget->GetPolicyStatusCMP0199()) {
case cmPolicies::WARN:
if (context->LG->GetMakefile()->PolicyOptionalWarningEnabled(
"CMAKE_POLICY_WARNING_CMP0199")) {
std::string const err =
cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0199),
"\nEvaluation of $<CONFIG> for imported target \"",
context->CurrentTarget->GetName(), "\", used by \"",
context->HeadTarget->GetName(),
"\", may match multiple configurations.\n");
context->LG->GetCMakeInstance()->IssueMessage(
MessageType ::AUTHOR_WARNING, err, context->Backtrace);
}
CM_FALLTHROUGH;
case cmPolicies::OLD:
return true;
case cmPolicies::NEW:
return false;
}
// Should be unreachable
assert(false);
return false;
}();
if (!targetIsImported || oldPolicy) {
// Does the consuming target's configuration match any of the arguments?
for (auto const& param : parameters) {
if (context->Config.empty()) {
if (param.empty()) {
return "1";
}
} else if (cmsysString_strcasecmp(param.c_str(),
context->Config.c_str()) == 0) {
return "1";
}
} else if (cmsysString_strcasecmp(param.c_str(),
context->Config.c_str()) == 0) {
return "1";
}
}
if (context->CurrentTarget && context->CurrentTarget->IsImported()) {
if (targetIsImported) {
cmValue loc = nullptr;
cmValue imp = nullptr;
std::string suffix;
if (context->CurrentTarget->Target->GetMappedConfig(context->Config, loc,
imp, suffix)) {
// This imported target has an appropriate location
// for this (possibly mapped) config.
// Check if there is a proper config mapping for the tested config.
cmList mappedConfigs;
std::string mapProp = cmStrCat(
"MAP_IMPORTED_CONFIG_", cmSystemTools::UpperCase(context->Config));
if (cmValue mapValue = context->CurrentTarget->GetProperty(mapProp)) {
mappedConfigs.assign(cmSystemTools::UpperCase(*mapValue));
if (oldPolicy) {
// If the target has a MAP_IMPORTED_CONFIG_<CONFIG> property for the
// consumer's <CONFIG>, match *any* config in that list, regardless
// of whether it's valid or of what GetMappedConfig actually picked.
// This will result in $<CONFIG> producing '1' for multiple configs,
// and is almost certainly wrong, but it's what CMake did for a very
// long time, and... Hyrum's Law.
cmList mappedConfigs;
std::string mapProp = cmStrCat(
"MAP_IMPORTED_CONFIG_", cmSystemTools::UpperCase(context->Config));
if (cmValue mapValue =
context->CurrentTarget->GetProperty(mapProp)) {
mappedConfigs.assign(cmSystemTools::UpperCase(*mapValue));
for (auto const& param : parameters) {
if (cm::contains(mappedConfigs, cmSystemTools::UpperCase(param))) {
return "1";
for (auto const& param : parameters) {
if (cm::contains(mappedConfigs,
cmSystemTools::UpperCase(param))) {
return "1";
}
}
return "0";
}
} else if (!suffix.empty()) {
// There is no explicit mapping for the tested config, so use
// the configuration of the imported location that was selected.
}
// This imported target has an appropriate location for this (possibly
// mapped) config.
if (!suffix.empty()) {
// Use the (possibly mapped) configuration of the imported location
// that was selected.
for (auto const& param : parameters) {
if (cmStrCat('_', cmSystemTools::UpperCase(param)) == suffix) {
return "1";
@@ -2291,6 +2361,7 @@ static const struct ConfigurationTestNode : public cmGeneratorExpressionNode
}
}
}
return "0";
}
} configurationTestNode;

View File

@@ -592,7 +592,11 @@ class cmMakefile;
WARN) \
SELECT(POLICY, CMP0198, \
"CMAKE_PARENT_LIST_FILE is not defined in CMakeLists.txt.", 4, 2, 0, \
WARN)
WARN) \
SELECT( \
POLICY, CMP0199, \
"$<CONFIG:cfgs> only matches the configuration of the consumed target.", \
4, 2, 0, WARN)
#define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
#define CM_FOR_EACH_POLICY_ID(POLICY) \
@@ -640,7 +644,8 @@ class cmMakefile;
F(CMP0179) \
F(CMP0181) \
F(CMP0182) \
F(CMP0195)
F(CMP0195) \
F(CMP0199)
#define CM_FOR_EACH_CUSTOM_COMMAND_POLICY(F) \
F(CMP0116) \

View File

@@ -0,0 +1,10 @@
project(test-CMP0199-NEW C)
cmake_policy(SET CMP0199 NEW)
include(CMP0199-cases.cmake)
# FIXME: CMake currently incorrectly selects the RELEASE configuration. See
# https://gitlab.kitware.com/cmake/cmake/-/issues/27022.
do_mapped_config_test(EXPECT_RELEASE)
do_unique_config_test(EXPECT_DEBUG)

View File

@@ -0,0 +1,8 @@
project(test-CMP0199-OLD C)
cmake_policy(SET CMP0199 OLD)
include(CMP0199-cases.cmake)
do_mapped_config_test(EXPECT_RELEASE EXPECT_DEBUG EXPECT_TEST)
do_unique_config_test(EXPECT_RELEASE EXPECT_DEBUG)

View File

@@ -0,0 +1,11 @@
CMake Warning \(dev\) at CMP0199-WARN\.cmake:[0-9]+ \(target_link_libraries\):
Policy CMP0199 is not set: \$<CONFIG:cfgs> only matches the configuration of
the consumed target\. Run "cmake --help-policy CMP0199" for policy details\.
Use the cmake_policy command to set the policy and suppress this warning\.
Evaluation of \$<CONFIG> for imported target "lib_test", used by "exe_test",
may match multiple configurations\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-dev to suppress it\.

View File

@@ -0,0 +1,11 @@
project(test-CMP0199-WARN C)
set(CMAKE_POLICY_WARNING_CMP0199 ON)
add_library(lib_test INTERFACE IMPORTED)
set_target_properties(lib_test PROPERTIES
INTERFACE_COMPILE_DEFINITIONS "$<$<CONFIG:release>:RELEASE>"
)
add_executable(exe_test configtest.c)
target_link_libraries(exe_test PRIVATE lib_test)

View File

@@ -0,0 +1,32 @@
# Under CMP0199 OLD, $<CONFIG> matches not just the selected configuration, but
# every entry in MAP_IMPORTED_CONFIG_<CONFIG>. Under NEW, it should only match
# the selected configuration.
function(do_mapped_config_test)
add_library(lib_mapped INTERFACE IMPORTED)
set_target_properties(lib_mapped PROPERTIES
IMPORTED_CONFIGURATIONS "TEST"
INTERFACE_COMPILE_DEFINITIONS
"$<$<CONFIG:debug>:DEBUG>;$<$<CONFIG:release>:RELEASE>;$<$<CONFIG:test>:TEST>"
MAP_IMPORTED_CONFIG_RELEASE "RELEASE;DEBUG;TEST"
)
add_executable(exe_mapped configtest.c)
target_compile_definitions(exe_mapped PRIVATE ${ARGN})
target_link_libraries(exe_mapped PRIVATE lib_mapped)
endfunction()
# Under CMake CMP0199 OLD, $<CONFIG> matches both the consumer's configuration
# AND the selected configuration of the library being consumed. Under NEW, it
# should only match the selected configuration.
function(do_unique_config_test)
add_library(lib_unique INTERFACE IMPORTED)
set_target_properties(lib_unique PROPERTIES
IMPORTED_CONFIGURATIONS "DEBUG"
INTERFACE_COMPILE_DEFINITIONS
"$<$<CONFIG:debug>:DEBUG>;$<$<CONFIG:release>:RELEASE>"
)
add_executable(exe_unique configtest.c)
target_compile_definitions(exe_unique PRIVATE ${ARGN})
target_link_libraries(exe_unique PRIVATE lib_unique)
endfunction()

View File

@@ -48,6 +48,20 @@ run_cmake(FILTER-InvalidOperator)
run_cmake(FILTER-Exclude)
run_cmake(FILTER-Include)
function(run_cmake_build test)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)
if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_CONFIGURATION_TYPES=Release)
else()
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Release)
endif()
run_cmake(${test})
set(RunCMake_TEST_NO_CLEAN TRUE)
run_cmake_command(${test}-build ${CMAKE_COMMAND} --build . --config Release)
endfunction()
function(run_linker_genex test lang type)
set(options_args CHECK_RESULT EXECUTE)
cmake_parse_arguments(PARSE_ARGV 3 RLG "${options_args}" "" "")
@@ -103,6 +117,11 @@ else()
set(RunCMake_TEST_OPTIONS [==[-DCMAKE_BUILD_TYPE=]==])
endif()
run_cmake(CONFIG-empty-entries)
unset(RunCMake_TEST_OPTIONS)
run_cmake(CMP0199-WARN)
run_cmake_build(CMP0199-OLD)
run_cmake_build(CMP0199-NEW)
set(RunCMake_TEST_OPTIONS -DCMAKE_POLICY_DEFAULT_CMP0085:STRING=OLD)
run_cmake(CMP0085-OLD)

View File

@@ -0,0 +1,34 @@
#ifdef EXPECT_DEBUG
# ifndef DEBUG
# error DEBUG should be defined
# endif
#else
# ifdef DEBUG
# error DEBUG should not be defined
# endif
#endif
#ifdef EXPECT_RELEASE
# ifndef RELEASE
# error RELEASE should be defined
# endif
#else
# ifdef RELEASE
# error RELEASE should not be defined
# endif
#endif
#ifdef EXPECT_TEST
# ifndef TEST
# error TEST should be defined
# endif
#else
# ifdef TEST
# error TEST should not be defined
# endif
#endif
int main(void)
{
return 0;
}

View File

@@ -47,6 +47,7 @@
\* CMP0181
\* CMP0182
\* CMP0195
\* CMP0199
Call Stack \(most recent call first\):
CMakeLists.txt:3 \(include\)