Skip to content

Commit

Permalink
feat: add support for source generators
Browse files Browse the repository at this point in the history
  • Loading branch information
rdavisau committed Apr 20, 2022
1 parent 71545fe commit 93a8f01
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 23 deletions.
35 changes: 33 additions & 2 deletions readme.md
Expand Up @@ -120,11 +120,42 @@ Since your `IReloadManager` is itself reloadable (provided it derives from `Relo
Since the incremental compiler builds directly off the source files you're working on, debugging reloaded code is possible. Nice!
VS for Mac seems to like to show the break higher in the callstack (at the first non-reloaded component), but you can select the current frame. Rider breaks in the expected place.

## source generators

Tested only on the latest Mvvm Community Toolkit preview, you might be able to use source generators with tbc. This is confgured by adding a `SourceGeneratorReferences` array to the
`AssemblyCompiler` configuration element. Here you can include references to dlls, nuget packages or csproj files.

```
"SourceGeneratorReferences": [
{
"Kind": "AssemblyPath",
"Reference": "/Users/rdavis/.nuget/packages/communitytoolkit.mvvm/8.0.0-preview3/analyzers/dotnet/roslyn4.0/cs/CommunityToolkit.Mvvm.SourceGenerators.dll"
},
{
"Kind": "NuGetPackageReference",
"Reference": "CommunityToolkit.Mvvm",
"Context": "8.0.0-preview3"
},
{
"Kind": "Csproj",
"Reference": "/Users/rdavis/Source/MyAppWithSourceGenerators/App1/App1/App1.iOS/App1.iOS.csproj"
},
]
```

* For an `AssemblyPath` reference, tbc will try to load the assembly and take any `ISourceGenerator` and `IIncrementalGenerator` types it can instantiate
* For a `NuGetPackageReference` reference, tbc will scan the local nuget package cache for the provided package/version folder and to try to find assemblies that might contain generators, then pass them to the `AssemblyPath` method
* For a `Csproj` reference, tbc will parse the provided csproj file for nuget package references, then pass them to the `NuGetPackageReference` method.

# alpha quality

I've only used this for myself but on several production-complexity-level apps. I've only tested on iOS.
I've only used this for myself but on several production-complexity-level apps. I've only used it heavily on iOS. At least the sample works on Android too.

Your mileage may vary. Messing with static classes probably won't work (`tree remove` them 🤠). Xaml files won't work (delete them 🤠🤠). Something that needs to be source generated won't work. If source generators are more common in maui, I'd see if it can be added.
Your mileage may vary. Messing with static classes probably won't work (`tree remove` them 🤠). Xaml files won't work (delete them 🤠🤠). Something that needs to be source generated might work with some effort (see source generators).

This used to use grpc.core for message interchange but it was not apple silicon friendly. I replaced grpc with a socket-based transport which hasn't yet had a huge amount of testing.
But now it's apple silicon friendly and with .NET maui, the simulator is apple silicon friendly too! Finally nirvana.
Expand Up @@ -32,13 +32,13 @@ public partial class FileEnvironment : TransientComponentBase<FileEnvironment>,
public FileEnvironment(IRemoteClientDefinition client,
IFileWatcher fileWatcher, ICommandProcessor commandProcessor,
Func<IRemoteClientDefinition, ITargetClient> targetClientFactory,
Func<IRemoteClientDefinition, IIncrementalCompiler> incrementalCompilerFactory,
Func<string, IIncrementalCompiler> incrementalCompilerFactory,
ILogger<FileEnvironment> logger) : base(logger)
{
Client = targetClientFactory(client);
CommandProcessor = commandProcessor;
FileWatcher = fileWatcher;
IncrementalCompiler = incrementalCompilerFactory(client);
IncrementalCompiler = incrementalCompilerFactory($"{client.Address}:{client.Port}");
IncrementalCompiler.RootPath = FileWatcher.WatchPath;
}

Expand Down
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.IO.Abstractions;
Expand All @@ -17,6 +18,8 @@
using Tbc.Host.Components.FileEnvironment.Models;
using Tbc.Host.Components.FileWatcher.Models;
using Tbc.Host.Components.IncrementalCompiler.Models;
using Tbc.Host.Components.SourceGeneratorResolver;
using Tbc.Host.Components.SourceGeneratorResolver.Models;
using Tbc.Host.Components.TargetClient;
using Tbc.Host.Config;
using Tbc.Host.Extensions;
Expand All @@ -28,43 +31,66 @@ public class IncrementalCompiler : ComponentBase<IncrementalCompiler>, IIncremen
private bool _disposing;

private readonly AssemblyCompilationOptions _options;
private readonly ITargetClient _client;

private readonly IFileSystem _fileSystem;
private readonly ISourceGeneratorResolver _sourceGeneratorResolver;

private int _incrementalCount = 0;
private readonly Guid _sessionGuid = Guid.NewGuid();
private readonly object _lock = new object();
private string _identifier;
private IEnumerable<TbcCommand> _commands;
private readonly string _identifier;

public string OutputPath { get; set; }
public string RootPath { get; set; }

public ImmutableDictionary<SourceGeneratorReference,ResolveSourceGeneratorsResponse> SourceGeneratorResolution { get; set; }
public (IEnumerable<ISourceGenerator> Srcs, IEnumerable<IIncrementalGenerator> Incs) SourceGenerators { get; set; }
public bool AnySourceGenerators => SourceGenerators.Srcs.Any() || SourceGenerators.Incs.Any();

public CSharpCompilation CurrentCompilation { get; set; }
public Dictionary<string, SyntaxTree> RawTrees { get; } = new Dictionary<string, SyntaxTree>();
public Dictionary<string, SyntaxTree> RawTrees { get; } = new();

public List<string> StagedFiles
=> CurrentCompilation.SyntaxTrees.Select(x => x.FilePath).ToList();

public IncrementalCompiler(
string clientIdentifier,
AssemblyCompilationOptions options,
IRemoteClientDefinition client, Func<IRemoteClientDefinition, ITargetClient> targetClientFactory,
IFileSystem fileSystem, ILogger<IncrementalCompiler> logger) : base(logger)
IFileSystem fileSystem, ILogger<IncrementalCompiler> logger,
ISourceGeneratorResolver sourceGeneratorResolver
) : base(logger)
{
_options = options;
_fileSystem = fileSystem;
_client = targetClientFactory(client);
_sourceGeneratorResolver = sourceGeneratorResolver;
_identifier = clientIdentifier;

var cscOptions = new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: options.Debug ? OptimizationLevel.Debug : OptimizationLevel.Release,
allowUnsafe: true);

SourceGeneratorResolution = ResolveSourceGenerators();
SourceGenerators =
(SourceGeneratorResolution.SelectMany(x => x.Value.SourceGenerators).DistinctBySelector(x => x.GetType()).ToList(),
SourceGeneratorResolution.SelectMany(x => x.Value.IncrementalGenerators).DistinctBySelector(x => x.GetType()).ToList());

Logger.LogInformation("Source Generator Resolution: {Count} generator(s)", SourceGeneratorResolution);
foreach (var resolutionOutcome in SourceGeneratorResolution)
Logger.LogDebug(
"Reference {@Reference}, Diagnostics: {@Diagnostics}, " +
"Source Generators: {@SourceGenerators}, Incremental Generators: {@IncrementalGenerators}",
resolutionOutcome.Key, resolutionOutcome.Value.Diagnostics, resolutionOutcome.Value.SourceGenerators, resolutionOutcome.Value.IncrementalGenerators);

CurrentCompilation =
CSharpCompilation.Create("r2", options: cscOptions);
}


private ImmutableDictionary<SourceGeneratorReference, ResolveSourceGeneratorsResponse> ResolveSourceGenerators()
=> Enumerable.Aggregate(_options.SourceGeneratorReferences,
ImmutableDictionary.Create<SourceGeneratorReference, ResolveSourceGeneratorsResponse>(),
(curr, next) => curr.Add(next, _sourceGeneratorResolver.ResolveSourceGenerators(new(next)).Result));

public EmittedAssembly StageFile(ChangedFile file, bool silent = false)
{
var sw = Stopwatch.StartNew();
Expand All @@ -80,14 +106,14 @@ public EmittedAssembly StageFile(ChangedFile file, bool silent = false)
.WithLanguageVersion(LanguageVersion.Preview)
.WithKind(SourceCodeKind.Regular)
.WithPreprocessorSymbols(_options.PreprocessorSymbols.ToArray()),
path: file.Path,
path: file.Path,
Encoding.Default);

EmittedAssembly emittedAssembly = null;

WithCompilation(c =>
{
var newC = RawTrees.TryGetValue(file.Path, out var oldSyntaxTree)
var newCompilation = RawTrees.TryGetValue(file.Path, out var oldSyntaxTree)
? c.ReplaceSyntaxTree(oldSyntaxTree, syntaxTree)
: c.AddSyntaxTrees(syntaxTree);
Expand All @@ -99,10 +125,28 @@ public EmittedAssembly StageFile(ChangedFile file, bool silent = false)
"Stage '{FileName}' without emit, Duration: {Duration:N0}ms, Types: [ {Types} ]",
file, sw.ElapsedMilliseconds, syntaxTree.GetContainedTypes());
return newC;
return newCompilation;
}
// keep a separate source generated compilation so we don't poison the raw tree with generated content
Compilation? sourceGeneratedCompilation = null;
if (AnySourceGenerators)
{
var (srcs, incs) = SourceGenerators;
var driver =
CSharpGeneratorDriver.Create(incs.ToArray())
.AddGenerators(srcs.ToImmutableArray())
.WithUpdatedParseOptions(new CSharpParseOptions(LanguageVersion.Preview, preprocessorSymbols: _options.PreprocessorSymbols));
driver.RunGeneratorsAndUpdateCompilation(
newCompilation, out sourceGeneratedCompilation, out var generatorDiagnostics);
if (generatorDiagnostics.Any())
Logger.LogWarning("Applying source generators resulted in diagnostics: {@Diagnostics}",
generatorDiagnostics);
}
var result = EmitAssembly(newC, out emittedAssembly);
var result = EmitAssembly(sourceGeneratedCompilation ?? newCompilation, out emittedAssembly);
if (!String.IsNullOrWhiteSpace(_options.WriteAssembliesPath))
WriteEmittedAssembly(emittedAssembly);
Expand All @@ -115,18 +159,18 @@ public EmittedAssembly StageFile(ChangedFile file, bool silent = false)
result.Success
? ""
: String.Join(
Environment.NewLine,
Environment.NewLine,
result.Diagnostics
.Where(x => x.Severity == DiagnosticSeverity.Error)
.Select(x => $"{x.Location}: {x.GetMessage()}")));
return newC;
return newCompilation;
});

return emittedAssembly;
}

public EmitResult EmitAssembly(CSharpCompilation compilation, out EmittedAssembly emittedAssembly)
public EmitResult EmitAssembly(Compilation compilation, out EmittedAssembly emittedAssembly)
{
var asmStream = new MemoryStream();
var pdbStream = new MemoryStream();
Expand Down Expand Up @@ -275,11 +319,10 @@ public string TryResolvePrimaryType(string typeHint)
public void Dispose()
{
_disposing = true;
_client.Dispose();
}

string IExposeCommands.Identifier
=> $"inc-{_client.ClientDefinition.Address}-{_client.ClientDefinition.Port}";
=> $"inc-{_identifier}";

IEnumerable<TbcCommand> IExposeCommands.Commands => new List<TbcCommand>
{
Expand Down
@@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
using Tbc.Host.Components.SourceGeneratorResolver.Models;

namespace Tbc.Host.Components.SourceGeneratorResolver;

public interface ISourceGeneratorResolver
{
Task<ResolveSourceGeneratorsResponse> ResolveSourceGenerators(ResolveSourceGeneratorsRequest request,
CancellationToken canceller = default);
}
@@ -0,0 +1,3 @@
namespace Tbc.Host.Components.SourceGeneratorResolver.Models;

public record PackageReference(string Include, string Version);
@@ -0,0 +1,3 @@
namespace Tbc.Host.Components.SourceGeneratorResolver.Models;

public record ResolveSourceGeneratorsRequest(SourceGeneratorReference Reference);
@@ -0,0 +1,11 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;

namespace Tbc.Host.Components.SourceGeneratorResolver.Models;

public record ResolveSourceGeneratorsResponse(
SourceGeneratorReference Reference,
ImmutableList<ISourceGenerator> SourceGenerators,
ImmutableList<IIncrementalGenerator> IncrementalGenerators,
ImmutableDictionary<string, object> Diagnostics
);
@@ -0,0 +1,3 @@
namespace Tbc.Host.Components.SourceGeneratorResolver.Models;

public record SourceGeneratorReference(SourceGeneratorReferenceKind Kind, string Reference, string? Context = null);
@@ -0,0 +1,3 @@
namespace Tbc.Host.Components.SourceGeneratorResolver.Models;

public enum SourceGeneratorReferenceKind { AssemblyPath, NuGetPackageReference, Csproj }

0 comments on commit 93a8f01

Please sign in to comment.