mirror of
https://github.com/Kitware/CMake.git
synced 2026-06-25 17:28:53 +00:00
GenEx: add $<LIST:TRANSFORM,...,APPLY,body> action
Add an APPLY action to $<LIST:TRANSFORM> that evaluates an arbitrary <body> once per selected element, with $<_0> bound to the element, so a list can be mapped through any generator expression at generate time. Unlike the configure-time list(TRANSFORM ... APPLY <function>) command, the genex form has no side effects and returns the body's value directly, and a list-valued result expands into multiple elements. The body evaluates in its own binding scope, so nested APPLY actions can shadow $<_0>, and context-sensitive state it observes (such as target dependencies) still propagates to the enclosing expression. APPLY accepts the same AT/FOR/REGEX selectors as the canned actions. Issue: #27892
This commit is contained in:
@@ -933,6 +933,23 @@ List Transformations
|
||||
element instead of the beginning of each repeated search.
|
||||
See policy :policy:`CMP0186`.
|
||||
|
||||
``APPLY``
|
||||
Transform each selected element by evaluating an arbitrary generator
|
||||
expression ``<body>`` once per element. Within ``<body>``, the bound
|
||||
operand ``$<_0>`` expands to the current element. Unlike the
|
||||
configure-time ``list(TRANSFORM ... APPLY <function>)`` command, the
|
||||
genex form returns the body's value directly and has no side effects. A
|
||||
list-valued body result expands into multiple elements, like any other
|
||||
list-valued generator expression.
|
||||
|
||||
.. code-block:: cmake
|
||||
|
||||
$<LIST:TRANSFORM,list,APPLY,body[,SELECTOR]>
|
||||
|
||||
See ``$<_0>`` below for the bound operand.
|
||||
|
||||
.. versionadded:: 4.5
|
||||
|
||||
``SELECTOR`` determines which items of the list will be transformed.
|
||||
Only one type of selector can be specified at a time. When given,
|
||||
``SELECTOR`` must be one of the following:
|
||||
@@ -1061,6 +1078,9 @@ Bound Operands
|
||||
generator expression that evaluates a ``<body>`` once for each value it
|
||||
supplies, with ``$<_0>`` expanding to that value.
|
||||
|
||||
For example, ``$<LIST:TRANSFORM,...,APPLY,body>`` evaluates ``body`` once per
|
||||
list element with ``$<_0>`` bound to the current element.
|
||||
|
||||
``$<_0>`` is only valid inside the body of a binding operation. Using it
|
||||
anywhere else is an error.
|
||||
|
||||
|
||||
6
Help/release/dev/genex-list-transform-apply.rst
Normal file
6
Help/release/dev/genex-list-transform-apply.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
genex-list-transform-apply
|
||||
--------------------------
|
||||
|
||||
* The :genex:`LIST` generator expression's ``TRANSFORM`` operation gained an
|
||||
``APPLY`` action that evaluates an arbitrary generator expression for each
|
||||
element, with ``$<_0>`` referring to the current element.
|
||||
@@ -107,6 +107,48 @@ std::string cmGeneratorExpressionNode::EvaluateDependentExpression(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Re-evaluate the unevaluated <body> subtree of a binding operation with
|
||||
// `$<_0>` bound to the given operand. A fresh Evaluation is built from a
|
||||
// copied, mutated Context so that nested binding operations can shadow `$<_0>`
|
||||
// and restore it on exit.
|
||||
static std::string EvaluateBodyWithBoundOperand(
|
||||
cmGeneratorExpressionEvaluatorVector const& bodyExpr,
|
||||
std::string const& operand, cm::GenEx::Evaluation* eval,
|
||||
cmGeneratorExpressionDAGChecker* dagChecker)
|
||||
{
|
||||
cm::GenEx::Context elemContext = eval->Context; // copy
|
||||
elemContext.SetBoundOperand(operand);
|
||||
cm::GenEx::Evaluation elemEval(
|
||||
elemContext, eval->Quiet, eval->HeadTarget, eval->CurrentTarget,
|
||||
eval->EvaluateForBuildsystem, eval->Backtrace);
|
||||
|
||||
std::string result;
|
||||
for (auto const& pExprEval : bodyExpr) {
|
||||
result += pExprEval->Evaluate(&elemEval, dagChecker);
|
||||
if (elemEval.HadError) {
|
||||
eval->HadError = true;
|
||||
return std::string{};
|
||||
}
|
||||
}
|
||||
eval->HadContextSensitiveCondition |= elemEval.HadContextSensitiveCondition;
|
||||
eval->HadHeadSensitiveCondition |= elemEval.HadHeadSensitiveCondition;
|
||||
eval->HadLinkLanguageSensitiveCondition |=
|
||||
elemEval.HadLinkLanguageSensitiveCondition;
|
||||
eval->DependTargets.insert(elemEval.DependTargets.begin(),
|
||||
elemEval.DependTargets.end());
|
||||
eval->AllTargets.insert(elemEval.AllTargets.begin(),
|
||||
elemEval.AllTargets.end());
|
||||
eval->SeenTargetProperties.insert(elemEval.SeenTargetProperties.begin(),
|
||||
elemEval.SeenTargetProperties.end());
|
||||
eval->SourceSensitiveTargets.insert(elemEval.SourceSensitiveTargets.begin(),
|
||||
elemEval.SourceSensitiveTargets.end());
|
||||
for (auto const& entry : elemEval.MaxLanguageStandard) {
|
||||
eval->MaxLanguageStandard[entry.first].insert(entry.second.begin(),
|
||||
entry.second.end());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static const struct ZeroNode : public cmGeneratorExpressionNode
|
||||
{
|
||||
ZeroNode() {} // NOLINT(modernize-use-equals-default)
|
||||
@@ -2005,11 +2047,74 @@ static const struct ListNode : public cmGeneratorExpressionNode
|
||||
|
||||
bool AcceptsArbitraryContentParameter() const override { return true; }
|
||||
|
||||
bool ShouldEvaluateNextParameter(std::vector<std::string> const& parameters,
|
||||
std::string&) const override
|
||||
{
|
||||
// Skip the APPLY <body> (4th parameter) so $<_0> is not evaluated unbound;
|
||||
// selector args (5th+) evaluate normally.
|
||||
return !(parameters.size() == 3 && parameters[0] == "TRANSFORM" &&
|
||||
parameters[2] == "APPLY");
|
||||
}
|
||||
|
||||
std::string Evaluate(
|
||||
std::vector<std::string> const& parameters, cm::GenEx::Evaluation* eval,
|
||||
GeneratorExpressionContent const* content,
|
||||
cmGeneratorExpressionDAGChecker* /*dagChecker*/) const override
|
||||
cmGeneratorExpressionDAGChecker* dagChecker) const override
|
||||
{
|
||||
if (parameters.size() >= 3 && parameters[0] == "TRANSFORM" &&
|
||||
parameters[2] == "APPLY") {
|
||||
if (parameters.size() < 4) {
|
||||
reportError(
|
||||
eval, content->GetOriginalExpression(),
|
||||
"sub-command TRANSFORM, action APPLY expects a <body> argument.");
|
||||
return std::string();
|
||||
}
|
||||
cmList list = GetList(parameters[1]);
|
||||
if (list.empty()) {
|
||||
return std::string{};
|
||||
}
|
||||
cmGeneratorExpressionEvaluatorVector const& bodyExpr =
|
||||
content->GetParamChildren()[3];
|
||||
std::vector<std::string> const selectorTokens(parameters.begin() + 4,
|
||||
parameters.end());
|
||||
std::unique_ptr<cmList::TransformSelector> selector =
|
||||
ParseTransformSelector(selectorTokens, eval, content);
|
||||
if (!selector) {
|
||||
return std::string();
|
||||
}
|
||||
// selector->Makefile is left unset: the REGEX/AT/FOR selectors never
|
||||
// consult it, and the only one that does (list()'s PREDICATE) cannot
|
||||
// reach here.
|
||||
std::vector<bool> selected;
|
||||
try {
|
||||
selected = list.GetTransformSelection(*selector);
|
||||
} catch (cmList::transform_error& e) {
|
||||
reportError(eval, content->GetOriginalExpression(), e.what());
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::vector<std::string> out;
|
||||
out.reserve(list.size());
|
||||
std::size_t i = 0;
|
||||
for (auto const& element : list) {
|
||||
if (i < selected.size() && selected[i]) {
|
||||
out.push_back(
|
||||
EvaluateBodyWithBoundOperand(bodyExpr, element, eval, dagChecker));
|
||||
if (eval->HadError) {
|
||||
return std::string();
|
||||
}
|
||||
} else {
|
||||
out.push_back(element);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
// Join per-element results with ';'; keep empty elements so a body that
|
||||
// yields "" is not dropped.
|
||||
return cmList{ out.begin(), out.end(), cmList::ExpandElements::No,
|
||||
cmList::EmptyElements::Yes }
|
||||
.to_string();
|
||||
}
|
||||
|
||||
static std::unordered_map<
|
||||
cm::string_view,
|
||||
std::function<std::string(cm::GenEx::Evaluation*,
|
||||
|
||||
@@ -15,6 +15,7 @@ set(CMakeLib_TESTS
|
||||
testGccDepfileReader.cxx
|
||||
testGeneratedFileStream.cxx
|
||||
testGenExBoundOperand.cxx
|
||||
testGenExTransformApply.cxx
|
||||
testJSONHelpers.cxx
|
||||
testRST.cxx
|
||||
testRange.cxx
|
||||
|
||||
174
Tests/CMakeLib/testGenExTransformApply.cxx
Normal file
174
Tests/CMakeLib/testGenExTransformApply.cxx
Normal file
@@ -0,0 +1,174 @@
|
||||
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||||
file LICENSE.rst or https://cmake.org/licensing for details. */
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include <cm/memory>
|
||||
|
||||
#include "cmGeneratorExpression.h"
|
||||
#include "cmGlobalGenerator.h"
|
||||
#include "cmLocalGenerator.h"
|
||||
#include "cmMakefile.h"
|
||||
#include "cmState.h"
|
||||
#include "cmStateDirectory.h"
|
||||
#include "cmStateSnapshot.h"
|
||||
#include "cmake.h"
|
||||
|
||||
namespace {
|
||||
struct GenExFixture
|
||||
{
|
||||
cmake CMake{ cmState::Role::Project };
|
||||
std::unique_ptr<cmGlobalGenerator> GG;
|
||||
std::unique_ptr<cmMakefile> MF;
|
||||
std::unique_ptr<cmLocalGenerator> LG;
|
||||
|
||||
GenExFixture()
|
||||
{
|
||||
this->GG = cm::make_unique<cmGlobalGenerator>(&this->CMake);
|
||||
cmStateSnapshot snapshot = this->CMake.GetCurrentSnapshot();
|
||||
snapshot.GetDirectory().SetCurrentBinary(".");
|
||||
snapshot.GetDirectory().SetCurrentSource(".");
|
||||
this->MF = cm::make_unique<cmMakefile>(this->GG.get(), snapshot);
|
||||
this->LG = this->GG->CreateLocalGenerator(this->MF.get());
|
||||
}
|
||||
|
||||
std::string Eval(std::string const& expr)
|
||||
{
|
||||
return cmGeneratorExpression::Evaluate(expr, this->LG.get(), "Debug");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static bool testApplyBasic()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return fx.Eval("$<LIST:TRANSFORM,net;audio,APPLY,$<UPPER_CASE:$<_0>>>") ==
|
||||
"NET;AUDIO";
|
||||
}
|
||||
|
||||
static bool testApplyConstantBody()
|
||||
{
|
||||
// The body need not reference the operand at all.
|
||||
GenExFixture fx;
|
||||
return fx.Eval("$<LIST:TRANSFORM,a;b;c,APPLY,X>") == "X;X;X";
|
||||
}
|
||||
|
||||
static bool testApplyOperandUsedTwice()
|
||||
{
|
||||
// The operand may be substituted more than once in a single body.
|
||||
GenExFixture fx;
|
||||
return fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<_0>$<_0>>") == "aa;bb";
|
||||
}
|
||||
|
||||
static bool testApplyFlatMapExpands()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// A list-valued body expands: a -> "x;y", b -> "z" => 3 items.
|
||||
std::string out =
|
||||
fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<IF:$<STREQUAL:$<_0>,a>,x;y,z>>");
|
||||
std::string len = fx.Eval("$<LIST:LENGTH,$<LIST:TRANSFORM,a;b,APPLY,"
|
||||
"$<IF:$<STREQUAL:$<_0>,a>,x;y,z>>>");
|
||||
return out == "x;y;z" && len == "3";
|
||||
}
|
||||
|
||||
static bool testApplySelector()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return fx.Eval(
|
||||
"$<LIST:TRANSFORM,a;b;c;d,APPLY,$<UPPER_CASE:$<_0>>,AT,0,2>") ==
|
||||
"A;b;C;d";
|
||||
}
|
||||
|
||||
static bool testApplyNestedShadowing()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return fx.Eval(
|
||||
"$<LIST:TRANSFORM,a;b,APPLY,"
|
||||
"$<LIST:TRANSFORM,$<_0>1;$<_0>2,APPLY,$<UPPER_CASE:$<_0>>>>") ==
|
||||
"A1;A2;B1;B2";
|
||||
}
|
||||
|
||||
static bool testApplyEmptyList()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// An empty list produces an empty string without error (matches canned
|
||||
// TRANSFORM behavior).
|
||||
std::string got = fx.Eval("$<LIST:TRANSFORM,,APPLY,x$<_0>y>");
|
||||
if (!got.empty()) {
|
||||
std::cerr << "testApplyEmptyList: expected empty string, got '" << got
|
||||
<< "'\n";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool testApplyEmptyElement()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// Every element is selected, including the leading empty one, so the body
|
||||
// wraps each: "" -> "xy", "b" -> "xby".
|
||||
return fx.Eval("$<LIST:TRANSFORM,;b,APPLY,x$<_0>y>") == "xy;xby";
|
||||
}
|
||||
|
||||
static bool testApplyEmptyPassthrough()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// The list ";b" has two elements: "" (index 0, unselected) and "b"
|
||||
// (index 1, selected). The unselected empty element must pass through
|
||||
// verbatim, not be silently dropped.
|
||||
std::string got = fx.Eval("$<LIST:TRANSFORM,;b,APPLY,X$<_0>Y,AT,1>");
|
||||
if (got != ";XbY") {
|
||||
std::cerr << "testApplyEmptyPassthrough: expected ';XbY', got '" << got
|
||||
<< "'\n";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool testApplyEmptyResult()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// Body yields "" for "a" and "z" for "b". The empty element produced by
|
||||
// the body must not be dropped.
|
||||
std::string got =
|
||||
fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<IF:$<STREQUAL:$<_0>,a>,,z>>");
|
||||
if (got != ";z") {
|
||||
std::cerr << "testApplyEmptyResult: expected ';z', got '" << got << "'\n";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
int testGenExTransformApply(int /*argc*/, char* /*argv*/[])
|
||||
{
|
||||
if (!testApplyBasic()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplyConstantBody()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplyOperandUsedTwice()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplyFlatMapExpands()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplySelector()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplyNestedShadowing()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplyEmptyList()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplyEmptyElement()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplyEmptyPassthrough()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testApplyEmptyResult()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/out.txt" actual)
|
||||
string(STRIP "${actual}" actual)
|
||||
if(NOT actual STREQUAL "NET;AUDIO;VIDEO")
|
||||
set(RunCMake_TEST_FAILED "unexpected APPLY output: [${actual}]")
|
||||
endif()
|
||||
@@ -0,0 +1,3 @@
|
||||
set(input "net;audio;video")
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/out.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,${input},APPLY,$<UPPER_CASE:$<_0>>>\n")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
sub-command TRANSFORM, selector AT expects at least one numeric value
|
||||
@@ -0,0 +1,4 @@
|
||||
# 'AT' with no following index is a malformed selector; APPLY must report the
|
||||
# same diagnostic the canned TRANSFORM actions do.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,a;b,APPLY,$<_0>,AT>")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
index: 9 out of range
|
||||
@@ -0,0 +1,5 @@
|
||||
# A body that errors for an element must fail the whole expression, not be
|
||||
# silently dropped. $<LIST:GET,...,9> is out of range for a single-element
|
||||
# operand.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,a;b,APPLY,$<LIST:GET,$<_0>,9>>")
|
||||
@@ -0,0 +1,6 @@
|
||||
/* A real symbol so the static archive is non-empty (some ranlib versions warn
|
||||
on an archive with no global symbols). */
|
||||
int cmListTransformApplyProvider(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
enable_language(C)
|
||||
|
||||
# `provider` is EXCLUDE_FROM_ALL, so it builds only when another target declares
|
||||
# a build-order dependency on it.
|
||||
add_library(provider STATIC ListTransformApplyDependTarget.c)
|
||||
set_property(TARGET provider PROPERTY EXCLUDE_FROM_ALL 1)
|
||||
|
||||
# `consumer`'s only reference to `provider` is the $<TARGET_FILE:$<_0>> in the
|
||||
# APPLY body. CMake derives the build-order dependency from that reference only
|
||||
# if APPLY propagates the target back out (DependTargets). If that regresses,
|
||||
# `provider` is never built and the copy below fails for lack of the archive, so
|
||||
# a green build of `consumer` alone is the regression guard.
|
||||
add_custom_target(consumer
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy
|
||||
"$<LIST:TRANSFORM,provider,APPLY,$<TARGET_FILE:$<_0>>>"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/copied.out"
|
||||
VERBATIM
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/ll.txt" actual)
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/expected.txt" expected)
|
||||
string(STRIP "${actual}" actual)
|
||||
string(STRIP "${expected}" expected)
|
||||
# APPLY over app's LINK_LIBRARIES must equal the explicit per-library
|
||||
# $<TARGET_FILE_NAME> expansion exactly.
|
||||
if(NOT actual STREQUAL expected)
|
||||
set(RunCMake_TEST_FAILED
|
||||
"APPLY-over-LINK_LIBRARIES output does not match the explicit expansion:\n"
|
||||
" actual: [${actual}]\n"
|
||||
" expected: [${expected}]")
|
||||
endif()
|
||||
@@ -0,0 +1,30 @@
|
||||
enable_language(C)
|
||||
|
||||
# Evaluate LINK_LIBRARIES transitively (CMP0189, CMake 4.1+); the test dir's
|
||||
# cmake_minimum_required would otherwise leave this OLD.
|
||||
cmake_policy(SET CMP0189 NEW)
|
||||
|
||||
# A dependency tree, linked PUBLIC so each library's dependencies propagate
|
||||
# through INTERFACE_LINK_LIBRARIES:
|
||||
# app -> { net, audio }; net -> { ssl, zlib }; audio -> codec
|
||||
add_library(ssl STATIC empty.c)
|
||||
add_library(zlib STATIC empty.c)
|
||||
add_library(codec STATIC empty.c)
|
||||
|
||||
add_library(net STATIC empty.c)
|
||||
target_link_libraries(net PUBLIC ssl zlib)
|
||||
add_library(audio STATIC empty.c)
|
||||
target_link_libraries(audio PUBLIC codec)
|
||||
|
||||
add_library(app STATIC empty.c)
|
||||
target_link_libraries(app PRIVATE net audio)
|
||||
|
||||
# app's LINK_LIBRARIES now evaluates to the transitive closure
|
||||
# (net;audio;ssl;zlib;codec); map each linked library to its on-disk file name.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/ll.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,$<TARGET_PROPERTY:app,LINK_LIBRARIES>,APPLY,$<TARGET_FILE_NAME:$<_0>>>\n")
|
||||
|
||||
# Exact, platform-independent reference: the explicit per-library expansion in
|
||||
# the transitive-closure order. The APPLY output must be byte-identical.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/expected.txt"
|
||||
CONTENT "$<TARGET_FILE_NAME:net>;$<TARGET_FILE_NAME:audio>;$<TARGET_FILE_NAME:ssl>;$<TARGET_FILE_NAME:zlib>;$<TARGET_FILE_NAME:codec>\n")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
sub-command TRANSFORM, action APPLY expects a <body> argument
|
||||
@@ -0,0 +1,3 @@
|
||||
# APPLY with no body argument is an error.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,a;b,APPLY>")
|
||||
@@ -0,0 +1,12 @@
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/nested.txt" actual)
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/expected.txt" expected)
|
||||
string(STRIP "${actual}" actual)
|
||||
string(STRIP "${expected}" expected)
|
||||
# The nested APPLY (outer binds each target, inner uppercases each include dir)
|
||||
# must equal the explicit per-target expansion exactly.
|
||||
if(NOT actual STREQUAL expected)
|
||||
set(RunCMake_TEST_FAILED
|
||||
"nested APPLY output does not match the explicit expansion:\n"
|
||||
" actual: [${actual}]\n"
|
||||
" expected: [${expected}]")
|
||||
endif()
|
||||
@@ -0,0 +1,17 @@
|
||||
add_library(libA INTERFACE)
|
||||
target_include_directories(libA INTERFACE /a/one /a/two)
|
||||
add_library(libB INTERFACE)
|
||||
target_include_directories(libB INTERFACE /b/one)
|
||||
|
||||
# Nested APPLY: the outer APPLY runs over targets; its body is an APPLY over
|
||||
# each target's include directories that uppercases them. The outer $<_0> (a
|
||||
# target) feeds the inner list arg $<TARGET_PROPERTY:...>; the inner $<_0> (a
|
||||
# directory) feeds $<UPPER_CASE> under the inner binding, while the outer
|
||||
# binding stays intact.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/nested.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,libA;libB,APPLY,$<LIST:TRANSFORM,$<TARGET_PROPERTY:$<_0>,INTERFACE_INCLUDE_DIRECTORIES>,APPLY,$<UPPER_CASE:$<_0>>>>\n")
|
||||
|
||||
# Exact reference: the same result without the inner APPLY (uppercase each
|
||||
# target's include dirs directly). The nested APPLY must match it.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/expected.txt"
|
||||
CONTENT "$<UPPER_CASE:$<TARGET_PROPERTY:libA,INTERFACE_INCLUDE_DIRECTORIES>>;$<UPPER_CASE:$<TARGET_PROPERTY:libB,INTERFACE_INCLUDE_DIRECTORIES>>\n")
|
||||
@@ -0,0 +1,7 @@
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/tp.txt" actual)
|
||||
string(STRIP "${actual}" actual)
|
||||
# flatMap: libA -> /a/inc (1 dir), libB -> /b/inc1;/b/inc2 (2 dirs) => 3 items.
|
||||
if(NOT actual STREQUAL "/a/inc;/b/inc1;/b/inc2")
|
||||
set(RunCMake_TEST_FAILED
|
||||
"unexpected APPLY target-property output: [${actual}]")
|
||||
endif()
|
||||
@@ -0,0 +1,6 @@
|
||||
add_library(libA INTERFACE)
|
||||
target_include_directories(libA INTERFACE /a/inc)
|
||||
add_library(libB INTERFACE)
|
||||
target_include_directories(libB INTERFACE /b/inc1 /b/inc2)
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/tp.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,libA;libB,APPLY,$<TARGET_PROPERTY:$<_0>,INTERFACE_INCLUDE_DIRECTORIES>>\n")
|
||||
@@ -57,6 +57,13 @@ run_cmake(FILTER-InvalidOperator)
|
||||
run_cmake(FILTER-Exclude)
|
||||
run_cmake(FILTER-Include)
|
||||
run_cmake(LIST-edgecases)
|
||||
run_cmake(ListTransformApply)
|
||||
run_cmake(ListTransformApplyTargetProperty)
|
||||
run_cmake(ListTransformApplyLinkLibraries)
|
||||
run_cmake(ListTransformApplyNested)
|
||||
run_cmake(ListTransformApplyBadSelector)
|
||||
run_cmake(ListTransformApplyBodyError)
|
||||
run_cmake(ListTransformApplyMissingBody)
|
||||
run_cmake(BoundOperandOutsideBinding)
|
||||
|
||||
function(run_cmake_build test)
|
||||
@@ -151,6 +158,8 @@ unset(RunCMake_TEST_OPTIONS)
|
||||
|
||||
run_cmake_build_target(COMPILE_ONLY-custom-target-deps consumer)
|
||||
|
||||
run_cmake_build_target(ListTransformApplyDependTarget consumer)
|
||||
|
||||
run_cmake(CMP0199-WARN)
|
||||
run_cmake_build(CMP0199-OLD)
|
||||
run_cmake_build(CMP0199-NEW)
|
||||
|
||||
Reference in New Issue
Block a user