From caa51f56896b83e9cd9c18a806c7a3635ad3b26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Germain?= Date: Wed, 17 Jun 2026 20:00:13 -0700 Subject: [PATCH 1/3] GenEx: factor TRANSFORM selector parsing and selection Give the AT/FOR/REGEX selector syntax shared by the $ actions a single definition, so its diagnostics and index/REGEX semantics cannot drift between actions. Also let a caller learn which elements a selector picks without running a transform, as groundwork for actions that drive their own per-element loop. Issue: #27892 --- Source/cmGeneratorExpressionNode.cxx | 247 +++++++++++++-------------- Source/cmList.cxx | 30 ++++ Source/cmList.h | 5 + 3 files changed, 155 insertions(+), 127 deletions(-) diff --git a/Source/cmGeneratorExpressionNode.cxx b/Source/cmGeneratorExpressionNode.cxx index 22b45bb601..4e8e15d00c 100644 --- a/Source/cmGeneratorExpressionNode.cxx +++ b/Source/cmGeneratorExpressionNode.cxx @@ -1858,6 +1858,122 @@ inline cmList GetList(std::string const& list) { return list.empty() ? cmList{} : cmList{ list, cmList::EmptyElements::Yes }; } + +// Parse the optional trailing selector of a $ action +// (AT ... / FOR [] / REGEX ) into a +// cmList::TransformSelector. Returns nullptr (after reporting via `eval`) on +// a malformed selector; empty `tokens` yields a select-all selector. +std::unique_ptr ParseTransformSelector( + std::vector const& tokens, cm::GenEx::Evaluation* eval, + GeneratorExpressionContent const* content) +{ + static std::string const REGEX{ "REGEX" }; + static std::string const AT{ "AT" }; + static std::string const FOR{ "FOR" }; + + std::unique_ptr selector; + + std::size_t i = 0; + while (i < tokens.size()) { + std::string const& tok = tokens[i]; + + if ((tok == REGEX || tok == AT || tok == FOR) && selector) { + reportError( + eval, content->GetOriginalExpression(), + cmStrCat("sub-command TRANSFORM, selector already specified (", + selector->GetTag(), ").")); + return nullptr; + } + + if (tok == REGEX) { + if (i + 1 >= tokens.size()) { + reportError(eval, content->GetOriginalExpression(), + "sub-command TRANSFORM, selector REGEX expects " + "'regular expression' argument."); + return nullptr; + } + selector = + cmList::TransformSelector::New( + tokens[i + 1]); + i += 2; + continue; + } + + if (tok == AT) { + ++i; + std::vector indexes; + for (; i < tokens.size(); ++i) { + cmList indexList{ tokens[i] }; + for (auto const& index : indexList) { + cmList::index_type value; + if (!GetNumericArgument(index, value)) { + reportError(eval, content->GetOriginalExpression(), + cmStrCat("sub-command TRANSFORM, selector AT: '", + index, "': unexpected argument.")); + return nullptr; + } + indexes.push_back(value); + } + } + if (indexes.empty()) { + reportError(eval, content->GetOriginalExpression(), + "sub-command TRANSFORM, selector AT expects at least one " + "numeric value."); + return nullptr; + } + selector = cmList::TransformSelector::New( + std::move(indexes)); + continue; + } + + if (tok == FOR) { + if (i + 2 >= tokens.size()) { + reportError(eval, content->GetOriginalExpression(), + "sub-command TRANSFORM, selector FOR expects, at least, " + "two arguments."); + return nullptr; + } + cmList::index_type start = 0; + cmList::index_type stop = 0; + cmList::index_type step = 1; + if (!GetNumericArgument(tokens[i + 1], start) || + !GetNumericArgument(tokens[i + 2], stop)) { + reportError(eval, content->GetOriginalExpression(), + "sub-command TRANSFORM, selector FOR expects, at least, " + "two numeric values."); + return nullptr; + } + i += 3; + if (i < tokens.size()) { + if (!GetNumericArgument(tokens[i], step)) { + step = -1; + } + ++i; + } + if (step <= 0) { + reportError(eval, content->GetOriginalExpression(), + "sub-command TRANSFORM, selector FOR expects positive " + "numeric value for ."); + return nullptr; + } + selector = + cmList::TransformSelector::New( + { start, stop, step }); + continue; + } + + std::vector const rest(tokens.begin() + i, tokens.end()); + reportError(eval, content->GetOriginalExpression(), + cmStrCat("sub-command TRANSFORM, '", cmJoin(rest, ", "), + "': unexpected argument(s).")); + return nullptr; + } + + if (!selector) { + selector = cmList::TransformSelector::New(); + } + return selector; +} } static const struct ListNode : public cmGeneratorExpressionNode @@ -2177,137 +2293,14 @@ static const struct ListNode : public cmGeneratorExpressionNode args.advance(descriptor->Arity); } - std::string const REGEX{ "REGEX" }; - std::string const AT{ "AT" }; - std::string const FOR{ "FOR" }; std::unique_ptr selector; try { - // handle optional arguments - while (!args.empty()) { - if ((args.front() == REGEX || args.front() == AT || - args.front() == FOR) && - selector) { - reportError(ev, cnt->GetOriginalExpression(), - cmStrCat("sub-command TRANSFORM, selector " - "already specified (", - selector->GetTag(), ").")); - - return std::string{}; - } - - // REGEX selector - if (args.front() == REGEX) { - if (args.advance(1).empty()) { - reportError( - ev, cnt->GetOriginalExpression(), - "sub-command TRANSFORM, selector REGEX expects " - "'regular expression' argument."); - return std::string{}; - } - - selector = cmList::TransformSelector::New< - cmList::TransformSelector::REGEX>(args.front()); - - args.advance(1); - continue; - } - - // AT selector - if (args.front() == AT) { - args.advance(1); - // get all specified indexes - std::vector indexes; - while (!args.empty()) { - cmList indexList{ args.front() }; - for (auto const& index : indexList) { - cmList::index_type value; - - if (!GetNumericArgument(index, value)) { - // this is not a number, stop processing - reportError( - ev, cnt->GetOriginalExpression(), - cmStrCat("sub-command TRANSFORM, selector AT: '", - index, "': unexpected argument.")); - return std::string{}; - } - indexes.push_back(value); - } - args.advance(1); - } - - if (indexes.empty()) { - reportError(ev, cnt->GetOriginalExpression(), - "sub-command TRANSFORM, selector AT " - "expects at least one " - "numeric value."); - return std::string{}; - } - - selector = cmList::TransformSelector::New< - cmList::TransformSelector::AT>(std::move(indexes)); - - continue; - } - - // FOR selector - if (args.front() == FOR) { - if (args.advance(1).size() < 2) { - reportError(ev, cnt->GetOriginalExpression(), - "sub-command TRANSFORM, selector FOR " - "expects, at least," - " two arguments."); - return std::string{}; - } - - cmList::index_type start = 0; - cmList::index_type stop = 0; - cmList::index_type step = 1; - bool valid = false; - - if (GetNumericArgument(args.front(), start) && - GetNumericArgument(args.advance(1).front(), stop)) { - valid = true; - } - - if (!valid) { - reportError( - ev, cnt->GetOriginalExpression(), - "sub-command TRANSFORM, selector FOR expects, " - "at least, two numeric values."); - return std::string{}; - } - // try to read a third numeric value for step - if (!args.advance(1).empty()) { - if (!GetNumericArgument(args.front(), step)) { - // this is not a number - step = -1; - } - args.advance(1); - } - - if (step <= 0) { - reportError( - ev, cnt->GetOriginalExpression(), - "sub-command TRANSFORM, selector FOR expects " - "positive numeric value for ."); - return std::string{}; - } - - selector = cmList::TransformSelector::New< - cmList::TransformSelector::FOR>({ start, stop, step }); - continue; - } - - reportError(ev, cnt->GetOriginalExpression(), - cmStrCat("sub-command TRANSFORM, '", - cmJoin(args, ", "), - "': unexpected argument(s).")); - return std::string{}; - } - + std::vector const tokens(args.begin(), + args.end()); + selector = ParseTransformSelector(tokens, ev, cnt); if (!selector) { - selector = cmList::TransformSelector::New(); + return std::string{}; } selector->Makefile = ev->Context.LG->GetMakefile(); diff --git a/Source/cmList.cxx b/Source/cmList.cxx index 754a3d3f8b..a5504090a5 100644 --- a/Source/cmList.cxx +++ b/Source/cmList.cxx @@ -437,6 +437,17 @@ public: std::transform(list.begin(), list.end(), list.begin(), transform); } + // Return, for each element, whether the selector selects it via InSelection. + virtual std::vector Selection(cmList::container_type const& list) + { + std::vector selected; + selected.reserve(list.size()); + for (auto const& value : list) { + selected.push_back(this->InSelection(value)); + } + return selected; + } + protected: TransformSelector(std::string&& tag) : Tag(std::move(tag)) @@ -516,6 +527,19 @@ public: } } + // Select the computed Indexes; Validate throws transform_error on an + // out-of-range index. + std::vector Selection(cmList::container_type const& list) override + { + this->Validate(list.size()); + + std::vector selected(list.size(), false); + for (auto index : this->Indexes) { + selected[index] = true; + } + return selected; + } + protected: TransformSelectorIndexes(std::string&& tag) : TransformSelector(std::move(tag)) @@ -1148,6 +1172,12 @@ cmList& cmList::transform(TransformAction action, std::string const& arg, return *this; } +std::vector cmList::GetTransformSelection( + cmList::TransformSelector& selector) const +{ + return static_cast<::TransformSelector&>(selector).Selection(this->Values); +} + std::string& cmList::append(std::string& list, std::string&& value) { if (list.empty()) { diff --git a/Source/cmList.h b/Source/cmList.h index cccfb2c793..d8bc4cc912 100644 --- a/Source/cmList.h +++ b/Source/cmList.h @@ -975,6 +975,11 @@ public: cmMakefile& makefile, std::unique_ptr = {}); + // Return, for each element of this list, whether `selector` selects it. + // Throws transform_error on a malformed selector (e.g. an out-of-range + // index), like transform(). + std::vector GetTransformSelection(TransformSelector& selector) const; + std::string join(cm::string_view glue) const { return cmList::Join(this->Values, glue); From c0a0b7fdd92487ee45248994d0b34cf77b321b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Germain?= Date: Wed, 17 Jun 2026 20:20:34 -0700 Subject: [PATCH 2/3] GenEx: add bound-operand binding mechanism and $<_0> Introduce "binding operations": generator expressions that evaluate a once for each value they supply, with $<_0> expanding to that value. This is the foundation the $ action and the predicate selectors build on, letting a refer to the element being processed. Using $<_0> outside a binding operation is reported as an error rather than silently expanding to nothing. Issue: #27892 --- Help/manual/cmake-generator-expressions.7.rst | 16 ++++++++++ Source/cmGenExContext.h | 18 +++++++++++ Source/cmGeneratorExpressionEvaluator.h | 6 ++++ Source/cmGeneratorExpressionNode.cxx | 22 ++++++++++++++ Tests/CMakeLib/CMakeLists.txt | 1 + Tests/CMakeLib/testGenExBoundOperand.cxx | 30 +++++++++++++++++++ .../BoundOperandOutsideBinding-result.txt | 1 + .../BoundOperandOutsideBinding-stderr.txt | 1 + .../BoundOperandOutsideBinding.cmake | 1 + .../GeneratorExpression/RunCMakeTest.cmake | 1 + 10 files changed, 97 insertions(+) create mode 100644 Tests/CMakeLib/testGenExBoundOperand.cxx create mode 100644 Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding-result.txt create mode 100644 Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding-stderr.txt create mode 100644 Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding.cmake diff --git a/Help/manual/cmake-generator-expressions.7.rst b/Help/manual/cmake-generator-expressions.7.rst index 51ed27e171..732c35017c 100644 --- a/Help/manual/cmake-generator-expressions.7.rst +++ b/Help/manual/cmake-generator-expressions.7.rst @@ -1048,6 +1048,22 @@ List Ordering $ +.. _GenEx Bound Operands: + +Bound Operands +^^^^^^^^^^^^^^ + +.. genex:: $<_0> + + .. versionadded:: 4.5 + + ``$<_0>`` is the *bound operand* of the enclosing *binding operation*: a + generator expression that evaluates a ```` once for each value it + supplies, with ``$<_0>`` expanding to that value. + + ``$<_0>`` is only valid inside the body of a binding operation. Using it + anywhere else is an error. + Path Expressions ---------------- diff --git a/Source/cmGenExContext.h b/Source/cmGenExContext.h index 85c8ab2a29..14f6fa20b0 100644 --- a/Source/cmGenExContext.h +++ b/Source/cmGenExContext.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include @@ -25,9 +26,26 @@ struct Context final void SetCMP0189(cmPolicies::PolicyStatus cmp0189); cmPolicies::PolicyStatus GetCMP0189() const; + void SetBoundOperand(std::string value); + bool HasBoundOperand() const; + std::string const& GetBoundOperand() const; + private: cm::optional CMP0189; + cm::optional BoundOperand; }; +inline void Context::SetBoundOperand(std::string value) +{ + this->BoundOperand = std::move(value); +} +inline bool Context::HasBoundOperand() const +{ + return this->BoundOperand.has_value(); +} +inline std::string const& Context::GetBoundOperand() const +{ + return *this->BoundOperand; +} } } diff --git a/Source/cmGeneratorExpressionEvaluator.h b/Source/cmGeneratorExpressionEvaluator.h index c71c33862a..a40efc1d1f 100644 --- a/Source/cmGeneratorExpressionEvaluator.h +++ b/Source/cmGeneratorExpressionEvaluator.h @@ -97,6 +97,12 @@ struct GeneratorExpressionContent : public cmGeneratorExpressionEvaluator std::string GetOriginalExpression() const; + std::vector const& GetParamChildren() + const + { + return this->ParamChildren; + } + ~GeneratorExpressionContent() override; private: diff --git a/Source/cmGeneratorExpressionNode.cxx b/Source/cmGeneratorExpressionNode.cxx index 4e8e15d00c..e2dbce8ece 100644 --- a/Source/cmGeneratorExpressionNode.cxx +++ b/Source/cmGeneratorExpressionNode.cxx @@ -141,6 +141,27 @@ static const struct OneNode : public cmGeneratorExpressionNode } } oneNode; +static const struct BoundOperandNode : public cmGeneratorExpressionNode +{ + BoundOperandNode() {} // NOLINT(modernize-use-equals-default) + + int NumExpectedParameters() const override { return 0; } + + std::string Evaluate( + std::vector const& /*parameters*/, + cm::GenEx::Evaluation* eval, GeneratorExpressionContent const* content, + cmGeneratorExpressionDAGChecker* /*dagChecker*/) const override + { + if (!eval->Context.HasBoundOperand()) { + reportError(eval, content->GetOriginalExpression(), + "$<_0> may only be used inside the body of a binding " + "operation."); + return std::string(); + } + return eval->Context.GetBoundOperand(); + } +} boundOperandNode; + static const struct OneNode buildInterfaceNode; static const struct ZeroNode installInterfaceNode; @@ -5932,6 +5953,7 @@ cmGeneratorExpressionNode const* cmGeneratorExpressionNode::GetNode( { "PATH_EQUAL", &pathEqualNode }, { "MAKE_C_IDENTIFIER", &makeCIdentifierNode }, { "BOOL", &boolNode }, + { "_0", &boundOperandNode }, { "IF", &ifNode }, { "ANGLE-R", &angle_rNode }, { "COMMA", &commaNode }, diff --git a/Tests/CMakeLib/CMakeLists.txt b/Tests/CMakeLib/CMakeLists.txt index 959cf3c2bb..19ac5a4318 100644 --- a/Tests/CMakeLib/CMakeLists.txt +++ b/Tests/CMakeLib/CMakeLists.txt @@ -14,6 +14,7 @@ set(CMakeLib_TESTS testDocumentationFormatter.cxx testGccDepfileReader.cxx testGeneratedFileStream.cxx + testGenExBoundOperand.cxx testJSONHelpers.cxx testRST.cxx testRange.cxx diff --git a/Tests/CMakeLib/testGenExBoundOperand.cxx b/Tests/CMakeLib/testGenExBoundOperand.cxx new file mode 100644 index 0000000000..aa2380e6a1 --- /dev/null +++ b/Tests/CMakeLib/testGenExBoundOperand.cxx @@ -0,0 +1,30 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#include +#include + +#include "cmGenExContext.h" + +static bool testContextBinding() +{ + cm::GenEx::Context ctx(nullptr, "Debug"); + bool ok = true; + if (ctx.HasBoundOperand()) { + std::cerr << "binding should start unset\n"; + ok = false; + } + ctx.SetBoundOperand("net"); + if (!ctx.HasBoundOperand() || ctx.GetBoundOperand() != "net") { + std::cerr << "binding did not round-trip\n"; + ok = false; + } + return ok; +} + +int testGenExBoundOperand(int /*argc*/, char* /*argv*/[]) +{ + if (!testContextBinding()) { + return 1; + } + return 0; +} diff --git a/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding-result.txt b/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding-stderr.txt b/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding-stderr.txt new file mode 100644 index 0000000000..c64a189ee1 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding-stderr.txt @@ -0,0 +1 @@ +\$<_0> may only be used inside the body of a binding operation diff --git a/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding.cmake b/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding.cmake new file mode 100644 index 0000000000..74277b00b3 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/BoundOperandOutsideBinding.cmake @@ -0,0 +1 @@ +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/bad.txt" CONTENT "$<_0>") diff --git a/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake b/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake index c826e5da99..2cfa99f5bd 100644 --- a/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake +++ b/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake @@ -57,6 +57,7 @@ run_cmake(FILTER-InvalidOperator) run_cmake(FILTER-Exclude) run_cmake(FILTER-Include) run_cmake(LIST-edgecases) +run_cmake(BoundOperandOutsideBinding) function(run_cmake_build test) set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build) From 0f6e8ded1d5efbb91ae741862f9090796ad649cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Germain?= Date: Wed, 17 Jun 2026 20:21:24 -0700 Subject: [PATCH 3/3] GenEx: add $ action Add an APPLY action to $ that evaluates an arbitrary 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 ) 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 --- Help/manual/cmake-generator-expressions.7.rst | 20 ++ .../dev/genex-list-transform-apply.rst | 6 + Source/cmGeneratorExpressionNode.cxx | 107 ++++++++++- Tests/CMakeLib/CMakeLists.txt | 1 + Tests/CMakeLib/testGenExTransformApply.cxx | 174 ++++++++++++++++++ .../ListTransformApply-check.cmake | 5 + .../ListTransformApply.cmake | 3 + .../ListTransformApplyBadSelector-result.txt | 1 + .../ListTransformApplyBadSelector-stderr.txt | 1 + .../ListTransformApplyBadSelector.cmake | 4 + .../ListTransformApplyBodyError-result.txt | 1 + .../ListTransformApplyBodyError-stderr.txt | 1 + .../ListTransformApplyBodyError.cmake | 5 + .../ListTransformApplyDependTarget.c | 6 + .../ListTransformApplyDependTarget.cmake | 18 ++ ...istTransformApplyLinkLibraries-check.cmake | 12 ++ .../ListTransformApplyLinkLibraries.cmake | 30 +++ .../ListTransformApplyMissingBody-result.txt | 1 + .../ListTransformApplyMissingBody-stderr.txt | 1 + .../ListTransformApplyMissingBody.cmake | 3 + .../ListTransformApplyNested-check.cmake | 12 ++ .../ListTransformApplyNested.cmake | 17 ++ ...stTransformApplyTargetProperty-check.cmake | 7 + .../ListTransformApplyTargetProperty.cmake | 6 + .../GeneratorExpression/RunCMakeTest.cmake | 9 + 25 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 Help/release/dev/genex-list-transform-apply.rst create mode 100644 Tests/CMakeLib/testGenExTransformApply.cxx create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApply-check.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApply.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector-result.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector-stderr.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError-result.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError-stderr.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyDependTarget.c create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyDependTarget.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyLinkLibraries-check.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyLinkLibraries.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody-result.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody-stderr.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyNested-check.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyNested.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyTargetProperty-check.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListTransformApplyTargetProperty.cmake diff --git a/Help/manual/cmake-generator-expressions.7.rst b/Help/manual/cmake-generator-expressions.7.rst index 732c35017c..2626d6a89e 100644 --- a/Help/manual/cmake-generator-expressions.7.rst +++ b/Help/manual/cmake-generator-expressions.7.rst @@ -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 ```` once per element. Within ````, the bound + operand ``$<_0>`` expands to the current element. Unlike the + configure-time ``list(TRANSFORM ... APPLY )`` 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 + + $ + + 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 ```` once for each value it supplies, with ``$<_0>`` expanding to that value. + For example, ``$`` 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. diff --git a/Help/release/dev/genex-list-transform-apply.rst b/Help/release/dev/genex-list-transform-apply.rst new file mode 100644 index 0000000000..23a5da90f1 --- /dev/null +++ b/Help/release/dev/genex-list-transform-apply.rst @@ -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. diff --git a/Source/cmGeneratorExpressionNode.cxx b/Source/cmGeneratorExpressionNode.cxx index e2dbce8ece..fb15464b4b 100644 --- a/Source/cmGeneratorExpressionNode.cxx +++ b/Source/cmGeneratorExpressionNode.cxx @@ -107,6 +107,48 @@ std::string cmGeneratorExpressionNode::EvaluateDependentExpression( return result; } +// Re-evaluate the unevaluated 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 const& parameters, + std::string&) const override + { + // Skip the APPLY (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 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 argument."); + return std::string(); + } + cmList list = GetList(parameters[1]); + if (list.empty()) { + return std::string{}; + } + cmGeneratorExpressionEvaluatorVector const& bodyExpr = + content->GetParamChildren()[3]; + std::vector const selectorTokens(parameters.begin() + 4, + parameters.end()); + std::unique_ptr 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 selected; + try { + selected = list.GetTransformSelection(*selector); + } catch (cmList::transform_error& e) { + reportError(eval, content->GetOriginalExpression(), e.what()); + return std::string(); + } + + std::vector 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 +#include + +#include + +#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 GG; + std::unique_ptr MF; + std::unique_ptr LG; + + GenExFixture() + { + this->GG = cm::make_unique(&this->CMake); + cmStateSnapshot snapshot = this->CMake.GetCurrentSnapshot(); + snapshot.GetDirectory().SetCurrentBinary("."); + snapshot.GetDirectory().SetCurrentSource("."); + this->MF = cm::make_unique(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("$>>") == + "NET;AUDIO"; +} + +static bool testApplyConstantBody() +{ + // The body need not reference the operand at all. + GenExFixture fx; + return fx.Eval("$") == "X;X;X"; +} + +static bool testApplyOperandUsedTwice() +{ + // The operand may be substituted more than once in a single body. + GenExFixture fx; + return fx.Eval("$$<_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("$,a>,x;y,z>>"); + std::string len = fx.Eval("$,a>,x;y,z>>>"); + return out == "x;y;z" && len == "3"; +} + +static bool testApplySelector() +{ + GenExFixture fx; + return fx.Eval( + "$>,AT,0,2>") == + "A;b;C;d"; +} + +static bool testApplyNestedShadowing() +{ + GenExFixture fx; + return fx.Eval( + "$1;$<_0>2,APPLY,$>>>") == + "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("$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("$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("$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("$,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; +} diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApply-check.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApply-check.cmake new file mode 100644 index 0000000000..9d7c8a3ddc --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApply-check.cmake @@ -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() diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApply.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApply.cmake new file mode 100644 index 0000000000..d596258de7 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApply.cmake @@ -0,0 +1,3 @@ +set(input "net;audio;video") +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/out.txt" + CONTENT "$>>\n") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector-result.txt b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector-stderr.txt b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector-stderr.txt new file mode 100644 index 0000000000..341859db05 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector-stderr.txt @@ -0,0 +1 @@ +sub-command TRANSFORM, selector AT expects at least one numeric value diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector.cmake new file mode 100644 index 0000000000..693f7a7109 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBadSelector.cmake @@ -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 "$,AT>") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError-result.txt b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError-stderr.txt b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError-stderr.txt new file mode 100644 index 0000000000..155a0321a1 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError-stderr.txt @@ -0,0 +1 @@ +index: 9 out of range diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError.cmake new file mode 100644 index 0000000000..5d60e58e9b --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyBodyError.cmake @@ -0,0 +1,5 @@ +# A body that errors for an element must fail the whole expression, not be +# silently dropped. $ is out of range for a single-element +# operand. +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt" + CONTENT "$,9>>") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyDependTarget.c b/Tests/RunCMake/GeneratorExpression/ListTransformApplyDependTarget.c new file mode 100644 index 0000000000..24ed223e10 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyDependTarget.c @@ -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; +} diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyDependTarget.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyDependTarget.cmake new file mode 100644 index 0000000000..182c5494d0 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyDependTarget.cmake @@ -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 $> 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 + "$>>" + "${CMAKE_CURRENT_BINARY_DIR}/copied.out" + VERBATIM +) diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyLinkLibraries-check.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyLinkLibraries-check.cmake new file mode 100644 index 0000000000..c4f1c24fa8 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyLinkLibraries-check.cmake @@ -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 +# $ 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() diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyLinkLibraries.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyLinkLibraries.cmake new file mode 100644 index 0000000000..50190adcd1 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyLinkLibraries.cmake @@ -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 "$,APPLY,$>>\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 "$;$;$;$;$\n") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody-result.txt b/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody-stderr.txt b/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody-stderr.txt new file mode 100644 index 0000000000..3a4e333c16 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody-stderr.txt @@ -0,0 +1 @@ +sub-command TRANSFORM, action APPLY expects a argument diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody.cmake new file mode 100644 index 0000000000..5c0c96f36c --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyMissingBody.cmake @@ -0,0 +1,3 @@ +# APPLY with no body argument is an error. +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt" + CONTENT "$") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyNested-check.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyNested-check.cmake new file mode 100644 index 0000000000..added95814 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyNested-check.cmake @@ -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() diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyNested.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyNested.cmake new file mode 100644 index 0000000000..c61474bf86 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyNested.cmake @@ -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 $; the inner $<_0> (a +# directory) feeds $ under the inner binding, while the outer +# binding stays intact. +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/nested.txt" + CONTENT "$,INTERFACE_INCLUDE_DIRECTORIES>,APPLY,$>>>\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 "$>;$>\n") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyTargetProperty-check.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyTargetProperty-check.cmake new file mode 100644 index 0000000000..583cd2e0f9 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyTargetProperty-check.cmake @@ -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() diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformApplyTargetProperty.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformApplyTargetProperty.cmake new file mode 100644 index 0000000000..a6155663cc --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformApplyTargetProperty.cmake @@ -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 "$,INTERFACE_INCLUDE_DIRECTORIES>>\n") diff --git a/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake b/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake index 2cfa99f5bd..d024ab7e34 100644 --- a/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake +++ b/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake @@ -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)