Use a task to launch the linker (#17313)

* Use a task to launch the linker


Fixes https://github.com/aspnet/AspNetCore/issues/17264
This commit is contained in:
Pranav K 2019-11-25 10:09:06 -08:00 committed by GitHub
parent e470aead3e
commit e862ce7cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 325 additions and 44 deletions

View File

@ -0,0 +1,56 @@
// 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.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Blazor.Build
{
// Based on https://github.com/mono/linker/blob/3b329b9481e300bcf4fb88a2eebf8cb5ef8b323b/src/ILLink.Tasks/CreateRootDescriptorFile.cs
public class BlazorCreateRootDescriptorFile : Task
{
[Required]
public ITaskItem[] AssemblyNames { get; set; }
[Required]
public ITaskItem RootDescriptorFilePath { get; set; }
public override bool Execute()
{
using var fileStream = File.Create(RootDescriptorFilePath.ItemSpec);
var assemblyNames = AssemblyNames.Select(a => a.ItemSpec);
WriteRootDescriptor(fileStream, assemblyNames);
return true;
}
internal static void WriteRootDescriptor(Stream stream, IEnumerable<string> assemblyNames)
{
var roots = new XElement("linker");
foreach (var assemblyName in assemblyNames)
{
roots.Add(new XElement("assembly",
new XAttribute("fullname", assemblyName),
new XElement("type",
new XAttribute("fullname", "*"),
new XAttribute("required", "true"))));
}
var xmlWriterSettings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true
};
using var writer = XmlWriter.Create(stream, xmlWriterSettings);
var xDocument = new XDocument(roots);
xDocument.Save(writer);
}
}
}

View File

@ -0,0 +1,189 @@
// 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.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Blazor.Build.Tasks
{
// Based on https://github.com/mono/linker/blob/3b329b9481e300bcf4fb88a2eebf8cb5ef8b323b/src/ILLink.Tasks/LinkTask.cs
public class BlazorILLink : ToolTask
{
private const string DotNetHostPathEnvironmentName = "DOTNET_HOST_PATH";
[Required]
public string ILLinkPath { get; set; }
[Required]
public ITaskItem[] AssemblyPaths { get; set; }
public ITaskItem[] ReferenceAssemblyPaths { get; set; }
[Required]
public ITaskItem[] RootAssemblyNames { get; set; }
[Required]
public ITaskItem OutputDirectory { get; set; }
public ITaskItem[] RootDescriptorFiles { get; set; }
public bool ClearInitLocals { get; set; }
public string ClearInitLocalsAssemblies { get; set; }
public string ExtraArgs { get; set; }
public bool DumpDependencies { get; set; }
private string _dotnetPath;
private string DotNetPath
{
get
{
if (!string.IsNullOrEmpty(_dotnetPath))
{
return _dotnetPath;
}
_dotnetPath = Environment.GetEnvironmentVariable(DotNetHostPathEnvironmentName);
if (string.IsNullOrEmpty(_dotnetPath))
{
throw new InvalidOperationException($"{DotNetHostPathEnvironmentName} is not set");
}
return _dotnetPath;
}
}
protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High;
protected override string ToolName => Path.GetFileName(DotNetPath);
protected override string GenerateFullPathToTool() => DotNetPath;
protected override string GenerateCommandLineCommands() => ILLinkPath;
private static string Quote(string path)
{
return $"\"{path.TrimEnd('\\')}\"";
}
protected override string GenerateResponseFileCommands()
{
var args = new StringBuilder();
if (RootDescriptorFiles != null)
{
foreach (var rootFile in RootDescriptorFiles)
{
args.Append("-x ").AppendLine(Quote(rootFile.ItemSpec));
}
}
foreach (var assemblyItem in RootAssemblyNames)
{
args.Append("-a ").AppendLine(Quote(assemblyItem.ItemSpec));
}
var assemblyNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var assembly in AssemblyPaths)
{
var assemblyPath = assembly.ItemSpec;
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
// If there are multiple paths with the same assembly name, only use the first one.
if (!assemblyNames.Add(assemblyName))
{
continue;
}
args.Append("-reference ")
.AppendLine(Quote(assemblyPath));
var action = assembly.GetMetadata("action");
if ((action != null) && (action.Length > 0))
{
args.Append("-p ");
args.Append(action);
args.Append(" ").AppendLine(Quote(assemblyName));
}
}
if (ReferenceAssemblyPaths != null)
{
foreach (var assembly in ReferenceAssemblyPaths)
{
var assemblyPath = assembly.ItemSpec;
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
// Don't process references for which we already have
// implementation assemblies.
if (assemblyNames.Contains(assemblyName))
{
continue;
}
args.Append("-reference ").AppendLine(Quote(assemblyPath));
// Treat reference assemblies as "skip". Ideally we
// would not even look at the IL, but only use them to
// resolve surface area.
args.Append("-p skip ").AppendLine(Quote(assemblyName));
}
}
if (OutputDirectory != null)
{
args.Append("-out ").AppendLine(Quote(OutputDirectory.ItemSpec));
}
if (ClearInitLocals)
{
args.AppendLine("--enable-opt clearinitlocals");
if ((ClearInitLocalsAssemblies != null) && (ClearInitLocalsAssemblies.Length > 0))
{
args.Append("-m ClearInitLocalsAssemblies ");
args.AppendLine(ClearInitLocalsAssemblies);
}
}
if (ExtraArgs != null)
{
args.AppendLine(ExtraArgs);
}
if (DumpDependencies)
{
args.AppendLine("--dump-dependencies");
}
return args.ToString();
}
protected override bool HandleTaskExecutionErrors()
{
// Show a slightly better error than the standard ToolTask message that says "dotnet" failed.
Log.LogError($"ILLink failed with exited code {ExitCode}.");
return false;
}
protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance)
{
if (!string.IsNullOrEmpty(singleLine) && singleLine.StartsWith("Unhandled exception.", StringComparison.Ordinal))
{
// The Mono linker currently prints out an entire stack trace when the linker fails.
// We want to show something actionable in the VS Error window.
Log.LogError(singleLine);
}
else
{
base.LogEventsFromTextOutput(singleLine, messageImportance);
}
}
}
}

