From 8cd79a6cdc4e8aec7b3dafec8c4108c34791ed74 Mon Sep 17 00:00:00 2001
From: Peter Collins <pecolli@microsoft.com>
Date: Thu, 26 May 2022 15:49:20 -0400
Subject: [PATCH] [AndroidDotnetToolTask] Use full path to dotnet

Context: https://github.com/xamarin/java.interop/pull/988

A recent attempt to update Java.Interop to target net7.0 produced some
test failures on the Xamarin.Android side:

   Using "ClassParse" task from assembly "/Users/runner/work/1/s/xamarin-android/bin/Release/dotnet/packs/Microsoft.Android.Sdk.Darwin/33.0.0-ci.pr.gh7028.23/tools/Xamarin.Android.Build.Tasks.dll".
   Task "ClassParse" (TaskId:139)
     Task Parameter:ToolPath=/Users/runner/work/1/s/xamarin-android/bin/Release/dotnet/packs/Microsoft.Android.Sdk.Darwin/33.0.0-ci.pr.gh7028.23/tools (TaskId:139)
     Task Parameter:OutputFile=obj/Debug/api.xml.class-parse (TaskId:139)
     Task Parameter:SourceJars=/Users/runner/work/1/s/xamarin-android/bin/Release/dotnet/packs/Microsoft.Android.Sdk.Darwin/33.0.0-ci.pr.gh7028.23/tools/android-support-multidex.jar (TaskId:139)
     Using: dotnet /Users/runner/work/1/s/xamarin-android/bin/Release/dotnet/packs/Microsoft.Android.Sdk.Darwin/33.0.0-ci.pr.gh7028.23/tools/class-parse.dll (TaskId:139)
     [class-parse] response file: obj/Debug/class-parse.rsp (TaskId:139)
     --o="obj/Debug/api.xml.class-parse" (TaskId:139)
     "/Users/runner/work/1/s/xamarin-android/bin/Release/dotnet/packs/Microsoft.Android.Sdk.Darwin/33.0.0-ci.pr.gh7028.23/tools/android-support-multidex.jar" (TaskId:139)
     /Users/runner/.dotnet/dotnet /Users/runner/work/1/s/xamarin-android/bin/Release/dotnet/packs/Microsoft.Android.Sdk.Darwin/33.0.0-ci.pr.gh7028.23/tools/class-parse.dll "@obj/Debug/class-parse.rsp"  (TaskId:139)
     It was not possible to find any compatible framework version (TaskId:139)
     The framework 'Microsoft.NETCore.App', version '7.0.0-preview.5.22271.4' (x64) was not found. (TaskId:139)
       - The following frameworks were found: (TaskId:139)
           3.1.1 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
           3.1.3 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
           3.1.6 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
           3.1.25 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
           5.0.2 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
           5.0.5 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
           5.0.8 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
           5.0.17 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
           6.0.5 at [/Users/runner/.dotnet/shared/Microsoft.NETCore.App] (TaskId:139)
      (TaskId:139)
     You can resolve the problem by installing the specified framework and/or SDK. (TaskId:139)
      (TaskId:139)
     The specified framework can be found at: (TaskId:139)
       - https://aka.ms/dotnet-core-applaunch?framework=Microsoft.NETCore.App&framework_version=7.0.0-preview.5.22271.4&arch=x64&rid=osx.12-x64 (TaskId:139)
    1:7>/Users/runner/work/1/s/xamarin-android/bin/Release/dotnet/packs/Microsoft.Android.Sdk.Darwin/33.0.0-ci.pr.gh7028.23/tools/Xamarin.Android.Bindings.ClassParse.targets(30,5): error MSB6006: "dotnet" exited with code 150. [/Users/runner/work/1/a/TestRelease/05-25_01.03.40/temp/BuildWithExternalJavaLibrary/BuildWithExternalJavaLibraryBinding/BuildWithExternalJavaLibraryBinding.csproj]

Commit ff7f4675 removed the need to have a global .NET 7 preview install
to build and test Xamarin.Android.  This appears to expose an issue in
`AndroidDotnetToolTask`, which uses the latest `dotnet` in $PATH to run
some of our tools.  After upgrading some of these tools (class-parse,
generator) to net7.0, we are no longer able to run them as there is no
net7.0 runtime installed globally.

