// 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 { /// /// Flags for the PVS analyzer mode /// public enum PVSAnalysisModeFlags : uint { /// /// Check for 64-bit portability issues /// Check64BitPortability = 1, /// /// Enable general analysis /// GeneralAnalysis = 4, /// /// Check for optimizations /// Optimizations = 8, /// /// Enable customer-specific rules /// CustomerSpecific = 16, /// /// Enable MISRA analysis /// MISRA = 32, } /// /// Flags for the PVS analyzer timeout /// public enum AnalysisTimeoutFlags { /// /// Analysis timeout for file 10 minutes (600 seconds) /// After_10_minutes = 600, /// /// Analysis timeout for file 30 minutes (1800 seconds) /// After_30_minutes = 1800, /// /// Analysis timeout for file 60 minutes (3600 seconds) /// After_60_minutes = 3600, /// /// Analysis timeout when not set (a lot of seconds) /// No_timeout = 999999 } /// /// Partial representation of PVS-Studio main settings file /// [XmlRoot("ApplicationSettings")] public class PVSApplicationSettings { /// /// Masks for paths excluded for analysis /// public string[]? PathMasks; /// /// Registered username /// public string? UserName; /// /// Registered serial number /// public string? SerialNumber; /// /// Disable the 64-bit Analysis /// public bool Disable64BitAnalysis; /// /// Disable the General Analysis /// public bool DisableGAAnalysis; /// /// Disable the Optimization Analysis /// public bool DisableOPAnalysis; /// /// Disable the Customer's Specific diagnostic rules /// public bool DisableCSAnalysis; /// /// Disable the MISRA Analysis /// public bool DisableMISRAAnalysis; /// /// File analysis timeout /// public AnalysisTimeoutFlags AnalysisTimeout; /// /// Disable analyzer Level 3 (Low) messages /// public bool NoNoise; /// /// Enable the display of analyzer rules exceptions which can be specified by comments and .pvsconfig files. /// public bool ReportDisabledRules; /// /// Gets the analysis mode flags from the settings /// /// Mode flags 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; } /// /// Attempts to read the application settings from the default location /// /// Application settings instance, or null if no file was present 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; } } /// /// Settings for the PVS Studio analyzer /// public class PVSTargetSettings { /// /// Returns the application settings /// internal Lazy ApplicationSettings { get; } = new Lazy(() => PVSApplicationSettings.Read()); /// /// Whether to use application settings to determine the analysis mode /// public bool UseApplicationSettings { get; set; } /// /// Override for the analysis mode to use /// 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; } /// /// Private storage for the mode flags /// PVSAnalysisModeFlags? ModePrivate; /// /// Override for the analysis timeoutFlag to use /// 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; } /// /// Private storage for the analysis timeout /// AnalysisTimeoutFlags? TimeoutPrivate; /// /// Override for the disable Level 3 (Low) analyzer messages /// 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; } /// /// Private storage for the NoNoise analyzer setting /// bool? EnableNoNoisePrivate; /// /// Override for the enable the display of analyzer rules exceptions which can be specified by comments and .pvsconfig files. /// 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; } /// /// Private storage for the ReportDisabledRules analyzer setting /// bool? EnableReportDisabledRulesPrivate; } /// /// Read-only version of the PVS toolchain settings /// /// /// Constructor /// /// The inner object public class ReadOnlyPVSTargetSettings(PVSTargetSettings Inner) { /// /// Accessor for the Application settings /// internal PVSApplicationSettings? ApplicationSettings => Inner.ApplicationSettings.Value; /// /// Whether to use the application settings for the mode /// public bool UseApplicationSettings => Inner.UseApplicationSettings; /// /// Override for the analysis mode to use /// public PVSAnalysisModeFlags ModeFlags => Inner.ModeFlags; /// /// Override for the analysis timeout to use /// public AnalysisTimeoutFlags AnalysisTimeoutFlag => Inner.AnalysisTimeoutFlag; /// /// Override NoNoise analysis setting to use /// public bool EnableNoNoise => Inner.EnableNoNoise; /// /// Override EnableReportDisabledRules analysis setting to use /// public bool EnableReportDisabledRules => Inner.EnableReportDisabledRules; } /// /// Special mode for gathering all the messages into a single output file /// [ToolMode("PVSGather", ToolModeOptions.None)] class PVSGatherMode : ToolMode { /// /// Path to the input file list /// [CommandLine("-Input", Required = true)] FileReference? _inputFileList = null; /// /// Output file to generate /// [CommandLine("-Output", Required = true)] FileReference? _outputFile = null; /// /// Path to file list of paths to ignore /// [CommandLine("-Ignored", Required = true)] FileReference? _ignoredFile = null; /// /// Path to file list of rootpaths,realpath mapping /// [CommandLine("-RootPaths", Required = true)] FileReference? _rootPathsFile = null; /// /// The maximum level of warnings to print /// [CommandLine("-PrintLevel")] int _printLevel = 1; /// /// If all ThirdParty code should be ignored /// bool _ignoreThirdParty = true; readonly CaptureLogger _parseLogger = new(); IEnumerable _ignoredDirectories = []; CppRootPaths _rootPaths = new(); /// /// Execute the command /// /// List of command line arguments /// Always zero, or throws an exception /// public override async Task 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 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 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 allErrors = allLines.Select(GetErrorInfo) .OfType(); OrderedParallelQuery 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 { [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(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 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 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 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 inputFiles, DirectoryReference outputDir, string moduleName, IActionGraphBuilder graph, out List 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 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 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 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 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 compileSourceFiles = [.. makefile.Actions.OfType().Select(x => x.SourceFile!)]; CppRootPaths rootPaths = makefile.Actions.OfType().FirstOrDefault(x => x.RootPaths.Any())?.RootPaths ?? new(); // Store list of system paths that should be excluded IEnumerable systemIncludePaths = [.. makefile.Actions.OfType().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(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); } } }