Coverage Report

Created: 2026-10-28 06:38

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmGeneratorTarget_HeaderSetVerification.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
/* clang-format off */
4
#include "cmGeneratorTarget.h"
5
/* clang-format on */
6
7
#include <algorithm>
8
#include <array>
9
#include <initializer_list>
10
#include <map>
11
#include <ostream>
12
#include <set>
13
#include <string>
14
#include <utility>
15
#include <vector>
16
17
#include <cm/memory>
18
#include <cm/optional>
19
#include <cm/string_view>
20
#include <cmext/string_view>
21
22
#include "cmFileSetMetadata.h"
23
#include "cmGenExContext.h"
24
#include "cmGeneratedFileStream.h"
25
#include "cmGeneratorExpression.h"
26
#include "cmGeneratorFileSet.h"
27
#include "cmGlobalGenerator.h"
28
#include "cmList.h"
29
#include "cmLocalGenerator.h"
30
#include "cmMakefile.h"
31
#include "cmMessageType.h"
32
#include "cmPolicies.h"
33
#include "cmSourceFile.h"
34
#include "cmSourceFileLocation.h"
35
#include "cmStringAlgorithms.h"
36
#include "cmSystemTools.h"
37
#include "cmTarget.h"
38
#include "cmTargetLinkLibraryType.h"
39
#include "cmTargetTypes.h"
40
#include "cmValue.h"
41
42
bool cmGeneratorTarget::AddHeaderSetVerification()
43
0
{
44
0
  for (bool const isInterface : { false, true }) {
45
0
    if (!this->GetPropertyAsBool(isInterface ? "VERIFY_INTERFACE_HEADER_SETS"
46
0
                                             : "VERIFY_PRIVATE_HEADER_SETS")) {
47
0
      continue;
48
0
    }
49
50
0
    if (this->GetType() != cm::TargetType::STATIC_LIBRARY &&
51
0
        this->GetType() != cm::TargetType::SHARED_LIBRARY &&
52
0
        (this->GetType() != cm::TargetType::MODULE_LIBRARY || isInterface) &&
53
0
        this->GetType() != cm::TargetType::UNKNOWN_LIBRARY &&
54
0
        this->GetType() != cm::TargetType::OBJECT_LIBRARY &&
55
0
        this->GetType() != cm::TargetType::INTERFACE_LIBRARY &&
56
0
        this->GetType() != cm::TargetType::EXECUTABLE) {
57
0
      continue;
58
0
    }
59
60
0
    char const* headerSetsProperty = isInterface
61
0
      ? "INTERFACE_HEADER_SETS_TO_VERIFY"
62
0
      : "PRIVATE_HEADER_SETS_TO_VERIFY";
63
64
0
    auto verifyValue = this->GetProperty(headerSetsProperty);
65
0
    bool const all = verifyValue.IsEmpty();
66
0
    std::set<std::string> verifySet;
67
0
    if (!all) {
68
0
      cmList verifyList{ verifyValue };
69
0
      verifySet.insert(verifyList.begin(), verifyList.end());
70
0
    }
71
72
0
    cmTarget* verifyTarget = nullptr;
73
0
    std::string const verifyTargetName =
74
0
      cmStrCat(this->GetName(),
75
0
               isInterface ? "_verify_interface_header_sets"
76
0
                           : "_verify_private_header_sets");
77
78
0
    char const* allVerifyTargetName = isInterface
79
0
      ? "all_verify_interface_header_sets"
80
0
      : "all_verify_private_header_sets";
81
0
    cmTarget* allVerifyTarget =
82
0
      this->GlobalGenerator->GetMakefiles().front()->FindTargetToUse(
83
0
        allVerifyTargetName, { cm::TargetDomain::NATIVE });
84
85
0
    auto fileSetEntries = isInterface
86
0
      ? this->GetInterfaceFileSets(cm::FileSetMetadata::HEADERS)
87
0
      : this->GetFileSets(cm::FileSetMetadata::HEADERS);
88
89
0
    std::set<cmGeneratorFileSet const*> fileSets;
90
0
    for (auto const& fileSet : fileSetEntries) {
91
0
      if (all || verifySet.count(fileSet->GetName())) {
92
0
        verifySet.erase(fileSet->GetName());
93
0
        if (fileSet->GetProperty("SKIP_LINTING").IsOn()) {
94
0
          continue;
95
0
        }
96
0
        fileSets.insert(fileSet);
97
0
      }
98
0
    }
99
100
0
    if (isInterface) {
101
0
      cmPolicies::PolicyStatus const cmp0209 = this->GetPolicyStatusCMP0209();
102
0
      if (cmp0209 != cmPolicies::NEW &&
103
0
          this->GetType() == cm::TargetType::EXECUTABLE &&
104
0
          !this->GetPropertyAsBool("ENABLE_EXPORTS")) {
105
0
        if (cmp0209 == cmPolicies::WARN && !fileSets.empty()) {
106
0
          this->Makefile->IssuePolicyWarning(
107
0
            cmPolicies::CMP0209, {},
108
0
            cmStrCat("Executable target \""_s, this->GetName(),
109
0
                     "\" has interface header file sets, but it does not "
110
0
                     "enable exports. Those headers would be verified under "
111
0
                     "CMP0209 NEW behavior.\n"_s));
112
0
        }
113
0
        continue;
114
0
      }
115
0
    }
116
117
0
    if (!verifySet.empty()) {
118
0
      this->Makefile->IssueMessage(
119
0
        MessageType::FATAL_ERROR,
120
0
        cmStrCat("Property ", headerSetsProperty, " of target \"",
121
0
                 this->GetName(),
122
0
                 "\" contained the following header sets that are nonexistent "
123
0
                 "or not ",
124
0
                 isInterface ? "INTERFACE" : "PRIVATE", ":\n  ",
125
0
                 cmJoin(verifySet, "\n  ")));
126
0
      return false;
127
0
    }
128
129
0
    cm::optional<cm::optional<std::string>> defaultLanguage;
130
131
    // First, collect all verification stubs before creating the target,
132
    // so we know whether to create an OBJECT library or not.
133
0
    std::vector<std::string> stubSources;
134
0
    for (auto const* fileSet : fileSets) {
135
0
      auto const& dirCges = fileSet->CompileDirectoryEntries();
136
0
      auto const& fileCges = fileSet->CompileFileEntries();
137
138
0
      static auto const contextSensitive =
139
0
        [](std::unique_ptr<cmCompiledGeneratorExpression> const& cge) {
140
0
          return cge->GetHadContextSensitiveCondition();
141
0
        };
142
0
      bool dirCgesContextSensitive = false;
143
0
      bool fileCgesContextSensitive = false;
144
145
0
      std::vector<std::string> dirs;
146
0
      std::map<std::string, std::vector<std::string>> filesPerDir;
147
0
      bool first = true;
148
0
      for (auto const& config : this->Makefile->GetGeneratorConfigs(
149
0
             cmMakefile::GeneratorConfigQuery::IncludeEmptyConfig)) {
150
0
        cm::GenEx::Context context(this->LocalGenerator, config);
151
0
        if (first || dirCgesContextSensitive) {
152
0
          dirs = fileSet->EvaluateDirectoryEntries(dirCges, context, this);
153
0
          dirCgesContextSensitive =
154
0
            std::any_of(dirCges.begin(), dirCges.end(), contextSensitive);
155
0
        }
156
0
        if (first || fileCgesContextSensitive) {
157
0
          filesPerDir.clear();
158
0
          for (auto const& fileCge : fileCges) {
159
0
            fileSet->EvaluateFileEntry(dirs, filesPerDir, fileCge, context,
160
0
                                       this);
161
0
            if (fileCge->GetHadContextSensitiveCondition()) {
162
0
              fileCgesContextSensitive = true;
163
0
            }
164
0
          }
165
0
        }
166
167
0
        for (auto const& files : filesPerDir) {
168
0
          for (auto const& file : files.second) {
169
0
            cm::optional<std::string> filenameOpt =
170
0
              this->GenerateHeaderSetVerificationFile(
171
0
                *this->Makefile->GetOrCreateSource(file), files.first,
172
0
                verifyTargetName, defaultLanguage);
173
0
            if (!filenameOpt) {
174
0
              continue;
175
0
            }
176
0
            std::string filename = *filenameOpt;
177
178
0
            if (fileCgesContextSensitive) {
179
0
              filename = cmStrCat("$<$<CONFIG:", config,
180
0
                                  ">:", std::move(filename), '>');
181
0
            }
182
0
            stubSources.emplace_back(std::move(filename));
183
0
          }
184
0
        }
185
186
0
        if (!dirCgesContextSensitive && !fileCgesContextSensitive) {
187
0
          break;
188
0
        }
189
0
        first = false;
190
0
      }
191
0
    }
192
193
0
    if (stubSources.empty()) {
194
      // No headers to verify. Create a utility target so the target
195
      // name always exists (e.g. for build system dependencies) without
196
      // needing a placeholder source. This avoids warnings from tools
197
      // like Xcode's libtool about empty static libraries.
198
0
      verifyTarget =
199
0
        this->Makefile->AddNewUtilityTarget(verifyTargetName, true);
200
0
    } else {
201
      // Create an OBJECT library to compile the verification stubs.
202
0
      {
203
0
        cmMakefile::PolicyPushPop polScope(this->Makefile);
204
0
        this->Makefile->SetPolicy(cmPolicies::CMP0119, cmPolicies::NEW);
205
0
        verifyTarget = this->Makefile->AddLibrary(
206
0
          verifyTargetName, cm::TargetType::OBJECT_LIBRARY, {}, true);
207
0
      }
208
209
0
      if (cmValue mode = this->Target->GetProperty("SYCL_EXTENSION_MODE")) {
210
0
        verifyTarget->SetProperty("SYCL_EXTENSION_MODE", mode);
211
0
      }
212
213
0
      if (isInterface) {
214
        // Link to the original target so that we pick up its
215
        // interface compile options just like a consumer would.
216
        // This also ensures any generated headers in the original
217
        // target will be created.
218
0
        verifyTarget->AddLinkLibrary(
219
0
          *this->Makefile, this->GetName(),
220
0
          cmTargetLinkLibraryType::GENERAL_LibraryType);
221
0
      } else {
222
        // For private file sets, we need to simulate compiling the
223
        // same way as the original target. That includes linking to
224
        // the same things so we pick up the same transitive
225
        // properties. For the <LANG>_... properties, we don't care if
226
        // we set them for languages this target won't eventually use.
227
        // Copy language-standard properties for all supported
228
        // languages. We don't care if we set properties for languages
229
        // this target won't eventually use.
230
0
        static std::array<std::string, 22> const propertiesToCopy{ {
231
0
          "COMPILE_DEFINITIONS",
232
0
          "COMPILE_FEATURES",
233
0
          "COMPILE_FLAGS",
234
0
          "COMPILE_OPTIONS",
235
0
          "DEFINE_SYMBOL",
236
0
          "INCLUDE_DIRECTORIES",
237
0
          "LINK_LIBRARIES",
238
0
          "C_STANDARD",
239
0
          "C_STANDARD_REQUIRED",
240
0
          "C_EXTENSIONS",
241
0
          "CXX_STANDARD",
242
0
          "CXX_STANDARD_REQUIRED",
243
0
          "CXX_EXTENSIONS",
244
0
          "OBJC_STANDARD",
245
0
          "OBJC_STANDARD_REQUIRED",
246
0
          "OBJC_EXTENSIONS",
247
0
          "OBJCXX_STANDARD",
248
0
          "OBJCXX_STANDARD_REQUIRED",
249
0
          "OBJCXX_EXTENSIONS",
250
0
          "SYCL_CXX_STANDARD",
251
0
          "SYCL_CXX_STANDARD_REQUIRED",
252
0
          "SYCL_EXTENSIONS",
253
0
        } };
254
0
        for (std::string const& prop : propertiesToCopy) {
255
0
          cmValue propValue = this->Target->GetProperty(prop);
256
0
          if (propValue.IsSet()) {
257
0
            verifyTarget->SetProperty(prop, propValue);
258
0
          }
259
0
        }
260
        // The original target might have generated headers. Since
261
        // we only link to the original target for compilation,
262
        // there's nothing to force such generation to happen yet.
263
        // Our verify target must depend on the original target to
264
        // ensure such generated files will be created.
265
0
        verifyTarget->AddUtility(this->GetName(), false, this->Makefile);
266
0
        verifyTarget->AddCodegenDependency(this->GetName());
267
0
      }
268
269
0
      verifyTarget->SetProperty("AUTOMOC", "OFF");
270
0
      verifyTarget->SetProperty("AUTORCC", "OFF");
271
0
      verifyTarget->SetProperty("AUTOUIC", "OFF");
272
0
      verifyTarget->SetProperty("DISABLE_PRECOMPILE_HEADERS", "ON");
273
0
      verifyTarget->SetProperty("UNITY_BUILD", "OFF");
274
0
      verifyTarget->SetProperty("CXX_SCAN_FOR_MODULES", "OFF");
275
276
0
      if (isInterface) {
277
0
        verifyTarget->FinalizeTargetConfiguration(
278
0
          this->Makefile->GetCompileDefinitionsEntries());
279
0
      } else {
280
        // Private verification only needs to add the directory scope
281
        // definitions here
282
0
        for (auto const& def :
283
0
             this->Makefile->GetCompileDefinitionsEntries()) {
284
0
          verifyTarget->InsertCompileDefinition(def);
285
0
        }
286
0
      }
287
288
0
      for (auto const& source : stubSources) {
289
0
        verifyTarget->AddSource(source);
290
0
      }
291
0
    }
292
0
    if (!allVerifyTarget) {
293
0
      allVerifyTarget =
294
0
        this->GlobalGenerator->GetMakefiles().front()->AddNewUtilityTarget(
295
0
          allVerifyTargetName, true);
296
0
    }
297
0
    allVerifyTarget->AddUtility(verifyTargetName, false);
298
299
0
    this->LocalGenerator->AddGeneratorTarget(
300
0
      cm::make_unique<cmGeneratorTarget>(verifyTarget, this->LocalGenerator));
301
0
  }
302
0
  return true;
303
0
}
304
305
cm::optional<std::string> cmGeneratorTarget::GenerateHeaderSetVerificationFile(
306
  cmSourceFile& source, std::string const& dir,
307
  std::string const& verifyTargetName,
308
  cm::optional<cm::optional<std::string>>& defaultLanguage) const
