Skip to content
This repository has been archived by the owner on Dec 12, 2020. It is now read-only.

Commit

Permalink
Use McMaster.NETCore.Plugins for assembly loading
Browse files Browse the repository at this point in the history
* Tasks deprecate `GeneratorAssemblySearchPaths` usage
* Tasks use `CodeGenerationRoslynPlugin` Item instead, which contains
  concrete assembly paths
* Engine targets `netcoreapp2.0` to reference McMaster package
* Tests use Amadevus.RecordGenerator NuGet generator for
  back-compat checks
* Tests.Generators use Bogus NuGet for NuGet dependency resolution check
  • Loading branch information
amis92 committed Dec 9, 2019
1 parent edf706b commit e888db1
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
PrepareGenerateCodeFromAttributes;
GenerateCodeFromAttributesCore
</GenerateCodeFromAttributesDependsOn>
<CgrUrl>https://github.com/AArnott/CodeGeneration.Roslyn</CgrUrl>
<WarnOnGeneratorAssemblySearchPaths Condition=" '$(WarnOnGeneratorAssemblySearchPaths)' == '' ">true</WarnOnGeneratorAssemblySearchPaths>
</PropertyGroup>

<Target
Expand All @@ -22,7 +24,21 @@
Include="@(Compile)"
Condition=" '%(Compile.Generator)' == 'MSBuild:GenerateCodeFromAttributes' " />
<DefineConstantsItems Include="$(DefineConstants)" />
<!-- Map GeneratorAssemblySearchPaths to actual DLLs until support is removed -->
<GeneratorAssemblySearchPaths
Update="@(GeneratorAssemblySearchPaths)"
PathWithTrailingSlash="$([MSBuild]::EnsureTrailingSlash(%(Identity)))" />
<_GeneratorAssemblySearchPathsResolved
Include="%(GeneratorAssemblySearchPaths.PathWithTrailingSlash)*.dll" />
<!-- Include resolved DLLs into CodeGenerationRoslynPlugin -->
<CodeGenerationRoslynPlugin
Include="@(_GeneratorAssemblySearchPathsResolved)"
Condition="Exists('%(Identity)')" />
</ItemGroup>
<!-- Warning for the time GeneratorAssemblySearchPaths are deprecated but supported -->
<Warning
Text="Using GeneratorAssemblySearchPaths is deprecated, please use CodeGenerationRoslynPlugin ItemGroup. See $(CgrUrl) for more info. Disable this warning by setting 'WarnOnGeneratorAssemblySearchPaths' property to false."
Condition=" '@(GeneratorAssemblySearchPaths)' != '' and '$(WarnOnGeneratorAssemblySearchPaths)' != 'false' " />
<PropertyGroup>
<GenerateCodeFromAttributesToolPathOverride
Condition="'$(GenerateCodeFromAttributesToolPathOverride)' == ''">codegen</GenerateCodeFromAttributesToolPathOverride>
Expand All @@ -32,7 +48,7 @@
<_CodeGenToolResponseFileLines>
@(ReferencePath->'-r;%(Identity)');
@(DefineConstantsItems->'-d;%(Identity)');
@(GeneratorAssemblySearchPaths->'--generatorSearchPath;%(Identity)');
@(CodeGenerationRoslynPlugin->'--plugin;%(Identity)');
--out;
$(IntermediateOutputPath);
--projectDir;
Expand Down Expand Up @@ -72,7 +88,7 @@
Condition="'$(_GenerateCodeToolVersionExitCode)' == '0'" />
<Error
Code="CGR1001"
Text="CodeGeneration.Roslyn.Tool (dotnet-codegen) is not available, code generation won't run. Please check https://github.com/AArnott/CodeGeneration.Roslyn for usage instructions."
Text="CodeGeneration.Roslyn.Tool (dotnet-codegen) is not available, code generation won't run. Please check $(CgrUrl) for usage instructions."
Condition="'$(_GenerateCodeToolVersionExitCode)' != '0'" />
<ItemGroup>
<FileWrites Include="$(_CodeGenToolResponseFileFullPath)" />
Expand All @@ -86,7 +102,7 @@
StandardOutputImportance="normal" />
<Error
Code="CGR1000"
Text="CodeGeneration.Roslyn.Tool (dotnet-codegen) failed to generate the list of generated files. The tool didn't run successfully. Please check https://github.com/AArnott/CodeGeneration.Roslyn for usage instructions."
Text="CodeGeneration.Roslyn.Tool (dotnet-codegen) failed to generate the list of generated files. The tool didn't run successfully. Please check $(CgrUrl) for usage instructions."
Condition="Exists('$(_CodeGenToolGeneratedFileListFullPath)') != 'true'" />
<ReadLinesFromFile File="$(_CodeGenToolGeneratedFileListFullPath)">
<Output TaskParameter="Lines" ItemName="CodeGenerationRoslynOutput_Compile"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Description>The engine of source code generation used by `dotnet-codegen` tool.</Description>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.0.4" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="0.2.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="[$(RoslynNugetVersion)]" />
<PackageReference Include="Validation" Version="2.4.18" />
</ItemGroup>
Expand Down
137 changes: 23 additions & 114 deletions src/CodeGeneration.Roslyn.Engine/CompilationGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ namespace CodeGeneration.Roslyn.Engine
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using McMaster.NETCore.Plugins;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.DependencyModel.Resolution;
using Validation;

