Decouple Razor tools from MVC
Adds a loader (with shadow copying in server mode) based on the Roslyn Analyzer loader design. Adds some targets to the Razor SDK that we can use to compute the configuration and extensions. Passes all of the metadata through to the command line tools so they can deal with extensions.
This commit is contained in:
parent
e200b69511
commit
56ead8118a
|
|
@ -0,0 +1,15 @@
|
|||
// 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 Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
||||
{
|
||||
internal class ExtensionInitializer : RazorExtensionInitializer
|
||||
{
|
||||
public override void Initialize(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
RazorExtensions.Register(builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,11 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
[assembly: ProvideRazorExtensionInitializer("MVC-2.0", typeof(ExtensionInitializer))]
|
||||
[assembly: ProvideRazorExtensionInitializer("MVC-2.1", typeof(ExtensionInitializer))]
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
|||
InheritsDirective.Register(builder);
|
||||
SectionDirective.Register(builder);
|
||||
|
||||
builder.Features.Add(new ViewComponentTagHelperDescriptorProvider());
|
||||
|
||||
builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension());
|
||||
builder.AddTargetExtension(new TemplateTargetExtension()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
Set the primary configuration supported by this pacakge as the default configuration for Razor.
|
||||
-->
|
||||
<RazorDefaultConfiguration Condition="'$(RazorDefaultConfiguration)'==''">MVC-2.1</RazorDefaultConfiguration>
|
||||
|
||||
<!-- Override for testing. This path is only correct inside a nuget package. -->
|
||||
<_MvcExtensionAssemblyPath Condition="'$(_MvcExtensionAssemblyPath)'==''">$(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll</_MvcExtensionAssemblyPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -26,7 +29,7 @@
|
|||
<ItemGroup>
|
||||
<RazorExtension Include="MVC-2.1">
|
||||
<AssemblyName>Microsoft.AspNetCore.Mvc.Razor.Extensions</AssemblyName>
|
||||
<AssemblyFilePath>$(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll</AssemblyFilePath>
|
||||
<AssemblyFilePath>$(_MvcExtensionAssemblyPath)</AssemblyFilePath>
|
||||
</RazorExtension>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -73,6 +73,9 @@
|
|||
UseServer="$(UseRazorBuildServer)"
|
||||
ForceServer="$(_RazorForceBuildServer)"
|
||||
PipeName="$(_RazorBuildServerPipeName)"
|
||||
Version="$(RazorLangVersion)"
|
||||
Configuration="@(ResolvedRazorConfiguration)"
|
||||
Extensions="@(ResolvedRazorExtension)"
|
||||
Assemblies="@(RazorReferencePath)"
|
||||
ProjectRoot="$(MSBuildProjectDirectory)"
|
||||
TagHelperManifest="$(_RazorTagHelperOutputCache)">
|
||||
|
|
@ -121,6 +124,9 @@
|
|||
UseServer="$(UseRazorBuildServer)"
|
||||
ForceServer="$(_RazorForceBuildServer)"
|
||||
PipeName="$(_RazorBuildServerPipeName)"
|
||||
Version="$(RazorLangVersion)"
|
||||
Configuration="@(ResolvedRazorConfiguration)"
|
||||
Extensions="@(ResolvedRazorExtension)"
|
||||
Sources="@(RazorGenerateWithTargetPath)"
|
||||
ProjectRoot="$(MSBuildProjectDirectory)"
|
||||
TagHelperManifest="$(_RazorTagHelperOutputCache)" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// 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.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language
|
||||
{
|
||||
internal class AssemblyExtension : RazorExtension
|
||||
{
|
||||
public AssemblyExtension(string extensionName, Assembly assembly)
|
||||
{
|
||||
if (extensionName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(extensionName));
|
||||
}
|
||||
|
||||
if (assembly == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
}
|
||||
|
||||
ExtensionName = extensionName;
|
||||
Assembly = assembly;
|
||||
}
|
||||
|
||||
public override string ExtensionName { get; }
|
||||
|
||||
public Assembly Assembly { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language
|
||||
{
|
||||
internal class EmptyProjectFileSystem : RazorProjectFileSystem
|
||||
{
|
||||
public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath)
|
||||
{
|
||||
NormalizeAndEnsureValidPath(basePath);
|
||||
return Enumerable.Empty<RazorProjectItem>();
|
||||
}
|
||||
|
||||
public override RazorProjectItem GetItem(string path)
|
||||
{
|
||||
NormalizeAndEnsureValidPath(path);
|
||||
return new NotFoundProjectItem(string.Empty, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
|
||||
public class ProvideRazorExtensionInitializerAttribute : Attribute
|
||||
{
|
||||
public ProvideRazorExtensionInitializerAttribute(string extensionName, Type initializerType)
|
||||
{
|
||||
if (extensionName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(extensionName));
|
||||
}
|
||||
|
||||
if (initializerType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(initializerType));
|
||||
}
|
||||
|
||||
ExtensionName = extensionName;
|
||||
InitializerType = initializerType;
|
||||
}
|
||||
|
||||
public string ExtensionName { get; }
|
||||
|
||||
public Type InitializerType { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// 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.Language
|
||||
{
|
||||
public abstract class RazorExtensionInitializer
|
||||
{
|
||||
public abstract void Initialize(RazorProjectEngineBuilder builder);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
|
||||
|
|
@ -71,9 +72,17 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
|
||||
var builder = new DefaultRazorProjectEngineBuilder(configuration, fileSystem);
|
||||
|
||||
// The intialization order is somewhat important.
|
||||
//
|
||||
// Defaults -> Extensions -> Additional customization
|
||||
//
|
||||
// This allows extensions to rely on default features, and customizations to override choices made by
|
||||
// extensions.
|
||||
RazorEngine.AddDefaultPhases(builder.Phases);
|
||||
AddDefaultsFeatures(builder.Features);
|
||||
|
||||
LoadExtensions(builder, configuration.Extensions);
|
||||
|
||||
configure?.Invoke(builder);
|
||||
|
||||
return builder.Build();
|
||||
|
|
@ -142,19 +151,38 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
});
|
||||
}
|
||||
|
||||
internal static void AddDefaultRuntimeFeatures(RazorConfiguration configuration, ICollection<IRazorEngineFeature> features)
|
||||
private static void LoadExtensions(RazorProjectEngineBuilder builder, IReadOnlyList<RazorExtension> extensions)
|
||||
{
|
||||
// Configure options
|
||||
features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: configuration.LanguageVersion));
|
||||
features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: false));
|
||||
}
|
||||
for (var i = 0; i < extensions.Count; i++)
|
||||
{
|
||||
// For now we only handle AssemblyExtension - which is not user-constructable. We're keeping a tight
|
||||
// lid on how things work until we add official support for extensibility everywhere. So, this is
|
||||
// intentionally inflexible for the time being.
|
||||
var extension = extensions[i] as AssemblyExtension;
|
||||
if (extension == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
internal static void AddDefaultDesignTimeFeatures(RazorConfiguration configuration, ICollection<IRazorEngineFeature> features)
|
||||
{
|
||||
// Configure options
|
||||
features.Add(new DefaultRazorParserOptionsFeature(designTime: true, version: configuration.LanguageVersion));
|
||||
features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: true));
|
||||
features.Add(new SuppressChecksumOptionsFeature());
|
||||
// It's not an error to have an assembly with no initializers. This is useful to specify a dependency
|
||||
// that doesn't really provide any Razor configuration.
|
||||
var attributes = extension.Assembly.GetCustomAttributes<ProvideRazorExtensionInitializerAttribute>();
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
// Using extension names and requiring them to line up allows a single assembly to ship multiple
|
||||
// extensions/initializers for different configurations.
|
||||
if (!string.Equals(attribute.ExtensionName, extension.ExtensionName, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// There's no real protection/exception handling here because this set isn't really user-extensible
|
||||
// right now. This would be a great place to add some additional diagnostics and hardening in the
|
||||
// future.
|
||||
var initializer = (RazorExtensionInitializer)Activator.CreateInstance(attribute.InitializerType);
|
||||
initializer.Initialize(builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
{
|
||||
public abstract class RazorProjectFileSystem : RazorProject
|
||||
{
|
||||
internal static readonly RazorProjectFileSystem Empty = new EmptyProjectFileSystem();
|
||||
|
||||
/// <summary>
|
||||
/// Create a Razor project file system based off of a root directory.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,18 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
|||
private const string GeneratedOutput = "GeneratedOutput";
|
||||
private const string TargetPath = "TargetPath";
|
||||
private const string FullPath = "FullPath";
|
||||
private const string Identity = "Identity";
|
||||
private const string AssemblyName = "AssemblyName";
|
||||
private const string AssemblyFilePath = "AssemblyFilePath";
|
||||
|
||||
[Required]
|
||||
public string Version { get; set; }
|
||||
|
||||
[Required]
|
||||
public ITaskItem[] Configuration { get; set; }
|
||||
|
||||
[Required]
|
||||
public ITaskItem[] Extensions { get; set; }
|
||||
|
||||
[Required]
|
||||
public ITaskItem[] Sources { get; set; }
|
||||
|
|
@ -36,6 +48,16 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
|||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < Extensions.Length; i++)
|
||||
{
|
||||
if (!EnsureRequiredMetadata(Extensions[i], Identity) ||
|
||||
!EnsureRequiredMetadata(Extensions[i], AssemblyName) ||
|
||||
!EnsureRequiredMetadata(Extensions[i], AssemblyFilePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return base.ValidateParameters();
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +87,21 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
|||
builder.AppendLine("-t");
|
||||
builder.AppendLine(TagHelperManifest);
|
||||
|
||||
builder.AppendLine("-v");
|
||||
builder.AppendLine(Version);
|
||||
|
||||
builder.AppendLine("-c");
|
||||
builder.AppendLine(Configuration[0].GetMetadata(Identity));
|
||||
|
||||
for (var i = 0; i < Extensions.Length; i++)
|
||||
{
|
||||
builder.AppendLine("-n");
|
||||
builder.AppendLine(Extensions[i].GetMetadata(Identity));
|
||||
|
||||
builder.AppendLine("-e");
|
||||
builder.AppendLine(Path.GetFullPath(Extensions[i].GetMetadata(AssemblyFilePath)));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,19 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
|||
{
|
||||
public class RazorTagHelper : DotNetToolTask
|
||||
{
|
||||
private const string Identity = "Identity";
|
||||
private const string AssemblyName = "AssemblyName";
|
||||
private const string AssemblyFilePath = "AssemblyFilePath";
|
||||
|
||||
[Required]
|
||||
public string Version { get; set; }
|
||||
|
||||
[Required]
|
||||
public ITaskItem[] Configuration { get; set; }
|
||||
|
||||
[Required]
|
||||
public ITaskItem[] Extensions { get; set; }
|
||||
|
||||
[Required]
|
||||
public string[] Assemblies { get; set; }
|
||||
|
||||
|
|
@ -51,6 +64,21 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
|||
builder.AppendLine("-p");
|
||||
builder.AppendLine(ProjectRoot);
|
||||
|
||||
builder.AppendLine("-v");
|
||||
builder.AppendLine(Version);
|
||||
|
||||
builder.AppendLine("-c");
|
||||
builder.AppendLine(Configuration[0].GetMetadata(Identity));
|
||||
|
||||
for (var i = 0; i < Extensions.Length; i++)
|
||||
{
|
||||
builder.AppendLine("-n");
|
||||
builder.AppendLine(Extensions[i].GetMetadata(Identity));
|
||||
|
||||
builder.AppendLine("-e");
|
||||
builder.AppendLine(Path.GetFullPath(Extensions[i].GetMetadata(AssemblyFilePath)));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
{
|
||||
internal class Application : CommandLineApplication
|
||||
{
|
||||
public Application(CancellationToken cancellationToken)
|
||||
public Application(CancellationToken cancellationToken, ExtensionAssemblyLoader loader, ExtensionDependencyChecker checker)
|
||||
{
|
||||
CancellationToken = cancellationToken;
|
||||
Checker = checker;
|
||||
Loader = loader;
|
||||
|
||||
Name = "rzc";
|
||||
FullName = "Microsoft ASP.NET Core Razor CLI tool";
|
||||
|
|
@ -31,6 +33,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public ExtensionAssemblyLoader Loader { get; }
|
||||
|
||||
public ExtensionDependencyChecker Checker { get; }
|
||||
|
||||
public new int Execute(params string[] args)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -19,6 +19,21 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
|
||||
private class DefaultCompilerHost : CompilerHost
|
||||
{
|
||||
public DefaultCompilerHost()
|
||||
{
|
||||
// The loader needs to live for the lifetime of the server.
|
||||
//
|
||||
// This means that if a request tries to use a set of binaries that are inconsistent with what
|
||||
// the server already has, then it will be rejected to try again on the client.
|
||||
//
|
||||
// We also check each set of extensions for missing depenencies individually, so that we can
|
||||
// consistently reject a request that doesn't specify everything it needs. Otherwise the request
|
||||
// could succeed sometimes if it relies on transient state.
|
||||
Loader = new DefaultExtensionAssemblyLoader(Path.Combine(Path.GetTempPath(), "Razor-Server"));
|
||||
}
|
||||
|
||||
public ExtensionAssemblyLoader Loader { get; }
|
||||
|
||||
public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryParseArguments(request, out var parsed))
|
||||
|
|
@ -28,28 +43,23 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
|
||||
var exitCode = 0;
|
||||
var output = string.Empty;
|
||||
var app = new Application(cancellationToken);
|
||||
var commandArgs = parsed.args.ToArray();
|
||||
|
||||
var writer = ServerLogger.IsLoggingEnabled ? new StringWriter() : TextWriter.Null;
|
||||
|
||||
var checker = new DefaultExtensionDependencyChecker(Loader, writer);
|
||||
var app = new Application(cancellationToken, Loader, checker)
|
||||
{
|
||||
Out = writer,
|
||||
Error = writer,
|
||||
};
|
||||
|
||||
exitCode = app.Execute(commandArgs);
|
||||
|
||||
if (ServerLogger.IsLoggingEnabled)
|
||||
{
|
||||
using (var writer = new StringWriter())
|
||||
{
|
||||
app.Out = writer;
|
||||
app.Error = writer;
|
||||
exitCode = app.Execute(commandArgs);
|
||||
output = writer.ToString();
|
||||
ServerLogger.Log(output);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var writer = new StreamWriter(Stream.Null))
|
||||
{
|
||||
app.Out = writer;
|
||||
app.Error = writer;
|
||||
exitCode = app.Execute(commandArgs);
|
||||
}
|
||||
output = writer.ToString();
|
||||
ServerLogger.Log(output);
|
||||
}
|
||||
|
||||
return new CompletedServerResponse(exitCode, utf8output: false, output: string.Empty);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,241 @@
|
|||
// 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.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Runtime.Loader;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal class DefaultExtensionAssemblyLoader : ExtensionAssemblyLoader
|
||||
{
|
||||
private readonly string _baseDirectory;
|
||||
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<string, (Assembly assembly, AssemblyIdentity identity)> _loadedByPath;
|
||||
private readonly Dictionary<AssemblyIdentity, Assembly> _loadedByIdentity;
|
||||
private readonly Dictionary<string, AssemblyIdentity> _identityCache;
|
||||
private readonly Dictionary<string, List<string>> _wellKnownAssemblies;
|
||||
|
||||
private ShadowCopyManager _shadowCopyManager;
|
||||
|
||||
public DefaultExtensionAssemblyLoader(string baseDirectory)
|
||||
{
|
||||
_baseDirectory = baseDirectory;
|
||||
|
||||
_loadedByPath = new Dictionary<string, (Assembly assembly, AssemblyIdentity identity)>(StringComparer.OrdinalIgnoreCase);
|
||||
_loadedByIdentity = new Dictionary<AssemblyIdentity, Assembly>();
|
||||
_identityCache = new Dictionary<string, AssemblyIdentity>(StringComparer.OrdinalIgnoreCase);
|
||||
_wellKnownAssemblies = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
LoadContext = new ExtensionAssemblyLoadContext(AssemblyLoadContext.GetLoadContext(typeof(ExtensionAssemblyLoader).Assembly), this);
|
||||
}
|
||||
|
||||
protected AssemblyLoadContext LoadContext { get; }
|
||||
|
||||
public override void AddAssemblyLocation(string filePath)
|
||||
{
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(filePath))
|
||||
{
|
||||
throw new ArgumentException(nameof(filePath));
|
||||
}
|
||||
|
||||
var assemblyName = Path.GetFileNameWithoutExtension(filePath);
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_wellKnownAssemblies.TryGetValue(assemblyName, out var paths))
|
||||
{
|
||||
paths = new List<string>();
|
||||
_wellKnownAssemblies.Add(assemblyName, paths);
|
||||
}
|
||||
|
||||
if (!paths.Contains(filePath))
|
||||
{
|
||||
paths.Add(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override Assembly Load(string assemblyName)
|
||||
{
|
||||
if (!AssemblyIdentity.TryParseDisplayName(assemblyName, out var identity))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// First, check if this loader already loaded the requested assembly:
|
||||
if (_loadedByIdentity.TryGetValue(identity, out var assembly))
|
||||
{
|
||||
return assembly;
|
||||
}
|
||||
|
||||
// Second, check if an assembly file of the same simple name was registered with the loader:
|
||||
if (_wellKnownAssemblies.TryGetValue(identity.Name, out var paths))
|
||||
{
|
||||
// Multiple assemblies of the same simple name but different identities might have been registered.
|
||||
// Load the one that matches the requested identity (if any).
|
||||
foreach (var path in paths)
|
||||
{
|
||||
var candidateIdentity = GetIdentity(path);
|
||||
|
||||
if (identity.Equals(candidateIdentity))
|
||||
{
|
||||
return LoadFromPathUnsafe(path, candidateIdentity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only support loading by name from 'well-known' paths. If you need to load something by
|
||||
// name and you get here, then that means we don't know where to look.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override Assembly LoadFromPath(string filePath)
|
||||
{
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(filePath))
|
||||
{
|
||||
throw new ArgumentException(nameof(filePath));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return LoadFromPathUnsafe(filePath, identity: null);
|
||||
}
|
||||
}
|
||||
|
||||
private Assembly LoadFromPathUnsafe(string filePath, AssemblyIdentity identity)
|
||||
{
|
||||
// If we've already loaded the assembly by path there should be nothing else to do,
|
||||
// all of our data is up to date.
|
||||
if (_loadedByPath.TryGetValue(filePath, out var entry))
|
||||
{
|
||||
return entry.assembly;
|
||||
}
|
||||
|
||||
// If we've already loaded the assembly by identity, then we might has some updating
|
||||
// to do.
|
||||
identity = identity ?? GetIdentity(filePath);
|
||||
if (identity != null && _loadedByIdentity.TryGetValue(identity, out var assembly))
|
||||
{
|
||||
// An assembly file might be replaced by another file with a different identity.
|
||||
// Last one wins.
|
||||
_loadedByPath[filePath] = (assembly, identity);
|
||||
return assembly;
|
||||
}
|
||||
|
||||
// Ok we don't have this cached. Let's actually try to load the assembly.
|
||||
assembly = LoadFromPathUnsafeCore(CopyAssembly(filePath));
|
||||
|
||||
identity = identity ?? AssemblyIdentity.FromAssemblyDefinition(assembly);
|
||||
|
||||
// It's possible an assembly was loaded by two different paths. Just use the original then.
|
||||
if (_loadedByIdentity.TryGetValue(identity, out var duplicate))
|
||||
{
|
||||
assembly = duplicate;
|
||||
}
|
||||
else
|
||||
{
|
||||
_loadedByIdentity.Add(identity, assembly);
|
||||
}
|
||||
|
||||
_loadedByPath[filePath] = (assembly, identity);
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private AssemblyIdentity GetIdentity(string filePath)
|
||||
{
|
||||
if (!_identityCache.TryGetValue(filePath, out var identity))
|
||||
{
|
||||
identity = ReadAssemblyIdentity(filePath);
|
||||
_identityCache.Add(filePath, identity);
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
protected virtual string CopyAssembly(string filePath)
|
||||
{
|
||||
if (_baseDirectory == null)
|
||||
{
|
||||
// Don't shadow-copy when base directory is null. This means we're running as a CLI not
|
||||
// a server.
|
||||
return filePath;
|
||||
}
|
||||
|
||||
if (_shadowCopyManager == null)
|
||||
{
|
||||
_shadowCopyManager = new ShadowCopyManager(_baseDirectory);
|
||||
}
|
||||
|
||||
return _shadowCopyManager.AddAssembly(filePath);
|
||||
}
|
||||
|
||||
protected virtual Assembly LoadFromPathUnsafeCore(string filePath)
|
||||
{
|
||||
return LoadContext.LoadFromAssemblyPath(filePath);
|
||||
}
|
||||
|
||||
private static AssemblyIdentity ReadAssemblyIdentity(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
using (var reader = new PEReader(stream))
|
||||
{
|
||||
var metadataReader = reader.GetMetadataReader();
|
||||
return metadataReader.GetAssemblyIdentity();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private class ExtensionAssemblyLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyLoadContext _parent;
|
||||
private readonly DefaultExtensionAssemblyLoader _loader;
|
||||
|
||||
public ExtensionAssemblyLoadContext(AssemblyLoadContext parent, DefaultExtensionAssemblyLoader loader)
|
||||
{
|
||||
_parent = parent;
|
||||
_loader = loader;
|
||||
}
|
||||
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
// Try to load from well-known paths. This will be called when loading a dependency of an extension.
|
||||
var assembly = _loader.Load(assemblyName.ToString());
|
||||
if (assembly != null)
|
||||
{
|
||||
return assembly;
|
||||
}
|
||||
|
||||
// If we don't have an entry, then fall back to the default load context. This allows extensions
|
||||
// to resolve assemblies that are provided by the host.
|
||||
return _parent.LoadFromAssemblyName(assemblyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal class DefaultExtensionDependencyChecker : ExtensionDependencyChecker
|
||||
{
|
||||
// These are treated as prefixes. So `Microsoft.CodeAnalysis.Razor` would be assumed to work.
|
||||
private static readonly string[] DefaultIgnoredAssemblies = new string[]
|
||||
{
|
||||
"mscorlib",
|
||||
"netstandard",
|
||||
"System",
|
||||
"Microsoft.CodeAnalysis",
|
||||
"Microsoft.AspNetCore.Razor.Language",
|
||||
};
|
||||
|
||||
private readonly ExtensionAssemblyLoader _loader;
|
||||
private readonly TextWriter _output;
|
||||
private readonly string[] _ignoredAssemblies;
|
||||
|
||||
public DefaultExtensionDependencyChecker(
|
||||
ExtensionAssemblyLoader loader,
|
||||
TextWriter output,
|
||||
string[] ignoredAssemblies = null)
|
||||
{
|
||||
_loader = loader;
|
||||
_output = output;
|
||||
_ignoredAssemblies = ignoredAssemblies ?? DefaultIgnoredAssemblies;
|
||||
}
|
||||
|
||||
public override bool Check(IEnumerable<string> assmblyFilePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
return CheckCore(assmblyFilePaths);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_output.WriteLine("Exception performing Extension dependency check:");
|
||||
_output.WriteLine(ex.ToString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckCore(IEnumerable<string> assemblyFilePaths)
|
||||
{
|
||||
var items = assemblyFilePaths.Select(a => ExtensionVerificationItem.Create(a)).ToArray();
|
||||
var assemblies = new HashSet<AssemblyIdentity>(items.Select(i => i.Identity));
|
||||
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
_output.WriteLine($"Verifying assembly at {item.FilePath}");
|
||||
|
||||
if (!Path.IsPathRooted(item.FilePath))
|
||||
{
|
||||
_output.WriteLine($"The file path '{item.FilePath}' is not a rooted path. File paths must be absolute and fully-qualified.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var reference in item.References)
|
||||
{
|
||||
if (_ignoredAssemblies.Any(n => reference.Name.StartsWith(n)))
|
||||
{
|
||||
// This is on the allow list, keep going.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assemblies.Contains(reference))
|
||||
{
|
||||
// This was also provided as a dependency, keep going.
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we get here we can't resolve this assembly. This is an error.
|
||||
_output.WriteLine($"Extension assembly '{item.Identity.Name}' depends on '{reference.ToString()} which is missing.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Assuming we get this far, the set of assemblies we have is at least a coherent set (barring
|
||||
// version conflicts). Register all of the paths with the loader so they can find each other by
|
||||
// name.
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
_loader.AddAssemblyLocation(items[i].FilePath);
|
||||
}
|
||||
|
||||
// Now try to load everything. This has the side effect of resolving all of these items
|
||||
// in the loader's caches.
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
item.Assembly = _loader.LoadFromPath(item.FilePath);
|
||||
}
|
||||
|
||||
// Third, check that the MVIDs of the files on disk match the MVIDs of the loaded assemblies.
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
if (item.Mvid != item.Assembly.ManifestModule.ModuleVersionId)
|
||||
{
|
||||
_output.WriteLine($"Extension assembly '{item.Identity.Name}' at '{item.FilePath}' has a different ModuleVersionId than loaded assembly '{item.Assembly.FullName}'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private class ExtensionVerificationItem
|
||||
{
|
||||
public static ExtensionVerificationItem Create(string filePath)
|
||||
{
|
||||
using (var peReader = new PEReader(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)))
|
||||
{
|
||||
var metadataReader = peReader.GetMetadataReader();
|
||||
var identity = metadataReader.GetAssemblyIdentity();
|
||||
var mvid = metadataReader.GetGuid(metadataReader.GetModuleDefinition().Mvid);
|
||||
var references = metadataReader.GetReferencedAssembliesOrThrow();
|
||||
|
||||
return new ExtensionVerificationItem(filePath, identity, mvid, references.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private ExtensionVerificationItem(string filePath, AssemblyIdentity identity, Guid mvid, AssemblyIdentity[] references)
|
||||
{
|
||||
FilePath = filePath;
|
||||
Identity = identity;
|
||||
Mvid = mvid;
|
||||
References = references;
|
||||
}
|
||||
|
||||
public string FilePath { get; }
|
||||
|
||||
public Assembly Assembly { get; set; }
|
||||
|
||||
public AssemblyIdentity Identity { get; }
|
||||
|
||||
public Guid Mvid { get; }
|
||||
|
||||
public IReadOnlyList<AssemblyIdentity> References { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ using System.Linq;
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
|
|
@ -26,6 +25,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
Assemblies = Argument("assemblies", "assemblies to search for tag helpers", multipleValues: true);
|
||||
TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue);
|
||||
ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue);
|
||||
Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue);
|
||||
Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue);
|
||||
ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue);
|
||||
ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue);
|
||||
}
|
||||
|
||||
public CommandArgument Assemblies { get; }
|
||||
|
|
@ -34,6 +37,14 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
|
||||
public CommandOption ProjectDirectory { get; }
|
||||
|
||||
public CommandOption Version { get; }
|
||||
|
||||
public CommandOption Configuration { get; }
|
||||
|
||||
public CommandOption ExtensionNames { get; }
|
||||
|
||||
public CommandOption ExtensionFilePaths { get; }
|
||||
|
||||
protected override bool ValidateArguments()
|
||||
{
|
||||
if (string.IsNullOrEmpty(TagHelperManifest.Value()))
|
||||
|
|
@ -53,12 +64,61 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
ProjectDirectory.Values.Add(Environment.CurrentDirectory);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Version.Value()))
|
||||
{
|
||||
Error.WriteLine($"{Version.ValueName} must be specified.");
|
||||
return false;
|
||||
}
|
||||
else if (!RazorLanguageVersion.TryParse(Version.Value(), out _))
|
||||
{
|
||||
Error.WriteLine($"{Version.ValueName} is not a valid language version.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Configuration.Value()))
|
||||
{
|
||||
Error.WriteLine($"{Configuration.ValueName} must be specified.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExtensionNames.Values.Count != ExtensionFilePaths.Values.Count)
|
||||
{
|
||||
Error.WriteLine($"{ExtensionNames.ValueName} and {ExtensionFilePaths.ValueName} should have the same number of values.");
|
||||
}
|
||||
|
||||
foreach (var filePath in ExtensionFilePaths.Values)
|
||||
{
|
||||
if (!Path.IsPathRooted(filePath))
|
||||
{
|
||||
Error.WriteLine($"Extension file paths must be fully-qualified, absolute paths.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Parent.Checker.Check(ExtensionFilePaths.Values))
|
||||
{
|
||||
Error.WriteLine($"Extenions could not be loaded. See output for details.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override Task<int> ExecuteCoreAsync()
|
||||
{
|
||||
// Loading all of the extensions should succeed as the dependency checker will have already
|
||||
// loaded them.
|
||||
var extensions = new RazorExtension[ExtensionNames.Values.Count];
|
||||
for (var i = 0; i < ExtensionNames.Values.Count; i++)
|
||||
{
|
||||
extensions[i] = new AssemblyExtension(ExtensionNames.Values[i], Parent.Loader.LoadFromPath(ExtensionFilePaths.Values[i]));
|
||||
}
|
||||
|
||||
var version = RazorLanguageVersion.Parse(Version.Value());
|
||||
var configuration = new RazorConfiguration(version, Configuration.Value(), extensions);
|
||||
|
||||
var result = ExecuteCore(
|
||||
configuration: configuration,
|
||||
projectDirectory: ProjectDirectory.Value(),
|
||||
outputFilePath: TagHelperManifest.Value(),
|
||||
assemblies: Assemblies.Values.ToArray());
|
||||
|
|
@ -66,7 +126,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private int ExecuteCore(string projectDirectory, string outputFilePath, string[] assemblies)
|
||||
private int ExecuteCore(RazorConfiguration configuration, string projectDirectory, string outputFilePath, string[] assemblies)
|
||||
{
|
||||
outputFilePath = Path.Combine(projectDirectory, outputFilePath);
|
||||
|
||||
|
|
@ -76,19 +136,14 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
metadataReferences[i] = MetadataReference.CreateFromFile(assemblies[i]);
|
||||
}
|
||||
|
||||
var engine = RazorEngine.Create((b) =>
|
||||
var engine = RazorProjectEngine.Create(configuration, RazorProjectFileSystem.Empty, b =>
|
||||
{
|
||||
RazorExtensions.Register(b);
|
||||
|
||||
b.Features.Add(new DefaultMetadataReferenceFeature() { References = metadataReferences });
|
||||
b.Features.Add(new CompilationTagHelperFeature());
|
||||
|
||||
// TagHelperDescriptorProviders (actually do tag helper discovery)
|
||||
b.Features.Add(new DefaultTagHelperDescriptorProvider());
|
||||
b.Features.Add(new ViewComponentTagHelperDescriptorProvider());
|
||||
});
|
||||
|
||||
var feature = engine.Features.OfType<ITagHelperFeature>().Single();
|
||||
var feature = engine.Engine.Features.OfType<ITagHelperFeature>().Single();
|
||||
var tagHelpers = feature.GetDescriptors();
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal abstract class ExtensionAssemblyLoader
|
||||
{
|
||||
public abstract void AddAssemblyLocation(string filePath);
|
||||
|
||||
public abstract Assembly Load(string assemblyName);
|
||||
|
||||
public abstract Assembly LoadFromPath(string filePath);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal abstract class ExtensionDependencyChecker
|
||||
{
|
||||
public abstract bool Check(IEnumerable<string> extensionFilePaths);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
using Microsoft.VisualStudio.LanguageServices.Razor;
|
||||
|
|
@ -14,13 +13,6 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal class Builder<T>
|
||||
{
|
||||
public static Builder<T> Make(CommandBase result) => null;
|
||||
|
||||
public static Builder<T> Make(T result) => null;
|
||||
}
|
||||
|
||||
internal class GenerateCommand : CommandBase
|
||||
{
|
||||
public GenerateCommand(Application parent)
|
||||
|
|
@ -31,6 +23,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
RelativePaths = Option("-r", "Relative path", CommandOptionType.MultipleValue);
|
||||
ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue);
|
||||
TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue);
|
||||
Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue);
|
||||
Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue);
|
||||
ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue);
|
||||
ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue);
|
||||
}
|
||||
|
||||
public CommandOption Sources { get; }
|
||||
|
|
@ -43,9 +39,29 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
|
||||
public CommandOption TagHelperManifest { get; }
|
||||
|
||||
public CommandOption Version { get; }
|
||||
|
||||
public CommandOption Configuration { get; }
|
||||
|
||||
public CommandOption ExtensionNames { get; }
|
||||
|
||||
public CommandOption ExtensionFilePaths { get; }
|
||||
|
||||
protected override Task<int> ExecuteCoreAsync()
|
||||
{
|
||||
// Loading all of the extensions should succeed as the dependency checker will have already
|
||||
// loaded them.
|
||||
var extensions = new RazorExtension[ExtensionNames.Values.Count];
|
||||
for (var i = 0; i < ExtensionNames.Values.Count; i++)
|
||||
{
|
||||
extensions[i] = new AssemblyExtension(ExtensionNames.Values[i], Parent.Loader.LoadFromPath(ExtensionFilePaths.Values[i]));
|
||||
}
|
||||
|
||||
var version = RazorLanguageVersion.Parse(Version.Value());
|
||||
var configuration = new RazorConfiguration(version, Configuration.Value(), extensions);
|
||||
|
||||
var result = ExecuteCore(
|
||||
configuration: configuration,
|
||||
projectDirectory: ProjectDirectory.Value(),
|
||||
tagHelperManifest: TagHelperManifest.Value(),
|
||||
sources: Sources.Values,
|
||||
|
|
@ -78,10 +94,48 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
ProjectDirectory.Values.Add(Environment.CurrentDirectory);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Version.Value()))
|
||||
{
|
||||
Error.WriteLine($"{Version.ValueName} must be specified.");
|
||||
return false;
|
||||
}
|
||||
else if (!RazorLanguageVersion.TryParse(Version.Value(), out _))
|
||||
{
|
||||
Error.WriteLine($"{Version.ValueName} is not a valid language version.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Configuration.Value()))
|
||||
{
|
||||
Error.WriteLine($"{Configuration.ValueName} must be specified.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExtensionNames.Values.Count != ExtensionFilePaths.Values.Count)
|
||||
{
|
||||
Error.WriteLine($"{ExtensionNames.ValueName} and {ExtensionFilePaths.ValueName} should have the same number of values.");
|
||||
}
|
||||
|
||||
foreach (var filePath in ExtensionFilePaths.Values)
|
||||
{
|
||||
if (!Path.IsPathRooted(filePath))
|
||||
{
|
||||
Error.WriteLine($"Extension file paths must be fully-qualified, absolute paths.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Parent.Checker.Check(ExtensionFilePaths.Values))
|
||||
{
|
||||
Error.WriteLine($"Extensions could not be loaded. See output for details.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int ExecuteCore(
|
||||
RazorConfiguration configuration,
|
||||
string projectDirectory,
|
||||
string tagHelperManifest,
|
||||
List<string> sources,
|
||||
|
|
@ -97,14 +151,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
GetVirtualRazorProjectSystem(inputItems),
|
||||
RazorProjectFileSystem.Create(projectDirectory),
|
||||
});
|
||||
var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, compositeFileSystem, b =>
|
||||
|
||||
var engine = RazorProjectEngine.Create(configuration, compositeFileSystem, b =>
|
||||
{
|
||||
RazorExtensions.Register(b);
|
||||
|
||||
b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, });
|
||||
});
|
||||
|
||||
var results = GenerateCode(projectEngine, inputItems);
|
||||
var results = GenerateCode(engine, inputItems);
|
||||
|
||||
var success = true;
|
||||
|
||||
|
|
@ -175,14 +228,14 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
return items;
|
||||
}
|
||||
|
||||
private OutputItem[] GenerateCode(RazorProjectEngine projectEngine, SourceItem[] inputs)
|
||||
private OutputItem[] GenerateCode(RazorProjectEngine engine, SourceItem[] inputs)
|
||||
{
|
||||
var outputs = new OutputItem[inputs.Length];
|
||||
Parallel.For(0, outputs.Length, new ParallelOptions() { MaxDegreeOfParallelism = Debugger.IsAttached ? 1 : 4 }, i =>
|
||||
{
|
||||
var inputItem = inputs[i];
|
||||
var projectItem = projectEngine.FileSystem.GetItem(inputItem.FilePath);
|
||||
var codeDocument = projectEngine.Process(projectItem);
|
||||
|
||||
var codeDocument = engine.Process(engine.FileSystem.GetItem(inputItem.FilePath));
|
||||
var csharpDocument = codeDocument.GetCSharpDocument();
|
||||
outputs[i] = new OutputItem(inputItem, csharpDocument);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) Microsoft. 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.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal static class MetadataReaderExtensions
|
||||
{
|
||||
internal static AssemblyIdentity GetAssemblyIdentity(this MetadataReader reader)
|
||||
{
|
||||
if (!reader.IsAssembly)
|
||||
{
|
||||
throw new BadImageFormatException();
|
||||
}
|
||||
|
||||
var definition = reader.GetAssemblyDefinition();
|
||||
|
||||
return CreateAssemblyIdentity(
|
||||
reader,
|
||||
definition.Version,
|
||||
definition.Flags,
|
||||
definition.PublicKey,
|
||||
definition.Name,
|
||||
definition.Culture,
|
||||
isReference: false);
|
||||
}
|
||||
|
||||
internal static AssemblyIdentity[] GetReferencedAssembliesOrThrow(this MetadataReader reader)
|
||||
{
|
||||
var references = new List<AssemblyIdentity>();
|
||||
|
||||
foreach (var referenceHandle in reader.AssemblyReferences)
|
||||
{
|
||||
var reference = reader.GetAssemblyReference(referenceHandle);
|
||||
references.Add(CreateAssemblyIdentity(
|
||||
reader,
|
||||
reference.Version,
|
||||
reference.Flags,
|
||||
reference.PublicKeyOrToken,
|
||||
reference.Name,
|
||||
reference.Culture,
|
||||
isReference: true));
|
||||
}
|
||||
|
||||
return references.ToArray();
|
||||
}
|
||||
|
||||
private static AssemblyIdentity CreateAssemblyIdentity(
|
||||
MetadataReader reader,
|
||||
Version version,
|
||||
AssemblyFlags flags,
|
||||
BlobHandle publicKey,
|
||||
StringHandle name,
|
||||
StringHandle culture,
|
||||
bool isReference)
|
||||
{
|
||||
var publicKeyOrToken = reader.GetBlobContent(publicKey);
|
||||
bool hasPublicKey;
|
||||
|
||||
if (isReference)
|
||||
{
|
||||
hasPublicKey = (flags & AssemblyFlags.PublicKey) != 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assembly definitions never contain a public key token, they only can have a full key or nothing,
|
||||
// so the flag AssemblyFlags.PublicKey does not make sense for them and is ignored.
|
||||
// See Ecma-335, Partition II Metadata, 22.2 "Assembly : 0x20".
|
||||
// This also corresponds to the behavior of the native C# compiler and sn.exe tool.
|
||||
hasPublicKey = !publicKeyOrToken.IsEmpty;
|
||||
}
|
||||
|
||||
if (publicKeyOrToken.IsEmpty)
|
||||
{
|
||||
publicKeyOrToken = default;
|
||||
}
|
||||
|
||||
return new AssemblyIdentity(
|
||||
name: reader.GetString(name),
|
||||
version: version,
|
||||
cultureName: culture.IsNil ? null : reader.GetString(culture),
|
||||
publicKeyOrToken: publicKeyOrToken,
|
||||
hasPublicKey: hasPublicKey,
|
||||
isRetargetable: (flags & AssemblyFlags.Retargetable) != 0,
|
||||
contentType: (AssemblyContentType)((int)(flags & AssemblyFlags.ContentTypeMask) >> 9));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Razor.Extensions\Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CodeAnalysis.Razor\Microsoft.CodeAnalysis.Razor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- This makes it so that the runtimeconfig.json is included as part of the build output of the project that references this project. -->
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
var cancel = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (sender, e) => { cancel.Cancel(); };
|
||||
|
||||
var application = new Application(cancel.Token);
|
||||
// Prevent shadow copying.
|
||||
var loader = new DefaultExtensionAssemblyLoader(baseDirectory: null);
|
||||
var checker = new DefaultExtensionDependencyChecker(loader, Console.Error);
|
||||
|
||||
var application = new Application(cancel.Token, loader, checker);
|
||||
return application.Execute(args);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
// Note that this class has no thread-safety guarantees. The caller should use a lock
|
||||
// if concurrency is required.
|
||||
internal class ShadowCopyManager : IDisposable
|
||||
{
|
||||
// Note that this class uses the *existance* of the Mutex to lock a directory.
|
||||
//
|
||||
// Nothing in this code actually ever acquires the Mutex, we just try to see if it exists
|
||||
// already.
|
||||
private readonly Mutex _mutex;
|
||||
|
||||
private int _counter;
|
||||
|
||||
public ShadowCopyManager(string baseDirectory = null)
|
||||
{
|
||||
BaseDirectory = baseDirectory ?? Path.Combine(Path.GetTempPath(), "Razor", "ShadowCopy");
|
||||
|
||||
var guid = Guid.NewGuid().ToString("N").ToLowerInvariant();
|
||||
UniqueDirectory = Path.Combine(BaseDirectory, guid);
|
||||
|
||||
_mutex = new Mutex(initiallyOwned: false, name: guid);
|
||||
|
||||
Directory.CreateDirectory(UniqueDirectory);
|
||||
}
|
||||
|
||||
public string BaseDirectory { get; }
|
||||
|
||||
public string UniqueDirectory { get; }
|
||||
|
||||
public string AddAssembly(string filePath)
|
||||
{
|
||||
var assemblyDirectory = CreateUniqueDirectory();
|
||||
|
||||
var destination = Path.Combine(assemblyDirectory, Path.GetFileName(filePath));
|
||||
CopyFile(filePath, destination);
|
||||
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
|
||||
var resourcesNameWithoutExtension = fileNameWithoutExtension + ".resources";
|
||||
var resourcesNameWithExtension = resourcesNameWithoutExtension + ".dll";
|
||||
|
||||
foreach (var directory in Directory.EnumerateDirectories(Path.GetDirectoryName(filePath)))
|
||||
{
|
||||
var directoryName = Path.GetFileName(directory);
|
||||
|
||||
var resourcesPath = Path.Combine(directory, resourcesNameWithExtension);
|
||||
if (File.Exists(resourcesPath))
|
||||
{
|
||||
var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithExtension);
|
||||
CopyFile(resourcesPath, resourcesShadowCopyPath);
|
||||
}
|
||||
|
||||
resourcesPath = Path.Combine(directory, resourcesNameWithoutExtension, resourcesNameWithExtension);
|
||||
if (File.Exists(resourcesPath))
|
||||
{
|
||||
var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithoutExtension, resourcesNameWithExtension);
|
||||
CopyFile(resourcesPath, resourcesShadowCopyPath);
|
||||
}
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_mutex.ReleaseMutex();
|
||||
}
|
||||
|
||||
public Task PurgeUnusedDirectoriesAsync()
|
||||
{
|
||||
return Task.Run((Action)PurgeUnusedDirectories);
|
||||
}
|
||||
|
||||
private string CreateUniqueDirectory()
|
||||
{
|
||||
var id = _counter++;
|
||||
|
||||
var directory = Path.Combine(UniqueDirectory, id.ToString());
|
||||
Directory.CreateDirectory(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
private void CopyFile(string originalPath, string shadowCopyPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(shadowCopyPath);
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
File.Copy(originalPath, shadowCopyPath);
|
||||
|
||||
MakeWritable(new FileInfo(shadowCopyPath));
|
||||
}
|
||||
|
||||
private void MakeWritable(string directoryPath)
|
||||
{
|
||||
var directory = new DirectoryInfo(directoryPath);
|
||||
|
||||
foreach (var file in directory.EnumerateFiles(searchPattern: "*", searchOption: SearchOption.AllDirectories))
|
||||
{
|
||||
MakeWritable(file);
|
||||
}
|
||||
}
|
||||
|
||||
private void MakeWritable(FileInfo file)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (file.IsReadOnly)
|
||||
{
|
||||
file.IsReadOnly = false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// There are many reasons this could fail. Ignore it and keep going.
|
||||
}
|
||||
}
|
||||
|
||||
private void PurgeUnusedDirectories()
|
||||
{
|
||||
IEnumerable<string> directories;
|
||||
try
|
||||
{
|
||||
directories = Directory.EnumerateDirectories(BaseDirectory);
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
Mutex mutex = null;
|
||||
try
|
||||
{
|
||||
// We only want to try deleting the directory if no-one else is currently using it.
|
||||
//
|
||||
// Note that the mutex name is the name of the directory. This is OK because we're using
|
||||
// GUIDs as directory/mutex names.
|
||||
if (!Mutex.TryOpenExisting(Path.GetFileName(directory).ToLowerInvariant(), out mutex))
|
||||
{
|
||||
MakeWritable(directory);
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If something goes wrong we will leave it to the next run to clean up.
|
||||
// Just swallow the exception and move on.
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (mutex != null)
|
||||
{
|
||||
mutex.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Razor.Tools;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
||||
{
|
||||
|
|
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
|||
throw new TimeoutException($"Shutting down the build server at pipe {PipeName} took longer than expected.");
|
||||
});
|
||||
|
||||
var application = new Application(cts.Token);
|
||||
var application = new Application(cts.Token, Mock.Of<ExtensionAssemblyLoader>(), Mock.Of<ExtensionDependencyChecker>());
|
||||
var exitCode = application.Execute("shutdown", "-w", "-p", PipeName);
|
||||
if (exitCode != 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
{
|
||||
internal class TestRazorProjectFileSystem : DefaultRazorProjectFileSystem
|
||||
{
|
||||
public static RazorProjectFileSystem Empty = new TestRazorProjectFileSystem();
|
||||
public new static RazorProjectFileSystem Empty = new TestRazorProjectFileSystem();
|
||||
|
||||
private readonly Dictionary<string, RazorProjectItem> _lookup;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
// 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.IO;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
public class DefaultExtensionAssemblyLoaderTest
|
||||
{
|
||||
[Fact]
|
||||
public void LoadFromPath_CanLoadAssembly()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
|
||||
var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow"));
|
||||
|
||||
// Act
|
||||
var assembly = loader.LoadFromPath(alphaFilePath);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromPath_DoesNotAddDuplicates_AfterLoadingByName()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
var alphaFilePath2 = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha2.dll");
|
||||
|
||||
var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow"));
|
||||
loader.AddAssemblyLocation(alphaFilePath);
|
||||
|
||||
var assembly1 = loader.Load("Alpha");
|
||||
|
||||
// Act
|
||||
var assembly2 = loader.LoadFromPath(alphaFilePath2);
|
||||
|
||||
// Assert
|
||||
Assert.Same(assembly1, assembly2);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromPath_DoesNotAddDuplicates_AfterLoadingByPath()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
var alphaFilePath2 = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha2.dll");
|
||||
|
||||
var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow"));
|
||||
var assembly1 = loader.LoadFromPath(alphaFilePath);
|
||||
|
||||
// Act
|
||||
var assembly2 = loader.LoadFromPath(alphaFilePath2);
|
||||
|
||||
// Assert
|
||||
Assert.Same(assembly1, assembly2);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_CanLoadAssemblyByName_AfterLoadingByPath()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
|
||||
var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow"));
|
||||
var assembly1 = loader.LoadFromPath(alphaFilePath);
|
||||
|
||||
// Act
|
||||
var assembly2 = loader.Load(assembly1.FullName);
|
||||
|
||||
// Assert
|
||||
Assert.Same(assembly1, assembly2);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromPath_WithDependencyPathsSpecified_CanLoadAssemblyDependencies()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll");
|
||||
var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll");
|
||||
var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll");
|
||||
|
||||
var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow"));
|
||||
loader.AddAssemblyLocation(gammaFilePath);
|
||||
loader.AddAssemblyLocation(deltaFilePath);
|
||||
|
||||
// Act
|
||||
var alpha = loader.LoadFromPath(alphaFilePath);
|
||||
var beta = loader.LoadFromPath(betaFilePath);
|
||||
|
||||
// Assert
|
||||
var builder = new StringBuilder();
|
||||
|
||||
var a = alpha.CreateInstance("Alpha.A");
|
||||
a.GetType().GetMethod("Write").Invoke(a, new object[] { builder, "Test A" });
|
||||
|
||||
var b = beta.CreateInstance("Beta.B");
|
||||
b.GetType().GetMethod("Write").Invoke(b, new object[] { builder, "Test B" });
|
||||
var expected = @"Delta: Gamma: Alpha: Test A
|
||||
Delta: Gamma: Beta: Test B
|
||||
";
|
||||
|
||||
var actual = builder.ToString();
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// 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.IO;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
public class DefaultExtensionDependencyCheckerTest
|
||||
{
|
||||
[Fact]
|
||||
public void Check_ReturnsFalse_WithMissingDependency()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var output = new StringWriter();
|
||||
|
||||
var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
|
||||
var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow"));
|
||||
var checker = new DefaultExtensionDependencyChecker(loader, output);
|
||||
|
||||
// Act
|
||||
var result = checker.Check(new[] { alphaFilePath, });
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Check should not have passed: " + output.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Check_ReturnsTrue_WithAllDependenciesProvided()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var output = new StringWriter();
|
||||
|
||||
var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll");
|
||||
var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll");
|
||||
var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll");
|
||||
|
||||
var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow"));
|
||||
var checker = new DefaultExtensionDependencyChecker(loader, output);
|
||||
|
||||
// Act
|
||||
var result = checker.Check(new[] { alphaFilePath, betaFilePath, gammaFilePath, deltaFilePath, });
|
||||
|
||||
// Assert
|
||||
Assert.True(result, "Check should have passed: " + output.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Check_ReturnsFalse_WhenAssemblyHasDifferentMVID()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var output = new StringWriter();
|
||||
|
||||
// Load Beta.dll from the future Alpha.dll path to prime the assembly loader
|
||||
var alphaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll");
|
||||
var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll");
|
||||
var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll");
|
||||
|
||||
var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow"));
|
||||
var checker = new DefaultExtensionDependencyChecker(loader, output);
|
||||
|
||||
// This will cause the loader to cache some inconsistent information.
|
||||
loader.LoadFromPath(alphaFilePath);
|
||||
LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll");
|
||||
|
||||
// Act
|
||||
var result = checker.Check(new[] { alphaFilePath, gammaFilePath, deltaFilePath, });
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Check should not have passed: " + output.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Check_ReturnsFalse_WhenLoaderThrows()
|
||||
{
|
||||
using (var directory = TempDirectory.Create())
|
||||
{
|
||||
// Arrange
|
||||
var output = new StringWriter();
|
||||
|
||||
var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll");
|
||||
|
||||
var loader = new Mock<ExtensionAssemblyLoader>();
|
||||
loader
|
||||
.Setup(l => l.LoadFromPath(It.IsAny<string>()))
|
||||
.Throws(new InvalidOperationException());
|
||||
var checker = new DefaultExtensionDependencyChecker(loader.Object, output);
|
||||
|
||||
// Act
|
||||
var result = checker.Check(new[] { deltaFilePath, });
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Check should not have passed: " + output.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
|
|
@ -116,7 +117,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
CancellationToken ct,
|
||||
EventBus eventBus,
|
||||
TimeSpan? keepAlive)
|
||||
: base(new Application(ct))
|
||||
: base(new Application(ct, Mock.Of<ExtensionAssemblyLoader>(), Mock.Of<ExtensionDependencyChecker>()))
|
||||
{
|
||||
_host = host;
|
||||
_compilerHost = compilerHost;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal static class LoaderTestResources
|
||||
{
|
||||
static LoaderTestResources()
|
||||
{
|
||||
Delta = CreateAssemblyBlob("Delta", Array.Empty<AssemblyBlob>(), @"
|
||||
using System.Text;
|
||||
|
||||
namespace Delta
|
||||
{
|
||||
public class D
|
||||
{
|
||||
public void Write(StringBuilder sb, string s)
|
||||
{
|
||||
sb.AppendLine(""Delta: "" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
");
|
||||
|
||||
Gamma = CreateAssemblyBlob("Gamma", new[] { Delta, }, @"
|
||||
using System.Text;
|
||||
using Delta;
|
||||
|
||||
namespace Gamma
|
||||
{
|
||||
public class G
|
||||
{
|
||||
public void Write(StringBuilder sb, string s)
|
||||
{
|
||||
D d = new D();
|
||||
|
||||
d.Write(sb, ""Gamma: "" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
");
|
||||
|
||||
Alpha = CreateAssemblyBlob("Alpha", new[] { Gamma, }, @"
|
||||
using System.Text;
|
||||
using Gamma;
|
||||
|
||||
namespace Alpha
|
||||
{
|
||||
public class A
|
||||
{
|
||||
public void Write(StringBuilder sb, string s)
|
||||
{
|
||||
G g = new G();
|
||||
|
||||
g.Write(sb, ""Alpha: "" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
");
|
||||
|
||||
Beta = CreateAssemblyBlob("Beta", new[] { Gamma, }, @"
|
||||
using System.Text;
|
||||
using Gamma;
|
||||
|
||||
namespace Beta
|
||||
{
|
||||
public class B
|
||||
{
|
||||
public void Write(StringBuilder sb, string s)
|
||||
{
|
||||
G g = new G();
|
||||
|
||||
g.Write(sb, ""Beta: "" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
");
|
||||
}
|
||||
|
||||
public static AssemblyBlob Alpha { get; }
|
||||
|
||||
public static AssemblyBlob Beta { get; }
|
||||
|
||||
public static AssemblyBlob Delta { get; }
|
||||
|
||||
public static AssemblyBlob Gamma { get; }
|
||||
|
||||
private static AssemblyBlob CreateAssemblyBlob(string assemblyName, AssemblyBlob[] references, string text)
|
||||
{
|
||||
var defaultReferences = new[]
|
||||
{
|
||||
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
||||
};
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
assemblyName,
|
||||
new[] { CSharpSyntaxTree.ParseText(text) },
|
||||
references.Select(r => r.ToMetadataReference()).Concat(defaultReferences),
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
using (var assemblyStream = new MemoryStream())
|
||||
using (var symbolStream = new MemoryStream())
|
||||
{
|
||||
var result = compilation.Emit(assemblyStream, symbolStream);
|
||||
Assert.Empty(result.Diagnostics);
|
||||
|
||||
return new AssemblyBlob(assemblyName, assemblyStream.GetBuffer(), symbolStream.GetBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
public class AssemblyBlob
|
||||
{
|
||||
public AssemblyBlob(string assemblyName, byte[] assemblyBytes, byte[] symbolBytes)
|
||||
{
|
||||
AssemblyName = assemblyName;
|
||||
AssemblyBytes = assemblyBytes;
|
||||
SymbolBytes = symbolBytes;
|
||||
}
|
||||
|
||||
public string AssemblyName { get; }
|
||||
|
||||
public byte[] AssemblyBytes { get; }
|
||||
|
||||
public byte[] SymbolBytes { get; }
|
||||
|
||||
public MetadataReference ToMetadataReference()
|
||||
{
|
||||
return MetadataReference.CreateFromImage(AssemblyBytes);
|
||||
}
|
||||
|
||||
internal string WriteToFile(string directoryPath, string fileName)
|
||||
{
|
||||
var filePath = Path.Combine(directoryPath, fileName);
|
||||
File.WriteAllBytes(filePath, AssemblyBytes);
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,4 +17,10 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System">
|
||||
<HintPath>System</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
// 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.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// 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.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal class TempDirectory : IDisposable
|
||||
{
|
||||
public static TempDirectory Create()
|
||||
{
|
||||
var directoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n"));
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
return new TempDirectory(directoryPath);
|
||||
}
|
||||
|
||||
private TempDirectory(string directoryPath)
|
||||
{
|
||||
DirectoryPath = directoryPath;
|
||||
}
|
||||
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Directory.Delete(DirectoryPath, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Tools
|
||||
{
|
||||
internal class TestDefaultExtensionAssemblyLoader : DefaultExtensionAssemblyLoader
|
||||
{
|
||||
public TestDefaultExtensionAssemblyLoader(string baseDirectory)
|
||||
: base(baseDirectory)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Assembly LoadFromPathUnsafeCore(string filePath)
|
||||
{
|
||||
// Force a load from streams so we don't lock the files on disk. This way we can test
|
||||
// shadow copying without leaving a mess behind.
|
||||
var bytes = File.ReadAllBytes(filePath);
|
||||
var stream = new MemoryStream(bytes);
|
||||
return LoadContext.LoadFromStream(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,11 @@
|
|||
</PropertyGroup>
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.NET.Sdk.Razor\Sdk\Sdk.props" />
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\build\netstandard2.0\Microsoft.AspNetCore.Razor.Design.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Override for the MVC extension -->
|
||||
<_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll</_MvcExtensionAssemblyPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\build\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@
|
|||
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.NET.Sdk.Razor\Sdk\Sdk.props" />
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\build\netstandard2.0\Microsoft.AspNetCore.Razor.Design.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Override for the MVC extension -->
|
||||
<_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll</_MvcExtensionAssemblyPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\build\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@
|
|||
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.NET.Sdk.Razor\Sdk\Sdk.props" />
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\build\netstandard2.0\Microsoft.AspNetCore.Razor.Design.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Override for the MVC extension -->
|
||||
<_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll</_MvcExtensionAssemblyPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\build\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@
|
|||
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.NET.Sdk.Razor\Sdk\Sdk.props" />
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\build\netstandard2.0\Microsoft.AspNetCore.Razor.Design.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Override for the MVC extension -->
|
||||
<_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll</_MvcExtensionAssemblyPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\build\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
Loading…
Reference in New Issue