Coverage Report

Created: 2026-10-28 06:38

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}