Skip to content
Draft
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
4 changes: 2 additions & 2 deletions Documentation/docs-mobile/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,11 +472,11 @@ This property is `False` by default.

A boolean property that enables R8 obfuscation of Java type, method, and field names
referenced by managed JNI metadata. The build uses an R8-generated mapping to rewrite
managed assemblies before trimming, then applies the same
managed assemblies before trimming or NativeAOT compilation, then applies the same
mapping during the final R8 invocation.

This property requires `AndroidLinkTool=r8`,
`AndroidTypeMapImplementation=trimmable`, the CoreCLR runtime, and
`AndroidTypeMapImplementation=trimmable`, and either NativeAOT or CoreCLR with
`PublishTrimmed=true`.
The default value is `False`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<_AndroidBuildRuntimeIdentifiersInParallel
Condition=" ('$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' or '$(IlcGenerateDgmlFile)' == 'true') and '$(_AndroidBuildRuntimeIdentifiersInParallel)' == '' ">false</_AndroidBuildRuntimeIdentifiersInParallel>
<_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider</_TrimmableRuntimeProviderJavaName>
<AndroidLinkTool Condition=" '$(AndroidLinkTool)' == '' ">r8</AndroidLinkTool>
<AndroidDexTool Condition=" '$(AndroidLinkTool)' == 'r8' ">d8</AndroidDexTool>
<AndroidEnableProguard Condition=" '$(AndroidLinkTool)' != '' ">True</AndroidEnableProguard>
<AndroidCreateProguardMappingFile Condition=" '$(AndroidCreateProguardMappingFile)' == '' and '$(AndroidLinkTool)' == 'r8' ">True</AndroidCreateProguardMappingFile>
Expand Down Expand Up @@ -73,6 +72,64 @@
</ItemGroup>
</Target>

<Target Name="_AndroidRewriteJniNamesBeforeIlcInputs"
Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' "
DependsOnTargets="_AndroidGenerateR8JniSeedMapping">
<PropertyGroup>
<_AndroidR8JniRewrittenAssemblyDirectory>$(IntermediateOutputPath)r8-jni-rewritten/</_AndroidR8JniRewrittenAssemblyDirectory>
<_AndroidR8JniRewriteStamp>$(_AndroidStampDirectory)_AndroidRewriteJniNamesBeforeIlc.stamp</_AndroidR8JniRewriteStamp>
</PropertyGroup>
<ItemGroup>
<_AndroidR8JniIlcAssembly Remove="@(_AndroidR8JniIlcAssembly)" />
<_AndroidR8JniHashedIlcAssembly Remove="@(_AndroidR8JniHashedIlcAssembly)" />
<_AndroidR8JniRewrittenIlcAssembly Remove="@(_AndroidR8JniRewrittenIlcAssembly)" />
<_AndroidR8JniExpectedRewriteOutput Remove="@(_AndroidR8JniExpectedRewriteOutput)" />
<_AndroidR8JniMissingRewriteOutput Remove="@(_AndroidR8JniMissingRewriteOutput)" />
<_AndroidR8JniIlcAssembly Include="@(ManagedBinary)">
<R8JniInputKind>ManagedBinary</R8JniInputKind>
</_AndroidR8JniIlcAssembly>
<_AndroidR8JniIlcAssembly Include="@(IlcReference)">
<R8JniInputKind>IlcReference</R8JniInputKind>
</_AndroidR8JniIlcAssembly>
<_AndroidR8JniIlcAssembly Remove="@(_AndroidR8JniIlcAssembly)" Condition=" '%(Extension)' != '.dll' " />
</ItemGroup>
<ComputeHash Source="@(_AndroidR8JniIlcAssembly)">
<Output TaskParameter="Output" ItemName="_AndroidR8JniHashedIlcAssembly" />
</ComputeHash>
<ItemGroup>
<_AndroidR8JniRewrittenIlcAssembly Include="@(_AndroidR8JniHashedIlcAssembly->'$(_AndroidR8JniRewrittenAssemblyDirectory)%(Hash)/%(Filename)%(Extension)')" />
<_AndroidR8JniExpectedRewriteOutput Include="@(_AndroidR8JniRewrittenIlcAssembly);$(_AndroidR8JniRewriteManifest)" />
<_AndroidR8JniMissingRewriteOutput Include="@(_AndroidR8JniExpectedRewriteOutput)" Condition="!Exists('%(Identity)')" />
</ItemGroup>
<Delete Files="$(_AndroidR8JniRewriteStamp)" Condition=" '@(_AndroidR8JniMissingRewriteOutput->Count())' != '0' " />
</Target>

