Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump to xamarin/xamarin-android-tools/main@76c076fc #7685

Closed
wants to merge 5 commits into from

Conversation

jonpryor
Copy link
Member

@jonpryor jonpryor commented Jan 11, 2023

Context: dotnet/maui#11605
Context: 8bc7a3e

Changes: http://github.com/xamarin/xamarin-android-tools/compare/9f02d77692bca8c6585941de03750d5eaaca5c5a...47f95ab99f6201d956eecfa1c7b2fd5fa7e43946

In dotnet/maui#11605, when $(AndroidEnableMarshalMethods)=True (8bc7a3e), 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() can be visible across project builds. Consider <GeneratePackageManagerJava/>:

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

dotnet/android-tools@76c076f updated the MSBuildExtensions extension methods so that IBuildEngine.ProjectFileOfTaskNode would be part of the RegisterTaskObject() key.

Review use of the .RegisterTaskObjectAssemblyLocal(), .GetRegisteredTaskObjectAssemblyLocal(), and
.UnregisterTaskObjectAssemblyLocal() extension methods to ensure that IBuildEngine.ProjectFileOfTaskNode is used or excluded, as appropriate. This should fix the XAGPM7009 build errors.

TODO: add unit test to trigger this build scenario.

Context: dotnet/maui#11605
Context: 8bc7a3e

