361 lines
8.9 KiB
C#
361 lines
8.9 KiB
C#
|
|
|
|
#nullable enable
|
|
|
|
|
|
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.Emit;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
public class MemorySourceText : SourceText
|
|
{
|
|
public override char this[int position] => ' ';
|
|
|
|
public override Encoding? Encoding => Encoding.UTF8;
|
|
|
|
public override int Length => 0;
|
|
|
|
public override void CopyTo( int sourceIndex, char[] destination, int destinationIndex, int count )
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
|
|
public class MemoryRefResolver : SourceReferenceResolver
|
|
{
|
|
public override bool Equals( object? other )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
private MemoryStream _fakeMS = new MemoryStream();
|
|
|
|
public override string? NormalizePath( string? path, string? baseFilePath ) => path;
|
|
|
|
public override Stream OpenRead( string resolvedPath ) => _fakeMS;
|
|
|
|
private MemorySourceText _fakeMST = new();
|
|
|
|
public override SourceText ReadText( string resolvedPath ) => _fakeMST;
|
|
|
|
public override string? ResolveReference( string? path, string? baseFilePath ) => path;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static class scr
|
|
{
|
|
|
|
public static FieldInfo? GetFieldInfo( Type? t, string name )
|
|
{
|
|
if( t == null )
|
|
return null;
|
|
|
|
var fi = t.GetField( name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly );
|
|
|
|
if( fi != null )
|
|
return fi;
|
|
|
|
if( t.BaseType != null )
|
|
return GetFieldInfo( t.BaseType, name );
|
|
|
|
return null;
|
|
}
|
|
|
|
// From stack overflow
|
|
static Lazy<ISet<Type>> typeSetLazy =
|
|
new Lazy<ISet<Type>>( () =>
|
|
{
|
|
var types = AppDomain
|
|
.CurrentDomain
|
|
.GetAssemblies()
|
|
.SelectMany( a => a.GetTypes()
|
|
.Where( t => t.IsClass ) );
|
|
var typesAndBaseTypes = types
|
|
.Select( t => new { Type = t, t.BaseType } )
|
|
.ToList();
|
|
var typesWithSubclasses = typesAndBaseTypes
|
|
.Join(
|
|
typesAndBaseTypes,
|
|
t => t.Type,
|
|
t => t.BaseType,
|
|
( t1, t2 ) => t2.BaseType );
|
|
var typesHs = new HashSet<Type>( types );
|
|
typesHs.ExceptWith( typesWithSubclasses );
|
|
return typesHs;
|
|
} );
|
|
|
|
static bool IsLeafType( this Type type )
|
|
{
|
|
return typeSetLazy.Value.Contains( type );
|
|
}
|
|
|
|
static HashSet<char> s_badChars = new( new char[] { '<', '>', ' ', ',', '.', '+', '[', ']', '$', ':' } );
|
|
|
|
static public string TypeToIdentifier( string typename )
|
|
{
|
|
var safeStr = new StringBuilder( typename );
|
|
|
|
for( int i = 0; i < safeStr.Length; ++i )
|
|
{
|
|
if( s_badChars.Contains( safeStr[i] ) )
|
|
safeStr[i] = '_';
|
|
}
|
|
|
|
return safeStr.ToString();
|
|
}
|
|
|
|
static public FileSystemWatcher s_watcher = new();
|
|
static public Action<Assembly> s_fnAss = ( ass ) =>
|
|
{
|
|
log.warn( $"Need to replace s_fnAss with custom function" );
|
|
};
|
|
|
|
public static void WatchPluginDir( string dir, Action<Assembly> fnAss )
|
|
{
|
|
log.info( $"Watching {dir} for changes" );
|
|
|
|
s_fnAss = fnAss;
|
|
|
|
s_watcher = new FileSystemWatcher( dir );
|
|
|
|
s_watcher.Created += OnCreated;
|
|
s_watcher.Deleted += OnDeleted;
|
|
s_watcher.Renamed += OnRenamed;
|
|
|
|
s_watcher.Filter = "*.cs";
|
|
s_watcher.IncludeSubdirectories = true;
|
|
s_watcher.EnableRaisingEvents = true;
|
|
|
|
var existingFiles = Directory.GetFiles( dir, "*.cs", SearchOption.AllDirectories );
|
|
|
|
foreach( var filename in existingFiles )
|
|
{
|
|
Process( filename );
|
|
}
|
|
|
|
}
|
|
|
|
static void OnCreated( object sender, FileSystemEventArgs fse )
|
|
{
|
|
log.debug( $"{fse.Name} got {fse.ChangeType}" );
|
|
|
|
if( fse.Name?.EndsWith( ".cs" ) ?? false )
|
|
{
|
|
Process( fse.FullPath );
|
|
}
|
|
}
|
|
|
|
static void OnDeleted( object sender, FileSystemEventArgs fse )
|
|
{
|
|
log.debug( $"{fse.Name} got {fse.ChangeType}" );
|
|
}
|
|
|
|
static void OnRenamed( object sender, FileSystemEventArgs fse )
|
|
{
|
|
log.debug( $"{fse.Name} got {fse.ChangeType}" );
|
|
|
|
if( fse.Name?.EndsWith( ".cs" ) ?? false )
|
|
{
|
|
while( true )
|
|
{
|
|
try
|
|
{
|
|
Process( fse.FullPath );
|
|
return;
|
|
}
|
|
catch( System.IO.IOException ex )
|
|
{
|
|
log.debug( $"Ignoring {ex.Message}" );
|
|
}
|
|
catch( Exception ex )
|
|
{
|
|
log.error( $"Got ex {ex.GetType().Name} trying to process {fse.FullPath}" );
|
|
log.error( $"-> {ex.Message}" );
|
|
return;
|
|
}
|
|
|
|
Thread.Sleep( 100 );
|
|
}
|
|
}
|
|
}
|
|
|
|
static void Process( string filename )
|
|
{
|
|
CompileFile( filename, ( ass ) => { s_fnAss( ass ); }, ( diags ) => { } );
|
|
}
|
|
|
|
public static void CompileFile( string filename, Action<Assembly> onSuccess, Action<ImmutableArray<Diagnostic>> onFailure, Platform platform = Platform.X86 )
|
|
{
|
|
var fullpath = Path.GetFullPath( filename );
|
|
|
|
var stream = File.OpenRead( fullpath );
|
|
|
|
var sourceText = SourceText.From( stream );
|
|
|
|
Compile( sourceText, fullpath, onSuccess, onFailure );
|
|
}
|
|
|
|
public static void Compile( string str, string uniquePath, Action<Assembly> onSuccess, Action<ImmutableArray<Diagnostic>> onFailure, Platform platform = Platform.X86 )
|
|
{
|
|
var sourceText = SourceText.From( str );
|
|
|
|
Compile( sourceText, uniquePath, onSuccess, onFailure );
|
|
}
|
|
|
|
|
|
public static void Compile( SourceText sourceText, string uniquePath, Action<Assembly> onSuccess, Action<ImmutableArray<Diagnostic>> onFailure, Platform platform = Platform.X86 )
|
|
{
|
|
string assemblyName = Path.GetRandomFileName();
|
|
|
|
var options = new CSharpParseOptions( documentationMode: DocumentationMode.Diagnose, kind: SourceCodeKind.Regular );
|
|
|
|
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText( sourceText, options, uniquePath );
|
|
|
|
var memRef = new MemoryRefResolver();
|
|
|
|
|
|
using MemoryStream ms = new();
|
|
using MemoryStream pdb = new();
|
|
|
|
var result = CompileAndEmit( assemblyName, new[] { syntaxTree }, ms, pdb, platform );
|
|
|
|
if( !result.Success )
|
|
{
|
|
if( onFailure == null )
|
|
{
|
|
}
|
|
else
|
|
{
|
|
LogDiags( uniquePath, result.Diagnostics.Length, result.Diagnostics );
|
|
|
|
onFailure( result.Diagnostics );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ms.Seek( 0, SeekOrigin.Begin );
|
|
var assembly = Assembly.Load( ms.ToArray(), pdb.ToArray() );
|
|
|
|
onSuccess( assembly );
|
|
}
|
|
}
|
|
|
|
public static void LogDiags( string uniquePath, int count, IEnumerable<Diagnostic> diags )
|
|
{
|
|
log.warn( $"{count} Problems building script with name {uniquePath}" );
|
|
foreach( var diag in diags )
|
|
{
|
|
log.debug( $"{diag}" );
|
|
}
|
|
}
|
|
|
|
private static EmitResult CompileAndEmit( string assemblyName, SyntaxTree[] syntaxTrees, MemoryStream ms, MemoryStream pdb, Platform platform )
|
|
{
|
|
MemoryRefResolver memRef = new();
|
|
|
|
// @@@@ TODO :: Config release / debug
|
|
CSharpCompilation compilation = CSharpCompilation.Create(
|
|
assemblyName,
|
|
syntaxTrees: syntaxTrees,
|
|
references: RefCache.References,
|
|
options: new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary,
|
|
sourceReferenceResolver: memRef,
|
|
optimizationLevel: OptimizationLevel.Release,
|
|
platform: platform,
|
|
specificDiagnosticOptions: new Dictionary<string, ReportDiagnostic>
|
|
{
|
|
{ "CS1701", ReportDiagnostic.Suppress }
|
|
} ) );
|
|
|
|
return compilation.Emit( ms, pdb );
|
|
}
|
|
|
|
private static class RefCache
|
|
{
|
|
// create the list of references on first use, but if two threads both *start* making the list thats fine since we'll just use whichever wins.
|
|
private static readonly Lazy<ImmutableArray<MetadataReference>> lazyRef = new Lazy<ImmutableArray<MetadataReference>>( GetReferences, LazyThreadSafetyMode.PublicationOnly );
|
|
|
|
public static IReadOnlyList<MetadataReference> References => lazyRef.Value;
|
|
|
|
private static ImmutableArray<MetadataReference> GetReferences()
|
|
{
|
|
var builder = ImmutableArray.CreateBuilder<MetadataReference>();
|
|
|
|
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
|
|
|
foreach( var ass in assemblies )
|
|
{
|
|
if( ass != null && !ass.IsDynamic && ass.Location != null )
|
|
{
|
|
try
|
|
{
|
|
builder.Add( MetadataReference.CreateFromFile( ass.Location ) );
|
|
}
|
|
catch( Exception ex )
|
|
{
|
|
log.warn( $"Got {ex.GetType().Name} sayaing {ex.Message}" );
|
|
}
|
|
}
|
|
}
|
|
|
|
return builder.ToImmutable();
|
|
}
|
|
}
|
|
|
|
public static string PrettyName( Type t )
|
|
{
|
|
if( t.GetGenericArguments().Length == 0 )
|
|
{
|
|
return t.FullName?.Replace( '+', '.' ) ?? "";
|
|
}
|
|
var genArgs = t.GetGenericArguments();
|
|
var typeDef = t.FullName ?? "";
|
|
|
|
var indexOfTick = typeDef.IndexOf( "`" );
|
|
|
|
var unmangledOuterName = typeDef.Substring( 0, typeDef.IndexOf( '`' ) ).Replace( '+', '.' );
|
|
|
|
var innerName = "";
|
|
|
|
//Check for inner class
|
|
if( typeDef.ElementAt( indexOfTick + 2 ) != '[' )
|
|
{
|
|
var indexOfOpenBracket = typeDef.IndexOf( '[', indexOfTick );
|
|
|
|
innerName = typeDef.Substring( indexOfTick + 2, indexOfOpenBracket - ( indexOfTick + 2 ) ).Replace( '+', '.' );
|
|
}
|
|
|
|
return unmangledOuterName + "<" + String.Join( ",", genArgs.Select( PrettyName ) ) + ">" + innerName;
|
|
}
|
|
|
|
|
|
private static void AddIfFirst<TKey, TValue>( IDictionary<TKey, TValue> dict, TKey key, TValue value )
|
|
{
|
|
if( !dict.ContainsKey( key ) )
|
|
{
|
|
dict.Add( key, value );
|
|
}
|
|
}
|
|
}
|
|
|