Skip to content

Commit

Permalink
[release/7.0.2xx-xcode14.3] [dotnet] Fix linker path on Windows (#18298)
Browse files Browse the repository at this point in the history
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.

Backport of #18285.

Co-authored-by: Emanuel Fernandez Dell'Oca <[email protected]>
  • Loading branch information
rolfbjarne and emaf authored May 19, 2023
1 parent edd0594 commit 6d9ed79
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 130 deletions.
8 changes: 8 additions & 0 deletions msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1459,4 +1459,12 @@
{2}: path to a file
</comment>
</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>
139 changes: 13 additions & 126 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/FindAotCompilerTaskBase.cs
Original file line number Diff line number Diff line change
@@ -1,160 +1,47 @@
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 == 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))
Log.LogError (MSBStrings.E7081 /*"The AOT compiler '{0}' does not exist." */, AotCompiler);

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;
}
}
}

48 changes: 48 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/FindILLink.cs
Original file line number Diff line number Diff line change
@@ -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 ();
}
}
}

113 changes: 113 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinBuildTask.cs
Original file line number Diff line number Diff line change
@@ -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 ();
}
}
}

Loading

5 comments on commit 6d9ed79

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

Please sign in to comment.