/// <summary>
Expand All @@ -28,16 +26,12 @@ public class CompilationGenerator
{
private const string InputAssembliesIntermediateOutputFileName = "CodeGeneration.Roslyn.InputAssemblies.txt";
private const int ProcessCannotAccessFileHR = unchecked((int)0x80070020);
private static readonly HashSet<string> AllowedAssemblyExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".dll" };

private readonly List<string> emptyGeneratedFiles = new List<string>();
private readonly List<string> generatedFiles = new List<string>();
private readonly List<string> additionalWrittenFiles = new List<string>();
private readonly List<string> loadedAssemblies = new List<string>();
private readonly Dictionary<string, Assembly> assembliesByPath = new Dictionary<string, Assembly>();
private readonly HashSet<string> directoriesWithResolver = new HashSet<string>();
private CompositeCompilationAssemblyResolver assemblyResolver;
private DependencyContext dependencyContext;
private readonly Dictionary<string, (PluginLoader loader, Assembly assembly)> plugins = new Dictionary<string, (PluginLoader, Assembly)>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Gets or sets the list of paths of files to be compiled.
Expand All @@ -55,9 +49,9 @@ public class CompilationGenerator
public IEnumerable<string> PreprocessorSymbols { get; set; }

/// <summary>
/// Gets or sets the paths to directories to search for generator assemblies.
/// Gets or sets the paths to plugins.
/// </summary>
public IReadOnlyList<string> GeneratorAssemblySearchPaths { get; set; }
public IReadOnlyList<string> PluginPaths { get; set; } = new List<string>();

/// <summary>
/// Gets or sets the path to the directory that contains generated source files.
Expand All @@ -84,23 +78,6 @@ public class CompilationGenerator
/// </summary>
public string ProjectDirectory { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="CompilationGenerator"/> class
/// with default dependency resolution and loading.
/// </summary>
public CompilationGenerator()
{
this.assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[]
{
new ReferenceAssemblyPathResolver(),
new PackageCompilationAssemblyResolver(),
});
this.dependencyContext = DependencyContext.Default;

var loadContext = AssemblyLoadContext.GetLoadContext(this.GetType().GetTypeInfo().Assembly);
loadContext.Resolving += this.ResolveAssembly;
}