View File

@ -97,6 +97,13 @@
<ItemGroup> <ItemGroup>
<_BlazorDependencyInput Include="@(ReferenceCopyLocalPaths->WithMetadataValue('Extension','.dll')->'%(FullPath)')" /> <_BlazorDependencyInput Include="@(ReferenceCopyLocalPaths->WithMetadataValue('Extension','.dll')->'%(FullPath)')" />
<_WebAssemblyBCLFolder Include="
$(DotNetWebAssemblyBCLPath);
$(DotNetWebAssemblyBCLFacadesPath);
$(DotNetWebAssemblyFrameworkPath)" />
<_WebAssemblyBCLAssembly Include="%(_WebAssemblyBCLFolder.Identity)*.dll" />
</ItemGroup> </ItemGroup>
<MakeDir Directories="$(BlazorIntermediateOutputPath)" /> <MakeDir Directories="$(BlazorIntermediateOutputPath)" />
@ -119,7 +126,7 @@
<Target <Target
Name="_ResolveBlazorOutputsWhenLinked" Name="_ResolveBlazorOutputsWhenLinked"
Condition="'$(BlazorLinkOnBuild)' == 'true'" Condition="'$(BlazorLinkOnBuild)' == 'true'"
DependsOnTargets="_GenerateLinkerDescriptor;_LinkBlazorApplication"> DependsOnTargets="_GenerateBlazorLinkerDescriptor;_LinkBlazorApplication">
<!-- _BlazorLinkerOutputCache records files linked during the last incremental build of the target. Read the contents and assign linked files to be copied to the output. --> <!-- _BlazorLinkerOutputCache records files linked during the last incremental build of the target. Read the contents and assign linked files to be copied to the output. -->
<ReadLinesFromFile File="$(_BlazorLinkerOutputCache)"> <ReadLinesFromFile File="$(_BlazorLinkerOutputCache)">
@ -133,36 +140,27 @@
</ItemGroup> </ItemGroup>
</Target> </Target>
<Target Name="_GenerateLinkerDescriptor" <UsingTask TaskName="BlazorCreateRootDescriptorFile" AssemblyFile="$(BlazorTasksPath)" />
<Target Name="_GenerateBlazorLinkerDescriptor"
Inputs="@(IntermediateAssembly)" Inputs="@(IntermediateAssembly)"
Outputs="$(GeneratedBlazorLinkerDescriptor)" Outputs="$(GeneratedBlazorLinkerDescriptor)"
Condition="'@(BlazorLinkerDescriptor)' == ''"> Condition="'@(BlazorLinkerDescriptor)' == ''">
<!-- Generate linker descriptors if the project doesn't explicitly provide one. --> <!-- Generate linker descriptors if the project doesn't explicitly provide one. -->
<ItemGroup> <BlazorCreateRootDescriptorFile
<_PrepareLinkerDescriptorAssemblyLine Include="@(IntermediateAssembly->'%(FileName)')" /> AssemblyNames="@(IntermediateAssembly->'%(Filename)')"
<_GeneratedLinkerDescriptorLine Include="&lt;linker&gt;" /> RootDescriptorFilePath="$(GeneratedBlazorLinkerDescriptor)" />
<_GeneratedLinkerDescriptorLine Include="@(_PrepareLinkerDescriptorAssemblyLine->'&lt;assembly fullname=&quot;%(Identity)&quot; /&gt;')" />
<_GeneratedLinkerDescriptorLine Include="&lt;/linker&gt;" />
</ItemGroup>
<WriteLinesToFile
Lines="@(_GeneratedLinkerDescriptorLine)"
File="$(GeneratedBlazorLinkerDescriptor)"
Overwrite="true"
WriteOnlyWhenDifferent="True" />
<ItemGroup> <ItemGroup>
<FileWrites Include="$(GeneratedBlazorLinkerDescriptor)" /> <FileWrites Include="$(GeneratedBlazorLinkerDescriptor)" />
</ItemGroup>
<ItemGroup>
<BlazorLinkerDescriptor Include="$(_BlazorBuiltInBclLinkerDescriptor)" />
<BlazorLinkerDescriptor Include="$(GeneratedBlazorLinkerDescriptor)" /> <BlazorLinkerDescriptor Include="$(GeneratedBlazorLinkerDescriptor)" />
<BlazorLinkerDescriptor Include="$(_BlazorBuiltInBclLinkerDescriptor)" />
</ItemGroup> </ItemGroup>
</Target> </Target>
<UsingTask TaskName="BlazorILLink" AssemblyFile="$(BlazorTasksPath)" />
<Target <Target
Name="_LinkBlazorApplication" Name="_LinkBlazorApplication"
Inputs="$(ProjectAssetsFile); Inputs="$(ProjectAssetsFile);
@ -173,39 +171,44 @@
Outputs="$(_BlazorLinkerOutputCache)"> Outputs="$(_BlazorLinkerOutputCache)">
<ItemGroup> <ItemGroup>
<_BlazorDependencyAssembly Include="@(_BlazorDependencyInput)"> <_BlazorDependencyAssembly Include="@(_BlazorDependencyInput)" IsLinkable="$([System.String]::Copy('%(FileName)').StartsWith('System.'))" />
<RelativeDirNoTrailingSlash>$([System.String]::Copy('%(RelativeDir)').TrimEnd('\').TrimEnd('/'))</RelativeDirNoTrailingSlash>
<IsLinkable Condition="$([System.String]::Copy('%(FileName)').StartsWith('System.'))">true</IsLinkable>
</_BlazorDependencyAssembly>
</ItemGroup>
<ItemGroup>
<_WebAssemblyBCLFolder Include="$(DotNetWebAssemblyBCLPath);$(DotNetWebAssemblyBCLFacadesPath);$(DotNetWebAssemblyFrameworkPath)" />
<_BlazorAssembliesToCopy Include="@(IntermediateAssembly->'-a &quot;%(FullPath)&quot;')" />
<_BlazorFolderLookupPaths Include="@(_WebAssemblyBCLFolder->'-d &quot;%(Identity)&quot;')" />
<!-- For linkable assemblies, add their directories as lookup paths --> <_BlazorAssemblyToLink Include="@(_WebAssemblyBCLAssembly)" />
<_BlazorFolderLookupPaths Condition="'%(_BlazorDependencyAssembly.IsLinkable)' == 'true'" Include="@(_BlazorDependencyAssembly->'-d &quot;%(RelativeDirNoTrailingSlash)&quot;')" /> <_BlazorAssemblyToLink Include="@(_BlazorDependencyAssembly)" Condition="'%(_BlazorDependencyAssembly.IsLinkable)' == 'true'" />
<!-- For non-linkable assemblies, reference the .dll directly --> <_BlazorLinkerRoot Include="@(IntermediateAssembly)" />
<_BlazorAssembliesToCopy Condition="'%(_BlazorDependencyAssembly.IsLinkable)' != 'true'" Include="@(_BlazorDependencyAssembly->'-a &quot;%(FullPath)&quot;')" /> <_BlazorLinkerRoot Include="@(_BlazorDependencyAssembly)" Condition="'%(_BlazorDependencyAssembly.IsLinkable)' != 'true'" />
<_BlazorAssemblyDescriptorFiles
Include="@(BlazorLinkerDescriptor->'-x &quot;%(FullPath)&quot;')" Condition="'@(BlazorLinkerDescriptor)' != ''" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions)</_BlazorLinkerAdditionalOptions> <_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions)</_BlazorLinkerAdditionalOptions>
</PropertyGroup> </PropertyGroup>
<!-- Clear the contents of /obj/<<configuration>>/<<targetframework>>/blazor/linker -->
<ItemGroup> <ItemGroup>
<_OldLinkedFile Include="$(BlazorIntermediateLinkerOutputPath)*.dll" /> <_OldLinkedFile Include="$(BlazorIntermediateLinkerOutputPath)*.dll" />
</ItemGroup> </ItemGroup>
<Delete Files="@(_OldLinkedFile)" /> <Delete Files="@(_OldLinkedFile)" />
<!-- Run the linker and put the results in /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker --> <!--
<Exec Command="dotnet &quot;$(MonoLinkerPath)&quot; $(_BlazorLinkerAdditionalOptions) @(_BlazorFolderLookupPaths, ' ') -o &quot;$(BlazorIntermediateLinkerOutputPath)&quot; @(_BlazorAssemblyDescriptorFiles, ' ') @(_BlazorAssembliesToCopy, ' ')" /> When running from Desktop MSBuild, DOTNET_HOST_PATH is not set.
In this case, explicitly specify the path to the dotnet host.
-->
<PropertyGroup Condition=" '$(DOTNET_HOST_PATH)' == '' ">
<_DotNetHostDirectory>$(NetCoreRoot)</_DotNetHostDirectory>
<_DotNetHostFileName>dotnet</_DotNetHostFileName>
<_DotNetHostFileName Condition=" '$(OS)' == 'Windows_NT' ">dotnet.exe</_DotNetHostFileName>
</PropertyGroup>
<BlazorILLink
ILLinkPath="$(MonoLinkerPath)"
AssemblyPaths="@(_BlazorAssemblyToLink)"
RootAssemblyNames="@(_BlazorLinkerRoot)"
RootDescriptorFiles="@(BlazorLinkerDescriptor)"
OutputDirectory="$(BlazorIntermediateLinkerOutputPath)"
ExtraArgs="$(_BlazorLinkerAdditionalOptions)"
ToolExe="$(_DotNetHostFileName)"
ToolPath="$(_DotNetHostDirectory)" />
<ItemGroup> <ItemGroup>
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.dll" /> <_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.dll" />
@ -214,6 +217,7 @@
<WriteLinesToFile File="$(_BlazorLinkerOutputCache)" Lines="@(_LinkerResult)" Overwrite="true" /> <WriteLinesToFile File="$(_BlazorLinkerOutputCache)" Lines="@(_LinkerResult)" Overwrite="true" />
</Target> </Target>
<UsingTask TaskName="ResolveBlazorRuntimeDependencies" AssemblyFile="$(BlazorTasksPath)" /> <UsingTask TaskName="ResolveBlazorRuntimeDependencies" AssemblyFile="$(BlazorTasksPath)" />
<Target <Target
Name="_ResolveBlazorOutputsWhenNotLinked" Name="_ResolveBlazorOutputsWhenNotLinked"
@ -242,13 +246,7 @@
At this point we have decided not to run the linker and instead to just copy the assemblies At this point we have decided not to run the linker and instead to just copy the assemblies
from the BCL referenced by the app the nuget package into the _framework/_bin folder. from the BCL referenced by the app the nuget package into the _framework/_bin folder.
The only thing we need to do here is collect the list of items that will go into _framework/_bin. The only thing we need to do here is collect the list of items that will go into _framework/_bin.
--> -->
<ItemGroup>
<_WebAssemblyBCLFolder Include="$(DotNetWebAssemblyBCLPath);$(DotNetWebAssemblyBCLFacadesPath);$(DotNetWebAssemblyFrameworkPath)" />
<_WebAssemblyBCLAssembly Include="%(_WebAssemblyBCLFolder.Identity)*.dll" />
</ItemGroup>
<ResolveBlazorRuntimeDependencies <ResolveBlazorRuntimeDependencies
EntryPoint="@(IntermediateAssembly)" EntryPoint="@(IntermediateAssembly)"
ApplicationDependencies="@(_BlazorDependencyInput)" ApplicationDependencies="@(_BlazorDependencyInput)"

View File

@ -0,0 +1,38 @@
// 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.Xml.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class BlazorCreateRootDescriptorFileTest
{
[Fact]
public void ProducesRootDescriptor()
{
// Arrange/Act
using var stream = new MemoryStream();
// Act
BlazorCreateRootDescriptorFile.WriteRootDescriptor(
stream,
new[] { "MyApp.dll" });
// Assert
stream.Position = 0;
var document = XDocument.Load(stream);
var rootElement = document.Root;
var assemblyElement = Assert.Single(rootElement.Elements());
Assert.Equal("assembly", assemblyElement.Name.ToString());
Assert.Equal("MyApp.dll", assemblyElement.Attribute("fullname").Value);
var typeElement = Assert.Single(assemblyElement.Elements());
Assert.Equal("type", typeElement.Name.ToString());
Assert.Equal("*", typeElement.Attribute("fullname").Value);
Assert.Equal("true", typeElement.Attribute("required").Value);
}
}
}