Coverage Report

Created: 2026-02-09 06:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmProjectCommand.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 "cmProjectCommand.h"
4
5
#include <array>
6
#include <cstdio>
7
#include <limits>
8
#include <set>
9
#include <utility>
10
11
#include <cm/optional>
12
#include <cm/string_view>
13
#include <cmext/string_view>
14
15
#include "cmsys/RegularExpression.hxx"
16
17
#include "cmArgumentParser.h"
18
#include "cmArgumentParserTypes.h"
19
#include "cmExecutionStatus.h"
20
#include "cmExperimental.h"
21
#include "cmList.h"
22
#include "cmMakefile.h"
23
#include "cmMessageType.h"
24
#include "cmPolicies.h"
25
#include "cmRange.h"
26
#include "cmStateTypes.h"
27
#include "cmStringAlgorithms.h"
28
#include "cmSystemTools.h"
29
#include "cmValue.h"
30
31
namespace {
32
33
bool IncludeByVariable(cmExecutionStatus& status, std::string const& variable);
34
void TopLevelCMakeVarCondSet(cmMakefile& mf, std::string const& name,
35
                             std::string const& value);
36
37
struct ProjectArguments : ArgumentParser::ParseResult
38
{
39
  cm::optional<std::string> Version;
40
  cm::optional<std::string> CompatVersion;
41
  cm::optional<std::string> License;
42
  cm::optional<std::string> Description;
43
  cm::optional<std::string> HomepageURL;
44
  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>> Languages;
45
};
46
47
struct ProjectArgumentParser : public cmArgumentParser<void>
48
{
49
  ProjectArgumentParser& BindKeywordMissingValue(
50
    std::vector<cm::string_view>& ref)
51
0
  {
52
0
    this->cmArgumentParser<void>::BindKeywordMissingValue(
53
0
      [&ref](Instance&, cm::string_view arg) { ref.emplace_back(arg); });
54
0
    return *this;
55
0
  }
56
};
57
58
} // namespace
59
60
bool cmProjectCommand(std::vector<std::string> const& args,
61
                      cmExecutionStatus& status)
62
0
{
63
0
  std::vector<std::string> unparsedArgs;
64
0
  std::vector<cm::string_view> missingValueKeywords;
65
0
  std::vector<cm::string_view> parsedKeywords;
66
0
  ProjectArguments prArgs;
67
0
  ProjectArgumentParser parser;
68
0
  parser.BindKeywordMissingValue(missingValueKeywords)
69
0
    .BindParsedKeywords(parsedKeywords)
70
0
    .Bind("VERSION"_s, prArgs.Version)
71
0
    .Bind("DESCRIPTION"_s, prArgs.Description)
72
0
    .Bind("HOMEPAGE_URL"_s, prArgs.HomepageURL)
73
0
    .Bind("LANGUAGES"_s, prArgs.Languages);
74
75
0
  cmMakefile& mf = status.GetMakefile();
76
0
  bool enablePackageInfo = cmExperimental::HasSupportEnabled(
77
0
    mf, cmExperimental::Feature::ExportPackageInfo);
78
79
0
  if (enablePackageInfo) {
80
0
    parser.Bind("COMPAT_VERSION"_s, prArgs.CompatVersion);
81
0
    parser.Bind("SPDX_LICENSE"_s, prArgs.License);
82
0
  }
83
84
0
  if (args.empty()) {
85
0
    status.SetError("PROJECT called with incorrect number of arguments");
86
0
    return false;
87
0
  }
88
89
0
  std::string const& projectName = args[0];
90
0
  if (parser.HasKeyword(projectName)) {
91
0
    mf.IssueMessage(
92
0
      MessageType::AUTHOR_WARNING,
93
0
      cmStrCat(
94
0
        "project() called with '", projectName,
95
0
        "' as first argument. The first parameter should be the project name, "
96
0
        "not a keyword argument. See the cmake-commands(7) manual for correct "
97
0
        "usage of the project() command."));
98
0
  }
99
100
0
  parser.Parse(cmMakeRange(args).advance(1), &unparsedArgs, 1);
101
102
0
  if (mf.IsRootMakefile() &&
103
0
      !mf.GetDefinition("CMAKE_MINIMUM_REQUIRED_VERSION")) {
104
0
    mf.IssueMessage(
105
0
      MessageType::AUTHOR_WARNING,
106
0
      "cmake_minimum_required() should be called prior to this top-level "
107
0
      "project() call. Please see the cmake-commands(7) manual for usage "
108
0
      "documentation of both commands.");
109
0
  }
110
111
0
  if (!IncludeByVariable(status, "CMAKE_PROJECT_INCLUDE_BEFORE")) {
112
0
    return false;
113
0
  }
114
115
0
  if (!IncludeByVariable(status,
116
0
                         "CMAKE_PROJECT_" + projectName + "_INCLUDE_BEFORE")) {
117
0
    return false;
118
0
  }
119
120
0
  mf.SetProjectName(projectName);
121
122
0
  cmPolicies::PolicyStatus cmp0180 = mf.GetPolicyStatus(cmPolicies::CMP0180);
123
124
0
  std::string varName = cmStrCat(projectName, "_BINARY_DIR"_s);
125
0
  bool nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName);