Improve the `AndroidDotnetToolTask` to better handle "sandboxed" .NET
installations by using a full path to `dotnet` if one is found.
---
 ...amarin.Android.Bindings.ClassParse.targets |  3 +
 .../Xamarin.Android.Bindings.Core.targets     |  7 +++
 ...rin.Android.Bindings.Documentation.targets |  2 +
 .../Tasks/AndroidDotnetToolTask.cs            | 49 +++++++++-------
 .../Tasks/ClassParse.cs                       |  2 -
 .../Tasks/Generator.cs                        |  2 -
 .../Tasks/ImportJavaDoc.cs                    |  2 -
 .../Tasks/AndroidDotnetToolTests.cs           | 58 +++++++++++++++++++
 8 files changed, 99 insertions(+), 26 deletions(-)
 create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/AndroidDotnetToolTests.cs

diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.ClassParse.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.ClassParse.targets
index e0f1240f3d1..e311d457ce9 100644
--- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.ClassParse.targets
+++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.ClassParse.targets
@@ -31,7 +31,9 @@ This file is only used by binding projects.
         OutputFile="$(ApiOutputFile).class-parse"
         SourceJars="@(EmbeddedJar);@(InputJar)"
         DocumentationPaths="@(_AndroidDocumentationPath)"
+        NetCoreRoot="$(NetCoreRoot)"
         ToolPath="$(MonoAndroidToolsDirectory)"
+        ToolExe="$(ClassParseToolExe)"
     />
     <BindingsGenerator
         OnlyRunXmlAdjuster="true"
@@ -41,6 +43,7 @@ This file is only used by binding projects.
         ApiXmlInput="$(ApiOutputFile).class-parse"
         ReferencedManagedLibraries="@(ReferencePath);@(ReferenceDependencyPaths)"
         MonoAndroidFrameworkDirectories="$(_XATargetFrameworkDirectories)"
+        NetCoreRoot="$(NetCoreRoot)"
         ToolPath="$(MonoAndroidToolsDirectory)"
         ToolExe="$(BindingsGeneratorToolExe)"
         Nullable="$(Nullable)"
diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Core.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Core.targets
index 7e69c7221d3..71dfa4542ee 100644
--- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Core.targets
+++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Core.targets
@@ -20,6 +20,12 @@ It is shared between "legacy" binding projects and .NET 5 projects.
     <GeneratedOutputPath        Condition=" '$(GeneratedOutputPath)' == '' ">$(IntermediateOutputPath)generated\</GeneratedOutputPath>
     <AndroidJavadocVerbosity    Condition=" '$(AndroidJavadocVerbosity)' == '' ">intellisense</AndroidJavadocVerbosity>
     <ApiOutputFile              Condition=" '$(ApiOutputFile)' == '' ">$(IntermediateOutputPath)api.xml</ApiOutputFile>
+    <ClassParseToolExe          Condition=" '$(UsingAndroidNETSdk)' == 'true' ">class-parse.dll</ClassParseToolExe>
+    <ClassParseToolExe          Condition=" '$(ClassParseToolExe)' == '' ">class-parse.exe</ClassParseToolExe>
+    <BindingsGeneratorToolExe   Condition=" '$(UsingAndroidNETSdk)' == 'true' ">generator.dll</BindingsGeneratorToolExe>
+    <BindingsGeneratorToolExe   Condition=" '$(BindingsGeneratorToolExe)' == '' ">generator.exe</BindingsGeneratorToolExe>
+    <JavadocToMdocToolExe       Condition=" '$(UsingAndroidNETSdk)' == 'true' ">javadoc-to-mdoc.dll</JavadocToMdocToolExe>
+    <JavadocToMdocToolExe       Condition=" '$(JavadocToMdocToolExe)' == '' ">javadoc-to-mdoc.exe</JavadocToMdocToolExe>
     <_GeneratorStampFile>$(_AndroidStampDirectory)generator.stamp</_GeneratorStampFile>
   </PropertyGroup>
 
@@ -73,6 +79,7 @@ It is shared between "legacy" binding projects and .NET 5 projects.
         ReferencedManagedLibraries="@(ReferencePath);@(ReferenceDependencyPaths)"
         MonoAndroidFrameworkDirectories="$(_XATargetFrameworkDirectories)"
         TypeMappingReportFile="$(GeneratedOutputPath)type-mapping.txt"
