[dotnet] Fix linker path on Windows (#18285)

When building from Windows, we need to pass the path to the illink
assembly located on the Mac to the linker task. The educated guess we've
been using is a bit fragile and has been getting us problems almost on
each new .NET major release. On top of that, from .NET 8 the linker is
in a separate NuGet package, so the assembly is no longer located in the
SDK directory on the Mac.

The fix is to follow the same approach we use to find the AOT compiler
on the Mac by running a target that finds that information on the remote
Mac, and brings it back to Windows, where it is cached and use across
build.

Created a new XamarinBuildTask class to share most of the code needed
for this approach.

---------

Co-authored-by: GitHub Actions Autoformatter <github-actions-autoformatter@xamarin.com>
Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
This commit is contained in:
Emanuel Fernandez Dell'Oca 2023-05-17 15:56:34 -04:00 коммит произвёл GitHub
Родитель 2906f0202d
Коммит 73fefdb0c1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 216 добавлений и 130 удалений

Просмотреть файл

@ -1497,4 +1497,11 @@
<value>The "{0}" task was not given a value for the parameter "{1}", which is required when building on this platform.</value>
</data>
<data name="E7115" xml:space="preserve">
<value>The illink assembly doesn't exist: '{0}'.</value>
<comment>
{0}: the path to the linker assembly
</comment>
</data>
</root>

Просмотреть файл

@ -1,37 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Xamarin.Localization.MSBuild;
using Threading = System.Threading.Tasks;
namespace Xamarin.MacDev.Tasks {
public abstract class FindAotCompilerTaskBase : XamarinTask {
public abstract class FindAotCompilerTaskBase : XamarinBuildTask {
[Required]
public ITaskItem [] MonoAotCrossCompiler { get; set; }
public bool KeepTemporaryOutput { get; set; }
[Required]
public string RuntimeIdentifier { get; set; }
[Output]
public string AotCompiler { get; set; }
public override bool Execute ()
{
// If we can't find the AOT compiler path in MonoAotCrossCompiler, evaluate a project file that does know how to find it.
// This happens when executing remotely from Windows, because the MonoAotCrossCompiler item group will be empty in that case.
var targetName = "ComputeAotCompilerPath";
var target = $@"<Target Name=""{targetName}"">
<PropertyGroup>
<_XamarinAOTCompiler>@(MonoAotCrossCompiler->WithMetadataValue(""RuntimeIdentifier"", ""$(RuntimeIdentifier)""))</_XamarinAOTCompiler>
</PropertyGroup>
<WriteLinesToFile File=""$(OutputFilePath)"" Lines=""$(_XamarinAOTCompiler)"" />
</Target>";
if (MonoAotCrossCompiler?.Length > 0 && string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("XAMARIN_FORCE_AOT_COMPILER_PATH_COMPUTATION"))) {
var aotCompilerItem = MonoAotCrossCompiler.SingleOrDefault (v => v.GetMetadata ("RuntimeIdentifier") == RuntimeIdentifier);
if (aotCompilerItem is null) {
Log.LogMessage (MessageImportance.Low, "Unable to find the AOT compiler for the RuntimeIdentifier '{0}' in the MonoAotCrossCompiler item group", RuntimeIdentifier);
AotCompiler = ComputeAotCompilerPath ();
AotCompiler = ComputeValueUsingTarget (target, targetName);
} else {
AotCompiler = aotCompilerItem.ItemSpec;
}
} else {
AotCompiler = ComputeAotCompilerPath ();
AotCompiler = ComputeValueUsingTarget (target, targetName);
}
if (!File.Exists (AotCompiler))
@ -39,122 +42,6 @@ namespace Xamarin.MacDev.Tasks {
return !Log.HasLoggedErrors;
}
// If we can't find the AOT compiler path in MonoAotCrossCompiler, evaluate a project file that does know how to find it.
// This happens when executing remotely from Windows, because the MonoAotCrossCompiler item group will be empty in that case.
string ComputeAotCompilerPath ()
{
var projectPath = Path.GetTempFileName ();
File.Delete (projectPath);
projectPath += ".csproj";
var csproj = $@"<?xml version=""1.0"" encoding=""utf-8""?>
<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>net{TargetFramework.Version}-{PlatformName}</TargetFramework>
</PropertyGroup>
<Target Name=""ComputeAotCompilerPath"">
<PropertyGroup>
<_XamarinAOTCompiler>@(MonoAotCrossCompiler->WithMetadataValue(""RuntimeIdentifier"", ""$(RuntimeIdentifier)""))</_XamarinAOTCompiler>
</PropertyGroup>
<WriteLinesToFile File=""$(OutputFilePath)"" Lines=""$(_XamarinAOTCompiler)"" />
</Target>
</Project>
";
File.WriteAllText (projectPath, csproj);
var dotnetPath = Environment.GetEnvironmentVariable ("DOTNET_HOST_PATH");
if (string.IsNullOrEmpty (dotnetPath)) {
dotnetPath = "dotnet";
}
var environment = default (Dictionary<string, string>);
var customHome = Environment.GetEnvironmentVariable ("DOTNET_CUSTOM_HOME");
if (!string.IsNullOrEmpty (customHome)) {
environment = new Dictionary<string, string> { { "HOME", customHome } };
}
try {
ExecuteRestoreAsync (dotnetPath, projectPath, environment).Wait ();
return ExecuteBuildAsync (dotnetPath, projectPath, environment).Result;
} finally {
if (KeepTemporaryOutput) {
Log.LogMessage (MessageImportance.Normal, $"Temporary project for the FindAotCompiler task: {projectPath}");
} else {
File.Delete (projectPath);
}
}
}
async Threading.Task ExecuteRestoreAsync (string dotnetPath, string projectPath, Dictionary<string, string> environment)
{
var binlog = GetTempBinLog ();
var arguments = new List<string> ();
arguments.Add ("restore");
var dotnetDir = Path.GetDirectoryName (dotnetPath);
var configFile = Path.Combine (dotnetDir, "NuGet.config");
if (File.Exists (configFile)) {
arguments.Add ("/p:RestoreConfigFile=" + configFile);
}
arguments.Add ("/bl:" + binlog);
arguments.Add (projectPath);
try {
await ExecuteAsync (dotnetPath, arguments, environment: environment);
} finally {
if (KeepTemporaryOutput) {
Log.LogMessage (MessageImportance.Normal, $"Temporary restore log for the FindAotCompiler task: {binlog}");
} else {
File.Delete (binlog);
}
}
}
async Threading.Task<string> ExecuteBuildAsync (string dotnetPath, string projectPath, Dictionary<string, string> environment)
{
var outputFile = Path.GetTempFileName ();
var binlog = GetTempBinLog ();
var arguments = new List<string> ();
arguments.Add ("build");
arguments.Add ("/p:OutputFilePath=" + outputFile);
arguments.Add ("/p:RuntimeIdentifier=" + RuntimeIdentifier);
arguments.Add ("/t:ComputeAotCompilerPath");
arguments.Add ("/bl:" + binlog);
arguments.Add (projectPath);
try {
await ExecuteAsync (dotnetPath, arguments, environment: environment);
return File.ReadAllText (outputFile).Trim ();
} finally {
if (KeepTemporaryOutput) {
Log.LogMessage (MessageImportance.Normal, $"Temporary output for the FindAotCompiler task: {outputFile}");
Log.LogMessage (MessageImportance.Normal, $"Temporary build log for the FindAotCompiler task: {binlog}");
} else {
File.Delete (outputFile);
File.Delete (binlog);
}
}
}
string GetTempBinLog ()
{
var binlog = Path.GetTempFileName ();
File.Delete (binlog);
binlog += ".binlog";
return binlog;
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Tasks;
using Xamarin.Localization.MSBuild;
using Xamarin.Messaging.Build.Client;
namespace Xamarin.MacDev.Tasks {
public class FindILLink : XamarinBuildTask, ITaskCallback, ICancelableTask {
[Output]
public string ILLinkPath { get; set; }
public override bool Execute ()
{
if (this.ShouldExecuteRemotely (SessionId))
return new TaskRunner (SessionId, BuildEngine4).RunAsync (this).Result;
var targetName = "ComputeILLinkTaskPath";
var target = $@"<Target Name=""{targetName}"">
<WriteLinesToFile File=""$(OutputFilePath)"" Lines=""$(ILLinkTasksAssembly)"" />
</Target>";
var illinkTaskPath = ComputeValueUsingTarget (target, targetName);
if (!string.IsNullOrEmpty (illinkTaskPath))
ILLinkPath = Path.Combine (Path.GetDirectoryName (illinkTaskPath), "illink.dll");
if (!File.Exists (ILLinkPath))
Log.LogError (MSBStrings.E7115 /*"The illink assembly doesn't exist: '{0}'" */, ILLinkPath);
return !Log.HasLoggedErrors;
}
public IEnumerable<ITaskItem> GetAdditionalItemsToBeCopied () => Enumerable.Empty<ITaskItem> ();
public bool ShouldCopyToBuildServer (ITaskItem item) => false;
public bool ShouldCreateOutputFile (ITaskItem item) => false;
public void Cancel ()
{
if (ShouldExecuteRemotely ())
BuildConnection.CancelAsync (BuildEngine4).Wait ();
}
}
}

Просмотреть файл

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Xamarin.Localization.MSBuild;
using Threading = System.Threading.Tasks;
namespace Xamarin.MacDev.Tasks {
public abstract class XamarinBuildTask : XamarinTask {
public bool KeepTemporaryOutput { get; set; }
[Required]
public string RuntimeIdentifier { get; set; }
/// <summary>
/// Runs the target passed in computeValueTarget and returns its result.
/// The target must write the result into a text file using $(OutputFilePath) as path.
/// </summary>
/// <returns></returns>
protected string ComputeValueUsingTarget (string computeValueTarget, string targetName)
{
var projectDirectory = Path.GetTempFileName ();
File.Delete (projectDirectory);
Directory.CreateDirectory (projectDirectory);
var projectPath = Path.Combine (projectDirectory, targetName + ".csproj");
var csproj = $@"<?xml version=""1.0"" encoding=""utf-8""?>
<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>net{TargetFramework.Version}-{PlatformName}</TargetFramework>
</PropertyGroup>
{computeValueTarget}
</Project>
";
File.WriteAllText (projectPath, csproj);
var dotnetPath = Environment.GetEnvironmentVariable ("DOTNET_HOST_PATH");
if (string.IsNullOrEmpty (dotnetPath)) {
dotnetPath = "dotnet";
}
var environment = default (Dictionary<string, string>);
var customHome = Environment.GetEnvironmentVariable ("DOTNET_CUSTOM_HOME");
if (!string.IsNullOrEmpty (customHome)) {
environment = new Dictionary<string, string> { { "HOME", customHome } };
}
try {
ExecuteRestoreAsync (dotnetPath, projectPath, targetName, environment).Wait ();
return ExecuteBuildAsync (dotnetPath, projectPath, targetName, environment).Result;
} finally {
if (KeepTemporaryOutput) {
Log.LogMessage (MessageImportance.Normal, $"Temporary project directory for the {targetName} task: {projectDirectory}");
} else {
Directory.Delete (projectDirectory, true);
}
}
}
async Threading.Task ExecuteRestoreAsync (string dotnetPath, string projectPath, string targetName, Dictionary<string, string> environment)
{
var projectDirectory = Path.GetDirectoryName (projectPath);
var binlog = Path.Combine (projectDirectory, targetName + ".binlog");
var arguments = new List<string> ();
arguments.Add ("restore");
var dotnetDir = Path.GetDirectoryName (dotnetPath);
var configFile = Path.Combine (dotnetDir, "NuGet.config");
if (File.Exists (configFile)) {
arguments.Add ("/p:RestoreConfigFile=" + configFile);
}
arguments.Add ("/bl:" + binlog);
arguments.Add (projectPath);
try {
await ExecuteAsync (dotnetPath, arguments, environment: environment);
} finally {
if (KeepTemporaryOutput) {
Log.LogMessage (MessageImportance.Normal, $"Temporary restore log for the {targetName} task: {binlog}");
} else {
File.Delete (binlog);
}
}
}
async Threading.Task<string> ExecuteBuildAsync (string dotnetPath, string projectPath, string targetName, Dictionary<string, string> environment)
{
var projectDirectory = Path.GetDirectoryName (projectPath);
var outputFile = Path.Combine (projectDirectory, "Output.txt");
var binlog = Path.Combine (projectDirectory, targetName + ".binlog");
var arguments = new List<string> ();
arguments.Add ("build");
arguments.Add ("/p:OutputFilePath=" + outputFile);
arguments.Add ("/p:RuntimeIdentifier=" + RuntimeIdentifier);
arguments.Add ($"/t:{targetName}");
arguments.Add ("/bl:" + binlog);
arguments.Add (projectPath);
await ExecuteAsync (dotnetPath, arguments, environment: environment);
return File.ReadAllText (outputFile).Trim ();
}
}
}

Просмотреть файл

@ -304,17 +304,48 @@ Copyright (C) 2011-2013 Xamarin. All rights reserved.
</PropertyGroup>
</Target>
<Target Name="_FindILLink" DependsOnTargets="_ComputeVariables">
<PropertyGroup>
<_RemoteILLinkCachePath>$(DeviceSpecificIntermediateOutputPath)remote-illink-path-$(NETCoreSdkVersion).txt</_RemoteILLinkCachePath>
</PropertyGroup>
<!-- This task does not take a SessionId because we cache the path on Windows for remote builds to avoid a round-trip -->
<ReadLinesFromFile
Condition="Exists('$(_RemoteILLinkCachePath)')"
File="$(_RemoteILLinkCachePath)"
>
<Output TaskParameter="Lines" PropertyName="_RemoteILLinkPath" />
</ReadLinesFromFile>
<FindILLink
SessionId="$(BuildSessionId)"
Condition="'$(IsMacEnabled)' == 'true' And '$(_RemoteILLinkPath)' == ''"
KeepTemporaryOutput="$(FindILLinkKeepKeepTemporaryOutput)"
RuntimeIdentifier="$(RuntimeIdentifier)"
TargetFrameworkMoniker="$(_ComputedTargetFrameworkMoniker)"
>
<Output TaskParameter="ILLinkPath" PropertyName="_RemoteILLinkPath" />
</FindILLink>
<!-- This task does not take a SessionId because we cache the path on Windows for remote builds to avoid a round-trip -->
<WriteLinesToFile
File="$(_RemoteILLinkCachePath)"
Lines="$(_RemoteILLinkPath)"
Overwrite="true"
/>
<ItemGroup>
<FileWrites Include="$(_RemoteILLinkCachePath)" />
</ItemGroup>
</Target>
<!-- Overrides Core SDK target to remote the ILLink execution -->
<!-- https://github.com/dotnet/sdk/blob/cdf57465e1cba9e44b5c9a76a403d41b1b8f178c/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets#L76-L132 -->
<Target Name="_RunILLink"
DependsOnTargets="_ComputeManagedAssemblyToLink;PrepareForILLink"
DependsOnTargets="_ComputeManagedAssemblyToLink;PrepareForILLink;_FindILLink"
Inputs="$(MSBuildAllProjects);@(ManagedAssemblyToLink);@(TrimmerRootDescriptor);@(ReferencePath)"
Outputs="$(_LinkSemaphore)">
<PropertyGroup>
<!-- We need to use netX.Y because when building from VS it sets MSBuildRuntimeType to Core and will pick net472 (which doesn't contain the illink assembly) -->
<_RemoteILLinkPath>$(ILLinkTasksAssembly.Replace('$(NetCoreRoot)', '$(_DotNetRootRemoteDirectory)').Replace('net472', 'net$(BundledNETCoreAppTargetFrameworkVersion)').Replace('$([System.IO.Path]::GetFileName('$(ILLinkTasksAssembly)'))', 'illink.dll'))</_RemoteILLinkPath>
<!-- The .NET 7 linker sets _TrimmerDefaultAction instead of TrimmerDefaultAction, so copy that value if it's set (and TrimmerDefaultAction is not set) -->
<TrimmerDefaultAction Condition="'$(TrimmerDefaultAction)' == ''">$(_TrimmerDefaultAction)</TrimmerDefaultAction>