diff --git a/Source/cmProjectCommand.cxx b/Source/cmProjectCommand.cxx index cdab869d13..f0356d5677 100644 --- a/Source/cmProjectCommand.cxx +++ b/Source/cmProjectCommand.cxx @@ -4,14 +4,18 @@ #include #include -#include #include +#include #include +#include +#include #include #include "cmsys/RegularExpression.hxx" +#include "cmArgumentParser.h" +#include "cmArgumentParserTypes.h" #include "cmExecutionStatus.h" #include "cmList.h" #include "cmMakefile.h" @@ -22,15 +26,52 @@ #include "cmSystemTools.h" #include "cmValue.h" -static bool IncludeByVariable(cmExecutionStatus& status, - std::string const& variable); -static void TopLevelCMakeVarCondSet(cmMakefile& mf, std::string const& name, - std::string const& value); +namespace { + +bool IncludeByVariable(cmExecutionStatus& status, std::string const& variable); +void TopLevelCMakeVarCondSet(cmMakefile& mf, std::string const& name, + std::string const& value); + +struct ProjectArguments : ArgumentParser::ParseResult +{ + cm::optional ProjectName; + cm::optional Version; + cm::optional Description; + cm::optional HomepageURL; + cm::optional>> Languages; +}; + +struct ProjectArgumentParser : public cmArgumentParser +{ + ProjectArgumentParser& BindKeywordMissingValue( + std::vector& ref) + { + this->cmArgumentParser::BindKeywordMissingValue( + [&ref](Instance&, cm::string_view arg) { ref.emplace_back(arg); }); + return *this; + } +}; + +} // namespace bool cmProjectCommand(std::vector const& args, cmExecutionStatus& status) { - if (args.empty()) { + std::vector unparsedArgs; + std::vector missingValueKeywords; + std::vector parsedKeywords; + ProjectArguments prArgs; + ProjectArgumentParser{} + .BindKeywordMissingValue(missingValueKeywords) + .BindParsedKeywords(parsedKeywords) + .Bind(0, prArgs.ProjectName) + .Bind("VERSION"_s, prArgs.Version) + .Bind("DESCRIPTION"_s, prArgs.Description) + .Bind("HOMEPAGE_URL"_s, prArgs.HomepageURL) + .Bind("LANGUAGES"_s, prArgs.Languages) + .Parse(args, &unparsedArgs, 0); + + if (!prArgs.ProjectName) { status.SetError("PROJECT called with incorrect number of arguments"); return false; } @@ -49,18 +90,16 @@ bool cmProjectCommand(std::vector const& args, return false; } - std::string const& projectName = args[0]; - - if (!IncludeByVariable(status, - "CMAKE_PROJECT_" + projectName + "_INCLUDE_BEFORE")) { + if (!IncludeByVariable( + status, "CMAKE_PROJECT_" + *prArgs.ProjectName + "_INCLUDE_BEFORE")) { return false; } - mf.SetProjectName(projectName); + mf.SetProjectName(*prArgs.ProjectName); cmPolicies::PolicyStatus cmp0180 = mf.GetPolicyStatus(cmPolicies::CMP0180); - std::string varName = cmStrCat(projectName, "_BINARY_DIR"_s); + std::string varName = cmStrCat(*prArgs.ProjectName, "_BINARY_DIR"_s); bool nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName); mf.AddCacheDefinition(varName, mf.GetCurrentBinaryDirectory(), "Value Computed by CMake", cmStateEnums::STATIC); @@ -68,7 +107,7 @@ bool cmProjectCommand(std::vector const& args, mf.AddDefinition(varName, mf.GetCurrentBinaryDirectory()); } - varName = cmStrCat(projectName, "_SOURCE_DIR"_s); + varName = cmStrCat(*prArgs.ProjectName, "_SOURCE_DIR"_s); nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName); mf.AddCacheDefinition(varName, mf.GetCurrentSourceDirectory(), "Value Computed by CMake", cmStateEnums::STATIC); @@ -79,11 +118,11 @@ bool cmProjectCommand(std::vector const& args, mf.AddDefinition("PROJECT_BINARY_DIR", mf.GetCurrentBinaryDirectory()); mf.AddDefinition("PROJECT_SOURCE_DIR", mf.GetCurrentSourceDirectory()); - mf.AddDefinition("PROJECT_NAME", projectName); + mf.AddDefinition("PROJECT_NAME", *prArgs.ProjectName); mf.AddDefinitionBool("PROJECT_IS_TOP_LEVEL", mf.IsRootMakefile()); - varName = cmStrCat(projectName, "_IS_TOP_LEVEL"_s); + varName = cmStrCat(*prArgs.ProjectName, "_IS_TOP_LEVEL"_s); nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName); mf.AddCacheDefinition(varName, mf.IsRootMakefile() ? "ON" : "OFF", "Value Computed by CMake", cmStateEnums::STATIC); @@ -99,143 +138,52 @@ bool cmProjectCommand(std::vector const& args, // will work. if (!mf.GetDefinition("CMAKE_PROJECT_NAME") || mf.IsRootMakefile()) { mf.RemoveDefinition("CMAKE_PROJECT_NAME"); - mf.AddCacheDefinition("CMAKE_PROJECT_NAME", projectName, + mf.AddCacheDefinition("CMAKE_PROJECT_NAME", *prArgs.ProjectName, "Value Computed by CMake", cmStateEnums::STATIC); } - bool haveVersion = false; - bool haveLanguages = false; - bool haveDescription = false; - bool haveHomepage = false; - std::string version; - std::string description; - std::string homepage; - std::vector languages; - std::function missedValueReporter; - auto resetReporter = [&missedValueReporter]() { - missedValueReporter = std::function(); - }; - enum Doing - { - DoingDescription, - DoingHomepage, - DoingLanguages, - DoingVersion - }; - Doing doing = DoingLanguages; - for (size_t i = 1; i < args.size(); ++i) { - if (args[i] == "LANGUAGES") { - if (haveLanguages) { - mf.IssueMessage(MessageType::FATAL_ERROR, - "LANGUAGES may be specified at most once."); - cmSystemTools::SetFatalErrorOccurred(); - return true; - } - haveLanguages = true; - if (missedValueReporter) { - missedValueReporter(); - } - doing = DoingLanguages; - if (!languages.empty()) { - std::string msg = cmStrCat( - "the following parameters must be specified after LANGUAGES " - "keyword: ", - cmJoin(languages, ", "), '.'); - mf.IssueMessage(MessageType::WARNING, msg); - } - } else if (args[i] == "VERSION") { - if (haveVersion) { - mf.IssueMessage(MessageType::FATAL_ERROR, - "VERSION may be specified at most once."); - cmSystemTools::SetFatalErrorOccurred(); - return true; - } - haveVersion = true; - if (missedValueReporter) { - missedValueReporter(); - } - doing = DoingVersion; - missedValueReporter = [&mf, &resetReporter]() { - mf.IssueMessage( - MessageType::WARNING, - "VERSION keyword not followed by a value or was followed by a " - "value that expanded to nothing."); - resetReporter(); - }; - } else if (args[i] == "DESCRIPTION") { - if (haveDescription) { - mf.IssueMessage(MessageType::FATAL_ERROR, - "DESCRIPTION may be specified at most once."); - cmSystemTools::SetFatalErrorOccurred(); - return true; - } - haveDescription = true; - if (missedValueReporter) { - missedValueReporter(); - } - doing = DoingDescription; - missedValueReporter = [&mf, &resetReporter]() { - mf.IssueMessage( - MessageType::WARNING, - "DESCRIPTION keyword not followed by a value or was followed " - "by a value that expanded to nothing."); - resetReporter(); - }; - } else if (args[i] == "HOMEPAGE_URL") { - if (haveHomepage) { - mf.IssueMessage(MessageType::FATAL_ERROR, - "HOMEPAGE_URL may be specified at most once."); - cmSystemTools::SetFatalErrorOccurred(); - return true; - } - haveHomepage = true; - doing = DoingHomepage; - missedValueReporter = [&mf, &resetReporter]() { - mf.IssueMessage( - MessageType::WARNING, - "HOMEPAGE_URL keyword not followed by a value or was followed " - "by a value that expanded to nothing."); - resetReporter(); - }; - } else if (doing == DoingVersion) { - doing = DoingLanguages; - version = args[i]; - resetReporter(); - } else if (doing == DoingDescription) { - doing = DoingLanguages; - description = args[i]; - resetReporter(); - } else if (doing == DoingHomepage) { - doing = DoingLanguages; - homepage = args[i]; - resetReporter(); - } else // doing == DoingLanguages - { - languages.push_back(args[i]); + std::set seenKeywords; + for (cm::string_view keyword : parsedKeywords) { + if (seenKeywords.find(keyword) != seenKeywords.end()) { + mf.IssueMessage(MessageType::FATAL_ERROR, + cmStrCat(keyword, " may be specified at most once.")); + cmSystemTools::SetFatalErrorOccurred(); + return true; } + seenKeywords.insert(keyword); } - if (missedValueReporter) { - missedValueReporter(); + for (cm::string_view keyword : missingValueKeywords) { + mf.IssueMessage(MessageType::WARNING, + cmStrCat(keyword, + " keyword not followed by a value or was " + "followed by a value that expanded to nothing.")); } - if ((haveVersion || haveDescription || haveHomepage) && !haveLanguages && - !languages.empty()) { - mf.IssueMessage(MessageType::FATAL_ERROR, - "project with VERSION, DESCRIPTION or HOMEPAGE_URL must " - "use LANGUAGES before language names."); - cmSystemTools::SetFatalErrorOccurred(); - return true; - } - if (haveLanguages && languages.empty()) { - languages.emplace_back("NONE"); + if (!unparsedArgs.empty()) { + if (prArgs.Languages) { + mf.IssueMessage( + MessageType::WARNING, + cmStrCat("the following parameters must be specified after LANGUAGES " + "keyword: ", + cmJoin(unparsedArgs, ", "), '.')); + } else if (prArgs.Version || prArgs.Description || prArgs.HomepageURL) { + mf.IssueMessage(MessageType::FATAL_ERROR, + "project with VERSION, DESCRIPTION or HOMEPAGE_URL must " + "use LANGUAGES before language names."); + cmSystemTools::SetFatalErrorOccurred(); + return true; + } + } else if (prArgs.Languages && prArgs.Languages->empty()) { + prArgs.Languages->emplace_back("NONE"); } - if (haveVersion) { + if (prArgs.Version) { cmsys::RegularExpression vx( R"(^([0-9]+(\.[0-9]+(\.[0-9]+(\.[0-9]+)?)?)?)?$)"); - if (!vx.find(version)) { - std::string e = R"(VERSION ")" + version + R"(" format invalid.)"; + if (!vx.find(*prArgs.Version)) { + std::string e = + R"(VERSION ")" + *prArgs.Version + R"(" format invalid.)"; mf.IssueMessage(MessageType::FATAL_ERROR, e); cmSystemTools::SetFatalErrorOccurred(); return true; @@ -253,8 +201,8 @@ bool cmProjectCommand(std::vector const& args, std::numeric_limits::digits10 + 2; char vb[MAX_VERSION_COMPONENTS][maxIntLength]; unsigned v[MAX_VERSION_COMPONENTS] = { 0, 0, 0, 0 }; - int const vc = std::sscanf(version.c_str(), "%u.%u.%u.%u", &v[0], &v[1], - &v[2], &v[3]); + int const vc = std::sscanf(prArgs.Version->c_str(), "%u.%u.%u.%u", &v[0], + &v[1], &v[2], &v[3]); for (auto i = 0u; i < MAX_VERSION_COMPONENTS; ++i) { if (static_cast(i) < vc) { std::snprintf(vb[i], maxIntLength, "%u", v[i]); @@ -268,7 +216,7 @@ bool cmProjectCommand(std::vector const& args, } else { // The regex above verified that we have a .-separated string of // non-negative integer components. Keep the original string. - version_string = std::move(version); + version_string = std::move(*prArgs.Version); // Split the integer components. auto components = cmSystemTools::SplitString(version_string, '.'); for (auto i = 0u; i < components.size(); ++i) { @@ -277,19 +225,19 @@ bool cmProjectCommand(std::vector const& args, } std::string vv; - vv = projectName + "_VERSION"; + vv = *prArgs.ProjectName + "_VERSION"; mf.AddDefinition("PROJECT_VERSION", version_string); mf.AddDefinition(vv, version_string); - vv = projectName + "_VERSION_MAJOR"; + vv = *prArgs.ProjectName + "_VERSION_MAJOR"; mf.AddDefinition("PROJECT_VERSION_MAJOR", version_components[0]); mf.AddDefinition(vv, version_components[0]); - vv = projectName + "_VERSION_MINOR"; + vv = *prArgs.ProjectName + "_VERSION_MINOR"; mf.AddDefinition("PROJECT_VERSION_MINOR", version_components[1]); mf.AddDefinition(vv, version_components[1]); - vv = projectName + "_VERSION_PATCH"; + vv = *prArgs.ProjectName + "_VERSION_PATCH"; mf.AddDefinition("PROJECT_VERSION_PATCH", version_components[2]); mf.AddDefinition(vv, version_components[2]); - vv = projectName + "_VERSION_TWEAK"; + vv = *prArgs.ProjectName + "_VERSION_TWEAK"; mf.AddDefinition("PROJECT_VERSION_TWEAK", version_components[3]); mf.AddDefinition(vv, version_components[3]); // Also, try set top level variables @@ -309,11 +257,11 @@ bool cmProjectCommand(std::vector const& args, "PROJECT_VERSION_MINOR", "PROJECT_VERSION_PATCH", "PROJECT_VERSION_TWEAK", - projectName + "_VERSION", - projectName + "_VERSION_MAJOR", - projectName + "_VERSION_MINOR", - projectName + "_VERSION_PATCH", - projectName + "_VERSION_TWEAK" }; + *prArgs.ProjectName + "_VERSION", + *prArgs.ProjectName + "_VERSION_MAJOR", + *prArgs.ProjectName + "_VERSION_MINOR", + *prArgs.ProjectName + "_VERSION_PATCH", + *prArgs.ProjectName + "_VERSION_TWEAK" }; if (mf.IsRootMakefile()) { vv.emplace_back("CMAKE_PROJECT_VERSION"); vv.emplace_back("CMAKE_PROJECT_VERSION_MAJOR"); @@ -329,34 +277,42 @@ bool cmProjectCommand(std::vector const& args, } } - mf.AddDefinition("PROJECT_DESCRIPTION", description); - mf.AddDefinition(projectName + "_DESCRIPTION", description); - TopLevelCMakeVarCondSet(mf, "CMAKE_PROJECT_DESCRIPTION", description); + std::string const& desc = prArgs.Description.value_or(""); + mf.AddDefinition("PROJECT_DESCRIPTION", desc); + mf.AddDefinition(*prArgs.ProjectName + "_DESCRIPTION", desc); + TopLevelCMakeVarCondSet(mf, "CMAKE_PROJECT_DESCRIPTION", desc); - mf.AddDefinition("PROJECT_HOMEPAGE_URL", homepage); - mf.AddDefinition(projectName + "_HOMEPAGE_URL", homepage); - TopLevelCMakeVarCondSet(mf, "CMAKE_PROJECT_HOMEPAGE_URL", homepage); + std::string const& url = prArgs.HomepageURL.value_or(""); + mf.AddDefinition("PROJECT_HOMEPAGE_URL", url); + mf.AddDefinition(*prArgs.ProjectName + "_HOMEPAGE_URL", url); + TopLevelCMakeVarCondSet(mf, "CMAKE_PROJECT_HOMEPAGE_URL", url); - if (languages.empty()) { + if (unparsedArgs.empty() && !prArgs.Languages) { // if no language is specified do c and c++ - languages = { "C", "CXX" }; + mf.EnableLanguage({ "C", "CXX" }, false); + } else { + if (!unparsedArgs.empty()) { + mf.EnableLanguage(unparsedArgs, false); + } + if (prArgs.Languages) { + mf.EnableLanguage(*prArgs.Languages, false); + } } - mf.EnableLanguage(languages, false); if (!IncludeByVariable(status, "CMAKE_PROJECT_INCLUDE")) { return false; } - if (!IncludeByVariable(status, - "CMAKE_PROJECT_" + projectName + "_INCLUDE")) { + if (!IncludeByVariable( + status, "CMAKE_PROJECT_" + *prArgs.ProjectName + "_INCLUDE")) { return false; } return true; } -static bool IncludeByVariable(cmExecutionStatus& status, - std::string const& variable) +namespace { +bool IncludeByVariable(cmExecutionStatus& status, std::string const& variable) { cmMakefile& mf = status.GetMakefile(); cmValue include = mf.GetDefinition(variable); @@ -414,8 +370,8 @@ static bool IncludeByVariable(cmExecutionStatus& status, return !failed; } -static void TopLevelCMakeVarCondSet(cmMakefile& mf, std::string const& name, - std::string const& value) +void TopLevelCMakeVarCondSet(cmMakefile& mf, std::string const& name, + std::string const& value) { // Set the CMAKE_PROJECT_XXX variable to be the highest-level // project name in the tree. If there are two project commands @@ -427,3 +383,4 @@ static void TopLevelCMakeVarCondSet(cmMakefile& mf, std::string const& name, cmStateEnums::STATIC); } } +}