<Target Name="_AndroidRewriteJniNamesBeforeIlc"
Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' "
BeforeTargets="WriteIlcRspFileForCompilation"
DependsOnTargets="_AndroidRewriteJniNamesBeforeIlcInputs"
Inputs="@(_AndroidR8JniHashedIlcAssembly);$(_AndroidR8JniSeedMapping)"
Outputs="$(_AndroidR8JniRewriteStamp)">
<RewriteJniNamesForR8
SourceFiles="@(_AndroidR8JniHashedIlcAssembly)"
DestinationFiles="@(_AndroidR8JniRewrittenIlcAssembly)"
MappingFile="$(_AndroidR8JniSeedMapping)"
RewriteManifestFile="$(_AndroidR8JniRewriteManifest)" />
<Touch Files="$(_AndroidR8JniRewriteStamp)" AlwaysCreate="true" />
<ItemGroup>
<ManagedBinary Remove="@(ManagedBinary)" />
<ManagedBinary Include="@(_AndroidR8JniRewrittenIlcAssembly)" Condition=" '%(R8JniInputKind)' == 'ManagedBinary' " />
<IlcCompileInput Remove="@(IlcCompileInput)" />
<IlcCompileInput Include="@(ManagedBinary)" />
<IlcReference Remove="@(IlcReference)" />
<IlcReference Include="@(_AndroidR8JniRewrittenIlcAssembly)" Condition=" '%(R8JniInputKind)' == 'IlcReference' " />
<FileWrites Include="@(_AndroidR8JniRewrittenIlcAssembly)" />
<FileWrites Include="$(_AndroidR8JniRewriteStamp)" />
<_AndroidR8JniExpectedRewriteOutput Remove="@(_AndroidR8JniExpectedRewriteOutput)" />
<_AndroidR8JniMissingRewriteOutput Remove="@(_AndroidR8JniMissingRewriteOutput)" />
</ItemGroup>
</Target>

<!--
Force the typemap to be generated in the OUTER build (the build that is not an inner
per-RID build, i.e. '$(_OuterIntermediateOutputPath)' == '') before _ResolveAssemblies
Expand Down Expand Up @@ -231,15 +288,19 @@
<Target Name="_GenerateTrimmableTypeMapProguardConfiguration"
DependsOnTargets="_WriteTrimmableNativeAotProguardConfigurationInputs;_CollectTrimmableNativeAotDgmlFiles"
Condition=" '$(PublishTrimmed)' == 'true' and '$(_ProguardProjectConfiguration)' != '' "
Inputs="$(_TrimmableNativeAotProguardConfigurationInputsStamp);@(_TrimmableNativeAotDgmlFiles);$(IntermediateOutputPath)acw-map.txt"
Outputs="$(_ProguardProjectConfiguration)">
Inputs="$(_TrimmableNativeAotProguardConfigurationInputsStamp);@(_TrimmableNativeAotDgmlFiles);$(IntermediateOutputPath)acw-map.txt;$(_AndroidR8JniSeedMapping);$(_AndroidR8JniRewriteManifest)"
Outputs="$(_ProguardProjectConfiguration);$(_AndroidR8JniReachabilityManifest)">
<GenerateNativeAotProguardConfiguration
NativeAotDgmlFiles="@(_TrimmableNativeAotDgmlFiles)"
AcwMapFile="$(IntermediateOutputPath)acw-map.txt"
TrimJavaCallableWrappers="$(_AndroidTrimmableTypemapTrimJavaCode)"
OutputFile="$(_ProguardProjectConfiguration)" />
OutputFile="$(_ProguardProjectConfiguration)"
R8MappingFile="$(_AndroidR8JniSeedMapping)"
R8RewriteManifestFile="$(_AndroidR8JniRewriteManifest)"
R8ReachabilityManifestFile="$(_AndroidR8JniReachabilityManifest)" />
<ItemGroup>
<FileWrites Include="$(_ProguardProjectConfiguration)" />
<FileWrites Include="$(_AndroidR8JniReachabilityManifest)" Condition=" '$(_AndroidR8JniReachabilityManifest)' != '' " />
</ItemGroup>
</Target>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

