diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index afcc44839fb..631a8ff7283 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -37,6 +37,10 @@ public class R8 : D8 public ITaskItem []? ProguardConfigurationFiles { get; set; } public bool UseTrimmableNativeAotProguardConfiguration { get; set; } + // User-authored AndroidJavaSource (Bind != true) .java files. These have no managed peer and are + // therefore absent from the acw-map, so they must be kept explicitly when shrinking is enabled. + public ITaskItem []? JavaSourceFiles { get; set; } + protected override string MainClass => "com.android.tools.r8.R8"; readonly List tempFiles = new List (); @@ -52,6 +56,52 @@ public override bool RunTask () } } + // Derive the fully-qualified Java type name from each user .java source file. Java requires the + // public top-level type name to match the file name, so '.' is + // the type to keep. Files that no longer exist are skipped. + IEnumerable GetUserJavaTypes () + { + if (JavaSourceFiles == null) { + yield break; + } + var seen = new HashSet (StringComparer.Ordinal); + foreach (var item in JavaSourceFiles) { + var path = item.ItemSpec; + if (path.IsNullOrEmpty () || !File.Exists (path)) { + continue; + } + var typeName = Path.GetFileNameWithoutExtension (path); + var package = ReadJavaPackage (path); + if (!package.IsNullOrEmpty ()) { + typeName = $"{package}.{typeName}"; + } + if (seen.Add (typeName)) { + yield return typeName; + } + } + } + + static string? ReadJavaPackage (string path) + { + foreach (var raw in File.ReadLines (path)) { + var line = raw.Trim (); + if (line.Length == 0 || line.StartsWith ("//", StringComparison.Ordinal) || line.StartsWith ("*", StringComparison.Ordinal) || line.StartsWith ("/*", StringComparison.Ordinal)) { + continue; + } + if (line.StartsWith ("package ", StringComparison.Ordinal)) { + var end = line.IndexOf (';'); + if (end > "package ".Length) { + return line.Substring ("package ".Length, end - "package ".Length).Trim (); + } + } + // The package declaration, if present, must precede any type declaration. + if (line.StartsWith ("import ", StringComparison.Ordinal) || line.Contains ("class ") || line.Contains ("interface ") || line.Contains ("enum ")) { + break; + } + } + return null; + } + /// /// Override CreateResponseFile to add R8-specific arguments to the response file. /// This ensures all arguments are passed via response file to avoid command line length limits. @@ -109,6 +159,11 @@ protected override string CreateResponseFile () foreach (var java in javaTypes) { appcfg.WriteLine ($"-keep class {java} {{ *; }}"); } + // User-authored AndroidJavaSource (Bind != true) has no managed peer and is absent + // from the acw-map, so keep it explicitly; otherwise shrinking removes it. + foreach (var java in GetUserJavaTypes ()) { + appcfg.WriteLine ($"-keep class {java} {{ *; }}"); + } } } if (!ProguardCommonXamarinConfiguration.IsNullOrWhiteSpace ()) { diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index 40b45798c35..f66903d0449 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -51,6 +51,9 @@ Copyright (C) 2018 Xamarin. All rights reserved. <_AndroidD8MapDiagnostics Condition=" '$(AndroidD8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> <_AndroidR8MapDiagnostics Condition=" '$(AndroidR8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> + + <_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />