From 258846d41a93355f0196de49701407626ca9cbd0 Mon Sep 17 00:00:00 2001
From: Dean Ellis <dellis1972@googlemail.com>
Date: Fri, 13 Jan 2023 16:42:53 +0000
Subject: [PATCH 1/6] Add new API for generating Project Specific Key for
 RegisterTaskObject
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Context: dotnet/maui#11605
Context: dotnet/maui#11387 (comment)
Context: http://github.com/xamarin/xamarin-android/commit/8bc7a3e84f95e70fe12790ac31ecd97957771cb2

In dotnet/maui#11605, when `$(AndroidEnableMarshalMethods)`=True
(xamarin/xamarin-android@8bc7a3e8), the build may fail if the `.sln`
contains more than one Android "App" project.  We tracked this down
to "undesired sharing" between project builds; the `obj` provided to
[`IBuildEngine4.RegisterTaskObject()`][0] can be visible across
project builds.  Consider [`<GeneratePackageManagerJava/>`][1]:

	var marshalMethodsState = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<MarshalMethodsState> (".:!MarshalMethods!:.", RegisteredTaskObjectLifetime.Build);

Further consider a `.sln` with two "App" projects, as the "App"
project build hits `<GeneratePackageManagerJava/>`.
The lifetime of `.Build` is *not* tied to the the `Build` target of a
given `.csproj`; rather, it's for the *build process*.  This can
result in:

 1. `dotnet build Project.sln` is run; `Project.sln` references
    `App1.csproj` and `App2.csproj`.
 2. `App1.csproj` is built.
 3. `App1.csproj` calls `<GeneratePackageManagerJava/>`.
 4. `App2.csproj` is later built as part in the process, and *also*
    calls `<GeneratePackageManagerJava/>`.

In particular note the key within `<GeneratePackageManagerJava/>`:
`".:!MarshalMethods!:."`.  This value is unchanged, and means that
that when `App2.csproj` is built, it will be using the same key as
was used with `App1.csproj`, and thus could be inadvertently using
data intended for `App1.csproj`!