126
0
  mf.AddCacheDefinition(varName, mf.GetCurrentBinaryDirectory(),
127
0
                        "Value Computed by CMake", cmStateEnums::STATIC);
128
0
  if (cmp0180 == cmPolicies::NEW || nonCacheVarAlreadySet) {
129
0
    mf.AddDefinition(varName, mf.GetCurrentBinaryDirectory());
130
0
  }
131
132
0
  varName = cmStrCat(projectName, "_SOURCE_DIR"_s);
133
0
  nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName);
134
0
  mf.AddCacheDefinition(varName, mf.GetCurrentSourceDirectory(),
135
0
                        "Value Computed by CMake", cmStateEnums::STATIC);
136
0
  if (cmp0180 == cmPolicies::NEW || nonCacheVarAlreadySet) {
137
0
    mf.AddDefinition(varName, mf.GetCurrentSourceDirectory());
138
0
  }
139
140
0
  mf.AddDefinition("PROJECT_BINARY_DIR", mf.GetCurrentBinaryDirectory());
141
0
  mf.AddDefinition("PROJECT_SOURCE_DIR", mf.GetCurrentSourceDirectory());
142
143
0
  mf.AddDefinition("PROJECT_NAME", projectName);
144
145
0
  mf.AddDefinitionBool("PROJECT_IS_TOP_LEVEL", mf.IsRootMakefile());
146
147
0
  varName = cmStrCat(projectName, "_IS_TOP_LEVEL"_s);
148
0
  nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName);
149
0
  mf.AddCacheDefinition(varName, mf.IsRootMakefile() ? "ON" : "OFF",
150
0
                        "Value Computed by CMake", cmStateEnums::STATIC);
151
0
  if (cmp0180 == cmPolicies::NEW || nonCacheVarAlreadySet) {
152
0
    mf.AddDefinition(varName, mf.IsRootMakefile() ? "ON" : "OFF");
153
0
  }
154
155
0
  TopLevelCMakeVarCondSet(mf, "CMAKE_PROJECT_NAME", projectName);
156
157
0
  std::set<cm::string_view> seenKeywords;
158
0
  for (cm::string_view keyword : parsedKeywords) {
159
0
    if (seenKeywords.find(keyword) != seenKeywords.end()) {
160
0
      mf.IssueMessage(MessageType::FATAL_ERROR,
161
0
                      cmStrCat(keyword, " may be specified at most once."));
162
0
      cmSystemTools::SetFatalErrorOccurred();
163
0
      return true;
164
0
    }
165
0
    seenKeywords.insert(keyword);
166
0
  }
