Skip to content

Commit

Permalink
[msbuild] Port Metal and MetalLib to subclass XamarinTask. Fixes #21437
Browse files Browse the repository at this point in the history
…. (#21439)

This has a few advantages:

* We simplify and unify more of our code.
* We have more control over the error reporting / logging behavior.

Additionally:

* Use 'xcrun' to invoke 'metal' and 'metallib' (partial fix for #3931).
* Allow for overriding the path to the command-line tool in question.
* Add support for cancellation.
* Fix nullability.

Fixes #21437.
  • Loading branch information
rolfbjarne authored Oct 16, 2024
1 parent 34d1fca commit 1fddf20
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 206 deletions.
12 changes: 12 additions & 0 deletions docs/build-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ Example:
```

This property was introduced in .NET 9.

## MetalLibPath

The full path to the `metallib` tool (the Metal Linker).

The default behavior is to use `xcrun metallib`.

## MetalPath

The full path to the Metal compiler.

The default behavior is to use `xcrun metal`.
112 changes: 43 additions & 69 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/Metal.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Threading;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
Expand All @@ -11,139 +12,112 @@
using Xamarin.Localization.MSBuild;
using Xamarin.Messaging.Build.Client;

// Disable until we get around to enable + fix any issues.
#nullable disable

namespace Xamarin.MacDev.Tasks {
public class Metal : XamarinToolTask {
public class Metal : XamarinTask {
CancellationTokenSource? cancellationTokenSource;

#region Inputs

[Required]
public string IntermediateOutputPath { get; set; }
public string IntermediateOutputPath { get; set; } = string.Empty;

public string MetalPath { get; set; } = string.Empty;

[Required]
public string MinimumOSVersion { get; set; }
public string MinimumOSVersion { get; set; } = string.Empty;

[Required]
public string ProjectDir { get; set; }
public string ProjectDir { get; set; } = string.Empty;

[Required]
public string ResourcePrefix { get; set; }
public string ResourcePrefix { get; set; } = string.Empty;

[Required]
public string SdkDevPath { get; set; }
public string SdkDevPath { get; set; } = string.Empty;

[Required]
public string SdkVersion { get; set; }
public string SdkVersion { get; set; } = string.Empty;

[Required]
public bool SdkIsSimulator { get; set; }

[Required]
public string SdkRoot { get; set; }
public string SdkRoot { get; set; } = string.Empty;

[Required]
public ITaskItem SourceFile { get; set; }
public ITaskItem? SourceFile { get; set; }

#endregion

[Output]
public ITaskItem OutputFile { get; set; }

string DevicePlatformBinDir {
get {
switch (Platform) {
case ApplePlatform.iOS:
case ApplePlatform.TVOS:
case ApplePlatform.WatchOS:
return AppleSdkSettings.XcodeVersion.Major >= 11
? Path.Combine (SdkDevPath, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin")
: Path.Combine (SdkDevPath, "Platforms", "iPhoneOS.platform", "usr", "bin");
case ApplePlatform.MacOSX:
case ApplePlatform.MacCatalyst:
return AppleSdkSettings.XcodeVersion.Major >= 10
? Path.Combine (SdkDevPath, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin")
: Path.Combine (SdkDevPath, "Platforms", "MacOSX.platform", "usr", "bin");
default:
throw new InvalidOperationException (string.Format (MSBStrings.InvalidPlatform, Platform));
}
}
}
public ITaskItem? OutputFile { get; set; }

protected override string ToolName {
get { return "metal"; }
}

protected override string GenerateFullPathToTool ()
static string GetExecutable (List<string> arguments, string toolName, string toolPathOverride)
{
if (!string.IsNullOrEmpty (ToolPath))
return Path.Combine (ToolPath, ToolExe);

var path = Path.Combine (DevicePlatformBinDir, ToolExe);

return File.Exists (path) ? path : ToolExe;
if (string.IsNullOrEmpty (toolPathOverride)) {
arguments.Insert (0, toolName);
return "xcrun";
}
return toolPathOverride;
}

public override bool Execute ()
{
if (ShouldExecuteRemotely ())
return new TaskRunner (SessionId, BuildEngine4).RunAsync (this).Result;

if (AppleSdkSettings.XcodeVersion.Major >= 11)
EnvironmentVariables = EnvironmentVariables.CopyAndAdd ($"SDKROOT={SdkRoot}");
return base.Execute ();
}
var env = new Dictionary<string, string?> {
{ "SDKROOT", SdkRoot },
};

protected override string GenerateCommandLineCommands ()
{
var prefixes = BundleResource.SplitResourcePrefixes (ResourcePrefix);
var intermediate = Path.Combine (IntermediateOutputPath, ToolName);
var logicalName = BundleResource.GetLogicalName (ProjectDir, prefixes, SourceFile, !string.IsNullOrEmpty (SessionId));
var intermediate = Path.Combine (IntermediateOutputPath, MetalPath);
var logicalName = BundleResource.GetLogicalName (ProjectDir, prefixes, SourceFile!, !string.IsNullOrEmpty (SessionId));
var path = Path.Combine (intermediate, logicalName);
var args = new CommandLineArgumentBuilder ();
var args = new List<string> ();
var dir = Path.GetDirectoryName (path);

if (!Directory.Exists (dir))
Directory.CreateDirectory (dir);
Directory.CreateDirectory (dir);

OutputFile = new TaskItem (Path.ChangeExtension (path, ".air"));
OutputFile.SetMetadata ("LogicalName", Path.ChangeExtension (logicalName, ".air"));

args.Add ("-arch", "air64");
var executable = GetExecutable (args, "metal", MetalPath);

args.Add ("-arch");
args.Add ("air64");
args.Add ("-emit-llvm");
args.Add ("-c");
args.Add ("-gline-tables-only");
args.Add ("-ffast-math");

args.Add ("-serialize-diagnostics");
args.AddQuoted (Path.ChangeExtension (path, ".dia"));
args.Add (Path.ChangeExtension (path, ".dia"));

args.Add ("-o");
args.AddQuoted (Path.ChangeExtension (path, ".air"));
args.Add (Path.ChangeExtension (path, ".air"));

if (Platform == ApplePlatform.MacCatalyst) {
args.Add ($"-target");
args.Add ($"air64-apple-ios{MinimumOSVersion}-macabi");
} else {
args.Add (PlatformFrameworkHelper.GetMinimumVersionArgument (TargetFrameworkMoniker, SdkIsSimulator, MinimumOSVersion));
}
args.AddQuoted (SourceFile.ItemSpec);
args.Add (SourceFile!.ItemSpec);

return args.ToString ();
}
cancellationTokenSource = new CancellationTokenSource ();
ExecuteAsync (Log, executable, args, environment: env, cancellationToken: cancellationTokenSource.Token).Wait ();

protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
{
// TODO: do proper parsing of error messages and such
Log.LogMessage (messageImportance, "{0}", singleLine);
return !Log.HasLoggedErrors;
}

public override void Cancel ()
public void Cancel ()
{
if (ShouldExecuteRemotely ())
if (ShouldExecuteRemotely ()) {
BuildConnection.CancelAsync (BuildEngine4).Wait ();

base.Cancel ();
} else {
cancellationTokenSource?.Cancel ();
}
}
}
}
103 changes: 36 additions & 67 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/MetalLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
Expand All @@ -10,78 +11,35 @@
using Xamarin.Messaging.Build.Client;
using Xamarin.Utils;

// Disable until we get around to enable + fix any issues.
#nullable disable

namespace Xamarin.MacDev.Tasks {
public class MetalLib : XamarinToolTask, ITaskCallback {
public class MetalLib : XamarinTask, ITaskCallback {
CancellationTokenSource? cancellationTokenSource;

#region Inputs

[Required]
public ITaskItem [] Items { get; set; }
public ITaskItem [] Items { get; set; } = Array.Empty<ITaskItem> ();

public string MetalLibPath { get; set; } = string.Empty;

[Required]
public string OutputLibrary { get; set; }
public string OutputLibrary { get; set; } = string.Empty;

[Required]
public string SdkDevPath { get; set; }
public string SdkDevPath { get; set; } = string.Empty;

[Required]
public string SdkRoot { get; set; }
public string SdkRoot { get; set; } = string.Empty;

#endregion

string DevicePlatformBinDir {
get {
switch (Platform) {
case ApplePlatform.iOS:
case ApplePlatform.TVOS:
case ApplePlatform.WatchOS:
return AppleSdkSettings.XcodeVersion.Major >= 11
? Path.Combine (SdkDevPath, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin")
: Path.Combine (SdkDevPath, "Platforms", "iPhoneOS.platform", "usr", "bin");
case ApplePlatform.MacOSX:
case ApplePlatform.MacCatalyst:
return AppleSdkSettings.XcodeVersion.Major >= 10
? Path.Combine (SdkDevPath, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin")
: Path.Combine (SdkDevPath, "Platforms", "MacOSX.platform", "usr", "bin");
default:
throw new InvalidOperationException (string.Format (MSBStrings.InvalidPlatform, Platform));
}
}
}

protected override string ToolName {
get { return "metallib"; }
}

protected override string GenerateFullPathToTool ()
static string GetExecutable (List<string> arguments, string toolName, string toolPathOverride)
{
if (!string.IsNullOrEmpty (ToolPath))
return Path.Combine (ToolPath, ToolExe);

var path = Path.Combine (DevicePlatformBinDir, ToolExe);

return File.Exists (path) ? path : ToolExe;
}

protected override string GenerateCommandLineCommands ()
{
var args = new CommandLineArgumentBuilder ();

args.Add ("-o");
args.AddQuoted (OutputLibrary);

foreach (var item in Items)
args.AddQuoted (item.ItemSpec);

return args.ToString ();
}

protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
{
// TODO: do proper parsing of error messages and such
Log.LogMessage (messageImportance, "{0}", singleLine);
if (string.IsNullOrEmpty (toolPathOverride)) {
arguments.Insert (0, toolName);
return "xcrun";
}
return toolPathOverride;
}

public override bool Execute ()
Expand All @@ -90,14 +48,24 @@ public override bool Execute ()
return new TaskRunner (SessionId, BuildEngine4).RunAsync (this).Result;

var dir = Path.GetDirectoryName (OutputLibrary);
Directory.CreateDirectory (dir);

if (!Directory.Exists (dir))
Directory.CreateDirectory (dir);
var env = new Dictionary<string, string?> {
{ "SDKROOT", SdkRoot },
};

if (AppleSdkSettings.XcodeVersion.Major >= 11)
EnvironmentVariables = EnvironmentVariables.CopyAndAdd ($"SDKROOT={SdkRoot}");
var args = new List<string> ();
args.Add ("-o");
args.Add (OutputLibrary);
foreach (var item in Items)
args.Add (item.ItemSpec);

var executable = GetExecutable (args, "metallib", MetalLibPath);

cancellationTokenSource = new CancellationTokenSource ();
ExecuteAsync (Log, executable, args, environment: env, cancellationToken: cancellationTokenSource.Token).Wait ();

return base.Execute ();
return !Log.HasLoggedErrors;
}

public bool ShouldCopyToBuildServer (ITaskItem item) => false;
Expand All @@ -106,12 +74,13 @@ public override bool Execute ()

public IEnumerable<ITaskItem> GetAdditionalItemsToBeCopied () => Enumerable.Empty<ITaskItem> ();

public override void Cancel ()
public void Cancel ()
{
base.Cancel ();

if (ShouldExecuteRemotely ())
if (ShouldExecuteRemotely ()) {
BuildConnection.CancelAsync (BuildEngine4).Wait ();
} else {
cancellationTokenSource?.Cancel ();
}
}
}
}
5 changes: 3 additions & 2 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

using Microsoft.Build.Framework;
using Microsoft.Build.Tasks;
Expand Down Expand Up @@ -120,15 +121,15 @@ protected System.Threading.Tasks.Task<Execution> ExecuteAsync (string fileName,
return ExecuteAsync (Log, fileName, arguments, sdkDevPath, environment, mergeOutput, showErrorIfFailure, workingDirectory);
}

internal protected static async System.Threading.Tasks.Task<Execution> ExecuteAsync (TaskLoggingHelper log, string fileName, IList<string> arguments, string? sdkDevPath = null, Dictionary<string, string?>? environment = null, bool mergeOutput = true, bool showErrorIfFailure = true, string? workingDirectory = null)
internal protected static async System.Threading.Tasks.Task<Execution> ExecuteAsync (TaskLoggingHelper log, string fileName, IList<string> arguments, string? sdkDevPath = null, Dictionary<string, string?>? environment = null, bool mergeOutput = true, bool showErrorIfFailure = true, string? workingDirectory = null, CancellationToken? cancellationToken = null)
{
// Create a new dictionary if we're given one, to make sure we don't change the caller's dictionary.
var launchEnvironment = environment is null ? new Dictionary<string, string?> () : new Dictionary<string, string?> (environment);
if (!string.IsNullOrEmpty (sdkDevPath))
launchEnvironment ["DEVELOPER_DIR"] = sdkDevPath;

log.LogMessage (MessageImportance.Normal, MSBStrings.M0001, fileName, StringUtils.FormatArguments (arguments));
var rv = await Execution.RunAsync (fileName, arguments, environment: launchEnvironment, mergeOutput: mergeOutput, workingDirectory: workingDirectory);
var rv = await Execution.RunAsync (fileName, arguments, environment: launchEnvironment, mergeOutput: mergeOutput, workingDirectory: workingDirectory, cancellationToken: cancellationToken);
log.LogMessage (rv.ExitCode == 0 ? MessageImportance.Low : MessageImportance.High, MSBStrings.M0002, fileName, rv.ExitCode);

// Show the output
Expand Down
2 changes: 2 additions & 0 deletions msbuild/Xamarin.Shared/Xamarin.Shared.targets
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
SessionId="$(BuildSessionId)"
Condition="'$(IsMacEnabled)' == 'true' and '%(Metal.Identity)' != ''"
IntermediateOutputPath="$(DeviceSpecificIntermediateOutputPath)"
MetalPath="$(MetalPath)"
MinimumOSVersion="$(_MinimumOSVersion)"
ProjectDir="$(MSBuildProjectDirectory)"
TargetFrameworkMoniker="$(_ComputedTargetFrameworkMoniker)"
Expand All @@ -1417,6 +1418,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
SessionId="$(BuildSessionId)"
Condition="'$(IsMacEnabled)' == 'true'"
Items="@(_SmeltedMetal)"
MetalLibPath="$(MetalLibPath)"
SdkDevPath="$(_SdkDevPath)"
SdkRoot="$(_SdkRoot)"
OutputLibrary="$(_AppResourcesPath)default.metallib">
Expand Down
Loading

6 comments on commit 1fddf20

@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.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

Please sign in to comment.