cmake: Add SARIF diagnostics output support

Closes: #26587
This commit is contained in:
Daniel Tierney
2025-01-23 13:34:16 -05:00
committed by Brad King
parent a2267f337a
commit abbe41578d
37 changed files with 1085 additions and 0 deletions

View File

@@ -1180,6 +1180,7 @@ syn keyword cmakeVariable contained
\ CMAKE_EXE_LINKER_FLAGS
\ CMAKE_EXE_LINKER_FLAGS_INIT
\ CMAKE_EXPORT_COMPILE_COMMANDS
\ CMAKE_EXPORT_SARIF
\ CMAKE_EXPORT_NO_PACKAGE_REGISTRY
\ CMAKE_EXPORT_PACKAGE_REGISTRY
\ CMAKE_EXTRA_GENERATOR

View File

@@ -203,6 +203,7 @@ Variables that Change Behavior
/variable/CMAKE_EXECUTE_PROCESS_COMMAND_ERROR_IS_FATAL
/variable/CMAKE_EXPORT_BUILD_DATABASE
/variable/CMAKE_EXPORT_COMPILE_COMMANDS
/variable/CMAKE_EXPORT_SARIF
/variable/CMAKE_EXPORT_PACKAGE_REGISTRY
/variable/CMAKE_EXPORT_NO_PACKAGE_REGISTRY
/variable/CMAKE_FIND_APPBUNDLE

View File

@@ -307,6 +307,16 @@ Options
When this command line option is given, :variable:`CMAKE_MESSAGE_CONTEXT_SHOW`
is ignored.
.. option:: --sarif-output=<path>
.. versionadded:: 4.0
Enable logging of diagnostic messages produced by CMake in the SARIF format.
Write diagnostic messages to a SARIF file at the path specified. Projects can
also set :variable:`CMAKE_EXPORT_SARIF` to ``ON`` to enable this feature for a
build tree.
.. option:: --debug-trycompile
Do not delete the files and directories created for

View File

@@ -0,0 +1,81 @@
CMAKE_EXPORT_SARIF
------------------
.. versionadded:: 4.0
Enable or disable CMake diagnostics output in SARIF format for a project.
If enabled, CMake will generate a SARIF log file containing diagnostic messages
output by CMake when running in a project. By default, the log file is written
to `.cmake/sarif/cmake.sarif`, but the location can be changed by setting the
command-line option :option:`cmake --sarif-output` to the desired path.
The Static Analysis Results Interchange Format (SARIF) is a JSON-based standard
format for static analysis tools (including build tools like CMake) to record
and communicate diagnostic messages. CMake generates a SARIF log entry for
warnings and errors produced while running CMake on a project (e.g.
:command:`message` calls). Each log entry includes the message, severity, and
location information if available.
An example of CMake's SARIF output is:
.. code-block:: json
{
"version" : "2.1.0",
"$schema" : "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
"runs" :
[
{
"tool" :
{
"driver" :
{
"name" : "CMake",
"rules" :
[
{
"id" : "CMake.Warning",
"messageStrings" :
{
"default" :
{
"text" : "CMake Warning: {0}"
}
},
"name" : "CMake Warning"
}
]
}
},
"results" :
[
{
"level" : "warning",
"locations" :
[
{
"physicalLocation" :
{
"artifactLocation" :
{
"uri" : "/home/user/development/project/CMakeLists.txt"
},
"region" :
{
"startLine" : 5
}
}
}
],
"message" :
{
"text" : "An example warning"
},
"ruleId" : "CMake.Warning",
"ruleIndex" : 0
}
]
}
]
}

View File