167
168
0
  for (cm::string_view keyword : missingValueKeywords) {
169
0
    mf.IssueMessage(MessageType::WARNING,
170
0
                    cmStrCat(keyword,
171
0
                             " keyword not followed by a value or was "
172
0
                             "followed by a value that expanded to nothing."));
173
0
  }
174
175
0
  if (!unparsedArgs.empty()) {
176
0
    if (prArgs.Languages) {
177
0
      mf.IssueMessage(
178
0
        MessageType::WARNING,
179
0
        cmStrCat("the following parameters must be specified after LANGUAGES "
180
0
                 "keyword: ",
181
0
                 cmJoin(unparsedArgs, ", "), '.'));
182
0
    } else if (prArgs.Version || prArgs.Description || prArgs.HomepageURL) {
183
0
      mf.IssueMessage(MessageType::FATAL_ERROR,
184
0
                      "project with VERSION, DESCRIPTION or HOMEPAGE_URL must "
185
0
                      "use LANGUAGES before language names.");
186
0
      cmSystemTools::SetFatalErrorOccurred();
187
0
      return true;
188
0
    }
189
0
  } else if (prArgs.Languages && prArgs.Languages->empty()) {
190
0
    prArgs.Languages->emplace_back("NONE");
191
0
  }
192
193
0
  if (prArgs.CompatVersion && !prArgs.Version) {
194
0
    mf.IssueMessage(MessageType::FATAL_ERROR,
195
0
                    "project with COMPAT_VERSION must also provide VERSION.");
196
0
    cmSystemTools::SetFatalErrorOccurred();
197
0
    return true;
198
0
  }
199
200
0
  cmsys::RegularExpression vx(
201
0
    R"(^([0-9]+(\.[0-9]+(\.[0-9]+(\.[0-9]+)?)?)?)?$)");
202
203
0
  constexpr std::size_t MAX_VERSION_COMPONENTS = 4u;
204
0
  std::string version_string;
205
0
  std::array<std::string, MAX_VERSION_COMPONENTS> version_components;
206
207
0
  bool has_version = prArgs.Version.has_value();
208
0
  if (prArgs.Version) {
209
0
    if (!vx.find(*prArgs.Version)) {
210
0
      std::string e =
211
0
        R"(VERSION ")" + *prArgs.Version + R"(" format invalid.)";
212
0
      mf.IssueMessage(MessageType::FATAL_ERROR, e);
213
0
      cmSystemTools::SetFatalErrorOccurred();
214
0
      return true;
215
0
    }
216
217
0
    cmPolicies::PolicyStatus const cmp0096 =
218
0
      mf.GetPolicyStatus(cmPolicies::CMP0096);
219
220
0
    if (cmp0096 == cmPolicies::OLD || cmp0096 == cmPolicies::WARN) {
221
0
      constexpr size_t maxIntLength =
222
0
        std::numeric_limits<unsigned>::digits10 + 2;
223
0
      char vb[MAX_VERSION_COMPONENTS][maxIntLength];
224
0
      unsigned v[MAX_VERSION_COMPONENTS] = { 0, 0, 0, 0 };
225
0
      int const vc = std::sscanf(prArgs.Version->c_str(), "%u.%u.%u.%u", &v[0],
226
0
                                 &v[1], &v[2], &v[3]);
227
0
      for (auto i = 0u; i < MAX_VERSION_COMPONENTS; ++i) {
228
0
        if (static_cast<int>(i) < vc) {
229
0
          std::snprintf(vb[i], maxIntLength, "%u", v[i]);
230
0
          version_string += &"."[static_cast<std::size_t>(i == 0)];
231
0
          version_string += vb[i];
232
0
          version_components[i] = vb[i];
233
0
        } else {
234
0
          vb[i][0] = '\x00';
235
0
        }
236
0
      }
237
0
    } else {
238
      // The regex above verified that we have a .-separated string of
239
      // non-negative integer components.  Keep the original string.
240
0
      version_string = std::move(*prArgs.Version);
241
      // Split the integer components.
242
0
      auto components = cmSystemTools::SplitString(version_string, '.');
243
0
      for (auto i = 0u; i < components.size(); ++i) {
244
0
        version_components[i] = std::move(components[i]);
245
0
      }
246
0
    }
247
0
  }
