Files
UnrealEngine/Engine/Source/Programs/UnrealBuildTool/Platform/Windows/PVSToolChain.cs
2025-05-18 13:04:45 +08:00

987 lines
34 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Serialization;
using UnrealBuildBase;
namespace UnrealBuildTool
{
/// <summary>
/// Flags for the PVS analyzer mode
/// </summary>
public enum PVSAnalysisModeFlags : uint
{
/// <summary>
/// Check for 64-bit portability issues
/// </summary>
Check64BitPortability = 1,
/// <summary>
/// Enable general analysis
/// </summary>
GeneralAnalysis = 4,
/// <summary>
/// Check for optimizations
/// </summary>
Optimizations = 8,
/// <summary>
/// Enable customer-specific rules
/// </summary>
CustomerSpecific = 16,
/// <summary>
/// Enable MISRA analysis
/// </summary>
MISRA = 32,
}
/// <summary>
/// Flags for the PVS analyzer timeout
/// </summary>
public enum AnalysisTimeoutFlags
{
/// <summary>
/// Analysis timeout for file 10 minutes (600 seconds)
/// </summary>
After_10_minutes = 600,
/// <summary>
/// Analysis timeout for file 30 minutes (1800 seconds)
/// </summary>
After_30_minutes = 1800,
/// <summary>
/// Analysis timeout for file 60 minutes (3600 seconds)
/// </summary>
After_60_minutes = 3600,
/// <summary>
/// Analysis timeout when not set (a lot of seconds)
/// </summary>
No_timeout = 999999
}
/// <summary>
/// Partial representation of PVS-Studio main settings file
/// </summary>
[XmlRoot("ApplicationSettings")]
public class PVSApplicationSettings
{
/// <summary>
/// Masks for paths excluded for analysis
/// </summary>
public string[]? PathMasks;
/// <summary>
/// Registered username
/// </summary>
public string? UserName;
/// <summary>
/// Registered serial number
/// </summary>
public string? SerialNumber;
/// <summary>
/// Disable the 64-bit Analysis
/// </summary>
public bool Disable64BitAnalysis;
/// <summary>
/// Disable the General Analysis
/// </summary>
public bool DisableGAAnalysis;
/// <summary>
/// Disable the Optimization Analysis
/// </summary>
public bool DisableOPAnalysis;
/// <summary>
/// Disable the Customer's Specific diagnostic rules
/// </summary>
public bool DisableCSAnalysis;
/// <summary>
/// Disable the MISRA Analysis
/// </summary>
public bool DisableMISRAAnalysis;
/// <summary>
/// File analysis timeout
/// </summary>
public AnalysisTimeoutFlags AnalysisTimeout;
/// <summary>
/// Disable analyzer Level 3 (Low) messages
/// </summary>
public bool NoNoise;
/// <summary>
/// Enable the display of analyzer rules exceptions which can be specified by comments and .pvsconfig files.
/// </summary>
public bool ReportDisabledRules;
/// <summary>
/// Gets the analysis mode flags from the settings
/// </summary>
/// <returns>Mode flags</returns>
public PVSAnalysisModeFlags GetModeFlags()
{
PVSAnalysisModeFlags Flags = 0;
if (!Disable64BitAnalysis)
{
Flags |= PVSAnalysisModeFlags.Check64BitPortability;
}
if (!DisableGAAnalysis)
{
Flags |= PVSAnalysisModeFlags.GeneralAnalysis;
}
if (!DisableOPAnalysis)
{
Flags |= PVSAnalysisModeFlags.Optimizations;
}
if (!DisableCSAnalysis)
{
Flags |= PVSAnalysisModeFlags.CustomerSpecific;
}
if (!DisableMISRAAnalysis)
{
Flags |= PVSAnalysisModeFlags.MISRA;
}
return Flags;
}
/// <summary>
/// Attempts to read the application settings from the default location
/// </summary>
/// <returns>Application settings instance, or null if no file was present</returns>
internal static PVSApplicationSettings? Read()
{
FileReference SettingsPath = FileReference.Combine(new DirectoryReference(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)), "PVS-Studio", "Settings.xml");
if (FileReference.Exists(SettingsPath))
{
try
{
XmlSerializer Serializer = new(typeof(PVSApplicationSettings));
using FileStream Stream = new(SettingsPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
return (PVSApplicationSettings?)Serializer.Deserialize(Stream);
}
catch (Exception Ex)
{
throw new BuildException(Ex, "Unable to read PVS-Studio settings file from {0}", SettingsPath);
}
}
return null;
}
}
/// <summary>
/// Settings for the PVS Studio analyzer
/// </summary>
public class PVSTargetSettings
{
/// <summary>
/// Returns the application settings
/// </summary>
internal Lazy<PVSApplicationSettings?> ApplicationSettings { get; } = new Lazy<PVSApplicationSettings?>(() => PVSApplicationSettings.Read());
/// <summary>
/// Whether to use application settings to determine the analysis mode
/// </summary>
public bool UseApplicationSettings { get; set; }
/// <summary>
/// Override for the analysis mode to use
/// </summary>
public PVSAnalysisModeFlags ModeFlags
{
get
{
if (ModePrivate.HasValue)
{
return ModePrivate.Value;
}
else if (UseApplicationSettings && ApplicationSettings.Value != null)
{
return ApplicationSettings.Value.GetModeFlags();
}
else
{
return PVSAnalysisModeFlags.GeneralAnalysis;
}
}
set => ModePrivate = value;
}
/// <summary>
/// Private storage for the mode flags
/// </summary>
PVSAnalysisModeFlags? ModePrivate;
/// <summary>
/// Override for the analysis timeoutFlag to use
/// </summary>
public AnalysisTimeoutFlags AnalysisTimeoutFlag
{
get
{
if (TimeoutPrivate.HasValue)
{
return TimeoutPrivate.Value;
}
else if (UseApplicationSettings && ApplicationSettings.Value != null)
{
return ApplicationSettings.Value.AnalysisTimeout;
}
else
{
return AnalysisTimeoutFlags.After_30_minutes;
}
}
set => TimeoutPrivate = value;
}
/// <summary>
/// Private storage for the analysis timeout
/// </summary>
AnalysisTimeoutFlags? TimeoutPrivate;
/// <summary>
/// Override for the disable Level 3 (Low) analyzer messages
/// </summary>
public bool EnableNoNoise
{
get
{
if (EnableNoNoisePrivate.HasValue)
{
return EnableNoNoisePrivate.Value;
}
else if (UseApplicationSettings && ApplicationSettings.Value != null)
{
return ApplicationSettings.Value.NoNoise;
}
else
{
return false;
}
}
set => EnableNoNoisePrivate = value;
}
/// <summary>
/// Private storage for the NoNoise analyzer setting
/// </summary>
bool? EnableNoNoisePrivate;
/// <summary>
/// Override for the enable the display of analyzer rules exceptions which can be specified by comments and .pvsconfig files.
/// </summary>
public bool EnableReportDisabledRules
{
get
{
if (EnableReportDisabledRulesPrivate.HasValue)
{
return EnableReportDisabledRulesPrivate.Value;
}
else if (UseApplicationSettings && ApplicationSettings.Value != null)
{
return ApplicationSettings.Value.ReportDisabledRules;
}
else
{
return false;
}
}
set => EnableReportDisabledRulesPrivate = value;
}
/// <summary>
/// Private storage for the ReportDisabledRules analyzer setting
/// </summary>
bool? EnableReportDisabledRulesPrivate;
}
/// <summary>
/// Read-only version of the PVS toolchain settings
/// </summary>
/// <remarks>
/// Constructor
/// </remarks>
/// <param name="Inner">The inner object</param>
public class ReadOnlyPVSTargetSettings(PVSTargetSettings Inner)
{
/// <summary>
/// Accessor for the Application settings
/// </summary>
internal PVSApplicationSettings? ApplicationSettings => Inner.ApplicationSettings.Value;
/// <summary>
/// Whether to use the application settings for the mode
/// </summary>
public bool UseApplicationSettings => Inner.UseApplicationSettings;
/// <summary>
/// Override for the analysis mode to use
/// </summary>
public PVSAnalysisModeFlags ModeFlags => Inner.ModeFlags;
/// <summary>
/// Override for the analysis timeout to use
/// </summary>
public AnalysisTimeoutFlags AnalysisTimeoutFlag => Inner.AnalysisTimeoutFlag;
/// <summary>
/// Override NoNoise analysis setting to use
/// </summary>
public bool EnableNoNoise => Inner.EnableNoNoise;
/// <summary>
/// Override EnableReportDisabledRules analysis setting to use
/// </summary>
public bool EnableReportDisabledRules => Inner.EnableReportDisabledRules;
}
/// <summary>
/// Special mode for gathering all the messages into a single output file
/// </summary>
[ToolMode("PVSGather", ToolModeOptions.None)]
class PVSGatherMode : ToolMode
{
/// <summary>
/// Path to the input file list
/// </summary>
[CommandLine("-Input", Required = true)]
FileReference? _inputFileList = null;
/// <summary>
/// Output file to generate
/// </summary>
[CommandLine("-Output", Required = true)]
FileReference? _outputFile = null;
/// <summary>
/// Path to file list of paths to ignore
/// </summary>
[CommandLine("-Ignored", Required = true)]
FileReference? _ignoredFile = null;
/// <summary>
/// Path to file list of rootpaths,realpath mapping
/// </summary>
[CommandLine("-RootPaths", Required = true)]
FileReference? _rootPathsFile = null;
/// <summary>
/// The maximum level of warnings to print
/// </summary>
[CommandLine("-PrintLevel")]
int _printLevel = 1;
/// <summary>
/// If all ThirdParty code should be ignored
/// </summary>
bool _ignoreThirdParty = true;
readonly CaptureLogger _parseLogger = new();
IEnumerable<DirectoryReference> _ignoredDirectories = [];
CppRootPaths _rootPaths = new();
/// <summary>
/// Execute the command
/// </summary>
/// <param name="arguments">List of command line arguments</param>
/// <returns>Always zero, or throws an exception</returns>
/// <param name="logger"></param>
public override async Task<int> ExecuteAsync(CommandLineArguments arguments, ILogger logger)
{
arguments.ApplyTo(this);
arguments.CheckAllArgumentsUsed();
if (_inputFileList == null || _outputFile == null || _ignoredFile == null || _rootPathsFile == null)
{
throw new NullReferenceException();
}
// Read the input files
string[] inputFileLines = await FileReference.ReadAllLinesAsync(_inputFileList);
IEnumerable<FileReference> inputFiles = inputFileLines.Select(x => x.Trim()).Where(x => x.Length > 0).Select(x => new FileReference(x));
// Read the ignore file
string[] ignoreFileLines = await FileReference.ReadAllLinesAsync(_ignoredFile);
_ignoredDirectories = ignoreFileLines.Select(x => x.Trim()).Where(x => x.Length > 0).Select(x => new DirectoryReference(x));
// Read the root paths file
_rootPaths = new(new BinaryArchiveReader(_rootPathsFile));
// Remove analyzedSourceFiles array from each line so all the lines can be more efficiently deduped.
Regex removeRegex = new("\"analyzedSourceFiles\":\\[.*?\\],", RegexOptions.Compiled);
ParallelQuery<string> allLines = inputFiles.AsParallel().SelectMany(FileReference.ReadAllLines)
.Select(x => removeRegex.Replace(x, String.Empty))
.Distinct();
// Task.Run to prevent blocking on parallel query
Task writeTask = Task.Run(() => FileReference.WriteAllLines(_outputFile, allLines.OrderBy(x => x)));
ParallelQuery<PVSErrorInfo> allErrors = allLines.Select(GetErrorInfo)
.OfType<PVSErrorInfo>();
OrderedParallelQuery<PVSErrorInfo> filteredErrors = allErrors
.Where(x => x.FalseAlarm != true && x.Level <= _printLevel) // Ignore false alarm warnings, and limit printing by PrintLevel
.Where(x => !String.IsNullOrWhiteSpace(x.Positions.FirstOrDefault()?.File)) // Ignore files with no position
.OrderBy(x => x.Positions.FirstOrDefault()?.File)
.ThenBy(x => x.Positions.FirstOrDefault()?.Lines.FirstOrDefault());
// Create the combined output file, and print the diagnostics to the log
foreach (PVSErrorInfo errorInfo in filteredErrors)
{
string file = errorInfo.Positions.FirstOrDefault()?.File ?? Unreal.EngineDirectory.FullName;
int lineNumber = errorInfo.Positions.FirstOrDefault()?.Lines.FirstOrDefault() ?? 1;
LogValue logValue = new(LogValueType.SourceFile, file, new Dictionary<Utf8String, object?> { [LogEventPropertyName.File] = file });
logger.LogWarning(KnownLogEvents.Compiler, "{Path}({LineNumber}): warning {WarningCode}: {WarningMessage}", logValue, lineNumber, errorInfo.Code, errorInfo.Message);
}
PVSErrorInfo? renewError = allErrors.FirstOrDefault(x => x.Code.Equals("Renew", StringComparison.Ordinal));
if (renewError != null)
{
logger.LogInformation("PVS-Studio Renewal Notice: {WarningMessage}", renewError.Message);
logger.LogWarning("Warning: PVS-Studio license will expire soon. See output log for details.");
}
await writeTask;
int count = allLines.Count();
logger.LogInformation("Written {NumItems} {Noun} to {File}.", count, (count == 1) ? "diagnostic" : "diagnostics", _outputFile.FullName);
_parseLogger.RenderTo(logger);
return 0;
}
// Ignore anything in the IgnoredDirectories folders or ThirdParty if ignored
bool IsFileIgnored(FileReference? fileReference) => fileReference != null
&& ((_ignoreThirdParty && fileReference.FullName.Contains("ThirdParty", StringComparison.OrdinalIgnoreCase)) || (_ignoredDirectories.Any() && _ignoredDirectories.Any(fileReference.IsUnderDirectory)));
PVSErrorInfo? GetErrorInfo(string line)
{
try
{
PVSErrorInfo errorInfo = JsonConvert.DeserializeObject<PVSErrorInfo>(line) ?? throw new FormatException();
FileReference? fileReference = errorInfo.Positions.FirstOrDefault()?.UpdateFilePath(_rootPaths);
return IsFileIgnored(fileReference) ? null : errorInfo;
}
catch (Exception ex)
{
_parseLogger.LogDebug(KnownLogEvents.Compiler, "warning: Unable to parse PVS output line '{Line}' ({Message})", line, ex.Message);
}
return null;
}
}
class PVSPosition
{
[JsonProperty(Required = Required.Always)]
public required string File;
[JsonProperty(Required = Required.Always)]
public required int[] Lines;
public FileReference? UpdateFilePath(CppRootPaths rootPaths)
{
FileReference? fileReference = !String.IsNullOrWhiteSpace(File) ? FileReference.FromString(File) : null;
if (fileReference != null && rootPaths.bUseVfs)
{
fileReference = rootPaths.GetLocalPath(fileReference);
File = fileReference.FullName;
}
return fileReference;
}
}
class PVSErrorInfo
{
[JsonProperty(Required = Required.Always)]
public required string Code;
[JsonProperty(Required = Required.Always)]
public required bool FalseAlarm;
[JsonProperty(Required = Required.Always)]
public required int Level;
[JsonProperty(Required = Required.Always)]
public required string Message;
[JsonProperty(Required = Required.Always)]
public required PVSPosition[] Positions;
}
class PVSToolChain : ISPCToolChain
{
readonly ReadOnlyTargetRules Target;
readonly ReadOnlyPVSTargetSettings Settings;
readonly PVSApplicationSettings? ApplicationSettings;
readonly VCToolChain InnerToolChain;
readonly FileReference AnalyzerFile;
readonly FileReference? LicenseFile;
readonly UnrealTargetPlatform Platform;
readonly Version AnalyzerVersion;
static readonly Version _minAnalyzerVersion = new Version("7.30");
static readonly Version _analysisPathsSkipVersion = new Version("7.34");
const string OutputFileExtension = ".PVS-Studio.log";
public PVSToolChain(ReadOnlyTargetRules Target, VCToolChain InInnerToolchain, ILogger Logger)
: base(Logger)
{
this.Target = Target;
Platform = Target.Platform;
InnerToolChain = InInnerToolchain;
AnalyzerFile = FileReference.Combine(Unreal.RootDirectory, "Engine", "Restricted", "NoRedist", "Extras", "ThirdPartyNotUE", "PVS-Studio", "PVS-Studio.exe");
if (!FileReference.Exists(AnalyzerFile))
{
FileReference InstalledAnalyzerFile = FileReference.Combine(new DirectoryReference(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)), "PVS-Studio", "x64", "PVS-Studio.exe");
if (FileReference.Exists(InstalledAnalyzerFile))
{
AnalyzerFile = InstalledAnalyzerFile;
}
else
{
throw new BuildException("Unable to find PVS-Studio at {0} or {1}", AnalyzerFile, InstalledAnalyzerFile);
}
}
AnalyzerVersion = GetAnalyzerVersion(AnalyzerFile);
if (AnalyzerVersion < _minAnalyzerVersion)
{
throw new BuildLogEventException("PVS-Studio version {Version} is older than the minimum supported version {MinVersion}", AnalyzerVersion, _minAnalyzerVersion);
}
Settings = Target.WindowsPlatform.PVS;
ApplicationSettings = Settings.ApplicationSettings;
if (ApplicationSettings != null)
{
if (Settings.ModeFlags == 0)
{
throw new BuildException("All PVS-Studio analysis modes are disabled.");
}
if (!String.IsNullOrEmpty(ApplicationSettings.UserName) && !String.IsNullOrEmpty(ApplicationSettings.SerialNumber))
{
LicenseFile = FileReference.Combine(Unreal.EngineDirectory, "Intermediate", "PVS", "PVS-Studio.lic");
Utils.WriteFileIfChanged(LicenseFile, String.Format("{0}\n{1}\n", ApplicationSettings.UserName, ApplicationSettings.SerialNumber), Logger);
}
}
else
{
FileReference defaultLicenseFile = AnalyzerFile.ChangeExtension(".lic");
if (FileReference.Exists(defaultLicenseFile))
{
LicenseFile = defaultLicenseFile;
}
}
if (BuildHostPlatform.Current.IsRunningOnWine())
{
throw new BuildException("PVS-Studio is not supported with Wine.");
}
}
public override void GetVersionInfo(List<string> Lines)
{
InnerToolChain.GetVersionInfo(Lines);
ReadOnlyPVSTargetSettings settings = Target.WindowsPlatform.PVS;
Lines.Add(String.Format("Using PVS-Studio {0} at {1} with analysis mode {2} ({3})", AnalyzerVersion, AnalyzerFile, (uint)settings.ModeFlags, settings.ModeFlags.ToString()));
}
public override void GetExternalDependencies(HashSet<FileItem> ExternalDependencies)
{
InnerToolChain.GetExternalDependencies(ExternalDependencies);
ExternalDependencies.Add(FileItem.GetItemByFileReference(FileReference.Combine(new DirectoryReference(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)), "PVS-Studio", "Settings.xml")));
ExternalDependencies.Add(FileItem.GetItemByFileReference(AnalyzerFile));
}
public override void SetUpGlobalEnvironment(ReadOnlyTargetRules Target, CppCompileEnvironment GlobalCompileEnvironment, LinkEnvironment GlobalLinkEnvironment)
{
base.SetUpGlobalEnvironment(Target, GlobalCompileEnvironment, GlobalLinkEnvironment);
InnerToolChain.SetUpGlobalEnvironment(Target, GlobalCompileEnvironment, GlobalLinkEnvironment);
if (!AnalyzerFile.IsUnderDirectory(Unreal.RootDirectory))
{
GlobalCompileEnvironment.RootPaths.AddExtraPath(("PVSAnalyzer", AnalyzerFile.Directory.FullName));
GlobalLinkEnvironment.RootPaths.AddExtraPath(("PVSAnalyzer", AnalyzerFile.Directory.FullName));
}
if (LicenseFile != null && !LicenseFile.IsUnderDirectory(Unreal.RootDirectory))
{
GlobalCompileEnvironment.RootPaths.AddExtraPath(("PVSLicense", LicenseFile.Directory.FullName));
GlobalLinkEnvironment.RootPaths.AddExtraPath(("PVSLicense", LicenseFile.Directory.FullName));
}
}
public override void SetEnvironmentVariables()
{
Target.WindowsPlatform.Environment?.SetEnvironmentVariables();
}
static Version GetAnalyzerVersion(FileReference AnalyzerPath)
{
string output = String.Empty;
Version? analyzerVersion = new(0, 0);
try
{
using (Process PvsProc = new())
{
PvsProc.StartInfo.FileName = AnalyzerPath.FullName;
PvsProc.StartInfo.Arguments = "--version";
PvsProc.StartInfo.UseShellExecute = false;
PvsProc.StartInfo.CreateNoWindow = true;
PvsProc.StartInfo.RedirectStandardOutput = true;
PvsProc.Start();
output = PvsProc.StandardOutput.ReadToEnd();
PvsProc.WaitForExit();
}
const string versionPattern = @"\d+(?:\.\d+)+";
Match match = Regex.Match(output, versionPattern);
if (match.Success)
{
string versionStr = match.Value;
if (!Version.TryParse(versionStr, out analyzerVersion))
{
throw new BuildLogEventException("Failed to parse PVS-Studio version: {Version}", versionStr);
}
}
}
catch (Exception ex)
{
if (ex is BuildException)
{
throw;
}
throw new BuildException(ex, "Failed to obtain PVS-Studio version.");
}
return analyzerVersion;
}
class ActionGraphCapture(IActionGraphBuilder inner, List<IExternalAction> actions) : ForwardingActionGraphBuilder(inner)
{
public override void AddAction(IExternalAction Action)
{
base.AddAction(Action);
actions.Add(Action);
}
}
const string CPP_20 = "c++20";
const string CPP_23 = "c++23";
public static string GetLangStandForCfgFile(CppStandardVersion cppStandard, VersionNumber compilerVersion)
{
return cppStandard switch
{
CppStandardVersion.Cpp20 => CPP_20,
CppStandardVersion.Cpp23 => CPP_23,
CppStandardVersion.Latest => CPP_23,
_ => CPP_20,
};
}
public static bool ShouldCompileAsC(string compilerCommandLine, string sourceFileName)
{
int cFlagLastPosition = Math.Max(Math.Max(compilerCommandLine.LastIndexOf("/TC "), compilerCommandLine.LastIndexOf("/Tc ")),
Math.Max(compilerCommandLine.LastIndexOf("-TC "), compilerCommandLine.LastIndexOf("-Tc ")));
int cppFlagLastPosition = Math.Max(Math.Max(compilerCommandLine.LastIndexOf("/TP "), compilerCommandLine.LastIndexOf("/Tp ")),
Math.Max(compilerCommandLine.LastIndexOf("-TP "), compilerCommandLine.LastIndexOf("-Tp ")));
bool compileAsCCode = cFlagLastPosition == cppFlagLastPosition
? Path.GetExtension(sourceFileName).Equals(".c", StringComparison.InvariantCultureIgnoreCase)
: cFlagLastPosition > cppFlagLastPosition;
return compileAsCCode;
}
public override CppCompileEnvironment CreateSharedResponseFile(CppCompileEnvironment compileEnvironment, FileReference outResponseFile, IActionGraphBuilder graph)
{
return compileEnvironment;
}
CPPOutput PreprocessCppFiles(CppCompileEnvironment compileEnvironment, IEnumerable<FileItem> inputFiles, DirectoryReference outputDir, string moduleName, IActionGraphBuilder graph, out List<IExternalAction> preprocessActions)
{
// Preprocess the source files with the regular toolchain
CppCompileEnvironment preprocessCompileEnvironment = new(compileEnvironment)
{
bPreprocessOnly = true
};
preprocessCompileEnvironment.AdditionalArguments += " /wd4005 /wd4828 /wd5105";
preprocessCompileEnvironment.Definitions.Add("PVS_STUDIO");
preprocessCompileEnvironment.CppCompileWarnings.UndefinedIdentifierWarningLevel = WarningLevel.Off; // Not sure why THIRD_PARTY_INCLUDES_START doesn't pick this up; the _Pragma appears in the preprocessed output. Perhaps in preprocess-only mode the compiler doesn't respect these?
preprocessActions = [];
return InnerToolChain.CompileAllCPPFiles(preprocessCompileEnvironment, inputFiles, outputDir, moduleName, new ActionGraphCapture(graph, preprocessActions));
}
void AnalyzeCppFile(VCCompileAction preprocessAction, CppCompileEnvironment compileEnvironment, DirectoryReference outputDir, CPPOutput result, IActionGraphBuilder graph)
{
FileItem sourceFileItem = preprocessAction.SourceFile!;
FileItem preprocessedFileItem = preprocessAction.PreprocessedFile!;
// Write the PVS studio config file
StringBuilder configFileContents = new();
foreach (DirectoryReference includePath in Target.WindowsPlatform.Environment!.IncludePaths)
{
configFileContents.AppendFormat("exclude-path={0}\n", includePath.FullName);
}
if (ApplicationSettings != null && ApplicationSettings.PathMasks != null)
{
foreach (string pathMask in ApplicationSettings.PathMasks)
{
if (pathMask.Contains(':') || pathMask.Contains('\\') || pathMask.Contains('/'))
{
if (Path.IsPathRooted(pathMask) && !pathMask.Contains(':'))
{
configFileContents.AppendFormat("exclude-path=*{0}*\n", pathMask);
}
else
{
configFileContents.AppendFormat("exclude-path={0}\n", pathMask);
}
}
}
}
if (Platform.IsInGroup(UnrealPlatformGroup.Microsoft))
{
configFileContents.Append("platform=x64\n");
}
else
{
throw new BuildException("PVS-Studio does not support this platform");
}
configFileContents.Append("preprocessor=visualcpp\n");
bool shouldCompileAsC = ShouldCompileAsC(String.Join(" ", preprocessAction.Arguments), sourceFileItem.AbsolutePath);
configFileContents.AppendFormat("language={0}\n", shouldCompileAsC ? "C" : "C++");
configFileContents.Append("skip-cl-exe=yes\n");
WindowsCompiler windowsCompiler = Target.WindowsPlatform.Compiler;
bool isVisualCppCompiler = windowsCompiler.IsMSVC();
if (!shouldCompileAsC)
{
VersionNumber compilerVersion = Target.WindowsPlatform.Environment.CompilerVersion;
string languageStandardForCfg = GetLangStandForCfgFile(compileEnvironment.CppStandard, compilerVersion);
configFileContents.AppendFormat("std={0}\n", languageStandardForCfg);
bool disableMsExtensionsFromArgs = preprocessAction.Arguments.Any(arg => arg.Equals("/Za") || arg.Equals("-Za") || arg.Equals("/permissive-"));
bool disableMsExtensions = isVisualCppCompiler && (languageStandardForCfg == CPP_20 || disableMsExtensionsFromArgs);
configFileContents.AppendFormat("disable-ms-extensions={0}\n", disableMsExtensions ? "yes" : "no");
}
if (isVisualCppCompiler && preprocessAction.Arguments.Any(arg => arg.StartsWith("/await")))
{
configFileContents.Append("msvc-await=yes\n");
}
if (Settings.EnableNoNoise)
{
configFileContents.Append("no-noise=yes\n");
}
if (Settings.EnableReportDisabledRules)
{
configFileContents.Append("report-disabled-rules=yes\n");
}
// TODO: Investigate into this disabled error
if (sourceFileItem.Location.IsUnderDirectory(Unreal.RootDirectory))
{
configFileContents.AppendFormat("errors-off=V1102\n");
}
foreach (string error in compileEnvironment.StaticAnalyzerPVSDisabledErrors.OrderBy(x => x))
{
configFileContents.AppendFormat($"errors-off={error}\n");
}
int timeout = Settings.AnalysisTimeoutFlag == AnalysisTimeoutFlags.No_timeout ? 0 : (int)Settings.AnalysisTimeoutFlag;
configFileContents.AppendFormat("timeout={0}\n", timeout);
configFileContents.Append("silent-exit-code-mode=yes\n");
configFileContents.Append("new-output-format=yes\n");
if (AnalyzerVersion.CompareTo(_analysisPathsSkipVersion) >= 0)
{
if (Target.bStaticAnalyzerProjectOnly)
{
configFileContents.Append($"analysis-paths=skip={Unreal.EngineSourceDirectory}\n");
}
if (!Target.bStaticAnalyzerIncludeGenerated)
{
configFileContents.Append($"analysis-paths=skip=*.gen.cpp\n");
configFileContents.Append($"analysis-paths=skip=*.generated.h\n");
}
}
string baseFileName = preprocessedFileItem.Location.GetFileName();
FileReference configFileLocation = FileReference.Combine(outputDir, baseFileName + ".cfg");
FileItem configFileItem = graph.CreateIntermediateTextFile(configFileLocation, configFileContents.ToString());
// Run the analyzer on the preprocessed source file
FileReference outputFileLocation = FileReference.Combine(outputDir, baseFileName + OutputFileExtension);
FileItem outputFileItem = FileItem.GetItemByFileReference(outputFileLocation);
Action analyzeAction = graph.CreateAction(ActionType.Compile);
analyzeAction.CommandDescription = "Analyzing";
analyzeAction.StatusDescription = baseFileName;
analyzeAction.WorkingDirectory = Unreal.EngineSourceDirectory;
analyzeAction.CommandPath = AnalyzerFile;
analyzeAction.CommandVersion = AnalyzerVersion.ToString();
List<string> arguments =
[
$"--source-file \"{sourceFileItem.AbsolutePath}\"",
$"--output-file \"{outputFileItem.AbsolutePath}\"",
$"--cfg \"{configFileItem.AbsolutePath}\"",
$"--i-file=\"{preprocessedFileItem.AbsolutePath}\"",
$"--analysis-mode {(uint)Settings.ModeFlags}",
$"--lic-name \"{ApplicationSettings?.UserName}\" --lic-key \"{ApplicationSettings?.SerialNumber}\"",
];
analyzeAction.CommandArguments = String.Join(' ', arguments);
analyzeAction.PrerequisiteItems.Add(sourceFileItem);
analyzeAction.PrerequisiteItems.Add(configFileItem);
analyzeAction.PrerequisiteItems.Add(preprocessedFileItem);
analyzeAction.ProducedItems.Add(outputFileItem);
analyzeAction.DeleteItems.Add(outputFileItem); // PVS Studio will append by default, so need to delete produced items
analyzeAction.bCanExecuteRemotely = true;
analyzeAction.bCanExecuteRemotelyWithSNDBS = false;
analyzeAction.bCanExecuteRemotelyWithXGE = false;
analyzeAction.RootPaths = compileEnvironment.RootPaths;
analyzeAction.CacheBucket = GetCacheBucket(Target, null);
analyzeAction.ArtifactMode = ArtifactMode.Enabled;
result.ObjectFiles.AddRange(analyzeAction.ProducedItems);
}
protected override CPPOutput CompileCPPFiles(CppCompileEnvironment compileEnvironment, IEnumerable<FileItem> inputFiles, DirectoryReference outputDir, string moduleName, IActionGraphBuilder graph)
{
if (compileEnvironment.bDisableStaticAnalysis)
{
return new CPPOutput();
}
// Use a subdirectory for PVS output, to avoid clobbering regular build artifacts
outputDir = DirectoryReference.Combine(outputDir, "PVS");
// Preprocess the source files with the regular toolchain
CPPOutput result = PreprocessCppFiles(compileEnvironment, inputFiles, outputDir, moduleName, graph, out List<IExternalAction> PreprocessActions);
// Run the source files through PVS-Studio
for (int Idx = 0; Idx < PreprocessActions.Count; Idx++)
{
if (PreprocessActions[Idx] is not VCCompileAction PreprocessAction)
{
continue;
}
FileItem? sourceFileItem = PreprocessAction.SourceFile;
if (sourceFileItem == null)
{
Logger.LogWarning("Unable to find source file from command producing: {File}", String.Join(", ", PreprocessActions[Idx].ProducedItems.Select(x => x.Location.GetFileName())));
continue;
}
if (PreprocessAction.PreprocessedFile == null)
{
Logger.LogWarning("Unable to find preprocessed output file from {File}", sourceFileItem.Location.GetFileName());
continue;
}
// We don't want to run these remotely since they are very lightweight but has lots of I/O
PreprocessAction.bCanExecuteRemotely = false;
AnalyzeCppFile(PreprocessAction, compileEnvironment, outputDir, result, graph);
}
return result;
}
public override void GenerateTypeLibraryHeader(CppCompileEnvironment compileEnvironment, ModuleRules.TypeLibrary typeLibrary, FileReference outputFile, FileReference? outputHeader, IActionGraphBuilder graph)
{
InnerToolChain.GenerateTypeLibraryHeader(compileEnvironment, typeLibrary, outputFile, outputHeader, graph);
}
public override FileItem LinkFiles(LinkEnvironment linkEnvironment, bool bBuildImportLibraryOnly, IActionGraphBuilder graph)
{
throw new BuildException("Unable to link with PVS toolchain.");
}
public override void FinalizeOutput(ReadOnlyTargetRules target, TargetMakefileBuilder makefileBuilder)
{
string outputFileExtension = OutputFileExtension;
FileReference outputFile = target.ProjectFile == null
? FileReference.Combine(Unreal.EngineDirectory, "Saved", "PVS-Studio", $"{target.Name}{outputFileExtension}")
: FileReference.Combine(target.ProjectFile.Directory, "Saved", "PVS-Studio", $"{target.Name}{outputFileExtension}");
TargetMakefile makefile = makefileBuilder.Makefile;
IEnumerable<FileReference> inputFiles = [.. makefile.OutputItems.Select(x => x.Location).Where(x => x.HasExtension(outputFileExtension))];
// Collect the sourcefile items off of the Compile action added in CompileCPPFiles so that in SingleFileCompile mode the PVSGather step is also not filtered out
IEnumerable<FileItem> compileSourceFiles = [.. makefile.Actions.OfType<VCCompileAction>().Select(x => x.SourceFile!)];
CppRootPaths rootPaths = makefile.Actions.OfType<VCCompileAction>().FirstOrDefault(x => x.RootPaths.Any())?.RootPaths ?? new();
// Store list of system paths that should be excluded
IEnumerable<DirectoryReference> systemIncludePaths = [.. makefile.Actions.OfType<VCCompileAction>().SelectMany(x => x.SystemIncludePaths)];
FileItem inputFileListItem = makefileBuilder.CreateIntermediateTextFile(FileReference.Combine(makefile.ProjectIntermediateDirectory, outputFile.ChangeExtension(".input").GetFileName()), inputFiles.Select(x => x.FullName).Distinct().Order());
FileItem ignoredFileListItem = makefileBuilder.CreateIntermediateTextFile(FileReference.Combine(makefile.ProjectIntermediateDirectory, outputFile.ChangeExtension(".ignored").GetFileName()), systemIncludePaths.Select(x => x.FullName).Distinct().Order());
FileItem rootPathsItem = FileItem.GetItemByFileReference(FileReference.Combine(makefile.ProjectIntermediateDirectory, outputFile.ChangeExtension(".rootpaths").GetFileName()));
{
using MemoryStream stream = new();
using BinaryArchiveWriter binaryArchiveWriter = new(stream);
rootPaths.Write(binaryArchiveWriter);
binaryArchiveWriter.Flush();
FileReference.WriteAllBytesIfDifferent(rootPathsItem.Location, stream.ToArray());
}
string arguments = $"-Input=\"{inputFileListItem.Location}\" -Output=\"{outputFile}\" -Ignored=\"{ignoredFileListItem.Location}\" -RootPaths=\"{rootPathsItem.Location}\" -PrintLevel={target.StaticAnalyzerPVSPrintLevel}";
Action finalizeAction = makefileBuilder.CreateRecursiveAction<PVSGatherMode>(ActionType.PostBuildStep, arguments);
finalizeAction.CommandDescription = "Process PVS-Studio Results";
finalizeAction.PrerequisiteItems.Add(inputFileListItem);
finalizeAction.PrerequisiteItems.Add(ignoredFileListItem);
finalizeAction.PrerequisiteItems.UnionWith(makefile.OutputItems);
finalizeAction.PrerequisiteItems.UnionWith(compileSourceFiles);
finalizeAction.ProducedItems.Add(FileItem.GetItemByFileReference(outputFile));
finalizeAction.ProducedItems.Add(FileItem.GetItemByPath(outputFile.FullName + "_does_not_exist")); // Force the gather step to always execute
finalizeAction.DeleteItems.UnionWith(finalizeAction.ProducedItems);
makefile.OutputItems.AddRange(finalizeAction.ProducedItems);
}
}
}