using Microsoft.CodeAnalysis; using System.Threading; namespace Sandbox; [SkipHotload] public class CompileGroup : IDisposable { Logger log; /// /// Build notifications start of suppressed until after startup proper. That way /// we don't get 4 build notification windows popping up on startup. /// public static bool SuppressBuildNotifications { get; set; } = true; /// /// The compilers within the group /// public IEnumerable Compilers => _compilers; /// /// The name of this compile group, for debugging/display purposes /// public string Name { get; set; } = "Compile Group"; /// /// Returns true if we have compiles pending /// public bool NeedsBuild => _recompileList.Count() > 0; /// /// Returns true if we are currently in the process of building /// public bool IsBuilding { get; private set; } /// /// True if we want to print errors in the console when compiling /// public bool PrintErrorsInConsole { get; set; } = true; /// /// True if we want to use fast hotloading with this compile group /// public bool AllowFastHotload { get; set; } = false; /// /// Returns true if build was successful /// public Results BuildResult { get; private set; } /// /// Called when a compiling starts /// public Action OnCompileStarted { get; set; } /// /// Called when a compiling ends /// public Action OnCompileFinished { get; set; } /// /// Called when a compile completes successfully. Can access the result from BuildResult. /// public Action OnCompileSuccess { get; set; } /// /// All created compilers. /// List _compilers = new List(); /// /// Compilers waiting for recompile /// HashSet _recompileList = new(); /// /// Allows providing an external way to find references /// public ICompileReferenceProvider ReferenceProvider { get; set; } /// /// AccessControl instance to use when verifying whitelist. Must be set to enable compile-time access control. /// public AccessControl AccessControl { get; set; } public CompileGroup( string name ) { log = new Logger( $"CompileGroup/{name}" ); Name = name; log.Trace( "Created" ); } /// /// Shut everything down /// public void Dispose() { _compilers.Clear(); _recompileList.Clear(); log.Trace( "Dispose" ); } /// /// Create a new compiler in this group. /// public Compiler CreateCompiler( string name, string path, Compiler.Configuration settings ) { if ( FindCompilerByPackageName( name ) != null ) throw new System.Exception( $"Compiler named {name} already exists" ); log.Trace( $"CreateCompiler '{name}' ({path})" ); var compiler = new Compiler( this, name, path, settings ); compiler.MarkForRecompile(); lock ( _compilers ) { _compilers.Add( compiler ); } return compiler; } public Compiler GetOrCreateCompiler( string name ) { var compiler = FindCompilerByPackageName( name ); if ( compiler is not null ) return compiler; log.Trace( $"CreateCompiler '{name}'" ); compiler = new Compiler( this, name ); lock ( _compilers ) { _compilers.Add( compiler ); } return compiler; } /// /// Mark this assembly as changed. /// internal void MarkForRecompile( Compiler compiler ) { if ( _recompileList.Add( compiler ) ) { log.Trace( $"MarkForRecompile ({compiler.Name})" ); } } /// /// Returns true if this group will compile next build. /// internal bool CompilerNeedsBuild( Compiler compiler ) => _recompileList.Contains( compiler ); /// /// Recompile anything that depends on us too /// void MarkDependantsForRecompile( Compiler compiler, HashSet found = null ) { found ??= new(); if ( !found.Add( compiler ) ) return; // If we're compiling base.dll, // then we need to compile sandbox.dll too foreach ( var c in _compilers ) { if ( c == compiler ) continue; if ( !c.HasReference( compiler.AssemblyName, true ) ) continue; MarkForRecompile( c ); MarkDependantsForRecompile( c, found ); } } internal void OnCompilerDisposed( Compiler compiler ) { lock ( _compilers ) { log.Trace( $"Compiler Disposed ({compiler.Name})" ); _compilers.Remove( compiler ); _recompileList.Remove( compiler ); } } internal Compiler FindCompilerByAssemblyName( string assemblyName ) { lock ( _compilers ) { return _compilers.SingleOrDefault( x => String.Equals( x.AssemblyName, assemblyName, StringComparison.OrdinalIgnoreCase ) ); } } internal Compiler FindCompilerByPackageName( string packageName ) { lock ( _compilers ) { return _compilers.SingleOrDefault( x => String.Equals( x.Name, packageName, StringComparison.OrdinalIgnoreCase ) ); } } /// /// Build the compilers /// public async Task BuildAsync() { if ( IsBuilding ) throw new System.Exception( "Tried to build but a build is already in process" ); log.Trace( $"BuildAsync Start" ); if ( !NeedsBuild ) { log.Trace( $"BuildAsync Finish - no build needed" ); return BuildResult.Success; } IsBuilding = true; BuildResult = default; Results result = default; result.Failed = true; result.Diagnostics = new(); result.Output = new(); BuildResult = result; if ( !NeedsBuild ) { // give the impression that everything is fine result.Failed = false; BuildResult = result; IsBuilding = false; log.Trace( $"BuildAsync Finish - no build needed" ); return true; } try { var timer = Stopwatch.StartNew(); // // Make sure all dependancies of the compilers we're compiling and marked to compile too // This will also throw an exception if we have cyclic dependancies // foreach ( var compiler in _recompileList.ToArray() ) { MarkDependantsForRecompile( compiler ); } // // When compiling we want to include the out of date compiler and everything it references. // The other compilers that aren't out of date won't be compiled - but they will get returned // as part of the output. // var compileList = _recompileList.SelectMany( x => x.GetReferencedCompilers() ).Distinct().ToList(); var toCompile = compileList.Where( x => x.NeedsBuild ).ToArray(); // Clear this now so that if something needs recompiling while we're compiling // it'll add to the list and recompile in the next run _recompileList.Clear(); if ( compileList.Count == 0 ) throw new System.Exception( "Compile list is empty - this should never happen (NeedsBuild check should prevent it)" ); log.Trace( $"Building {compileList.Count()} compilers" ); OnCompileStarted.InvokeWithWarning(); // Reset each compiler so they're ready to reference each other during the build foreach ( var compiler in toCompile ) { compiler.PreBuild(); } // Do the actual build, let compilers wait for each other as needed await Task.WhenAll( toCompile.Select( x => x.BuildAsync() ) ); // // Accumulate the build result // bool allSuccess = compileList.All( x => x.BuildResult?.Success ?? false ); result.Failed = !allSuccess; foreach ( var compiler in toCompile.OrderBy( x => x.DependencyIndex() ) ) { if ( compiler.Output is null ) continue; result.Diagnostics.AddRange( compiler.Output.Diagnostics ); if ( compiler.Output.Successful ) { result.Output.Add( compiler.Output ); } else { // if we were supressing build notifications then this was probably // during startup. So print them so they show in the log file to give // us a clue as to what's gone wrong. if ( PrintErrorsInConsole || SuppressBuildNotifications ) { Log.Warning( $"Compile of '{compiler.Name}' Failed:" ); foreach ( var diag in compiler.Output.Diagnostics ) { if ( diag.Severity <= DiagnosticSeverity.Info ) continue; Log.Warning( $"{diag.Severity} | {diag.GetMessage()} - {diag.Location.SourceTree?.FilePath}:{diag.Location.GetLineSpan().StartLinePosition}" ); } } } } BuildResult = result; OnCompileFinished.InvokeWithWarning(); // // There was a build error - don't load them! // if ( !allSuccess ) { return false; } timer.Restart(); log.Trace( $"OnCompileSuccess" ); OnCompileSuccess?.InvokeWithWarning(); return true; } finally { IsBuilding = false; log.Trace( $"BuildAsync Finish" ); } } /// /// Find a reference for this dll. Throws if a reference is not found / invalid, and returns if /// a reference should be silently ignored (like self-referencing). /// internal async Task FindReferenceAsync( string reference, Compiler fromCompiler ) { // To retain backwards compatibility if ( reference == "package.local.base" ) reference = "package.base"; var compiler = FindCompilerByAssemblyName( reference ); if ( compiler != null ) { // // Ignore cyclic dependencies to avoid compilers waiting for each other forever // if ( compiler.GetReferencedCompilers().Contains( fromCompiler ) ) return null; // // If we're relying on a compiler to build, and it's building, // give it a few seconds to build.. // const double timeoutSeconds = 60d; var output = await compiler.GetCompileOutputAsync().WaitAsync( TimeSpan.FromSeconds( timeoutSeconds ) ); if ( !output.Successful ) throw new System.Exception( $"Broken Reference: {reference} (the compiler failed)" ); if ( output.MetadataReference is null ) throw new System.Exception( $"Broken Reference: {reference} (the metadata is null)" ); return output.MetadataReference; } // // Search globally if we have that capability // if ( ReferenceProvider?.Lookup( reference ) is { } providedRef ) { return providedRef; } // // package. references aren't going to be in FrameworkReferences // so we'll complain about them here // if ( reference.StartsWith( "package." ) ) { throw new System.Exception( $"Couldn't find reference {reference}" ); } return FrameworkReferences.FindByName( reference ); } /// /// Reset the compile group. Clear errors and outputs. /// public void Reset() { if ( IsBuilding ) throw new System.Exception( "Tried to reset CompileGroup while compiling!" ); BuildResult = default; } public async Task WaitForCompile( CancellationToken token ) { while ( NeedsBuild || IsBuilding ) { await Task.Delay( 10 ); if ( token.IsCancellationRequested ) return; } } public struct Results { public bool Success => !Failed; public bool Failed { get; set; } public List Diagnostics { get; set; } public List Output { get; set; } public string BuildDiagnosticsString( DiagnosticSeverity severity = DiagnosticSeverity.Warning ) { if ( Diagnostics == null ) return "No build result diagnostics found"; var lines = Diagnostics .Where( x => x.Severity >= severity ) .Select( diag => $"{diag.Severity} | {diag.GetMessage()} - {diag.Location?.SourceTree?.FilePath}:{diag?.Location?.GetLineSpan().StartLinePosition}" ); return string.Join( "\n", lines ); } } } /// /// Allows you to look up references for a compiler. /// public interface ICompileReferenceProvider { /// /// Find a reference for this dll /// PortableExecutableReference Lookup( string reference ); }