Coverage Report

Created: 2026-02-09 06:05

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