Files
sbox-public/engine/Sandbox.Compiling/CompileGroup.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

463 lines
12 KiB
C#

using Microsoft.CodeAnalysis;
using System.Threading;
namespace Sandbox;
[SkipHotload]
public class CompileGroup : IDisposable
{
Logger log;
/// <summary>
/// Build notifications start of suppressed until after startup proper. That way
/// we don't get 4 build notification windows popping up on startup.
/// </summary>
public static bool SuppressBuildNotifications { get; set; } = true;
/// <summary>
/// The compilers within the group
/// </summary>
public IEnumerable<Compiler> Compilers => _compilers;
/// <summary>
/// The name of this compile group, for debugging/display purposes
/// </summary>
public string Name { get; set; } = "Compile Group";
/// <summary>
/// Returns true if we have compiles pending
/// </summary>
public bool NeedsBuild => _recompileList.Count() > 0;
/// <summary>
/// Returns true if we are currently in the process of building
/// </summary>
public bool IsBuilding { get; private set; }
/// <summary>
/// True if we want to print errors in the console when compiling
/// </summary>
public bool PrintErrorsInConsole { get; set; } = true;
/// <summary>
/// True if we want to use fast hotloading with this compile group
/// </summary>
public bool AllowFastHotload { get; set; } = false;
/// <summary>
/// Returns true if build was successful
/// </summary>
public Results BuildResult { get; private set; }
/// <summary>
/// Called when a compiling starts
/// </summary>
public Action OnCompileStarted { get; set; }
/// <summary>
/// Called when a compiling ends
/// </summary>
public Action OnCompileFinished { get; set; }
/// <summary>
/// Called when a compile completes successfully. Can access the result from BuildResult.
/// </summary>
public Action OnCompileSuccess { get; set; }
/// <summary>
/// All created compilers.
/// </summary>
List<Compiler> _compilers = new List<Compiler>();
/// <summary>
/// Compilers waiting for recompile
/// </summary>
HashSet<Compiler> _recompileList = new();
/// <summary>
/// Allows providing an external way to find references
/// </summary>
public ICompileReferenceProvider ReferenceProvider { get; set; }
/// <summary>
/// AccessControl instance to use when verifying whitelist. Must be set to enable compile-time access control.
/// </summary>
public AccessControl AccessControl { get; set; }
public CompileGroup( string name )
{
log = new Logger( $"CompileGroup/{name}" );
Name = name;
log.Trace( "Created" );
}
/// <summary>
/// Shut everything down
/// </summary>
public void Dispose()
{
_compilers.Clear();
_recompileList.Clear();
log.Trace( "Dispose" );
}
/// <summary>
/// Create a new compiler in this group.
/// </summary>
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;
}
/// <summary>
/// Mark this assembly as changed.
/// </summary>
internal void MarkForRecompile( Compiler compiler )
{
if ( _recompileList.Add( compiler ) )
{
log.Trace( $"MarkForRecompile ({compiler.Name})" );
}
}
/// <summary>
/// Returns true if this group will compile <paramref name="compiler"/> next build.
/// </summary>
internal bool CompilerNeedsBuild( Compiler compiler ) => _recompileList.Contains( compiler );
/// <summary>
/// Recompile anything that depends on us too
/// </summary>
void MarkDependantsForRecompile( Compiler compiler, HashSet<Compiler> 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 ) );
}
}
/// <summary>
/// Build the compilers
/// </summary>
public async Task<bool> 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" );
}
}
/// <summary>
/// Find a reference for this dll. Throws if a reference is not found / invalid, and returns <see langword="null"/> if
/// a reference should be silently ignored (like self-referencing).
/// </summary>
internal async Task<PortableExecutableReference> 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 );
}
/// <summary>
/// Reset the compile group. Clear errors and outputs.
/// </summary>
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<Microsoft.CodeAnalysis.Diagnostic> Diagnostics { get; set; }
public List<CompilerOutput> 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 );
}
}
}
/// <summary>
/// Allows you to look up references for a compiler.
/// </summary>
public interface ICompileReferenceProvider
{
/// <summary>
/// Find a reference for this dll
/// </summary>
PortableExecutableReference Lookup( string reference );
}