Coverage Report

Created: 2026-04-29 07:01

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