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
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,6 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer)
return true;
}

// User-defined ACW types (not MCW bindings, not interfaces) are unconditional
// because Android can instantiate them from Java at any time.
if (!peer.IsFrameworkAssembly && !peer.DoNotGenerateAcw && !peer.IsInterface) {
return true;
}

// Types marked unconditional by the scanner (component attributes: Activity, Service, etc.)
if (peer.IsUnconditional) {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,12 @@ internal AssemblyManifestInfo ScanAssemblyManifestInfo ()
/// [Application(ManageSpaceActivity = typeof(X))] must be unconditional,
/// because the manifest will reference them even if nothing else does.
/// </summary>
static void ForceUnconditionalCrossReferences (Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results, Dictionary<string, AssemblyIndex> assemblyCache)
void ForceUnconditionalCrossReferences (Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results, Dictionary<string, AssemblyIndex> assemblyCache)
{
foreach (var index in assemblyCache.Values) {
if (frameworkAssemblyNames.Contains (index.AssemblyName)) {
continue;
}
foreach (var attrInfo in index.AttributesByType.Values) {
if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) {
ForceUnconditionalIfPresent (results, applicationAttributeInfo.BackupAgent);
Expand Down Expand Up @@ -343,7 +346,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0;
var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0;

var isUnconditional = attrInfo is not null;
var isFrameworkAssembly = frameworkAssemblyNames.Contains (index.AssemblyName);
var isUnconditional = !isFrameworkAssembly &&
(attrInfo is not null || registerInfo?.IsFromJniTypeSignature == true);
var cannotRegisterInStaticConstructor = attrInfo is ApplicationAttributeInfo or InstrumentationAttributeInfo;
string? invokerTypeName = null;
ActivationCtorStyle? invokerActivationCtorStyle = null;
Expand Down Expand Up @@ -382,7 +387,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
ManagedTypeNamespace = ExtractNamespace (fullName),
ManagedTypeShortName = ExtractShortName (fullName),
AssemblyName = index.AssemblyName,
IsFrameworkAssembly = frameworkAssemblyNames.Contains (index.AssemblyName),
IsFrameworkAssembly = isFrameworkAssembly,
BaseJavaName = baseJavaName,
ImplementedInterfaceJavaNames = implementedInterfaces,
IsInterface = isInterface,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public TrimmableTypeMapResult Execute (
XDocument? manifestTemplate = null,
string? packageNamingPolicy = null,
bool generateTypeMapAssemblies = true,
bool errorOnCustomJavaObject = true)
bool errorOnCustomJavaObject = true,
IReadOnlyCollection<string>? customViewTypeNames = null)
{
_ = assemblies ?? throw new ArgumentNullException (nameof (assemblies));
_ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
Expand All @@ -47,6 +48,7 @@ public TrimmableTypeMapResult Execute (
}
MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames);

RootCustomViewTypes (allPeers, customViewTypeNames);
RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig), manifestConfig?.ApplicationJavaClass);
PropagateDeferredRegistrationToBaseClasses (allPeers);
PropagateCannotRegisterToDescendants (allPeers);
Expand All @@ -73,6 +75,20 @@ public TrimmableTypeMapResult Execute (
return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes);
}

internal static void RootCustomViewTypes (List<JavaPeerInfo> allPeers, IReadOnlyCollection<string>? customViewTypeNames)
{
if (customViewTypeNames is null || customViewTypeNames.Count == 0) {
return;
}

var names = new HashSet<string> (customViewTypeNames, StringComparer.Ordinal);
foreach (var peer in allPeers) {
if (names.Contains (peer.ManagedTypeName)) {
peer.IsUnconditional = true;
}
}
}