Changes: http://github.com/xamarin/xamarin-android-tools/compare/9f02d77692bca8c6585941de03750d5eaaca5c5a...47f95ab99f6201d956eecfa1c7b2fd5fa7e43946

  * dotnet/android-tools@47f95ab: Fix CS0121 ambiguity errors. (dotnet/android-tools#200)
  * dotnet/android-tools@76c076f: Add support for Project Specific RegisterTaskObject. (dotnet/android-tools#199)

In dotnet/maui#11605, when `$(AndroidEnableMarshalMethods)`=True
(8bc7a3e), 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 could 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.

dotnet/android-tools@76c076f updated the `MSBuildExtensions`
extension methods so that [`IBuildEngine.ProjectFileOfTaskNode`][2]
would be part of the `RegisterTaskObject()` key.

Review use of the `.RegisterTaskObjectAssemblyLocal()`,
`.GetRegisteredTaskObjectAssemblyLocal()`, and
`.UnregisterTaskObjectAssemblyLocal()` extension methods to ensure
that `IBuildEngine.ProjectFileOfTaskNode` is used or excluded,
as appropriate.  This should fix the XAGPM7009 build errors.

TODO: add unit test to trigger this build scenario.

[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
@jonpryor jonpryor requested a review from dellis1972 January 11, 2023 19:30
@jonpryor jonpryor marked this pull request as draft January 11, 2023 19:30
@jonpryor
Copy link
Member Author

Also TODO: run all the unit tests. (I forget the voodoo to do that.)

@jonpryor
Copy link
Member Author

@dellis1972: could you please add unit test to trigger the described scenario? I believe you have something lying around?

@jonpryor
Copy link
Member Author

@dellis1972
Copy link
Contributor

This looks good. Should we wait for the monodroid changes?
The monodroid stuff is dependent on https://github.com/xamarin/androidtools/pull/374 being merged.

@dellis1972
Copy link
Contributor

Draft of monodroid changes https://github.com/xamarin/monodroid/pull/1281

@jonpryor
Copy link
Member Author

@dellis1972 asked:

Should we wait for the monodroid changes?

I don't think we need to wait, unless the it takes so long to get this PR "good" that monodroid can come along for the ride…

I don't think that this PR is "good" though; consider Aapt2DaemonInstances(6,6,6,50):

Should have got a Daemon
Expected: not null
But was:  null

   at Xamarin.Android.Build.Tests.Aapt2Tests.Aapt2DaemonInstances(Int32 maxInstances, Int32 expectedMax, Int32 expectedInstances, Int32 numLayouts) in /Users/builder/azdo/_work/1/s/xamarin-android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/Aapt2Tests.cs:line 109
   at InvokeStub_Aapt2Tests.Aapt2DaemonInstances(Object, Object, IntPtr*)

Or ReportAaptErrorsInOriginalFileName:

error with expected file name is not found
Expected: True
But was:  False

   at Xamarin.Android.Build.Tests.AndroidUpdateResourcesTest.ReportAaptErrorsInOriginalFileName() in /Users/builder/azdo/_work/1/s/xamarin-android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs:line 201

@dellis1972
Copy link
Contributor

@dellis1972 asked:

Should we wait for the monodroid changes?

I don't think we need to wait, unless the it takes so long to get this PR "good" that monodroid can come along for the ride…

I don't think that this PR is "good" though; consider Aapt2DaemonInstances(6,6,6,50):

Should have got a Daemon
Expected: not null
But was:  null

   at Xamarin.Android.Build.Tests.Aapt2Tests.Aapt2DaemonInstances(Int32 maxInstances, Int32 expectedMax, Int32 expectedInstances, Int32 numLayouts) in /Users/builder/azdo/_work/1/s/xamarin-android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/Aapt2Tests.cs:line 109
   at InvokeStub_Aapt2Tests.Aapt2DaemonInstances(Object, Object, IntPtr*)

Or ReportAaptErrorsInOriginalFileName:

error with expected file name is not found
Expected: True
But was:  False

   at Xamarin.Android.Build.Tests.AndroidUpdateResourcesTest.ReportAaptErrorsInOriginalFileName() in /Users/builder/azdo/_work/1/s/xamarin-android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs:line 201

The First unit test is using GetRegisteredTaskObjectAssemblyLocal but is not providing the None so its getting a different item.

    Should have got a Daemon
    Expected: not null
    But was:  null

       at Xamarin.Android.Build.Tests.Aapt2Tests.Aapt2DaemonInstances(Int32 maxInstances, Int32 expectedMax, Int32 expectedInstances, Int32 numLayouts) in /Users/builder/azdo/_work/1/s/xamarin-android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/Aapt2Tests.cs:line 109
       at InvokeStub_Aapt2Tests.Aapt2DaemonInstances(Object, Ob

The tests *also* need to use the correct & consistent arguments
to `engine.GetRegisteredTaskObjectAssemblyLocal<Appt2Daemon>(…)`.
@jonpryor
Copy link
Member Author

So…

Or ReportAaptErrorsInOriginalFileName:

I apply this patch:

diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs
index acd5230b3..70fb6d838 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs
@@ -497,11 +497,47 @@ namespace Xamarin.Android.Tasks
 
 		static readonly string ResourceCaseMapKey = $"{nameof (MonoAndroidHelper)}_ResourceCaseMap";
 
-		public static void SaveResourceCaseMap (IBuildEngine4 engine, Dictionary<string, string> map) =>
+		public static void SaveResourceCaseMap (IBuildEngine4 engine, Dictionary<string, string> map) {
+			engine.LogMessageEvent (new BuildMessageEventArgs 
+					($"# jonp: SaveResourceCaseMap: key={ResourceCaseMapKey}; engine.ProjectFileOfTaskNode={engine.ProjectFileOfTaskNode}",
+					helpKeyword: "",
+					senderName: "SaveResourceCaseMap",
+					importance: MessageImportance.Normal
+			));
+			var s = new System.Text.StringBuilder();
+			foreach (var e in map) {
+				s.AppendLine ($"{e.Key}={e.Value}");
+			}
+			engine.LogMessageEvent (new BuildMessageEventArgs 
+					($"# jonp: SaveResourceCaseMap: map={s}",
+					helpKeyword: "",
+					senderName: "SaveResourceCaseMap",
+					importance: MessageImportance.Normal
+			));
 			engine.RegisterTaskObjectAssemblyLocal (ResourceCaseMapKey, map, RegisteredTaskObjectLifetime.Build);
+		}
+
+		public static Dictionary<string, string> LoadResourceCaseMap (IBuildEngine4 engine) {
 
-		public static Dictionary<string, string> LoadResourceCaseMap (IBuildEngine4 engine) =>
-			engine.GetRegisteredTaskObjectAssemblyLocal<Dictionary<string, string>> (ResourceCaseMapKey, RegisteredTaskObjectLifetime.Build) ?? new Dictionary<string, string> (0);
+			var map = engine.GetRegisteredTaskObjectAssemblyLocal<Dictionary<string, string>> (ResourceCaseMapKey, RegisteredTaskObjectLifetime.Build) ?? new Dictionary<string, string> (0);
+			engine.LogMessageEvent (new BuildMessageEventArgs 
+					($"# jonp: LoadResourceCaseMap: key={ResourceCaseMapKey}; engine.ProjectFileOfTaskNode={engine.ProjectFileOfTaskNode}; found? {map != null}",
+					helpKeyword: "",
+					senderName: "LoadResourceCaseMap",
+					importance: MessageImportance.Normal
+			));
+			var s = new System.Text.StringBuilder();
+			foreach (var e in map) {
+				s.AppendLine ($"{e.Key}={e.Value}");
+			}
+			engine.LogMessageEvent (new BuildMessageEventArgs 
+					($"# jonp: LoadResourceCaseMap: map={s}",
+					helpKeyword: "",
+					senderName: "LoadResourceCaseMap",
+					importance: MessageImportance.Normal
+			));
+			return map;
+		}
 
 		public static string FixUpAndroidResourcePath (string file, string resourceDirectory, string resourceDirectoryFullPath, Dictionary<string, string> resource_name_case_map)
 		{

And I see the following fascinating output in build.log:

# jonp: SaveResourceCaseMap: key=MonoAndroidHelper_ResourceCaseMap; engine.ProjectFileOfTaskNode=…/xamarin-android/bin/Debug/lib/packs/Microsoft.Android.Sdk.Darwin/34.0.0/tools/Xamarin.Android.Common.targets
# jonp: SaveResourceCaseMap: map=drawable-mdpi/icon.png=drawable-mdpi/Icon.png
drawable-hdpi/icon.png=drawable-hdpi/Icon.png
drawable-xhdpi/icon.png=drawable-xhdpi/Icon.png
drawable-xxhdpi/icon.png=drawable-xxhdpi/Icon.png
drawable-xxxhdpi/icon.png=drawable-xxxhdpi/Icon.png
layout/main.xml=layout/Main.axml
values/strings.xml=values/Strings.xml
…
# jonp: LoadResourceCaseMap: key=MonoAndroidHelper_ResourceCaseMap; engine.ProjectFileOfTaskNode=…/xamarin-android/bin/Debug/lib/packs/Microsoft.Android.Sdk.Darwin/34.0.0/tools/Xamarin.Android.Aapt2.targets; found? True
# jonp: LoadResourceCaseMap: map=

In particular, note the engine.ProjectFileOfTaskNode values:

  • SaveResourceCaseMap: …/xamarin-android/bin/Debug/lib/packs/Microsoft.Android.Sdk.Darwin/34.0.0/tools/Xamarin.Android.Common.targets
  • LoadResourceCaseMap: …/xamarin-android/bin/Debug/lib/packs/Microsoft.Android.Sdk.Darwin/34.0.0/tools/Xamarin.Android.Aapt2.targets

They're not the same!

Consequently, the map that LoadResourceCaseMap() finds doesn't have any entries. (Why it isn't null confuses me, but it certainly doesn't have the same content.)

Thus, engine.ProjectFileOfTaskNode is the path of the file containing the Task that eventually invokes SaveResourceCaseMap()/LoadResourceCaseMap(), not the .csproj!

Which means the underlying idea of dotnet/android-tools@76c076f will not work; we still can have potential unexpected data sharing, because the MSBuild file which invokes <GeneratePackageManagerJava/> will be e.g. Xamarin.Android.Common.targets, which will be common/shared across project builds.

@jonpryor
Copy link
Member Author

Behold, docs!

Returns the full path to the project file that contained the call to this task.

Apparently "project file" does not mean .csproj, but instead means "file containing the task invocation" (which the docs say!).

@jonpryor
Copy link
Member Author

Given that engine.ProjectFileOfTaskNode doesn't work the way we thought it did, I think we should revert dotnet/android-tools@47f95ab and dotnet/android-tools@76c076f, and then do one of two things:

  1. Audit all use of *RegisterTaskObject*() and ensure that the "key" contains a project-specific value, or

  2. (1) plus we [Obsolete(…, error:true)] the MSBuildExtensions.*AssemblyLocal* methods and overload those methods so that a "project-specific" value is required, a'la:

    partial class MSBuildExtensions {
        [Obsolete ("Use …", error; true)]
        public static void RegisterTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, object value, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection = false) =>;
    
        public static void RegisterTaskObjectAssemblyLocal (
                this IBuildEngine4 engine,
                (object Key, string ProjectSpecificFilePath) key,
                object value,
                RegisteredTaskObjectLifetime lifetime,
                bool allowEarlyCollection = false)
        {}
    }

    wherein the key tuple is merged with AssemblyLocation for the "real" key, with usage similar to:

    engine.RegisterTaskObjectAssemblyLocal (
            (".:!MarshalMethods!:.", pathToProjectFile),
            value,
            RegisteredTaskObjectLifetime.Build
    );

I'm not entirely certain that the added complexity of (2) is actually worth it; (1) may be best.

jonpryor added a commit to jonpryor/xamarin-android-tools that referenced this pull request Jan 12, 2023
Reverts: 47f95ab
Reverts: 76c076f

Context: dotnet/android#7685 (comment)

The problem is that we misunderstood
[`IBuildEngine.ProjectFileOfTaskNode`]: we *thought* (hoped?) that it
would return `$(MSBuildProjectFullPath)`, the path to the `.csproj`
being built.

In actuality it returns `$(MSBuildThisFileFullPath)` for the file
containing the Task invocation.  Meaning if e.g.
`Xamarin.Android.Common.targets` contains the Task invocation, then
IBuildEngine.ProjectFileOfTaskNode` would be the path to
`Xamarin.Android.Common.targets`, *not* the path of the `.csproj`.

This in turn means it doesn't actually solve the problem we had.
We will instead need to audit all use of
`IBuildEngine4.RegisterTaskObject()` & related to ensure that the
keys provided *also* contain paths to project-specific files,
if necessary.

Revert 47f95ab and 76c076f, as the changes are not needed.

[0]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.build.framework.ibuildengine.projectfileoftasknode?view=msbuild-17-netcore
jonpryor added a commit to dotnet/android-tools that referenced this pull request Jan 13, 2023
Reverts: 47f95ab
Reverts: 76c076f

Context: dotnet/android#7685 (comment)

The problem is that we misunderstood
[`IBuildEngine.ProjectFileOfTaskNode`][0]: we *thought* (hoped?) that
it would return `$(MSBuildProjectFullPath)`, the path to the
`.csproj` being built.

In actuality it returns `$(MSBuildThisFileFullPath)` for the file
containing the Task invocation.  Meaning if e.g.
`Xamarin.Android.Common.targets` contains the Task invocation, then
IBuildEngine.ProjectFileOfTaskNode` would be the path to
`Xamarin.Android.Common.targets`, *not* the path of the `.csproj`.

This in turn means it doesn't actually solve the problem we had.

We will instead need to audit all use of
`IBuildEngine4.RegisterTaskObject()` & related to ensure that the
keys provided *also* contain paths to project-specific files,
if necessary.

Revert 47f95ab and 76c076f, as the changes are not needed.

[0]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.build.framework.ibuildengine.projectfileoftasknode?view=msbuild-17-netcore
@jonpryor
Copy link
Member Author

Closing this branch, as the approach doesn't work as we hoped.

@jonpryor jonpryor closed this Jan 13, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Jan 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants