2018-12-17 06:31:16 -05:00
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
2014-03-14 14:13:41 -04:00
using System ;
using System.Collections.Generic ;
using System.IO ;
2015-09-03 08:47:24 -04:00
using System.Linq ;
2014-03-14 14:13:41 -04:00
using System.CodeDom.Compiler ;
using Microsoft.CSharp ;
using System.Reflection ;
using System.Diagnostics ;
2015-07-17 16:38:17 -04:00
using Tools.DotNETCommon ;
2014-03-14 14:13:41 -04:00
2017-08-31 12:08:38 -04:00
#if NET_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 ;
#endif
2014-03-14 14:13:41 -04:00
namespace UnrealBuildTool
{
2017-01-30 16:52:08 -05:00
/// <summary>
/// Methods for dynamically compiling C# source files
/// </summary>
2014-03-14 14:13:41 -04:00
public class DynamicCompilation
{
2019-01-22 06:48:04 -05:00
/// <summary>
/// Checks to see if the assembly needs compilation
/// </summary>
/// <param name="SourceFiles">Set of source files</param>
/// <param name="AssemblySourceListFilePath">File to use to cache source file names</param>
/// <param name="OutputAssemblyPath">Output path for the assembly</param>
/// <returns>True if the assembly needs to be built</returns>
private static bool RequiresCompilation ( HashSet < FileReference > SourceFiles , FileReference AssemblySourceListFilePath , FileReference OutputAssemblyPath )
2014-03-14 14:13:41 -04:00
{
// Check to see if we already have a compiled assembly file on disk
2019-01-22 06:48:04 -05:00
FileItem OutputAssemblyInfo = FileItem . GetItemByFileReference ( OutputAssemblyPath ) ;
if ( ! OutputAssemblyInfo . Exists )
2014-03-14 14:13:41 -04:00
{
2019-01-22 06:48:04 -05:00
Log . TraceLog ( "Compiling {0}: Assembly does not exist" , OutputAssemblyPath ) ;
return true ;
}
2014-03-14 14:13:41 -04:00
2019-01-22 06:48:04 -05:00
// Check the time stamp of the UnrealBuildTool.exe file. 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 ( AssemblySourceListFilePath ) ;
if ( ! AssemblySourceListFile . Exists )
{
Log . TraceLog ( "Compiling {0}: Missing source file list ({1})" , OutputAssemblyPath , AssemblySourceListFilePath ) ;
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 < FileItem > CurrentSourceFileItems = new HashSet < FileItem > ( ) ;
foreach ( string Line in FileReference . ReadAllLines ( AssemblySourceListFile . Location ) )
{
CurrentSourceFileItems . Add ( FileItem . GetItemByPath ( Line ) ) ;
}
// Get the new source files
HashSet < FileItem > SourceFileItems = new HashSet < FileItem > ( ) ;
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 , AssemblySourceListFilePath ) ;
2014-03-14 14:13:41 -04:00
return true ;
}
2019-01-22 06:48:04 -05:00
}
foreach ( FileItem SourceFileItem in SourceFileItems )
{
if ( ! CurrentSourceFileItems . Contains ( SourceFileItem ) )
2014-03-14 14:13:41 -04:00
{
2019-01-22 06:48:04 -05:00
Log . TraceLog ( "Compiling {0}: Added source file ({1})" , OutputAssemblyPath , AssemblySourceListFilePath ) ;
return true ;
2014-03-14 14:13:41 -04:00
}
}
2019-01-22 06:48:04 -05:00
// Check if any of the timestamps are newer
foreach ( FileItem SourceFileItem in SourceFileItems )
2014-03-14 14:13:41 -04:00
{
2019-01-22 06:48:04 -05:00
if ( SourceFileItem . LastWriteTimeUtc > OutputAssemblyInfo . LastWriteTimeUtc )
{
Log . TraceLog ( "Compiling {0}: {1} is newer" , OutputAssemblyPath , SourceFileItem ) ;
return true ;
}
2014-03-14 14:13:41 -04:00
}
return false ;
}
2017-08-31 12:08:38 -04:00
#if NET_CORE
private static void LogDiagnostics ( IEnumerable < Diagnostic > Diagnostics )
{
foreach ( Diagnostic Diag in Diagnostics )
{
switch ( Diag . Severity )
{
case DiagnosticSeverity . Error :
{
Log . TraceError ( Diag . ToString ( ) ) ;
break ;
}
case DiagnosticSeverity . Hidden :
{
break ;
}
case DiagnosticSeverity . Warning :
{
Log . TraceWarning ( Diag . ToString ( ) ) ;
break ;
}
case DiagnosticSeverity . Info :
{
Log . TraceInformation ( Diag . ToString ( ) ) ;
break ;
}
}
}
}
private static Assembly CompileAssembly ( FileReference OutputAssemblyPath , List < FileReference > SourceFileNames , List < string > ReferencedAssembies , List < string > PreprocessorDefines = null , bool TreatWarningsAsErrors = false )
{
CSharpParseOptions ParseOptions = new CSharpParseOptions (
languageVersion : LanguageVersion . Latest ,
kind : SourceCodeKind . Regular ,
preprocessorSymbols : PreprocessorDefines
) ;
List < SyntaxTree > SyntaxTrees = new List < SyntaxTree > ( ) ;
foreach ( FileReference SourceFileName in SourceFileNames )
{
SourceText Source = SourceText . From ( File . ReadAllText ( SourceFileName . FullName ) ) ;
SyntaxTree Tree = CSharpSyntaxTree . ParseText ( Source , ParseOptions , SourceFileName . FullName ) ;
IEnumerable < Diagnostic > Diagnostics = Tree . GetDiagnostics ( ) ;
if ( Diagnostics . Count ( ) > 0 )
{
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 < MetadataReference > MetadataReferences = new List < MetadataReference > ( ) ;
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.Console" ) . Location ) ) ;
MetadataReferences . Add ( MetadataReference . CreateFromFile ( Assembly . Load ( "System.Runtime.Extensions" ) . Location ) ) ;
MetadataReferences . Add ( MetadataReference . CreateFromFile ( Assembly . Load ( "Microsoft.Win32.Registry" ) . Location ) ) ;
MetadataReferences . Add ( MetadataReference . CreateFromFile ( typeof ( UnrealBuildTool ) . Assembly . Location ) ) ;
MetadataReferences . Add ( MetadataReference . CreateFromFile ( typeof ( FileReference ) . Assembly . Location ) ) ;
CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions (
outputKind : OutputKind . DynamicallyLinkedLibrary ,
optimizationLevel : OptimizationLevel . Release ,
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 ) )
{
EmitOptions EmitOptions = new EmitOptions (
includePrivateMembers : true
) ;
EmitResult Result = Compilation . Emit (
peStream : AssemblyStream ,
options : EmitOptions ) ;
if ( ! Result . Success )
{
LogDiagnostics ( Result . Diagnostics ) ;
return null ;
}
}
return Assembly . LoadFile ( OutputAssemblyPath . FullName ) ;
}
#else
2019-01-22 06:48:04 -05:00
private static Assembly CompileAssembly ( FileReference OutputAssemblyPath , HashSet < FileReference > SourceFileNames , List < string > ReferencedAssembies , List < string > PreprocessorDefines = null , bool TreatWarningsAsErrors = false )
2014-03-14 14:13:41 -04:00
{
2016-03-08 09:00:48 -05:00
TempFileCollection TemporaryFiles = new TempFileCollection ( ) ;
2014-03-14 14:13:41 -04:00
// Setup compile parameters
2016-03-08 09:00:48 -05:00
CompilerParameters CompileParams = new CompilerParameters ( ) ;
2014-03-14 14:13:41 -04:00
{
// Always compile the assembly to a file on disk, so that we can load a cached version later if we have one
CompileParams . GenerateInMemory = false ;
// This is the full path to the assembly file we're generating
2015-09-03 08:47:24 -04:00
CompileParams . OutputAssembly = OutputAssemblyPath . FullName ;
2014-03-14 14:13:41 -04:00
// We always want to generate a class library, not an executable
CompileParams . GenerateExecutable = false ;
2015-10-09 15:13:41 -04:00
// Never fail compiles for warnings
CompileParams . TreatWarningsAsErrors = false ;
2014-03-14 14:13:41 -04:00
2016-02-03 15:40:40 -05:00
// Set the warning level so that we will actually receive warnings -
// doesn't abort compilation as stated in documentation!
CompileParams . WarningLevel = 4 ;
2014-03-14 14:13:41 -04:00
// Always generate debug information as it takes minimal time
CompileParams . IncludeDebugInformation = true ;
#if ! DEBUG
// Optimise the managed code in Development
CompileParams . CompilerOptions + = " /optimize" ;
#endif
Log . TraceVerbose ( "Compiling " + OutputAssemblyPath ) ;
// Keep track of temporary files emitted by the compiler so we can clean them up later
CompileParams . TempFiles = TemporaryFiles ;
// Warnings as errors if desired
CompileParams . TreatWarningsAsErrors = TreatWarningsAsErrors ;
// Add assembly references
{
if ( ReferencedAssembies = = null )
{
// Always depend on the CLR System assembly
CompileParams . ReferencedAssemblies . Add ( "System.dll" ) ;
}
else
{
// Add in the set of passed in referenced assemblies
CompileParams . ReferencedAssemblies . AddRange ( ReferencedAssembies . ToArray ( ) ) ;
}
// The assembly will depend on this application
2016-03-08 09:00:48 -05:00
Assembly UnrealBuildToolAssembly = Assembly . GetExecutingAssembly ( ) ;
2014-03-14 14:13:41 -04:00
CompileParams . ReferencedAssemblies . Add ( UnrealBuildToolAssembly . Location ) ;
2017-08-31 12:08:38 -04:00
// The assembly will depend on the utilities assembly. Find that assembly
// by looking for the one that contains a common utility class
Assembly UtilitiesAssembly = Assembly . GetAssembly ( typeof ( FileReference ) ) ;
CompileParams . ReferencedAssemblies . Add ( UtilitiesAssembly . Location ) ;
2014-03-14 14:13:41 -04:00
}
// Add preprocessor definitions
if ( PreprocessorDefines ! = null & & PreprocessorDefines . Count > 0 )
{
CompileParams . CompilerOptions + = " /define:" ;
for ( int DefinitionIndex = 0 ; DefinitionIndex < PreprocessorDefines . Count ; + + DefinitionIndex )
{
if ( DefinitionIndex > 0 )
{
CompileParams . CompilerOptions + = ";" ;
}
CompileParams . CompilerOptions + = PreprocessorDefines [ DefinitionIndex ] ;
}
}
// @todo: Consider embedding resources in generated assembly file (version/copyright/signing)
}
// Create the output directory if it doesn't exist already
2015-09-24 12:37:21 -04:00
DirectoryInfo DirInfo = new DirectoryInfo ( OutputAssemblyPath . Directory . FullName ) ;
if ( ! DirInfo . Exists )
2014-03-14 14:13:41 -04:00
{
try
{
DirInfo . Create ( ) ;
}
2015-09-24 12:37:21 -04:00
catch ( Exception Ex )
2014-03-14 14:13:41 -04:00
{
2015-09-24 12:37:21 -04:00
throw new BuildException ( Ex , "Unable to create directory '{0}' for intermediate assemblies (Exception: {1})" , OutputAssemblyPath , Ex . Message ) ;
2014-03-14 14:13:41 -04:00
}
}
// Compile the code
CompilerResults CompileResults ;
try
{
2016-03-08 09:00:48 -05:00
Dictionary < string , string > ProviderOptions = new Dictionary < string , string > ( ) { { "CompilerVersion" , "v4.0" } } ;
CSharpCodeProvider Compiler = new CSharpCodeProvider ( ProviderOptions ) ;
2015-09-24 12:37:21 -04:00
CompileResults = Compiler . CompileAssemblyFromFile ( CompileParams , SourceFileNames . Select ( x = > x . FullName ) . ToArray ( ) ) ;
2014-03-14 14:13:41 -04:00
}
2015-09-24 12:37:21 -04:00
catch ( Exception Ex )
2014-03-14 14:13:41 -04:00
{
2018-08-14 18:32:34 -04:00
throw new BuildException ( Ex , "Failed to launch compiler to compile assembly from source files:\n {0}\n(Exception: {1})" , String . Join ( "\n " , SourceFileNames ) , Ex . ToString ( ) ) ;
2014-03-14 14:13:41 -04:00
}
2015-10-09 15:13:41 -04:00
// Display compilation warnings and errors
2015-09-24 12:37:21 -04:00
if ( CompileResults . Errors . Count > 0 )
2014-03-14 14:13:41 -04:00
{
2017-07-21 12:42:36 -04:00
Log . TraceInformation ( "While compiling {0}:" , OutputAssemblyPath ) ;
2016-02-03 15:40:40 -05:00
foreach ( CompilerError CurError in CompileResults . Errors )
2014-03-14 14:13:41 -04:00
{
2018-01-20 11:19:29 -05:00
Log . WriteLine ( 0 , CurError . IsWarning ? LogEventType . Warning : LogEventType . Error , LogFormatOptions . NoSeverityPrefix , "{0}" , CurError . ToString ( ) ) ;
2014-03-14 14:13:41 -04:00
}
2016-02-05 11:54:00 -05:00
if ( CompileResults . Errors . HasErrors | | TreatWarningsAsErrors )
2015-10-09 15:13:41 -04:00
{
2017-07-21 12:42:36 -04:00
throw new BuildException ( "Unable to compile source files." ) ;
2015-10-09 15:13:41 -04:00
}
2014-03-14 14:13:41 -04:00
}
// Grab the generated assembly
Assembly CompiledAssembly = CompileResults . CompiledAssembly ;
2015-09-24 12:37:21 -04:00
if ( CompiledAssembly = = null )
2014-03-14 14:13:41 -04:00
{
2015-09-24 12:37:21 -04:00
throw new BuildException ( "UnrealBuildTool was unable to compile an assembly for '{0}'" , SourceFileNames . ToString ( ) ) ;
2014-03-14 14:13:41 -04:00
}
// Clean up temporary files that the compiler saved
TemporaryFiles . Delete ( ) ;
return CompiledAssembly ;
}
2017-08-31 12:08:38 -04:00
#endif
2014-03-14 14:13:41 -04:00
/// <summary>
/// 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.
/// </summary>
/// <param name="OutputAssemblyPath">Full path to the assembly to be created</param>
2017-01-30 16:52:08 -05:00
/// <param name="SourceFileNames">List of source file name</param>
/// <param name="ReferencedAssembies"></param>
/// <param name="PreprocessorDefines"></param>
/// <param name="DoNotCompile"></param>
/// <param name="TreatWarningsAsErrors"></param>
2014-03-14 14:13:41 -04:00
/// <returns>The assembly that was loaded</returns>
2019-01-22 06:48:04 -05:00
public static Assembly CompileAndLoadAssembly ( FileReference OutputAssemblyPath , HashSet < FileReference > SourceFileNames , List < string > ReferencedAssembies = null , List < string > PreprocessorDefines = null , bool DoNotCompile = false , bool TreatWarningsAsErrors = false )
2014-03-14 14:13:41 -04:00
{
// Check to see if the resulting assembly is compiled and up to date
2015-09-24 12:37:21 -04:00
FileReference AssemblySourcesListFilePath = FileReference . Combine ( OutputAssemblyPath . Directory , Path . GetFileNameWithoutExtension ( OutputAssemblyPath . FullName ) + "SourceFiles.txt" ) ;
2014-03-14 14:13:41 -04:00
bool bNeedsCompilation = false ;
if ( ! DoNotCompile )
{
bNeedsCompilation = RequiresCompilation ( SourceFileNames , AssemblySourcesListFilePath , OutputAssemblyPath ) ;
}
// Load the assembly to ensure it is correct
Assembly CompiledAssembly = null ;
2015-09-24 12:37:21 -04:00
if ( ! bNeedsCompilation )
2014-03-14 14:13:41 -04:00
{
try
{
// Load the previously-compiled assembly from disk
2015-09-24 12:37:21 -04:00
CompiledAssembly = Assembly . LoadFile ( OutputAssemblyPath . FullName ) ;
2014-03-14 14:13:41 -04:00
}
2015-09-24 12:37:21 -04:00
catch ( FileLoadException Ex )
2014-03-14 14:13:41 -04:00
{
2015-09-24 12:37:21 -04:00
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 ) ) ;
2014-03-14 14:13:41 -04:00
bNeedsCompilation = true ;
}
2015-09-24 12:37:21 -04:00
catch ( BadImageFormatException Ex )
2014-03-14 14:13:41 -04:00
{
2015-09-24 12:37:21 -04:00
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 ) ) ;
2014-03-14 14:13:41 -04:00
bNeedsCompilation = true ;
}
2018-08-14 18:32:34 -04:00
catch ( FileNotFoundException )
{
throw new BuildException ( "Precompiled rules assembly '{0}' does not exist." , OutputAssemblyPath ) ;
}
2015-09-24 12:37:21 -04:00
catch ( Exception Ex )
2014-03-14 14:13:41 -04:00
{
2015-09-24 12:37:21 -04:00
throw new BuildException ( Ex , "Error while loading previously-compiled assembly file '{0}'. (Exception: {1})" , OutputAssemblyPath , Ex . Message ) ;
2014-03-14 14:13:41 -04:00
}
}
// Compile the assembly if me
2015-09-24 12:37:21 -04:00
if ( bNeedsCompilation )
2014-03-14 14:13:41 -04:00
{
2019-01-22 06:48:04 -05:00
using ( Timeline . ScopeEvent ( String . Format ( "Compiling rules assembly ({0})" , OutputAssemblyPath . GetFileName ( ) ) ) )
{
CompiledAssembly = CompileAssembly ( OutputAssemblyPath , SourceFileNames , ReferencedAssembies , PreprocessorDefines , TreatWarningsAsErrors ) ;
}
2014-03-14 14:13:41 -04:00
// 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!
2019-01-22 06:48:04 -05:00
FileReference . WriteAllLines ( AssemblySourcesListFilePath , SourceFileNames . Select ( x = > x . FullName ) ) ;
2014-03-14 14:13:41 -04:00
}
2017-08-31 12:08:38 -04:00
#if ! NET_CORE
2014-03-14 14:13:41 -04:00
// Load the assembly into our app domain
try
{
2015-09-24 12:37:21 -04:00
AppDomain . CurrentDomain . Load ( CompiledAssembly . GetName ( ) ) ;
2014-03-14 14:13:41 -04:00
}
2015-09-24 12:37:21 -04:00
catch ( Exception Ex )
2014-03-14 14:13:41 -04:00
{
2015-09-24 12:37:21 -04:00
throw new BuildException ( Ex , "Unable to load the compiled build assembly '{0}' into our application's domain. (Exception: {1})" , OutputAssemblyPath , Ex . Message ) ;
2014-03-14 14:13:41 -04:00
}
2017-08-31 12:08:38 -04:00
#endif
2014-03-14 14:13:41 -04:00
return CompiledAssembly ;
}
}
}