| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
| file Copyright.txt or https://cmake.org/licensing for details. */ |
| #include "cmCMakeLanguageCommand.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <cstddef> |
| #include <string> |
| #include <utility> |
| |
| #include <cm/optional> |
| #include <cm/string_view> |
| #include <cmext/string_view> |
| |
| #include "cmArgumentParser.h" |
| #include "cmArgumentParserTypes.h" |
| #include "cmDependencyProvider.h" |
| #include "cmExecutionStatus.h" |
| #include "cmExperimental.h" |
| #include "cmGlobalGenerator.h" |
| #include "cmListFileCache.h" |
| #include "cmMakefile.h" |
| #include "cmMessageType.h" // IWYU pragma: keep |
| #include "cmRange.h" |
| #include "cmState.h" |
| #include "cmStringAlgorithms.h" |
| #include "cmSystemTools.h" |
| #include "cmake.h" |
| |
| namespace { |
| |
| bool FatalError(cmExecutionStatus& status, std::string const& error) |
| { |
| status.SetError(error); |
| cmSystemTools::SetFatalErrorOccurred(); |
| return false; |
| } |
| |
| std::array<cm::static_string_view, 14> InvalidCommands{ |
| { // clang-format off |
| "function"_s, "endfunction"_s, |
| "macro"_s, "endmacro"_s, |
| "if"_s, "elseif"_s, "else"_s, "endif"_s, |
| "while"_s, "endwhile"_s, |
| "foreach"_s, "endforeach"_s, |
| "block"_s, "endblock"_s |
| } // clang-format on |
| }; |
| |
| std::array<cm::static_string_view, 1> InvalidDeferCommands{ |
| { |
| // clang-format off |
| "return"_s, |
| } // clang-format on |
| }; |
| |
| struct Defer |
| { |
| std::string Id; |
| std::string IdVar; |
| cmMakefile* Directory = nullptr; |
| }; |
| |
| bool cmCMakeLanguageCommandCALL(std::vector<cmListFileArgument> const& args, |
| std::string const& callCommand, |
| size_t startArg, cm::optional<Defer> defer, |
| cmExecutionStatus& status) |
| { |
| // ensure specified command is valid |
| // start/end flow control commands are not allowed |
| auto cmd = cmSystemTools::LowerCase(callCommand); |
| if (std::find(InvalidCommands.cbegin(), InvalidCommands.cend(), cmd) != |
| InvalidCommands.cend()) { |
| return FatalError(status, |
| cmStrCat("invalid command specified: "_s, callCommand)); |
| } |
| if (defer && |
| std::find(InvalidDeferCommands.cbegin(), InvalidDeferCommands.cend(), |
| cmd) != InvalidDeferCommands.cend()) { |
| return FatalError(status, |
| cmStrCat("invalid command specified: "_s, callCommand)); |
| } |
| |
| cmMakefile& makefile = status.GetMakefile(); |
| cmListFileContext context = makefile.GetBacktrace().Top(); |
| |
| std::vector<cmListFileArgument> funcArgs; |
| funcArgs.reserve(args.size() - startArg); |
| |
| // The rest of the arguments are passed to the function call above |
| for (size_t i = startArg; i < args.size(); ++i) { |
| funcArgs.emplace_back(args[i].Value, args[i].Delim, context.Line); |
| } |
| cmListFileFunction func{ callCommand, context.Line, context.Line, |
| std::move(funcArgs) }; |
| |
| if (defer) { |
| if (defer->Id.empty()) { |
| defer->Id = makefile.NewDeferId(); |
| } |
| if (!defer->IdVar.empty()) { |
| makefile.AddDefinition(defer->IdVar, defer->Id); |
| } |
| cmMakefile* deferMakefile = |
| defer->Directory ? defer->Directory : &makefile; |
| if (!deferMakefile->DeferCall(defer->Id, context.FilePath, func)) { |
| return FatalError( |
| status, |
| cmStrCat("DEFER CALL may not be scheduled in directory:\n "_s, |
| deferMakefile->GetCurrentBinaryDirectory(), |
| "\nat this time."_s)); |
| } |
| return true; |
| } |
| return makefile.ExecuteCommand(func, status); |
| } |
| |
| bool cmCMakeLanguageCommandDEFER(Defer const& defer, |
| std::vector<std::string> const& args, |
| size_t arg, cmExecutionStatus& status) |
| { |
| cmMakefile* deferMakefile = |
| defer.Directory ? defer.Directory : &status.GetMakefile(); |
| if (args[arg] == "CANCEL_CALL"_s) { |
| ++arg; // Consume CANCEL_CALL. |
| auto ids = cmMakeRange(args).advance(arg); |
| for (std::string const& id : ids) { |
| if (id[0] >= 'A' && id[0] <= 'Z') { |
| return FatalError( |
| status, cmStrCat("DEFER CANCEL_CALL unknown argument:\n "_s, id)); |
| } |
| if (!deferMakefile->DeferCancelCall(id)) { |
| return FatalError( |
| status, |
| cmStrCat("DEFER CANCEL_CALL may not update directory:\n "_s, |
| deferMakefile->GetCurrentBinaryDirectory(), |
| "\nat this time."_s)); |
| } |
| } |
| return true; |
| } |
| if (args[arg] == "GET_CALL_IDS"_s) { |
| ++arg; // Consume GET_CALL_IDS. |
| if (arg == args.size()) { |
| return FatalError(status, "DEFER GET_CALL_IDS missing output variable"); |
| } |
| std::string const& var = args[arg++]; |
| if (arg != args.size()) { |
| return FatalError(status, "DEFER GET_CALL_IDS given too many arguments"); |
| } |
| cm::optional<std::string> ids = deferMakefile->DeferGetCallIds(); |
| if (!ids) { |
| return FatalError( |
| status, |
| cmStrCat("DEFER GET_CALL_IDS may not access directory:\n "_s, |
| deferMakefile->GetCurrentBinaryDirectory(), |
| "\nat this time."_s)); |
| } |
| status.GetMakefile().AddDefinition(var, *ids); |
| return true; |
| } |
| if (args[arg] == "GET_CALL"_s) { |
| ++arg; // Consume GET_CALL. |
| if (arg == args.size()) { |
| return FatalError(status, "DEFER GET_CALL missing id"); |
| } |
| std::string const& id = args[arg++]; |
| if (arg == args.size()) { |
| return FatalError(status, "DEFER GET_CALL missing output variable"); |
| } |
| std::string const& var = args[arg++]; |
| if (arg != args.size()) { |
| return FatalError(status, "DEFER GET_CALL given too many arguments"); |
| } |
| if (id.empty()) { |
| return FatalError(status, "DEFER GET_CALL id may not be empty"); |
| } |
| if (id[0] >= 'A' && id[0] <= 'Z') { |
| return FatalError(status, |
| cmStrCat("DEFER GET_CALL unknown argument:\n "_s, id)); |
| } |
| cm::optional<std::string> call = deferMakefile->DeferGetCall(id); |
| if (!call) { |
| return FatalError( |
| status, |
| cmStrCat("DEFER GET_CALL may not access directory:\n "_s, |
| deferMakefile->GetCurrentBinaryDirectory(), |
| "\nat this time."_s)); |
| } |
| status.GetMakefile().AddDefinition(var, *call); |
| return true; |
| } |
| return FatalError(status, |
| cmStrCat("DEFER operation unknown: "_s, args[arg])); |
| } |
| |
| bool cmCMakeLanguageCommandEVAL(std::vector<cmListFileArgument> const& args, |
| cmExecutionStatus& status) |
| { |
| cmMakefile& makefile = status.GetMakefile(); |
| cmListFileContext context = makefile.GetBacktrace().Top(); |
| std::vector<std::string> expandedArgs; |
| makefile.ExpandArguments(args, expandedArgs); |
| |
| if (expandedArgs.size() < 2) { |
| return FatalError(status, "called with incorrect number of arguments"); |
| } |
| |
| if (expandedArgs[1] != "CODE") { |
| auto code_iter = |
| std::find(expandedArgs.begin() + 2, expandedArgs.end(), "CODE"); |
| if (code_iter == expandedArgs.end()) { |
| return FatalError(status, "called without CODE argument"); |
| } |
| return FatalError( |
| status, |
| "called with unsupported arguments between EVAL and CODE arguments"); |
| } |
| |
| std::string const code = |
| cmJoin(cmMakeRange(expandedArgs.begin() + 2, expandedArgs.end()), " "); |
| return makefile.ReadListFileAsString( |
| code, cmStrCat(context.FilePath, ":", context.Line, ":EVAL")); |
| } |
| |
| bool cmCMakeLanguageCommandSET_DEPENDENCY_PROVIDER( |
| std::vector<std::string> const& args, cmExecutionStatus& status) |
| { |
| cmState* state = status.GetMakefile().GetState(); |
| if (!state->InTopLevelIncludes()) { |
| return FatalError( |
| status, |
| "Dependency providers can only be set as part of the first call to " |
| "project(). More specifically, cmake_language(SET_DEPENDENCY_PROVIDER) " |
| "can only be called while the first project() command processes files " |
| "listed in CMAKE_PROJECT_TOP_LEVEL_INCLUDES."); |
| } |
| |
| struct SetProviderArgs |
| { |
| std::string Command; |
| ArgumentParser::NonEmpty<std::vector<std::string>> Methods; |
| }; |
| |
| auto const ArgsParser = |
| cmArgumentParser<SetProviderArgs>() |
| .Bind("SET_DEPENDENCY_PROVIDER"_s, &SetProviderArgs::Command) |
| .Bind("SUPPORTED_METHODS"_s, &SetProviderArgs::Methods); |
| |
| std::vector<std::string> unparsed; |
| auto parsedArgs = ArgsParser.Parse(args, &unparsed); |
| |
| if (!unparsed.empty()) { |
| return FatalError( |
| status, cmStrCat("Unrecognized keyword: \"", unparsed.front(), "\"")); |
| } |
| |
| // We store the command that FetchContent_MakeAvailable() can call in a |
| // global (but considered internal) property. If the provider doesn't |
| // support this method, we set this property to an empty string instead. |
| // This simplifies the logic in FetchContent_MakeAvailable() and doesn't |
| // require us to define a new internal command or sub-command. |
| std::string fcmasProperty = "__FETCHCONTENT_MAKEAVAILABLE_SERIAL_PROVIDER"; |
| |
| if (parsedArgs.Command.empty()) { |
| if (!parsedArgs.Methods.empty()) { |
| return FatalError(status, |
| "Must specify a non-empty command name when provider " |
| "methods are given"); |
| } |
| state->ClearDependencyProvider(); |
| state->SetGlobalProperty(fcmasProperty, ""); |
| return true; |
| } |
| |
| cmState::Command command = state->GetCommand(parsedArgs.Command); |
| if (!command) { |
| return FatalError(status, |
| cmStrCat("Command \"", parsedArgs.Command, |
| "\" is not a defined command")); |
| } |
| |
| if (parsedArgs.Methods.empty()) { |
| return FatalError(status, "Must specify at least one provider method"); |
| } |
| |
| bool supportsFetchContentMakeAvailableSerial = false; |
| std::vector<cmDependencyProvider::Method> methods; |
| for (auto const& method : parsedArgs.Methods) { |
| if (method == "FIND_PACKAGE") { |
| methods.emplace_back(cmDependencyProvider::Method::FindPackage); |
| } else if (method == "FETCHCONTENT_MAKEAVAILABLE_SERIAL") { |
| supportsFetchContentMakeAvailableSerial = true; |
| methods.emplace_back( |
| cmDependencyProvider::Method::FetchContentMakeAvailableSerial); |
| } else { |
| return FatalError( |
| status, |
| cmStrCat("Unknown dependency provider method \"", method, "\"")); |
| } |
| } |
| |
| state->SetDependencyProvider({ parsedArgs.Command, methods }); |
| state->SetGlobalProperty( |
| fcmasProperty, |
| supportsFetchContentMakeAvailableSerial ? parsedArgs.Command : ""); |
| |
| return true; |
| } |
| |
| bool cmCMakeLanguageCommandGET_MESSAGE_LOG_LEVEL( |
| std::vector<cmListFileArgument> const& args, cmExecutionStatus& status) |
| { |
| cmMakefile& makefile = status.GetMakefile(); |
| std::vector<std::string> expandedArgs; |
| makefile.ExpandArguments(args, expandedArgs); |
| |
| if (args.size() < 2 || expandedArgs.size() > 2) { |
| return FatalError( |
| status, |
| "sub-command GET_MESSAGE_LOG_LEVEL expects exactly one argument"); |
| } |
| |
| Message::LogLevel logLevel = makefile.GetCurrentLogLevel(); |
| std::string outputValue = cmake::LogLevelToString(logLevel); |
| |
| std::string const& outputVariable = expandedArgs[1]; |
| makefile.AddDefinition(outputVariable, outputValue); |
| return true; |
| } |
| |
| bool cmCMakeLanguageCommandGET_EXPERIMENTAL_FEATURE_ENABLED( |
| std::vector<cmListFileArgument> const& args, cmExecutionStatus& status) |
| { |
| cmMakefile& makefile = status.GetMakefile(); |
| std::vector<std::string> expandedArgs; |
| makefile.ExpandArguments(args, expandedArgs); |
| |
| if (expandedArgs.size() != 3) { |
| return FatalError(status, |
| "sub-command GET_EXPERIMENTAL_FEATURE_ENABLED expects " |
| "exactly two arguments"); |
| } |
| |
| auto const& featureName = expandedArgs[1]; |
| auto const& variableName = expandedArgs[2]; |
| |
| if (auto feature = cmExperimental::FeatureByName(featureName)) { |
| if (cmExperimental::HasSupportEnabled(makefile, *feature)) { |
| makefile.AddDefinition(variableName, "TRUE"); |
| } else { |
| makefile.AddDefinition(variableName, "FALSE"); |
| } |
| } else { |
| return FatalError(status, |
| cmStrCat("Experimental feature name \"", featureName, |
| "\" does not exist.")); |
| } |
| |
| return true; |
| } |
| } |
| |
| bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args, |
| cmExecutionStatus& status) |
| { |
| std::vector<std::string> expArgs; |
| size_t rawArg = 0; |
| size_t expArg = 0; |
| |
| // Helper to consume and expand one raw argument at a time. |
| auto moreArgs = [&]() -> bool { |
| while (expArg >= expArgs.size()) { |
| if (rawArg >= args.size()) { |
| return false; |
| } |
| std::vector<cmListFileArgument> tmpArg; |
| tmpArg.emplace_back(args[rawArg++]); |
| status.GetMakefile().ExpandArguments(tmpArg, expArgs); |
| } |
| return true; |
| }; |
| auto finishArgs = [&]() { |
| std::vector<cmListFileArgument> tmpArgs(args.begin() + rawArg, args.end()); |
| status.GetMakefile().ExpandArguments(tmpArgs, expArgs); |
| rawArg = args.size(); |
| }; |
| |
| if (!moreArgs()) { |
| return FatalError(status, "called with incorrect number of arguments"); |
| } |
| if (expArgs[expArg] == "EXIT"_s) { |
| ++expArg; // consume "EXIT". |
| |
| if (!moreArgs()) { |
| return FatalError(status, "EXIT requires one argument"); |
| } |
| |
| auto workingMode = |
| status.GetMakefile().GetCMakeInstance()->GetWorkingMode(); |
| if (workingMode != cmake::SCRIPT_MODE) { |
| return FatalError(status, "EXIT can be used only in SCRIPT mode"); |
| } |
| |
| long retCode = 0; |
| |
| if (!cmStrToLong(expArgs[expArg], &retCode)) { |
| return FatalError(status, |
| cmStrCat("EXIT requires one integral argument, got \"", |
| expArgs[expArg], '\"')); |
| } |
| |
| if (workingMode == cmake::SCRIPT_MODE) { |
| status.SetExitCode(static_cast<int>(retCode)); |
| } |
| return true; |
| } |
| |
| if (expArgs[expArg] == "SET_DEPENDENCY_PROVIDER"_s) { |
| finishArgs(); |
| return cmCMakeLanguageCommandSET_DEPENDENCY_PROVIDER(expArgs, status); |
| } |
| |
| cm::optional<Defer> maybeDefer; |
| if (expArgs[expArg] == "DEFER"_s) { |
| ++expArg; // Consume "DEFER". |
| |
| if (!moreArgs()) { |
| return FatalError(status, "DEFER requires at least one argument"); |
| } |
| |
| Defer defer; |
| |
| // Process optional arguments. |
| while (moreArgs()) { |
| if (expArgs[expArg] == "CALL"_s) { |
| break; |
| } |
| if (expArgs[expArg] == "CANCEL_CALL"_s || |
| expArgs[expArg] == "GET_CALL_IDS"_s || |
| expArgs[expArg] == "GET_CALL"_s) { |
| if (!defer.Id.empty() || !defer.IdVar.empty()) { |
| return FatalError(status, |
| cmStrCat("DEFER "_s, expArgs[expArg], |
| " does not accept ID or ID_VAR."_s)); |
| } |
| finishArgs(); |
| return cmCMakeLanguageCommandDEFER(defer, expArgs, expArg, status); |
| } |
| if (expArgs[expArg] == "DIRECTORY"_s) { |
| ++expArg; // Consume "DIRECTORY". |
| if (defer.Directory) { |
| return FatalError(status, |
| "DEFER given multiple DIRECTORY arguments"); |
| } |
| if (!moreArgs()) { |
| return FatalError(status, "DEFER DIRECTORY missing value"); |
| } |
| std::string dir = expArgs[expArg++]; |
| if (dir.empty()) { |
| return FatalError(status, "DEFER DIRECTORY may not be empty"); |
| } |
| dir = cmSystemTools::CollapseFullPath( |
| dir, status.GetMakefile().GetCurrentSourceDirectory()); |
| defer.Directory = |
| status.GetMakefile().GetGlobalGenerator()->FindMakefile(dir); |
| if (!defer.Directory) { |
| return FatalError(status, |
| cmStrCat("DEFER DIRECTORY:\n "_s, dir, |
| "\nis not known. "_s, |
| "It may not have been processed yet."_s)); |
| } |
| } else if (expArgs[expArg] == "ID"_s) { |
| ++expArg; // Consume "ID". |
| if (!defer.Id.empty()) { |
| return FatalError(status, "DEFER given multiple ID arguments"); |
| } |
| if (!moreArgs()) { |
| return FatalError(status, "DEFER ID missing value"); |
| } |
| defer.Id = expArgs[expArg++]; |
| if (defer.Id.empty()) { |
| return FatalError(status, "DEFER ID may not be empty"); |
| } |
| if (defer.Id[0] >= 'A' && defer.Id[0] <= 'Z') { |
| return FatalError(status, "DEFER ID may not start in A-Z."); |
| } |
| } else if (expArgs[expArg] == "ID_VAR"_s) { |
| ++expArg; // Consume "ID_VAR". |
| if (!defer.IdVar.empty()) { |
| return FatalError(status, "DEFER given multiple ID_VAR arguments"); |
| } |
| if (!moreArgs()) { |
| return FatalError(status, "DEFER ID_VAR missing variable name"); |
| } |
| defer.IdVar = expArgs[expArg++]; |
| if (defer.IdVar.empty()) { |
| return FatalError(status, "DEFER ID_VAR may not be empty"); |
| } |
| } else { |
| return FatalError( |
| status, cmStrCat("DEFER unknown option:\n "_s, expArgs[expArg])); |
| } |
| } |
| |
| if (!(moreArgs() && expArgs[expArg] == "CALL"_s)) { |
| return FatalError(status, "DEFER must be followed by a CALL argument"); |
| } |
| |
| maybeDefer = std::move(defer); |
| } |
| |
| if (expArgs[expArg] == "CALL") { |
| ++expArg; // Consume "CALL". |
| |
| // CALL requires a command name. |
| if (!moreArgs()) { |
| return FatalError(status, "CALL missing command name"); |
| } |
| std::string const& callCommand = expArgs[expArg++]; |
| |
| // CALL accepts no further expanded arguments. |
| if (expArg != expArgs.size()) { |
| return FatalError(status, "CALL command's arguments must be literal"); |
| } |
| |
| // Run the CALL. |
| return cmCMakeLanguageCommandCALL(args, callCommand, rawArg, |
| std::move(maybeDefer), status); |
| } |
| |
| if (expArgs[expArg] == "EVAL") { |
| return cmCMakeLanguageCommandEVAL(args, status); |
| } |
| |
| if (expArgs[expArg] == "GET_MESSAGE_LOG_LEVEL") { |
| return cmCMakeLanguageCommandGET_MESSAGE_LOG_LEVEL(args, status); |
| } |
| |
| if (expArgs[expArg] == "GET_EXPERIMENTAL_FEATURE_ENABLED") { |
| return cmCMakeLanguageCommandGET_EXPERIMENTAL_FEATURE_ENABLED(args, |
| status); |
| } |
| |
| return FatalError(status, "called with unknown meta-operation"); |
| } |