aspnetcore/src/Microsoft.AspNetCore.Mvc.Ra.../Internal/PrecompileRunCommand.cs

264 lines
9.3 KiB
C#

// 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;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.AspNetCore.Mvc.Razor.ViewCompilation.Internal
{
internal class PrecompileRunCommand
{
private static readonly ParallelOptions ParalellOptions = new ParallelOptions
{
MaxDegreeOfParallelism = 4
};
private CommandLineApplication Application { get; set; }
private MvcServiceProvider MvcServiceProvider { get; set; }
private CompilationOptions Options { get; set; }
private string ProjectPath { get; set; }
public void Configure(CommandLineApplication app)
{
Application = app;
Options = new CompilationOptions(app);
app.OnExecute(() => Execute());
}
private int Execute()
{
if (!ParseArguments())
{
return 1;
}
MvcServiceProvider = new MvcServiceProvider(
ProjectPath,
Options.ApplicationNameOption.Value(),
Options.ContentRootOption.Value(),
Options.ConfigureCompilationType.Value());
var results = GenerateCode();
var success = true;
foreach (var result in results)
{
if (result.CSharpDocument.Diagnostics.Count > 0)
{
success = false;
foreach (var error in result.CSharpDocument.Diagnostics)
{
Application.Error.WriteLine($"{result.ViewFileInfo.FullPath} ({error.Span.LineIndex}): {error.GetMessage()}");
}
}
}
if (!success)
{
return 1;
}
var precompileAssemblyName = $"{Options.ApplicationName}{CompiledViewManfiest.PrecompiledViewsAssemblySuffix}";
var compilation = CompileViews(results, precompileAssemblyName);
var resources = GetResources(results);
var assemblyPath = Path.Combine(Options.OutputPath, precompileAssemblyName + ".dll");
var emitResult = EmitAssembly(
compilation,
MvcServiceProvider.Compiler.EmitOptions,
assemblyPath,
resources);
if (!emitResult.Success)
{
foreach (var diagnostic in emitResult.Diagnostics)
{
Application.Error.WriteLine(CSharpDiagnosticFormatter.Instance.Format(diagnostic));
}
return 1;
}
return 0;
}
private ResourceDescription[] GetResources(ViewCompilationInfo[] results)
{
if (!Options.EmbedViewSourcesOption.HasValue())
{
return new ResourceDescription[0];
}
var resources = new ResourceDescription[results.Length];
for (var i = 0; i < results.Length; i++)
{
var fileInfo = results[i].ViewFileInfo;
resources[i] = new ResourceDescription(
fileInfo.ViewEnginePath,
fileInfo.CreateReadStream,
isPublic: true);
}
return resources;
}
public EmitResult EmitAssembly(
CSharpCompilation compilation,
EmitOptions emitOptions,
string assemblyPath,
ResourceDescription[] resources)
{
EmitResult emitResult;
using (var assemblyStream = new MemoryStream())
{
using (var pdbStream = new MemoryStream())
{
emitResult = compilation.Emit(
assemblyStream,
pdbStream,
manifestResources: resources,
options: emitOptions);
if (emitResult.Success)
{
Directory.CreateDirectory(Path.GetDirectoryName(assemblyPath));
var pdbPath = Path.ChangeExtension(assemblyPath, ".pdb");
assemblyStream.Position = 0;
pdbStream.Position = 0;
// Avoid writing to disk unless the compilation is successful.
using (var assemblyFileStream = File.OpenWrite(assemblyPath))
{
assemblyStream.CopyTo(assemblyFileStream);
}
using (var pdbFileStream = File.OpenWrite(pdbPath))
{
pdbStream.CopyTo(pdbFileStream);
}
}
}
}
return emitResult;
}
private CSharpCompilation CompileViews(ViewCompilationInfo[] results, string assemblyname)
{
var compiler = MvcServiceProvider.Compiler;
var compilation = compiler.CreateCompilation(assemblyname);
var syntaxTrees = new SyntaxTree[results.Length];
Parallel.For(0, results.Length, ParalellOptions, i =>
{
var result = results[i];
var sourceText = SourceText.From(result.CSharpDocument.GeneratedCode, Encoding.UTF8);
var fileInfo = result.ViewFileInfo;
var syntaxTree = compiler.CreateSyntaxTree(sourceText)
.WithFilePath(fileInfo.FullPath ?? fileInfo.ViewEnginePath);
syntaxTrees[i] = syntaxTree;
});
compilation = compilation.AddSyntaxTrees(syntaxTrees);
// Post process the compilation - run ExpressionRewritter and any user specified callbacks.
compilation = ExpressionRewriter.Rewrite(compilation);
var compilationContext = new RoslynCompilationContext(compilation);
MvcServiceProvider.ViewEngineOptions.CompilationCallback(compilationContext);
compilation = AssemblyMetadataGenerator.AddAssemblyMetadata(
compiler,
compilationContext.Compilation,
Options);
return compilation;
}
private bool ParseArguments()
{
ProjectPath = Options.ProjectArgument.Value;
if (string.IsNullOrEmpty(ProjectPath))
{
Application.Error.WriteLine("Project path not specified.");
return false;
}
if (!Options.OutputPathOption.HasValue())
{
Application.Error.WriteLine($"Option {CompilationOptions.OutputPathTemplate} does not specify a value.");
return false;
}
if (!Options.ApplicationNameOption.HasValue())
{
Application.Error.WriteLine($"Option {CompilationOptions.ApplicationNameTemplate} does not specify a value.");
return false;
}
if (!Options.ContentRootOption.HasValue())
{
Application.Error.WriteLine($"Option {CompilationOptions.ContentRootTemplate} does not specify a value.");
return false;
}
return true;
}
private ViewCompilationInfo[] GenerateCode()
{
var files = GetRazorFiles();
var results = new ViewCompilationInfo[files.Count];
Parallel.For(0, results.Length, ParalellOptions, i =>
{
var fileInfo = files[i];
var templateEngine = MvcServiceProvider.TemplateEngine;
ViewCompilationInfo compilationInfo;
using (var fileStream = fileInfo.CreateReadStream())
{
var csharpDocument = templateEngine.GenerateCode(fileInfo.ViewEnginePath);
compilationInfo = new ViewCompilationInfo(fileInfo, csharpDocument);
}
results[i] = compilationInfo;
});
return results;
}
private List<ViewFileInfo> GetRazorFiles()
{
var contentRoot = Options.ContentRootOption.Value();
var viewFiles = Options.ViewsToCompile;
var viewFileInfo = new List<ViewFileInfo>(viewFiles.Count);
var trimLength = contentRoot.EndsWith("/") ? contentRoot.Length - 1 : contentRoot.Length;
for (var i = 0; i < viewFiles.Count; i++)
{
var fullPath = viewFiles[i];
if (fullPath.StartsWith(contentRoot, StringComparison.OrdinalIgnoreCase))
{
var viewEnginePath = fullPath.Substring(trimLength).Replace('\\', '/');
viewFileInfo.Add(new ViewFileInfo(fullPath, viewEnginePath));
}
}
return viewFileInfo;
}
}
}