Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android.
<DisableUnsupportedError Condition=" $([MSBuild]::IsOSPlatform('windows')) and '$(DisableUnsupportedError)' == '' ">true</DisableUnsupportedError>
<!-- HACK: make dotnet restore include Microsoft.NETCore.App.Runtime.NativeAOT.linux-bionic-arm64 -->
<_IsPublishing Condition=" '$(_IsPublishing)' == '' ">true</_IsPublishing>
<!-- ResolvedFileToPublish is not populated until _AndroidComputeIlcCompileInputs, so
proguard generation must run after that target instead of after ILLink. -->
<_GenerateProguardAfterTargets>_AndroidComputeIlcCompileInputs</_GenerateProguardAfterTargets>
</PropertyGroup>

<!-- Default property values for NativeAOT Debug configuration -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<UsingTask TaskName="Xamarin.Android.Tasks.GenerateTypeMappings" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.Tasks.GetMonoPlatformJar" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.Tasks.RemoveRegisterAttribute" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.Tasks.GenerateProguardConfiguration" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />

<PropertyGroup>
<_RemoveRegisterFlag>$(MonoAndroidIntermediateAssemblyDir)shrunk\shrunk.flag</_RemoveRegisterFlag>
Expand Down Expand Up @@ -179,11 +180,6 @@
-->
<_TrimmerCustomData Include="AndroidHttpClientHandlerType" Value="$(AndroidHttpClientHandlerType)" />
<_TrimmerCustomData Include="AndroidCustomViewMapFile" Value="$(_OuterCustomViewMapFile)" />
<_TrimmerCustomData
Condition=" '$(_ProguardProjectConfiguration)' != '' "
Include="ProguardConfiguration"
Value="$(_ProguardProjectConfiguration)"
/>
<_TrimmerCustomData Include="SystemIOHashingAssemblyPath" Value="$(_SystemIOHashingAssemblyPath)" />

<!--
Expand All @@ -200,12 +196,6 @@
<_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveJavaInterfaces" />
<_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.FixAbstractMethodsStep" />
<!-- Custom steps that run after MarkStep -->
<_TrimmerCustomSteps
Condition=" '$(_ProguardProjectConfiguration)' != '' "
Include="$(_AndroidLinkerCustomStepAssembly)"
AfterStep="CleanStep"
Type="Mono.Linker.Steps.GenerateProguardConfiguration"
/>
<_TrimmerCustomSteps
Condition=" '$(AndroidAddKeepAlives)' == 'true' "
Include="$(_AndroidLinkerCustomStepAssembly)"
Expand Down Expand Up @@ -350,4 +340,33 @@
</CollectTypeMapFilesForArchive>
</Target>

