// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.CodeDom.Compiler; using Microsoft.CSharp; using System.Reflection; using System.Diagnostics; using EpicGames.Core; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; using System.Reflection.Metadata; using Microsoft.CodeAnalysis.Text; using System.Runtime.InteropServices; using OpenTracing.Util; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Methods for dynamically compiling C# source files /// public class DynamicCompilation { /// /// Checks to see if the assembly needs compilation /// /// Set of source files /// File containing information about this assembly, like which source files it was built with and engine version /// Output path for the assembly /// True if the assembly needs to be built private static bool RequiresCompilation(HashSet SourceFiles, FileReference AssemblyManifestFilePath, FileReference OutputAssemblyPath) { // Do not compile the file if it's installed if (UnrealBuildTool.IsFileInstalled(OutputAssemblyPath)) { Log.TraceLog("Skipping {0}: File is installed", OutputAssemblyPath); return false; } // Check to see if we already have a compiled assembly file on disk FileItem OutputAssemblyInfo = FileItem.GetItemByFileReference(OutputAssemblyPath); if (!OutputAssemblyInfo.Exists) { Log.TraceLog("Compiling {0}: Assembly does not exist", OutputAssemblyPath); return true; } // Check the time stamp of the UnrealBuildTool assembly. If Unreal Build Tool was compiled more // recently than the dynamically-compiled assembly, then we'll always recompile it. This is // because Unreal Build Tool's code may have changed in such a way that invalidate these // previously-compiled assembly files. FileItem ExecutableItem = FileItem.GetItemByFileReference(UnrealBuildTool.GetUBTPath()); if (ExecutableItem.LastWriteTimeUtc > OutputAssemblyInfo.LastWriteTimeUtc) { Log.TraceLog("Compiling {0}: {1} is newer", OutputAssemblyPath, ExecutableItem.Name); return true; } // Make sure we have a manifest of source files used to compile the output assembly. If it doesn't exist // for some reason (not an expected case) then we'll need to recompile. FileItem AssemblySourceListFile = FileItem.GetItemByFileReference(AssemblyManifestFilePath); if (!AssemblySourceListFile.Exists) { Log.TraceLog("Compiling {0}: Missing source file list ({1})", OutputAssemblyPath, AssemblyManifestFilePath); return true; } JsonObject Manifest = JsonObject.Read(AssemblyManifestFilePath); // check if the engine version is different string EngineVersionManifest = Manifest.GetStringField("EngineVersion"); string EngineVersionCurrent = FormatVersionNumber(ReadOnlyBuildVersion.Current); if (EngineVersionManifest != EngineVersionCurrent) { Log.TraceLog("Compiling {0}: Engine Version changed from {1} to {2}", OutputAssemblyPath, EngineVersionManifest, EngineVersionCurrent); return true; } // Make sure the source files we're compiling are the same as the source files that were compiled // for the assembly that we want to load HashSet CurrentSourceFileItems = new HashSet(); foreach(string Line in Manifest.GetStringArrayField("SourceFiles")) { CurrentSourceFileItems.Add(FileItem.GetItemByPath(Line)); } // Get the new source files HashSet SourceFileItems = new HashSet(); foreach(FileReference SourceFile in SourceFiles) { SourceFileItems.Add(FileItem.GetItemByFileReference(SourceFile)); } // Check if there are any differences between the sets foreach(FileItem CurrentSourceFileItem in CurrentSourceFileItems) { if(!SourceFileItems.Contains(CurrentSourceFileItem)) { Log.TraceLog("Compiling {0}: Removed source file ({1})", OutputAssemblyPath, CurrentSourceFileItem); return true; } } foreach(FileItem SourceFileItem in SourceFileItems) { if(!CurrentSourceFileItems.Contains(SourceFileItem)) { Log.TraceLog("Compiling {0}: Added source file ({1})", OutputAssemblyPath, SourceFileItem); return true; } } // Check if any of the timestamps are newer foreach(FileItem SourceFileItem in SourceFileItems) { if(SourceFileItem.LastWriteTimeUtc > OutputAssemblyInfo.LastWriteTimeUtc) { Log.TraceLog("Compiling {0}: {1} is newer", OutputAssemblyPath, SourceFileItem); return true; } } return false; } private static void LogDiagnostics(IEnumerable Diagnostics) { foreach (Diagnostic Diag in Diagnostics) { switch (Diag.Severity) { // Diagnostics are pre-formatted suitable for Visual Studio consumption - print them without an additional severity prefix case DiagnosticSeverity.Error: { Log.WriteLine(LogEventType.Error, LogFormatOptions.NoSeverityPrefix, Diag.ToString()); break; } case DiagnosticSeverity.Hidden: { break; } case DiagnosticSeverity.Warning: { Log.WriteLine(LogEventType.Warning, LogFormatOptions.NoSeverityPrefix, Diag.ToString()); break; } case DiagnosticSeverity.Info: { Log.WriteLine(LogEventType.Console, LogFormatOptions.NoSeverityPrefix, Diag.ToString()); break; } } } } private static Assembly? CompileAssembly(FileReference OutputAssemblyPath, HashSet SourceFileNames, List? ReferencedAssembies, List? PreprocessorDefines = null, bool TreatWarningsAsErrors = false) { CSharpParseOptions ParseOptions = new CSharpParseOptions( languageVersion:LanguageVersion.Latest, kind:SourceCodeKind.Regular, preprocessorSymbols:PreprocessorDefines ); List SyntaxTrees = new List(); foreach (FileReference SourceFileName in SourceFileNames) { SourceText Source = SourceText.From(File.ReadAllText(SourceFileName.FullName), System.Text.Encoding.UTF8); SyntaxTree Tree = CSharpSyntaxTree.ParseText(Source, ParseOptions, SourceFileName.FullName); IEnumerable Diagnostics = Tree.GetDiagnostics(); if (Diagnostics.Any()) { Log.TraceWarning($"Errors generated while parsing '{SourceFileName.FullName}'"); LogDiagnostics(Tree.GetDiagnostics()); return null; } SyntaxTrees.Add(Tree); } // Create the output directory if it doesn't exist already DirectoryInfo DirInfo = new DirectoryInfo(OutputAssemblyPath.Directory.FullName); if (!DirInfo.Exists) { try { DirInfo.Create(); } catch (Exception Ex) { throw new BuildException(Ex, "Unable to create directory '{0}' for intermediate assemblies (Exception: {1})", OutputAssemblyPath, Ex.Message); } } List MetadataReferences = new List(); if (ReferencedAssembies != null) { foreach (string Reference in ReferencedAssembies) { MetadataReferences.Add(MetadataReference.CreateFromFile(Reference)); } } MetadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Collections").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.IO").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.IO.FileSystem").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Linq").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Private.Xml").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Private.Xml.Linq").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Console").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Runtime.Extensions").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location)); // process start dependencies MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.ComponentModel.Primitives").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Diagnostics.Process").Location)); // registry access MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("Microsoft.Win32.Registry").Location)); // RNGCryptoServiceProvider, used to generate random hex bytes MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Security.Cryptography.Algorithms").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(Assembly.Load("System.Security.Cryptography.Csp").Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(typeof(UnrealBuildTool).Assembly.Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(typeof(FileReference).Assembly.Location)); MetadataReferences.Add(MetadataReference.CreateFromFile(typeof(UEBuildPlatformSDK).Assembly.Location)); CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions( outputKind:OutputKind.DynamicallyLinkedLibrary, #if DEBUG optimizationLevel: OptimizationLevel.Debug, #else // Optimize the managed code in Development optimizationLevel: OptimizationLevel.Release, #endif warningLevel:4, assemblyIdentityComparer:DesktopAssemblyIdentityComparer.Default, reportSuppressedDiagnostics:true ); CSharpCompilation Compilation = CSharpCompilation.Create( assemblyName:OutputAssemblyPath.GetFileNameWithoutAnyExtensions(), syntaxTrees:SyntaxTrees, references:MetadataReferences, options:CompilationOptions ); using (FileStream AssemblyStream = FileReference.Open(OutputAssemblyPath, FileMode.Create)) { using (FileStream? PdbStream = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)? FileReference.Open(OutputAssemblyPath.ChangeExtension(".pdb"), FileMode.Create) : null) { EmitOptions EmitOptions = new EmitOptions( includePrivateMembers: true ); EmitResult Result = Compilation.Emit( peStream: AssemblyStream, pdbStream: PdbStream, options: EmitOptions); LogDiagnostics(Result.Diagnostics); if (!Result.Success) { return null; } } } return Assembly.LoadFile(OutputAssemblyPath.FullName); } /// /// Dynamically compiles an assembly for the specified source file and loads that assembly into the application's /// current domain. If an assembly has already been compiled and is not out of date, then it will be loaded and /// no compilation is necessary. /// /// Full path to the assembly to be created /// List of source file name /// /// /// /// /// /// The assembly that was loaded public static Assembly? CompileAndLoadAssembly(FileReference OutputAssemblyPath, HashSet SourceFileNames, List? ReferencedAssembies = null, List? PreprocessorDefines = null, bool DoNotCompile = false, bool ForceCompile = false, bool TreatWarningsAsErrors = false) { // Check to see if the resulting assembly is compiled and up to date FileReference AssemblyManifestFilePath = FileReference.Combine(OutputAssemblyPath.Directory, Path.GetFileNameWithoutExtension(OutputAssemblyPath.FullName) + "Manifest.json"); bool bNeedsCompilation = ForceCompile; if (!DoNotCompile) { bNeedsCompilation = RequiresCompilation(SourceFileNames, AssemblyManifestFilePath, OutputAssemblyPath); } // Load the assembly to ensure it is correct Assembly? CompiledAssembly = null; if (!bNeedsCompilation) { try { // Load the previously-compiled assembly from disk CompiledAssembly = Assembly.LoadFile(OutputAssemblyPath.FullName); } catch (FileLoadException Ex) { Log.TraceInformation(String.Format("Unable to load the previously-compiled assembly file '{0}'. Unreal Build Tool will try to recompile this assembly now. (Exception: {1})", OutputAssemblyPath, Ex.Message)); bNeedsCompilation = true; } catch (BadImageFormatException Ex) { Log.TraceInformation(String.Format("Compiled assembly file '{0}' appears to be for a newer CLR version or is otherwise invalid. Unreal Build Tool will try to recompile this assembly now. (Exception: {1})", OutputAssemblyPath, Ex.Message)); bNeedsCompilation = true; } catch (FileNotFoundException) { throw new BuildException("Precompiled rules assembly '{0}' does not exist.", OutputAssemblyPath); } catch (Exception Ex) { throw new BuildException(Ex, "Error while loading previously-compiled assembly file '{0}'. (Exception: {1})", OutputAssemblyPath, Ex.Message); } } // Compile the assembly if me if (bNeedsCompilation) { using (GlobalTracer.Instance.BuildSpan(String.Format("Compiling rules assembly ({0})", OutputAssemblyPath.GetFileName())).StartActive()) { CompiledAssembly = CompileAssembly(OutputAssemblyPath, SourceFileNames, ReferencedAssembies, PreprocessorDefines, TreatWarningsAsErrors); } using (JsonWriter Writer = new JsonWriter(AssemblyManifestFilePath)) { ReadOnlyBuildVersion Version = ReadOnlyBuildVersion.Current; Writer.WriteObjectStart(); // Save out a list of all the source files we compiled. This is so that we can tell if whole files were added or removed // since the previous time we compiled the assembly. In that case, we'll always want to recompile it! Writer.WriteStringArrayField("SourceFiles", SourceFileNames.Select(x => x.FullName)); Writer.WriteValue("EngineVersion", FormatVersionNumber(Version)); Writer.WriteObjectEnd(); } } return CompiledAssembly; } private static string FormatVersionNumber(ReadOnlyBuildVersion Version) { return string.Format("{0}.{1}.{2}", Version.MajorVersion, Version.MinorVersion, Version.PatchVersion); } } }