248
249
0
  if (prArgs.CompatVersion) {
250
0
    if (!vx.find(*prArgs.CompatVersion)) {
251
0
      std::string e =
252
0
        R"(COMPAT_VERSION ")" + *prArgs.CompatVersion + R"(" format invalid.)";
253
0
      mf.IssueMessage(MessageType::FATAL_ERROR, e);
254
0
      cmSystemTools::SetFatalErrorOccurred();
255
0
      return true;
256
0
    }
257
258
0
    if (cmSystemTools::VersionCompareGreater(*prArgs.CompatVersion,
259
0
                                             version_string)) {
260
0
      mf.IssueMessage(MessageType::FATAL_ERROR,
261
0
                      "COMPAT_VERSION must be less than or equal to VERSION");
262
0
      cmSystemTools::SetFatalErrorOccurred();
263
0
      return true;
264
0
    }
265
0
  }
266
267
0
  auto createVariables = [&](cm::string_view var, std::string const& val) {
268
0
    mf.AddDefinition(cmStrCat("PROJECT_"_s, var), val);
269
0
    mf.AddDefinition(cmStrCat(projectName, "_"_s, var), val);
270
0
    TopLevelCMakeVarCondSet(mf, cmStrCat("CMAKE_PROJECT_"_s, var), val);
271
0
  };
272
273
  // Note, this intentionally doesn't touch cache variables as the legacy
274
  // behavior did not modify cache
275
0
  auto checkAndClearVariables = [&](cm::string_view var) {
276
0
    std::vector<std::string> vv = { "PROJECT_", cmStrCat(projectName, '_') };
277
0
    if (mf.IsRootMakefile()) {
278
0
      vv.push_back("CMAKE_PROJECT_");
279
0
    }
280
0
    for (std::string const& prefix : vv) {
281
0
      std::string def = cmStrCat(prefix, var);
282
0
      if (!mf.GetDefinition(def).IsEmpty()) {
283
0
        mf.AddDefinition(def, "");
284
0
      }
285
0
    }
286
0
  };
287
288
  // TODO: We should treat VERSION the same as all other project variables, but
289
  // setting it to empty string unconditionally causes various behavior
290
  // changes. It needs a policy. For now, maintain the old behavior and add a
291
  // policy in a future release.
292
293
0
  if (has_version) {
294
0
    createVariables("VERSION"_s, version_string);
295
0
    createVariables("VERSION_MAJOR"_s, version_components[0]);
296
0
    createVariables("VERSION_MINOR"_s, version_components[1]);
297
0
    createVariables("VERSION_PATCH"_s, version_components[2]);
298
0
    createVariables("VERSION_TWEAK"_s, version_components[3]);
299
0
  } else {
300
0
    checkAndClearVariables("VERSION"_s);
301
0
    checkAndClearVariables("VERSION_MAJOR"_s);
302
0
    checkAndClearVariables("VERSION_MINOR"_s);
303
0
    checkAndClearVariables("VERSION_PATCH"_s);
304
0
    checkAndClearVariables("VERSION_TWEAK"_s);
305
0
  }
306
307
0
  createVariables("COMPAT_VERSION"_s, prArgs.CompatVersion.value_or(""));
308
0
  createVariables("SPDX_LICENSE"_s, prArgs.License.value_or(""));
309
0
  createVariables("DESCRIPTION"_s, prArgs.Description.value_or(""));
310
0
  createVariables("HOMEPAGE_URL"_s, prArgs.HomepageURL.value_or(""));