/// <summary>
/// Runs the code generation as configured using this instance's properties.
/// </summary>
Expand All @@ -110,8 +87,8 @@ public void Generate(IProgress<Diagnostic> progress = null, CancellationToken ca
{
Verify.Operation(this.Compile != null, $"{nameof(Compile)} must be set first.");
Verify.Operation(this.ReferencePath != null, $"{nameof(ReferencePath)} must be set first.");
Verify.Operation(this.PluginPaths != null, $"{nameof(PluginPaths)} must be set first.");
Verify.Operation(this.IntermediateOutputDirectory != null, $"{nameof(IntermediateOutputDirectory)} must be set first.");
Verify.Operation(this.GeneratorAssemblySearchPaths != null, $"{nameof(GeneratorAssemblySearchPaths)} must be set first.");

var compilation = this.CreateCompilation(cancellationToken);

Expand Down Expand Up @@ -146,7 +123,7 @@ public void Generate(IProgress<Diagnostic> progress = null, CancellationToken ca
compilation,
inputSyntaxTree,
this.ProjectDirectory,
this.LoadAssembly,
this.LoadPlugin,
progress).GetAwaiter().GetResult();

var outputText = generatedSyntaxTree.GetText(cancellationToken);
Expand Down Expand Up @@ -195,79 +172,31 @@ public void Generate(IProgress<Diagnostic> progress = null, CancellationToken ca
}
}

private Assembly LoadAssembly(string path)
private Assembly LoadPlugin(AssemblyName assemblyName)
{
if (this.assembliesByPath.ContainsKey(path))
return this.assembliesByPath[path];

var loadContext = AssemblyLoadContext.GetLoadContext(this.GetType().GetTypeInfo().Assembly);
var assembly = loadContext.LoadFromAssemblyPath(path);

var newDependencyContext = DependencyContext.Load(assembly);
if (newDependencyContext != null)
this.dependencyContext = this.dependencyContext.Merge(newDependencyContext);
var basePath = Path.GetDirectoryName(path);
if (!this.directoriesWithResolver.Contains(basePath))
if (plugins.TryGetValue(assemblyName.Name, out var cached))
{
this.assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[]
{
new AppBaseCompilationAssemblyResolver(basePath),
this.assemblyResolver,
});
this.directoriesWithResolver.Add(basePath);
Logger.Info($"CGR retrieved cached plugin for {assemblyName.Name}: {cached.assembly.Location}");
return cached.assembly;
}

this.assembliesByPath.Add(path, assembly);
return assembly;
}

private Assembly ResolveAssembly(AssemblyLoadContext context, AssemblyName name)
{
var library = FindMatchingLibrary(this.dependencyContext.RuntimeLibraries, name);
if (library == null)
return null;
var wrapper = new CompilationLibrary(
library.Type,
library.Name,
library.Version,
library.Hash,
library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths),
library.Dependencies,
library.Serviceable);

var assemblyPaths = new List<string>();
this.assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblyPaths);

if (assemblyPaths.Count == 0)
var pluginPath = PluginPaths.FirstOrDefault(IsRequestedPlugin);
if (pluginPath is null)
{
var matches = from refAssemblyPath in this.ReferencePath
where Path.GetFileNameWithoutExtension(refAssemblyPath).Equals(name.Name, StringComparison.OrdinalIgnoreCase)
select context.LoadFromAssemblyPath(refAssemblyPath);
return matches.FirstOrDefault();
Logger.Info($"CGR didn't find plugin for {assemblyName.Name}");
return null;
}
var loader = PluginLoader.CreateFromAssemblyFile(pluginPath, PluginLoaderOptions.PreferSharedTypes);
var assembly = loader.LoadDefaultAssembly();
plugins[assemblyName.Name] = (loader, assembly);
this.loadedAssemblies.Add(pluginPath);
Logger.Info($"CGR loaded plugin for {assemblyName.Name}: {assembly.Location}");
return assembly;

return assemblyPaths.Select(context.LoadFromAssemblyPath).FirstOrDefault();
}

private static RuntimeLibrary FindMatchingLibrary(IEnumerable<RuntimeLibrary> libraries, AssemblyName name)
{
foreach (var runtime in libraries)
bool IsRequestedPlugin(string path)
{
if (string.Equals(runtime.Name, name.Name, StringComparison.OrdinalIgnoreCase))
{
return runtime;
}

// If the NuGet package name does not exactly match the AssemblyName,
// we check whether the assembly file name is matching
if (runtime.RuntimeAssemblyGroups.Any(
g => g.AssetPaths.Any(
p => string.Equals(Path.GetFileNameWithoutExtension(p), name.Name, StringComparison.OrdinalIgnoreCase))))
{
return runtime;
}
var fileName = Path.GetFileNameWithoutExtension(path);
return string.Equals(assemblyName.Name, fileName, StringComparison.OrdinalIgnoreCase);
}
return null;
}

