Coverage Report

Created: 2026-03-12 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmCxxModuleMetadata.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
4
#include "cmCxxModuleMetadata.h"
5
6
#include <algorithm>
7
#include <set>
8
#include <string>
9
#include <utility>
10
11
#include <cmext/string_view>
12
13
#include <cm3p/json/value.h>
14
#include <cm3p/json/writer.h>
15
16
#include "cmsys/FStream.hxx"
17
18
#include "cmFileSet.h"
19
#include "cmFileSetMetadata.h"
20
#include "cmGeneratedFileStream.h"
21
#include "cmJSONState.h"
22
#include "cmListFileCache.h"
23
#include "cmStringAlgorithms.h"
24
#include "cmSystemTools.h"
25
#include "cmTarget.h"
26
27
namespace {
28
29
bool JsonIsStringArray(Json::Value const& v)
30
0
{
31
0
  return v.isArray() &&
32
0
    std::all_of(v.begin(), v.end(),
33
0
                [](Json::Value const& it) { return it.isString(); });
34
0
}
35
36
bool ParsePreprocessorDefine(Json::Value& dval,
37
                             cmCxxModuleMetadata::PreprocessorDefineData& out,
38
                             cmJSONState* state)
39
0
{
40
0
  if (!dval.isObject()) {
41
0
    state->AddErrorAtValue("each entry in 'definitions' must be an object",
42
0
                           &dval);
43
0
    return false;
44
0
  }
45
46
0
  if (!dval.isMember("name") || !dval["name"].isString() ||
47
0
      dval["name"].asString().empty()) {
48
0
    state->AddErrorAtValue(
49
0
      "preprocessor definition requires a non-empty 'name'", &dval["name"]);
50
0
    return false;
51
0
  }
52
0
  out.Name = dval["name"].asString();
53
54
0
  if (dval.isMember("value")) {
55
0
    if (dval["value"].isString()) {
56
0
      out.Value = dval["value"].asString();
57
0
    } else if (!dval["value"].isNull()) {
58
0
      state->AddErrorAtValue(
59
0
        "'value' in preprocessor definition must be string or null",
60
0
        &dval["value"]);
61
0
      return false;
62
0
    }
63
0
  }
64
65
0
  if (dval.isMember("undef")) {
66
0
    if (!dval["undef"].isBool()) {
67
0
      state->AddErrorAtValue(
68
0
        "'undef' in preprocessor definition must be boolean", &dval["undef"]);
69
0
      return false;
70
0
    }
71
0
    out.Undef = dval["undef"].asBool();
72
0
  }
73
74
0
  return true;
75
0
}
76
77
bool ParseCMakeLocalArgumentsVendor(
78
  Json::Value& cmlav, cmCxxModuleMetadata::LocalArgumentsData& out,
79
  cmJSONState* state)
80
0
{
81
82
0
  if (!cmlav.isObject()) {
83
0
    state->AddErrorAtValue("'vendor' must be an object", &cmlav);
84
0
    return false;
85
0
  }
86
87
0
  if (cmlav.isMember("compile-options")) {
88
0
    if (!JsonIsStringArray(cmlav["compile-options"])) {
89
0
      state->AddErrorAtValue("'compile-options' must be an array of strings",
90
0
                             &cmlav["compile-options"]);
91
0
      return false;
92
0
    }
93
0
    for (auto const& s : cmlav["compile-options"]) {
94
0
      out.CompileOptions.push_back(s.asString());
95
0
    }
96
0
  }
97
98
0
  if (cmlav.isMember("compile-features")) {
99
0
    if (!JsonIsStringArray(cmlav["compile-features"])) {
100
0
      state->AddErrorAtValue("'compile-features' must be an array of strings",
101
0
                             &cmlav["compile-features"]);
102
0
      return false;
103
0
    }
104
0
    for (auto const& s : cmlav["compile-features"]) {
105
0
      out.CompileFeatures.push_back(s.asString());
106
0
    }
107
0
  }
108
109
0
  return true;
110
0
}
111
112
bool ParseLocalArguments(Json::Value& lav,
113
                         cmCxxModuleMetadata::LocalArgumentsData& out,
114
                         cmJSONState* state)
115
0
{
116
0
  if (!lav.isObject()) {
117
0
    state->AddErrorAtValue("'local-arguments' must be an object", &lav);
118
0
    return false;
119
0
  }
120
121
0
  if (lav.isMember("include-directories")) {
122
0
    if (!JsonIsStringArray(lav["include-directories"])) {
123
0
      state->AddErrorAtValue(
124
0
        "'include-directories' must be an array of strings",
125
0
        &lav["include-directories"]);
126
0
      return false;
127
0
    }
128
0
    for (auto const& s : lav["include-directories"]) {
129
0
      out.IncludeDirectories.push_back(s.asString());
130
0
    }
131
0
  }
132
133
0
  if (lav.isMember("system-include-directories")) {
134
0
    if (!JsonIsStringArray(lav["system-include-directories"])) {
135
0
      state->AddErrorAtValue(
136
0
        "'system-include-directories' must be an array of strings",
137
0
        &lav["system-include-directories"]);
138
0
      return false;
139
0
    }
140
0
    for (auto const& s : lav["system-include-directories"]) {
141
0
      out.SystemIncludeDirectories.push_back(s.asString());
142
0
    }
143
0
  }
144
145
0
  if (lav.isMember("definitions")) {
146
0
    if (!lav["definitions"].isArray()) {
147
0
      state->AddErrorAtValue("'definitions' must be an array",
148
0
                             &lav["definitions"]);
149
0
      return false;
150
0
    }
151
0
    for (Json::Value& dval : lav["definitions"]) {
152
0
      out.Definitions.emplace_back();
153
0
      if (!ParsePreprocessorDefine(dval, out.Definitions.back(), state)) {
154
0
        return false;
155
0
      }
156
0
    }
157
0
  }
158
159
0
  if (lav.isMember("vendor")) {
160
0
    if (!ParseCMakeLocalArgumentsVendor(lav["vendor"], out, state)) {
161
0
      return false;
162
0
    }
163
0
  }
164
165
0
  return true;
166
0
}
167
168
bool ParseModule(Json::Value& mval, cmCxxModuleMetadata::ModuleData& mod,
169
                 cmJSONState* state)
170
0
{
171
0
  if (!mval.isObject()) {
172
0
    state->AddErrorAtValue("each entry in 'modules' must be an object", &mval);
173
0
    return false;
174
0
  }
175
176
0
  if (!mval.isMember("logical-name") || !mval["logical-name"].isString() ||
177
0
      mval["logical-name"].asString().empty()) {
178
0
    state->AddErrorAtValue(
179
0
      "module entries require a non-empty 'logical-name' string",
180
0
      &mval["logical-name"]);
181
0
    return false;
182
0
  }
183
0
  mod.LogicalName = mval["logical-name"].asString();
184
185
0
  if (!mval.isMember("source-path") || !mval["source-path"].isString() ||
186
0
      mval["source-path"].asString().empty()) {
187
0
    state->AddErrorAtValue(
188
0
      "module entries require a non-empty 'source-path' string",
189
0
      &mval["source-path"]);
190
0
    return false;
191
0
  }
192
0
  mod.SourcePath = mval["source-path"].asString();
193
194
0
  if (mval.isMember("is-interface")) {
195
0
    if (!mval["is-interface"].isBool()) {
196
0
      state->AddErrorAtValue("'is-interface' must be boolean",
197
0
                             &mval["is-interface"]);
198
0
      return false;
199
0
    }
200
0
    mod.IsInterface = mval["is-interface"].asBool();
201
0
  } else {
202
0
    mod.IsInterface = true;
203
0
  }
204
205
0
  if (mval.isMember("is-std-library")) {
206
0
    if (!mval["is-std-library"].isBool()) {
207
0
      state->AddErrorAtValue("'is-std-library' must be boolean",
208
0
                             &mval["is-std-library"]);
209
0
      return false;
210
0
    }
211
0
    mod.IsStdLibrary = mval["is-std-library"].asBool();
212
0
  } else {
213
0
    mod.IsStdLibrary = false;
214
0
  }
215
216
0
  if (mval.isMember("local-arguments")) {
217
0
    mod.LocalArguments.emplace();
218
0
    if (!ParseLocalArguments(mval["local-arguments"], *mod.LocalArguments,
219
0
                             state)) {
220
0
      return false;
221
0
    }
222
0
  }
223
224
0
  return true;
225
0
}
226
227
bool ParseRoot(Json::Value& root, cmCxxModuleMetadata& meta,
228
               cmJSONState* state)
229
0
{
230
0
  if (!root.isMember("version") || !root["version"].isInt()) {
231
0
    state->AddErrorAtValue(
232
0
      "Top-level member 'version' is required and must be an integer", &root);
233
0
    return false;
234
0
  }
235
0
  meta.Version = root["version"].asInt();
236
237
0
  if (root.isMember("revision")) {
238
0
    if (!root["revision"].isInt()) {
239
0
      state->AddErrorAtValue("'revision' must be an integer",
240
0
                             &root["revision"]);
241
0
      return false;
242
0
    }
243
0
    meta.Revision = root["revision"].asInt();
244
0
  }
245
246
0
  if (meta.Version != 1) {
247
0
    state->AddErrorAtValue(cmStrCat("Module manifest version number, '",
248
0
                                    meta.Version, '.', meta.Revision,
249
0
                                    "' is newer than max supported (1.1)"),
250
0
                           &root);
251
0
    return false;
252
0
  }
253
254
0
  if (root.isMember("modules")) {
255
0
    if (!root["modules"].isArray()) {
256
0
      state->AddErrorAtValue("'modules' must be an array", &root["modules"]);
257
0
      return false;
258
0
    }
259
0
    for (Json::Value& mval : root["modules"]) {
260
0
      meta.Modules.emplace_back();
261
0
      if (!ParseModule(mval, meta.Modules.back(), state)) {
262
0
        return false;
263
0
      }
264
0
    }
265
0
  }
266
267
0
  return true;
268
0
}
269
270
} // namespace
271
272
cmCxxModuleMetadata::ParseResult cmCxxModuleMetadata::LoadFromFile(
273
  std::string const& path)