311
312
0
  if (unparsedArgs.empty() && !prArgs.Languages) {
313
    // if no language is specified do c and c++
314
0
    mf.EnableLanguage({ "C", "CXX" }, false);
315
0
  } else {
316
0
    if (!unparsedArgs.empty()) {
317
0
      mf.EnableLanguage(unparsedArgs, false);
318
0
    }
319
0
    if (prArgs.Languages) {
320
0
      mf.EnableLanguage(*prArgs.Languages, false);
321
0
    }
322
0
  }
323
324
0
  if (!IncludeByVariable(status, "CMAKE_PROJECT_INCLUDE")) {
325
0
    return false;
326
0
  }
327
328
0
  if (!IncludeByVariable(status,
329
0
                         "CMAKE_PROJECT_" + projectName + "_INCLUDE")) {
330
0
    return false;
331
0
  }
332
333
0
  return true;
334
0
}
335
336
namespace {
337
bool IncludeByVariable(cmExecutionStatus& status, std::string const& variable)
338
0
{
339
0
  cmMakefile& mf = status.GetMakefile();
340
0
  cmValue include = mf.GetDefinition(variable);
341
0
  if (!include) {
342
0
    return true;
343
0
  }
344
0
  cmList includeFiles{ *include };
345
346
0
  bool failed = false;
347
0
  for (auto filePath : includeFiles) {
348
    // Any relative path without a .cmake extension is checked for valid cmake
349
    // modules. This logic should be consistent with CMake's include() command.
350
    // Otherwise default to checking relative path w.r.t. source directory
351
0
    if (!cmSystemTools::FileIsFullPath(filePath) &&
352
0
        !cmHasLiteralSuffix(filePath, ".cmake")) {
353
0
      std::string mfile = mf.GetModulesFile(cmStrCat(filePath, ".cmake"));
354
0
      if (mfile.empty()) {
355
0
        status.SetError(
356
0
          cmStrCat("could not find requested module:\n  ", filePath));
357
0
        failed = true;
358
0
        continue;
359
0
      }
360
0
      filePath = mfile;
361
0
    }
362
0
    std::string includeFile = cmSystemTools::CollapseFullPath(
363
0
      filePath, mf.GetCurrentSourceDirectory());
364
0
    if (!cmSystemTools::FileExists(includeFile)) {
365
0
      status.SetError(
366
0
        cmStrCat("could not find requested file:\n  ", filePath));
367
0
      failed = true;
368
0
      continue;
369
0
    }
370
0
    if (cmSystemTools::FileIsDirectory(includeFile)) {
371
0
      status.SetError(
372
0
        cmStrCat("requested file is a directory:\n  ", filePath));
373
0
      failed = true;
374
0
      continue;
375
0
    }
376
377
0
    bool const readit = mf.ReadDependentFile(filePath);
378
0
    if (readit) {
379
      // If the included file ran successfully, continue to the next file
380
0
      continue;
381
0
    }
382
383
0
    if (cmSystemTools::GetFatalErrorOccurred()) {
384
0
      failed = true;
385
0
      continue;
386
0
    }
387
388
0
    status.SetError(cmStrCat("could not load requested file:\n  ", filePath));
389
0
    failed = true;
390
0
  }
391
  // At this point all files were processed
392
0
  return !failed;
393
0
}
394
395
void TopLevelCMakeVarCondSet(cmMakefile& mf, std::string const& name,
396
                             std::string const& value)
397
0
{
398
  // Set the CMAKE_PROJECT_XXX variable to be the highest-level
399
  // project name in the tree. If there are two project commands
400
  // in the same CMakeLists.txt file, and it is the top level
401
  // CMakeLists.txt file, then go with the last one.
402
0
  if (!mf.GetDefinition(name) || mf.IsRootMakefile()) {
403
0
    mf.RemoveDefinition(name);
404
0
    mf.AddCacheDefinition(name, value, "Value Computed by CMake",
405
0
                          cmStateEnums::STATIC);
406
0
  }
407
0
}
408
}