/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmCTestLaunch.h" #include "cmsys/FStream.hxx" #include "cmsys/Process.h" #include "cmsys/RegularExpression.hxx" #include #include #include #include "cmCryptoHash.h" #include "cmGeneratedFileStream.h" #include "cmGlobalGenerator.h" #include "cmMakefile.h" #include "cmProcessOutput.h" #include "cmState.h" #include "cmStateSnapshot.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmXMLWriter.h" #include "cmake.h" #ifdef _WIN32 # include // for _O_BINARY # include // for _setmode # include // for std{out,err} and fileno #endif cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv) { this->Passthru = true; this->Process = nullptr; this->ExitCode = 1; this->CWD = cmSystemTools::GetCurrentWorkingDirectory(); if (!this->ParseArguments(argc, argv)) { return; } this->ComputeFileNames(); this->ScrapeRulesLoaded = false; this->HaveOut = false; this->HaveErr = false; this->Process = cmsysProcess_New(); } cmCTestLaunch::~cmCTestLaunch() { cmsysProcess_Delete(this->Process); if (!this->Passthru) { cmSystemTools::RemoveFile(this->LogOut); cmSystemTools::RemoveFile(this->LogErr); } } bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv) { // Launcher options occur first and are separated from the real // command line by a '--' option. enum Doing { DoingNone, DoingOutput, DoingSource, DoingLanguage, DoingTargetName, DoingTargetType, DoingBuildDir, DoingCount, DoingFilterPrefix }; Doing doing = DoingNone; int arg0 = 0; for (int i = 1; !arg0 && i < argc; ++i) { const char* arg = argv[i]; if (strcmp(arg, "--") == 0) { arg0 = i + 1; } else if (strcmp(arg, "--output") == 0) { doing = DoingOutput; } else if (strcmp(arg, "--source") == 0) { doing = DoingSource; } else if (strcmp(arg, "--language") == 0) { doing = DoingLanguage; } else if (strcmp(arg, "--target-name") == 0) { doing = DoingTargetName; } else if (strcmp(arg, "--target-type") == 0) { doing = DoingTargetType; } else if (strcmp(arg, "--build-dir") == 0) { doing = DoingBuildDir; } else if (strcmp(arg, "--filter-prefix") == 0) { doing = DoingFilterPrefix; } else if (doing == DoingOutput) { this->OptionOutput = arg; doing = DoingNone; } else if (doing == DoingSource) { this->OptionSource = arg; doing = DoingNone; } else if (doing == DoingLanguage) { this->OptionLanguage = arg; if (this->OptionLanguage == "CXX") { this->OptionLanguage = "C++"; } doing = DoingNone; } else if (doing == DoingTargetName) { this->OptionTargetName = arg; doing = DoingNone; } else if (doing == DoingTargetType) { this->OptionTargetType = arg; doing = DoingNone; } else if (doing == DoingBuildDir) { this->OptionBuildDir = arg; doing = DoingNone; } else if (doing == DoingFilterPrefix) { this->OptionFilterPrefix = arg; doing = DoingNone; } } // Extract the real command line. if (arg0) { this->RealArgC = argc - arg0; this->RealArgV = argv + arg0; for (int i = 0; i < this->RealArgC; ++i) { this->HandleRealArg(this->RealArgV[i]); } return true; } this->RealArgC = 0; this->RealArgV = nullptr; std::cerr << "No launch/command separator ('--') found!\n"; return false; } void cmCTestLaunch::HandleRealArg(const char* arg) { #ifdef _WIN32 // Expand response file arguments. if (arg[0] == '@' && cmSystemTools::FileExists(arg + 1)) { cmsys::ifstream fin(arg + 1); std::string line; while (cmSystemTools::GetLineFromStream(fin, line)) { cmSystemTools::ParseWindowsCommandLine(line.c_str(), this->RealArgs); } return; } #endif this->RealArgs.emplace_back(arg); } void cmCTestLaunch::ComputeFileNames() { // We just passthru the behavior of the real command unless the // CTEST_LAUNCH_LOGS environment variable is set. const char* d = getenv("CTEST_LAUNCH_LOGS"); if (!(d && *d)) { return; } this->Passthru = false; // The environment variable specifies the directory into which we // generate build logs. this->LogDir = d; cmSystemTools::ConvertToUnixSlashes(this->LogDir); this->LogDir += "/"; // We hash the input command working dir and command line to obtain // a repeatable and (probably) unique name for log files. cmCryptoHash md5(cmCryptoHash::AlgoMD5); md5.Initialize(); md5.Append(this->CWD); for (std::string const& realArg : this->RealArgs) { md5.Append(realArg); } this->LogHash = md5.FinalizeHex(); // We store stdout and stderr in temporary log files. this->LogOut = this->LogDir; this->LogOut += "launch-"; this->LogOut += this->LogHash; this->LogOut += "-out.txt"; this->LogErr = this->LogDir; this->LogErr += "launch-"; this->LogErr += this->LogHash; this->LogErr += "-err.txt"; } void cmCTestLaunch::RunChild() { // Ignore noopt make rules if (this->RealArgs.empty() || this->RealArgs[0] == ":") { this->ExitCode = 0; return; } // Prepare to run the real command. cmsysProcess* cp = this->Process; cmsysProcess_SetCommand(cp, this->RealArgV); cmsys::ofstream fout; cmsys::ofstream ferr; if (this->Passthru) { // In passthru mode we just share the output pipes. cmsysProcess_SetPipeShared(cp, cmsysProcess_Pipe_STDOUT, 1); cmsysProcess_SetPipeShared(cp, cmsysProcess_Pipe_STDERR, 1); } else { // In full mode we record the child output pipes to log files. fout.open(this->LogOut.c_str(), std::ios::out | std::ios::binary); ferr.open(this->LogErr.c_str(), std::ios::out | std::ios::binary); } #ifdef _WIN32 // Do this so that newline transformation is not done when writing to cout // and cerr below. _setmode(fileno(stdout), _O_BINARY); _setmode(fileno(stderr), _O_BINARY); #endif // Run the real command. cmsysProcess_Execute(cp); // Record child stdout and stderr if necessary. if (!this->Passthru) { char* data = nullptr; int length = 0; cmProcessOutput processOutput; std::string strdata; while (int p = cmsysProcess_WaitForData(cp, &data, &length, nullptr)) { if (p == cmsysProcess_Pipe_STDOUT) { processOutput.DecodeText(data, length, strdata, 1); fout.write(strdata.c_str(), strdata.size()); std::cout.write(strdata.c_str(), strdata.size()); this->HaveOut = true; } else if (p == cmsysProcess_Pipe_STDERR) { processOutput.DecodeText(data, length, strdata, 2); ferr.write(strdata.c_str(), strdata.size()); std::cerr.write(strdata.c_str(), strdata.size()); this->HaveErr = true; } } processOutput.DecodeText(std::string(), strdata, 1); if (!strdata.empty()) { fout.write(strdata.c_str(), strdata.size()); std::cout.write(strdata.c_str(), strdata.size()); } processOutput.DecodeText(std::string(), strdata, 2); if (!strdata.empty()) { ferr.write(strdata.c_str(), strdata.size()); std::cerr.write(strdata.c_str(), strdata.size()); } } // Wait for the real command to finish. cmsysProcess_WaitForExit(cp, nullptr); this->ExitCode = cmsysProcess_GetExitValue(cp); } int cmCTestLaunch::Run() { if (!this->Process) { std::cerr << "Could not allocate cmsysProcess instance!\n"; return -1; } this->RunChild(); if (this->CheckResults()) { return this->ExitCode; } this->LoadConfig(); this->WriteXML(); return this->ExitCode; } void cmCTestLaunch::LoadLabels() { if (this->OptionBuildDir.empty() || this->OptionTargetName.empty()) { return; } // Labels are listed in per-target files. std::string fname = this->OptionBuildDir; fname += "/CMakeFiles"; fname += "/"; fname += this->OptionTargetName; fname += ".dir/Labels.txt"; // We are interested in per-target labels for this source file. std::string source = this->OptionSource; cmSystemTools::ConvertToUnixSlashes(source); // Load the labels file. cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); if (!fin) { return; } bool inTarget = true; bool inSource = false; std::string line; while (cmSystemTools::GetLineFromStream(fin, line)) { if (line.empty() || line[0] == '#') { // Ignore blank and comment lines. continue; } if (line[0] == ' ') { // Label lines appear indented by one space. if (inTarget || inSource) { this->Labels.insert(line.substr(1)); } } else if (!this->OptionSource.empty() && !inSource) { // Non-indented lines specify a source file name. The first one // is the end of the target-wide labels. Use labels following a // matching source. inTarget = false; inSource = this->SourceMatches(line, source); } else { return; } } } bool cmCTestLaunch::SourceMatches(std::string const& lhs, std::string const& rhs) { // TODO: Case sensitivity, UseRelativePaths, etc. Note that both // paths in the comparison get generated by CMake. This is done for // every source in the target, so it should be efficient (cannot use // cmSystemTools::IsSameFile). return lhs == rhs; } bool cmCTestLaunch::IsError() const { return this->ExitCode != 0; } void cmCTestLaunch::WriteXML() { // Name the xml file. std::string logXML = this->LogDir; logXML += this->IsError() ? "error-" : "warning-"; logXML += this->LogHash; logXML += ".xml"; // Use cmGeneratedFileStream to atomically create the report file. cmGeneratedFileStream fxml(logXML); cmXMLWriter xml(fxml, 2); cmXMLElement e2(xml, "Failure"); e2.Attribute("type", this->IsError() ? "Error" : "Warning"); this->WriteXMLAction(e2); this->WriteXMLCommand(e2); this->WriteXMLResult(e2); this->WriteXMLLabels(e2); } void cmCTestLaunch::WriteXMLAction(cmXMLElement& e2) { e2.Comment("Meta-information about the build action"); cmXMLElement e3(e2, "Action"); // TargetName if (!this->OptionTargetName.empty()) { e3.Element("TargetName", this->OptionTargetName); } // Language if (!this->OptionLanguage.empty()) { e3.Element("Language", this->OptionLanguage); } // SourceFile if (!this->OptionSource.empty()) { std::string source = this->OptionSource; cmSystemTools::ConvertToUnixSlashes(source); // If file is in source tree use its relative location. if (cmSystemTools::FileIsFullPath(this->SourceDir) && cmSystemTools::FileIsFullPath(source) && cmSystemTools::IsSubDirectory(source, this->SourceDir)) { source = cmSystemTools::RelativePath(this->SourceDir, source); } e3.Element("SourceFile", source); } // OutputFile if (!this->OptionOutput.empty()) { e3.Element("OutputFile", this->OptionOutput); } // OutputType const char* outputType = nullptr; if (!this->OptionTargetType.empty()) { if (this->OptionTargetType == "EXECUTABLE") { outputType = "executable"; } else if (this->OptionTargetType == "SHARED_LIBRARY") { outputType = "shared library"; } else if (this->OptionTargetType == "MODULE_LIBRARY") { outputType = "module library"; } else if (this->OptionTargetType == "STATIC_LIBRARY") { outputType = "static library"; } } else if (!this->OptionSource.empty()) { outputType = "object file"; } if (outputType) { e3.Element("OutputType", outputType); } } void cmCTestLaunch::WriteXMLCommand(cmXMLElement& e2) { e2.Comment("Details of command"); cmXMLElement e3(e2, "Command"); if (!this->CWD.empty()) { e3.Element("WorkingDirectory", this->CWD); } for (std::string const& realArg : this->RealArgs) { e3.Element("Argument", realArg); } } void cmCTestLaunch::WriteXMLResult(cmXMLElement& e2) { e2.Comment("Result of command"); cmXMLElement e3(e2, "Result"); // StdOut this->DumpFileToXML(e3, "StdOut", this->LogOut); // StdErr this->DumpFileToXML(e3, "StdErr", this->LogErr); // ExitCondition cmXMLElement e4(e3, "ExitCondition"); cmsysProcess* cp = this->Process; switch (cmsysProcess_GetState(cp)) { case cmsysProcess_State_Starting: e4.Content("No process has been executed"); break; case cmsysProcess_State_Executing: e4.Content("The process is still executing"); break; case cmsysProcess_State_Disowned: e4.Content("Disowned"); break; case cmsysProcess_State_Killed: e4.Content("Killed by parent"); break; case cmsysProcess_State_Expired: e4.Content("Killed when timeout expired"); break; case cmsysProcess_State_Exited: e4.Content(this->ExitCode); break; case cmsysProcess_State_Exception: e4.Content("Terminated abnormally: "); e4.Content(cmsysProcess_GetExceptionString(cp)); break; case cmsysProcess_State_Error: e4.Content("Error administrating child process: "); e4.Content(cmsysProcess_GetErrorString(cp)); break; } } void cmCTestLaunch::WriteXMLLabels(cmXMLElement& e2) { this->LoadLabels(); if (!this->Labels.empty()) { e2.Comment("Interested parties"); cmXMLElement e3(e2, "Labels"); for (std::string const& label : this->Labels) { e3.Element("Label", label); } } } void cmCTestLaunch::DumpFileToXML(cmXMLElement& e3, const char* tag, std::string const& fname) { cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); std::string line; const char* sep = ""; cmXMLElement e4(e3, tag); while (cmSystemTools::GetLineFromStream(fin, line)) { if (MatchesFilterPrefix(line)) { continue; } if (this->Match(line, this->RegexWarningSuppress)) { line = cmStrCat("[CTest: warning suppressed] ", line); } else if (this->Match(line, this->RegexWarning)) { line = cmStrCat("[CTest: warning matched] ", line); } e4.Content(sep); e4.Content(line); sep = "\n"; } } bool cmCTestLaunch::CheckResults() { // Skip XML in passthru mode. if (this->Passthru) { return true; } // We always report failure for error conditions. if (this->IsError()) { return false; } // Scrape the output logs to look for warnings. if ((this->HaveErr && this->ScrapeLog(this->LogErr)) || (this->HaveOut && this->ScrapeLog(this->LogOut))) { return false; } return true; } void cmCTestLaunch::LoadScrapeRules() { if (this->ScrapeRulesLoaded) { return; } this->ScrapeRulesLoaded = true; // Common compiler warning formats. These are much simpler than the // full log-scraping expressions because we do not need to extract // file and line information. this->RegexWarning.emplace_back("(^|[ :])[Ww][Aa][Rr][Nn][Ii][Nn][Gg]"); this->RegexWarning.emplace_back("(^|[ :])[Rr][Ee][Mm][Aa][Rr][Kk]"); this->RegexWarning.emplace_back("(^|[ :])[Nn][Oo][Tt][Ee]"); // Load custom match rules given to us by CTest. this->LoadScrapeRules("Warning", this->RegexWarning); this->LoadScrapeRules("WarningSuppress", this->RegexWarningSuppress); } void cmCTestLaunch::LoadScrapeRules( const char* purpose, std::vector& regexps) { std::string fname = this->LogDir; fname += "Custom"; fname += purpose; fname += ".txt"; cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); std::string line; cmsys::RegularExpression rex; while (cmSystemTools::GetLineFromStream(fin, line)) { if (rex.compile(line)) { regexps.push_back(rex); } } } bool cmCTestLaunch::ScrapeLog(std::string const& fname) { this->LoadScrapeRules(); // Look for log file lines matching warning expressions but not // suppression expressions. cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); std::string line; while (cmSystemTools::GetLineFromStream(fin, line)) { if (MatchesFilterPrefix(line)) { continue; } if (this->Match(line, this->RegexWarning) && !this->Match(line, this->RegexWarningSuppress)) { return true; } } return false; } bool cmCTestLaunch::Match(std::string const& line, std::vector& regexps) { for (cmsys::RegularExpression& r : regexps) { if (r.find(line)) { return true; } } return false; } bool cmCTestLaunch::MatchesFilterPrefix(std::string const& line) const { return !this->OptionFilterPrefix.empty() && cmHasPrefix(line, this->OptionFilterPrefix); } int cmCTestLaunch::Main(int argc, const char* const argv[]) { if (argc == 2) { std::cerr << "ctest --launch: this mode is for internal CTest use only" << std::endl; return 1; } cmCTestLaunch self(argc, argv); return self.Run(); } void cmCTestLaunch::LoadConfig() { cmake cm(cmake::RoleScript, cmState::CTest); cm.SetHomeDirectory(""); cm.SetHomeOutputDirectory(""); cm.GetCurrentSnapshot().SetDefaultDefinitions(); cmGlobalGenerator gg(&cm); cmMakefile mf(&gg, cm.GetCurrentSnapshot()); std::string fname = this->LogDir; fname += "CTestLaunchConfig.cmake"; if (cmSystemTools::FileExists(fname) && mf.ReadListFile(fname)) { this->SourceDir = mf.GetSafeDefinition("CTEST_SOURCE_DIRECTORY"); cmSystemTools::ConvertToUnixSlashes(this->SourceDir); } }