mirror of
https://github.com/Kitware/CMake.git
synced 2026-06-24 08:47:59 +00:00
Merge topic 'genex-list-transform-apply'
0f6e8ded1dGenEx: add $<LIST:TRANSFORM,...,APPLY,body> actionc0a0b7fdd9GenEx: add bound-operand binding mechanism and $<_0>caa51f5689GenEx: factor TRANSFORM selector parsing and selection Acked-by: Kitware Robot <kwrobot@kitware.com> Tested-by: buildbot <buildbot@kitware.com> Merge-request: !12203
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:
|
||||
@@ -1048,6 +1065,25 @@ List Ordering
|
||||
|
||||
$<LIST:SORT,list,CASE:SENSITIVE,COMPARE:STRING,ORDER:DESCENDING>
|
||||
|
||||
.. _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 ``<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.
|
||||
|
||||
Path Expressions
|
||||
----------------
|
||||
|
||||
|
||||
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.
|
||||
@@ -3,6 +3,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <cm/optional>
|
||||
|
||||
@@ -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<cmPolicies::PolicyStatus> CMP0189;
|
||||
cm::optional<std::string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,12 @@ struct GeneratorExpressionContent : public cmGeneratorExpressionEvaluator
|
||||
|
||||
std::string GetOriginalExpression() const;
|
||||
|
||||
std::vector<cmGeneratorExpressionEvaluatorVector> const& GetParamChildren()
|
||||
const
|
||||
{
|
||||
return this->ParamChildren;
|
||||
}
|
||||
|
||||
~GeneratorExpressionContent() override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -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)
|
||||
@@ -141,6 +183,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<std::string> 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;
|
||||
@@ -1858,6 +1921,122 @@ inline cmList GetList(std::string const& list)
|
||||
{
|
||||
return list.empty() ? cmList{} : cmList{ list, cmList::EmptyElements::Yes };
|
||||
}
|
||||
|
||||
// Parse the optional trailing selector of a $<LIST:TRANSFORM,...> action
|
||||
// (AT <i>... / FOR <start> <stop> [<step>] / REGEX <re>) into a
|
||||
// cmList::TransformSelector. Returns nullptr (after reporting via `eval`) on
|
||||
// a malformed selector; empty `tokens` yields a select-all selector.
|
||||
std::unique_ptr<cmList::TransformSelector> ParseTransformSelector(
|
||||
std::vector<std::string> 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<cmList::TransformSelector> 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<cmList::TransformSelector::REGEX>(
|
||||
tokens[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tok == AT) {
|
||||
++i;
|
||||
std::vector<cmList::index_type> 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<cmList::TransformSelector::AT>(
|
||||
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 <step>.");
|
||||
return nullptr;
|
||||
}
|
||||
selector =
|
||||
cmList::TransformSelector::New<cmList::TransformSelector::FOR>(
|
||||
{ start, stop, step });
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::string> 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
|
||||
@@ -1868,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*,
|
||||
@@ -2177,137 +2419,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<cmList::TransformSelector> 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<cmList::index_type> 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 <step>.");
|
||||
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<std::string> 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();
|
||||
|
||||
@@ -5939,6 +6058,7 @@ cmGeneratorExpressionNode const* cmGeneratorExpressionNode::GetNode(
|
||||
{ "PATH_EQUAL", &pathEqualNode },
|
||||
{ "MAKE_C_IDENTIFIER", &makeCIdentifierNode },
|
||||
{ "BOOL", &boolNode },
|
||||
{ "_0", &boundOperandNode },
|
||||
{ "IF", &ifNode },
|
||||
{ "ANGLE-R", &angle_rNode },
|
||||
{ "COMMA", &commaNode },
|
||||
|
||||
@@ -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<bool> Selection(cmList::container_type const& list)
|
||||
{
|
||||
std::vector<bool> 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<bool> Selection(cmList::container_type const& list) override
|
||||
{
|
||||
this->Validate(list.size());
|
||||
|
||||
std::vector<bool> 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<bool> 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()) {
|
||||
|
||||
@@ -975,6 +975,11 @@ public:
|
||||
cmMakefile& makefile,
|
||||
std::unique_ptr<TransformSelector> = {});
|
||||
|
||||
// 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<bool> GetTransformSelection(TransformSelector& selector) const;
|
||||
|
||||
std::string join(cm::string_view glue) const
|
||||
{
|
||||
return cmList::Join(this->Values, glue);
|
||||
|
||||
@@ -14,6 +14,8 @@ set(CMakeLib_TESTS
|
||||
testDocumentationFormatter.cxx
|
||||
testGccDepfileReader.cxx
|
||||
testGeneratedFileStream.cxx
|
||||
testGenExBoundOperand.cxx
|
||||
testGenExTransformApply.cxx
|
||||
testJSONHelpers.cxx
|
||||
testRST.cxx
|
||||
testRange.cxx
|
||||
|
||||
30
Tests/CMakeLib/testGenExBoundOperand.cxx
Normal file
30
Tests/CMakeLib/testGenExBoundOperand.cxx
Normal file
@@ -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 <iostream>
|
||||
#include <string>
|
||||
|
||||
#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;
|
||||
}
|
||||
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 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
\$<_0> may only be used inside the body of a binding operation
|
||||
@@ -0,0 +1 @@
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/bad.txt" CONTENT "$<_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,14 @@ 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)
|
||||
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)
|
||||
@@ -150,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