309
0
{
310
0
  if (source.GetPropertyAsBool("SKIP_LINTING")) {
311
0
    return cm::nullopt;
312
0
  }
313
314
0
  cm::optional<std::string> language =
315
0
    this->ResolveHeaderLanguage(source, defaultLanguage);
316
0
  if (!language) {
317
0
    return cm::nullopt;
318
0
  }
319
320
0
  std::string headerFilename = dir;
321
0
  if (!headerFilename.empty()) {
322
0
    headerFilename += '/';
323
0
  }
324
0
  headerFilename += source.GetLocation().GetName();
325
326
0
  return this->GenerateStubForLanguage(*language, headerFilename,
327
0
                                       verifyTargetName, source);
328
0
}
329
330
cm::optional<std::string> cmGeneratorTarget::ResolveHeaderLanguage(
331
  cmSourceFile& source,
332
  cm::optional<cm::optional<std::string>>& defaultLanguage) const
333
0
{
334
0
  static std::array<cm::string_view, 5> const supportedLangs{ {
335
0
    "C",
336
0
    "CXX",
337
0
    "OBJC",
338
0
    "OBJCXX",
339
0
    "SYCL",
340
0
  } };
341
0
  auto isSupported = [](cm::string_view lang) -> bool {
342
0
    return std::find(supportedLangs.begin(), supportedLangs.end(), lang) !=
343
0
      supportedLangs.end();
344
0
  };
345
346
  // If the source has an explicit language, validate and return it.
347
0
  std::string language = source.GetOrDetermineLanguage();
348
0
  if (!language.empty()) {
349
0
    if (!isSupported(language)) {
350
0
      return cm::nullopt;
351
0
    }
352
0
    return cm::optional<std::string>(std::move(language));
353
0
  }
354
355
  /*
356
    Compute and cache the default language for unlanguaged headers.
357
    The lattice join is run once per file set, not once per header.
358
    Lattice:   OBJCXX
359
              /      \
360
            CXX      OBJC
361
              \      /
362
                 C
363
  */
364
0
  if (!defaultLanguage) {
365
0
    std::set<std::string> langs;
366
0
    for (std::string const& lang : this->GetAllConfigCompileLanguages()) {
367
0
      if (isSupported(lang)) {
368
0
        langs.insert(lang);
369
0
      }
370
0
    }
371
0
    if (langs.empty()) {
372
0
      std::vector<std::string> languagesVector;
373
0
      this->GlobalGenerator->GetEnabledLanguages(languagesVector);
374
0
      for (std::string const& lang : languagesVector) {
375
0
        if (isSupported(lang)) {
376
0
          langs.insert(lang);
377
0
        }
378
0
      }
379
0
    }
380
381
0
    cm::optional<std::string> resolved;
382
0
    if (langs.count("OBJCXX") ||
383
0
        ((langs.count("CXX") || langs.count("SYCL")) && langs.count("OBJC"))) {
384
0
      resolved = "OBJCXX"; // promote
385
0
    } else if (langs.count("SYCL")) {
386
0
      resolved = "SYCL";
387
0
    } else if (langs.count("CXX")) {
388
0
      resolved = "CXX";
389
0
    } else if (langs.count("OBJC")) {
390
0
      resolved = "OBJC";
391
0
    } else if (langs.count("C")) {
392
0
      resolved = "C";
393
0
    }
394
0
    defaultLanguage = resolved;
395
0
  }
396
397
0
  return *defaultLanguage;
398
0
}
399
400
cm::optional<std::string> cmGeneratorTarget::GenerateStubForLanguage(
401
  std::string const& language, std::string const& headerFilename,
402
  std::string const& verifyTargetName, cmSourceFile& source) const
