Skip to content

Commit

Permalink
[trimming] preserve custom views and $(AndroidHttpClientHandlerType)
Browse files Browse the repository at this point in the history
Fixes: dotnet#8797
Context: https://github.com/dotnet/runtime/blob/714a4420805ed53c311b05381c83c88894100fa9/docs/tools/illink/data-formats.md
Context:

Here are two cases `TrimMode=full` can break applications:

* `AndroidHttpClientHandlerType` set to a custom type

* Custom views (Android `.xml`) that are not referenced in C# code

For either of these cases, the traditional approach in Xamarin.Android
would have been to author a trimmer step to preserve these types.

However, thinking about NativeAOT in the future, it is more
"future-proof" to write an MSBuild task that generates trimmer `.xml`
to preserve these types. This same XML file could be passed to the
NativeAOT trimmer (`Ilc`). If we had a custom trimmer step, we would
have to reimplement the same logic for the NativeAOT trimmer.

This is WIP as custom views are not yet preserved.
  • Loading branch information
jonathanpeppers committed May 14, 2024
1 parent 98c1063 commit 768005e
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 16 deletions.
4 changes: 1 addition & 3 deletions src/Mono.Android/Android.Runtime/AndroidEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,7 @@ static IWebProxy GetDefaultProxy ()
[DynamicDependency (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof (Xamarin.Android.Net.AndroidMessageHandler))]
static object GetHttpMessageHandler ()
{
// FIXME: https://github.com/xamarin/xamarin-android/issues/8797
// Note that this is a problem for custom $(AndroidHttpClientHandlerType) or $XA_HTTP_CLIENT_HANDLER_TYPE
[UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "DynamicDependency should preserve AndroidMessageHandler.")]
[UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "Rooted by the <GenerateILLinkXml/> MSBuild task.")]
[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
static Type TypeGetType (string typeName) =>
Type.GetType (typeName, throwOnError: false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ _ResolveAssemblies MSBuild target.
</ItemGroup>
</Target>

<Target Name="_ResolveAssemblies">
<Target Name="_ResolveAssemblies"
DependsOnTargets="_GenerateILLinkXml">
<ItemGroup>
<_RIDs Include="$(RuntimeIdentifier)" Condition=" '$(RuntimeIdentifiers)' == '' " />
<_RIDs Include="$(RuntimeIdentifiers)" Condition=" '$(RuntimeIdentifiers)' != '' " />
Expand All @@ -96,6 +97,7 @@ _ResolveAssemblies MSBuild target.
;_OuterIntermediateAssembly=@(IntermediateAssembly)
;_OuterOutputPath=$(OutputPath)
;_OuterIntermediateOutputPath=$(IntermediateOutputPath)
;_AndroidGeneratedRootDescriptor=$(_AndroidGeneratedRootDescriptor)
</_AdditionalProperties>
<_AndroidBuildRuntimeIdentifiersInParallel Condition=" '$(_AndroidBuildRuntimeIdentifiersInParallel)' == '' ">true</_AndroidBuildRuntimeIdentifiersInParallel>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ properties that determine build ordering.
_SetLatestTargetFrameworkVersion;
_GetLibraryImports;
_RemoveRegisterAttribute;
_GenerateILLinkXml;
_ResolveAssemblies;
_ResolveSatellitePaths;
_CreatePackageWorkspace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ This file contains the .NET 5-specific targets to customize ILLink

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<UsingTask TaskName="Xamarin.Android.Tasks.GenerateILLinkXml" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />

<Target Name="_PrepareLinking"
Condition=" '$(PublishTrimmed)' == 'true' "
AfterTargets="ComputeResolvedFilesToPublishList"
Expand Down Expand Up @@ -94,6 +96,25 @@ This file contains the .NET 5-specific targets to customize ILLink
Condition=" '@(ResolvedFileToPublish->Count())' != '0' and '%(Filename)' != '' "
Include="@(_PreserveLists)" />
<TrimmerRootDescriptor Include="@(LinkDescription)" />
<TrimmerRootDescriptor
Condition=" Exists('$(_AndroidGeneratedRootDescriptor)') "
Include="$(_AndroidGeneratedRootDescriptor)"
/>
</ItemGroup>
</Target>

<Target Name="_GenerateILLinkXml"
Condition=" '$(PublishTrimmed)' == 'true' ">
<PropertyGroup>
<_AndroidGeneratedRootDescriptor>$(IntermediateOutputPath)net-android-trimmer.xml</_AndroidGeneratedRootDescriptor>
</PropertyGroup>
<GenerateILLinkXml
AndroidHttpClientHandlerType="$(AndroidHttpClientHandlerType)"
CustomViewMapFile="$(_CustomViewMapFile)"
OutputFile="$(_AndroidGeneratedRootDescriptor)"
/>
<ItemGroup>
<FileWrites Include="$(_AndroidGeneratedRootDescriptor)" />
</ItemGroup>
</Target>

Expand Down
70 changes: 70 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/GenerateILLinkXml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Xml.Linq;
using Microsoft.Android.Build.Tasks;
using Microsoft.Build.Framework;

namespace Xamarin.Android.Tasks;

public class GenerateILLinkXml : AndroidTask
{
public override string TaskPrefix => "GILX";

[Required]
public string AndroidHttpClientHandlerType { get; set; }

[Required]
public string CustomViewMapFile { get; set; }

[Required]
public string OutputFile { get; set; }

public override bool RunTask ()
{
string assemblyName, typeName;

var index = AndroidHttpClientHandlerType.IndexOf (',');
if (index != -1) {
typeName = AndroidHttpClientHandlerType.Substring (0, index).Trim ();
assemblyName = AndroidHttpClientHandlerType.Substring (index + 1).Trim ();
} else {
typeName = AndroidHttpClientHandlerType;
assemblyName = "Mono.Android";
}

// public parameterless constructors
// example: https://github.com/dotnet/runtime/blob/039d2ecb46687e89337d6d629c295687cfe226be/src/mono/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.xml
var ctor = new XElement ("method", new XAttribute("signature", "System.Void .ctor()"));

XElement linker;
var doc = new XDocument (
linker = new XElement ("linker",
new XElement ("assembly",
new XAttribute ("fullname", assemblyName),
new XElement ("type", new XAttribute ("fullname", typeName), ctor)
)
)
);

var customViewMap = MonoAndroidHelper.LoadCustomViewMapFile (BuildEngine4, CustomViewMapFile);
foreach (var pair in customViewMap) {
index = pair.Key.IndexOf (',');
if (index == -1)
continue;

typeName = pair.Key.Substring (0, index).Trim ();
assemblyName = pair.Key.Substring (index + 1).Trim ();

linker.Add (new XElement ("assembly",
new XAttribute ("fullname", assemblyName),
new XElement ("type", new XAttribute ("fullname", typeName), ctor)
));
}

if (doc.SaveIfChanged (OutputFile)) {
Log.LogDebugMessage ($"Saving {OutputFile}");
} else {
Log.LogDebugMessage ($"{OutputFile} is unchanged. Skipping.");
}

return !Log.HasLoggedErrors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,45 @@ static AssemblyDefinition CreateFauxMonoAndroidAssembly ()
return assm;
}

private void PreserveCustomHttpClientHandler (string handlerType, string handlerAssembly, string testProjectName, string assemblyPath)
private void PreserveCustomHttpClientHandler (
string handlerType,
string handlerAssembly,
string testProjectName,
string assemblyPath,
TrimMode trimMode)
{
var proj = new XamarinAndroidApplicationProject () { IsRelease = true };
testProjectName += trimMode.ToString ();

var class_library = new XamarinAndroidLibraryProject {
IsRelease = true,
ProjectName = "MyClassLibrary",
Sources = {
new BuildItem.Source ("MyCustomHandler.cs") {
TextContent = () => """
class MyCustomHandler : System.Net.Http.HttpMessageHandler
{
protected override Task <HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken) =>
throw new NotImplementedException ();
}
"""
}
}
};
using (var libBuilder = CreateDllBuilder ($"{testProjectName}/{class_library.ProjectName}")) {
Assert.IsTrue (libBuilder.Build (class_library), $"Build for {class_library.ProjectName} should have succeeded.");
}

var proj = new XamarinAndroidApplicationProject {
ProjectName = "MyApp",
IsRelease = true,
TrimModeRelease = trimMode
};
proj.AddReference (class_library);
proj.AddReferences ("System.Net.Http");
string handlerTypeFullName = string.IsNullOrEmpty(handlerAssembly) ? handlerType : handlerType + ", " + handlerAssembly;
proj.SetProperty (proj.ActiveConfigurationProperties, "AndroidHttpClientHandlerType", handlerTypeFullName);
proj.MainActivity = proj.DefaultMainActivity.Replace ("base.OnCreate (bundle);", "base.OnCreate (bundle);\nvar client = new System.Net.Http.HttpClient ();");
using (var b = CreateApkBuilder (testProjectName)) {
using (var b = CreateApkBuilder ($"{testProjectName}/{proj.ProjectName}")) {
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");

using (var assembly = AssemblyDefinition.ReadAssembly (Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, assemblyPath))) {
Expand All @@ -173,12 +204,14 @@ private void PreserveCustomHttpClientHandler (string handlerType, string handler
}

[Test]
public void PreserveCustomHttpClientHandlers ()
public void PreserveCustomHttpClientHandlers ([Values (TrimMode.Partial, TrimMode.Full)] TrimMode trimMode)
{
PreserveCustomHttpClientHandler ("Xamarin.Android.Net.AndroidMessageHandler", "",
"temp/PreserveAndroidMessageHandler", "android-arm64/linked/Mono.Android.dll");
"temp/PreserveAndroidMessageHandler", "android-arm64/linked/Mono.Android.dll", trimMode);
PreserveCustomHttpClientHandler ("System.Net.Http.SocketsHttpHandler", "System.Net.Http",
"temp/PreserveSocketsHttpHandler", "android-arm64/linked/System.Net.Http.dll");
"temp/PreserveSocketsHttpHandler", "android-arm64/linked/System.Net.Http.dll", trimMode);
PreserveCustomHttpClientHandler ("MyCustomHandler", "MyClassLibrary",
"temp/MyCustomHandler", "android-arm64/linked/MyClassLibrary.dll", trimMode);
}

[Test]
Expand Down
7 changes: 1 addition & 6 deletions tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using Android.App;
using Android.App;
using Android.Content;
using Android.Util;
using Android.Views;
using Android.Widget;
using NUnit.Framework;
using Mono.Android_Test.Library;

namespace Xamarin.Android.RuntimeTests
{
Expand All @@ -14,7 +12,6 @@ public class CustomWidgetTests
{
// https://bugzilla.xamarin.com/show_bug.cgi?id=23880
[Test]
[DynamicDependency (DynamicallyAccessedMemberTypes.All, typeof (CustomTextView))]
public void UpperCaseCustomWidget_ShouldNotThrowInflateException ()
{
Assert.DoesNotThrow (() => {
Expand All @@ -24,7 +21,6 @@ public void UpperCaseCustomWidget_ShouldNotThrowInflateException ()
}

[Test]
[DynamicDependency (DynamicallyAccessedMemberTypes.All, typeof (CustomTextView))]
public void LowerCaseCustomWidget_ShouldNotThrowInflateException ()
{
Assert.DoesNotThrow (() => {
Expand All @@ -34,7 +30,6 @@ public void LowerCaseCustomWidget_ShouldNotThrowInflateException ()
}

[Test]
[DynamicDependency (DynamicallyAccessedMemberTypes.All, typeof (CustomTextView))]
public void UpperAndLowerCaseCustomWidget_FromLibrary_ShouldNotThrowInflateException ()
{
Assert.DoesNotThrow (() => {
Expand Down

0 comments on commit 768005e

Please sign in to comment.