+        NetCoreRoot="$(NetCoreRoot)"
         ToolPath="$(MonoAndroidToolsDirectory)"
         ToolExe="$(BindingsGeneratorToolExe)"
         LangVersion="$(LangVersion)"
diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Documentation.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Documentation.targets
index d5a268134a7..785bb580b29 100644
--- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Documentation.targets
+++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Documentation.targets
@@ -64,7 +64,9 @@ This file is only used by binding projects. .NET 5 can eventually use it, once `
     />
     <ImportJavaDoc
         ContinueOnError="true"
+        NetCoreRoot="$(NetCoreRoot)"
         ToolPath="$(MonoAndroidToolsDirectory)"
+        ToolExe="$(JavadocToMdocToolExe)"
         JavaDocs="@(JavaDocIndex)"
         References="@(ReferencePath);@(ReferenceDependencyPaths)"
         Transforms="@(TransformFile)"
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AndroidDotnetToolTask.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AndroidDotnetToolTask.cs
index 426dee8eaf7..efa01c48478 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/AndroidDotnetToolTask.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/AndroidDotnetToolTask.cs
@@ -13,6 +13,11 @@ namespace Xamarin.Android.Tasks
 	/// </summary>
 	public abstract class AndroidDotnetToolTask : AndroidToolTask
 	{
+		/// <summary>
+		/// Path to the folder that contains dotnet / dotnet.exe.
+		/// </summary>
+		public string NetCoreRoot { get; set; }
+
 		/// <summary>
 		/// If `true`, this task should run `dotnet foo.dll` and `foo.exe` otherwise.
 		/// </summary>
@@ -31,61 +36,65 @@ public abstract class AndroidDotnetToolTask : AndroidToolTask
 
 		public override bool Execute ()
 		{
-			if (string.IsNullOrEmpty (ToolExe)) {
-				ToolExe = $"{BaseToolName}.exe";
-			}
-
-			var assemblyPath = Path.Combine (ToolPath, $"{BaseToolName}.dll");
-			if (File.Exists (assemblyPath)) {
+			if (Path.GetExtension (ToolExe) == ".dll") {
 				NeedsDotnet = true;
-				AssemblyPath = assemblyPath;
+				AssemblyPath = Path.Combine (ToolPath, ToolExe);
 				ToolPath = null;
+				ToolExe = null;
 
-				Log.LogDebugMessage ($"Using: dotnet {AssemblyPath}");
+				Log.LogDebugMessage ($"Using: {FindDotnet ()} {AssemblyPath}");
 			} else {
 				if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows) &&
 						!RuntimeInformation.FrameworkDescription.StartsWith ("Mono", StringComparison.OrdinalIgnoreCase)) {
 					// If not Windows and not running under Mono
 					NeedsMono = true;
-					AssemblyPath = Path.Combine (ToolPath, $"{BaseToolName}.exe");
+					AssemblyPath = Path.Combine (ToolPath, ToolExe);
 					ToolPath = null;
+					ToolExe = null;
 
-					Log.LogDebugMessage ($"Using: mono {AssemblyPath}");
+					Log.LogDebugMessage ($"Using: {FindMono ()} {AssemblyPath}");
 				} else {
 					// Otherwise running the .exe directly should work
-					Log.LogDebugMessage ($"Using: {GenerateFullPathToTool ()}");
+					Log.LogDebugMessage ($"Using: {Path.Combine (ToolPath, ToolExe)}");
 				}
 			}
 
 			return base.Execute ();
 		}
 
-		/// <summary>
-		/// The base tool name, such as "generator" for `generator.exe` and `dotnet generator.dll`
-		/// </summary>
-		protected abstract string BaseToolName {
-			get;
-		}
-
 		protected override string ToolName {
 			get {
 				if (NeedsDotnet)
 					return "dotnet";
 				if (NeedsMono)
 					return "mono";
-				return $"{BaseToolName}.exe";
+				return ToolExe;
 			}
 		}
 
 		protected override string GenerateFullPathToTool ()
 		{
 			if (NeedsDotnet)
-				return "dotnet";
+				return FindDotnet ();
 			if (NeedsMono)
 				return FindMono ();
 			return Path.Combine (ToolPath, ToolExe);
 		}
 
+		string FindDotnet ()
+		{
+			if (Directory.Exists (NetCoreRoot)) {
+				var dotnetPath = Path.Combine (NetCoreRoot, (RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ? "dotnet.exe" : "dotnet"));
+				if (File.Exists (dotnetPath))
+					return dotnetPath;
+			}
+
+			var dotnetHostPath = Environment.GetEnvironmentVariable ("DOTNET_HOST_PATH");
+			if (File.Exists (dotnetHostPath))
+				return dotnetHostPath;
+
+			return "dotnet";
+		}
 
 		const RegisteredTaskObjectLifetime Lifetime = RegisteredTaskObjectLifetime.Build;
 		const string MonoKey = nameof (AndroidDotnetToolTask) + "_Mono";
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs
index 984bc8b4ab1..ce217d907fc 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs
@@ -10,8 +10,6 @@ public class ClassParse : AndroidDotnetToolTask
 	{
 		public override string TaskPrefix => "CLP";
 
-		protected override string BaseToolName => "class-parse";
-
 		[Required]
 		public string OutputFile { get; set; }
 
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Generator.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Generator.cs
index f9b434e0e45..6d5848b96c6 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/Generator.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/Generator.cs
@@ -241,8 +241,6 @@ protected override string GenerateCommandLineCommands ()
 			return cmd.ToString ();
 		}
 
-		protected override string BaseToolName => "generator";
-
 		protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
 		{
 			base.LogEventsFromTextOutput (singleLine, messageImportance);
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ImportJavaDoc.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ImportJavaDoc.cs
index 4ae60e681c9..3b2c4e89ddb 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/ImportJavaDoc.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/ImportJavaDoc.cs
@@ -24,8 +24,6 @@ public class ImportJavaDoc : AndroidDotnetToolTask
 		[Required]
 		public string OutputDocDirectory { get; set; }
 
-		protected override string BaseToolName => "javadoc-to-mdoc";
-
 		protected override string GenerateCommandLineCommands ()
 		{
 			if (!Directory.Exists (OutputDocDirectory))
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/AndroidDotnetToolTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/AndroidDotnetToolTests.cs
new file mode 100644
index 00000000000..99d78ee4b4e
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/AndroidDotnetToolTests.cs
@@ -0,0 +1,58 @@
+using System;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+using NUnit.Framework;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Xamarin.Android.Tasks;
+using Xamarin.ProjectTools;
+
+namespace Xamarin.Android.Build.Tests
+{
+	[TestFixture]
+	[Category ("Node-5")]
+	public class AndroidDotnetToolTests : BaseTest
+	{
+		MockBuildEngine engine;
+		List<BuildErrorEventArgs> errors;
+		List<BuildWarningEventArgs> warnings;
+		List<BuildMessageEventArgs> messages;
+
+		[SetUp]
+		public void Setup ()
+		{
+			engine = new MockBuildEngine (TestContext.Out,
+				errors: errors = new List<BuildErrorEventArgs> (),
+				warnings: warnings = new List<BuildWarningEventArgs> (),
+				messages: messages = new List<BuildMessageEventArgs> ());
+		}
+
+		[Test]
+		public void ShouldUseFullToolPath ()
+		{
+			var dotnetDir = AndroidSdkResolver.GetDotNetPreviewPath ();
+			var dotnetPath = Path.Combine (dotnetDir, (TestEnvironment.IsWindows ? "dotnet.exe" : "dotnet"));
+			var classParseTask = new ClassParseTestTask {
+				BuildEngine = engine,
+				NetCoreRoot = dotnetDir,
+				ToolPath = Builder.UseDotNet ? TestEnvironment.DotNetAndroidSdkToolsDirectory : AndroidMSBuildDirectory,
+				ToolExe = Builder.UseDotNet ? "class-parse.dll" : "class-parse.exe",
+			};
+
+			Assert.True (classParseTask.Execute (), "Task should have succeeded.");
+			var expectedTool = Builder.UseDotNet ? dotnetPath : Path.Combine (AndroidMSBuildDirectory, "class-parse.exe");
+			Assert.IsTrue (messages.Any (m => m.Message.StartsWith (expectedTool)), "Task did not use expected tool path.");
+		}
+	}
+
+	public class ClassParseTestTask : AndroidDotnetToolTask
+	{
+		public override string TaskPrefix => "TEST";
+		protected override string GenerateCommandLineCommands ()
+		{
+			return GetCommandLineBuilder ().ToString ();
+		}
+	}
+}