403
0
{
404
0
  static std::array<std::pair<cm::string_view, cm::string_view>, 5> const
405
0
    langToExt = { {
406
0
      { "C", ".c" },
407
0
      { "CXX", ".cxx" },
408
0
      { "OBJC", ".m" },
409
0
      { "OBJCXX", ".mm" },
410
0
      { "SYCL", ".sycl" },
411
0
    } };
412
413
  // NOLINTNEXTLINE(readability-qualified-auto)
414
0
  auto const it =
415
0
    std::find_if(langToExt.begin(), langToExt.end(),
416
0
                 [&](std::pair<cm::string_view, cm::string_view> const& p) {
417
0
                   return p.first == language;
418
0
                 });
419
0
  if (it == langToExt.end()) {
420
0
    return cm::nullopt;
421
0
  }
422
423
0
  std::string filename =
424
0
    cmStrCat(this->LocalGenerator->GetCurrentBinaryDirectory(), '/',
425
0
             verifyTargetName, '/', headerFilename, it->second);
426
427
0
  cmSourceFile* verificationSource =
428
0
    this->Makefile->GetOrCreateSource(filename);
429
0
  source.SetSpecialSourceType(
430
0
    cmSourceFile::SpecialSourceType::HeaderSetVerificationSource);
431
0
  verificationSource->SetProperty("LANGUAGE", language);
432
433
0
  cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(filename));
434
435
0
  cmGeneratedFileStream fout(filename);
436
0
  fout.SetCopyIfDifferent(true);
437
  // The IWYU "associated" pragma tells include-what-you-use to
438
  // consider the headerFile as part of the entire language
439
  // unit within include-what-you-use and as a result allows
440
  // one to get IWYU advice for headers.
441
  // Also suppress clang-tidy include checks in generated code.
442
0
  fout
443
0
    << "/* NOLINTNEXTLINE(misc-header-include-cycle,misc-include-cleaner) */\n"
444
0
    << "#include <" << headerFilename << "> /* IWYU pragma: associated */\n";
445
0
  fout.close();
446
447
0
  return cm::optional<std::string>(std::move(filename));
448
0
}