@@ -446,6 +446,8 @@ add_library(
cmRST.h
cmRuntimeDependencyArchive.cxx
cmRuntimeDependencyArchive.h
cmSarifLog.cxx
cmSarifLog.h
cmScriptGenerator.h
cmScriptGenerator.cxx
cmSourceFile.cxx

View File

@@ -10,6 +10,8 @@
#if !defined(CMAKE_BOOTSTRAP)
# include "cmsys/SystemInformation.hxx"
# include "cmSarifLog.h"
#endif
#include <sstream>
@@ -218,6 +220,11 @@ void cmMessenger::DisplayMessage(MessageType t, std::string const& text,
displayMessage(t, msg);
#ifndef CMAKE_BOOTSTRAP
// Add message to SARIF logs
this->SarifLog.LogMessage(t, text, backtrace);
#endif
#ifdef CMake_ENABLE_DEBUGGER
if (DebuggerAdapter) {
DebuggerAdapter->OnMessageOutput(t, msg.str());

View File

@@ -13,6 +13,10 @@
#include "cmListFileCache.h"
#include "cmMessageType.h" // IWYU pragma: keep
#ifndef CMAKE_BOOTSTRAP
# include "cmSarifLog.h"
#endif
#ifdef CMake_ENABLE_DEBUGGER
namespace cmDebugger {
class cmDebuggerAdapter;
@@ -59,6 +63,10 @@ public:
return this->DeprecatedWarningsAsErrors;
}
#ifndef CMAKE_BOOTSTRAP
cmSarif::ResultsLog const& GetSarifResultsLog() const { return SarifLog; }
#endif
// Print the top of a backtrace.
void PrintBacktraceTitle(std::ostream& out,
cmListFileBacktrace const& bt) const;
@@ -76,6 +84,10 @@ private:
cm::optional<std::string> TopSource;
#ifndef CMAKE_BOOTSTRAP
cmSarif::ResultsLog SarifLog;
#endif
bool SuppressDevWarnings = false;
bool SuppressDeprecatedWarnings = false;
bool DevWarningsAsErrors = false;

383
Source/cmSarifLog.cxx Normal file
View File

@@ -0,0 +1,383 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmSarifLog.h"
#include <memory>
#include <stdexcept>
#include <cm/filesystem>
#include <cm3p/json/value.h>
#include <cm3p/json/writer.h>
#include "cmsys/FStream.hxx"
#include "cmListFileCache.h"
#include "cmMessageType.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmValue.h"
#include "cmVersionConfig.h"
#include "cmake.h"
cmSarif::ResultsLog::ResultsLog()
{
// Add the known CMake rules
this->KnownRules.emplace(RuleBuilder("CMake.AuthorWarning")
.Name("CMake Warning (dev)")
.DefaultMessage("CMake Warning (dev): {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.Warning")
.Name("CMake Warning")
.DefaultMessage("CMake Warning: {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.DeprecationWarning")
.Name("CMake Deprecation Warning")
.DefaultMessage("CMake Deprecation Warning: {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.AuthorError")
.Name("CMake Error (dev)")
.DefaultMessage("CMake Error (dev): {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.FatalError")
.Name("CMake Error")
.DefaultMessage("CMake Error: {0}")
.Build());
this->KnownRules.emplace(
RuleBuilder("CMake.InternalError")
.Name("CMake Internal Error")
.DefaultMessage("CMake Internal Error (please report a bug): {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.DeprecationError")
.Name("CMake Deprecation Error")
.DefaultMessage("CMake Deprecation Error: {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.Message")
.Name("CMake Message")
.DefaultMessage("CMake Message: {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.Log")
.Name("CMake Log")
.DefaultMessage("CMake Log: {0}")
.Build());
}
void cmSarif::ResultsLog::Log(cmSarif::Result&& result) const
{
// The rule ID is optional, but if it is present, enable metadata output for
// the rule by marking it as used
if (result.RuleId) {
std::size_t index = this->UseRule(*result.RuleId);
result.RuleIndex = index;
}
// Add the result to the log
this->Results.emplace_back(result);
}
void cmSarif::ResultsLog::LogMessage(
MessageType t, std::string const& text,
cmListFileBacktrace const& backtrace) const
{
// Add metadata to the result object
// The CMake SARIF rules for messages all expect 1 string argument with the
// message text
Json::Value additionalProperties(Json::objectValue);
Json::Value args(Json::arrayValue);
args.append(text);
additionalProperties["message"]["id"] = "default";
additionalProperties["message"]["arguments"] = args;
// Create and log a result object
// Rule indices are assigned when writing the final JSON output. Right now,
// leave it as nullopt. The other optional fields are filled if available
this->Log(cmSarif::Result{
text, cmSarif::SourceFileLocation::FromBacktrace(backtrace),
cmSarif::MessageSeverityLevel(t), cmSarif::MessageRuleId(t), cm::nullopt,
additionalProperties });
}
std::size_t cmSarif::ResultsLog::UseRule(std::string const& id) const
{
// Check if the rule is already in the index
auto it = this->RuleToIndex.find(id);
if (it != this->RuleToIndex.end()) {
// The rule is already in use. Return the known index
return it->second;
}
// This rule is not yet in the index, so check if it is recognized
auto itKnown = this->KnownRules.find(id);
if (itKnown == this->KnownRules.end()) {
// The rule is not known. Add an empty rule to the known rules so that it
// is included in the output
this->KnownRules.emplace(RuleBuilder(id.c_str()).Build());
}
// Since this is the first time the rule is used, enable it and add it to the
// index
std::size_t idx = this->EnabledRules.size();
this->RuleToIndex[id] = idx;
this->EnabledRules.emplace_back(id);
return idx;
}
cmSarif::ResultSeverityLevel cmSarif::MessageSeverityLevel(MessageType t)
{
switch (t) {
case MessageType::AUTHOR_WARNING:
case MessageType::WARNING:
case MessageType::DEPRECATION_WARNING:
return ResultSeverityLevel::SARIF_WARNING;
case MessageType::AUTHOR_ERROR:
case MessageType::FATAL_ERROR:
case MessageType::INTERNAL_ERROR:
case MessageType::DEPRECATION_ERROR:
return ResultSeverityLevel::SARIF_ERROR;
case MessageType::MESSAGE:
case MessageType::LOG:
return ResultSeverityLevel::SARIF_NOTE;
default:
return ResultSeverityLevel::SARIF_NONE;
}
}
cm::optional<std::string> cmSarif::MessageRuleId(MessageType t)
{
switch (t) {
case MessageType::AUTHOR_WARNING:
return "CMake.AuthorWarning";
case MessageType::WARNING:
return "CMake.Warning";
case MessageType::DEPRECATION_WARNING:
return "CMake.DeprecationWarning";
case MessageType::AUTHOR_ERROR:
return "CMake.AuthorError";
case MessageType::FATAL_ERROR:
return "CMake.FatalError";
case MessageType::INTERNAL_ERROR:
return "CMake.InternalError";
case MessageType::DEPRECATION_ERROR:
return "CMake.DeprecationError";
case MessageType::MESSAGE:
return "CMake.Message";
case MessageType::LOG:
return "CMake.Log";
default:
return cm::nullopt;
}
}
Json::Value cmSarif::Rule::GetJson() const
{
Json::Value rule(Json::objectValue);
rule["id"] = this->Id;
if (this->Name) {
rule["name"] = *this->Name;
}
if (this->FullDescription) {
rule["fullDescription"]["text"] = *this->FullDescription;
}
if (this->DefaultMessage) {
rule["messageStrings"]["default"]["text"] = *this->DefaultMessage;
}
return rule;
}
cmSarif::SourceFileLocation::SourceFileLocation(
cmListFileBacktrace const& backtrace)
{
if (backtrace.Empty()) {
throw std::runtime_error("Empty source file location");
}
cmListFileContext const& lfc = backtrace.Top();
this->Uri = lfc.FilePath;
this->Line = lfc.Line;
}
cm::optional<cmSarif::SourceFileLocation>
cmSarif::SourceFileLocation::FromBacktrace(
cmListFileBacktrace const& backtrace)
{
if (backtrace.Empty()) {
return cm::nullopt;
}
cmListFileContext const& lfc = backtrace.Top();
if (lfc.Line <= 0 || lfc.FilePath.empty()) {
return cm::nullopt;
}
return cm::make_optional<cmSarif::SourceFileLocation>(backtrace);
}
void cmSarif::ResultsLog::WriteJson(Json::Value& root) const
{
// Add SARIF metadata
root["version"] = "2.1.0";
root["$schema"] = "https://schemastore.azurewebsites.net/schemas/json/"
"sarif-2.1.0-rtm.4.json";
// JSON object for the SARIF runs array
Json::Value runs(Json::arrayValue);
// JSON object for the current (only) run
Json::Value currentRun(Json::objectValue);
// Accumulate info about the reported rules
Json::Value jsonRules(Json::arrayValue);
for (auto const& ruleId : this->EnabledRules) {
jsonRules.append(KnownRules.at(ruleId).GetJson());
}
// Add info the driver for the current run (CMake)
Json::Value driverTool(Json::objectValue);
driverTool["name"] = "CMake";
driverTool["version"] = CMake_VERSION;
driverTool["rules"] = jsonRules;
currentRun["tool"]["driver"] = driverTool;
runs.append(currentRun);
// Add all results
Json::Value jsonResults(Json::arrayValue);
for (auto const& res : this->Results) {
Json::Value jsonResult(Json::objectValue);
if (res.Message) {
jsonResult["message"]["text"] = *(res.Message);
}
// If the result has a level, add it to the result
if (res.Level) {
switch (*res.Level) {
case ResultSeverityLevel::SARIF_WARNING:
jsonResult["level"] = "warning";
break;
case ResultSeverityLevel::SARIF_ERROR:
jsonResult["level"] = "error";
break;
case ResultSeverityLevel::SARIF_NOTE:
jsonResult["level"] = "note";
break;
case ResultSeverityLevel::SARIF_NONE:
jsonResult["level"] = "none";
break;
}
}
// If the result has a rule ID or index, add it to the result
if (res.RuleId) {
jsonResult["ruleId"] = *res.RuleId;
}
if (res.RuleIndex) {
jsonResult["ruleIndex"] = Json::UInt64(*res.RuleIndex);
}
if (res.Location) {
jsonResult["locations"][0]["physicalLocation"]["artifactLocation"]
["uri"] = (res.Location)->Uri;
jsonResult["locations"][0]["physicalLocation"]["region"]["startLine"] =
Json::Int64((res.Location)->Line);
}
jsonResults.append(jsonResult);
}
currentRun["results"] = jsonResults;
runs[0] = currentRun;
root["runs"] = runs;
}
cmSarif::LogFileWriter::~LogFileWriter()
{
// If the file has not been written yet, try to finalize it
if (!this->FileWritten) {
// Try to write and check the result
if (this->TryWrite() == WriteResult::FAILURE) {
// If the result is `FAILURE`, it means the write condition is true but
// the file still wasn't written. This is an error.
cmSystemTools::Error("Failed to write SARIF log to " +
this->FilePath.generic_string());
}
}
}
bool cmSarif::LogFileWriter::EnsureFileValid()
{
// First, ensure directory exists
cm::filesystem::path dir = this->FilePath.parent_path();
if (!cmSystemTools::FileIsDirectory(dir.generic_string())) {
if (!this->CreateDirectories ||
!cmSystemTools::MakeDirectory(dir.generic_string()).IsSuccess()) {
return false;
}
}
// Open the file for writing
cmsys::ofstream outputFile(this->FilePath.generic_string().c_str());
if (!outputFile.good()) {
return false;
}
return true;
}
cmSarif::LogFileWriter::WriteResult cmSarif::LogFileWriter::TryWrite()
{
// Check that SARIF logging is enabled
if (!this->WriteCondition || !this->WriteCondition()) {
return WriteResult::SKIPPED;
}
// Open the file
if (!this->EnsureFileValid()) {
return WriteResult::FAILURE;
}
cmsys::ofstream outputFile(this->FilePath.generic_string().c_str());
// The file is available, so proceed to write the log
// Assemble the SARIF JSON from the results in the log
Json::Value root(Json::objectValue);
this->Log.WriteJson(root);
// Serialize the JSON to the file
Json::StreamWriterBuilder builder;
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &outputFile);
outputFile.close();
this->FileWritten = true;
return WriteResult::SUCCESS;
}
bool cmSarif::LogFileWriter::ConfigureForCMakeRun(cmake& cm)
{
// If an explicit SARIF output path has been provided, set and check it
cm::optional<std::string> sarifFilePath = cm.GetSarifFilePath();
if (sarifFilePath) {
this->SetPath(cm::filesystem::path(*sarifFilePath));
if (!this->EnsureFileValid()) {
cmSystemTools::Error(
cmStrCat("Invalid SARIF output file path: ", *sarifFilePath));
return false;
}
}
// The write condition is checked immediately before writing the file, which
// allows projects to enable SARIF diagnostics by setting a cache variable
// and have it take effect for the current run.
this->SetWriteCondition([&cm]() {
// The command-line option can be used to set an explicit path, but in
// normal mode, the project variable `CMAKE_EXPORT_SARIF` can also enable
// SARIF logging.
return cm.GetSarifFilePath().has_value() ||
(cm.GetWorkingMode() == cmake::NORMAL_MODE &&
cm.GetCacheDefinition(cmSarif::PROJECT_SARIF_FILE_VARIABLE).IsOn());
});
return true;
}

287
Source/cmSarifLog.h Normal file
View File

@@ -0,0 +1,287 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#pragma once
#include <cstddef>
#include <functional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <cm/filesystem>
#include <cm/optional>
#include <cm3p/json/value.h>
class cmake;
class cmListFileBacktrace;
enum class MessageType;
/// @brief CMake support for SARIF logging
namespace cmSarif {
constexpr char const* PROJECT_SARIF_FILE_VARIABLE = "CMAKE_EXPORT_SARIF";
constexpr char const* PROJECT_DEFAULT_SARIF_FILE = ".cmake/sarif/cmake.sarif";
/// @brief The severity level of a result in SARIF
///
/// The SARIF specification section 3.27.10 defines four levels of severity
/// for results.
enum class ResultSeverityLevel
{
SARIF_WARNING,
SARIF_ERROR,
SARIF_NOTE,
SARIF_NONE,
};
/// @brief A location in a source file logged with a SARIF result
struct SourceFileLocation
{
std::string Uri;
long Line = 0;
/// @brief Construct a SourceFileLocation at the top of the call stack
SourceFileLocation(cmListFileBacktrace const& backtrace);
/// @brief Get the SourceFileLocation from the top of a call stack, if any
/// @return The location or nullopt if the call stack is empty or is missing
/// location information
static cm::optional<SourceFileLocation> FromBacktrace(
cmListFileBacktrace const& backtrace);
};
/// @brief A result defined by SARIF reported by a CMake run
///
/// This is the data model for results in a SARIF log. Typically, a result only
/// requires either a message or a rule index. The most common properties are
/// named in this struct, but arbitrary metadata can be added to the result
/// using the additionalProperties field.
struct Result
{
/// @brief The message text of the result (required if no rule index)
cm::optional<std::string> Message;
/// @brief The location of the result (optional)
cm::optional<cmSarif::SourceFileLocation> Location;
/// @brief The severity level of the result (optional)
cm::optional<cmSarif::ResultSeverityLevel> Level;
/// @brief The rule ID of the result (optional)
cm::optional<std::string> RuleId;
/// @brief The index of the rule in the log's rule array (optional)
cm::optional<std::size_t> RuleIndex;
/// @brief Additional JSON properties for the result (optional)
///
/// The additional properties should be merged into the result object when it
/// is written to the SARIF log.
Json::Value AdditionalProperties;
};
/// @brief A SARIF reporting rule
///
/// A rule in SARIF is described by a reportingDescriptor object (SARIF
/// specification section 3.49). The only property required for a rule is the
/// ID property. The ID is normally an opaque string that identifies a rule
/// applicable to a class of results. The other included properties are
/// optional but recommended for rules reported by CMake.
struct Rule
{
/// @brief The ID of the rule. Required by SARIF
std::string Id;
/// @brief The end-user name of the rule (optional)
cm::optional<std::string> Name;
/// @brief The extended description of the rule (optional)
cm::optional<std::string> FullDescription;
/// @brief The default message for the rule (optional)
cm::optional<std::string> DefaultMessage;
/// @brief Get the JSON representation of this rule
Json::Value GetJson() const;
};
/// @brief A builder for SARIF rules
///
/// `Rule` is a data model for SARIF rules. Known rules are usually initialized
/// manually by field. Using a builder makes initialization more readable and
/// prevents issues with reordering and optional fields.
class RuleBuilder
{
public:
/// @brief Construct a new rule builder for a rule with the given ID
RuleBuilder(char const* id) { this->NewRule.Id = id; }
/// @brief Set the name of the rule
RuleBuilder& Name(std::string name)
{
this->NewRule.Name = std::move(name);
return *this;
}
/// @brief Set the full description of the rule
RuleBuilder& FullDescription(std::string fullDescription)
{
this->NewRule.FullDescription = std::move(fullDescription);
return *this;
}
/// @brief Set the default message for the rule
RuleBuilder& DefaultMessage(std::string defaultMessage)
{
this->NewRule.DefaultMessage = std::move(defaultMessage);
return *this;
}
/// @brief Build the rule
std::pair<std::string, Rule> Build() const
{
return std::make_pair(this->NewRule.Id, this->NewRule);
}
private:
Rule NewRule;
};
/// @brief Get the SARIF severity level of a CMake message type
ResultSeverityLevel MessageSeverityLevel(MessageType t);
/// @brief Get the SARIF rule ID of a CMake message type
/// @return The rule ID or nullopt if the message type is unrecognized
///
/// The rule ID is a string assigned to SARIF results to identify the category
/// of the result. CMake maps messages to rules based on the message type.
/// CMake's rules are of the form "CMake.<MessageType>".
cm::optional<std::string> MessageRuleId(MessageType t);
/// @brief A log for reporting results in the SARIF format
class ResultsLog
{
public:
ResultsLog();
/// @brief Log a result of this run to the SARIF output
void Log(cmSarif::Result&& result) const;
/// @brief Log a result from a CMake message with a source file location
/// @param t The type of the message, which corresponds to the level and rule
/// of the result
/// @param text The contents of the message
/// @param backtrace The call stack where the message originated (may be
/// empty)
void LogMessage(MessageType t, std::string const& text,
cmListFileBacktrace const& backtrace) const;
/// @brief Write this SARIF log to an empty JSON object
/// @param[out] root The JSON object to write to
void WriteJson(Json::Value& root) const;
private:
// Private methods
// Log that a rule was used and should be included in the output. Returns the
// index of the rule in the log
std::size_t UseRule(std::string const& id) const;
// Private data
// All data is mutable since log results are often added in const methods
// All results added chronologically
mutable std::vector<cmSarif::Result> Results;
// Mapping of rule IDs to rule indices in the log.
// In SARIF, rule metadata is typically only included if the rule is
// referenced. The indices are unique to one log output and and vary
// depending on when the rule was first encountered.
mutable std::unordered_map<std::string, std::size_t> RuleToIndex;
// Rules that will be added to the log in order of appearance
mutable std::vector<std::string> EnabledRules;
// All known rules that could be included in a log
mutable std::unordered_map<std::string, Rule> KnownRules;
};
/// @brief Writes contents of a `cmSarif::ResultsLog` to a file
///
/// The log file writer is a helper class that writes the contents of a
/// `cmSarif::ResultsLog` upon destruction if a condition (e.g. project
/// variable is enabled) is met.
class LogFileWriter
{
public:
/// @brief Create a new, disabled log file writer
///
/// The returned writer will not write anything until the path generator
/// and write condition are set. If the log has not been written when the
/// object is being destroyed, the destructor will write the log if the
/// condition is met and a valid path is available.
LogFileWriter(ResultsLog const& log)
: Log(log)
{
}
/// @brief Configure a log file writer for a CMake run
///
/// CMake should write a SARIF log if the project variable
/// `CMAKE_EXPORT_SARIF` is `ON` or if the `--sarif-output=<path>` command
/// line option is set. The writer will be configured to respond to these
/// conditions.
///
/// This does not configure a default path, so one must be set once it is
/// known that we're in normal mode if none was explicitly provided.
bool ConfigureForCMakeRun(cmake& cm);
~LogFileWriter();
/// @brief Check if a valid path is set by opening the output file
/// @return True if the file can be opened for writing
bool EnsureFileValid();
/// @brief The possible outcomes of trying to write the log file
enum class WriteResult
{
SUCCESS, ///< File written with no issues
FAILURE, ///< Error encountered while writing the file
SKIPPED, ///< Writing was skipped due to false write condition
};
/// @brief Try to write the log file and return `true` if it was written
///
/// Check the write condition and path generator to determine if the log
/// file should be written.
WriteResult TryWrite();
/// @brief Set a lambda to check if the log file should be written
void SetWriteCondition(std::function<bool()> const& checkConditionCallback)
{
this->WriteCondition = checkConditionCallback;
}
/// @brief Set the output file path, optionally creating parent directories
///
/// The settings will apply when the log file is written. If the output
/// file should be checked earlier, use `CheckFileValidity`.
void SetPath(cm::filesystem::path const& path,
bool createParentDirectories = false)
{
this->FilePath = path;
this->CreateDirectories = createParentDirectories;
}
private:
ResultsLog const& Log;
std::function<bool()> WriteCondition;
cm::filesystem::path FilePath;
bool CreateDirectories = false;
bool FileWritten = false;
};
} // namespace cmSarif

View File

@@ -14,6 +14,7 @@
#include <stdexcept>
#include <utility>
#include <cm/filesystem>
#include <cm/memory>
#include <cm/optional>
#include <cm/string_view>
@@ -61,6 +62,9 @@
#include "cmJSONState.h"
#include "cmList.h"
#include "cmMessenger.h"
#ifndef CMAKE_BOOTSTRAP
# include "cmSarifLog.h"
#endif
#include "cmState.h"
#include "cmStateDirectory.h"
#include "cmStringAlgorithms.h"
@@ -1272,6 +1276,16 @@ void cmake::SetArgs(std::vector<std::string> const& args)
state->SetIgnoreLinkWarningAsError(true);
return true;
} },
#ifndef CMAKE_BOOTSTRAP
CommandArgument{ "--sarif-output", "No file specified for --sarif-output",
CommandArgument::Values::One,
[](std::string const& value, cmake* state) -> bool {
state->SarifFilePath =
cmSystemTools::ToNormalizedPathOnDisk(value);
state->SarifFileOutput = true;
return true;
} },
#endif
CommandArgument{ "--debugger", CommandArgument::Values::Zero,
[](std::string const&, cmake* state) -> bool {
#ifdef CMake_ENABLE_DEBUGGER
@@ -2853,6 +2867,15 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
return 0;
}
#ifndef CMAKE_BOOTSTRAP
// Configure the SARIF log for the current run
cmSarif::LogFileWriter sarifLogFileWriter(
this->GetMessenger()->GetSarifResultsLog());
if (!sarifLogFileWriter.ConfigureForCMakeRun(*this)) {
return -1;
}
#endif
// Log the trace format version to the desired output
if (this->GetTrace()) {
this->PrintTraceFormatVersion();
@@ -2879,6 +2902,17 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
cmSystemTools::Error("Error executing cmake::LoadCache(). Aborting.\n");
return -1;
}
#ifndef CMAKE_BOOTSTRAP
// If no SARIF file has been explicitly specified, use the default path
if (!this->SarifFileOutput) {
// If no output file is specified, use the default path
// Enable parent directory creation for the default path
sarifLogFileWriter.SetPath(
cm::filesystem::path(this->GetHomeOutputDirectory()) /
std::string(cmSarif::PROJECT_DEFAULT_SARIF_FILE),
true);
}
#endif
} else {
if (this->FreshCache) {
cmSystemTools::Error("--fresh allowed only when configuring a project");
@@ -2909,6 +2943,11 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
return this->HasScriptModeExitCode() ? this->GetScriptModeExitCode() : 0;
}
#ifndef CMAKE_BOOTSTRAP
// CMake only responds to the SARIF variable in normal mode
this->MarkCliAsUsed(cmSarif::PROJECT_SARIF_FILE_VARIABLE);
#endif
// If MAKEFLAGS are given in the environment, remove the environment
// variable. This will prevent try-compile from succeeding when it
// should fail (if "-i" is an option). We cannot simply test

View File

@@ -29,6 +29,8 @@
#include "cmValue.h"
#if !defined(CMAKE_BOOTSTRAP)
# include <type_traits>
# include <cm/optional>
# include <cm3p/json/value.h>
@@ -575,6 +577,15 @@ public:
cmMessenger* GetMessenger() const { return this->Messenger.get(); }
#ifndef CMAKE_BOOTSTRAP
/// Get the SARIF file path if set manually for this run
cm::optional<std::string> GetSarifFilePath() const
{
return (this->SarifFileOutput ? cm::make_optional(this->SarifFilePath)
: cm::nullopt);
}
#endif
/**
* Get the state of the suppression of developer (author) warnings.
* Returns false, by default, if developer warnings should be shown, true
@@ -811,6 +822,11 @@ private:
cmStateSnapshot CurrentSnapshot;
std::unique_ptr<cmMessenger> Messenger;
#ifndef CMAKE_BOOTSTRAP
bool SarifFileOutput = false;
std::string SarifFilePath;
#endif
std::vector<std::string> TraceOnlyThisSources;
std::set<std::string> DebugFindPkgs;

View File

@@ -650,6 +650,7 @@ add_RunCMake_test(project_injected)
add_RunCMake_test(property_init)
add_RunCMake_test(DependencyProviders)
add_RunCMake_test(return)
add_RunCMake_test(SarifOutput)
add_RunCMake_test(separate_arguments)
add_RunCMake_test(set_property)
add_RunCMake_test(string)

View File

@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.10)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)

View File

@@ -0,0 +1,4 @@
# By default, no SARIF file should be generated
if (EXISTS "${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif")
message(FATAL_ERROR "SARIF file should not have been generated by default")
endif()

View File

@@ -0,0 +1,4 @@
^CMake Warning at DefaultSarifOutput\.cmake:1 \(message\):
Example warning message
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)$

View File

@@ -0,0 +1 @@
message(WARNING "Example warning message")

View File

@@ -0,0 +1,4 @@
include("${CMAKE_CURRENT_LIST_DIR}/check-sarif.cmake")
check_sarif_output("${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif"
"${CMAKE_CURRENT_LIST_DIR}/GenerateSarifResults-expected.sarif")

View File

@@ -0,0 +1,67 @@
{
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
"runs": [
{
"results": [
{
"level": "warning",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "PATH:<SOURCE_DIR>/GenerateSarifResults.cmake"
},
"region": {
"startLine": 2
}
}
}
],
"message": {
"text": "Example warning message"
},
"ruleId": "CMake.Warning",
"ruleIndex": 0
},
{
"level": "warning",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "PATH:<SOURCE_DIR>/GenerateSarifResults.cmake"
},
"region": {
"startLine": 5
}
}
}
],
"message": {
"text": "A second example warning message"
},
"ruleId": "CMake.Warning",
"ruleIndex": 0
}
],
"tool": {
"driver": {
"name": "CMake",
"rules": [
{
"id": "CMake.Warning",
"messageStrings": {
"default": {
"text": "CMake Warning: {0}"
}
},
"name": "CMake Warning"
}
],
"version": "<IGNORE>"
}
}
}
],
"version": "2.1.0"
}

View File

@@ -0,0 +1,9 @@
^CMake Warning at GenerateSarifResults\.cmake:2 \(message\):
Example warning message
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
+
CMake Warning at GenerateSarifResults\.cmake:5 \(message\):
A second example warning message
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)$

View File

@@ -0,0 +1,8 @@
# Write some user messages to produce SARIF results
message(WARNING "Example warning message")
# The second warning should be logged, but the rule should not be duplicated
message(WARNING "A second example warning message")
# Status message should not be logged
message(STATUS "Example status message")

View File

@@ -0,0 +1,4 @@
include("${CMAKE_CURRENT_LIST_DIR}/check-sarif.cmake")
check_sarif_output("${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif"
"${CMAKE_CURRENT_LIST_DIR}/ProjectFatalError-expected.sarif")

View File

@@ -0,0 +1,47 @@
{
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
"runs": [
{
"results": [
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "PATH:<SOURCE_DIR>/ProjectFatalError.cmake"
},
"region": {
"startLine": 1
}
}
}
],
"message": {
"text": "Example error"
},
"ruleId": "CMake.FatalError",
"ruleIndex": 0
}
],
"tool": {
"driver": {
"name": "CMake",
"rules": [
{
"id": "CMake.FatalError",
"messageStrings": {
"default": {
"text": "CMake Error: {0}"
}
},
"name": "CMake Error"
}
],
"version": "<IGNORE>"
}
}
}
],
"version": "2.1.0"
}

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,4 @@
^CMake Error at ProjectFatalError\.cmake:1 \(message\):
Example error
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)$

View File

@@ -0,0 +1 @@
message(FATAL_ERROR "Example error")

View File

@@ -0,0 +1,25 @@
include(RunCMake)
include("${CMAKE_CURRENT_LIST_DIR}/check-sarif.cmake")
# Default case: the SARIF file should not be generated
run_cmake(DefaultSarifOutput)
# Ensure the expected messages are present in the SARIF output
run_cmake_with_options(GenerateSarifResults -DCMAKE_EXPORT_SARIF=ON)
# Activate SARIF output using the `CMAKE_EXPORT_SARIF` variable
run_cmake(ToggleExportSarifVariable)
# If CMake stops with a fatal error, it should still generate a SARIF file if
# requested (and the fatal error should be in the log)
run_cmake_with_options(ProjectFatalError -DCMAKE_EXPORT_SARIF=ON)
# ScriptModeSarifVariable Test: Script mode must ignore the
# `CMAKE_EXPORT_SARIF`variable
run_cmake_script(ScriptModeSarifVariable -DCMAKE_EXPORT_SARIF=ON)
# Check that the command-line option can be used to set the file output path
run_cmake_with_options(SarifFileArgument --sarif-output=test_cmake_run.sarif)
# Test the command-line option in script mode as well
run_cmake_script(SarifFileArgumentScript --sarif-output=test_cmake_run.sarif)

View File

@@ -0,0 +1,4 @@
# Make sure the output exists
if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/test_cmake_run.sarif")
message(FATAL_ERROR "SARIF file not generated in the expected location")
endif()

View File

@@ -0,0 +1,4 @@
^CMake Warning at SarifFileArgument\.cmake:1 \(message\):
SARIF file test
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)$

View File

@@ -0,0 +1 @@
message(WARNING "SARIF file test")

View File

@@ -0,0 +1,4 @@
# Make sure the output exists
if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/test_cmake_run.sarif")
message(FATAL_ERROR "SARIF file not generated in the expected location")
endif()

View File

@@ -0,0 +1,2 @@
# This won't appear in the SARIF log, but it gives the script something to do.
message(STATUS "SARIF file test")

View File

@@ -0,0 +1,4 @@
# Script mode should ignore the SARIF project variable and export nothing
if (EXISTS "${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif")
message(FATAL_ERROR "SARIF file should not have been generated in script mode")
endif()

View File

@@ -0,0 +1,3 @@
# Try enabling SARIF output in script mode
# No file should be generated since script mode ignores the variable
set(CMAKE_EXPORT_SARIF ON CACHE BOOL "Export SARIF results" FORCE)

View File

@@ -0,0 +1,5 @@
include("${CMAKE_CURRENT_LIST_DIR}/check-sarif.cmake")
# This test should produce the same output as GenerateSarifResults
check_sarif_output("${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif"
"${CMAKE_CURRENT_LIST_DIR}/GenerateSarifResults-expected.sarif")

View File

@@ -0,0 +1,11 @@
^CMake Warning at GenerateSarifResults\.cmake:2 \(message\):
Example warning message
Call Stack \(most recent call first\):
ToggleExportSarifVariable\.cmake:[0-9]+ \(include\)
CMakeLists\.txt:[0-9]+ \(include\)
+
CMake Warning at GenerateSarifResults.cmake:5 \(message\):
A second example warning message
Call Stack \(most recent call first\):
ToggleExportSarifVariable\.cmake:[0-9]+ \(include\)
CMakeLists\.txt:[0-9]+ \(include\)$

View File

@@ -0,0 +1,6 @@
# Generate potential SARIF results
include("${CMAKE_CURRENT_LIST_DIR}/GenerateSarifResults.cmake")
# Enable SARIF logging at the end for the most behavior coverage
# All results should be captured regardless of when enabled
set(CMAKE_EXPORT_SARIF ON CACHE BOOL "Export SARIF results" FORCE)

View File

@@ -0,0 +1,19 @@
include("${CMAKE_CURRENT_LIST_DIR}/../CXXModules/check-json.cmake")
# Check that the SARIF results from a test match the expected results
macro(check_sarif_output sarif_output_file expected_sarif_output_file)
# Make sure the output file exists before reading it
if (NOT EXISTS "${sarif_output_file}")
message(FATAL_ERROR "SARIF output file not found: ${sarif_output_file}")
endif()
file(READ "${sarif_output_file}" actual_output)
# Make sure the expected output file exists before reading it
if (NOT EXISTS "${expected_sarif_output_file}")
message(FATAL_ERROR "Expected SARIF output file not found: ${expected_sarif_output_file}")
endif()
file(READ "${expected_sarif_output_file}" expected_output)
# Check the actual output against the expected output
check_json("${actual_output}" "${expected_output}")
endmacro()