-keep class net.dot.jni.** { *; <init>(...); }
-keep class net.dot.android.crypto.** { *; <init>(...); }
# The prebuilt native runtime resolves this class and its fields by JNI name.
-keep class mono.android.Runtime { *; }
# NativeAOT resolves these interface methods through JNI during startup.
-keep class mono.android.IGCUserPeer { *; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Android.Build.Tasks;
using Xamarin.Android.Tasks.JniRemapping;

namespace Xamarin.Android.Tasks;

Expand All @@ -23,6 +24,12 @@ public class GenerateNativeAotProguardConfiguration : AndroidTask
[Required]
public string OutputFile { get; set; } = "";

public string? R8MappingFile { get; set; }

public string? R8RewriteManifestFile { get; set; }

public string? R8ReachabilityManifestFile { get; set; }

// When false, the ILC DGML is not consulted (it may not have been generated at all) and a
// -keep rule is emitted for every Java type in the ACW map, so R8 keeps them all instead of
// shrinking the unused ones. Large binding closures can add several MB of compressed DEX, but
Expand Down Expand Up @@ -56,15 +63,73 @@ public override bool RunTask ()
retainedTypeKeys = LoadRetainedTypeKeysFromDgml ();
}

var allJavaTypes = LoadJavaTypesFromAcwMap (null);
// A null retainedTypeKeys means "keep every Java type in the ACW map" (Java trimming disabled).
var javaTypes = LoadJavaTypesFromAcwMap (retainedTypeKeys);
var javaTypes = retainedTypeKeys == null ? allJavaTypes : LoadJavaTypesFromAcwMap (retainedTypeKeys);
var reachableR8Entries = new HashSet<string> (StringComparer.Ordinal);
R8Mapping? mapping = null;
if (!R8MappingFile.IsNullOrEmpty ()) {
if (!File.Exists (R8MappingFile)) {
LogR8JniMappingError (string.Format (Properties.Resources.XA4327_SeedMappingNotFound, R8MappingFile));
return false;
}
if (R8RewriteManifestFile.IsNullOrEmpty () || !File.Exists (R8RewriteManifestFile)) {
LogR8JniMappingError (string.Format (Properties.Resources.XA4327_RewriteManifestNotFound, R8RewriteManifestFile));
return false;
}
try {
mapping = R8Mapping.Load (R8MappingFile);
var rewriteEntries = File.ReadAllLines (R8RewriteManifestFile);
// Comparing the seed mapping to itself validates every manifest entry and verifies
// that it identifies an entry in the seed mapping.
foreach (string conflict in mapping.GetReachabilityConflicts (mapping, rewriteEntries)) {
throw new FormatException (conflict);
}

var allAcwTypes = new HashSet<string> (StringComparer.Ordinal);
foreach (string javaTypeName in allJavaTypes) {
allAcwTypes.Add (javaTypeName.Replace ('.', '/'));
}
var retainedAcwTypes = new HashSet<string> (StringComparer.Ordinal);
foreach (string javaTypeName in javaTypes) {
string jniTypeName = javaTypeName.Replace ('.', '/');
retainedAcwTypes.Add (jniTypeName);
if (mapping.TryGetRenamedClass (jniTypeName, out _)) {
reachableR8Entries.Add (R8Mapping.BuildClassEntry (jniTypeName));
}
}
foreach (string entry in rewriteEntries) {
string [] parts = entry.Split ('\t');
string owningType = parts [1];
if (!allAcwTypes.Contains (owningType) || retainedAcwTypes.Contains (owningType)) {
reachableR8Entries.Add (entry);
}
}
} catch (FormatException ex) {
LogR8JniMappingError (string.Format (Properties.Resources.XA4327_MappingDataFailure, ex.Message));
return false;
} catch (IOException ex) {
LogR8JniMappingError (string.Format (Properties.Resources.XA4327_MappingDataFailure, ex.Message));
return false;
} catch (UnauthorizedAccessException ex) {
LogR8JniMappingError (string.Format (Properties.Resources.XA4327_MappingDataFailure, ex.Message));
return false;
}
}

