Coverage Report

Created: 2026-03-12 06:35

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