private static DateTime GetLastModifiedAssemblyTime(string assemblyListPath)
Expand Down Expand Up @@ -315,26 +244,6 @@ private static void ReportError(IProgress<Diagnostic> progress, string id, Synta
progress.Report(reportDiagnostic);
}

private Assembly LoadAssembly(AssemblyName assemblyName)
{
var matchingRefAssemblies = from refPath in this.ReferencePath
where Path.GetFileNameWithoutExtension(refPath).Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase)
select refPath;
var matchingAssemblies = from path in this.GeneratorAssemblySearchPaths
from file in Directory.EnumerateFiles(path, $"{assemblyName.Name}.dll", SearchOption.TopDirectoryOnly)
where AllowedAssemblyExtensions.Contains(Path.GetExtension(file))
select file;

string matchingRefAssembly = matchingRefAssemblies.Concat(matchingAssemblies).FirstOrDefault();
if (matchingRefAssembly != null)
{
this.loadedAssemblies.Add(matchingRefAssembly);
return this.LoadAssembly(matchingRefAssembly);
}

return Assembly.Load(assemblyName);
}

private void SaveGeneratorAssemblyList(string assemblyListPath)
{
// Union our current list with the one on disk, since our incremental code generation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Bogus" Version="28.0.2" />
<PackageReference Include="Validation" Version="2.4.18" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationCon
if (applyToClass != null)
{
copy = applyToClass
.WithIdentifier(SyntaxFactory.Identifier(NameGenerator.Combine(applyToClass.Identifier.ValueText, this.suffix)));
.WithIdentifier(SyntaxFactory.Identifier(NameGenerator.Combine(applyToClass.Identifier.ValueText, this.suffix)))
.WithLeadingTrivia(SyntaxFactory.Comment($"// Bogus content: {new Bogus.Faker().Hacker.Phrase()}"));
}

if (copy != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
<Import Project="..\CodeGeneration.Roslyn.BuildTime\build\CodeGeneration.Roslyn.BuildTime.props" />

<PropertyGroup>
<TargetFrameworks>net462;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>netcoreapp2.1</TargetFrameworks>
<GenerateCodeFromAttributesToolPathOverride>$(OutputPath)..\..\CodeGeneration.Roslyn.Tool\$(Configuration)\netcoreapp2.1\dotnet-codegen.dll</GenerateCodeFromAttributesToolPathOverride>
<WarnOnGeneratorAssemblySearchPaths>false</WarnOnGeneratorAssemblySearchPaths>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Amadevus.RecordGenerator.Attributes" Version="0.4.1" PrivateAssets="all" />
<PackageReference Include="Amadevus.RecordGenerator.Generators" Version="0.4.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(RoslynNugetVersion)" />
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Xunit" Version="2.4.1" />
Expand All @@ -20,7 +23,8 @@

<ItemGroup>
<ProjectReference Include="..\CodeGeneration.Roslyn.Engine\CodeGeneration.Roslyn.Engine.csproj" />
<ProjectReference Include="..\CodeGeneration.Roslyn.Tests.Generators\CodeGeneration.Roslyn.Tests.Generators.csproj" />
<ProjectReference Include="..\CodeGeneration.Roslyn.Tests.Generators\CodeGeneration.Roslyn.Tests.Generators.csproj"
OutputItemType="CodeGenerationRoslynPlugin" />
<ProjectReference Include="..\CodeGeneration.Roslyn.Tool\CodeGeneration.Roslyn.Tool.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
Expand Down
15 changes: 15 additions & 0 deletions src/CodeGeneration.Roslyn.Tests/CodeGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public void ExternalDependencyFound()
d.TestMethodSuffix();
}

[Fact]
public void NuGetRecordGeneratorWorks()
{
var record = new MyRecord(1, "id");
record.ToBuilder();
}

public partial class Wrapper
{
[ExternalDuplicateWithSuffixByName("Suffix")]
Expand All @@ -39,6 +46,14 @@ public void TestMethod()
}
}

[Record]
public partial class MyRecord
{
public int Id { get; }

public string Name { get; }
}

[DuplicateWithSuffixByName("A")]
[DuplicateWithSuffixByType("B")]
public class Foo
Expand Down
Loading

0 comments on commit e888db1

Please sign in to comment.