|
2 | 2 | using System.Collections.Generic; |
3 | 3 | using System.IO; |
4 | 4 | using System.Linq; |
| 5 | +using System.Text.RegularExpressions; |
5 | 6 | using Microsoft.Build.Framework; |
6 | 7 | using Microsoft.Build.Utilities; |
7 | 8 | using NUnit.Framework; |
|
10 | 11 | namespace Xamarin.Android.Build.Tests |
11 | 12 | { |
12 | 13 | [TestFixture] |
13 | | - public class R8Tests |
| 14 | + public class R8Tests : BaseTest |
14 | 15 | { |
15 | 16 | [TestCase ("-keep class com.example.Foo { *; }", false, "")] |
16 | 17 | [TestCase ("-dontwarn com.example.**", false, "")] |
@@ -129,6 +130,155 @@ public void GenerateSeedMappingAllowsAcwObfuscation () |
129 | 130 | } |
130 | 131 | } |
131 | 132 |
|
| 133 | + [TestCase (false)] |
| 134 | + [TestCase (true)] |
| 135 | + public void R8JniObfuscationExplicitlyKeepsRuntimeOwnedJniTypes (bool nativeAot) |
| 136 | + { |
| 137 | + string path = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N")); |
| 138 | + Directory.CreateDirectory (path); |
| 139 | + string responseFile = ""; |
| 140 | + try { |
| 141 | + string acwMap = Path.Combine (path, "acw-map.txt"); |
| 142 | + string applicationConfiguration = Path.Combine (path, "application.cfg"); |
| 143 | + string commonConfiguration = Path.Combine (path, "xamarin.cfg"); |
| 144 | + File.WriteAllText (acwMap, "Managed.GeneratedPeer;com.example.GeneratedPeer\n"); |
| 145 | + var task = new R8TestTask { |
| 146 | + AcwMapFile = acwMap, |
| 147 | + BuildEngine = new MockBuildEngine (TestContext.Out), |
| 148 | + EnableObfuscation = true, |
| 149 | + EnableShrinking = true, |
| 150 | + JarPath = "r8.jar", |
| 151 | + JavaPlatformJarPath = "android.jar", |
| 152 | + OutputDirectory = path, |
| 153 | + ProguardCommonXamarinConfiguration = commonConfiguration, |
| 154 | + ProguardGeneratedApplicationConfiguration = applicationConfiguration, |
| 155 | + UseTrimmableNativeAotProguardConfiguration = nativeAot, |
| 156 | + }; |
| 157 | + |
| 158 | + task.TestGenerateCommandLineCommands (); |
| 159 | + responseFile = task.ResponseFilePath; |
| 160 | + string configuration = File.ReadAllText (commonConfiguration) + File.ReadAllText (applicationConfiguration); |
| 161 | + var keepTargets = Regex.Matches (configuration, @"^-keep (?:class|interface) (?<name>[^\s{]+)", RegexOptions.Multiline) |
| 162 | + .Cast<Match> () |
| 163 | + .Select (match => match.Groups ["name"].Value) |
| 164 | + .ToHashSet (StringComparer.Ordinal); |
| 165 | + |
| 166 | + foreach (string jniName in GetNativeRuntimeJniTypeNames ()) { |
| 167 | + string javaName = jniName.Replace ('/', '.'); |
| 168 | + Assert.That (keepTargets, Does.Contain (javaName), $"Runtime JNI type `{javaName}` must have an explicit keep rule."); |
| 169 | + } |
| 170 | + |
| 171 | + StringAssert.DoesNotContain ("-keep class net.dot.jni.**", configuration); |
| 172 | + StringAssert.DoesNotContain ("-keep class mono.android.**", configuration); |
| 173 | + StringAssert.Contains ("void monodroidAddReference(java.lang.Object);", configuration); |
| 174 | + StringAssert.Contains ("void monodroidClearReferences();", configuration); |
| 175 | + StringAssert.Contains ("public static native void registerNativeMembers(java.lang.Class,java.lang.String);", configuration); |
| 176 | + StringAssert.Contains ("public static native void construct(java.lang.Object,java.lang.String,java.lang.Object[]);", configuration); |
| 177 | + StringAssert.DoesNotContain ("com.example.GeneratedPeer", configuration, |
| 178 | + "An ordinary generated app peer must remain eligible for R8 obfuscation."); |
| 179 | + |
| 180 | + if (nativeAot) { |
| 181 | + Assert.That (keepTargets, Does.Contain ("net.dot.jni.nativeaot.JavaInteropRuntime")); |
| 182 | + Assert.That (keepTargets, Does.Contain ("net.dot.jni.nativeaot.NativeAotRuntimeProvider*")); |
| 183 | + StringAssert.Contains ("public static native void init(java.lang.ClassLoader,java.lang.String,java.lang.String,java.lang.String);", configuration); |
| 184 | + } else { |
| 185 | + Assert.That (keepTargets, Does.Not.Contain ("net.dot.jni.nativeaot.JavaInteropRuntime")); |
| 186 | + Assert.That (keepTargets, Does.Not.Contain ("net.dot.jni.nativeaot.NativeAotRuntimeProvider*")); |
| 187 | + } |
| 188 | + } finally { |
| 189 | + if (File.Exists (responseFile)) { |
| 190 | + File.Delete (responseFile); |
| 191 | + } |
| 192 | + Directory.Delete (path, recursive: true); |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + [TestCase (false)] |
| 197 | + [TestCase (true)] |
| 198 | + public void R8WithoutJniObfuscationRetainsBroadRuntimeKeepRules (bool nativeAot) |
| 199 | + { |
| 200 | + string path = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N")); |
| 201 | + Directory.CreateDirectory (path); |
| 202 | + string responseFile = ""; |
| 203 | + try { |
| 204 | + string commonConfiguration = Path.Combine (path, "xamarin.cfg"); |
| 205 | + var task = new R8TestTask { |
| 206 | + BuildEngine = new MockBuildEngine (TestContext.Out), |
| 207 | + EnableShrinking = true, |
| 208 | + JarPath = "r8.jar", |
| 209 | + JavaPlatformJarPath = "android.jar", |
| 210 | + OutputDirectory = path, |
| 211 | + ProguardCommonXamarinConfiguration = commonConfiguration, |
| 212 | + UseTrimmableNativeAotProguardConfiguration = nativeAot, |
| 213 | + }; |
| 214 | + |
| 215 | + task.TestGenerateCommandLineCommands (); |
| 216 | + responseFile = task.ResponseFilePath; |
| 217 | + string configuration = File.ReadAllText (commonConfiguration); |
| 218 | + |
| 219 | + StringAssert.Contains ("-dontobfuscate", configuration); |
| 220 | + StringAssert.Contains ("-keep class net.dot.jni.**", configuration); |
| 221 | + StringAssert.Contains ("-keep class mono.android.Runtime { *; }", configuration); |
| 222 | + if (nativeAot) { |
| 223 | + StringAssert.DoesNotContain ("-keep class mono.android.**", configuration); |
| 224 | + } else { |
| 225 | + StringAssert.Contains ("-keep class mono.android.**", configuration); |
| 226 | + } |
| 227 | + } finally { |
| 228 | + if (File.Exists (responseFile)) { |
| 229 | + File.Delete (responseFile); |
| 230 | + } |
| 231 | + Directory.Delete (path, recursive: true); |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + IEnumerable<string> GetNativeRuntimeJniTypeNames () |
| 236 | + { |
| 237 | + string sourceRoot = GetAssemblyMetadataValue ("XamarinAndroidSourcePath"); |
| 238 | + string headerPath = Path.Combine (sourceRoot, "src", "native", "common", "include", "shared", "runtime-jni-names.hh"); |
| 239 | + string header = File.ReadAllText (headerPath); |
| 240 | + string [] names = Regex.Matches (header, @"std::string_view \w+ \{ ""(?<value>[^""]+)"" \};") |
| 241 | + .Cast<Match> () |
| 242 | + .Select (match => match.Groups ["value"].Value) |
| 243 | + .ToArray (); |
| 244 | + var jniTypes = names |
| 245 | + .Where (name => name.Contains ('/')) |
| 246 | + .ToHashSet (StringComparer.Ordinal); |
| 247 | + |
| 248 | + string runtimeJavaPath = Path.Combine (sourceRoot, "src", "java-runtime", "java", "mono", "android", "Runtime.java"); |
| 249 | + string runtimeJava = File.ReadAllText (runtimeJavaPath); |
| 250 | + foreach (string fieldName in names.Where (name => name.StartsWith ("mono_android_", StringComparison.Ordinal) || name.StartsWith ("net_dot_jni_", StringComparison.Ordinal))) { |
| 251 | + Match field = Regex.Match (runtimeJava, $@"static java\.lang\.Class {Regex.Escape (fieldName)} = (?<type>[\w.]+)\.class;"); |
| 252 | + Assert.That (field.Success, Is.True, $"Runtime field `{fieldName}` must resolve to a Java class."); |
| 253 | + jniTypes.Add (field.Groups ["type"].Value.Replace ('.', '/')); |
| 254 | + } |
| 255 | + |
| 256 | + foreach (string directory in new [] { |
| 257 | + Path.Combine (sourceRoot, "src", "native", "clr"), |
| 258 | + Path.Combine (sourceRoot, "src", "native", "nativeaot"), |
| 259 | + }) { |
| 260 | + foreach (string file in Directory.EnumerateFiles (directory, "*.cc", SearchOption.AllDirectories)) { |
| 261 | + string source = File.ReadAllText (file); |
| 262 | + Assert.That (Regex.IsMatch (source, @"FindClass\s*\(\s*""(?:mono/|net/dot/)", RegexOptions.CultureInvariant), Is.False, |
| 263 | + $"SDK-owned FindClass names in `{file}` must use RuntimeJniNames and explicit keep coverage."); |
| 264 | + Assert.That (Regex.IsMatch (source, @"get_class_from_runtime_field\s*\([^;]*""(?:mono_android_|net_dot_jni_)", RegexOptions.CultureInvariant), Is.False, |
| 265 | + $"SDK-owned runtime fields in `{file}` must use RuntimeJniNames and explicit keep coverage."); |
| 266 | + } |
| 267 | + } |
| 268 | + |
| 269 | + string javaInteropPath = Path.Combine (sourceRoot, "external", "Java.Interop", "src", "Java.Interop", "Java.Interop"); |
| 270 | + foreach (string file in Directory.EnumerateFiles (javaInteropPath, "*.cs", SearchOption.TopDirectoryOnly)) { |
| 271 | + string source = File.ReadAllText (file); |
| 272 | + foreach (Match match in Regex.Matches (source, @"JniTypeName\s*=\s*""(?<name>net/dot/jni/(?:ManagedPeer|internal/JavaProxy(?:Object|Throwable)))""")) { |
| 273 | + jniTypes.Add (match.Groups ["name"].Value); |
| 274 | + } |
| 275 | + } |
| 276 | + |
| 277 | + Assert.That (jniTypes, Does.Contain ("mono/android/Runtime"), |
| 278 | + "CoreCLR JNI exports and NativeAOT startup require mono.android.Runtime to remain stable."); |
| 279 | + return jniTypes; |
| 280 | + } |
| 281 | + |
132 | 282 | [Test] |
133 | 283 | public void ValidateAppliedMappingUsesXA4327 () |
134 | 284 | { |
|
0 commit comments