cmProjectCommand.cxx 12 KB
Newer Older
1 2
/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
3
#include "cmProjectCommand.h"
4

5
#include "cmsys/RegularExpression.hxx"
6
#include <functional>
7 8 9
#include <sstream>
#include <stdio.h>

10
#include "cmAlgorithms.h"
11
#include "cmMakefile.h"
12
#include "cmMessageType.h"
13 14
#include "cmPolicies.h"
#include "cmStateTypes.h"
15
#include "cmSystemTools.h"
16 17

class cmExecutionStatus;
18

19
// cmProjectCommand
20 21
bool cmProjectCommand::InitialPass(std::vector<std::string> const& args,
                                   cmExecutionStatus&)
22
{
23
  if (args.empty()) {
24 25
    this->SetError("PROJECT called with incorrect number of arguments");
    return false;
26
  }
Ken Martin's avatar
Ken Martin committed
27

28 29 30 31 32
  std::string const& projectName = args[0];

  this->Makefile->SetProjectName(projectName);

  std::string bindir = projectName;
Ken Martin's avatar
Ken Martin committed
33
  bindir += "_BINARY_DIR";
34
  std::string srcdir = projectName;
Ken Martin's avatar
Ken Martin committed
35
  srcdir += "_SOURCE_DIR";
36

37
  this->Makefile->AddCacheDefinition(
38
    bindir, this->Makefile->GetCurrentBinaryDirectory().c_str(),
39
    "Value Computed by CMake", cmStateEnums::STATIC);
40
  this->Makefile->AddCacheDefinition(
41
    srcdir, this->Makefile->GetCurrentSourceDirectory().c_str(),
42
    "Value Computed by CMake", cmStateEnums::STATIC);
43

Ken Martin's avatar
Ken Martin committed
44 45 46
  bindir = "PROJECT_BINARY_DIR";
  srcdir = "PROJECT_SOURCE_DIR";

47 48 49 50
  this->Makefile->AddDefinition(
    bindir, this->Makefile->GetCurrentBinaryDirectory().c_str());
  this->Makefile->AddDefinition(
    srcdir, this->Makefile->GetCurrentSourceDirectory().c_str());
Ken Martin's avatar
Ken Martin committed
51

52
  this->Makefile->AddDefinition("PROJECT_NAME", projectName.c_str());
53

54
  // Set the CMAKE_PROJECT_NAME variable to be the highest-level
55 56 57 58 59
  // project name in the tree. If there are two project commands
  // in the same CMakeLists.txt file, and it is the top level
  // CMakeLists.txt file, then go with the last one, so that
  // CMAKE_PROJECT_NAME will match PROJECT_NAME, and cmake --build
  // will work.
60 61
  if (!this->Makefile->GetDefinition("CMAKE_PROJECT_NAME") ||
      (this->Makefile->IsRootMakefile())) {
62 63 64 65
    this->Makefile->AddDefinition("CMAKE_PROJECT_NAME", projectName.c_str());
    this->Makefile->AddCacheDefinition(
      "CMAKE_PROJECT_NAME", projectName.c_str(), "Value Computed by CMake",
      cmStateEnums::STATIC);
66
  }
67

68
  bool haveVersion = false;
69
  bool haveLanguages = false;
70
  bool haveDescription = false;
71
  bool haveHomepage = false;
72
  bool injectedProjectCommand = false;
73
  std::string version;
74
  std::string description;
75
  std::string homepage;
76
  std::vector<std::string> languages;
77 78 79 80
  std::function<void()> missedValueReporter;
  auto resetReporter = [&missedValueReporter]() {
    missedValueReporter = std::function<void()>();
  };
81 82
  enum Doing
  {
83
    DoingDescription,
84
    DoingHomepage,
85 86 87
    DoingLanguages,
    DoingVersion
  };
88
  Doing doing = DoingLanguages;
89 90 91 92
  for (size_t i = 1; i < args.size(); ++i) {
    if (args[i] == "LANGUAGES") {
      if (haveLanguages) {
        this->Makefile->IssueMessage(
93 94
          MessageType::FATAL_ERROR,
          "LANGUAGES may be specified at most once.");
95 96
        cmSystemTools::SetFatalErrorOccured();
        return true;
97
      }
98
      haveLanguages = true;
99 100 101
      if (missedValueReporter) {
        missedValueReporter();
      }
102
      doing = DoingLanguages;
103 104 105 106 107 108
      if (!languages.empty()) {
        std::string msg =
          "the following parameters must be specified after LANGUAGES "
          "keyword: ";
        msg += cmJoin(languages, ", ");
        msg += '.';
109
        this->Makefile->IssueMessage(MessageType::WARNING, msg);
110
      }
111 112
    } else if (args[i] == "VERSION") {
      if (haveVersion) {
113
        this->Makefile->IssueMessage(MessageType::FATAL_ERROR,
114
                                     "VERSION may be specified at most once.");
115 116
        cmSystemTools::SetFatalErrorOccured();
        return true;
117
      }
118
      haveVersion = true;
119 120 121
      if (missedValueReporter) {
        missedValueReporter();
      }
122
      doing = DoingVersion;
123 124
      missedValueReporter = [this, &resetReporter]() {
        this->Makefile->IssueMessage(
125
          MessageType::WARNING,
126 127 128 129
          "VERSION keyword not followed by a value or was followed by a "
          "value that expanded to nothing.");
        resetReporter();
      };
130 131 132
    } else if (args[i] == "DESCRIPTION") {
      if (haveDescription) {
        this->Makefile->IssueMessage(
133 134
          MessageType::FATAL_ERROR,
          "DESCRIPTION may be specified at most once.");
135 136 137 138
        cmSystemTools::SetFatalErrorOccured();
        return true;
      }
      haveDescription = true;
139 140 141
      if (missedValueReporter) {
        missedValueReporter();
      }
142
      doing = DoingDescription;
143 144
      missedValueReporter = [this, &resetReporter]() {
        this->Makefile->IssueMessage(
145
          MessageType::WARNING,
146 147 148 149
          "DESCRIPTION keyword not followed by a value or was followed "
          "by a value that expanded to nothing.");
        resetReporter();
      };
150 151 152
    } else if (args[i] == "HOMEPAGE_URL") {
      if (haveHomepage) {
        this->Makefile->IssueMessage(
153 154
          MessageType::FATAL_ERROR,
          "HOMEPAGE_URL may be specified at most once.");
155 156 157 158 159 160 161
        cmSystemTools::SetFatalErrorOccured();
        return true;
      }
      haveHomepage = true;
      doing = DoingHomepage;
      missedValueReporter = [this, &resetReporter]() {
        this->Makefile->IssueMessage(
162
          MessageType::WARNING,
163 164 165 166
          "HOMEPAGE_URL keyword not followed by a value or was followed "
          "by a value that expanded to nothing.");
        resetReporter();
      };
167 168
    } else if (i == 1 && args[i] == "__CMAKE_INJECTED_PROJECT_COMMAND__") {
      injectedProjectCommand = true;
169
    } else if (doing == DoingVersion) {
170 171
      doing = DoingLanguages;
      version = args[i];
172
      resetReporter();
173 174 175
    } else if (doing == DoingDescription) {
      doing = DoingLanguages;
      description = args[i];
176
      resetReporter();
177 178 179 180
    } else if (doing == DoingHomepage) {
      doing = DoingLanguages;
      homepage = args[i];
      resetReporter();
181 182
    } else // doing == DoingLanguages
    {
183
      languages.push_back(args[i]);
184
    }
185
  }
186

187 188 189 190
  if (missedValueReporter) {
    missedValueReporter();
  }

191
  if ((haveVersion || haveDescription || haveHomepage) && !haveLanguages &&
192
      !languages.empty()) {
193
    this->Makefile->IssueMessage(
194
      MessageType::FATAL_ERROR,
195 196
      "project with VERSION, DESCRIPTION or HOMEPAGE_URL must "
      "use LANGUAGES before language names.");
197 198
    cmSystemTools::SetFatalErrorOccured();
    return true;
199 200
  }
  if (haveLanguages && languages.empty()) {
wahikihiki's avatar
wahikihiki committed
201
    languages.emplace_back("NONE");
202
  }
203

204 205
  cmPolicies::PolicyStatus cmp0048 =
    this->Makefile->GetPolicyStatus(cmPolicies::CMP0048);
206
  if (haveVersion) {
207
    // Set project VERSION variables to given values
208 209
    if (cmp0048 == cmPolicies::OLD || cmp0048 == cmPolicies::WARN) {
      this->Makefile->IssueMessage(
210
        MessageType::FATAL_ERROR,
211
        "VERSION not allowed unless CMP0048 is set to NEW");
212 213
      cmSystemTools::SetFatalErrorOccured();
      return true;
214
    }
215

216 217 218
    cmsys::RegularExpression vx(
      "^([0-9]+(\\.[0-9]+(\\.[0-9]+(\\.[0-9]+)?)?)?)?$");
    if (!vx.find(version)) {
219
      std::string e = "VERSION \"" + version + "\" format invalid.";
220
      this->Makefile->IssueMessage(MessageType::FATAL_ERROR, e);
221 222
      cmSystemTools::SetFatalErrorOccured();
      return true;
223
    }
224 225 226 227

    std::string vs;
    const char* sep = "";
    char vb[4][64];
228 229 230 231 232
    unsigned int v[4] = { 0, 0, 0, 0 };
    int vc =
      sscanf(version.c_str(), "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
    for (int i = 0; i < 4; ++i) {
      if (i < vc) {
233 234 235 236
        sprintf(vb[i], "%u", v[i]);
        vs += sep;
        vs += vb[i];
        sep = ".";
237
      } else {
238 239
        vb[i][0] = 0;
      }
240
    }
241 242

    std::string vv;
243
    vv = projectName + "_VERSION";
244
    this->Makefile->AddDefinition("PROJECT_VERSION", vs.c_str());
245
    this->Makefile->AddDefinition(vv, vs.c_str());
246
    vv = projectName + "_VERSION_MAJOR";
247
    this->Makefile->AddDefinition("PROJECT_VERSION_MAJOR", vb[0]);
248
    this->Makefile->AddDefinition(vv, vb[0]);
249
    vv = projectName + "_VERSION_MINOR";
250
    this->Makefile->AddDefinition("PROJECT_VERSION_MINOR", vb[1]);
251
    this->Makefile->AddDefinition(vv, vb[1]);
252
    vv = projectName + "_VERSION_PATCH";
253
    this->Makefile->AddDefinition("PROJECT_VERSION_PATCH", vb[2]);
254
    this->Makefile->AddDefinition(vv, vb[2]);
255
    vv = projectName + "_VERSION_TWEAK";
256
    this->Makefile->AddDefinition("PROJECT_VERSION_TWEAK", vb[3]);
257
    this->Makefile->AddDefinition(vv, vb[3]);
258 259 260 261 262 263
    // Also, try set top level variables
    TopLevelCMakeVarCondSet("CMAKE_PROJECT_VERSION", vs.c_str());
    TopLevelCMakeVarCondSet("CMAKE_PROJECT_VERSION_MAJOR", vb[0]);
    TopLevelCMakeVarCondSet("CMAKE_PROJECT_VERSION_MINOR", vb[1]);
    TopLevelCMakeVarCondSet("CMAKE_PROJECT_VERSION_PATCH", vb[2]);
    TopLevelCMakeVarCondSet("CMAKE_PROJECT_VERSION_TWEAK", vb[3]);
264
  } else if (cmp0048 != cmPolicies::OLD) {
265 266
    // Set project VERSION variables to empty
    std::vector<std::string> vv;
wahikihiki's avatar
wahikihiki committed
267 268 269 270 271
    vv.emplace_back("PROJECT_VERSION");
    vv.emplace_back("PROJECT_VERSION_MAJOR");
    vv.emplace_back("PROJECT_VERSION_MINOR");
    vv.emplace_back("PROJECT_VERSION_PATCH");
    vv.emplace_back("PROJECT_VERSION_TWEAK");
272 273 274 275 276
    vv.push_back(projectName + "_VERSION");
    vv.push_back(projectName + "_VERSION_MAJOR");
    vv.push_back(projectName + "_VERSION_MINOR");
    vv.push_back(projectName + "_VERSION_PATCH");
    vv.push_back(projectName + "_VERSION_TWEAK");
277
    if (this->Makefile->IsRootMakefile()) {
wahikihiki's avatar
wahikihiki committed
278 279 280 281 282
      vv.emplace_back("CMAKE_PROJECT_VERSION");
      vv.emplace_back("CMAKE_PROJECT_VERSION_MAJOR");
      vv.emplace_back("CMAKE_PROJECT_VERSION_MINOR");
      vv.emplace_back("CMAKE_PROJECT_VERSION_PATCH");
      vv.emplace_back("CMAKE_PROJECT_VERSION_TWEAK");
283
    }
284
    std::string vw;
285 286
    for (std::string const& i : vv) {
      const char* v = this->Makefile->GetDefinition(i);
287 288
      if (v && *v) {
        if (cmp0048 == cmPolicies::WARN) {
289 290 291 292
          if (!injectedProjectCommand) {
            vw += "\n  ";
            vw += i;
          }
293
        } else {
294
          this->Makefile->AddDefinition(i, "");
295 296
        }
      }
297 298
    }
    if (!vw.empty()) {
299
      std::ostringstream w;
300
      w << cmPolicies::GetPolicyWarning(cmPolicies::CMP0048)
301
        << "\nThe following variable(s) would be set to empty:" << vw;
302
      this->Makefile->IssueMessage(MessageType::AUTHOR_WARNING, w.str());
303
    }
304
  }
305

306 307 308 309
  this->Makefile->AddDefinition("PROJECT_DESCRIPTION", description.c_str());
  this->Makefile->AddDefinition(projectName + "_DESCRIPTION",
                                description.c_str());
  TopLevelCMakeVarCondSet("CMAKE_PROJECT_DESCRIPTION", description.c_str());
310

311 312 313 314
  this->Makefile->AddDefinition("PROJECT_HOMEPAGE_URL", homepage.c_str());
  this->Makefile->AddDefinition(projectName + "_HOMEPAGE_URL",
                                homepage.c_str());
  TopLevelCMakeVarCondSet("CMAKE_PROJECT_HOMEPAGE_URL", homepage.c_str());
315

316
  if (languages.empty()) {
317
    // if no language is specified do c and c++
wahikihiki's avatar
wahikihiki committed
318 319
    languages.emplace_back("C");
    languages.emplace_back("CXX");
320
  }
321
  this->Makefile->EnableLanguage(languages, false);
322
  std::string extraInclude = "CMAKE_PROJECT_" + projectName + "_INCLUDE";
323
  const char* include = this->Makefile->GetDefinition(extraInclude);
324 325 326 327 328
  if (include) {
    bool readit = this->Makefile->ReadDependentFile(include);
    if (!readit && !cmSystemTools::GetFatalErrorOccured()) {
      std::string m = "could not find file:\n"
                      "  ";
329
      m += include;
330
      this->SetError(m);
331 332
      return false;
    }
333
  }
334 335
  return true;
}
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350

void cmProjectCommand::TopLevelCMakeVarCondSet(const char* const name,
                                               const char* const value)
{
  // Set the CMAKE_PROJECT_XXX variable to be the highest-level
  // project name in the tree. If there are two project commands
  // in the same CMakeLists.txt file, and it is the top level
  // CMakeLists.txt file, then go with the last one.
  if (!this->Makefile->GetDefinition(name) ||
      (this->Makefile->IsRootMakefile())) {
    this->Makefile->AddDefinition(name, value);
    this->Makefile->AddCacheDefinition(name, value, "Value Computed by CMake",
                                       cmStateEnums::STATIC);
  }
}