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 ); } }