internal bool ValidateJavaNames (IReadOnlyList<JavaPeerInfo> peers, string? applicationJavaClass = null)
{
bool valid = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
<_GenerateTrimmableTypeMapDependsOn Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">$(IlcDynamicBuildPropertyDependencies)</_GenerateTrimmableTypeMapDependsOn>
<!-- _GetLibraryImports populates @(ExtractedManifestDocuments) (the extracted library .aar
AndroidManifest.xml files) that the legacy manifest merger merges into the app manifest. -->
<_GenerateTrimmableTypeMapDependsOn>$(_GenerateTrimmableTypeMapDependsOn);_GetLibraryImports</_GenerateTrimmableTypeMapDependsOn>
<_GenerateTrimmableTypeMapDependsOn>$(_GenerateTrimmableTypeMapDependsOn);_GetLibraryImports;_ConvertResourcesCases</_GenerateTrimmableTypeMapDependsOn>
<IncrementalCleanDependsOn>
_RecordTrimmableTypeMapFileWrites;
$(IncrementalCleanDependsOn);
Expand Down Expand Up @@ -120,7 +120,7 @@
Condition=" '$(AndroidTypeMapImplementation)' == 'trimmable' and '$(DesignTimeBuild)' != 'true' and '@(ReferencePath->Count())' != '0' and '$(_OuterIntermediateOutputPath)' == '' "
AfterTargets="CoreCompile"
DependsOnTargets="$(_GenerateTrimmableTypeMapDependsOn)"
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);@(ExtractedManifestDocuments);@(_AndroidTrimmableTypeMapExtraFrameworkAssembly);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)"
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);@(ExtractedManifestDocuments);@(_AndroidTrimmableTypeMapExtraFrameworkAssembly);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_CustomViewMapFile);$(_AndroidBuildPropertiesCache)"
Outputs="$(_TrimmableTypeMapOutputStamp)">

<ItemGroup>
Expand Down Expand Up @@ -152,6 +152,7 @@
JavaSourceOutputDirectory="$(_TypeMapJavaOutputDirectory)"
TargetFrameworkVersion="$(TargetFrameworkVersion)"
ManifestTemplate="$(_AndroidManifestAbs)"
CustomViewMapFile="$(_CustomViewMapFile)"
MergedAndroidManifestOutput="$(_TypeMapBaseOutputDir)AndroidManifest.xml"
MergedManifestDocuments="@(_MergedManifestDocuments)"
PackageName="$(_AndroidPackage)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public void LogCustomJavaObjectWarning (string managedTypeName) =>

public string? ManifestTemplate { get; set; }

public string? CustomViewMapFile { get; set; }

public string? MergedAndroidManifestOutput { get; set; }

/// <summary>
Expand Down Expand Up @@ -211,6 +213,9 @@ public override bool RunTask ()
if (!ManifestTemplate.IsNullOrEmpty () && File.Exists (ManifestTemplate)) {
manifestTemplate = XDocument.Load (ManifestTemplate);
}
IReadOnlyCollection<string>? customViewTypeNames = CustomViewMapFile.IsNullOrEmpty ()
? null
: MonoAndroidHelper.LoadCustomViewMapFile (BuildEngine4, CustomViewMapFile).Keys;

result = generator.Execute (
assemblies,
Expand All @@ -221,7 +226,8 @@ public override bool RunTask ()
manifestTemplate: manifestTemplate,
packageNamingPolicy: PackageNamingPolicy,
generateTypeMapAssemblies: GenerateTypeMapAssemblies,
errorOnCustomJavaObject: ErrorOnCustomJavaObject);
errorOnCustomJavaObject: ErrorOnCustomJavaObject,
customViewTypeNames: customViewTypeNames);
if (Log.HasLoggedErrors) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.RegularExpressions;
using Mono.Cecil;
using NUnit.Framework;
using Xamarin.Android.AssemblyStore;
using Xamarin.Android.Tasks;
Expand Down Expand Up @@ -770,6 +771,169 @@ public void ReleaseCoreClrTrimmableTypeMap_SingleRuntimeIdentifier_PackagesLinke
AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (dexFile, javaSourceDirectory, acwMapPath, proguardPrimaryPath);
}

[Test]
public void ReleaseCoreClrTrimmableTypeMap_TrimsUnusedBindingListenerImplementors ()
{
if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) {
return;
}

var testRoot = Path.Combine ("temp", $"{TestName}_{Guid.NewGuid ():N}");
var binding = new XamarinAndroidBindingProject {
IsRelease = true,
ProjectName = "ListenerBinding",
AndroidClassParser = "class-parse",
};
binding.SetRuntime (AndroidRuntime.CoreCLR);

