diff --git a/dev/DebugHost/Program.cs b/dev/DebugHost/Program.cs index 3ce94efd0..a581449f6 100644 --- a/dev/DebugHost/Program.cs +++ b/dev/DebugHost/Program.cs @@ -17,6 +17,8 @@ await Host.CreateDefaultBuilder(args) .UseNetDaemonTextToSpeech() .ConfigureServices((_, services) => services + // change type of compilation here + // .AddAppsFromSource(true) .AddAppsFromAssembly(Assembly.GetEntryAssembly()!) // Remove this is you are not running the integration! .AddNetDaemonStateManager() @@ -29,4 +31,4 @@ await Host.CreateDefaultBuilder(args) { Console.WriteLine($"Failed to start host... {e}"); throw; -} \ No newline at end of file +} diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Compiler/CompilerIntegrationTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/Compiler/CompilerIntegrationTests.cs index deff1c639..bee135c10 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Compiler/CompilerIntegrationTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Compiler/CompilerIntegrationTests.cs @@ -1,6 +1,6 @@ using NetDaemon.AppModel.Internal.Compiler; -namespace NetDaemon.AppModel.Tests.Internal.Compiler; +namespace NetDaemon.AppModel.Tests.Internal.CompilerTests; public class CompilerIntegrationTests { @@ -19,8 +19,7 @@ public void TestDynamicCompileHasType() serviceCollection.AddAppsFromSource(); var provider = serviceCollection.BuildServiceProvider(); - var factory = provider.GetService(); - using var compiler = factory?.New(); + using var compiler = provider.GetService(); // ACT var (collectibleAssemblyLoadContext, compiledAssembly) = compiler?.Compile() @@ -32,4 +31,32 @@ public void TestDynamicCompileHasType() var types = collectibleAssemblyLoadContext.Assemblies.SelectMany(n => n.GetTypes()).ToList(); types.Where(n => n.Name == "SimpleApp").Should().HaveCount(1); } -} \ No newline at end of file + + [Fact] + public void TestDynamicCompileHasTypeUsingDebugFlag() + { + // ARRANGE + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddLogging(); + serviceCollection.AddOptions() + .Configure(options => + { + options.ApplicationConfigurationFolder = Path.Combine(AppContext.BaseDirectory, "Compiler", "Fixtures"); + }); + serviceCollection.AddAppsFromSource(true); + var provider = serviceCollection.BuildServiceProvider(); + + using var compiler = provider.GetService(); + + // ACT + var (collectibleAssemblyLoadContext, compiledAssembly) = compiler?.Compile() + ?? throw new NullReferenceException( + "Not expected null"); + + // CHECK + compiledAssembly.FullName.Should().StartWith("daemon_apps_"); + var types = collectibleAssemblyLoadContext.Assemblies.SelectMany(n => n.GetTypes()).ToList(); + types.Where(n => n.Name == "SimpleApp").Should().HaveCount(1); + } +} diff --git a/src/AppModel/NetDaemon.AppModel.Tests/TypeResolver/TypeResolverTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/TypeResolver/TypeResolverTests.cs index cbc061504..aa61154b7 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/TypeResolver/TypeResolverTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/TypeResolver/TypeResolverTests.cs @@ -72,8 +72,8 @@ public class FakeClass serviceCollection.AddSingleton(_ => syntaxTreeResolverMock.Object); serviceCollection.AddAppModelIfNotExist(); serviceCollection.AddAppTypeResolverIfNotExist(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => s.GetRequiredService()); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(s => s.GetRequiredService()); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(s => s.GetRequiredService()); @@ -128,8 +128,8 @@ public class FakeClass serviceCollection.AddSingleton(_ => syntaxTreeResolverMock.Object); serviceCollection.AddAppModelIfNotExist(); serviceCollection.AddAppTypeResolverIfNotExist(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => s.GetRequiredService()); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(s => s.GetRequiredService()); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(s => s.GetRequiredService()); @@ -186,8 +186,8 @@ public class FakeClass serviceCollection.AddSingleton(_ => syntaxTreeResolverMock.Object); serviceCollection.AddAppModelIfNotExist(); serviceCollection.AddAppTypeResolverIfNotExist(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => s.GetRequiredService()); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(s => s.GetRequiredService()); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(s => s.GetRequiredService()); @@ -224,4 +224,4 @@ public void TestAddAppFromTypeShouldLoadSingleApp() appResolvers.Should().HaveCount(1); appResolvers.First().GetTypes().Should().BeEquivalentTo(new[] {typeof(MyAppLocalApp)}); } -} \ No newline at end of file +} diff --git a/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs b/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs index 541ebe642..25c904951 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs @@ -38,17 +38,17 @@ public static IServiceCollection AddAppFromType(this IServiceCollection services /// Add apps from c# source code using the configuration source to find path /// /// Services - public static IServiceCollection AddAppsFromSource(this IServiceCollection services) + public static IServiceCollection AddAppsFromSource(this IServiceCollection services, bool useDebug = false) { // We make sure we only add AppModel services once - services .AddAppModelIfNotExist() .AddAppTypeResolverIfNotExist() - .AddSingleton() - .AddSingleton(s => s.GetRequiredService()) + .AddSingleton() + .AddSingleton(s => s.GetRequiredService()) .AddSingleton() - .AddSingleton(s => s.GetRequiredService()); + .AddSingleton(s => s.GetRequiredService()) + .AddOptions().Configure(settings => settings.UseDebug = useDebug); // We need to compile it here so we can dynamically add the service providers var assemblyResolver = @@ -126,4 +126,4 @@ private static IServiceCollection AddConfigManagement(this IServiceCollection se services.AddTransient(typeof(IAppConfig<>), typeof(AppConfig<>)); return services; } -} \ No newline at end of file +} diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/AssemblyResolver.cs b/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/AssemblyResolver.cs index bef34e979..c6a90f110 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/AssemblyResolver.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/AssemblyResolver.cs @@ -22,15 +22,15 @@ public Assembly GetResolvedAssembly() internal class DynamicallyCompiledAssemblyResolver : IAssemblyResolver, IDisposable { - private readonly ICompilerFactory _compilerFactory; + private readonly ICompiler _compiler; private Assembly? _compiledAssembly; private CollectibleAssemblyLoadContext? _currentContext; public DynamicallyCompiledAssemblyResolver( - ICompilerFactory compilerFactory + ICompiler compiler ) { - _compilerFactory = compilerFactory; + _compiler = compiler; } public Assembly GetResolvedAssembly() @@ -40,8 +40,7 @@ public Assembly GetResolvedAssembly() if (_compiledAssembly is not null) return _compiledAssembly; - var compiler = _compilerFactory.New(); - var (loadContext, compiledAssembly) = compiler.Compile(); + var (loadContext, compiledAssembly) = _compiler.Compile(); _currentContext = loadContext; _compiledAssembly = compiledAssembly; return compiledAssembly; @@ -55,4 +54,4 @@ public void Dispose() GC.Collect(); GC.WaitForPendingFinalizers(); } -} \ No newline at end of file +} diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Compiler/CompileSettings.cs b/src/AppModel/NetDaemon.AppModel/Internal/Compiler/CompileSettings.cs new file mode 100644 index 000000000..c49e57b52 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/Compiler/CompileSettings.cs @@ -0,0 +1,9 @@ +namespace NetDaemon.AppModel.Internal.Compiler; + +/// +/// Used to set debug or release mode for dynamic compiled apps +/// +internal record CompileSettings +{ + public bool UseDebug { get; set; } = false; +} diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Compiler/Compiler.cs b/src/AppModel/NetDaemon.AppModel/Internal/Compiler/Compiler.cs index a5d855aae..2c33c8e2c 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/Compiler/Compiler.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/Compiler/Compiler.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime; +using System.Runtime.Loader; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -17,35 +18,40 @@ internal record CompiledAssemblyResult(CollectibleAssemblyLoadContext AssemblyCo internal class Compiler : ICompiler { private readonly ILogger _logger; + private readonly bool _useDebug; private readonly ISyntaxTreeResolver _syntaxResolver; public Compiler( ISyntaxTreeResolver syntaxResolver, - ILogger logger - ) + ILogger logger, + IOptions compileSettings) { _syntaxResolver = syntaxResolver; _logger = logger; + _useDebug = compileSettings.Value.UseDebug; } public CompiledAssemblyResult Compile() { CollectibleAssemblyLoadContext context = new(); + var compilation = GetSharpCompilation(); using var peStream = new MemoryStream(); - var emitResult = compilation.Emit(peStream); + using MemoryStream? symStream = _useDebug ? new MemoryStream() : null; + + var emitResult = compilation.Emit(peStream, symStream); if (emitResult.Success) { peStream.Seek(0, SeekOrigin.Begin); - var assembly = context.LoadFromStream(peStream); + symStream?.Seek(0, SeekOrigin.Begin); + var assembly = context.LoadFromStream(peStream, symStream); return new CompiledAssemblyResult(context, assembly); } var error = PrettyPrintCompileError(emitResult); - - _logger.LogError("Failed to compile applications\n{error}", error); + _logger.LogError("Failed to compile applications\n{Error}", error); context.Unload(); // Finally do cleanup and release memory @@ -69,8 +75,9 @@ private CSharpCompilation GetSharpCompilation() metaDataReference.ToArray(), new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, - optimizationLevel: OptimizationLevel.Release, - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default + optimizationLevel: _useDebug ? OptimizationLevel.Debug : OptimizationLevel.Release, + assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default, + platform: Platform.AnyCpu ) ); } @@ -112,4 +119,4 @@ private static string PrettyPrintCompileError(EmitResult emitResult) return msg.ToString(); } -} \ No newline at end of file +} diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Compiler/CompilerFactory.cs b/src/AppModel/NetDaemon.AppModel/Internal/Compiler/CompilerFactory.cs deleted file mode 100644 index 3ae82722a..000000000 --- a/src/AppModel/NetDaemon.AppModel/Internal/Compiler/CompilerFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace NetDaemon.AppModel.Internal.Compiler; - -internal class CompilerFactory : ICompilerFactory -{ - private readonly ILogger _logger; - private readonly ISyntaxTreeResolver _syntaxResolver; - - public CompilerFactory( - ISyntaxTreeResolver syntaxResolver, - ILogger logger - ) - { - _syntaxResolver = syntaxResolver; - _logger = logger; - } - - public ICompiler New() - { - return new Compiler(_syntaxResolver, _logger); - } -} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Compiler/ICompilerFactory.cs b/src/AppModel/NetDaemon.AppModel/Internal/Compiler/ICompilerFactory.cs deleted file mode 100644 index a77a0418f..000000000 --- a/src/AppModel/NetDaemon.AppModel/Internal/Compiler/ICompilerFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NetDaemon.AppModel.Internal.Compiler; - -internal interface ICompilerFactory -{ - ICompiler New(); -} \ No newline at end of file