274
0
{
275
0
  ParseResult res;
276
277
0
  Json::Value root;
278
0
  cmJSONState parseState(path, &root);
279
0
  if (!parseState.errors.empty()) {
280
0
    res.Error = parseState.GetErrorMessage();
281
0
    return res;
282
0
  }
283
284
0
  cmCxxModuleMetadata meta;
285
0
  if (!ParseRoot(root, meta, &parseState)) {
286
0
    res.Error = parseState.GetErrorMessage();
287
0
    return res;
288
0
  }
289
290
0
  meta.MetadataFilePath = path;
291
0
  res.Meta = std::move(meta);
292
0
  return res;
293
0
}
294
295
namespace {
296
297
Json::Value SerializePreprocessorDefine(
298
  cmCxxModuleMetadata::PreprocessorDefineData const& d)
299
0
{
300
0
  Json::Value dv(Json::objectValue);
301
0
  dv["name"] = d.Name;
302
0
  if (d.Value) {
303
0
    dv["value"] = *d.Value;
304
0
  }
305
0
  if (d.Undef) {
306
0
    dv["undef"] = d.Undef;
307
0
  }
308
0
  return dv;
309
0
}
310
311
Json::Value SerializeCMakeLocalArgumentsVendor(
312
  cmCxxModuleMetadata::LocalArgumentsData const& la)
313
0
{
314
0
  Json::Value vend(Json::objectValue);
315
316
0
  if (!la.CompileOptions.empty()) {
317
0
    Json::Value& opts = vend["compile-options"] = Json::arrayValue;
318
0
    for (auto const& s : la.CompileOptions) {
319
0
      opts.append(s);
320
0
    }
321
0
  }
322
323
0
  if (!la.CompileFeatures.empty()) {
324
0
    Json::Value& feats = vend["compile-features"] = Json::arrayValue;
325
0
    for (auto const& s : la.CompileFeatures) {
326
0
      feats.append(s);
327
0
    }
328
0
  }
329
330
0
  return vend;
331
0
}
332
333
Json::Value SerializeLocalArguments(
334
  cmCxxModuleMetadata::LocalArgumentsData const& la)
335
0
{
336
0
  Json::Value lav(Json::objectValue);
337
338
0
  if (!la.IncludeDirectories.empty()) {
339
0
    Json::Value& inc = lav["include-directories"] = Json::arrayValue;
340
0
    for (auto const& s : la.IncludeDirectories) {
341
0
      inc.append(s);
342
0
    }
343
0
  }
344
345
0
  if (!la.SystemIncludeDirectories.empty()) {
346
0
    Json::Value& sinc = lav["system-include-directories"] = Json::arrayValue;
347
0
    for (auto const& s : la.SystemIncludeDirectories) {
348
0
      sinc.append(s);
349
0
    }
350
0
  }
351
352
0
  if (!la.Definitions.empty()) {
353
0
    Json::Value& defs = lav["definitions"] = Json::arrayValue;
354
0
    for (auto const& d : la.Definitions) {
355
0
      defs.append(SerializePreprocessorDefine(d));
356
0
    }
357
0
  }
358
359
0
  Json::Value vend = SerializeCMakeLocalArgumentsVendor(la);
360
0
  if (!vend.empty()) {
361
0
    Json::Value& cmvend = lav["vendor"] = Json::objectValue;
362
0
    cmvend["cmake"] = std::move(vend);
363
0
  }
364
365
0
  return lav;
366
0
}
367
368
Json::Value SerializeModule(std::string& manifestRoot,
369
                            cmCxxModuleMetadata::ModuleData const& m)
370
0
{
371
0
  Json::Value mv(Json::objectValue);
372
0
  mv["logical-name"] = m.LogicalName;
373
0
  if (cmSystemTools::FileIsFullPath(m.SourcePath)) {
374
0
    mv["source-path"] = m.SourcePath;
375
0
  } else {
376
0
    mv["source-path"] = cmSystemTools::ForceToRelativePath(
377
0
      manifestRoot, cmStrCat('/', m.SourcePath));
378
0
  }
379
0
  mv["is-interface"] = m.IsInterface;
380
0
  mv["is-std-library"] = m.IsStdLibrary;
381
382
0
  if (m.LocalArguments) {
383
0
    mv["local-arguments"] = SerializeLocalArguments(*m.LocalArguments);
384
0
  }
385
386
0
  return mv;
387
0
}
388
389
} // namespace
390
391
Json::Value cmCxxModuleMetadata::ToJsonValue(cmCxxModuleMetadata const& meta)
392
0
{
393
0
  Json::Value root(Json::objectValue);
394
395
0
  root["version"] = meta.Version;
396
0
  root["revision"] = meta.Revision;
397
398
0
  Json::Value& modules = root["modules"] = Json::arrayValue;
399
0
  std::string manifestRoot =
400
0
    cmSystemTools::GetFilenamePath(meta.MetadataFilePath);
401
402
0
  if (!cmSystemTools::FileIsFullPath(meta.MetadataFilePath)) {
403
0
    manifestRoot = cmStrCat('/', manifestRoot);
404
0
  }
405
406
0
  for (auto const& m : meta.Modules) {
407
0
    modules.append(SerializeModule(manifestRoot, m));
408
0
  }
409
410
0
  return root;
411
0
}
412
413
cmCxxModuleMetadata::SaveResult cmCxxModuleMetadata::SaveToFile(
414
  std::string const& path, cmCxxModuleMetadata const& meta)