var javaRoot = Path.Combine (Root, testRoot, "java");
var javaSource = Path.Combine ("com", "example", "listener", "Widget.java");
Directory.CreateDirectory (Path.Combine (javaRoot, Path.GetDirectoryName (javaSource) ?? ""));
binding.Jars.Add (new AndroidItem.EmbeddedJar (Path.Combine ("java", "listener.jar")) {
BinaryContent = new JarContentBuilder {
BaseDirectory = javaRoot,
JarFileName = "listener.jar",
JavaSourceFileName = javaSource,
JavaSourceText = """
package com.example.listener;

public class Widget {
public interface OnChangedListener {
void onChanged ();
}

public void setOnChangedListener (OnChangedListener listener) {
}
}
""",
}.Build,
});

using var bindingBuilder = CreateDllBuilder (Path.Combine (testRoot, binding.ProjectName));
Assert.IsTrue (bindingBuilder.Build (binding), "Listener binding build should have succeeded.");

foreach (bool useListener in new [] { false, true }) {
var app = new XamarinAndroidApplicationProject {
IsRelease = true,
PackageName = useListener ? "com.xamarin.listenerused" : "com.xamarin.listenerunused",
ProjectName = useListener ? "ListenerUsed" : "ListenerUnused",
};
app.SetRuntime (AndroidRuntime.CoreCLR);
app.SetProperty (KnownProperties.RuntimeIdentifier, "android-arm64");
app.SetProperty ("AndroidPackageFormat", "apk");
app.SetProperty (KnownProperties.AndroidLinkTool, "r8");
app.SetProperty ("TrimMode", "full");
app.SetProperty ("PublishReadyToRun", "false");
app.SetProperty ("AndroidTypeMapImplementation", "trimmable");
app.References.Add (new BuildItem.ProjectReference ($"..\\{binding.ProjectName}\\{binding.ProjectName}.csproj", binding.ProjectName, binding.ProjectGuid));
if (useListener) {
app.MainActivity = app.DefaultMainActivity.Replace (
"//${AFTER_ONCREATE}",
"""
var widget = new Com.Example.Listener.Widget ();
widget.Changed += (sender, args) => { };
""");
}

using var builder = CreateApkBuilder (Path.Combine (testRoot, app.ProjectName));
Assert.IsTrue (builder.Build (app), $"{app.ProjectName} build should have succeeded.");

var linkedDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked"));
var linkedBinding = Path.Combine (linkedDirectory, $"{binding.ProjectName}.dll");
var javaDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "typemap", "linked-java"));
var implementorJava = Path.Combine (javaDirectory, "mono", "com", "example", "listener", "Widget_OnChangedListenerImplementor.java");
var acwMapPath = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "acw-map.txt"));
var proguardPath = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "proguard", "proguard_project_primary.cfg"));
var dexPath = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "android", "bin", "classes.dex"));

Assert.AreEqual (
useListener,
AssemblyContainsType (linkedBinding, "Com.Example.Listener.Widget/IOnChangedListenerImplementor"),
$"{app.ProjectName} linked managed output should {(useListener ? "retain" : "trim")} the listener implementor.");
Assert.AreEqual (
useListener,
File.Exists (implementorJava),
$"{app.ProjectName} post-trim Java output should {(useListener ? "retain" : "trim")} the listener implementor.");
AssertFileContains (
acwMapPath,
"IOnChangedListenerImplementor",
useListener,
$"{app.ProjectName} ACW map");
AssertFileContains (
proguardPath,
"mono.com.example.listener.Widget_OnChangedListenerImplementor",
useListener,
$"{app.ProjectName} ProGuard configuration");
Assert.AreEqual (
useListener,
DexUtils.ContainsClass ("Lmono/com/example/listener/Widget_OnChangedListenerImplementor;", dexPath, AndroidSdkPath),
$"{app.ProjectName} DEX should {(useListener ? "retain" : "trim")} the listener implementor.");
}
}

[Test]
public void ReleaseCoreClrTrimmableTypeMap_UsesExternalJavaRoots ()
{
if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) {
return;
}

