/src/CMake/Source/cmQtAutoGen.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 "cmQtAutoGen.h" |
4 | | |
5 | | #include <algorithm> |
6 | | #include <array> |
7 | | #include <cstddef> |
8 | | #include <initializer_list> |
9 | | #include <sstream> |
10 | | #include <unordered_set> |
11 | | #include <utility> |
12 | | |
13 | | #include <cmext/algorithm> |
14 | | |
15 | | #include "cmsys/FStream.hxx" |
16 | | #include "cmsys/RegularExpression.hxx" |
17 | | |
18 | | #include "cmCryptoHash.h" |
19 | | #include "cmDuration.h" |
20 | | #include "cmFileTime.h" |
21 | | #include "cmGeneratedFileStream.h" |
22 | | #include "cmProcessOutput.h" |
23 | | #include "cmStringAlgorithms.h" |
24 | | #include "cmSystemTools.h" |
25 | | |
26 | | // - Static functions |
27 | | |
28 | | /// @brief Merges newOpts into baseOpts |
29 | | /// @arg valueOpts list of options that accept a value |
30 | | static void MergeOptions(std::vector<std::string>& baseOpts, |
31 | | std::vector<std::string> const& newOpts, |
32 | | std::initializer_list<cm::string_view> valueOpts, |
33 | | bool isQt5OrLater) |
34 | 0 | { |
35 | 0 | if (newOpts.empty()) { |
36 | 0 | return; |
37 | 0 | } |
38 | 0 | if (baseOpts.empty()) { |
39 | 0 | baseOpts = newOpts; |
40 | 0 | return; |
41 | 0 | } |
42 | | |
43 | 0 | std::vector<std::string> extraOpts; |
44 | 0 | for (auto fit = newOpts.begin(), fitEnd = newOpts.end(); fit != fitEnd; |
45 | 0 | ++fit) { |
46 | 0 | std::string const& newOpt = *fit; |
47 | 0 | auto existIt = std::find(baseOpts.begin(), baseOpts.end(), newOpt); |
48 | 0 | if (existIt != baseOpts.end()) { |
49 | 0 | if (newOpt.size() >= 2) { |
50 | | // Acquire the option name |
51 | 0 | std::string optName; |
52 | 0 | { |
53 | 0 | auto oit = newOpt.begin(); |
54 | 0 | if (*oit == '-') { |
55 | 0 | ++oit; |
56 | 0 | if (isQt5OrLater && (*oit == '-')) { |
57 | 0 | ++oit; |
58 | 0 | } |
59 | 0 | optName.assign(oit, newOpt.end()); |
60 | 0 | } |
61 | 0 | } |
62 | | // Test if this is a value option and change the existing value |
63 | 0 | if (!optName.empty() && cm::contains(valueOpts, optName)) { |
64 | 0 | auto const existItNext(existIt + 1); |
65 | 0 | auto const fitNext(fit + 1); |
66 | 0 | if ((existItNext != baseOpts.end()) && (fitNext != fitEnd)) { |
67 | 0 | *existItNext = *fitNext; |
68 | 0 | ++fit; |
69 | 0 | } |
70 | 0 | } |
71 | 0 | } |
72 | 0 | } else { |
73 | 0 | extraOpts.push_back(newOpt); |
74 | 0 | } |
75 | 0 | } |
76 | | // Append options |
77 | 0 | cm::append(baseOpts, extraOpts); |
78 | 0 | } |
79 | | |
80 | | // - Class definitions |
81 | | |
82 | | unsigned int const cmQtAutoGen::ParallelMax = 64; |
83 | | |
84 | | cm::string_view cmQtAutoGen::GeneratorName(GenT genType) |
85 | 0 | { |
86 | 0 | switch (genType) { |
87 | 0 | case GenT::GEN: |
88 | 0 | return "AutoGen"; |
89 | 0 | case GenT::MOC: |
90 | 0 | return "AutoMoc"; |
91 | 0 | case GenT::UIC: |
92 | 0 | return "AutoUic"; |
93 | 0 | case GenT::RCC: |
94 | 0 | return "AutoRcc"; |
95 | 0 | } |
96 | 0 | return "AutoGen"; |
97 | 0 | } |
98 | | |
99 | | cm::string_view cmQtAutoGen::GeneratorNameUpper(GenT genType) |
100 | 0 | { |
101 | 0 | switch (genType) { |
102 | 0 | case GenT::GEN: |
103 | 0 | return "AUTOGEN"; |
104 | 0 | case GenT::MOC: |
105 | 0 | return "AUTOMOC"; |
106 | 0 | case GenT::UIC: |
107 | 0 | return "AUTOUIC"; |
108 | 0 | case GenT::RCC: |
109 | 0 | return "AUTORCC"; |
110 | 0 | } |
111 | 0 | return "AUTOGEN"; |
112 | 0 | } |
113 | | |
114 | | std::string cmQtAutoGen::Tools(bool moc, bool uic, bool rcc) |
115 | 0 | { |
116 | 0 | std::array<cm::string_view, 3> lst; |
117 | 0 | decltype(lst)::size_type num = 0; |
118 | 0 | if (moc) { |
119 | 0 | lst.at(num++) = "AUTOMOC"; |
120 | 0 | } |
121 | 0 | if (uic) { |
122 | 0 | lst.at(num++) = "AUTOUIC"; |
123 | 0 | } |
124 | 0 | if (rcc) { |
125 | 0 | lst.at(num++) = "AUTORCC"; |
126 | 0 | } |
127 | 0 | switch (num) { |
128 | 0 | case 1: |
129 | 0 | return std::string(lst[0]); |
130 | 0 | case 2: |
131 | 0 | return cmStrCat(lst[0], " and ", lst[1]); |
132 | 0 | case 3: |
133 | 0 | return cmStrCat(lst[0], ", ", lst[1], " and ", lst[2]); |
134 | 0 | default: |
135 | 0 | break; |
136 | 0 | } |
137 | 0 | return std::string(); |
138 | 0 | } |
139 | | |
140 | | std::string cmQtAutoGen::Quoted(cm::string_view text) |
141 | 0 | { |
142 | 0 | static std::initializer_list<std::pair<char const*, char const*>> const |
143 | 0 | replacements = { { "\\", "\\\\" }, { "\"", "\\\"" }, { "\a", "\\a" }, |
144 | 0 | { "\b", "\\b" }, { "\f", "\\f" }, { "\n", "\\n" }, |
145 | 0 | { "\r", "\\r" }, { "\t", "\\t" }, { "\v", "\\v" } }; |
146 | |
|
147 | 0 | std::string res(text); |
148 | 0 | for (auto const& pair : replacements) { |
149 | 0 | cmSystemTools::ReplaceString(res, pair.first, pair.second); |
150 | 0 | } |
151 | 0 | return cmStrCat('"', res, '"'); |
152 | 0 | } |
153 | | |
154 | | std::string cmQtAutoGen::QuotedCommand(std::vector<std::string> const& command) |
155 | 0 | { |
156 | 0 | std::string res; |
157 | 0 | for (std::string const& item : command) { |
158 | 0 | if (!res.empty()) { |
159 | 0 | res.push_back(' '); |
160 | 0 | } |
161 | 0 | std::string const cesc = cmQtAutoGen::Quoted(item); |
162 | 0 | if (item.empty() || (cesc.size() > (item.size() + 2)) || |
163 | 0 | (cesc.find(' ') != std::string::npos)) { |
164 | 0 | res += cesc; |
165 | 0 | } else { |
166 | 0 | res += item; |
167 | 0 | } |
168 | 0 | } |
169 | 0 | return res; |
170 | 0 | } |
171 | | |
172 | | std::string cmQtAutoGen::FileNameWithoutLastExtension(cm::string_view filename) |
173 | 0 | { |
174 | 0 | auto slashPos = filename.rfind('/'); |
175 | 0 | if (slashPos != cm::string_view::npos) { |
176 | 0 | filename.remove_prefix(slashPos + 1); |
177 | 0 | } |
178 | 0 | auto dotPos = filename.rfind('.'); |
179 | 0 | return std::string(filename.substr(0, dotPos)); |
180 | 0 | } |
181 | | |
182 | | std::string cmQtAutoGen::ParentDir(cm::string_view filename) |
183 | 0 | { |
184 | 0 | auto slashPos = filename.rfind('/'); |
185 | 0 | if (slashPos == cm::string_view::npos) { |
186 | 0 | return std::string(); |
187 | 0 | } |
188 | 0 | return std::string(filename.substr(0, slashPos)); |
189 | 0 | } |
190 | | |
191 | | std::string cmQtAutoGen::SubDirPrefix(cm::string_view filename) |
192 | 0 | { |
193 | 0 | auto slashPos = filename.rfind('/'); |
194 | 0 | if (slashPos == cm::string_view::npos) { |
195 | 0 | return std::string(); |
196 | 0 | } |
197 | 0 | return std::string(filename.substr(0, slashPos + 1)); |
198 | 0 | } |
199 | | |
200 | | std::string cmQtAutoGen::AppendFilenameSuffix(cm::string_view filename, |
201 | | cm::string_view suffix) |
202 | 0 | { |
203 | 0 | auto dotPos = filename.rfind('.'); |
204 | 0 | if (dotPos == cm::string_view::npos) { |
205 | 0 | return cmStrCat(filename, suffix); |
206 | 0 | } |
207 | 0 | return cmStrCat(filename.substr(0, dotPos), suffix, |
208 | 0 | filename.substr(dotPos, filename.size() - dotPos)); |
209 | 0 | } |
210 | | |
211 | | void cmQtAutoGen::UicMergeOptions(std::vector<std::string>& baseOpts, |
212 | | std::vector<std::string> const& newOpts, |
213 | | bool isQt5OrLater) |
214 | 0 | { |
215 | 0 | static std::initializer_list<cm::string_view> const valueOpts = { |
216 | 0 | "tr", "translate", "postfix", "generator", |
217 | 0 | "include", // Since Qt 5.3 |
218 | 0 | "g" |
219 | 0 | }; |
220 | 0 | MergeOptions(baseOpts, newOpts, valueOpts, isQt5OrLater); |
221 | 0 | } |
222 | | |
223 | | void cmQtAutoGen::RccMergeOptions(std::vector<std::string>& baseOpts, |
224 | | std::vector<std::string> const& newOpts, |
225 | | bool isQt5OrLater) |
226 | 0 | { |
227 | 0 | static std::initializer_list<cm::string_view> const valueOpts = { |
228 | 0 | "name", "root", "compress", "threshold" |
229 | 0 | }; |
230 | 0 | MergeOptions(baseOpts, newOpts, valueOpts, isQt5OrLater); |
231 | 0 | } |
232 | | |
233 | | static void RccListParseContent(std::string const& content, |
234 | | std::vector<std::string>& files) |
235 | 0 | { |
236 | 0 | cmsys::RegularExpression fileMatchRegex("(<file[^<]+)"); |
237 | 0 | cmsys::RegularExpression fileReplaceRegex("(^<file[^>]*>)"); |
238 | |
|
239 | 0 | char const* contentChars = content.c_str(); |
240 | 0 | while (fileMatchRegex.find(contentChars)) { |
241 | 0 | std::string const qrcEntry = fileMatchRegex.match(1); |
242 | 0 | contentChars += qrcEntry.size(); |
243 | 0 | { |
244 | 0 | fileReplaceRegex.find(qrcEntry); |
245 | 0 | std::string const tag = fileReplaceRegex.match(1); |
246 | 0 | files.push_back(qrcEntry.substr(tag.size())); |
247 | 0 | } |
248 | 0 | } |
249 | 0 | } |
250 | | |
251 | | static bool RccListReadQrcContent(std::string const& qrcFile, |
252 | | std::string& qrcContents) |
253 | 0 | { |
254 | 0 | cmsys::ifstream ifs(qrcFile.c_str()); |
255 | 0 | if (!ifs) { |
256 | 0 | return false; |
257 | 0 | } |
258 | 0 | std::ostringstream osst; |
259 | 0 | osst << ifs.rdbuf(); |
260 | 0 | qrcContents = osst.str(); |
261 | 0 | return true; |
262 | 0 | } |
263 | | |
264 | | static bool RccListReadQrc(std::string const& qrcFile, |
265 | | std::vector<std::string>& files) |
266 | 0 | { |
267 | 0 | std::string qrcContents; |
268 | 0 | if (!RccListReadQrcContent(qrcFile, qrcContents)) { |
269 | 0 | return false; |
270 | 0 | } |
271 | 0 | RccListParseContent(qrcContents, files); |
272 | 0 | return true; |
273 | 0 | } |
274 | | |
275 | | static bool RccListParseOutput(std::string const& rccStdOut, |
276 | | std::string const& rccStdErr, |
277 | | std::vector<std::string>& files, |
278 | | std::string& error) |
279 | 0 | { |
280 | | // Lambda to strip CR characters |
281 | 0 | auto StripCR = [](std::string& line) { |
282 | 0 | std::string::size_type cr = line.find('\r'); |
283 | 0 | if (cr != std::string::npos) { |
284 | 0 | line.resize(cr); |
285 | 0 | } |
286 | 0 | }; |
287 | | |
288 | | // Parse rcc std output |
289 | 0 | { |
290 | 0 | std::istringstream ostr(rccStdOut); |
291 | 0 | std::string oline; |
292 | 0 | while (std::getline(ostr, oline)) { |
293 | 0 | StripCR(oline); |
294 | 0 | if (!oline.empty()) { |
295 | 0 | files.push_back(oline); |
296 | 0 | } |
297 | 0 | } |
298 | 0 | } |
299 | | // Parse rcc error output |
300 | 0 | { |
301 | 0 | std::istringstream estr(rccStdErr); |
302 | 0 | std::string eline; |
303 | 0 | while (std::getline(estr, eline)) { |
304 | 0 | StripCR(eline); |
305 | 0 | if (cmHasLiteralPrefix(eline, "RCC: Error in")) { |
306 | 0 | static std::string const searchString = "Cannot find file '"; |
307 | |
|
308 | 0 | std::string::size_type pos = eline.find(searchString); |
309 | 0 | if (pos == std::string::npos) { |
310 | 0 | error = cmStrCat("rcc lists unparsable output:\n", |
311 | 0 | cmQtAutoGen::Quoted(eline), '\n'); |
312 | 0 | return false; |
313 | 0 | } |
314 | 0 | pos += searchString.length(); |
315 | 0 | std::string::size_type sz = eline.size() - pos - 1; |
316 | 0 | files.push_back(eline.substr(pos, sz)); |
317 | 0 | } |
318 | 0 | } |
319 | 0 | } |
320 | | |
321 | 0 | return true; |
322 | 0 | } |
323 | | |
324 | | // The header of a cached rcc listing identifies the rcc executable, its |
325 | | // options, and the qrc file. The qrc file is identified by a hash of its |
326 | | // content, because its modification time may not change if it is modified |
327 | | // again within the time resolution of the file system. |
328 | | static std::string RccListCacheHeader(std::string const& rccExecutable, |
329 | | std::vector<std::string> const& options, |
330 | | std::string const& qrcFile, |
331 | | std::string const& qrcContent) |
332 | 0 | { |
333 | 0 | cmFileTime rccTime; |
334 | 0 | if (!rccTime.Load(rccExecutable)) { |
335 | 0 | return std::string(); |
336 | 0 | } |
337 | 0 | cmCryptoHash sha256(cmCryptoHash::AlgoSHA256); |
338 | 0 | return cmStrCat( |
339 | 0 | "rcc=", rccExecutable, "\nrcc_time=", std::to_string(rccTime.GetTime()), |
340 | 0 | "\nrcc_size=", std::to_string(cmSystemTools::FileLength(rccExecutable)), |
341 | 0 | "\noptions=", cmJoin(options, ";"), "\nqrc=", qrcFile, |
342 | 0 | "\nqrc_sha256=", sha256.HashString(qrcContent), '\n'); |
343 | 0 | } |
344 | | |
345 | | static bool RccListReadCache(std::string const& cacheFile, |
346 | | std::string const& header, |
347 | | std::vector<std::string>& files) |
348 | 0 | { |
349 | 0 | cmsys::ifstream fin(cacheFile.c_str(), std::ios::in | std::ios::binary); |
350 | 0 | if (!fin) { |
351 | 0 | return false; |
352 | 0 | } |
353 | 0 | std::ostringstream content; |
354 | 0 | content << fin.rdbuf(); |
355 | 0 | std::string const cache = content.str(); |
356 | 0 | if (!cmHasPrefix(cache, header)) { |
357 | 0 | return false; |
358 | 0 | } |
359 | 0 | std::vector<std::string> cachedFiles; |
360 | 0 | std::istringstream lines(cache.substr(header.size())); |
361 | 0 | std::string line; |
362 | 0 | while (std::getline(lines, line)) { |
363 | | // Tolerate line endings converted to CRLF. |
364 | 0 | if (!line.empty() && line.back() == '\r') { |
365 | 0 | line.pop_back(); |
366 | 0 | } |
367 | 0 | if (line.empty()) { |
368 | 0 | continue; |
369 | 0 | } |
370 | | // rcc lists the content of directories, so the listing is outdated |
371 | | // if a listed path became a directory. |
372 | 0 | if (cmSystemTools::FileIsDirectory(line)) { |
373 | 0 | return false; |
374 | 0 | } |
375 | 0 | cachedFiles.push_back(std::move(line)); |
376 | 0 | } |
377 | 0 | cm::append(files, cachedFiles); |
378 | 0 | return true; |
379 | 0 | } |
380 | | |
381 | | static void RccListWriteCache(std::string const& cacheFile, |
382 | | std::string const& header, |
383 | | std::string const& qrcContent, |
384 | | std::string const& qrcDir, |
385 | | std::vector<std::string>::const_iterator first, |
386 | | std::vector<std::string>::const_iterator last) |
387 | 0 | { |
388 | | // rcc lists the content of directories referred to by the qrc file, which |
389 | | // may change without the qrc file changing. Do not cache such listings. |
390 | | // Rather than interpreting the qrc file the way rcc does, cache only |
391 | | // listings in which rcc listed each entry of the qrc file as given. |
392 | | // Directories, and entries rcc resolves differently, e.g. names with |
393 | | // XML character references, are not listed as given. |
394 | 0 | { |
395 | 0 | std::vector<std::string> entries; |
396 | 0 | RccListParseContent(qrcContent, entries); |
397 | 0 | std::unordered_set<std::string> const listed(first, last); |
398 | 0 | for (std::string const& entry : entries) { |
399 | 0 | if (!listed.count(cmSystemTools::CollapseFullPath( |
400 | 0 | cmTrimWhitespace(entry), qrcDir))) { |
401 | 0 | cmSystemTools::RemoveFile(cacheFile); |
402 | 0 | return; |
403 | 0 | } |
404 | 0 | } |
405 | 0 | } |
406 | | |
407 | 0 | std::string content = header; |
408 | 0 | for (; first != last; ++first) { |
409 | 0 | content += *first; |
410 | 0 | content += '\n'; |
411 | 0 | } |
412 | 0 | cmGeneratedFileStream::WriteIfDifferent(cacheFile, content, true); |
413 | 0 | } |
414 | | |
415 | 0 | cmQtAutoGen::RccLister::RccLister() = default; |
416 | | |
417 | | cmQtAutoGen::RccLister::RccLister(std::string rccExecutable, |
418 | | std::vector<std::string> listOptions) |
419 | 0 | : RccExecutable_(std::move(rccExecutable)) |
420 | 0 | , ListOptions_(std::move(listOptions)) |
421 | 0 | { |
422 | 0 | } |
423 | | |
424 | | bool cmQtAutoGen::RccLister::list(std::string const& qrcFile, |
425 | | std::vector<std::string>& files, |
426 | | std::string& error, bool verbose, |
427 | | std::string const& cacheFile) const |
428 | 0 | { |
429 | 0 | error.clear(); |
430 | |
|
431 | 0 | if (!cmSystemTools::FileExists(qrcFile, true)) { |
432 | 0 | error = |
433 | 0 | cmStrCat("The resource file ", Quoted(qrcFile), " does not exist."); |
434 | 0 | return false; |
435 | 0 | } |
436 | | |
437 | | // Run rcc list command in the directory of the qrc file with the pathless |
438 | | // qrc file name argument. This way rcc prints relative paths. |
439 | | // This avoids issues on Windows when the qrc file is in a path that |
440 | | // contains non-ASCII characters. |
441 | 0 | std::string const fileDir = cmSystemTools::GetFilenamePath(qrcFile); |
442 | |
|
443 | 0 | std::size_t const firstFile = files.size(); |
444 | 0 | std::string cacheHeader; |
445 | 0 | std::string qrcContent; |
446 | 0 | if (!this->RccExecutable_.empty() && |
447 | 0 | cmSystemTools::FileExists(this->RccExecutable_, true) && |
448 | 0 | !this->ListOptions_.empty()) { |
449 | | |
450 | | // Running rcc is expensive. Reuse the listing of a previous run if |
451 | | // neither rcc nor the qrc file changed since. |
452 | 0 | if (!cacheFile.empty() && RccListReadQrcContent(qrcFile, qrcContent)) { |
453 | 0 | cacheHeader = RccListCacheHeader( |
454 | 0 | this->RccExecutable_, this->ListOptions_, qrcFile, qrcContent); |
455 | 0 | if (!cacheHeader.empty() && |
456 | 0 | RccListReadCache(cacheFile, cacheHeader, files)) { |
457 | 0 | return true; |
458 | 0 | } |
459 | 0 | } |
460 | | |
461 | 0 | bool result = false; |
462 | 0 | int retVal = 0; |
463 | 0 | std::string rccStdOut; |
464 | 0 | std::string rccStdErr; |
465 | 0 | { |
466 | 0 | std::vector<std::string> cmd; |
467 | 0 | cmd.emplace_back(this->RccExecutable_); |
468 | 0 | cm::append(cmd, this->ListOptions_); |
469 | 0 | cmd.emplace_back(cmSystemTools::GetFilenameName(qrcFile)); |
470 | | |
471 | | // Log command |
472 | 0 | if (verbose) { |
473 | 0 | cmSystemTools::Stdout( |
474 | 0 | cmStrCat("Running command:\n", QuotedCommand(cmd), '\n')); |
475 | 0 | } |
476 | |
|
477 | 0 | result = cmSystemTools::RunSingleCommand( |
478 | 0 | cmd, &rccStdOut, &rccStdErr, &retVal, fileDir.c_str(), |
479 | 0 | cmSystemTools::OUTPUT_NONE, cmDuration::zero(), cmProcessOutput::Auto); |
480 | 0 | } |
481 | 0 | if (!result || retVal) { |
482 | 0 | error = |
483 | 0 | cmStrCat("The rcc list process failed for ", Quoted(qrcFile), '\n'); |
484 | 0 | if (!rccStdOut.empty()) { |
485 | 0 | error = cmStrCat(std::move(error), rccStdOut, '\n'); |
486 | 0 | } |
487 | 0 | if (!rccStdErr.empty()) { |
488 | 0 | error = cmStrCat(std::move(error), rccStdErr, '\n'); |
489 | 0 | } |
490 | 0 | return false; |
491 | 0 | } |
492 | 0 | if (!RccListParseOutput(rccStdOut, rccStdErr, files, error)) { |
493 | 0 | return false; |
494 | 0 | } |
495 | 0 | } else { |
496 | | // We can't use rcc for the file listing. |
497 | | // Read the qrc file content into string and parse it. |
498 | 0 | if (!RccListReadQrc(qrcFile, files)) { |
499 | 0 | error = |
500 | 0 | cmStrCat("The resource file ", Quoted(qrcFile), " is not readable\n"); |
501 | 0 | return false; |
502 | 0 | } |
503 | 0 | } |
504 | | |
505 | | // Convert relative paths to absolute paths |
506 | 0 | for (std::string& entry : files) { |
507 | 0 | entry = cmSystemTools::CollapseFullPath(entry, fileDir); |
508 | 0 | } |
509 | |
|
510 | 0 | if (!cacheHeader.empty()) { |
511 | 0 | RccListWriteCache(cacheFile, cacheHeader, qrcContent, fileDir, |
512 | 0 | files.cbegin() + firstFile, files.cend()); |
513 | 0 | } |
514 | 0 | return true; |
515 | 0 | } |