From 7e80a4fe0fcc0591238baa0e0cd7e343a70141ac Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Thu, 7 Jul 2022 16:39:08 +0300 Subject: [PATCH] Use Roslyn C#-compiler for build rules compilation --- Source/Tools/Flax.Build/Build/Assembler.cs | 141 +++++++++++++----- .../Tools/Flax.Build/Build/Builder.Rules.cs | 4 +- 2 files changed, 103 insertions(+), 42 deletions(-) diff --git a/Source/Tools/Flax.Build/Build/Assembler.cs b/Source/Tools/Flax.Build/Build/Assembler.cs index 0ac982667..3930d25f1 100644 --- a/Source/Tools/Flax.Build/Build/Assembler.cs +++ b/Source/Tools/Flax.Build/Build/Assembler.cs @@ -1,10 +1,13 @@ // Copyright (c) 2012-2020 Flax Engine. All rights reserved. -using System; -using System.CodeDom.Compiler; -using System.Collections.Generic; -using System.Linq; +using Microsoft.CodeAnalysis; +using System.Diagnostics; using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; namespace Flax.Build { @@ -18,7 +21,7 @@ namespace Flax.Build /// public static readonly Assembly[] DefaultReferences = { - //typeof(IntPtr).Assembly, // mscorlib.dll + typeof(IntPtr).Assembly, // mscorlib.dll typeof(Enumerable).Assembly, // System.Linq.dll typeof(ISet<>).Assembly, // System.dll typeof(Builder).Assembly, // Flax.Build.exe @@ -29,6 +32,13 @@ namespace Flax.Build /// public string OutputPath = null; + /// + /// + /// + public string CachePath = null; + + private string CacheAssemblyPath = null; + /// /// The source files for compilation. /// @@ -44,6 +54,13 @@ namespace Flax.Build /// public readonly List References = new List(); + public Assembler(List sourceFiles, string cachePath = null) + { + SourceFiles.AddRange(sourceFiles); + CachePath = cachePath; + CacheAssemblyPath = cachePath != null ? Path.Combine(Directory.GetParent(cachePath).FullName, "BuilderRulesCache.dll") : null; + } + /// /// Builds the assembly. /// @@ -51,9 +68,28 @@ namespace Flax.Build /// The created and loaded assembly. public Assembly Build() { - Dictionary providerOptions = new Dictionary(); - providerOptions.Add("CompilerVersion", "v4.0"); - CodeDomProvider provider = new Microsoft.CSharp.CSharpCodeProvider(providerOptions); + DateTime recentWriteTime = DateTime.MinValue; + if (CachePath != null) + { + foreach (var sourceFile in SourceFiles) + { + // FIXME: compare and cache individual write times! + DateTime lastWriteTime = File.GetLastWriteTime(sourceFile); + if (lastWriteTime > recentWriteTime) + recentWriteTime = lastWriteTime; + } + + DateTime cacheTime = File.Exists(CachePath) + ? DateTime.FromBinary(long.Parse(File.ReadAllText(CachePath))) + : DateTime.MinValue; + if (recentWriteTime <= cacheTime && File.Exists(CacheAssemblyPath)) + { + Assembly cachedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(CacheAssemblyPath); + return cachedAssembly; + } + } + + Stopwatch sw = Stopwatch.StartNew(); // Collect references HashSet references = new HashSet(); @@ -66,49 +102,74 @@ namespace Flax.Build if (!assembly.IsDynamic) references.Add(assembly.Location); } - - // Setup compilation options - CompilerParameters cp = new CompilerParameters(); - cp.GenerateExecutable = false; - cp.WarningLevel = 4; - cp.TreatWarningsAsErrors = false; - cp.ReferencedAssemblies.AddRange(references.ToArray()); - if (string.IsNullOrEmpty(OutputPath)) - { - cp.GenerateInMemory = true; - cp.IncludeDebugInformation = false; - } - else - { - cp.GenerateInMemory = false; - cp.IncludeDebugInformation = true; - cp.OutputAssembly = OutputPath; - } + references.Add(Path.Combine(Directory.GetParent(DefaultReferences[0].Location).FullName, "System.dll")); + references.Add(Path.Combine(Directory.GetParent(DefaultReferences[0].Location).FullName, "System.Runtime.dll")); + references.Add(Path.Combine(Directory.GetParent(DefaultReferences[0].Location).FullName, "System.Collections.dll")); + references.Add(Path.Combine(Directory.GetParent(DefaultReferences[0].Location).FullName, "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 defaultReferences = new List(); + foreach (var r in references) + defaultReferences.Add(MetadataReference.CreateFromFile(r)); + // Run the compilation - CompilerResults cr = provider.CompileAssemblyFromFile(cp, SourceFiles.ToArray()); + using var memoryStream = new MemoryStream(); + + var syntaxTrees = new List(); + foreach (var sourceFile in SourceFiles) + { + var stringText = SourceText.From(File.ReadAllText(sourceFile), Encoding.UTF8); + var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(stringText, + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp9), sourceFile); + syntaxTrees.Add(parsedSyntaxTree); + } + + var compilation = CSharpCompilation.Create("BuilderRulesCache.dll", syntaxTrees.ToArray(), defaultReferences, defaultCompilationOptions); + EmitResult emitResult = compilation.Emit(memoryStream); // Process warnings and errors - bool hasError = false; - foreach (CompilerError ce in cr.Errors) + foreach (var diagnostic in emitResult.Diagnostics) { - if (ce.IsWarning) - { - Log.Warning(string.Format("{0} at {1}: {2}", ce.FileName, ce.Line, ce.ErrorText)); - } - else - { - Log.Error(string.Format("{0} at line {1}: {2}", ce.FileName, ce.Line, ce.ErrorText)); - hasError = true; - } + var msg = diagnostic.ToString(); + if (diagnostic.Severity == DiagnosticSeverity.Warning) + Log.Warning(msg); + else if (diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error) + Log.Error(msg); } - if (hasError) + if (!emitResult.Success) throw new Exception("Failed to build assembly."); - return cr.CompiledAssembly; + memoryStream.Seek(0, SeekOrigin.Begin); + Assembly compiledAssembly = AssemblyLoadContext.Default.LoadFromStream(memoryStream); + + if (CachePath != null && CacheAssemblyPath != null) + { + memoryStream.Seek(0, SeekOrigin.Begin); + + var cacheDirectory = Path.GetDirectoryName(CacheAssemblyPath); + if (!Directory.Exists(cacheDirectory)) + Directory.CreateDirectory(cacheDirectory); + + using (FileStream fileStream = File.Open(CacheAssemblyPath, FileMode.Create, FileAccess.Write)) + { + memoryStream.CopyTo(fileStream); + fileStream.Close(); + } + + File.WriteAllText(CachePath, recentWriteTime.ToBinary().ToString()); + } + + sw.Stop(); + Log.Info("Assembler time: " + sw.Elapsed.TotalSeconds.ToString() + "s"); + return compiledAssembly; } } } diff --git a/Source/Tools/Flax.Build/Build/Builder.Rules.cs b/Source/Tools/Flax.Build/Build/Builder.Rules.cs index 9470e3e19..43666837d 100644 --- a/Source/Tools/Flax.Build/Build/Builder.Rules.cs +++ b/Source/Tools/Flax.Build/Build/Builder.Rules.cs @@ -188,8 +188,8 @@ namespace Flax.Build Assembly assembly; using (new ProfileEventScope("CompileRules")) { - var assembler = new Assembler(); - assembler.SourceFiles.AddRange(files); + var assembler = new Assembler(files, Path.Combine(Globals.Root, Configuration.IntermediateFolder, "BuilderRules.cache")); + //var assembler = new Assembler(files); assembly = assembler.Build(); }