<!-- In the non-NativeAOT path, ResolvedFileToPublish is populated by the ILLink target
(via the SDK's ComputeResolvedFilesToPublishList), so we run proguard generation after ILLink.
In NativeAOT, ResolvedFileToPublish remains empty after ILLink because the ILC package's
_ComputeManagedAssemblyForILLink replaces ManagedAssemblyToLink after _LinkedResolvedFileToPublishCandidate
is computed, so the ILLink target's ResolvedFileToPublish rewrite adds nothing.
Instead, _AndroidComputeIlcCompileInputs populates ResolvedFileToPublish, so NativeAOT.targets
sets _GenerateProguardAfterTargets to run these targets after that. -->
<PropertyGroup>
<_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink</_GenerateProguardAfterTargets>
</PropertyGroup>

<Target Name="_PrepareLinkedAssembliesForProguard"
AfterTargets="$(_GenerateProguardAfterTargets)"
Condition=" '$(PublishTrimmed)' == 'true' and '$(_ProguardProjectConfiguration)' != '' ">
<ItemGroup>
<_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " />
</ItemGroup>
</Target>

<Target Name="_GenerateProguardConfiguration"
AfterTargets="_PrepareLinkedAssembliesForProguard"
Condition=" '$(PublishTrimmed)' == 'true' and '$(_ProguardProjectConfiguration)' != '' "
Inputs="@(_LinkedAssemblyForProguard)"
Outputs="$(_ProguardProjectConfiguration)">
<GenerateProguardConfiguration
LinkedAssemblies="@(_LinkedAssemblyForProguard)"
OutputFile="$(_ProguardProjectConfiguration)" />
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#nullable enable

using System;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using Microsoft.Build.Framework;
using Microsoft.Android.Build.Tasks;

namespace Xamarin.Android.Tasks
{
public class GenerateProguardConfiguration : AndroidTask
{
public override string TaskPrefix => "GPC";

[Required]
public ITaskItem[] LinkedAssemblies { get; set; } = [];

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

public override bool RunTask ()
{
var dir = Path.GetDirectoryName (OutputFile);
if (!Directory.Exists (dir))
Directory.CreateDirectory (dir);
Comment on lines +25 to +26
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path.GetDirectoryName can return null when the path doesn't contain directory information. The variable 'dir' should be checked for null before being passed to Directory.Exists and Directory.CreateDirectory. Consider using a pattern like the one in KeyTool.cs line 42 where the result is checked with IsNullOrEmpty before use.

Suggested change
if (!Directory.Exists (dir))
Directory.CreateDirectory (dir);
if (!dir.IsNullOrEmpty () && !Directory.Exists (dir)) {
Directory.CreateDirectory (dir);
}

Copilot uses AI. Check for mistakes.
using var writer = File.CreateText (OutputFile);

foreach (var assembly in LinkedAssemblies) {
ProcessAssembly (assembly.ItemSpec, writer);
}

return !Log.HasLoggedErrors;
}

void ProcessAssembly (string assemblyPath, TextWriter writer)
{
try {
using var stream = File.OpenRead (assemblyPath);
using var pe = new PEReader (stream);

if (!pe.HasMetadata)
return;

var reader = pe.GetMetadataReader ();

// Those assemblies that do not reference Mono.Android.dll (such as System.*
// assemblies and Mono.Android.dll itself) can be skipped.
// (Mono.Android.dll is special; android.jar is not part of classes.dex).
//
// FIXME: Those non-embedded jar bindings could visit here too, and they don't have to
// be part of proguard configuration. But they don't break (they will be NOTEd though).
if (!ReferencesMonoAndroid (reader))
return;

var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
writer.WriteLine ($"# ACW for {assemblyName}");

foreach (var typeHandle in reader.TypeDefinitions) {
var type = reader.GetTypeDefinition (typeHandle);
ProcessType (reader, type, writer);
}
} catch (BadImageFormatException ex) {
// Skip non-managed assemblies
Log.LogDebugMessage ($"Could not read assembly '{assemblyPath}': {ex.Message}");
}
}

static bool ReferencesMonoAndroid (MetadataReader reader)
{
foreach (var refHandle in reader.AssemblyReferences) {
var reference = reader.GetAssemblyReference (refHandle);
if (reader.GetString (reference.Name) == "Mono.Android")
return true;
}
return false;
}

void ProcessType (MetadataReader reader, TypeDefinition type, TextWriter writer)
{
// RegisterAttribute can be applied to interfaces, but proguard rules are only needed for classes.
// Structs don't need to be checked because RegisterAttribute cannot be applied to them.
if ((type.Attributes & System.Reflection.TypeAttributes.Interface) != 0)
return;

string? javaTypeName = null;
foreach (var attrHandle in type.GetCustomAttributes ()) {
var attr = reader.GetCustomAttribute (attrHandle);
var attrName = reader.GetCustomAttributeFullName (attr, Log);
if (attrName == "Android.Runtime.RegisterAttribute") {
var args = attr.GetCustomAttributeArguments ();
if (args.FixedArguments.Length > 0 && args.FixedArguments[0].Value is string jtype) {
javaTypeName = jtype.Replace ('/', '.');
}
break;
}
}

if (javaTypeName == null)
return;

writer.WriteLine ($"-keep class {javaTypeName}");
writer.WriteLine ($"-keepclassmembers class {javaTypeName} {{");

foreach (var methodHandle in type.GetMethods ()) {
ProcessMethod (reader, methodHandle, writer);
}

writer.WriteLine ("}");
writer.WriteLine ();
}

void ProcessMethod (MetadataReader reader, MethodDefinitionHandle methodHandle, TextWriter writer)
{
var method = reader.GetMethodDefinition (methodHandle);

foreach (var attrHandle in method.GetCustomAttributes ()) {
var attr = reader.GetCustomAttribute (attrHandle);
var attrName = reader.GetCustomAttributeFullName (attr, Log);
if (attrName == "Android.Runtime.RegisterAttribute") {
var args = attr.GetCustomAttributeArguments ();
if (args.FixedArguments.Length >= 2 &&
args.FixedArguments[0].Value is string jname &&
args.FixedArguments[1].Value is string jargs) {
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to jargs is useless, since its value is never read.

Suggested change
args.FixedArguments[1].Value is string jargs) {
args.FixedArguments[1].Value is string) {

Copilot uses AI. Check for mistakes.
if (jname == ".ctor") {
writer.WriteLine (" <init>(...);");
} else {
writer.WriteLine ($" *** {jname}(...);");
}
}
break;
}
}
}
}
}