216 lines
9.2 KiB
C#
216 lines
9.2 KiB
C#
// Copyright (c) 2012-2024 Flax Engine. All rights reserved.
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Linq;
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Runtime.Loader;
|
|
using System.Collections.Generic;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.Emit;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
|
|
namespace Flax.Build
|
|
{
|
|
/// <summary>
|
|
/// Utility for building C# assemblies from custom set of source files.
|
|
/// </summary>
|
|
public class Assembler
|
|
{
|
|
internal static string CacheFileName = "BuilderRules.dll";
|
|
private string _cacheFolderPath;
|
|
|
|
/// <summary>
|
|
/// The default assembly references added to the projects.
|
|
/// </summary>
|
|
public static readonly Assembly[] DefaultReferences =
|
|
{
|
|
typeof(IntPtr).Assembly, // mscorlib.dll
|
|
typeof(Enumerable).Assembly, // System.Linq.dll
|
|
typeof(ISet<>).Assembly, // System.dll
|
|
typeof(Builder).Assembly, // Flax.Build.dll
|
|
};
|
|
|
|
/// <summary>
|
|
/// The source files for compilation.
|
|
/// </summary>
|
|
public readonly List<string> SourceFiles = new List<string>();
|
|
|
|
/// <summary>
|
|
/// The external user assembly references to use while compiling.
|
|
/// </summary>
|
|
public readonly List<Assembly> Assemblies = new List<Assembly>();
|
|
|
|
/// <summary>
|
|
/// The external user assembly file names to use while compiling.
|
|
/// </summary>
|
|
public readonly List<string> References = new List<string>();
|
|
|
|
/// <summary>
|
|
/// The C# preprocessor symbols to use while compiling.
|
|
/// </summary>
|
|
public readonly List<string> PreprocessorSymbols = new List<string>();
|
|
|
|
public Assembler(List<string> sourceFiles, string cacheFolderPath = null)
|
|
{
|
|
SourceFiles.AddRange(sourceFiles);
|
|
_cacheFolderPath = cacheFolderPath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the assembly.
|
|
/// </summary>
|
|
/// <remarks>Throws an exception in case of any errors.</remarks>
|
|
/// <returns>The created and loaded assembly.</returns>
|
|
public Assembly Build()
|
|
{
|
|
DateTime recentWriteTime = DateTime.MinValue;
|
|
string cacheAssemblyPath = null, cacheInfoPath = null, buildInfo = null;
|
|
if (_cacheFolderPath != null)
|
|
{
|
|
cacheAssemblyPath = Path.Combine(_cacheFolderPath, CacheFileName);
|
|
cacheInfoPath = Path.Combine(_cacheFolderPath, "BuilderRulesInfo.txt");
|
|
|
|
foreach (var sourceFile in SourceFiles)
|
|
{
|
|
// FIXME: compare and cache individual write times!
|
|
DateTime lastWriteTime = File.GetLastWriteTime(sourceFile);
|
|
if (lastWriteTime > recentWriteTime)
|
|
recentWriteTime = lastWriteTime;
|
|
}
|
|
|
|
// Include build tool version (eg. skip using cached assembly after editing build tool)
|
|
var executingAssembly = Assembly.GetExecutingAssembly();
|
|
var executingAssemblyLocation = executingAssembly.Location;
|
|
{
|
|
DateTime lastWriteTime = File.GetLastWriteTime(executingAssemblyLocation);
|
|
if (lastWriteTime > recentWriteTime)
|
|
recentWriteTime = lastWriteTime;
|
|
}
|
|
|
|
// Skip when project references were changed
|
|
if (Globals.Project != null)
|
|
{
|
|
DateTime lastWriteTime = File.GetLastWriteTime(Globals.Project.ProjectPath);
|
|
if (lastWriteTime > recentWriteTime)
|
|
recentWriteTime = lastWriteTime;
|
|
}
|
|
|
|
// Construct current configuration and runtime info to be cached alongside with compiled assembly so the build rules are cached only when using the same .NET/Flax.Build/etc.
|
|
buildInfo = Globals.EngineRoot + ';' + System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription + ';' + executingAssemblyLocation;
|
|
|
|
// Check if cache files exist
|
|
if (File.Exists(cacheAssemblyPath) && File.Exists(cacheInfoPath))
|
|
{
|
|
var lines = File.ReadAllLines(cacheInfoPath);
|
|
if (lines.Length == 3 &&
|
|
long.TryParse(lines[0], out var cacheTimeTicks) &&
|
|
string.Equals(buildInfo, lines[1], StringComparison.Ordinal) &&
|
|
!lines[2].Split(';').Except(SourceFiles).Any())
|
|
{
|
|
// Cached time and
|
|
var cacheTime = DateTime.FromBinary(cacheTimeTicks);
|
|
if (recentWriteTime <= cacheTime)
|
|
{
|
|
// use cached assembly
|
|
Assembly cachedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(cacheAssemblyPath);
|
|
return cachedAssembly;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Stopwatch sw = Stopwatch.StartNew();
|
|
|
|
// Collect references
|
|
HashSet<string> references = new HashSet<string>();
|
|
foreach (var defaultReference in DefaultReferences)
|
|
references.Add(defaultReference.Location);
|
|
foreach (var assemblyFile in References)
|
|
references.Add(assemblyFile);
|
|
foreach (var assembly in Assemblies)
|
|
{
|
|
if (!assembly.IsDynamic)
|
|
references.Add(assembly.Location);
|
|
}
|
|
var stdLibPath = Directory.GetParent(DefaultReferences[0].Location).FullName;
|
|
references.Add(Path.Combine(stdLibPath, "System.dll"));
|
|
references.Add(Path.Combine(stdLibPath, "System.Runtime.dll"));
|
|
references.Add(Path.Combine(stdLibPath, "System.Collections.dll"));
|
|
references.Add(Path.Combine(stdLibPath, "Microsoft.Win32.Registry.dll"));
|
|
|
|
// HACK: C# will give compilation errors if a LIB variable contains non-existing directories
|
|
Environment.SetEnvironmentVariable("LIB", null);
|
|
|
|
CSharpCompilationOptions defaultCompilationOptions =
|
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
|
.WithUsings(new[] { "System", })
|
|
.WithPlatform(Microsoft.CodeAnalysis.Platform.AnyCpu)
|
|
.WithOptimizationLevel(OptimizationLevel.Debug);
|
|
|
|
List<MetadataReference> defaultReferences = new List<MetadataReference>();
|
|
foreach (var r in references)
|
|
defaultReferences.Add(MetadataReference.CreateFromFile(r));
|
|
|
|
// Run the compilation
|
|
using var memoryStream = new MemoryStream();
|
|
CSharpParseOptions parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest).WithPreprocessorSymbols(PreprocessorSymbols);
|
|
var syntaxTrees = new List<SyntaxTree>();
|
|
foreach (var sourceFile in SourceFiles)
|
|
{
|
|
var stringText = SourceText.From(File.ReadAllText(sourceFile), Encoding.UTF8);
|
|
var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(stringText, parseOptions, sourceFile);
|
|
syntaxTrees.Add(parsedSyntaxTree);
|
|
}
|
|
|
|
var compilation = CSharpCompilation.Create(CacheFileName, syntaxTrees.ToArray(), defaultReferences, defaultCompilationOptions);
|
|
EmitResult emitResult = compilation.Emit(memoryStream);
|
|
|
|
// Process warnings and errors
|
|
foreach (var diagnostic in emitResult.Diagnostics)
|
|
{
|
|
var msg = diagnostic.ToString();
|
|
if (diagnostic.Severity == DiagnosticSeverity.Warning)
|
|
Log.Warning(msg);
|
|
else if (diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error)
|
|
Log.Error(msg);
|
|
}
|
|
if (!emitResult.Success)
|
|
throw new Exception("Failed to build assembly.");
|
|
|
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
|
Assembly compiledAssembly = AssemblyLoadContext.Default.LoadFromStream(memoryStream);
|
|
|
|
if (_cacheFolderPath != null)
|
|
{
|
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
if (!Directory.Exists(_cacheFolderPath))
|
|
Directory.CreateDirectory(_cacheFolderPath);
|
|
|
|
// Save assembly to cache file
|
|
using (FileStream fileStream = File.Open(cacheAssemblyPath, FileMode.Create, FileAccess.Write))
|
|
{
|
|
memoryStream.CopyTo(fileStream);
|
|
fileStream.Close();
|
|
}
|
|
|
|
// Save build info to cache file
|
|
File.WriteAllLines(cacheInfoPath, new[]
|
|
{
|
|
recentWriteTime.ToBinary().ToString(),
|
|
buildInfo,
|
|
string.Join(';', SourceFiles)
|
|
});
|
|
}
|
|
|
|
sw.Stop();
|
|
Log.Verbose("Assembler time: " + sw.Elapsed.TotalSeconds.ToString() + "s");
|
|
return compiledAssembly;
|
|
}
|
|
}
|
|
}
|