This would result build errors:

	…\Xamarin.Android.Common.targets(1717,3): error XAGPM7009: System.InvalidOperationException: Unable to translate assembly name 'Xamarin.AndroidX.Activity' to its index
	…\Xamarin.Android.Common.targets(1717,3): error XAGPM7009:    at Xamarin.Android.Tasks.MarshalMethodsNativeAssemblyGenerator.WriteNativeMethods(LlvmIrGenerator generator, Dictionary`2 asmNameToIndex, LlvmIrVariableReference get_function_pointer_ref)
	…\Xamarin.Android.Common.targets(1717,3): error XAGPM7009:    at Xamarin.Android.Tasks.MarshalMethodsNativeAssemblyGenerator.Write(LlvmIrGenerator generator)
	…\Xamarin.Android.Common.targets(1717,3): error XAGPM7009:    at Xamarin.Android.Tasks.LLVMIR.LlvmIrComposer.Write(AndroidTargetArch arch, StreamWriter output, String fileName)
	…\Xamarin.Android.Common.targets(1717,3): error XAGPM7009:    at Xamarin.Android.Tasks.GeneratePackageManagerJava.AddEnvironment()
	…\Xamarin.Android.Common.targets(1717,3): error XAGPM7009:    at Xamarin.Android.Tasks.GeneratePackageManagerJava.RunTask()

The sharing of `RegisterTaskObject` data across project builds is
rarely desirable.  There are a few instances where it is safe to share
the registered objects between projects, e.g. `java` version
information (keyed on `java` path).  However, most of the time it is
specific to the project that is being built.  Historically we have
probably got away with this because "most" users only have one project.

Update the `AndroidTask` to capture the current directory in a
`WorkingDirectory` property like `AsyncTask` does. Then introduce a new
method `ProjectSpecificTaskObjectKey` which can be used to generate a
key which includes the `WorkingDirectory`. Both `AndroidTask` and
`AndroidAsyncTask` include this new method.

Allowing:

	var marshalMethodsState = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<MarshalMethodsState> (
			ProjectSpecificTaskObjectKey (".:!MarshalMethods!:."),
			RegisteredTaskObjectLifetime.Build,
	);

When `ProjectSpecificTaskObjectKey` is used the `WorkingDirectory` is included
in the key with `RegisterTaskObject()`.  This helps ensure that builds
in different `.csproj` files will result in different keys.

[0]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.build.framework.ibuildengine4.registertaskobject?view=msbuild-17-netcore
[1]: https://github.com/xamarin/xamarin-android/blob/c92ae5eb9fdcb3a2fd7c20f5b42dddf8b3ea781a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs#L407
[2]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.build.framework.ibuildengine.projectfileoftasknode?view=msbuild-17-netcore
---
 .../AndroidAsyncTask.cs                       |   4 +
 .../AndroidTask.cs                            |  12 +++
 .../AndroidToolTaskTests.cs                   | 100 +++++++++++++++++
 .../Utilites/MockBuildEngine.cs               | 102 ++++++++++++++++++
 4 files changed, 218 insertions(+)
 create mode 100644 tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
 create mode 100644 tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs

diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
index f3859e50..08cda05a 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
@@ -46,5 +46,9 @@ public virtual bool RunTask ()
 		/// * RunTaskAsync is already on a background thread
 		/// </summary>
 		public virtual System.Threading.Tasks.Task RunTaskAsync () => System.Threading.Tasks.Task.CompletedTask;
+
+		protected (object, string) ProjectSpecificTaskObjectKey (object key){
+			return (key, WorkingDirectory);
+		}
 	}
 }
diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
index 4b511c3c..3dc68392 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
@@ -1,6 +1,7 @@
 // https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L10
 
 using System;
+using System.IO;
 using Microsoft.Build.Utilities;
 
 namespace Microsoft.Android.Build.Tasks
@@ -11,6 +12,13 @@ public abstract class AndroidTask : Task
 	{
 		public abstract string TaskPrefix { get; }
 
+		protected string WorkingDirectory { get; private set; }
+
+		public AndroidTask ()
+		{
+			WorkingDirectory = Directory.GetCurrentDirectory();
+		}
+
 		public override bool Execute ()
 		{
 			try {
@@ -22,5 +30,9 @@ public override bool Execute ()
 		}
 
 		public abstract bool RunTask ();
+
+		protected object ProjectSpecificTaskObjectKey (object key){
+			return (key, WorkingDirectory);
+		}
 	}
 }
diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
new file mode 100644
index 00000000..af38705a
--- /dev/null
+++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
@@ -0,0 +1,100 @@
+using System.IO;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Android.Build.BaseTasks.Tests.Utilities;
+using Microsoft.Android.Build.Tasks;
+using NUnit.Framework;
+using Microsoft.Build.Framework;
+using Xamarin.Build;
+
+namespace Microsoft.Android.Build.BaseTasks.Tests
+{
+	[TestFixture]
+	public class AndroidToolTaskTests
+	{
+		public class MyAndroidTask : AndroidTask {
+			public override string TaskPrefix {get;} = "MAT";
+			public string Key { get; set; }
+			public string Value { get; set; }
+			public bool ProjectSpecific { get; set; } = false;
+			public override bool RunTask ()
+			{
+				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : Key;
+				BuildEngine4.RegisterTaskObjectAssemblyLocal (key, Value, RegisteredTaskObjectLifetime.Build);
+				return true;
+			}
+		}
+
+		public class MyOtherAndroidTask : AndroidTask {
+			public override string TaskPrefix {get;} = "MOAT";
+			public string Key { get; set; }
+			public bool ProjectSpecific { get; set; } = false;
+
+			[Output]
+			public string Value { get; set; }
+			public override bool RunTask ()
+			{
+				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : Key;
+				Value = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<string> (key, RegisteredTaskObjectLifetime.Build);
+				return true;
+			}
+		}
+
+		[Test]
+		[TestCase (true, true, true)]
+		[TestCase (false, false, true)]
+		[TestCase (true, false, false)]
+		[TestCase (false, true, false)]
+		public void TestRegisterTaskObjectCanRetrieveCorrectItem (bool projectSpecificA, bool projectSpecificB, bool expectedResult)
+		{
+			var engine = new MockBuildEngine (TestContext.Out) {
+			};
+			var task = new MyAndroidTask () {
+				BuildEngine = engine,
+				Key = "Foo",
+				Value = "Foo",
+				ProjectSpecific = projectSpecificA,
+			};
+			task.Execute ();
+			var task2 = new MyOtherAndroidTask () {
+				BuildEngine = engine,
+				Key = "Foo",
+				ProjectSpecific = projectSpecificB,
+			};
+			task2.Execute ();
+			Assert.AreEqual (expectedResult, string.Compare (task2.Value, task.Value, ignoreCase: true) == 0);
+		}
+
+		[Test]
+		[TestCase (true, true, false)]
+		[TestCase (false, false, true)]
+		[TestCase (true, false, false)]
+		[TestCase (false, true, false)]
+		public void TestRegisterTaskObjectFailsWhenDirectoryChanges (bool projectSpecificA, bool projectSpecificB, bool expectedResult)
+		{
+			var engine = new MockBuildEngine (TestContext.Out) {
+			};
+			MyAndroidTask task;
+			var currentDir = Directory.GetCurrentDirectory ();
+			Directory.SetCurrentDirectory (Path.Combine (currentDir, ".."));
+			try {
+				task = new MyAndroidTask () {
+					BuildEngine = engine,
+					Key = "Foo",
+					Value = "Foo",
+					ProjectSpecific = projectSpecificA,
+				};
+			} finally {
+				Directory.SetCurrentDirectory (currentDir);
+			}
+			task.Execute ();
+			var task2 = new MyOtherAndroidTask () {
+				BuildEngine = engine,
+				Key = "Foo",
+				ProjectSpecific = projectSpecificB,
+			};
+			task2.Execute ();
+			Assert.AreEqual (expectedResult, string.Compare (task2.Value, task.Value, ignoreCase: true) == 0);
+		}
+	}
+}
diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs
new file mode 100644
index 00000000..38155fcf
--- /dev/null
+++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Build.Framework;
+
+namespace Microsoft.Android.Build.BaseTasks.Tests.Utilities {
+	public class MockBuildEngine : IBuildEngine, IBuildEngine2, IBuildEngine3, IBuildEngine4 {
+		public MockBuildEngine (TextWriter output, IList<BuildErrorEventArgs> errors = null, IList<BuildWarningEventArgs> warnings = null, IList<BuildMessageEventArgs> messages = null, IList<CustomBuildEventArgs> customEvents = null)
+		{
+			this.Output = output;
+			this.Errors = errors;
+			this.Warnings = warnings;
+			this.Messages = messages;
+			this.CustomEvents = customEvents;
+		}
+
+		private TextWriter Output { get; }
+
+		private IList<BuildErrorEventArgs> Errors { get; }
+
+		private IList<BuildWarningEventArgs> Warnings { get; }
+
+		private IList<BuildMessageEventArgs> Messages { get; }
+
+		private IList<CustomBuildEventArgs> CustomEvents { get; }
+
+		int IBuildEngine.ColumnNumberOfTaskNode => -1;
+
+		bool IBuildEngine.ContinueOnError => false;
+
+		int IBuildEngine.LineNumberOfTaskNode => -1;
+
+		string IBuildEngine.ProjectFileOfTaskNode => "this.xml";
+
+		bool IBuildEngine2.IsRunningMultipleNodes => false;
+
+		bool IBuildEngine.BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true;
+
+		void IBuildEngine.LogCustomEvent (CustomBuildEventArgs e)
+		{
+			this.Output.WriteLine ($"Custom: {e.Message}");
+			if (CustomEvents != null)
+				CustomEvents.Add (e);
+		}
+
+		void IBuildEngine.LogErrorEvent (BuildErrorEventArgs e)
+		{
+			this.Output.WriteLine ($"Error: {e.Message}");
+			if (Errors != null)
+				Errors.Add (e);
+		}
+
+		void IBuildEngine.LogMessageEvent (BuildMessageEventArgs e)
+		{
+			this.Output.WriteLine ($"Message: {e.Message}");
+			if (Messages != null)
+				Messages.Add (e);
+		}
+
+		void IBuildEngine.LogWarningEvent (BuildWarningEventArgs e)
+		{
+			this.Output.WriteLine ($"Warning: {e.Message}");
+			if (Warnings != null)
+				Warnings.Add (e);
+		}
+
+		private Dictionary<object, object> Tasks = new Dictionary<object, object> ();
+
+		void IBuildEngine4.RegisterTaskObject (object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection)
+		{
+			Tasks.Add (key, obj);
+		}
+
+		object IBuildEngine4.GetRegisteredTaskObject (object key, RegisteredTaskObjectLifetime lifetime)
+		{
+			object obj = null;
+			Tasks.TryGetValue (key, out obj);
+			return obj;
+		}
+
+		object IBuildEngine4.UnregisterTaskObject (object key, RegisteredTaskObjectLifetime lifetime)
+		{
+			var obj = Tasks [key];
+			Tasks.Remove (key);
+			return obj;
+		}
+
+		BuildEngineResult IBuildEngine3.BuildProjectFilesInParallel (string [] projectFileNames, string [] targetNames, IDictionary [] globalProperties, IList<string> [] removeGlobalProperties, string [] toolsVersion, bool returnTargetOutputs)
+		{
+			throw new NotImplementedException ();
+		}
+
+		void IBuildEngine3.Yield () { }
+
+		void IBuildEngine3.Reacquire () { }
+
+		bool IBuildEngine2.BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion) => true;
+
+		bool IBuildEngine2.BuildProjectFilesInParallel (string [] projectFileNames, string [] targetNames, IDictionary [] globalProperties, IDictionary [] targetOutputsPerProject, string [] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion) => true;
+	}
+}

From 602068a977269dc4bf008b6e9195816380d8ede6 Mon Sep 17 00:00:00 2001
From: Dean Ellis <dellis1972@googlemail.com>
Date: Mon, 16 Jan 2023 10:41:32 +0000
Subject: [PATCH 2/6] Add missing method to AndroidToolTask

---
 .../AndroidToolTask.cs                               | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
index 38093a61..ec6c392c 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
@@ -1,6 +1,7 @@
 // https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L75
 
 using System;
+using System.IO;
 using Microsoft.Build.Utilities;
 
 namespace Microsoft.Android.Build.Tasks
@@ -9,6 +10,13 @@ public abstract class AndroidToolTask : ToolTask
 	{
 		public abstract string TaskPrefix { get; }
 
+		protected string WorkingDirectory { get; private set; }
+
+		public AndroidToolTask ()
+		{
+			WorkingDirectory = Directory.GetCurrentDirectory();
+		}
+
 		public override bool Execute ()
 		{
 			try {
@@ -22,5 +30,9 @@ public override bool Execute ()
 		// Most ToolTask's do not override Execute and
 		// just expect the base to be called
 		public virtual bool RunTask () => base.Execute ();
+
+		protected object ProjectSpecificTaskObjectKey (object key){
+			return (key, WorkingDirectory);
+		}
 	}
 }

From f48cbb2537cd823c487990412110f466f18ea9c2 Mon Sep 17 00:00:00 2001
From: Dean Ellis <dellis1972@googlemail.com>
Date: Mon, 16 Jan 2023 10:44:07 +0000
Subject: [PATCH 3/6] Make all methods return the same types

---
 src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs     | 2 +-
 src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
index 3dc68392..1233672a 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
@@ -31,7 +31,7 @@ public override bool Execute ()
 
 		public abstract bool RunTask ();
 
-		protected object ProjectSpecificTaskObjectKey (object key){
+		protected (object, string) ProjectSpecificTaskObjectKey (object key){
 			return (key, WorkingDirectory);
 		}
 	}
diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
index ec6c392c..3d48a011 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
@@ -31,7 +31,7 @@ public override bool Execute ()
 		// just expect the base to be called
 		public virtual bool RunTask () => base.Execute ();
 
-		protected object ProjectSpecificTaskObjectKey (object key){
+		protected (object, string) ProjectSpecificTaskObjectKey (object key){
 			return (key, WorkingDirectory);
 		}
 	}

From c38edbf49f75c54050a9ce7eac9e2866756d9661 Mon Sep 17 00:00:00 2001
From: Dean Ellis <dellis1972@googlemail.com>
Date: Mon, 16 Jan 2023 12:02:47 +0000
Subject: [PATCH 4/6] Fix Unit Test

---
 .../AndroidToolTaskTests.cs                                   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
index af38705a..8898222d 100644
--- a/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
+++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
@@ -19,7 +19,7 @@ public class MyAndroidTask : AndroidTask {
 			public bool ProjectSpecific { get; set; } = false;
 			public override bool RunTask ()
 			{
-				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : Key;
+				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, string.Empty);
 				BuildEngine4.RegisterTaskObjectAssemblyLocal (key, Value, RegisteredTaskObjectLifetime.Build);
 				return true;
 			}
@@ -34,7 +34,7 @@ public class MyOtherAndroidTask : AndroidTask {
 			public string Value { get; set; }
 			public override bool RunTask ()
 			{
-				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : Key;
+				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, string.Empty);
 				Value = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<string> (key, RegisteredTaskObjectLifetime.Build);
 				return true;
 			}

From c3e9b4bcbf313adf9cf17d4fa6b386fa7f722f21 Mon Sep 17 00:00:00 2001
From: Dean Ellis <dellis1972@googlemail.com>
Date: Mon, 16 Jan 2023 12:15:51 +0000
Subject: [PATCH 5/6] Add comments

---
 .../MSBuildExtensions.cs                      | 22 ++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs b/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs
index d50b3d93..b467e833 100644
--- a/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs
@@ -251,18 +251,30 @@ public static void SetDestinationSubPath (this ITaskItem assembly)
 
 		/// <summary>
 		/// IBuildEngine4.RegisterTaskObject, but adds the current assembly path into the key
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static void RegisterTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, object value, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection = false) =>
 			engine.RegisterTaskObject ((AssemblyLocation, key), value, lifetime, allowEarlyCollection);
 
 		/// <summary>
 		/// IBuildEngine4.GetRegisteredTaskObject, but adds the current assembly path into the key
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static object GetRegisteredTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime) =>
 			engine.GetRegisteredTaskObject ((AssemblyLocation, key), lifetime);
 
 		/// <summary>
 		/// Generic version of IBuildEngine4.GetRegisteredTaskObject, but adds the current assembly path into the key
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static T GetRegisteredTaskObjectAssemblyLocal<T> (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime)
 			where T : class =>
@@ -271,12 +283,20 @@ public static T GetRegisteredTaskObjectAssemblyLocal<T> (this IBuildEngine4 engi
 
 		/// <summary>
 		/// IBuildEngine4.UnregisterTaskObject, but adds the current assembly path into the key
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static object UnregisterTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime) =>
 			engine.UnregisterTaskObject ((AssemblyLocation, key), lifetime);
 
 		/// <summary>
-		/// Generic version of IBuildEngine4.UnregisterTaskObject, but adds the current assembly path into the key
+		/// Generic version of IBuildEngine4.UnregisterTaskObject, but adds the current assembly path into the key.
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static T UnregisterTaskObjectAssemblyLocal<T> (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime)
 			where T : class =>

From b3b5311b1f2b3625e023faf0c12aff13c3ff96e3 Mon Sep 17 00:00:00 2001
From: Dean Ellis <dellis1972@googlemail.com>
Date: Tue, 17 Jan 2023 16:38:47 +0000
Subject: [PATCH 6/6] Fix formatting and change API to return an object only

---
 src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs     | 4 +---
 src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs          | 4 +---
 src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs      | 4 +---
 .../AndroidToolTaskTests.cs                                   | 4 ++--
 4 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
index 08cda05a..a04d86ff 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
@@ -47,8 +47,6 @@ public virtual bool RunTask ()
 		/// </summary>
 		public virtual System.Threading.Tasks.Task RunTaskAsync () => System.Threading.Tasks.Task.CompletedTask;
 
-		protected (object, string) ProjectSpecificTaskObjectKey (object key){
-			return (key, WorkingDirectory);
-		}
+		protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory);
 	}
 }
diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
index 1233672a..9acb7b8b 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
@@ -31,8 +31,6 @@ public override bool Execute ()
 
 		public abstract bool RunTask ();
 
-		protected (object, string) ProjectSpecificTaskObjectKey (object key){
-			return (key, WorkingDirectory);
-		}
+		protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory);
 	}
 }
diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
index 3d48a011..cc01e8f5 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
@@ -31,8 +31,6 @@ public override bool Execute ()
 		// just expect the base to be called
 		public virtual bool RunTask () => base.Execute ();
 
-		protected (object, string) ProjectSpecificTaskObjectKey (object key){
-			return (key, WorkingDirectory);
-		}
+		protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory);
 	}
 }
diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
index 8898222d..8b35ce5b 100644
--- a/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
+++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
@@ -19,7 +19,7 @@ public class MyAndroidTask : AndroidTask {
 			public bool ProjectSpecific { get; set; } = false;
 			public override bool RunTask ()
 			{
-				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, string.Empty);
+				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, (object)string.Empty);
 				BuildEngine4.RegisterTaskObjectAssemblyLocal (key, Value, RegisteredTaskObjectLifetime.Build);
 				return true;
 			}
@@ -34,7 +34,7 @@ public class MyOtherAndroidTask : AndroidTask {
 			public string Value { get; set; }
 			public override bool RunTask ()
 			{
-				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, string.Empty);
+				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, (object)string.Empty);
 				Value = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<string> (key, RegisteredTaskObjectLifetime.Build);
 				return true;
 			}
