using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Emit;
namespace Sandbox;
///
/// Given a folder of .cs files, this will produce (and load) an assembly
///
[SkipHotload]
public sealed partial class Compiler : IDisposable
{
private Logger log { get; set; }
///
/// Each compiler must belong to a compile group
///
public CompileGroup Group { get; private set; }
///
/// The output from the previous build
///
public CompilerOutput Output { get; set; }
///
/// Is this compiler currently building?
///
public bool IsBuilding => _compileTcs?.Task is { IsCompleted: false };
///
/// Returns true if this compiler is pending a build, or currently building.
///
public bool NeedsBuild => IsBuilding || Group.CompilerNeedsBuild( this );
///
/// Name of the project this compiler was created for. This could be something like "base" or "org.ident".
///
public string Name { get; }
///
/// During development we use absolute source paths so that debugging works better. In a packed/release build it's
/// good to use relative paths instead, just to avoid exposing the builder's file system.
///
public bool UseAbsoluteSourcePaths { get; set; } = true;
///
/// A list of warnings and errors created by the last build
///
public Microsoft.CodeAnalysis.Diagnostic[] Diagnostics => Output?.Diagnostics.ToArray() ?? Array.Empty();
///
/// Generated assembly name, without an extension. This will be "package.{Name}".
///
public string AssemblyName { get; }
///
/// Global namespaces
///
public StringBuilder GeneratedCode { get; set; } = new();
///
/// Directories to search for code
///
private List SourceLocations { get; } = new();
///
/// An aggregate of all the filesystem this compiler has
///
public BaseFileSystem FileSystem { get; } = new AggregateFileSystem();
///
/// After compile is completed successfully this will be non null.
///
internal PortableExecutableReference MetadataReference;
///
/// Keeps track of the most recent values,
/// in case the current one is revoked because it was fast-hotloaded.
/// This dictionary is cleared when a version is built that doesn't support
/// fast hotload at all.
///
private readonly Dictionary _recentMetadataReferences = new();
private IncrementalCompileState incrementalState = new IncrementalCompileState();
///
/// The compiler's settings.
///
private Compiler.Configuration config;
///
/// Should only ever get called from CompileGroup.
///
internal Compiler( CompileGroup group, string name, string fullPath, Compiler.Configuration settings )
{
Group = group;
Name = name;
log = new Logger( $"Compiler/{name}" );
AssemblyName = $"package.{name}";
if ( fullPath is not null )
{
AddSourcePath( fullPath );
}
SetConfiguration( settings );
}
///
/// Should only ever get called from CompileGroup.
///
internal Compiler( CompileGroup group, string name )
{
Group = group;
Name = name;
log = new Logger( $"Compiler/{name}" );
AssemblyName = $"package.{name}";
}
///
/// Add an extra source path. Useful for situations where you want to combine multiple addons into one.
///
public void AddSourcePath( string fullPath )
{
AddSourceLocation( new LocalFileSystem( fullPath ) );
}
internal void AddSourceLocation( BaseFileSystem fileSystem )
{
fileSystem.TraceChanges = true;
SourceLocations.Add( fileSystem );
FileSystem.Mount( fileSystem );
}
public void SetConfiguration( Compiler.Configuration newConfig )
{
config = newConfig;
incrementalState.Reset();
}
public Configuration GetConfiguration()
{
return config;
}
///
/// Results for the assembly build. This can contain warnings or errors.
///
public EmitResult BuildResult { get; private set; }
///
/// Accesses Output.Successful
///
public bool BuildSuccess => Output?.Successful ?? false;
///
/// Keep tabs of how many times we've compiled
///
static int compileCounter = 100;
public void NotifyFastHotload( Version fastHotloadedVersion )
{
log.Trace( $"{Name}@{fastHotloadedVersion} was fast hotloaded" );
if ( !_recentMetadataReferences.Remove( fastHotloadedVersion, out var reference ) )
{
log.Trace( $" Not found!!" );
return;
}
//
// MetadataReference shouldn't be a fast hotloaded version, otherwise other compilers
// that reference this compiler can't be loaded properly!
//
if ( reference == MetadataReference )
{
var mostRecent = _recentMetadataReferences
.MaxBy( x => x.Key );
MetadataReference = mostRecent.Value;
log.Trace( $" Now using {Name}@{mostRecent.Key}" );
}
}
///
/// Read text from a file while dealing with the fact that it might be being saved right
/// when we're loading it so it's likely to throw IOExceptions.
///
private string ReadTextForgiving( string file, int retryCount = 10, int millisecondsBetweenChanges = 5 )
{
for ( var i = 0; i < retryCount; i++ )
{
try
{
return System.IO.File.ReadAllText( file );
}
catch ( System.IO.IOException )
{
System.Threading.Thread.Sleep( millisecondsBetweenChanges );
}
}
return null;
}
internal async Task> BuildReferencesAsync( CodeArchive archive )
{
var output = new List( FrameworkReferences.All.Values );
var foundHash = new HashSet( StringComparer.OrdinalIgnoreCase );
foreach ( var name in archive.References )
{
// We already got it from a package reference
// this is cool for when referencing something that includes package.base.dll
if ( foundHash.Contains( name ) )
continue;
// FindReferenceAsync throws if not found
if ( await Group.FindReferenceAsync( name, this ) is { } mr )
{
log.Trace( $"Found reference: {name}" );
output.Add( mr );
}
}
return output;
}
///
/// Waits for the current build to finish, then outputs that build's result.
/// This is only valid during .
///
internal Task GetCompileOutputAsync()
{
// Build hasn't started
Assert.NotNull( _compileTcs, $"The containing group isn't currently compiling ({Name})" );
return _compileTcs.Task;
}
///
/// Return this compiler and all child compilers
///
internal IEnumerable GetReferencedCompilers()
{
var referenced = new HashSet();
var queue = new Queue();
referenced.Add( this );
queue.Enqueue( this );
while ( queue.TryDequeue( out var next ) )
{
foreach ( var reference in next._references )
{
if ( Group.FindCompilerByAssemblyName( reference ) is not { } otherCompiler ) continue;
if ( !referenced.Add( otherCompiler ) ) continue;
queue.Enqueue( otherCompiler );
}
}
return referenced;
}
~Compiler()
{
Dispose( false );
}
public void Dispose()
{
Dispose( true );
GC.SuppressFinalize( this );
}
private void Dispose( bool disposing )
{
if ( disposing )
{
Group?.OnCompilerDisposed( this );
Group = null;
}
foreach ( var watcher in sourceWatchers ) watcher.Dispose();
sourceWatchers.Clear();
FileSystem?.Dispose();
foreach ( var fs in SourceLocations ) fs.Dispose();
SourceLocations.Clear();
}
public int DependencyIndex( int depth = 0 )
{
int index = 0;
depth++;
if ( depth > 10 )
throw new System.Exception( "Cyclic references detected - aborting." );
foreach ( var r in _references )
{
var g = Group.FindCompilerByAssemblyName( r );
if ( g == null ) continue; // this is allowed - it might be Sandbox.Game or something
if ( g == this ) continue;
index = Math.Max( index, g.DependencyIndex( depth ) );
}
return index + 1;
}
///
/// Recompile this as soon as is appropriate
///
public void MarkForRecompile()
{
Group.MarkForRecompile( this );
}
}