| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| file Copyright.txt or https://cmake.org/licensing for details. */ | |
| #include "cmExecuteProcessCommand.h" | |
| #include <algorithm> | |
| #include <cctype> /* isspace */ | |
| #include <cstdio> | |
| #include <iostream> | |
| #include <map> | |
| #include <memory> | |
| #include <sstream> | |
| #include <utility> | |
| #include <vector> | |
| #include <cm/string_view> | |
| #include <cmext/algorithm> | |
| #include <cmext/string_view> | |
| #include "cmsys/Process.h" | |
| #include "cmArgumentParser.h" | |
| #include "cmExecutionStatus.h" | |
| #include "cmMakefile.h" | |
| #include "cmMessageType.h" | |
| #include "cmProcessOutput.h" | |
| #include "cmStringAlgorithms.h" | |
| #include "cmSystemTools.h" | |
| namespace { | |
| bool cmExecuteProcessCommandIsWhitespace(char c) | |
| { | |
| return (isspace(static_cast<int>(c)) || c == '\n' || c == '\r'); | |
| } | |
| void cmExecuteProcessCommandFixText(std::vector<char>& output, | |
| bool strip_trailing_whitespace); | |
| void cmExecuteProcessCommandAppend(std::vector<char>& output, const char* data, | |
| int length); | |
| } | |
| // cmExecuteProcessCommand | |
| bool cmExecuteProcessCommand(std::vector<std::string> const& args, | |
| cmExecutionStatus& status) | |
| { | |
| if (args.empty()) { | |
| status.SetError("called with incorrect number of arguments"); | |
| return false; | |
| } | |
| struct Arguments | |
| { | |
| std::vector<std::vector<std::string>> Commands; | |
| std::string OutputVariable; | |
| std::string ErrorVariable; | |
| std::string ResultVariable; | |
| std::string ResultsVariable; | |
| std::string WorkingDirectory; | |
| std::string InputFile; | |
| std::string OutputFile; | |
| std::string ErrorFile; | |
| std::string Timeout; | |
| std::string CommandEcho; | |
| bool OutputQuiet = false; | |
| bool ErrorQuiet = false; | |
| bool OutputStripTrailingWhitespace = false; | |
| bool ErrorStripTrailingWhitespace = false; | |
| bool EchoOutputVariable = false; | |
| bool EchoErrorVariable = false; | |
| std::string Encoding; | |
| std::string CommandErrorIsFatal; | |
| }; | |
| static auto const parser = | |
| cmArgumentParser<Arguments>{} | |
| .Bind("COMMAND"_s, &Arguments::Commands) | |
| .Bind("COMMAND_ECHO"_s, &Arguments::CommandEcho) | |
| .Bind("OUTPUT_VARIABLE"_s, &Arguments::OutputVariable) | |
| .Bind("ERROR_VARIABLE"_s, &Arguments::ErrorVariable) | |
| .Bind("RESULT_VARIABLE"_s, &Arguments::ResultVariable) | |
| .Bind("RESULTS_VARIABLE"_s, &Arguments::ResultsVariable) | |
| .Bind("WORKING_DIRECTORY"_s, &Arguments::WorkingDirectory) | |
| .Bind("INPUT_FILE"_s, &Arguments::InputFile) | |
| .Bind("OUTPUT_FILE"_s, &Arguments::OutputFile) | |
| .Bind("ERROR_FILE"_s, &Arguments::ErrorFile) | |
| .Bind("TIMEOUT"_s, &Arguments::Timeout) | |
| .Bind("OUTPUT_QUIET"_s, &Arguments::OutputQuiet) | |
| .Bind("ERROR_QUIET"_s, &Arguments::ErrorQuiet) | |
| .Bind("OUTPUT_STRIP_TRAILING_WHITESPACE"_s, | |
| &Arguments::OutputStripTrailingWhitespace) | |
| .Bind("ERROR_STRIP_TRAILING_WHITESPACE"_s, | |
| &Arguments::ErrorStripTrailingWhitespace) | |
| .Bind("ENCODING"_s, &Arguments::Encoding) | |
| .Bind("ECHO_OUTPUT_VARIABLE"_s, &Arguments::EchoOutputVariable) | |
| .Bind("ECHO_ERROR_VARIABLE"_s, &Arguments::EchoErrorVariable) | |
| .Bind("COMMAND_ERROR_IS_FATAL"_s, &Arguments::CommandErrorIsFatal); | |
| std::vector<std::string> unparsedArguments; | |
| std::vector<std::string> keywordsMissingValue; | |
| Arguments const arguments = | |
| parser.Parse(args, &unparsedArguments, &keywordsMissingValue); | |
| if (!keywordsMissingValue.empty()) { | |
| status.SetError(" called with no value for " + | |
| keywordsMissingValue.front() + "."); | |
| return false; | |
| } | |
| if (!unparsedArguments.empty()) { | |
| status.SetError(" given unknown argument \"" + unparsedArguments.front() + | |
| "\"."); | |
| return false; | |
| } | |
| if (!status.GetMakefile().CanIWriteThisFile(arguments.OutputFile)) { | |
| status.SetError("attempted to output into a file: " + | |
| arguments.OutputFile + " into a source directory."); | |
| cmSystemTools::SetFatalErrorOccured(); | |
| return false; | |
| } | |
| // Check for commands given. | |
| if (arguments.Commands.empty()) { | |
| status.SetError(" called with no COMMAND argument."); | |
| return false; | |
| } | |
| for (std::vector<std::string> const& cmd : arguments.Commands) { | |
| if (cmd.empty()) { | |
| status.SetError(" given COMMAND argument with no value."); | |
| return false; | |
| } | |
| } | |
| // Parse the timeout string. | |
| double timeout = -1; | |
| if (!arguments.Timeout.empty()) { | |
| if (sscanf(arguments.Timeout.c_str(), "%lg", &timeout) != 1) { | |
| status.SetError(" called with TIMEOUT value that could not be parsed."); | |
| return false; | |
| } | |
| } | |
| if (!arguments.CommandErrorIsFatal.empty()) { | |
| if (arguments.CommandErrorIsFatal != "ANY"_s && | |
| arguments.CommandErrorIsFatal != "LAST"_s) { | |
| status.SetError("COMMAND_ERROR_IS_FATAL option can be ANY or LAST"); | |
| return false; | |
| } | |
| } | |
| // Create a process instance. | |
| std::unique_ptr<cmsysProcess, void (*)(cmsysProcess*)> cp_ptr( | |
| cmsysProcess_New(), cmsysProcess_Delete); | |
| cmsysProcess* cp = cp_ptr.get(); | |
| // Set the command sequence. | |
| for (std::vector<std::string> const& cmd : arguments.Commands) { | |
| std::vector<const char*> argv(cmd.size() + 1); | |
| std::transform(cmd.begin(), cmd.end(), argv.begin(), | |
| [](std::string const& s) { return s.c_str(); }); | |
| argv.back() = nullptr; | |
| cmsysProcess_AddCommand(cp, argv.data()); | |
| } | |
| // Set the process working directory. | |
| if (!arguments.WorkingDirectory.empty()) { | |
| cmsysProcess_SetWorkingDirectory(cp, arguments.WorkingDirectory.c_str()); | |
| } | |
| // Always hide the process window. | |
| cmsysProcess_SetOption(cp, cmsysProcess_Option_HideWindow, 1); | |
| // Check the output variables. | |
| bool merge_output = false; | |
| if (!arguments.InputFile.empty()) { | |
| cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDIN, | |
| arguments.InputFile.c_str()); | |
| } | |
| if (!arguments.OutputFile.empty()) { | |
| cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDOUT, | |
| arguments.OutputFile.c_str()); | |
| } | |
| if (!arguments.ErrorFile.empty()) { | |
| if (arguments.ErrorFile == arguments.OutputFile) { | |
| merge_output = true; | |
| } else { | |
| cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDERR, | |
| arguments.ErrorFile.c_str()); | |
| } | |
| } | |
| if (!arguments.OutputVariable.empty() && | |
| arguments.OutputVariable == arguments.ErrorVariable) { | |
| merge_output = true; | |
| } | |
| if (merge_output) { | |
| cmsysProcess_SetOption(cp, cmsysProcess_Option_MergeOutput, 1); | |
| } | |
| // Set the timeout if any. | |
| if (timeout >= 0) { | |
| cmsysProcess_SetTimeout(cp, timeout); | |
| } | |
| bool echo_stdout = false; | |
| bool echo_stderr = false; | |
| bool echo_output_from_variable = true; | |
| std::string echo_output = status.GetMakefile().GetSafeDefinition( | |
| "CMAKE_EXECUTE_PROCESS_COMMAND_ECHO"); | |
| if (!arguments.CommandEcho.empty()) { | |
| echo_output_from_variable = false; | |
| echo_output = arguments.CommandEcho; | |
| } | |
| if (!echo_output.empty()) { | |
| if (echo_output == "STDERR") { | |
| echo_stderr = true; | |
| } else if (echo_output == "STDOUT") { | |
| echo_stdout = true; | |
| } else if (echo_output != "NONE") { | |
| std::string error; | |
| if (echo_output_from_variable) { | |
| error = "CMAKE_EXECUTE_PROCESS_COMMAND_ECHO set to '"; | |
| } else { | |
| error = " called with '"; | |
| } | |
| error += echo_output; | |
| error += "' expected STDERR|STDOUT|NONE"; | |
| if (!echo_output_from_variable) { | |
| error += " for COMMAND_ECHO."; | |
| } | |
| status.GetMakefile().IssueMessage(MessageType::FATAL_ERROR, error); | |
| return true; | |
| } | |
| } | |
| if (echo_stdout || echo_stderr) { | |
| std::string command; | |
| for (const auto& cmd : arguments.Commands) { | |
| command += "'"; | |
| command += cmJoin(cmd, "' '"); | |
| command += "'"; | |
| command += "\n"; | |
| } | |
| if (echo_stdout) { | |
| std::cout << command; | |
| } else if (echo_stderr) { | |
| std::cerr << command; | |
| } | |
| } | |
| // Start the process. | |
| cmsysProcess_Execute(cp); | |
| // Read the process output. | |
| std::vector<char> tempOutput; | |
| std::vector<char> tempError; | |
| int length; | |
| char* data; | |
| int p; | |
| cmProcessOutput processOutput( | |
| cmProcessOutput::FindEncoding(arguments.Encoding)); | |
| std::string strdata; | |
| while ((p = cmsysProcess_WaitForData(cp, &data, &length, nullptr))) { | |
| // Put the output in the right place. | |
| if (p == cmsysProcess_Pipe_STDOUT && !arguments.OutputQuiet) { | |
| if (arguments.OutputVariable.empty() || arguments.EchoOutputVariable) { | |
| processOutput.DecodeText(data, length, strdata, 1); | |
| cmSystemTools::Stdout(strdata); | |
| } | |
| if (!arguments.OutputVariable.empty()) { | |
| cmExecuteProcessCommandAppend(tempOutput, data, length); | |
| } | |
| } else if (p == cmsysProcess_Pipe_STDERR && !arguments.ErrorQuiet) { | |
| if (arguments.ErrorVariable.empty() || arguments.EchoErrorVariable) { | |
| processOutput.DecodeText(data, length, strdata, 2); | |
| cmSystemTools::Stderr(strdata); | |
| } | |
| if (!arguments.ErrorVariable.empty()) { | |
| cmExecuteProcessCommandAppend(tempError, data, length); | |
| } | |
| } | |
| } | |
| if (!arguments.OutputQuiet && | |
| (arguments.OutputVariable.empty() || arguments.EchoOutputVariable)) { | |
| processOutput.DecodeText(std::string(), strdata, 1); | |
| if (!strdata.empty()) { | |
| cmSystemTools::Stdout(strdata); | |
| } | |
| } | |
| if (!arguments.ErrorQuiet && | |
| (arguments.ErrorVariable.empty() || arguments.EchoErrorVariable)) { | |
| processOutput.DecodeText(std::string(), strdata, 2); | |
| if (!strdata.empty()) { | |
| cmSystemTools::Stderr(strdata); | |
| } | |
| } | |
| // All output has been read. Wait for the process to exit. | |
| cmsysProcess_WaitForExit(cp, nullptr); | |
| processOutput.DecodeText(tempOutput, tempOutput); | |
| processOutput.DecodeText(tempError, tempError); | |
| // Fix the text in the output strings. | |
| cmExecuteProcessCommandFixText(tempOutput, | |
| arguments.OutputStripTrailingWhitespace); | |
| cmExecuteProcessCommandFixText(tempError, | |
| arguments.ErrorStripTrailingWhitespace); | |
| // Store the output obtained. | |
| if (!arguments.OutputVariable.empty() && !tempOutput.empty()) { | |
| status.GetMakefile().AddDefinition(arguments.OutputVariable, | |
| tempOutput.data()); | |
| } | |
| if (!merge_output && !arguments.ErrorVariable.empty() && | |
| !tempError.empty()) { | |
| status.GetMakefile().AddDefinition(arguments.ErrorVariable, | |
| tempError.data()); | |
| } | |
| // Store the result of running the process. | |
| if (!arguments.ResultVariable.empty()) { | |
| switch (cmsysProcess_GetState(cp)) { | |
| case cmsysProcess_State_Exited: { | |
| int v = cmsysProcess_GetExitValue(cp); | |
| char buf[16]; | |
| sprintf(buf, "%d", v); | |
| status.GetMakefile().AddDefinition(arguments.ResultVariable, buf); | |
| } break; | |
| case cmsysProcess_State_Exception: | |
| status.GetMakefile().AddDefinition( | |
| arguments.ResultVariable, cmsysProcess_GetExceptionString(cp)); | |
| break; | |
| case cmsysProcess_State_Error: | |
| status.GetMakefile().AddDefinition(arguments.ResultVariable, | |
| cmsysProcess_GetErrorString(cp)); | |
| break; | |
| case cmsysProcess_State_Expired: | |
| status.GetMakefile().AddDefinition( | |
| arguments.ResultVariable, "Process terminated due to timeout"); | |
| break; | |
| } | |
| } | |
| // Store the result of running the processes. | |
| if (!arguments.ResultsVariable.empty()) { | |
| switch (cmsysProcess_GetState(cp)) { | |
| case cmsysProcess_State_Exited: { | |
| std::vector<std::string> res; | |
| for (size_t i = 0; i < arguments.Commands.size(); ++i) { | |
| switch (cmsysProcess_GetStateByIndex(cp, static_cast<int>(i))) { | |
| case kwsysProcess_StateByIndex_Exited: { | |
| int exitCode = | |
| cmsysProcess_GetExitValueByIndex(cp, static_cast<int>(i)); | |
| char buf[16]; | |
| sprintf(buf, "%d", exitCode); | |
| res.emplace_back(buf); | |
| } break; | |
| case kwsysProcess_StateByIndex_Exception: | |
| res.emplace_back(cmsysProcess_GetExceptionStringByIndex( | |
| cp, static_cast<int>(i))); | |
| break; | |
| case kwsysProcess_StateByIndex_Error: | |
| default: | |
| res.emplace_back("Error getting the child return code"); | |
| break; | |
| } | |
| } | |
| status.GetMakefile().AddDefinition(arguments.ResultsVariable, | |
| cmJoin(res, ";")); | |
| } break; | |
| case cmsysProcess_State_Exception: | |
| status.GetMakefile().AddDefinition( | |
| arguments.ResultsVariable, cmsysProcess_GetExceptionString(cp)); | |
| break; | |
| case cmsysProcess_State_Error: | |
| status.GetMakefile().AddDefinition(arguments.ResultsVariable, | |
| cmsysProcess_GetErrorString(cp)); | |
| break; | |
| case cmsysProcess_State_Expired: | |
| status.GetMakefile().AddDefinition( | |
| arguments.ResultsVariable, "Process terminated due to timeout"); | |
| break; | |
| } | |
| } | |
| auto queryProcessStatusByIndex = [&cp](int index) -> std::string { | |
| std::string processStatus; | |
| switch (cmsysProcess_GetStateByIndex(cp, static_cast<int>(index))) { | |
| case kwsysProcess_StateByIndex_Exited: { | |
| int exitCode = cmsysProcess_GetExitValueByIndex(cp, index); | |
| if (exitCode) { | |
| processStatus = "Child return code: " + std::to_string(exitCode); | |
| } | |
| } break; | |
| case kwsysProcess_StateByIndex_Exception: { | |
| processStatus = cmStrCat( | |
| "Abnormal exit with child return code: ", | |
| cmsysProcess_GetExceptionStringByIndex(cp, static_cast<int>(index))); | |
| break; | |
| } | |
| case kwsysProcess_StateByIndex_Error: | |
| default: | |
| processStatus = "Error getting the child return code"; | |
| break; | |
| } | |
| return processStatus; | |
| }; | |
| if (arguments.CommandErrorIsFatal == "ANY"_s) { | |
| bool ret = true; | |
| switch (cmsysProcess_GetState(cp)) { | |
| case cmsysProcess_State_Exited: { | |
| std::map<int, std::string> failureIndices; | |
| for (int i = 0; i < static_cast<int>(arguments.Commands.size()); ++i) { | |
| std::string processStatus = queryProcessStatusByIndex(i); | |
| if (!processStatus.empty()) { | |
| failureIndices[i] = processStatus; | |
| } | |
| if (!failureIndices.empty()) { | |
| std::ostringstream oss; | |
| oss << "failed command indexes:\n"; | |
| for (auto const& e : failureIndices) { | |
| oss << " " << e.first + 1 << ": \"" << e.second << "\"\n"; | |
| } | |
| status.SetError(oss.str()); | |
| ret = false; | |
| } | |
| } | |
| } break; | |
| case cmsysProcess_State_Exception: | |
| status.SetError( | |
| cmStrCat("abnormal exit: ", cmsysProcess_GetExceptionString(cp))); | |
| ret = false; | |
| break; | |
| case cmsysProcess_State_Error: | |
| status.SetError(cmStrCat("error getting child return code: ", | |
| cmsysProcess_GetErrorString(cp))); | |
| ret = false; | |
| break; | |
| case cmsysProcess_State_Expired: | |
| status.SetError("Process terminated due to timeout"); | |
| ret = false; | |
| break; | |
| } | |
| if (!ret) { | |
| cmSystemTools::SetFatalErrorOccured(); | |
| return false; | |
| } | |
| } | |
| if (arguments.CommandErrorIsFatal == "LAST"_s) { | |
| bool ret = true; | |
| switch (cmsysProcess_GetState(cp)) { | |
| case cmsysProcess_State_Exited: { | |
| int lastIndex = static_cast<int>(arguments.Commands.size() - 1); | |
| const std::string processStatus = queryProcessStatusByIndex(lastIndex); | |
| if (!processStatus.empty()) { | |
| status.SetError("last command failed"); | |
| ret = false; | |
| } | |
| } break; | |
| case cmsysProcess_State_Exception: | |
| status.SetError( | |
| cmStrCat("Abnormal exit: ", cmsysProcess_GetExceptionString(cp))); | |
| ret = false; | |
| break; | |
| case cmsysProcess_State_Error: | |
| status.SetError(cmStrCat("Error getting child return code: ", | |
| cmsysProcess_GetErrorString(cp))); | |
| ret = false; | |
| break; | |
| case cmsysProcess_State_Expired: | |
| status.SetError("Process terminated due to timeout"); | |
| ret = false; | |
| break; | |
| } | |
| if (!ret) { | |
| cmSystemTools::SetFatalErrorOccured(); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| namespace { | |
| void cmExecuteProcessCommandFixText(std::vector<char>& output, | |
| bool strip_trailing_whitespace) | |
| { | |
| // Remove \0 characters and the \r part of \r\n pairs. | |
| unsigned int in_index = 0; | |
| unsigned int out_index = 0; | |
| while (in_index < output.size()) { | |
| char c = output[in_index++]; | |
| if ((c != '\r' || | |
| !(in_index < output.size() && output[in_index] == '\n')) && | |
| c != '\0') { | |
| output[out_index++] = c; | |
| } | |
| } | |
| // Remove trailing whitespace if requested. | |
| if (strip_trailing_whitespace) { | |
| while (out_index > 0 && | |
| cmExecuteProcessCommandIsWhitespace(output[out_index - 1])) { | |
| --out_index; | |
| } | |
| } | |
| // Shrink the vector to the size needed. | |
| output.resize(out_index); | |
| // Put a terminator on the text string. | |
| output.push_back('\0'); | |
| } | |
| void cmExecuteProcessCommandAppend(std::vector<char>& output, const char* data, | |
| int length) | |
| { | |
| #if defined(__APPLE__) | |
| // HACK on Apple to work around bug with inserting at the | |
| // end of an empty vector. This resulted in random failures | |
| // that were hard to reproduce. | |
| if (output.empty() && length > 0) { | |
| output.push_back(data[0]); | |
| ++data; | |
| --length; | |
| } | |
| #endif | |
| cm::append(output, data, data + length); | |
| } | |
| } |