/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 | | } |