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