-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Do build time discovery of MVC ApplicationParts (#598)
* Do build time discovery of MVC ApplicationParts
- Loading branch information
Showing
15 changed files
with
659 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
namespace Microsoft.AspNetCore.Razor.Tasks | ||
{ | ||
public class AssemblyItem | ||
{ | ||
public string Path { get; set; } | ||
|
||
public bool IsSystemReference { get; set; } | ||
|
||
public string AssemblyName { get; set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 169 additions & 0 deletions
169
src/Razor/src/Microsoft.NET.Sdk.Razor/ReferenceResolver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Reflection.Metadata; | ||
using System.Reflection.PortableExecutable; | ||
|
||
namespace Microsoft.AspNetCore.Razor.Tasks | ||
{ | ||
/// <summary> | ||
/// Resolves assemblies that reference one of the specified "targetAssemblies" either directly or transitively. | ||
/// </summary> | ||
public class ReferenceResolver | ||
{ | ||
private readonly HashSet<string> _mvcAssemblies; | ||
private readonly Dictionary<string, ClassifiedAssemblyItem> _lookup = new Dictionary<string, ClassifiedAssemblyItem>(StringComparer.Ordinal); | ||
|
||
public ReferenceResolver(IReadOnlyList<string> targetAssemblies, IReadOnlyList<AssemblyItem> assemblyItems) | ||
{ | ||
_mvcAssemblies = new HashSet<string>(targetAssemblies, StringComparer.Ordinal); | ||
|
||
foreach (var item in assemblyItems) | ||
{ | ||
var classifiedItem = new ClassifiedAssemblyItem(item); | ||
_lookup[item.AssemblyName] = classifiedItem; | ||
} | ||
} | ||
|
||
public IReadOnlyList<string> ResolveAssemblies() | ||
{ | ||
var applicationParts = new List<string>(); | ||
foreach (var item in _lookup) | ||
{ | ||
var classification = Resolve(item.Value); | ||
if (classification == DependencyClassification.ReferencesMvc) | ||
{ | ||
applicationParts.Add(item.Key); | ||
} | ||
|
||
// It's not interesting for us to know if a dependency has a classification of MvcReference. | ||
// All applications targeting the Microsoft.AspNetCore.App will have have a reference to Mvc. | ||
} | ||
|
||
return applicationParts; | ||
} | ||
|
||
private DependencyClassification Resolve(ClassifiedAssemblyItem classifiedItem) | ||
{ | ||
if (classifiedItem.DependencyClassification != DependencyClassification.Unknown) | ||
{ | ||
return classifiedItem.DependencyClassification; | ||
} | ||
|
||
if (classifiedItem.AssemblyItem == null) | ||
{ | ||
// We encountered a dependency that isn't part of this assembly's dependency set. We'll see if it happens to be an MVC assembly. | ||
// This might be useful in scenarios where the app does not have a framework reference at the entry point, | ||
// but the transitive dependency does. | ||
classifiedItem.DependencyClassification = _mvcAssemblies.Contains(classifiedItem.Name) ? | ||
DependencyClassification.MvcReference : | ||
DependencyClassification.DoesNotReferenceMvc; | ||
|
||
return classifiedItem.DependencyClassification; | ||
} | ||
|
||
if (classifiedItem.AssemblyItem.IsSystemReference) | ||
{ | ||
// We do not allow transitive references to MVC via a framework reference to count. | ||
// e.g. depending on Microsoft.AspNetCore.SomeThingNewThatDependsOnMvc would not result in an assembly being treated as | ||
// referencing MVC. | ||
classifiedItem.DependencyClassification = _mvcAssemblies.Contains(classifiedItem.Name) ? | ||
DependencyClassification.MvcReference : | ||
DependencyClassification.DoesNotReferenceMvc; | ||
|
||
return classifiedItem.DependencyClassification; | ||
} | ||
|
||
if (_mvcAssemblies.Contains(classifiedItem.Name)) | ||
{ | ||
classifiedItem.DependencyClassification = DependencyClassification.MvcReference; | ||
return classifiedItem.DependencyClassification; | ||
} | ||
|
||
var dependencyClassification = DependencyClassification.DoesNotReferenceMvc; | ||
foreach (var assemblyItem in GetReferences(classifiedItem.AssemblyItem.Path)) | ||
{ | ||
var classification = Resolve(assemblyItem); | ||
if (classification == DependencyClassification.MvcReference || classification == DependencyClassification.ReferencesMvc) | ||
{ | ||
dependencyClassification = DependencyClassification.ReferencesMvc; | ||
break; | ||
} | ||
} | ||
|
||
classifiedItem.DependencyClassification = dependencyClassification; | ||
return dependencyClassification; | ||
} | ||
|
||
protected virtual IReadOnlyList<ClassifiedAssemblyItem> GetReferences(string file) | ||
{ | ||
try | ||
{ | ||
using var peReader = new PEReader(File.OpenRead(file)); | ||
if (!peReader.HasMetadata) | ||
{ | ||
return Array.Empty<ClassifiedAssemblyItem>(); // not a managed assembly | ||
} | ||
|
||
var metadataReader = peReader.GetMetadataReader(); | ||
|
||
var assemblyItems = new List<ClassifiedAssemblyItem>(); | ||
foreach (var handle in metadataReader.AssemblyReferences) | ||
{ | ||
var reference = metadataReader.GetAssemblyReference(handle); | ||
var referenceName = metadataReader.GetString(reference.Name); | ||
|
||
if (_lookup.TryGetValue(referenceName, out var classifiedItem)) | ||
{ | ||
assemblyItems.Add(classifiedItem); | ||
} | ||
else | ||
{ | ||
// A dependency references an item that isn't referenced by this project. | ||
// We'll construct an item for so that we can calculate the classification based on it's name. | ||
assemblyItems.Add(new ClassifiedAssemblyItem(referenceName)); | ||
} | ||
} | ||
|
||
return assemblyItems; | ||
} | ||
catch (BadImageFormatException) | ||
{ | ||
// not a PE file, or invalid metadata | ||
} | ||
|
||
return Array.Empty<ClassifiedAssemblyItem>(); // not a managed assembly | ||
} | ||
|
||
protected enum DependencyClassification | ||
{ | ||
Unknown, | ||
DoesNotReferenceMvc, | ||
ReferencesMvc, | ||
MvcReference, | ||
} | ||
|
||
protected class ClassifiedAssemblyItem | ||
{ | ||
public ClassifiedAssemblyItem(AssemblyItem classifiedItem) | ||
: this(classifiedItem.AssemblyName) | ||
{ | ||
AssemblyItem = classifiedItem; | ||
} | ||
|
||
public ClassifiedAssemblyItem(string name) | ||
{ | ||
Name = name; | ||
} | ||
|
||
public string Name { get; } | ||
|
||
public AssemblyItem AssemblyItem { get; } | ||
|
||
public DependencyClassification DependencyClassification { get; set; } | ||
} | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
src/Razor/src/Microsoft.NET.Sdk.Razor/ResolveAssemblyWithReferences.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
using Microsoft.Build.Framework; | ||
using Microsoft.Build.Utilities; | ||
|
||
namespace Microsoft.AspNetCore.Razor.Tasks | ||
{ | ||
public class FindAssembliesWithReferencesTo : Task | ||
{ | ||
[Required] | ||
public ITaskItem[] TargetAssemblyNames { get; set; } | ||
|
||
[Required] | ||
public ITaskItem[] Assemblies { get; set; } | ||
|
||
[Output] | ||
public string[] ResolvedAssemblies { get; set; } | ||
|
||
public override bool Execute() | ||
{ | ||
var referenceItems = new List<AssemblyItem>(); | ||
foreach (var item in Assemblies) | ||
{ | ||
const string FusionNameKey = "FusionName"; | ||
var fusionName = item.GetMetadata(FusionNameKey); | ||
if (string.IsNullOrEmpty(fusionName)) | ||
{ | ||
Log.LogError($"Missing required metadata '{FusionNameKey}' for '{item.ItemSpec}."); | ||
return false; | ||
} | ||
|
||
var assemblyName = new AssemblyName(fusionName).Name; | ||
referenceItems.Add(new AssemblyItem | ||
{ | ||
AssemblyName = assemblyName, | ||
IsSystemReference = item.GetMetadata("IsSystemReference") == "true", | ||
Path = item.ItemSpec, | ||
}); | ||
} | ||
|
||
var targetAssemblyNames = TargetAssemblyNames.Select(s => s.ItemSpec).ToList(); | ||
|
||
var provider = new ReferenceResolver(targetAssemblyNames, referenceItems); | ||
var assemblyNames = provider.ResolveAssemblies(); | ||
|
||
ResolvedAssemblies = assemblyNames.ToArray(); | ||
|
||
return !Log.HasLoggedErrors; | ||
} | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
...k.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.MvcApplicationPartsDiscovery.targets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<!-- | ||
*********************************************************************************************** | ||
Microsoft.NET.Sdk.Razor.ApplicationPartsDiscovery | ||
WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have | ||
created a backup copy. Incorrect changes to this file will make it | ||
impossible to load or build your projects from the command-line or the IDE. | ||
Copyright (c) .NET Foundation. All rights reserved. | ||
*********************************************************************************************** | ||
--> | ||
|
||
<Project ToolsVersion="14.0"> | ||
<UsingTask | ||
TaskName="Microsoft.AspNetCore.Razor.Tasks.FindAssembliesWithReferencesTo" | ||
AssemblyFile="$(RazorSdkBuildTasksAssembly)" | ||
Condition="'$(RazorSdkBuildTasksAssembly)' != ''" /> | ||
|
||
<PropertyGroup> | ||
<GenerateMvcApplicationPartsAttribute Condition="'$(GenerateMvcApplicationPartsAssemblyAttributes)' == '' AND '$(OutputType)' == 'Exe'">true</GenerateMvcApplicationPartsAttribute> | ||
|
||
<CoreCompileDependsOn Condition="'$(GenerateMvcApplicationPartsAttribute)' == 'true' AND '$(DesignTimeBuild)' != 'true'"> | ||
_DiscoverMvcApplicationParts; | ||
$(CoreCompileDependsOn); | ||
</CoreCompileDependsOn> | ||
|
||
<_MvcApplicationPartAttributeGeneratedFile>$(IntermediateOutputPath)$(TargetName).MvcApplicationPartsAssemblyInfo$(DefaultLanguageSourceExtension)</_MvcApplicationPartAttributeGeneratedFile> | ||
</PropertyGroup> | ||
|
||
<Target | ||
Name="_DiscoverMvcApplicationParts" | ||
Inputs="$(ProjectAssetsFile);$(MSBuildAllProjects)" | ||
Outputs="$(_MvcApplicationPartAttributeGeneratedFile)" | ||
DependsOnTargets="ResolveAssemblyReferences"> | ||
|
||
<ItemGroup> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Abstractions" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.ApiExplorer" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Core" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Cors" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.DataAnnotations" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Formatters.Json" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Formatters.Xml" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Localization" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Razor" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.RazorPages" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.TagHelpers" /> | ||
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.ViewFeatures" /> | ||
</ItemGroup> | ||
|
||
<FindAssembliesWithReferencesTo Assemblies="@(ReferencePath)" TargetAssemblyNames="@(_MvcAssemblyName)"> | ||
<Output TaskParameter="ResolvedAssemblies" ItemName="_ApplicationPartAssemblyNames"/> | ||
</FindAssembliesWithReferencesTo> | ||
|
||
<ItemGroup> | ||
<_MvcApplicationPartAttribute Include="Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartAttribute"> | ||
<_Parameter1>%(_ApplicationPartAssemblyNames.Identity)</_Parameter1> | ||
</_MvcApplicationPartAttribute> | ||
</ItemGroup> | ||
|
||
<!-- If we found application part assemblies, generate attributes for it and add it to compilation list --> | ||
<WriteCodeFragment | ||
AssemblyAttributes="@(_MvcApplicationPartAttribute)" | ||
Language="$(Language)" | ||
OutputFile="$(_MvcApplicationPartAttributeGeneratedFile)" | ||
Condition="'@(_ApplicationPartAssemblyNames->Count())' != '0'" /> | ||
|
||
<ItemGroup Condition="'@(_ApplicationPartAssemblyNames->Count())' != '0'"> | ||
<Compile Remove="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'!='F#'" /> | ||
<Compile Include="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'!='F#'" /> | ||
|
||
<CompileBefore Remove="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'=='F#'" /> | ||
<CompileBefore Include="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'=='F#'" /> | ||
</ItemGroup> | ||
|
||
<!-- | ||
If we did not find any application parts, produce an empty file which is not added to compilation. | ||
This is required to play nicely with incremental builds. | ||
--> | ||
<Touch | ||
Files="$(_MvcApplicationPartAttributeGeneratedFile)" | ||
AlwaysCreate="true" /> | ||
|
||
<ItemGroup> | ||
<FileWrites Include="$(_MvcApplicationPartAttributeGeneratedFile)" /> | ||
</ItemGroup> | ||
</Target> | ||
</Project> |
Oops, something went wrong.