using var writer = new StringWriter ();
writer.WriteLine ("# ACWs retained by NativeAOT ILC");
foreach (var javaTypeName in javaTypes) {
writer.WriteLine ($"-keep class {javaTypeName} {{ *; }}");
if (mapping == null) {
foreach (var javaTypeName in javaTypes) {
writer.WriteLine ($"-keep class {javaTypeName} {{ *; }}");
}
} else {
GenerateProguardConfiguration.WriteMappedRules (writer, reachableR8Entries);
}
File.WriteAllText (OutputFile, writer.ToString ());
if (!R8ReachabilityManifestFile.IsNullOrEmpty ()) {
WriteReachabilityManifest (R8ReachabilityManifestFile, reachableR8Entries);
}
Files.CopyIfStringChanged (writer.ToString (), OutputFile);

if (TrimJavaCallableWrappers) {
Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT trimmable typemap ProGuard rules from {1} DGML file(s).", javaTypes.Count, NativeAotDgmlFiles.Length);
Expand All @@ -74,6 +139,18 @@ public override bool RunTask ()
return !Log.HasLoggedErrors;
}

void LogR8JniMappingError (string detail)
=> Log.LogCodedError ("XA4327", Properties.Resources.XA4327, detail);

static void WriteReachabilityManifest (string path, IEnumerable<string> entries)
{
string? directory = Path.GetDirectoryName (path);
if (!directory.IsNullOrEmpty ()) {
Directory.CreateDirectory (directory);
}
File.WriteAllText (path, R8Mapping.CreateManifestContent (entries));
}

List<string> LoadJavaTypesFromAcwMap (HashSet<string>? retainedTypeKeys)
{
var javaTypes = new List<string> (retainedTypeKeys?.Count ?? 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public override bool RunTask ()
foreach (var assembly in LinkedAssemblies) {
ScanRewrittenAssembly (assembly.ItemSpec, mapping);
}
WriteMappedRules (writer, mapping);
WriteMappedRules (writer, mapping.AccessedEntries);
} else {
foreach (var assembly in LinkedAssemblies) {
ProcessAssembly (assembly.ItemSpec, writer);
Expand Down Expand Up @@ -110,18 +110,25 @@ void ScanRewrittenAssembly (string assemblyPath, R8Mapping mapping)
void LogR8JniMappingError (string detail)
=> Log.LogCodedError ("XA4327", Properties.Resources.XA4327, detail);

void WriteMappedRules (TextWriter writer, R8Mapping mapping)
internal static void WriteMappedRules (TextWriter writer, IEnumerable<string> entries)
{
var rules = new SortedDictionary<string, SortedSet<string>> (StringComparer.Ordinal);
foreach (string entry in mapping.AccessedEntries) {
foreach (string entry in entries) {
string [] parts = entry.Split ('\t');
if (parts.Length < 2) {
continue;
switch (parts.Length > 0 ? parts [0] : "") {
case "C" when parts.Length == 2:
break;
case "F" when parts.Length == 3:
break;
case "M" when parts.Length == 3:
break;
default:
throw new FormatException ($"Invalid R8 JNI reachability manifest entry '{entry}'.");
}
if (!rules.TryGetValue (parts [1], out var members)) {
rules [parts [1]] = members = new SortedSet<string> (StringComparer.Ordinal);
}
if (parts.Length != 3) {
if (parts [0] == "C") {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,9 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata
var path = Path.Combine (Root, "temp", TestName);
var dgmlFile = Path.Combine (path, "app.scan.dgml.xml");
var acwMapFile = Path.Combine (path, "acw-map.txt");
var mappingFile = Path.Combine (path, "mapping.txt");
var rewriteManifestFile = Path.Combine (path, "r8-jni-rewrite-manifest.txt");
var reachabilityManifestFile = Path.Combine (path, "r8-jni-reachability-manifest.txt");
var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg");
Directory.CreateDirectory (path);
File.WriteAllText (dgmlFile, """
Expand All @@ -394,23 +397,96 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata
Duplicate.Type;wrong.Duplicate
Other.Type;other.Type
""");
File.WriteAllText (mappingFile, """
crc64a1.MainActivity -> a.a:
my.app.Duplicate -> a.b:
androidx.activity.result.contract.ActivityResultContracts$TakePicture -> a.c:
non.acw.Helper -> a.d:
int value -> e
void run() -> f
other.Type -> a.g:
void removed() -> h:

""");
File.WriteAllText (rewriteManifestFile, """
C crc64a1/MainActivity
C non/acw/Helper
C other/Type
F non/acw/Helper value
M non/acw/Helper run():void
M other/Type removed():void

""".ReplaceLineEndings ("\n"));

var task = new GenerateNativeAotProguardConfiguration {
BuildEngine = new MockBuildEngine (TestContext.Out),
NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) },
AcwMapFile = acwMapFile,
OutputFile = outputFile,
R8MappingFile = mappingFile,
R8RewriteManifestFile = rewriteManifestFile,
R8ReachabilityManifestFile = reachabilityManifestFile,
TrimJavaCallableWrappers = true,
};

Assert.IsTrue (task.Execute (), "Task should succeed.");
var proguard = File.ReadAllText (outputFile);
StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard);
StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard);
StringAssert.Contains ("-keep class my.app.Duplicate { *; }", proguard);
StringAssert.Contains ("-keep class androidx.activity.result.contract.ActivityResultContracts$TakePicture { *; }", proguard);
StringAssert.DoesNotContain ("wrong.Duplicate", proguard);
StringAssert.Contains ("-keep,allowobfuscation class crc64a1.MainActivity", proguard);
StringAssert.Contains ("-keep,allowobfuscation class my.app.Duplicate", proguard);
StringAssert.Contains ("-keep,allowobfuscation class androidx.activity.result.contract.ActivityResultContracts$TakePicture", proguard);
StringAssert.Contains ("-keep,allowobfuscation class non.acw.Helper", proguard);
StringAssert.Contains ("-keepclassmembers,allowobfuscation class non.acw.Helper", proguard);
StringAssert.Contains ("*** value;", proguard);
StringAssert.Contains ("*** run(...);", proguard);
StringAssert.DoesNotContain ("-keep class crc64a1.MainActivity", proguard);
StringAssert.DoesNotContain ("other.Type", proguard);
StringAssert.DoesNotContain ("wrong.Duplicate", proguard);
CollectionAssert.AreEqual (new [] {
"C\tandroidx/activity/result/contract/ActivityResultContracts$TakePicture",
"C\tcrc64a1/MainActivity",
"C\tmy/app/Duplicate",
"C\tnon/acw/Helper",
"F\tnon/acw/Helper\tvalue",
"M\tnon/acw/Helper\trun():void",
}, File.ReadAllLines (reachabilityManifestFile));
StringAssert.DoesNotContain ("\r", File.ReadAllText (reachabilityManifestFile), "R8 JNI manifests should use deterministic LF line endings.");
}

[TestCase ("missing-mapping")]
[TestCase ("malformed-mapping")]
[TestCase ("missing-rewrite-manifest")]
[TestCase ("malformed-rewrite-manifest")]
public void Execute_GenerateNativeAotProguardConfiguration_InvalidR8InputUsesXA4327 (string invalidInput)
{
var path = Path.Combine (Root, "temp", TestName);
var acwMapFile = Path.Combine (path, "acw-map.txt");
var mappingFile = Path.Combine (path, "mapping.txt");
var rewriteManifestFile = Path.Combine (path, "r8-jni-rewrite-manifest.txt");
Directory.CreateDirectory (path);
File.WriteAllText (acwMapFile, "Managed.Type;managed.Type\n");
if (invalidInput != "missing-mapping") {
File.WriteAllText (mappingFile, invalidInput == "malformed-mapping"
? "not a mapping\n"
: "managed.Type -> a:\n");
}
if (invalidInput != "missing-rewrite-manifest") {
File.WriteAllText (rewriteManifestFile, invalidInput == "malformed-rewrite-manifest"
? "not a manifest entry\n"
: "C\tmanaged/Type\n");
}
var errors = new List<BuildErrorEventArgs> ();
var task = new GenerateNativeAotProguardConfiguration {
BuildEngine = new MockBuildEngine (TestContext.Out, errors),
AcwMapFile = acwMapFile,
OutputFile = Path.Combine (path, "proguard.cfg"),
R8MappingFile = mappingFile,
R8RewriteManifestFile = rewriteManifestFile,
TrimJavaCallableWrappers = false,
};

Assert.IsFalse (task.Execute (), "Invalid R8 input should fail the task.");
Assert.That (errors, Has.Count.EqualTo (1));
Assert.AreEqual ("XA4327", errors [0].Code);
}

[Test]
Expand Down
Loading
Loading