415
0
{
416
0
  SaveResult st;
417
418
0
  cmGeneratedFileStream ofs(path);
419
0
  if (!ofs.is_open()) {
420
0
    st.Error = "Unable to open temp file for writing";
421
0
    return st;
422
0
  }
423
424
0
  Json::StreamWriterBuilder wbuilder;
425
0
  wbuilder["indentation"] = "  ";
426
0
  ofs << Json::writeString(wbuilder, ToJsonValue(meta));
427
428
0
  ofs.Close();
429
430
0
  if (!ofs.good()) {
431
0
    st.Error = cmStrCat("Write failed for file: "_s, path);
432
0
    return st;
433
0
  }
434
435
0
  st.Ok = true;
436
0
  return st;
437
0
}
438
439
namespace {
440
441
struct MetaDataProperties
442
{
443
  std::string MetadataDir;
444
  std::set<cm::string_view> AllCompileFeatures;
445
  std::set<cm::string_view> AllCompileOptions;
446
  std::set<std::string> AllIncludeDirectories;
447
  std::set<std::string> AllCompileDefinitions;
448
  std::set<std::string> BaseDirs;
449
  std::set<std::string> Sources;
450
451
  std::string NormalizePath(std::string const& in) const
452
0
  {
453
0
    std::string out = in;
454
0
    if (!cmSystemTools::FileIsFullPath(in)) {
455
0
      out = cmStrCat(MetadataDir, '/', in);
456
0
    }
457
0
    return cmSystemTools::CollapseFullPath(out);
458
0
  }
459
};
460
461
MetaDataProperties CollectMetaProperties(cmCxxModuleMetadata const& meta)
462
0
{
463
0
  MetaDataProperties props;
464
465
0
  props.MetadataDir = cmSystemTools::GetFilenamePath(meta.MetadataFilePath);
466
467
0
  for (auto const& module : meta.Modules) {
468
0
    std::string sourcePath = props.NormalizePath(module.SourcePath);
469
0
    props.Sources.insert(sourcePath);
470
471
    // Module metadata files can reference files in different roots,
472
    // just use the immediate parent directory as a base directory
473
0
    props.BaseDirs.insert(cmSystemTools::GetFilenamePath(sourcePath));
474
475
0
    if (module.LocalArguments) {
476
0
      for (auto const& incDir : module.LocalArguments->IncludeDirectories) {
477
0
        props.AllIncludeDirectories.emplace(props.NormalizePath(incDir));
478
0
      }
479
0
      for (auto const& sysIncDir :
480
0
           module.LocalArguments->SystemIncludeDirectories) {
481
0
        props.AllIncludeDirectories.emplace(props.NormalizePath(sysIncDir));
482
0
      }
483
0
      for (auto const& opt : module.LocalArguments->CompileOptions) {
484
0
        props.AllCompileOptions.emplace(opt);
485
0
      }
486
0
      for (auto const& opt : module.LocalArguments->CompileFeatures) {
487
0
        props.AllCompileFeatures.emplace(opt);
488
0
      }
489
490
0
      for (auto const& def : module.LocalArguments->Definitions) {
491
0
        if (!def.Undef) {
492
0
          if (def.Value) {
493
0
            props.AllCompileDefinitions.emplace(
494
0
              cmStrCat(def.Name, "="_s, *def.Value));
495
0
          } else {
496
0
            props.AllCompileDefinitions.emplace(def.Name);
497
0
          }
498
0
        }
499
0
      }
500
0
    }
501
0
  }
502
503
0
  return props;
504
0
}
505
506
void PopulateFileSet(cmTarget& target, MetaDataProperties const& props)
507
0
{
508
0
  auto fileSet =
509
0
    target.GetOrCreateFileSet(std::string{ cm::FileSetMetadata::CXX_MODULES },
510
0
                              std::string{ cm::FileSetMetadata::CXX_MODULES },
511
0
                              cm::FileSetMetadata::Visibility::Public);
512
513
0
  for (auto const& source : props.Sources) {
514
0
    fileSet.first->AddFileEntry(source);
515
0
  }
516
517
0
  for (auto const& baseDir : props.BaseDirs) {
518
0
    fileSet.first->AddDirectoryEntry(baseDir);
519
0
  }
520
0
}
521
522
void PopulateLocalTarget(cmTarget& target, MetaDataProperties const& props)
523
0
{
524
0
  if (!props.AllIncludeDirectories.empty()) {
525
0
    target.AppendProperty("INCLUDE_DIRECTORIES",
526
0
                          cmJoin(props.AllIncludeDirectories, ";"));
527
0
  }
528
529
0
  if (!props.AllCompileDefinitions.empty()) {
530
0
    target.AppendProperty("COMPILE_DEFINITIONS",
531
0
                          cmJoin(props.AllCompileDefinitions, ";"));
532
0
  }
533
534
0
  if (!props.AllCompileOptions.empty()) {
535
0
    target.AppendProperty("COMPILE_OPTIONS",
536
0
                          cmJoin(props.AllCompileOptions, ";"));
537
0
  }
538
539
0
  if (!props.AllCompileFeatures.empty()) {
540
0
    target.AppendProperty("COMPILE_FEATURES",
541
0
                          cmJoin(props.AllCompileFeatures, ";"));
542
0
  }
543
0
}
544
545
void PopulateImportedTarget(cmTarget& target, MetaDataProperties const& props,
546
                            cmCxxModuleMetadata const& meta,
547
                            std::vector<std::string> const& configs)
548
0
{
549
0
  if (!props.AllIncludeDirectories.empty()) {
550
0
    target.SetProperty("IMPORTED_CXX_MODULES_INCLUDE_DIRECTORIES",
551
0
                       cmJoin(props.AllIncludeDirectories, ";"));
552
0
  }
553
554
0
  if (!props.AllCompileDefinitions.empty()) {
555
0
    target.SetProperty("IMPORTED_CXX_MODULES_COMPILE_DEFINITIONS",
556
0
                       cmJoin(props.AllCompileDefinitions, ";"));
557
0
  }
558
559
0
  if (!props.AllCompileOptions.empty()) {
560
0
    target.SetProperty("IMPORTED_CXX_MODULES_COMPILE_OPTIONS",
561
0
                       cmJoin(props.AllCompileOptions, ";"));
562
0
  }
563
564
0
  if (!props.AllCompileFeatures.empty()) {
565
0
    target.SetProperty("IMPORTED_CXX_MODULES_COMPILE_FEATURES",
566
0
                       cmJoin(props.AllCompileFeatures, ";"));
567
0
  }
568
569
0
  for (auto const& config : configs) {
570
0
    std::vector<std::string> moduleList;
571
0
    for (auto const& module : meta.Modules) {
572
0
      if (module.IsInterface) {
573
0
        moduleList.push_back(cmStrCat(module.LogicalName, "="_s,
574
0
                                      props.NormalizePath(module.SourcePath)));
575
0
      }
576
0
    }
577
578
0
    if (!moduleList.empty()) {
579
0
      std::string upperConfig = cmSystemTools::UpperCase(config);
580
0
      std::string propertyName =
581
0
        cmStrCat("IMPORTED_CXX_MODULES_"_s, upperConfig);
582
0
      target.SetProperty(propertyName, cmJoin(moduleList, ";"));
583
0
    }
584
0
  }
585
0
}
586
587
} // namespace
588
589
void cmCxxModuleMetadata::PopulateTarget(
590
  cmTarget& target, cmCxxModuleMetadata const& meta,
591
  std::vector<std::string> const& configs)
592
0
{
593
0
  auto props = CollectMetaProperties(meta);
594
0
  PopulateFileSet(target, props);
595
596
0
  if (target.IsImported()) {
597
0
    PopulateImportedTarget(target, props, meta, configs);
598
0
  } else {
599
0
    PopulateLocalTarget(target, props);
600
0
  }
601
0
}