CMake/Source/CTest/cmCTestLaunch.cxx

627 lines
17 KiB
C++
Raw Normal View History

Simplify CMake per-source license notices Per-source copyright/license notice headers that spell out copyright holder names and years are hard to maintain and often out-of-date or plain wrong. Precise contributor information is already maintained automatically by the version control tool. Ultimately it is the receiver of a file who is responsible for determining its licensing status, and per-source notices are merely a convenience. Therefore it is simpler and more accurate for each source to have a generic notice of the license name and references to more detailed information on copyright holders and full license terms. Our `Copyright.txt` file now contains a list of Contributors whose names appeared source-level copyright notices. It also references version control history for more precise information. Therefore we no longer need to spell out the list of Contributors in each source file notice. Replace CMake per-source copyright/license notice headers with a short description of the license and links to `Copyright.txt` and online information available from "https://cmake.org/licensing". The online URL also handles cases of modules being copied out of our source into other projects, so we can drop our notices about replacing links with full license text. Run the `Utilities/Scripts/filter-notices.bash` script to perform the majority of the replacements mechanically. Manually fix up shebang lines and trailing newlines in a few files. Manually update the notices in a few files that the script does not handle.
2016-09-27 22:01:08 +03:00
/* 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 <cmConfigure.h>
#include "cmGeneratedFileStream.h"
#include "cmGlobalGenerator.h"
#include "cmMakefile.h"
#include "cmState.h"
#include "cmSystemTools.h"
2015-05-24 21:32:33 +03:00
#include "cmXMLWriter.h"
#include "cmake.h"
#include <cm_auto_ptr.hxx>
#include <cmsys/FStream.hxx>
#include <cmsys/MD5.h>
#include <cmsys/Process.h>
#include <cmsys/RegularExpression.hxx>
#include <iostream>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <fcntl.h> // for _O_BINARY
#include <io.h> // for _setmode
#include <stdio.h> // for std{out,err} and fileno
#endif
cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv)
{
this->Passthru = true;
2016-06-27 23:44:16 +03:00
this->Process = CM_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;
}
2016-08-18 21:04:21 +03:00
this->RealArgC = 0;
this->RealArgV = CM_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.push_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.
char hash[32];
cmsysMD5* md5 = cmsysMD5_New();
cmsysMD5_Initialize(md5);
cmsysMD5_Append(md5, (unsigned char const*)(this->CWD.c_str()), -1);
for (std::vector<std::string>::const_iterator ai = this->RealArgs.begin();
ai != this->RealArgs.end(); ++ai) {
cmsysMD5_Append(md5, (unsigned char const*)ai->c_str(), -1);
}
cmsysMD5_FinalizeHex(md5, hash);
cmsysMD5_Delete(md5);
this->LogHash.assign(hash, 32);
// 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) {
2016-06-27 23:44:16 +03:00
char* data = CM_NULLPTR;
int length = 0;
2016-06-27 23:44:16 +03:00
while (int p = cmsysProcess_WaitForData(cp, &data, &length, CM_NULLPTR)) {
if (p == cmsysProcess_Pipe_STDOUT) {
fout.write(data, length);
std::cout.write(data, length);
this->HaveOut = true;
} else if (p == cmsysProcess_Pipe_STDERR) {
ferr.write(data, length);
std::cerr.write(data, length);
this->HaveErr = true;
}
}
}
// Wait for the real command to finish.
2016-06-27 23:44:16 +03:00
cmsysProcess_WaitForExit(cp, CM_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 += cmake::GetCMakeFilesDirectory();
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;
} else if (line[0] == ' ') {
// Label lines appear indented by one space.
if (inTarget || inSource) {
this->Labels.insert(line.c_str() + 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.c_str());
2015-05-24 21:32:33 +03:00
cmXMLWriter xml(fxml, 2);
xml.StartElement("Failure");
xml.Attribute("type", this->IsError() ? "Error" : "Warning");
this->WriteXMLAction(xml);
this->WriteXMLCommand(xml);
this->WriteXMLResult(xml);
this->WriteXMLLabels(xml);
xml.EndElement(); // Failure
}
2015-05-24 21:32:33 +03:00
void cmCTestLaunch::WriteXMLAction(cmXMLWriter& xml)
{
2015-05-24 21:32:33 +03:00
xml.Comment("Meta-information about the build action");
xml.StartElement("Action");
// TargetName
if (!this->OptionTargetName.empty()) {
2015-05-24 21:32:33 +03:00
xml.Element("TargetName", this->OptionTargetName);
}
// Language
if (!this->OptionLanguage.empty()) {
2015-05-24 21:32:33 +03:00
xml.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.c_str()) &&
cmSystemTools::FileIsFullPath(source.c_str()) &&
cmSystemTools::IsSubDirectory(source, this->SourceDir)) {
source =
cmSystemTools::RelativePath(this->SourceDir.c_str(), source.c_str());
}
2015-05-24 21:32:33 +03:00
xml.Element("SourceFile", source);
}
// OutputFile
if (!this->OptionOutput.empty()) {
2015-05-24 21:32:33 +03:00
xml.Element("OutputFile", this->OptionOutput);
}
// OutputType
2016-06-27 23:44:16 +03:00
const char* outputType = CM_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) {
2015-05-24 21:32:33 +03:00
xml.Element("OutputType", outputType);
}
2015-05-24 21:32:33 +03:00
xml.EndElement(); // Action
}
2015-05-24 21:32:33 +03:00
void cmCTestLaunch::WriteXMLCommand(cmXMLWriter& xml)
{
2015-05-24 21:32:33 +03:00
xml.Comment("Details of command");
xml.StartElement("Command");
if (!this->CWD.empty()) {
2015-05-24 21:32:33 +03:00
xml.Element("WorkingDirectory", this->CWD);
}
for (std::vector<std::string>::const_iterator ai = this->RealArgs.begin();
ai != this->RealArgs.end(); ++ai) {
2015-05-24 21:32:33 +03:00
xml.Element("Argument", *ai);
}
2015-05-24 21:32:33 +03:00
xml.EndElement(); // Command
}
2015-05-24 21:32:33 +03:00
void cmCTestLaunch::WriteXMLResult(cmXMLWriter& xml)
{
2015-05-24 21:32:33 +03:00
xml.Comment("Result of command");
xml.StartElement("Result");
// StdOut
2015-05-24 21:32:33 +03:00
xml.StartElement("StdOut");
this->DumpFileToXML(xml, this->LogOut);
xml.EndElement(); // StdOut
// StdErr
2015-05-24 21:32:33 +03:00
xml.StartElement("StdErr");
this->DumpFileToXML(xml, this->LogErr);
xml.EndElement(); // StdErr
// ExitCondition
2015-05-24 21:32:33 +03:00
xml.StartElement("ExitCondition");
cmsysProcess* cp = this->Process;
switch (cmsysProcess_GetState(cp)) {
case cmsysProcess_State_Starting:
xml.Content("No process has been executed");
break;
case cmsysProcess_State_Executing:
xml.Content("The process is still executing");
break;
case cmsysProcess_State_Disowned:
xml.Content("Disowned");
break;
case cmsysProcess_State_Killed:
xml.Content("Killed by parent");
break;
case cmsysProcess_State_Expired:
xml.Content("Killed when timeout expired");
break;
case cmsysProcess_State_Exited:
xml.Content(this->ExitCode);
break;
case cmsysProcess_State_Exception:
2015-05-24 21:32:33 +03:00
xml.Content("Terminated abnormally: ");
xml.Content(cmsysProcess_GetExceptionString(cp));
break;
case cmsysProcess_State_Error:
2015-05-24 21:32:33 +03:00
xml.Content("Error administrating child process: ");
xml.Content(cmsysProcess_GetErrorString(cp));
break;
};
2015-05-24 21:32:33 +03:00
xml.EndElement(); // ExitCondition
2015-05-24 21:32:33 +03:00
xml.EndElement(); // Result
}
2015-05-24 21:32:33 +03:00
void cmCTestLaunch::WriteXMLLabels(cmXMLWriter& xml)
{
this->LoadLabels();
if (!this->Labels.empty()) {
2015-05-24 21:32:33 +03:00
xml.Comment("Interested parties");
xml.StartElement("Labels");
for (std::set<std::string>::const_iterator li = this->Labels.begin();
li != this->Labels.end(); ++li) {
2015-05-24 21:32:33 +03:00
xml.Element("Label", *li);
}
xml.EndElement(); // Labels
}
}
void cmCTestLaunch::DumpFileToXML(cmXMLWriter& xml, std::string const& fname)
{
cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
std::string line;
const char* sep = "";
while (cmSystemTools::GetLineFromStream(fin, line)) {
if (MatchesFilterPrefix(line)) {
continue;
}
2015-05-24 21:32:33 +03:00
xml.Content(sep);
xml.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.push_back("(^|[ :])[Ww][Aa][Rr][Nn][Ii][Nn][Gg]");
this->RegexWarning.push_back("(^|[ :])[Rr][Ee][Mm][Aa][Rr][Kk]");
this->RegexWarning.push_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<cmsys::RegularExpression>& 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.c_str())) {
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<cmsys::RegularExpression>& regexps)
{
for (std::vector<cmsys::RegularExpression>::iterator ri = regexps.begin();
ri != regexps.end(); ++ri) {
if (ri->find(line.c_str())) {
return true;
}
}
return false;
}
bool cmCTestLaunch::MatchesFilterPrefix(std::string const& line) const
{
return !this->OptionFilterPrefix.empty() &&
cmSystemTools::StringStartsWith(line, this->OptionFilterPrefix.c_str());
}
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;
cm.SetHomeDirectory("");
cm.SetHomeOutputDirectory("");
cm.GetCurrentSnapshot().SetDefaultDefinitions();
cmGlobalGenerator gg(&cm);
CM_AUTO_PTR<cmMakefile> mf(new cmMakefile(&gg, cm.GetCurrentSnapshot()));
std::string fname = this->LogDir;
fname += "CTestLaunchConfig.cmake";
if (cmSystemTools::FileExists(fname.c_str()) &&
mf->ReadListFile(fname.c_str())) {
this->SourceDir = mf->GetSafeDefinition("CTEST_SOURCE_DIRECTORY");
cmSystemTools::ConvertToUnixSlashes(this->SourceDir);
}
}