/src/CMake/Source/cmForEachCommand.cxx
Line | Count | Source |
1 | | /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
2 | | file LICENSE.rst or https://cmake.org/licensing for details. */ |
3 | | #include "cmForEachCommand.h" |
4 | | |
5 | | #include <algorithm> |
6 | | #include <cassert> |
7 | | #include <cstddef> // IWYU pragma: keep |
8 | | // NOTE The declaration of `std::abs` has moved to `cmath` since C++17 |
9 | | // See https://en.cppreference.com/w/cpp/numeric/math/abs |
10 | | // ALERT But IWYU used to lint `#include`s do not "understand" |
11 | | // conditional compilation (i.e. `#if __cplusplus >= 201703L`) |
12 | | #include <cstdlib> |
13 | | #include <iterator> |
14 | | #include <map> |
15 | | #include <stdexcept> |
16 | | #include <utility> |
17 | | |
18 | | #include <cm/memory> |
19 | | #include <cm/optional> |
20 | | #include <cm/string_view> |
21 | | #include <cmext/string_view> |
22 | | |
23 | | #include "cmExecutionStatus.h" |
24 | | #include "cmFunctionBlocker.h" |
25 | | #include "cmList.h" |
26 | | #include "cmListFileCache.h" |
27 | | #include "cmMakefile.h" |
28 | | #include "cmMessageType.h" |
29 | | #include "cmPolicies.h" |
30 | | #include "cmRange.h" |
31 | | #include "cmStringAlgorithms.h" |
32 | | #include "cmSystemTools.h" |
33 | | #include "cmValue.h" |
34 | | |
35 | | namespace { |
36 | | class cmForEachFunctionBlocker : public cmFunctionBlocker |
37 | | { |
38 | | public: |
39 | | explicit cmForEachFunctionBlocker(cmMakefile* mf); |
40 | | ~cmForEachFunctionBlocker() override; |
41 | | |
42 | 0 | cm::string_view StartCommandName() const override { return "foreach"_s; } |
43 | 0 | cm::string_view EndCommandName() const override { return "endforeach"_s; } |
44 | | |
45 | | bool ArgumentsMatch(cmListFileFunction const& lff, |
46 | | cmMakefile& mf) const override; |
47 | | |
48 | | bool Replay(std::vector<cmListFileFunction> functions, |
49 | | cmExecutionStatus& inStatus) override; |
50 | | |
51 | | void SetIterationVarsCount(std::size_t const varsCount) |
52 | 0 | { |
53 | 0 | this->IterationVarsCount = varsCount; |
54 | 0 | } |
55 | 0 | void SetZipLists() { this->ZipLists = true; } |
56 | | |
57 | | std::vector<std::string> Args; |
58 | | |
59 | | private: |
60 | | struct InvokeResult |
61 | | { |
62 | | bool Restore; |
63 | | bool Break; |
64 | | }; |
65 | | |
66 | | bool ReplayItems(std::vector<cmListFileFunction> const& functions, |
67 | | cmExecutionStatus& inStatus); |
68 | | |
69 | | bool ReplayZipLists(std::vector<cmListFileFunction> const& functions, |
70 | | cmExecutionStatus& inStatus); |
71 | | |
72 | | InvokeResult invoke(std::vector<cmListFileFunction> const& functions, |
73 | | cmExecutionStatus& inStatus, cmMakefile& mf); |
74 | | |
75 | | cmMakefile* Makefile; |
76 | | std::size_t IterationVarsCount = 0u; |
77 | | bool ZipLists = false; |
78 | | }; |
79 | | |
80 | | cmForEachFunctionBlocker::cmForEachFunctionBlocker(cmMakefile* mf) |
81 | 0 | : Makefile(mf) |
82 | 0 | { |
83 | 0 | this->Makefile->PushLoopBlock(); |
84 | 0 | } |
85 | | |
86 | | cmForEachFunctionBlocker::~cmForEachFunctionBlocker() |
87 | 0 | { |
88 | 0 | this->Makefile->PopLoopBlock(); |
89 | 0 | } |
90 | | |
91 | | bool cmForEachFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff, |
92 | | cmMakefile& mf) const |
93 | 0 | { |
94 | 0 | std::vector<std::string> expandedArguments; |
95 | 0 | mf.ExpandArguments(lff.Arguments(), expandedArguments); |
96 | 0 | return expandedArguments.empty() || |
97 | 0 | expandedArguments.front() == this->Args.front(); |
98 | 0 | } |
99 | | |
100 | | bool cmForEachFunctionBlocker::Replay( |
101 | | std::vector<cmListFileFunction> functions, cmExecutionStatus& inStatus) |
102 | 0 | { |
103 | 0 | if (this->Args.size() == this->IterationVarsCount) { |
104 | 0 | return true; |
105 | 0 | } |
106 | 0 | return this->ZipLists ? this->ReplayZipLists(functions, inStatus) |
107 | 0 | : this->ReplayItems(functions, inStatus); |
108 | 0 | } |
109 | | |
110 | | bool cmForEachFunctionBlocker::ReplayItems( |
111 | | std::vector<cmListFileFunction> const& functions, |
112 | | cmExecutionStatus& inStatus) |
113 | 0 | { |
114 | 0 | assert("Unexpected number of iteration variables" && |
115 | 0 | this->IterationVarsCount == 1); |
116 | |
|
117 | 0 | auto& mf = inStatus.GetMakefile(); |
118 | | |
119 | | // At end of for each execute recorded commands |
120 | | // store the old value |
121 | 0 | cm::optional<std::string> oldDef; |
122 | 0 | if (mf.GetPolicyStatus(cmPolicies::CMP0124) != cmPolicies::NEW) { |
123 | 0 | oldDef = mf.GetSafeDefinition(this->Args.front()); |
124 | 0 | } else if (mf.IsNormalDefinitionSet(this->Args.front())) { |
125 | 0 | oldDef = *mf.GetDefinition(this->Args.front()); |
126 | 0 | } |
127 | |
|
128 | 0 | auto restore = false; |
129 | 0 | for (std::string const& arg : cmMakeRange(this->Args).advance(1)) { |
130 | | // Set the variable to the loop value |
131 | 0 | mf.AddDefinition(this->Args.front(), arg); |
132 | | // Invoke all the functions that were collected in the block. |
133 | 0 | auto r = this->invoke(functions, inStatus, mf); |
134 | 0 | restore = r.Restore; |
135 | 0 | if (r.Break) { |
136 | 0 | break; |
137 | 0 | } |
138 | 0 | } |
139 | |
|
140 | 0 | if (restore) { |
141 | 0 | if (oldDef) { |
142 | | // restore the variable to its prior value |
143 | 0 | mf.AddDefinition(this->Args.front(), *oldDef); |
144 | 0 | } else { |
145 | 0 | mf.RemoveDefinition(this->Args.front()); |
146 | 0 | } |
147 | 0 | } |
148 | |
|
149 | 0 | return true; |
150 | 0 | } |
151 | | |
152 | | bool cmForEachFunctionBlocker::ReplayZipLists( |
153 | | std::vector<cmListFileFunction> const& functions, |
154 | | cmExecutionStatus& inStatus) |
155 | 0 | { |
156 | 0 | assert("Unexpected number of iteration variables" && |
157 | 0 | this->IterationVarsCount >= 1); |
158 | |
|
159 | 0 | auto& mf = inStatus.GetMakefile(); |
160 | | |
161 | | // Expand the list of list-variables into a list of lists of strings |
162 | 0 | std::vector<cmList> values; |
163 | 0 | values.reserve(this->Args.size() - this->IterationVarsCount); |
164 | | // Also track the longest list size |
165 | 0 | std::size_t maxItems = 0u; |
166 | 0 | for (auto const& var : |
167 | 0 | cmMakeRange(this->Args).advance(this->IterationVarsCount)) { |
168 | 0 | cmList items; |
169 | 0 | auto const& value = mf.GetSafeDefinition(var); |
170 | 0 | if (!value.empty()) { |
171 | 0 | items.assign(value, cmList::EmptyElements::Yes); |
172 | 0 | } |
173 | 0 | maxItems = std::max(maxItems, items.size()); |
174 | 0 | values.emplace_back(std::move(items)); |
175 | 0 | } |
176 | | |
177 | | // Form the list of iteration variables |
178 | 0 | std::vector<std::string> iterationVars; |
179 | 0 | if (this->IterationVarsCount > 1) { |
180 | | // If multiple iteration variables has given, |
181 | | // just copy them to the `iterationVars` list. |
182 | 0 | iterationVars.reserve(values.size()); |
183 | 0 | std::copy(this->Args.begin(), |
184 | 0 | this->Args.begin() + this->IterationVarsCount, |
185 | 0 | std::back_inserter(iterationVars)); |
186 | 0 | } else { |
187 | | // In case of the only iteration variable, |
188 | | // generate names as `var_name_N`, |
189 | | // where `N` is the count of lists to zip |
190 | 0 | iterationVars.resize(values.size()); |
191 | 0 | auto const iter_var_prefix = cmStrCat(this->Args.front(), '_'); |
192 | 0 | auto i = 0u; |
193 | 0 | std::generate( |
194 | 0 | iterationVars.begin(), iterationVars.end(), |
195 | 0 | [&]() -> std::string { return cmStrCat(iter_var_prefix, i++); }); |
196 | 0 | } |
197 | 0 | assert("Sanity check" && iterationVars.size() == values.size()); |
198 | | |
199 | | // Store old values for iteration variables |
200 | 0 | std::map<std::string, cm::optional<std::string>> oldDefs; |
201 | 0 | for (auto i = 0u; i < values.size(); ++i) { |
202 | 0 | auto const& varName = iterationVars[i]; |
203 | 0 | if (mf.GetPolicyStatus(cmPolicies::CMP0124) != cmPolicies::NEW) { |
204 | 0 | oldDefs.emplace(varName, mf.GetSafeDefinition(varName)); |
205 | 0 | } else if (mf.IsNormalDefinitionSet(varName)) { |
206 | 0 | oldDefs.emplace(varName, *mf.GetDefinition(varName)); |
207 | 0 | } else { |
208 | 0 | oldDefs.emplace(varName, cm::nullopt); |
209 | 0 | } |
210 | 0 | } |
211 | | |
212 | | // Form a vector of current positions in all lists (Ok, vectors) of values |
213 | 0 | std::vector<decltype(values)::value_type::iterator> positions; |
214 | 0 | positions.reserve(values.size()); |
215 | 0 | std::transform( |
216 | 0 | values.begin(), values.end(), std::back_inserter(positions), |
217 | | // Set the initial position to the beginning of every list |
218 | 0 | [](decltype(values)::value_type& list) { return list.begin(); }); |
219 | 0 | assert("Sanity check" && positions.size() == values.size()); |
220 | |
|
221 | 0 | auto restore = false; |
222 | | // Iterate over all the lists simulateneously |
223 | 0 | for (auto i = 0u; i < maxItems; ++i) { |
224 | | // Declare iteration variables |
225 | 0 | for (auto j = 0u; j < values.size(); ++j) { |
226 | | // Define (or not) the iteration variable if the current position |
227 | | // still not at the end... |
228 | 0 | if (positions[j] != values[j].end()) { |
229 | 0 | mf.AddDefinition(iterationVars[j], *positions[j]); |
230 | 0 | ++positions[j]; |
231 | 0 | } else { |
232 | 0 | mf.RemoveDefinition(iterationVars[j]); |
233 | 0 | } |
234 | 0 | } |
235 | | // Invoke all the functions that were collected in the block. |
236 | 0 | auto r = this->invoke(functions, inStatus, mf); |
237 | 0 | restore = r.Restore; |
238 | 0 | if (r.Break) { |
239 | 0 | break; |
240 | 0 | } |
241 | 0 | } |
242 | | |
243 | | // Restore the variables to its prior value |
244 | 0 | if (restore) { |
245 | 0 | for (auto const& p : oldDefs) { |
246 | 0 | if (p.second) { |
247 | 0 | mf.AddDefinition(p.first, *p.second); |
248 | 0 | } else { |
249 | 0 | mf.RemoveDefinition(p.first); |
250 | 0 | } |
251 | 0 | } |
252 | 0 | } |
253 | 0 | return true; |
254 | 0 | } |
255 | | |
256 | | auto cmForEachFunctionBlocker::invoke( |
257 | | std::vector<cmListFileFunction> const& functions, |
258 | | cmExecutionStatus& inStatus, cmMakefile& mf) -> InvokeResult |
259 | 0 | { |
260 | 0 | InvokeResult result = { true, false }; |
261 | | // Invoke all the functions that were collected in the block. |
262 | 0 | for (cmListFileFunction const& func : functions) { |
263 | 0 | cmExecutionStatus status(mf); |
264 | 0 | mf.ExecuteCommand(func, status); |
265 | 0 | if (status.GetReturnInvoked()) { |
266 | 0 | inStatus.SetReturnInvoked(status.GetReturnVariables()); |
267 | 0 | result.Break = true; |
268 | 0 | break; |
269 | 0 | } |
270 | 0 | if (status.GetBreakInvoked()) { |
271 | 0 | result.Break = true; |
272 | 0 | break; |
273 | 0 | } |
274 | 0 | if (status.GetContinueInvoked()) { |
275 | 0 | break; |
276 | 0 | } |
277 | 0 | if (status.HasExitCode()) { |
278 | 0 | inStatus.SetExitCode(status.GetExitCode()); |
279 | 0 | result.Break = true; |
280 | 0 | break; |
281 | 0 | } |
282 | 0 | if (cmSystemTools::GetFatalErrorOccurred()) { |
283 | 0 | result.Restore = false; |
284 | 0 | result.Break = true; |
285 | 0 | break; |
286 | 0 | } |
287 | 0 | } |
288 | 0 | return result; |
289 | 0 | } |
290 | | |
291 | | bool HandleInMode(std::vector<std::string> const& args, |
292 | | std::vector<std::string>::const_iterator kwInIter, |
293 | | cmMakefile& makefile) |
294 | 0 | { |
295 | 0 | assert("A valid iterator expected" && kwInIter != args.end()); |
296 | |
|
297 | 0 | auto fb = cm::make_unique<cmForEachFunctionBlocker>(&makefile); |
298 | | |
299 | | // Copy iteration variable names first |
300 | 0 | std::copy(args.begin(), kwInIter, std::back_inserter(fb->Args)); |
301 | | // Remember the count of given iteration variable names |
302 | 0 | auto const varsCount = fb->Args.size(); |
303 | 0 | fb->SetIterationVarsCount(varsCount); |
304 | |
|
305 | 0 | enum Doing |
306 | 0 | { |
307 | 0 | DoingNone, |
308 | 0 | DoingLists, |
309 | 0 | DoingItems, |
310 | 0 | DoingZipLists |
311 | 0 | }; |
312 | 0 | Doing doing = DoingNone; |
313 | | // Iterate over arguments past the "IN" keyword |
314 | 0 | for (std::string const& arg : cmMakeRange(++kwInIter, args.end())) { |
315 | 0 | if (arg == "LISTS") { |
316 | 0 | if (doing == DoingZipLists) { |
317 | 0 | makefile.IssueMessage(MessageType::FATAL_ERROR, |
318 | 0 | "ZIP_LISTS can not be used with LISTS or ITEMS"); |
319 | 0 | return true; |
320 | 0 | } |
321 | 0 | if (varsCount != 1u) { |
322 | 0 | makefile.IssueMessage( |
323 | 0 | MessageType::FATAL_ERROR, |
324 | 0 | "ITEMS or LISTS require exactly one iteration variable"); |
325 | 0 | return true; |
326 | 0 | } |
327 | 0 | doing = DoingLists; |
328 | |
|
329 | 0 | } else if (arg == "ITEMS") { |
330 | 0 | if (doing == DoingZipLists) { |
331 | 0 | makefile.IssueMessage(MessageType::FATAL_ERROR, |
332 | 0 | "ZIP_LISTS can not be used with LISTS or ITEMS"); |
333 | 0 | return true; |
334 | 0 | } |
335 | 0 | if (varsCount != 1u) { |
336 | 0 | makefile.IssueMessage( |
337 | 0 | MessageType::FATAL_ERROR, |
338 | 0 | "ITEMS or LISTS require exactly one iteration variable"); |
339 | 0 | return true; |
340 | 0 | } |
341 | 0 | doing = DoingItems; |
342 | |
|
343 | 0 | } else if (arg == "ZIP_LISTS") { |
344 | 0 | if (doing != DoingNone) { |
345 | 0 | makefile.IssueMessage(MessageType::FATAL_ERROR, |
346 | 0 | "ZIP_LISTS can not be used with LISTS or ITEMS"); |
347 | 0 | return true; |
348 | 0 | } |
349 | 0 | doing = DoingZipLists; |
350 | 0 | fb->SetZipLists(); |
351 | |
|
352 | 0 | } else if (doing == DoingLists) { |
353 | 0 | auto const& value = makefile.GetSafeDefinition(arg); |
354 | 0 | if (!value.empty()) { |
355 | 0 | cmExpandList(value, fb->Args, cmList::EmptyElements::Yes); |
356 | 0 | } |
357 | |
|
358 | 0 | } else if (doing == DoingItems || doing == DoingZipLists) { |
359 | 0 | fb->Args.push_back(arg); |
360 | |
|
361 | 0 | } else { |
362 | 0 | makefile.IssueMessage(MessageType::FATAL_ERROR, |
363 | 0 | cmStrCat("Unknown argument:\n ", arg, '\n')); |
364 | 0 | return true; |
365 | 0 | } |
366 | 0 | } |
367 | | |
368 | | // If `ZIP_LISTS` given and variables count more than 1, |
369 | | // make sure the given lists count matches variables... |
370 | 0 | if (doing == DoingZipLists && varsCount > 1u && |
371 | 0 | (2u * varsCount) != fb->Args.size()) { |
372 | 0 | makefile.IssueMessage(MessageType::FATAL_ERROR, |
373 | 0 | cmStrCat("Expected ", varsCount, |
374 | 0 | " list variables, but given ", |
375 | 0 | fb->Args.size() - varsCount)); |
376 | 0 | return true; |
377 | 0 | } |
378 | | |
379 | 0 | makefile.AddFunctionBlocker(std::move(fb)); |
380 | |
|
381 | 0 | return true; |
382 | 0 | } |
383 | | |
384 | | bool TryParseInteger(cmExecutionStatus& status, std::string const& str, int& i) |
385 | 0 | { |
386 | 0 | try { |
387 | 0 | i = std::stoi(str); |
388 | 0 | } catch (std::invalid_argument const&) { |
389 | 0 | status.SetError(cmStrCat("Invalid integer: '", str, '\'')); |
390 | 0 | cmSystemTools::SetFatalErrorOccurred(); |
391 | 0 | return false; |
392 | 0 | } catch (std::out_of_range const&) { |
393 | 0 | status.SetError(cmStrCat("Integer out of range: '", str, '\'')); |
394 | 0 | cmSystemTools::SetFatalErrorOccurred(); |
395 | 0 | return false; |
396 | 0 | } |
397 | | |
398 | 0 | return true; |
399 | 0 | } |
400 | | |
401 | | } // anonymous namespace |
402 | | |
403 | | bool cmForEachCommand(std::vector<std::string> const& args, |
404 | | cmExecutionStatus& status) |
405 | 0 | { |
406 | 0 | if (args.empty()) { |
407 | 0 | status.SetError("called with incorrect number of arguments"); |
408 | 0 | return false; |
409 | 0 | } |
410 | 0 | auto kwInIter = std::find(args.begin(), args.end(), "IN"); |
411 | 0 | if (kwInIter != args.end()) { |
412 | 0 | return HandleInMode(args, kwInIter, status.GetMakefile()); |
413 | 0 | } |
414 | | |
415 | | // create a function blocker |
416 | 0 | auto fb = cm::make_unique<cmForEachFunctionBlocker>(&status.GetMakefile()); |
417 | 0 | if (args.size() > 1) { |
418 | 0 | if (args[1] == "RANGE") { |
419 | 0 | int start = 0; |
420 | 0 | int stop = 0; |
421 | 0 | int step = 0; |
422 | 0 | if (args.size() == 3) { |
423 | 0 | if (!TryParseInteger(status, args[2], stop)) { |
424 | 0 | return false; |
425 | 0 | } |
426 | 0 | } |
427 | 0 | if (args.size() == 4) { |
428 | 0 | if (!TryParseInteger(status, args[2], start)) { |
429 | 0 | return false; |
430 | 0 | } |
431 | 0 | if (!TryParseInteger(status, args[3], stop)) { |
432 | 0 | return false; |
433 | 0 | } |
434 | 0 | } |
435 | 0 | if (args.size() == 5) { |
436 | 0 | if (!TryParseInteger(status, args[2], start)) { |
437 | 0 | return false; |
438 | 0 | } |
439 | 0 | if (!TryParseInteger(status, args[3], stop)) { |
440 | 0 | return false; |
441 | 0 | } |
442 | 0 | if (!TryParseInteger(status, args[4], step)) { |
443 | 0 | return false; |
444 | 0 | } |
445 | 0 | } |
446 | 0 | if (step == 0) { |
447 | 0 | if (start > stop) { |
448 | 0 | step = -1; |
449 | 0 | } else { |
450 | 0 | step = 1; |
451 | 0 | } |
452 | 0 | } |
453 | 0 | if ((start > stop && step > 0) || (start < stop && step < 0) || |
454 | 0 | step == 0) { |
455 | 0 | status.SetError( |
456 | 0 | cmStrCat("called with incorrect range specification: start ", start, |
457 | 0 | ", stop ", stop, ", step ", step)); |
458 | 0 | cmSystemTools::SetFatalErrorOccurred(); |
459 | 0 | return false; |
460 | 0 | } |
461 | | |
462 | | // Calculate expected iterations count and reserve enough space |
463 | | // in the `fb->Args` vector. The first item is the iteration variable |
464 | | // name... |
465 | 0 | std::size_t const iter_cnt = 2u + |
466 | 0 | static_cast<int>(start < stop) * (stop - start) / std::abs(step) + |
467 | 0 | static_cast<int>(start > stop) * (start - stop) / std::abs(step); |
468 | 0 | fb->Args.resize(iter_cnt); |
469 | 0 | fb->Args.front() = args.front(); |
470 | 0 | auto cc = start; |
471 | 0 | auto generator = [&cc, step]() -> std::string { |
472 | 0 | auto result = std::to_string(cc); |
473 | 0 | cc += step; |
474 | 0 | return result; |
475 | 0 | }; |
476 | | // Fill the `range` vector w/ generated string values |
477 | | // (starting from 2nd position) |
478 | 0 | std::generate(++fb->Args.begin(), fb->Args.end(), generator); |
479 | 0 | } else { |
480 | 0 | fb->Args = args; |
481 | 0 | } |
482 | 0 | } else { |
483 | 0 | fb->Args = args; |
484 | 0 | } |
485 | | |
486 | 0 | fb->SetIterationVarsCount(1u); |
487 | 0 | status.GetMakefile().AddFunctionBlocker(std::move(fb)); |
488 | |
|
489 | 0 | return true; |
490 | 0 | } |