var app = new XamarinAndroidApplicationProject {
IsRelease = true,
PackageName = "com.xamarin.externaljavaroots",
ProjectName = "ExternalJavaRoots",
};
app.SetRuntime (AndroidRuntime.CoreCLR);
app.SetProperty (KnownProperties.RuntimeIdentifier, "android-arm64");
app.SetProperty ("AndroidPackageFormat", "apk");
app.SetProperty ("TrimMode", "full");
app.SetProperty ("PublishReadyToRun", "false");
app.SetProperty ("AndroidTypeMapImplementation", "trimmable");
app.Sources.Add (new BuildItem.Source ("Views.cs") {
TextContent = () => """
using Android.Content;
using Android.Util;
using Android.Views;

namespace ExternalJavaRoots;

public class LayoutOnlyView : View
{
public LayoutOnlyView (Context context, IAttributeSet attributes) : base (context, attributes)
{
}
}

public class UnusedView : View
{
public UnusedView (Context context) : base (context)
{
}
}
""",
});
app.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\layout\\layout_only.xml") {
TextContent = () => """
<?xml version="1.0" encoding="utf-8"?>
<ExternalJavaRoots.LayoutOnlyView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />
""",
});

using var builder = CreateApkBuilder (Path.Combine ("temp", $"{TestName}_{Guid.NewGuid ():N}"));
Assert.IsTrue (builder.Build (app), "External Java roots build should have succeeded.");

var linkedApp = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked", $"{app.ProjectName}.dll"));
Assert.IsTrue (AssemblyContainsType (linkedApp, "ExternalJavaRoots.LayoutOnlyView"), "The XML-only custom view should survive linking.");
Assert.IsFalse (AssemblyContainsType (linkedApp, "ExternalJavaRoots.UnusedView"), "An unreferenced ACW should be trimmed.");

var javaDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "typemap", "linked-java"));
Assert.IsNotEmpty (Directory.GetFiles (javaDirectory, "LayoutOnlyView.java", SearchOption.AllDirectories));
Assert.IsEmpty (Directory.GetFiles (javaDirectory, "UnusedView.java", SearchOption.AllDirectories));
}

[Test]
public void TrimmableTypeMap_PreserveLists_ArePackagedInSdk ()
{
Expand Down Expand Up @@ -1010,6 +1174,38 @@ static void AssertTrimmableTypeMapOutputs (string typemapDir)
var javaFiles = Directory.GetFiles (javaDir, "*.java", SearchOption.AllDirectories);
Assert.IsNotEmpty (javaFiles, "At least one trimmable JCW Java source file should be generated.");
}

static bool AssemblyContainsType (string assemblyPath, string typeFullName)
{
if (!File.Exists (assemblyPath)) {
return false;
}

using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath);
return ContainsType (assembly.MainModule.Types, typeFullName);
}

static bool ContainsType (IEnumerable<TypeDefinition> types, string typeFullName)
{
foreach (var type in types) {
if (type.FullName == typeFullName || ContainsType (type.NestedTypes, typeFullName)) {
return true;
}
}

return false;
}

static void AssertFileContains (string path, string value, bool expected, string description)
{
FileAssert.Exists (path, $"{description} should exist.");
var contents = File.ReadAllText (path);
Assert.AreEqual (
expected,
contents.Contains (value, StringComparison.Ordinal),
$"{description} should {(expected ? "contain" : "exclude")} '{value}'.");
}

DynamicCodeSupportProfile BuildDynamicCodeSupportProfile (string typemapImplementation, bool? dynamicCodeSupport)
{
var dynamicCodeSuffix = dynamicCodeSupport.HasValue ? $"_{dynamicCodeSupport.Value.ToString ().ToLowerInvariant ()}" : "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,28 @@ public void RootManifestReferencedTypes_RootsManifestReferencedTypes (
Assert.Contains (logMessages, m => m.Contains ("Rooting manifest-referenced type"));
}

[Fact]
public void RootCustomViewTypes_RootsOnlyReferencedManagedTypes ()
{
var peers = new List<JavaPeerInfo> {
new JavaPeerInfo {
JavaName = "crc64123456789abc/CustomView", CompatJniName = "my.app.CustomView",
ManagedTypeName = "MyApp.CustomView", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "CustomView",
AssemblyName = "MyApp",
},
new JavaPeerInfo {
JavaName = "crc64123456789abc/UnusedView", CompatJniName = "my.app.UnusedView",
ManagedTypeName = "MyApp.UnusedView", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "UnusedView",
AssemblyName = "MyApp",
},
};

TrimmableTypeMapGenerator.RootCustomViewTypes (peers, ["MyApp.CustomView"]);

Assert.True (peers [0].IsUnconditional);
Assert.False (peers [1].IsUnconditional);
}

[Fact]
public void RootManifestReferencedTypes_RootsApplicationAndInstrumentationTypes ()
{
Expand Down
Loading
Loading