/* 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 #include /* isspace */ #include #include #include #include #include #include #include #include #include #include #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(c)) || c == '\n' || c == '\r'); } void cmExecuteProcessCommandFixText(std::vector& output, bool strip_trailing_whitespace); void cmExecuteProcessCommandAppend(std::vector& output, const char* data, int length); } // cmExecuteProcessCommand bool cmExecuteProcessCommand(std::vector const& args, cmExecutionStatus& status) { if (args.empty()) { status.SetError("called with incorrect number of arguments"); return false; } struct Arguments { std::vector> 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{} .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 unparsedArguments; std::vector 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 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 cp_ptr( cmsysProcess_New(), cmsysProcess_Delete); cmsysProcess* cp = cp_ptr.get(); // Set the command sequence. for (std::vector const& cmd : arguments.Commands) { std::vector 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 (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 tempOutput; std::vector 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 res; for (size_t i = 0; i < arguments.Commands.size(); ++i) { switch (cmsysProcess_GetStateByIndex(cp, static_cast(i))) { case kwsysProcess_StateByIndex_Exited: { int exitCode = cmsysProcess_GetExitValueByIndex(cp, static_cast(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(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(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(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 failureIndices; for (int i = 0; i < static_cast(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(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& 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& 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); } }