diff --git a/.github/skills/tests/references/test-catalog.md b/.github/skills/tests/references/test-catalog.md index 583f32cfce6..6cdc6968fc9 100644 --- a/.github/skills/tests/references/test-catalog.md +++ b/.github/skills/tests/references/test-catalog.md @@ -39,6 +39,13 @@ These tests can be run immediately with `dotnet test` on the `.csproj`, even if --- +The NativeAOT object/MSBuild integration cases in the trimmable type map suite +also require the NDK `llvm-readobj` and adjacent `llvm-objdump` and `clang` executables. Pass +`-p:_NativeAotLlvmReadObjPath=/path/to/ndk/toolchains/llvm/prebuilt//bin/llvm-readobj` +(with `.exe` on Windows) to execute those cases; without it, those cases are +reported as skipped. The NativeFormat parser and other typemap unit tests do +not require native tools. + ## Host-Side MSBuild Tests (full-build — requires local SDK) Assembly: `bin/TestDebug/${TFM}/Xamarin.Android.Build.Tests.dll` diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml index 6116d95ea22..dfd9f46be5b 100644 --- a/Documentation/docs-mobile/TOC.yml +++ b/Documentation/docs-mobile/TOC.yml @@ -382,6 +382,10 @@ href: messages/xa4325.md - name: XA4326 href: messages/xa4326.md + - name: XA4327 + href: messages/xa4327.md + - name: XA4328 + href: messages/xa4328.md - name: "XA5xxx: GCC and toolchain" items: - name: "XA5xxx: GCC and toolchain" diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index dc0a3d7f918..2e2ea53cde5 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -1145,6 +1145,12 @@ An enum-style property that specifies how `r8` obfuscates Java names when This property does not disable R8 code shrinking. +When managed-trimmed CoreCLR builds use retained typemap rules, all Java +names are preserved regardless of this setting, but R8 optimization remains +enabled. This path uses the optimizing Android defaults together with +`-dontobfuscate` and `--no-minification`; disabling renaming does not disable +optimization. + This property was introduced in a .NET 10 servicing release. It defaults to `disabled` in .NET 10 and to `private-members` in .NET 11 and later. diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index abe2d010109..05dc8b57f3f 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -255,14 +255,16 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4316](xa4316.md): Specified input file '{file}' does not exist. Ignoring. + [XA4317](xa4317.md): Input file '{file}' does not start with ``. Skipping. + [XA4318](xa4318.md): Input file '{file}' could not be read: {message}. Skipping. -+ [XA4319](xa4319.md): No NativeAOT DGML files were provided. -+ [XA4320](xa4320.md): ACW map file '{file}' was not found. -+ [XA4321](xa4321.md): NativeAOT DGML file '{file}' was not found. ++ [XA4319](xa4319.md): Retired: No NativeAOT DGML files were provided. ++ [XA4320](xa4320.md): Retired: ACW map file '{file}' was not found. ++ [XA4321](xa4321.md): Retired: NativeAOT DGML file '{file}' was not found. + [XA4322](xa4322.md): Skipping library ProGuard configuration file '{file}' (from {source}) because it contains the unsupported global option '{option}'. Global ProGuard options are only allowed in application projects. + [XA4323](xa4323.md): Ignoring directory '{directory}' as it does not exist. + [XA4324](xa4324.md): [{arch}] Unable to delete source file '{file}'. + [XA4325](xa4325.md): Failed to rewrite managed JNI names for R8. {message} + [XA4326](xa4326.md): Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous `JNIEnv.FindClass` source. ++ [XA4327](xa4327.md): Could not extract Java type map keys from '{0}': {1} ++ [XA4328](xa4328.md): Could not generate typemap ProGuard configuration from '{0}': {1} ## XA5xxx: GCC and toolchain diff --git a/Documentation/docs-mobile/messages/xa4319.md b/Documentation/docs-mobile/messages/xa4319.md index 636c8b9dcd8..eb4bd3eb291 100644 --- a/Documentation/docs-mobile/messages/xa4319.md +++ b/Documentation/docs-mobile/messages/xa4319.md @@ -1,26 +1,13 @@ --- title: .NET for Android error XA4319 description: XA4319 error code -ms.date: 05/27/2026 +ms.date: 09/18/2026 f1_keywords: - "XA4319" --- # .NET for Android error XA4319 -## Example messages - -``` -error XA4319: No NativeAOT DGML files were provided. -``` - -## Issue - -The .NET for Android build could not find any NativeAOT DGML files to use when -generating trimmable type map ProGuard configuration. - -## Solution - -This error is expected only for internal build state inconsistencies. Ensure the -project is building with the intended Runtime Identifier settings and that the -NativeAOT compile produced its DGML scan file. +This diagnostic has been retired. NativeAOT Java trimming reads ILC object +files and does not require dependency graphs. See [XA4327](xa4327.md) for +current typemap extraction failures. diff --git a/Documentation/docs-mobile/messages/xa4320.md b/Documentation/docs-mobile/messages/xa4320.md index cb70fac90bf..648c4128aef 100644 --- a/Documentation/docs-mobile/messages/xa4320.md +++ b/Documentation/docs-mobile/messages/xa4320.md @@ -1,25 +1,13 @@ --- title: .NET for Android error XA4320 description: XA4320 error code -ms.date: 05/27/2026 +ms.date: 09/18/2026 f1_keywords: - "XA4320" --- # .NET for Android error XA4320 -## Example messages - -``` -error XA4320: ACW map file '{file}' was not found. -``` - -## Issue - -The .NET for Android build could not find the Android Callable Wrapper (ACW) map -file needed to generate trimmable type map ProGuard configuration. - -## Solution - -This error is expected only for internal build state inconsistencies. Rebuild -the project from a clean state so the ACW map is regenerated. +This diagnostic has been retired. NativeAOT Java trimming reads ILC object +files instead of joining dependency graphs with an ACW map. See +[XA4327](xa4327.md) for current typemap extraction failures. diff --git a/Documentation/docs-mobile/messages/xa4321.md b/Documentation/docs-mobile/messages/xa4321.md index f082e1e7157..3574fb5e09e 100644 --- a/Documentation/docs-mobile/messages/xa4321.md +++ b/Documentation/docs-mobile/messages/xa4321.md @@ -1,26 +1,13 @@ --- title: .NET for Android error XA4321 description: XA4321 error code -ms.date: 05/27/2026 +ms.date: 09/18/2026 f1_keywords: - "XA4321" --- # .NET for Android error XA4321 -## Example messages - -``` -error XA4321: NativeAOT DGML file '{file}' was not found. -``` - -## Issue - -The .NET for Android build could not find a NativeAOT DGML scan file needed to -generate trimmable type map ProGuard configuration. - -## Solution - -This error is expected only for internal build state inconsistencies. Rebuild -the project from a clean state and verify the NativeAOT compile produced the -DGML scan file for the selected Runtime Identifier. +This diagnostic has been retired. NativeAOT Java trimming reads ILC object +files and does not require dependency graphs. See [XA4327](xa4327.md) for +current typemap extraction failures. diff --git a/Documentation/docs-mobile/messages/xa4327.md b/Documentation/docs-mobile/messages/xa4327.md new file mode 100644 index 00000000000..403fa484b84 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4327.md @@ -0,0 +1,37 @@ +--- +title: .NET for Android error XA4327 +description: XA4327 error code +ms.date: 09/18/2026 +f1_keywords: + - "XA4327" +--- + +# .NET for Android error XA4327 + +## Example message + +``` +error XA4327: Could not extract Java type map keys from 'typemaps.arm64-v8a.ll': ... +``` + +## Issue + +The build could not read retained Java class names from a NativeAOT object, +a CoreCLR LLVM typemap, or linked trimmable typemap assemblies. +The input is missing, unreadable, malformed, or uses an unsupported representation. +The diagnostic includes the input and the underlying failure. + +An empty, valid typemap is supported. A failed extraction is not treated as an +empty typemap, because doing so could remove Java classes required at runtime. + +## Solution + +Fix the reported input or the earlier task that produces it. If the input was +generated by .NET for Android, clean and rebuild the application. If the error +persists, [report an issue](https://github.com/dotnet/android/issues/new/choose) +with a binary build log and the affected typemap input. + +NativeAOT object extraction also requires the Android NDK's `llvm-readobj` +and adjacent `llvm-objdump`. +Install the NDK selected by `GetAndroidDependencies`, or point +`AndroidNdkDirectory` at a complete installation. diff --git a/Documentation/docs-mobile/messages/xa4328.md b/Documentation/docs-mobile/messages/xa4328.md new file mode 100644 index 00000000000..47600405eef --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4328.md @@ -0,0 +1,33 @@ +--- +title: .NET for Android error XA4328 +description: XA4328 error code +ms.date: 09/18/2026 +f1_keywords: + - "XA4328" +--- + +# .NET for Android error XA4328 + +## Example message + +``` +error XA4328: Could not generate typemap ProGuard configuration from 'typemap.keys.txt': Line 1 contains an invalid retained Java class name: 'example/*'. +``` + +## Issue + +The class-only ProGuard generator could not read a retained-keys file, found +an invalid class name, received no input files, or could not write its output. +Keys must be UTF-8 Java class names using `/` as the package separator, such as +`example/Outer$Inner`. Wildcards, ProGuard directives, dotted names, type +descriptors, and typemap alias suffixes are not valid records. + +A zero-byte input is valid and produces no class keep rules. A missing file is +an error, not an empty input. + +## Solution + +Fix the reported input or output path and permissions. For files generated by +.NET for Android, clean and rebuild. If the error persists, +[report an issue](https://github.com/dotnet/android/issues/new/choose) with a +binary build log and the affected keys file. diff --git a/Documentation/guides/D8andR8.md b/Documentation/guides/D8andR8.md index b44b1ea04a8..95b82647c46 100644 --- a/Documentation/guides/D8andR8.md +++ b/Documentation/guides/D8andR8.md @@ -1,5 +1,104 @@ This is the D8 and R8 integration specification for .NET for Android. +# Retained typemap class roots + +When managed trimming and R8 are enabled, CoreCLR and trimmable NativeAOT +builds derive Java class roots from the final typemap rather than retaining +every class in the ACW map: + +| Runtime and typemap | Retained-key source | +| --- | --- | +| NativeAOT, trimmable | Retained external typemap records in each RID's final ILC object | +| CoreCLR, LLVM IR | The generated LLVM Java-name blob, after `GenerateTypeMappings` | +| CoreCLR, trimmable | Surviving `TypeMapAttribute` records in linked typemap assemblies, including empty stubs | + +Inner builds return their exact source paths to the outer build. Keys are +unioned across the requested RIDs/ABIs; stale files from other builds are not +discovered by globbing. NativeAOT locates external typemap blobs with +`llvm-readobj`, resolves Java group identities using bounded `llvm-objdump` +relocations, reads their object-file byte ranges, and decodes the retained +NativeFormat keys. It does not require an ACW map or generate dependency graphs. + +All three adapters and both ProGuard generators are `Microsoft.Android.Tasks` +tasks in `Microsoft.Android.Build.Tasks.dll`. +The adapters write `typemap.keys.txt` in the outer intermediate directory. +The format is UTF-8 without a BOM, one canonical JNI class name per line, +ordinal-sorted and distinct, with LF endings and a final LF when nonempty. +For example: + +```text +android/app/Activity +example/Outer$Inner +``` + +The assembly and NativeAOT object adapters collapse implementation-specific +numeric aliases before writing this format. Valid JNI array entries contribute +their reference element class; primitive arrays do not contribute a class. +A valid zero-byte file means no retained classes; +missing, unreadable, or unsupported inputs fail the build. + +`GenerateTypeMapProguardConfiguration` knows only this format. It accepts a +union of keys files and writes deterministic class-only rules to +`proguard/proguard_project_references.cfg`: + +```text +-keep class android.app.Activity +-keep class example.Outer$Inner +``` + +The class-root generator never emits member rules or global R8 options. +CoreCLR uses a separate `GenerateTypeMapMemberProguardConfiguration` task to +write `proguard/proguard_typemap_members.cfg` from the same retained keys: + +```text +-keepclassmembers class android.app.Activity { *; } +-keepclassmembers class example.Outer$Inner { *; } +``` + +This preserves JNI-facing methods, constructors, and fields without also +preserving every member of Java-only dependencies. R8's member matching +includes inherited accessible methods and superclass fields. The separate +`proguard_typemap_coreclr.cfg` configuration preserves runtime bootstrap +classes and their members, plus framework-driven view members. Dynamic JNI +or reflection access to types or members not represented by retained managed +bindings still needs application/library ProGuard rules. + +NativeAOT continues to use `proguard_typemap.cfg`, which retains members of +all surviving classes, including third-party classes, while allowing unused +classes to disappear. Both policies keep explicit runtime bootstrap roots +rather than whole wrapper packages. User Java source retention and +application/library ProGuard rules remain separate. + +The temporary private override `_AndroidEnableTypemapR8Trimming` controls this +pipeline. It replaces the old NativeAOT-specific trimming and ProGuard switches. +Leave it unset for automatic CoreCLR behavior, set it to `true` to enable +the pipeline in eligible managed-trimmed CoreCLR/NativeAOT R8 builds, or set it +to `false` to use legacy ACW retention without running the new tasks. Disabling +it avoids NativeAOT object inspection. The switch does not enable R8 or managed +trimming in otherwise ineligible builds. + +NativeAOT object inspection currently requires explicit `true` and the Android +NDK's `llvm-readobj` and adjacent `llvm-objdump`, even when using the +workload-provided native linker. The workload's native tools do not yet include them. +`GetAndroidDependencies` includes the NDK for this opt-in configuration. +Leaving the override unset or `false` preserves NativeAOT's no-NDK build path +and keeps all ACW classes. + +This pipeline disables all obfuscation, including private-member +obfuscation, with both `-dontobfuscate` and R8's `--no-minification` option. +CoreCLR uses `proguard-android-optimize.txt` so optimization remains enabled +independently of obfuscation. Its JNI class/member keep rules still protect +the names and signatures required by managed code. NativeAOT retains its +existing non-optimizing defaults. +MonoVM, nonshrinking/multidex-only builds, and the existing complete +`ProguardConfigFiles` override keep their previous behavior. + +Both extraction and rule generation use their real files as incremental +outputs. A content-sensitive input manifest also tracks source-list and +runtime/trim-policy changes, so removing a RID or switching modes cannot +reuse stale class roots. The outputs are registered in `FileWrites` and +regenerated if deleted. + # What is D8? What is R8? At a high level, here are the steps that occur during an Android diff --git a/src/Microsoft.Android.Build.Tasks/Microsoft.Android.Build.Tasks.csproj b/src/Microsoft.Android.Build.Tasks/Microsoft.Android.Build.Tasks.csproj index d4949cffaec..5b8d17b5792 100644 --- a/src/Microsoft.Android.Build.Tasks/Microsoft.Android.Build.Tasks.csproj +++ b/src/Microsoft.Android.Build.Tasks/Microsoft.Android.Build.Tasks.csproj @@ -12,6 +12,7 @@ $(DotNetTargetFramework) Microsoft.Android.Tasks enable + true Nullable + + <_AndroidLinkedTypeMapKeySource Include="@(_GeneratedTypeMapAssembliesFromList->'$(IntermediateOutputPath)linked\%(Filename)%(Extension)')" + Condition=" '$(_AndroidEnableTypemapR8Trimming)' != 'false' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(AndroidTypeMapImplementation)' == 'trimmable' and '$(PublishTrimmed)' == 'true' " /> + + + + + + + + + + + <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_AndroidGenerateTypeMapProguardConfiguration + + + + + + <_ProguardProjectConfiguration>$(IntermediateOutputPath)proguard\proguard_project_references.cfg + <_AndroidTypeMapProguardConfiguration>$(_ProguardProjectConfiguration) + + <_AndroidUseTypeMapProguardConfiguration Condition=" '$(ProguardConfigFiles)' == '' ">true + <_AndroidUseScopedTypeMapMembers Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and '$(_AndroidUseTypeMapProguardConfiguration)' == 'true' ">true + + + + + + <_ProguardProjectConfiguration>$(IntermediateOutputPath)proguard\proguard_project_references.cfg + <_AndroidTypeMapProguardConfiguration>$(_ProguardProjectConfiguration) + + + + + + + + + + + + + + + <_AndroidTypeMapKeysFile>$(IntermediateOutputPath)typemap.keys.txt + <_AndroidTypeMapKeysInputsFile>$(IntermediateOutputPath)typemap.keys.inputs + <_AndroidTypeMapTasksAssemblyFile>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', '$(_MicrosoftAndroidBuildTasksAssembly)')) + <_AndroidTypeMapKeysKind Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">native-object + <_AndroidTypeMapKeysKind Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and '$(AndroidTypeMapImplementation)' != 'trimmable' ">llvm-ir + <_AndroidTypeMapKeysKind Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and '$(AndroidTypeMapImplementation)' == 'trimmable' ">assemblies + <_AndroidTypeMapLinkedAssemblies>@(ResolvedFileToPublish->'%(AndroidTypeMapLinkedAssemblies)'->Distinct()) + <_AndroidTypeMapLlvmReadObjPath /> + <_AndroidTypeMapLlvmReadObjPath Condition=" '$(_AndroidTypeMapKeysKind)' == 'native-object' ">@(ResolvedFileToPublish->'%(AndroidTypeMapLlvmReadObjPath)'->Distinct()) + <_AndroidTypeMapLlvmObjDumpPath /> + <_AndroidTypeMapLlvmObjDumpPath Condition=" '$(_AndroidTypeMapKeysKind)' == 'native-object' ">@(ResolvedFileToPublish->'%(AndroidTypeMapLlvmObjDumpPath)'->Distinct()) + + + <_AndroidTypeMapKeySource Remove="@(_AndroidTypeMapKeySource)" /> + + <_AndroidTypeMapKeySource Include="@(ResolvedFileToPublish->'%(AndroidTypeMapNativeObject)'->Distinct())" + Condition=" '$(_AndroidTypeMapKeysKind)' == 'native-object' " /> + <_AndroidTypeMapKeySource Include="@(_TypeMapAssemblySource->Distinct())" + Condition=" '$(_AndroidTypeMapKeysKind)' == 'llvm-ir' " /> + <_AndroidTypeMapKeySource Include="$(_AndroidTypeMapLinkedAssemblies)" + Condition=" '$(_AndroidTypeMapKeysKind)' == 'assemblies' " /> + <_AndroidTypeMapKeysState Remove="@(_AndroidTypeMapKeysState)" /> + <_AndroidTypeMapKeysState Include="Runtime=$(_AndroidRuntime);Representation=$(_AndroidTypeMapKeysKind);Optimize=$(Optimize);RuntimeIdentifier=$(RuntimeIdentifier);RuntimeIdentifiers=$(RuntimeIdentifiers);TypemapPolicy=$(_AndroidUseTypeMapProguardConfiguration);Enabled=$(_AndroidEnableTypemapR8Trimming);ReadObj=$(_AndroidTypeMapLlvmReadObjPath);ObjDump=$(_AndroidTypeMapLlvmObjDumpPath)" /> + <_AndroidTypeMapKeysState Include="@(_AndroidTypeMapKeySource->'%(FullPath)')" /> + + + + + + + + + + + + + + + + + + + + <_AndroidTypeMapKeyFile Remove="@(_AndroidTypeMapKeyFile)" /> + <_AndroidTypeMapKeyFile Include="$(_AndroidTypeMapKeysFile)" /> + + + + + + + + + + + + + <_AndroidTypeMapMemberProguardConfiguration>$(IntermediateOutputPath)proguard\proguard_typemap_members.cfg + + + + + + + <_ProguardConfiguration Include="$(_AndroidTypeMapMemberProguardConfiguration)" /> + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 5dd6a30bb97..e94d110e885 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -43,8 +43,8 @@ - - - - <_AndroidTrimmableTypemapTrimJavaCode Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == '' and '$(Optimize)' == 'true' ">true - <_AndroidTrimmableTypemapTrimJavaCode Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == '' ">false - - <_AndroidBuildRuntimeIdentifiersInParallel - Condition=" ('$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' or '$(IlcGenerateDgmlFile)' == 'true') and '$(_AndroidBuildRuntimeIdentifiersInParallel)' == '' ">false <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider r8 d8 True True - <_UseTrimmableNativeAotProguardConfiguration Condition=" '$(_UseTrimmableNativeAotProguardConfiguration)' == '' ">true - <_TrimmableNativeAotProguardConfigurationInputsStamp>$(_AndroidStampDirectory)_GenerateTrimmableTypeMapProguardConfiguration.inputs - <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_GenerateTrimmableTypeMapProguardConfiguration - - @@ -162,85 +134,4 @@ - - - <_TrimmableNativeAotRuntimeIdentifiers Remove="@(_TrimmableNativeAotRuntimeIdentifiers)" /> - <_TrimmableNativeAotSiblingRuntimeIdentifiers Remove="@(_TrimmableNativeAotSiblingRuntimeIdentifiers)" /> - <_TrimmableNativeAotDgmlFiles Remove="@(_TrimmableNativeAotDgmlFiles)" /> - <_TrimmableNativeAotScanDgmlCandidates Remove="@(_TrimmableNativeAotScanDgmlCandidates)" /> - <_TrimmableNativeAotCodegenDgmlFiles Remove="@(_TrimmableNativeAotCodegenDgmlFiles)" /> - <_TrimmableNativeAotCodegenDgmlCandidates Remove="@(_TrimmableNativeAotCodegenDgmlCandidates)" /> - - <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifier)" Condition=" '$(RuntimeIdentifiers)' == '' and '$(RuntimeIdentifier)' != '' " /> - <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifiers)" Condition=" '$(RuntimeIdentifiers)' != '' " /> - <_TrimmableNativeAotSiblingRuntimeIdentifiers Include="@(_TrimmableNativeAotRuntimeIdentifiers)" /> - <_TrimmableNativeAotSiblingRuntimeIdentifiers Remove="$(RuntimeIdentifier)" /> - - <_TrimmableNativeAotScanDgmlCandidates Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" /> - <_TrimmableNativeAotScanDgmlCandidates - Include="@(_TrimmableNativeAotRuntimeIdentifiers->'$(IntermediateOutputPath)%(Identity)\native\$(TargetName).scan.dgml.xml')" /> - <_TrimmableNativeAotScanDgmlCandidates - Include="@(_TrimmableNativeAotSiblingRuntimeIdentifiers->'$(IntermediateOutputPath)..\%(Identity)\native\$(TargetName).scan.dgml.xml')" - Condition=" '$(RuntimeIdentifier)' != '' " /> - <_TrimmableNativeAotScanDgmlCandidates Remove="@(_TrimmableNativeAotScanDgmlCandidates)" - Condition=" !Exists('%(_TrimmableNativeAotScanDgmlCandidates.Identity)') " /> - <_TrimmableNativeAotDgmlFiles Include="@(_TrimmableNativeAotScanDgmlCandidates)" /> - - <_TrimmableNativeAotCodegenDgmlCandidates Include="$(NativeIntermediateOutputPath)$(TargetName).codegen.dgml.xml" /> - <_TrimmableNativeAotCodegenDgmlCandidates - Include="@(_TrimmableNativeAotRuntimeIdentifiers->'$(IntermediateOutputPath)%(Identity)\native\$(TargetName).codegen.dgml.xml')" /> - <_TrimmableNativeAotCodegenDgmlCandidates - Include="@(_TrimmableNativeAotSiblingRuntimeIdentifiers->'$(IntermediateOutputPath)..\%(Identity)\native\$(TargetName).codegen.dgml.xml')" - Condition=" '$(RuntimeIdentifier)' != '' " /> - <_TrimmableNativeAotCodegenDgmlCandidates Remove="@(_TrimmableNativeAotCodegenDgmlCandidates)" - Condition=" !Exists('%(_TrimmableNativeAotCodegenDgmlCandidates.Identity)') " /> - <_TrimmableNativeAotCodegenDgmlFiles Include="@(_TrimmableNativeAotCodegenDgmlCandidates)" /> - <_TrimmableNativeAotDgmlFiles Include="@(_TrimmableNativeAotCodegenDgmlFiles)" - Condition=" '@(_TrimmableNativeAotDgmlFiles->Count())' == '0' " /> - - - - - - - - - - - - - - - - - - diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 13986a46813..20cffe34eb9 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1941,33 +1941,6 @@ public static string XA4318 { } } - /// - /// Looks up a localized string similar to No NativeAOT DGML files were provided.. - /// - public static string XA4319 { - get { - return ResourceManager.GetString("XA4319", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ACW map file '{0}' was not found.. - /// - public static string XA4320 { - get { - return ResourceManager.GetString("XA4320", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to NativeAOT DGML file '{0}' was not found.. - /// - public static string XA4321 { - get { - return ResourceManager.GetString("XA4321", resourceCulture); - } - } - /// /// Looks up a localized string similar to Skipping library ProGuard configuration file '{1}' (from {2}) because it contains the unsupported global option '{0}'. Global ProGuard options are only allowed in application projects.. /// @@ -2031,6 +2004,42 @@ public static string XA4326 { } } + /// + /// Looks up a localized string similar to Could not extract Java type map keys from '{0}': {1}. + /// + public static string XA4327 { + get { + return ResourceManager.GetString("XA4327", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not generate typemap ProGuard configuration from '{0}': {1}. + /// + public static string XA4328 { + get { + return ResourceManager.GetString("XA4328", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No retained Java type map key files were provided.. + /// + public static string XA4328_NoInputs { + get { + return ResourceManager.GetString("XA4328_NoInputs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Line {0} contains an invalid retained Java class name: '{1}'.. + /// + public static string XA4328_InvalidName { + get { + return ResourceManager.GetString("XA4328_InvalidName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Missing Android NDK toolchains directory '{0}'. Please install the Android NDK.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index c85742d1345..11f002a791e 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -853,20 +853,6 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins {0} - The path to the file. {1} - The exception message. - - - No NativeAOT DGML files were provided. - The following are literal names and should not be translated: NativeAOT, DGML - - - ACW map file '{0}' was not found. - The following are literal names and should not be translated: ACW -{0} - The path to the ACW map file. - - - NativeAOT DGML file '{0}' was not found. - The following are literal names and should not be translated: NativeAOT, DGML -{0} - The path to the NativeAOT DGML file. Skipping library ProGuard configuration file '{1}' (from {2}) because it contains the unsupported global option '{0}'. Global ProGuard options are only allowed in application projects. @@ -901,6 +887,24 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous JNIEnv.FindClass source. The following are literal API names and should not be translated: JNI, JNIEnv.FindClass. + + Could not extract Java type map keys from '{0}': {1} + {0} - The input file path or representation name. +{1} - The underlying parser or input/output failure. It is not localized. + + + Could not generate typemap ProGuard configuration from '{0}': {1} + {0} - The input or output file path. +{1} - The underlying failure, or a formatted XA4328_* resource. + + + No retained Java type map key files were provided. + + + Line {0} contains an invalid retained Java class name: '{1}'. + {0} - The one-based line number. +{1} - The invalid input record. + Missing Android NDK toolchains directory '{0}'. Please install the Android NDK. {0} - The path of the missing directory diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg deleted file mode 100644 index 8c53ede5e9c..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg +++ /dev/null @@ -1,21 +0,0 @@ -# Xamarin.Android NativeAOT trimmable typemap configuration. - --keep class net.dot.jni.** { *; (...); } -# Loaded by name from managed code. --keep class net.dot.android.ApplicationRegistration { *; } --keep class net.dot.android.crypto.** { *; (...); } -# NativeAOT resolves these interface methods through JNI during startup. --keep class mono.android.IGCUserPeer { *; } -# Native hosts resolve these package-private fields by name during startup. --keepclassmembers class mono.android.Runtime { - static java.lang.Class *; -} - --keepclassmembers class * extends android.view.View { - *** set*(...); -} - --keepclassmembers class * extends android.view.View { - (android.content.Context,android.util.AttributeSet); - (android.content.Context,android.util.AttributeSet,int); -} diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_typemap.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_typemap.cfg new file mode 100644 index 00000000000..a8c120c0947 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Resources/proguard_typemap.cfg @@ -0,0 +1,38 @@ +# Class-only shrinking for the retained typemap pipeline. JNI member liveness +# is deliberately independent of the generated class roots. +-keepclassmembers class * { + *; +} + +# Java.Interop resolves these support classes by name. +-keep class net.dot.jni.ManagedPeer +-keep interface net.dot.jni.GCUserPeerable +-keep class net.dot.jni.internal.JavaProxyObject +-keep class net.dot.jni.internal.JavaProxyThrowable +# Manifest providers and their runtime bootstrap entry points. +-keep class net.dot.jni.nativeaot.NativeAotRuntimeProvider +-keep class net.dot.jni.nativeaot.JavaInteropRuntime +-keep class net.dot.jni.nativeaot.NativeAotEnvironmentVars +-keep class mono.MonoRuntimeProvider* +-keep class mono.MonoPackageManager +-keep class mono.MonoPackageManager_Resources +# Native hosts resolve Runtime and its GC peer interfaces by name. +-keep class mono.android.Runtime +-keep interface mono.android.IGCUserPeer +-keep class mono.android.GCUserPeer +# Loaded by name from managed code. +-keep class net.dot.android.ApplicationRegistration { *; } +-keep class net.dot.android.crypto.** { *; (...); } +# Native hosts resolve these package-private fields by name during startup. +-keepclassmembers class mono.android.Runtime { + static java.lang.Class *; +} + +-keepclassmembers class * extends android.view.View { + *** set*(...); +} + +-keepclassmembers class * extends android.view.View { + (android.content.Context,android.util.AttributeSet); + (android.content.Context,android.util.AttributeSet,int); +} diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_typemap_coreclr.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_typemap_coreclr.cfg new file mode 100644 index 00000000000..b9a0067a2ac --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Resources/proguard_typemap_coreclr.cfg @@ -0,0 +1,25 @@ +# Members of JNI-facing types are preserved by proguard_typemap_members.cfg. +# Java-only dependencies remain eligible for member shrinking. + +# Java.Interop resolves these support classes and their members by name. +-keep class net.dot.jni.ManagedPeer { *; } +-keep interface net.dot.jni.GCUserPeerable { *; } +-keep class net.dot.jni.internal.JavaProxyObject { *; } +-keep class net.dot.jni.internal.JavaProxyThrowable { *; } + +# Manifest providers and native runtime bootstrap entry points. +-keep class mono.MonoRuntimeProvider* { *; } +-keep class mono.MonoPackageManager { *; } +-keep class mono.MonoPackageManager_Resources { *; } +-keep class mono.android.Runtime { *; } +-keep interface mono.android.IGCUserPeer { *; } +-keep class mono.android.GCUserPeer { *; } +-keep class net.dot.android.ApplicationRegistration { *; } +-keep class net.dot.android.crypto.** { *; (...); } + +# Framework-driven view inflation and property setters. +-keepclassmembers class * extends android.view.View { + *** set*(...); + (android.content.Context,android.util.AttributeSet); + (android.content.Context,android.util.AttributeSet,int); +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs deleted file mode 100644 index b8369374be9..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs +++ /dev/null @@ -1,139 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.IO; -using System.Xml; -using Microsoft.Build.Framework; -using Microsoft.Android.Build.Tasks; - -namespace Xamarin.Android.Tasks; - -public class GenerateNativeAotProguardConfiguration : AndroidTask -{ - const string TypeMetadataPrefix = "Type metadata: ["; - - public override string TaskPrefix => "GNAPC"; - - public ITaskItem [] NativeAotDgmlFiles { get; set; } = []; - - [Required] - public string AcwMapFile { get; set; } = ""; - - [Required] - public string OutputFile { 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 - // this avoids generating and processing the very large ILC dependency graph. - public bool TrimJavaCallableWrappers { get; set; } = true; - - public override bool RunTask () - { - var dir = Path.GetDirectoryName (OutputFile); - if (!dir.IsNullOrEmpty () && !Directory.Exists (dir)) { - Directory.CreateDirectory (dir); - } - - if (!File.Exists (AcwMapFile)) { - Log.LogCodedError ("XA4320", Properties.Resources.XA4320, AcwMapFile); - return !Log.HasLoggedErrors; - } - - HashSet? retainedTypeKeys = null; - if (TrimJavaCallableWrappers) { - if (NativeAotDgmlFiles.Length == 0) { - Log.LogCodedError ("XA4319", Properties.Resources.XA4319); - return !Log.HasLoggedErrors; - } - foreach (var dgmlFile in NativeAotDgmlFiles) { - if (!File.Exists (dgmlFile.ItemSpec)) { - Log.LogCodedError ("XA4321", Properties.Resources.XA4321, dgmlFile.ItemSpec); - return !Log.HasLoggedErrors; - } - } - retainedTypeKeys = LoadRetainedTypeKeysFromDgml (); - } - - // A null retainedTypeKeys means "keep every Java type in the ACW map" (Java trimming disabled). - var javaTypes = LoadJavaTypesFromAcwMap (retainedTypeKeys); - - using var writer = new StringWriter (); - writer.WriteLine ("# ACWs retained by NativeAOT ILC"); - foreach (var javaTypeName in javaTypes) { - writer.WriteLine ($"-keep class {javaTypeName} {{ *; }}"); - } - 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); - } else { - Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT ProGuard rules keeping every Java type in the ACW map (Java trimming is disabled).", javaTypes.Count); - } - return !Log.HasLoggedErrors; - } - - List LoadJavaTypesFromAcwMap (HashSet? retainedTypeKeys) - { - var javaTypes = new List (retainedTypeKeys?.Count ?? 0); - var seenJavaTypes = new HashSet (StringComparer.Ordinal); - foreach (var line in File.ReadLines (AcwMapFile)) { - var separator = line.IndexOf (";", StringComparison.Ordinal); - if (separator <= 0 || separator == line.Length - 1) { - continue; - } - var managedTypeName = line.Substring (0, separator); - var javaTypeName = line.Substring (separator + 1); - if ((retainedTypeKeys == null || retainedTypeKeys.Contains (managedTypeName)) && seenJavaTypes.Add (javaTypeName)) { - javaTypes.Add (javaTypeName); - } - } - return javaTypes; - } - - HashSet LoadRetainedTypeKeysFromDgml () - { - var typeKeys = new HashSet (StringComparer.Ordinal); - foreach (var dgmlFile in NativeAotDgmlFiles) { - using var reader = XmlReader.Create (dgmlFile.ItemSpec, new XmlReaderSettings { - DtdProcessing = DtdProcessing.Prohibit, - XmlResolver = null, - }); - - bool readingNodes = false; - while (reader.Read ()) { - if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "Nodes") { - readingNodes = true; - continue; - } - if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == "Nodes") { - break; - } - if (!readingNodes) { - continue; - } - if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "Node") { - continue; - } - - var label = reader.GetAttribute ("Label"); - if (label.IsNullOrEmpty () || !label.StartsWith (TypeMetadataPrefix, StringComparison.Ordinal)) { - continue; - } - - var assemblyStart = TypeMetadataPrefix.Length; - var assemblyEnd = label.IndexOf (']', assemblyStart); - if (assemblyEnd < 0 || assemblyEnd == label.Length - 1) { - continue; - } - - var assemblyName = label.Substring (assemblyStart, assemblyEnd - assemblyStart); - var managedTypeName = label.Substring (assemblyEnd + 1); - typeKeys.Add ($"{managedTypeName}, {assemblyName}"); - } - } - - return typeKeys; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index ad89bebeb29..b8614604b81 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -36,7 +36,8 @@ public class R8 : D8 public string? ProguardMappingFileOutput { get; set; } public string? BuildMetadataFileOutput { get; set; } public ITaskItem []? ProguardConfigurationFiles { get; set; } - public bool UseTrimmableNativeAotProguardConfiguration { get; set; } + public bool UseTypeMapProguardConfiguration { get; set; } + public bool UseScopedTypeMapMembers { get; set; } public string ObfuscationMode { get; set; } = "private-members"; // User-authored AndroidJavaSource (Bind != true) .java files. These have no managed peer and are @@ -161,18 +162,21 @@ protected override string CreateResponseFile () } if (EnableShrinking) { - if (UseTrimmableNativeAotProguardConfiguration && !ProguardGeneratedApplicationConfiguration.IsNullOrEmpty ()) { - // ACW keep rules come from the DGML/acw-map-driven proguard_project_references.cfg on - // the trimmable path. User-authored AndroidJavaSource (Bind != true) has no managed peer + if (UseTypeMapProguardConfiguration) { + WriteArg (response, "--no-minification"); + } + if (UseTypeMapProguardConfiguration && !ProguardGeneratedApplicationConfiguration.IsNullOrEmpty ()) { + // Class roots come from retained typemap keys, not the complete ACW map. + // User-authored AndroidJavaSource (Bind != true) has no managed peer // and is absent from that map, so keep it here explicitly; otherwise R8 shrinks it away // (e.g. dropping large unreferenced sources so an app that needs multidex no longer does). using (var appcfg = File.CreateText (ProguardGeneratedApplicationConfiguration)) { - appcfg.WriteLine ("# ACW keep rules are generated from NativeAOT ILC metadata."); + appcfg.WriteLine ("# Class keep rules are generated from retained typemap keys."); foreach (var java in GetUserJavaTypes ()) { appcfg.WriteLine ($"-keep class {java} {{ *; }}"); } } - } else if (!AcwMapFile.IsNullOrEmpty ()) { + } else if (!UseTypeMapProguardConfiguration && !AcwMapFile.IsNullOrEmpty ()) { var acwMap = MonoAndroidHelper.LoadMapFile (BuildEngine4, Path.GetFullPath (AcwMapFile), StringComparer.OrdinalIgnoreCase); var javaTypes = new List (acwMap.Values.Count); foreach (var v in acwMap.Values) { @@ -192,11 +196,11 @@ protected override string CreateResponseFile () } if (!ProguardCommonXamarinConfiguration.IsNullOrWhiteSpace ()) { using (var xamcfg = File.CreateText (ProguardCommonXamarinConfiguration)) { - WriteObfuscationRules (xamcfg, ObfuscationMode); + WriteObfuscationRules (xamcfg, UseTypeMapProguardConfiguration ? "disabled" : ObfuscationMode); xamcfg.WriteLine (); xamcfg.Flush (); - if (UseTrimmableNativeAotProguardConfiguration) { - using var stream = GetEmbeddedResourceStream ("proguard_trimmable_nativeaot.cfg"); + if (UseTypeMapProguardConfiguration) { + using var stream = GetEmbeddedResourceStream (UseScopedTypeMapMembers ? "proguard_typemap_coreclr.cfg" : "proguard_typemap.cfg"); stream.CopyTo (xamcfg.BaseStream); } else { using var stream = GetEmbeddedResourceStream ("proguard_xamarin.cfg"); @@ -344,7 +348,7 @@ static string DescribeProguardSource (ITaskItem item) Stream GetEmbeddedResourceStream (string resourceName) { - var stream = GetType ().Assembly.GetManifestResourceStream (resourceName); + var stream = typeof (R8).Assembly.GetManifestResourceStream (resourceName); if (stream == null) { throw new InvalidOperationException ($"Missing embedded resource '{resourceName}'."); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 4f52bf67ff0..e170f8d9638 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -1635,8 +1635,8 @@ public void NativeAotKeepsRuntimeAcwJavaTypesUnderR8 () var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex"); FileAssert.Exists (dexFile); - // Regression test: the trimmable NativeAOT path generates its ACW keep rules from the - // ILC DGML into proguard_project_references.cfg. If that file is not passed to R8, R8 + // Regression test: NativeAOT must pass its generated ACW keep rules in + // proguard_project_references.cfg to R8. Otherwise R8 // tree-shakes the runtime ACW/JCW classes out of classes.dex and the app crashes at // startup inside JavaInteropRuntime.init with a ClassNotFoundException for the // UncaughtExceptionMarshaler Java Callable Wrapper. The JCW class name is CRC-hashed diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs deleted file mode 100644 index fd9679e8c27..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.IO; -using Microsoft.Build.Utilities; -using NUnit.Framework; -using Xamarin.Android.Tasks; - -namespace Xamarin.Android.Build.Tests; - -[TestFixture] -[Parallelizable (ParallelScope.Children)] -public class GenerateNativeAotProguardConfigurationTests : BaseTest -{ - [Test] - public void Execute_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 outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); - Directory.CreateDirectory (path); - File.WriteAllText (dgmlFile, """ - - - - - - - - - - - - - - """); - File.WriteAllText (acwMapFile, """ - UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity - Android.App.Activity, Mono.Android;android.app.Activity - Duplicate.Type, My.Assembly;my.app.Duplicate - AndroidX.Activity.Result.Contract.ActivityResultContracts+TakePicture, Xamarin.AndroidX.Activity;androidx.activity.result.contract.ActivityResultContracts$TakePicture - Duplicate.Type;wrong.Duplicate - Other.Type;other.Type - """); - - var task = new GenerateNativeAotProguardConfiguration { - BuildEngine = new MockBuildEngine (TestContext.Out), - NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, - AcwMapFile = acwMapFile, - OutputFile = outputFile, - 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.DoesNotContain ("other.Type", proguard); - } - - [Test] - public void Execute_KeepsAllWhenTrimmingDisabled () - { - var path = Path.Combine (Root, "temp", TestName); - var acwMapFile = Path.Combine (path, "acw-map.txt"); - var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); - Directory.CreateDirectory (path); - File.WriteAllText (acwMapFile, """ - UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity - Android.App.Activity, Mono.Android;android.app.Activity - Duplicate.Type, My.Assembly;my.app.Duplicate - Other.Type;other.Type - """); - - var task = new GenerateNativeAotProguardConfiguration { - BuildEngine = new MockBuildEngine (TestContext.Out), - AcwMapFile = acwMapFile, - OutputFile = outputFile, - TrimJavaCallableWrappers = false, - }; - - Assert.IsTrue (task.Execute (), "Task should succeed without a DGML when trimming is disabled."); - 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 other.Type { *; }", proguard); - } - - [Test] - public void Execute_IgnoresDgmlWhenTrimmingDisabled () - { - 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 outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); - Directory.CreateDirectory (path); - File.WriteAllText (dgmlFile, """ - - - - - - - - """); - File.WriteAllText (acwMapFile, """ - UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity - Android.App.Activity, Mono.Android;android.app.Activity - Other.Type;other.Type - """); - - var task = new GenerateNativeAotProguardConfiguration { - BuildEngine = new MockBuildEngine (TestContext.Out), - NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, - AcwMapFile = acwMapFile, - OutputFile = outputFile, - TrimJavaCallableWrappers = false, - }; - - Assert.IsTrue (task.Execute (), "Task should succeed and ignore the DGML when trimming is disabled."); - var proguard = File.ReadAllText (outputFile); - StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); - StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); - StringAssert.Contains ("-keep class other.Type { *; }", proguard); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs index 1362ba24aed..93fe2fadb9a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -1,4 +1,5 @@ using System.IO; +using Microsoft.Build.Utilities; using NUnit.Framework; using Xamarin.Android.Tasks; @@ -77,5 +78,61 @@ public void WriteDisabledObfuscationRules () Assert.AreEqual ("-dontobfuscate" + System.Environment.NewLine, writer.ToString ()); } + + [TestCase (false)] + [TestCase (true)] + public void RetainedTypeMapRulesDoNotRootAllAcwsOrObfuscatePrivateMembers (bool scopedMembers) + { + var directory = Path.Combine (Path.GetTempPath (), "R8TypeMap_" + System.Guid.NewGuid ().ToString ("N")); + Directory.CreateDirectory (directory); + try { + var map = Path.Combine (directory, "acw-map.txt"); + File.WriteAllText (map, "Unused.Type;unused.Wrapper\n"); + var source = Path.Combine (directory, "UserSource.java"); + File.WriteAllText (source, "package example;\npublic class UserSource {}"); + var task = new R8ResponseTestTask { + BuildEngine = new MockBuildEngine (TestContext.Out), + UseTypeMapProguardConfiguration = true, + UseScopedTypeMapMembers = scopedMembers, + EnableShrinking = true, + ObfuscationMode = "private-members", + AcwMapFile = map, + JavaSourceFiles = [new TaskItem (source)], + JavaPlatformJarPath = Path.Combine (directory, "android.jar"), + ProguardGeneratedApplicationConfiguration = Path.Combine (directory, "primary.cfg"), + ProguardCommonXamarinConfiguration = Path.Combine (directory, "common.cfg"), + ResponseFile = Path.Combine (directory, "r8.rsp"), + }; + var response = task.WriteResponse (); + StringAssert.Contains ("--no-minification", response); + StringAssert.DoesNotContain ("--no-tree-shaking", response); + var primary = File.ReadAllText (task.ProguardGeneratedApplicationConfiguration); + StringAssert.DoesNotContain ("unused.Wrapper", primary); + StringAssert.Contains ("-keep class example.UserSource { *; }", primary); + var common = File.ReadAllText (task.ProguardCommonXamarinConfiguration); + StringAssert.Contains ("-dontobfuscate", common); + StringAssert.DoesNotContain ("-keep,allowshrinking,allowoptimization class **", common); + StringAssert.DoesNotContain ("-keep class mono.android.**", common); + if (scopedMembers) { + StringAssert.DoesNotContain ("-keepclassmembers class * {", common); + StringAssert.Contains ("-keep class mono.android.Runtime { *; }", common); + StringAssert.Contains ("-keep class net.dot.jni.ManagedPeer { *; }", common); + StringAssert.Contains ("-keep interface mono.android.IGCUserPeer { *; }", common); + } else { + StringAssert.Contains ("-keepclassmembers class * {", common); + } + } finally { + Directory.Delete (directory, recursive: true); + } + } + + sealed class R8ResponseTestTask : R8 + { + public string ResponseFile { get; set; } = ""; + + protected override string CreateResponseFilePath () => ResponseFile; + + public string WriteResponse () => File.ReadAllText (CreateResponseFile ()); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 38fd53867a1..2d5bb4fd882 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -19,6 +19,79 @@ namespace Xamarin.Android.Build.Tests { [Category ("Node-2")] public class TrimmableTypeMapBuildTests : BaseTest { + [TestCase (AndroidRuntime.CoreCLR, "llvm-ir")] + [TestCase (AndroidRuntime.CoreCLR, "trimmable")] + [TestCase (AndroidRuntime.NativeAOT, "trimmable")] + public void RetainedTypeMapClassRulesDriveR8 (AndroidRuntime runtime, string implementation) + { + if (IgnoreUnsupportedConfiguration (runtime, release: true)) { + return; + } + var proj = new XamarinAndroidApplicationProject { IsRelease = true }; + proj.SetRuntime (runtime); + proj.SetProperty (KnownProperties.RuntimeIdentifier, "android-arm64"); + proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); + proj.SetProperty ("AndroidTypeMapImplementation", implementation); + proj.SetProperty ("TrimMode", "full"); + proj.SetProperty ("AndroidR8ObfuscationMode", "private-members"); + proj.SetProperty ("_AndroidEnableTypemapR8Trimming", "true"); + if (runtime == AndroidRuntime.NativeAOT) { + proj.SetProperty ("_SkipNdkResolution", "false"); + } + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj)); + + var intermediate = builder.Output.GetIntermediaryPath ("android-arm64"); + var keysFile = Path.Combine (intermediate, "typemap.keys.txt"); + var keys = File.ReadAllLines (keysFile); + var live = keys.Single (key => key.EndsWith ("/MainActivity", StringComparison.Ordinal)); + const string dead = "mono/android/animation/Animator_AnimatorListenerImplementor"; + CollectionAssert.DoesNotContain (keys, dead); + var configuration = Path.Combine (intermediate, "proguard", "proguard_project_references.cfg"); + CollectionAssert.AreEqual ( + keys.Select (key => "-keep class " + key.Replace ('/', '.')).OrderBy (line => line, StringComparer.Ordinal), + File.ReadAllLines (configuration)); + var common = File.ReadAllText (Path.Combine (intermediate, "proguard", "proguard_xamarin.cfg")); + StringAssert.Contains ("-dontobfuscate", common); + var members = Path.Combine (intermediate, "proguard", "proguard_typemap_members.cfg"); + if (runtime == AndroidRuntime.CoreCLR) { + StringAssert.DoesNotContain ("-keepclassmembers class * {", common); + CollectionAssert.AreEqual ( + keys.Select (key => "-keepclassmembers class " + key.Replace ('/', '.') + " { *; }").OrderBy (line => line, StringComparer.Ordinal), + File.ReadAllLines (members)); + } else { + StringAssert.Contains ("-keepclassmembers class * {", common); + FileAssert.DoesNotExist (members); + } + StringAssert.DoesNotContain ("-keep class mono.android.**", common); + StringAssert.DoesNotContain ("-keep class net.dot.jni.**", common); + var dex = Path.Combine (intermediate, "android", "bin", "classes.dex"); + Assert.IsTrue (DexUtils.ContainsClass ($"L{live};", dex, AndroidSdkPath), "Live class names must not be obfuscated."); + Assert.IsFalse (DexUtils.ContainsClass ($"L{dead};", dex, AndroidSdkPath), "Dead wrappers must not be rooted by runtime package rules."); + using (var metadata = JsonDocument.Parse (File.ReadAllText (Path.Combine (intermediate, "r8.json")))) { + var options = metadata.RootElement.GetProperty ("options"); + Assert.IsTrue (options.GetProperty ("isShrinkingEnabled").GetBoolean ()); + Assert.IsFalse (options.GetProperty ("isObfuscationEnabled").GetBoolean ()); + Assert.AreEqual (runtime == AndroidRuntime.CoreCLR, options.GetProperty ("isOptimizationsEnabled").GetBoolean ()); + } + + Assert.IsTrue (builder.Build (proj)); + builder.Output.AssertTargetIsSkipped ("_AndroidExtractTypeMapKeys"); + builder.Output.AssertTargetIsSkipped ("_AndroidGenerateTypeMapProguardConfiguration"); + if (runtime == AndroidRuntime.CoreCLR) { + builder.Output.AssertTargetIsSkipped ("_AndroidGenerateTypeMapMemberProguardConfiguration"); + File.Delete (members); + Assert.IsTrue (builder.Build (proj)); + builder.Output.AssertTargetIsNotSkipped ("_AndroidGenerateTypeMapMemberProguardConfiguration"); + builder.Output.AssertTargetIsNotSkipped ("_CompileToDalvik"); + FileAssert.Exists (members); + } + File.Delete (configuration); + Assert.IsTrue (builder.Build (proj)); + builder.Output.AssertTargetIsNotSkipped ("_AndroidGenerateTypeMapProguardConfiguration"); + FileAssert.Exists (configuration); + } + [Test] public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) { @@ -1482,6 +1555,10 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease proj.MainActivity = proj.DefaultMainActivity; proj.SetRuntime (runtime); proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + proj.SetProperty ("_AndroidEnableTypemapR8Trimming", "true"); + if (runtime == AndroidRuntime.NativeAOT) { + proj.SetProperty ("_SkipNdkResolution", "false"); + } bool trimNativeAotJavaCode = isRelease && runtime == AndroidRuntime.NativeAOT; using var builder = CreateApkBuilder (); @@ -1496,15 +1573,17 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease var typemapFingerprintContent = File.ReadAllText (typemapFingerprints); var typemapWriteTimes = typemapDlls.ToDictionary (path => path, File.GetLastWriteTimeUtc); - string scanDgml = ""; - DateTime scanDgmlTimestamp = default; + string nativeObject = ""; + DateTime nativeObjectTimestamp = default; if (trimNativeAotJavaCode) { var ridIntermediateDir = builder.Output.GetIntermediaryPath ("android-arm64"); - scanDgml = Path.Combine (ridIntermediateDir, "native", $"{proj.ProjectName}.scan.dgml.xml"); + nativeObject = Path.Combine (ridIntermediateDir, "native", $"{proj.ProjectName}.o"); + var scanDgml = Path.Combine (ridIntermediateDir, "native", $"{proj.ProjectName}.scan.dgml.xml"); var codegenDgml = Path.Combine (ridIntermediateDir, "native", $"{proj.ProjectName}.codegen.dgml.xml"); - FileAssert.Exists (scanDgml); - FileAssert.DoesNotExist (codegenDgml, "Optimized builds should emit only the scan DGML needed for Java trimming."); - scanDgmlTimestamp = File.GetLastWriteTimeUtc (scanDgml); + FileAssert.Exists (nativeObject); + FileAssert.DoesNotExist (scanDgml, "Typemap extraction should not request a scan DGML."); + FileAssert.DoesNotExist (codegenDgml, "Typemap extraction should not request a codegen DGML."); + nativeObjectTimestamp = File.GetLastWriteTimeUtc (nativeObject); } Assert.IsTrue (builder.Build (proj), "Second build should have succeeded."); @@ -1513,8 +1592,9 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease builder.Output.IsTargetSkipped ("_GenerateJavaStubs"), "_GenerateJavaStubs should be skipped on incremental build."); if (trimNativeAotJavaCode) { - builder.Output.AssertTargetIsSkipped ("_GenerateTrimmableTypeMapProguardConfiguration"); - Assert.AreEqual (scanDgmlTimestamp, File.GetLastWriteTimeUtc (scanDgml), "No-op builds should not rewrite the scan DGML."); + builder.Output.AssertTargetIsSkipped ("_AndroidExtractTypeMapKeys"); + builder.Output.AssertTargetIsSkipped ("_AndroidGenerateTypeMapProguardConfiguration"); + Assert.AreEqual (nativeObjectTimestamp, File.GetLastWriteTimeUtc (nativeObject), "No-op builds should not rewrite the ILC object."); } if (isRelease && runtime == AndroidRuntime.CoreCLR) { builder.Output.AssertTargetIsSkipped ("_RemoveRegisterAttributeCoreClr"); @@ -2914,10 +2994,22 @@ void AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (string dexFile, stri Assert.IsFalse (acwMap.Contains (deadManagedType, StringComparison.Ordinal), $"{acwMapPath} should be based on linked assemblies."); Assert.IsFalse (acwMap.Contains (deadJavaDotName, StringComparison.Ordinal), $"{acwMapPath} should not keep removed framework listener implementors."); - FileAssert.Exists (proguardPrimaryPath, "R8 should generate a primary proguard configuration from the post-trim acw-map."); + FileAssert.Exists (proguardPrimaryPath, "R8 should generate a separate configuration for user Java sources."); Assert.IsFalse ( File.ReadAllText (proguardPrimaryPath).Contains (deadJavaDotName, StringComparison.Ordinal), $"{proguardPrimaryPath} should not keep removed framework listener implementors."); + var proguardDirectory = Path.GetDirectoryName (proguardPrimaryPath); + Assert.IsNotNull (proguardDirectory); + var references = Path.Combine (proguardDirectory, "proguard_project_references.cfg"); + var rules = File.ReadAllLines (references); + Assert.IsNotEmpty (rules, "Retained typemap keys should provide class roots."); + Assert.IsTrue (rules.All (line => line.StartsWith ("-keep class ", StringComparison.Ordinal) && !line.Contains ('{')), + "Typemap ProGuard rules must contain only class roots, not member rules."); + Assert.IsFalse (rules.Any (line => line.Contains (deadJavaDotName, StringComparison.Ordinal)), + "Retained typemap class roots should exclude trimmed framework implementors."); + var common = File.ReadAllText (Path.Combine (proguardDirectory, "proguard_xamarin.cfg")); + StringAssert.Contains ("-dontobfuscate", common); + StringAssert.DoesNotContain ("-keep,allowshrinking,allowoptimization class **", common); FileAssert.Exists (dexFile, "R8 should produce classes.dex."); Assert.IsFalse ( diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs index d88603519bb..55d6b36cd06 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs @@ -638,11 +638,11 @@ protected bool IgnoreUnsupportedConfiguration (AndroidRuntime runtime, bool aot // NativeAOT trims with ILC and does not emit illink's `obj///linked/` output. // Tests that inspect the `linked/` directory (e.g. to verify trimming or type-map behavior) // therefore cannot run as-is on NativeAOT. - // TODO: add DGML-based counterparts to verify these behaviors on NativeAOT (follow-up issue). + // NativeAOT typemap retention is covered by native object extraction tests. protected bool IgnoreNativeAotLinkedAssemblyChecks (AndroidRuntime runtime) { if (runtime == AndroidRuntime.NativeAOT) { - Assert.Ignore ("NativeAOT does not produce illink's `linked/` output; skipping `linked/` assembly inspection (DGML counterpart tracked as a follow-up)."); + Assert.Ignore ("NativeAOT produces ILC native objects rather than illink's `linked/` assemblies; skipping linked assembly inspection."); return true; } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index a70c32a55d5..f015bc5295c 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1002,6 +1002,11 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="ProjectFullPath=$(MSBuildProjectFullPath)" /> <_PropertyCacheItems Include="AndroidUseDesignerAssembly=$(AndroidUseDesignerAssembly)" /> <_PropertyCacheItems Include="AndroidTypeMapImplementation=$(AndroidTypeMapImplementation)" /> + <_PropertyCacheItems Include="_AndroidRuntime=$(_AndroidRuntime)" /> + <_PropertyCacheItems Include="PublishTrimmed=$(PublishTrimmed)" /> + <_PropertyCacheItems Include="Optimize=$(Optimize)" /> + <_PropertyCacheItems Include="_AndroidEnableTypemapR8Trimming=$(_AndroidEnableTypemapR8Trimming)" /> + <_PropertyCacheItems Include="ProguardConfigFiles=$(ProguardConfigFiles)" /> <_PropertyCacheItems Include="_AndroidUseMarshalMethods=$(_AndroidUseMarshalMethods)" /> <_PropertyCacheItems Include="_AndroidJcwCodegenTarget=$(_AndroidJcwCodegenTarget)" /> <_PropertyCacheItems Include="_AndroidAssemblyStoreCompressionLevel=$(_AndroidAssemblyStoreCompressionLevel)" /> @@ -2026,8 +2031,8 @@ because xbuild doesn't support framework reference assemblies. <_ProguardConfiguration Include="$(ProguardConfigFiles)" /> - <_ProguardConfiguration Include="$(MSBuildThisFileDirectory)proguard-android.txt" Condition=" '$(AndroidR8ObfuscationMode)' == 'disabled' " /> - <_ProguardConfiguration Include="$(MSBuildThisFileDirectory)proguard-android-optimize.txt" Condition=" '$(AndroidR8ObfuscationMode)' != 'disabled' " /> + <_ProguardConfiguration Include="$(MSBuildThisFileDirectory)proguard-android.txt" Condition=" ('$(AndroidR8ObfuscationMode)' == 'disabled' or '$(_AndroidUseTypeMapProguardConfiguration)' == 'true') and !('$(_AndroidUseTypeMapProguardConfiguration)' == 'true' and '$(_AndroidRuntime)' == 'CoreCLR') " /> + <_ProguardConfiguration Include="$(MSBuildThisFileDirectory)proguard-android-optimize.txt" Condition=" ('$(AndroidR8ObfuscationMode)' != 'disabled' and '$(_AndroidUseTypeMapProguardConfiguration)' != 'true') or ('$(_AndroidUseTypeMapProguardConfiguration)' == 'true' and '$(_AndroidRuntime)' == 'CoreCLR') " /> <_ProguardConfiguration Include="$(IntermediateOutputPath)proguard\proguard_xamarin.cfg" Condition=" '$(AndroidLinkTool)' != '' " /> <_ProguardConfiguration Include="$(_ProguardProjectConfiguration)" Condition=" '$(AndroidLinkTool)' != '' " /> <_ProguardConfiguration Include="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg" Condition=" '$(AndroidLinkTool)' != '' " /> @@ -3050,6 +3055,7 @@ because xbuild doesn't support framework reference assemblies. <_ProjectAndroidManifest>$(ProjectDir)$(AndroidManifest) <_NdkRequired Condition="'$(EnableLLVM)' == 'True'">true <_NdkRequired Condition="'$(PublishAot)' == 'true' and '$(_AndroidUseWorkloadNativeLinker)' != 'true'">true + <_NdkRequired Condition=" '$(_AndroidRuntime)' == 'NativeAOT' and '$(AndroidTypeMapImplementation)' == 'trimmable' and '$(_AndroidEnableTypemapR8Trimming)' == 'true' and '$(PublishTrimmed)' == 'true' and '$(AndroidLinkTool)' == 'r8' ">true <_NdkRequired Condition="'$(_NdkRequired)' == ''">false @@ -3110,5 +3116,6 @@ because xbuild doesn't support framework reference assemblies. Condition=" '$(AndroidTypeMapImplementation)' == 'trimmable' " /> + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index 4c4efd1f8b7..6701a613540 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -19,7 +19,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. @@ -80,7 +80,8 @@ Copyright (C) 2018 Xamarin. All rights reserved. ProguardMappingFileOutput="$(AndroidProguardMappingFile)" BuildMetadataFileOutput="$(_AndroidR8BuildMetadataFile)" ProguardConfigurationFiles="@(_ProguardConfiguration)" - UseTrimmableNativeAotProguardConfiguration="$(_UseTrimmableNativeAotProguardConfiguration)" + UseTypeMapProguardConfiguration="$(_AndroidUseTypeMapProguardConfiguration)" + UseScopedTypeMapMembers="$(_AndroidUseScopedTypeMapMembers)" ObfuscationMode="$(AndroidR8ObfuscationMode)" EnableShrinking="$(_R8EnableShrinking)" EnableMultiDex="$(AndroidEnableMultiDex)" diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/rooting.dgml.xml b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/rooting.dgml.xml deleted file mode 100644 index 336f4cd924f..00000000000 --- a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/rooting.dgml.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionRootingTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionRootingTests.cs deleted file mode 100644 index f94e19bbec4..00000000000 --- a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionRootingTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.IO; -using System.Linq; -using System.Xml.Linq; - -using NUnit.Framework; -using Xamarin.ProjectTools; - -namespace Xamarin.Android.Build.Tests; - -[TestFixture] -public class InterfaceCollectionRootingTests : BaseTest -{ - static readonly XNamespace DgmlNamespace = "http://schemas.microsoft.com/vs/2009/dgml"; - - static string GraphPath => Path.Combine ( - XABuildPaths.TopDirectory, "tests", "MSBuildDeviceIntegration", "Resources", "InterfaceCollectionApp", "rooting.dgml.xml"); - - [Test] - public void CanonicalWrapperRootingGraph () - { - var path = TestContext.Parameters.Get ("InterfaceCollectionRootingGraph", GraphPath); - InterfaceCollectionTests.AssertCanonicalWrapperRooting (path); - } - - [TestCase ("list")] - [TestCase ("collection")] - [TestCase ("dictionary")] - public void DirectFactoryRooting (string wrapper) - { - var graph = XDocument.Load (GraphPath); - UseDirectFactoryRoot (graph, wrapper); - AssertGraph (graph); - } - - [TestCase ("dictionary-factory", "conditional factory primary dependency")] - [TestCase ("peer-metadata", "conditional factory metadata dependency")] - public void RejectsMissingConditionalInput (string source, string message) - { - var graph = XDocument.Load (GraphPath); - FindLink (graph, source, "dictionary-conditional").Remove (); - Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains (message)); - } - - [TestCase ("dictionary-factory", "conditional factory primary dependency")] - [TestCase ("peer-metadata", "conditional factory metadata dependency")] - public void RejectsWrongConditionalInput (string source, string message) - { - var graph = XDocument.Load (GraphPath); - FindLink (graph, source, "dictionary-conditional").SetAttributeValue ("Source", "collection-factory"); - Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains (message)); - } - - [TestCase (false)] - [TestCase (true)] - public void RejectsMissingAllocation (bool conditional) - { - var graph = XDocument.Load (GraphPath); - if (!conditional) { - UseDirectFactoryRoot (graph, "dictionary"); - } - FindLink (graph, conditional ? "dictionary-conditional" : "dictionary-factory", "dictionary-type").Remove (); - Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains ("newobj dependency was not found")); - } - - [TestCase (false)] - [TestCase (true)] - public void RejectsUnexpectedRoot (bool conditional) - { - var graph = XDocument.Load (GraphPath); - if (!conditional) { - UseDirectFactoryRoot (graph, "dictionary"); - } - var unexpectedLink = new XElement (FindLink ( - graph, conditional ? "dictionary-conditional" : "dictionary-factory", "dictionary-type")); - unexpectedLink.SetAttributeValue ("Source", "collection-factory"); - graph.Descendants (DgmlNamespace + "Links").Single ().Add (unexpectedLink); - Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains ("unexpected incoming dependency")); - } - - [TestCase ("dictionary-factory")] - [TestCase ("dictionary-conditional")] - public void RejectsAmbiguousFactoryNodes (string nodeId) - { - var graph = XDocument.Load (GraphPath); - var duplicate = new XElement (FindNode (graph, nodeId)); - duplicate.SetAttributeValue ("Id", "duplicate"); - graph.Descendants (DgmlNamespace + "Nodes").Single ().Add (duplicate); - Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains ("ambiguous node matches")); - } - - static void UseDirectFactoryRoot (XDocument graph, string wrapper) - { - FindLink (graph, $"{wrapper}-conditional", $"{wrapper}-type").SetAttributeValue ("Source", $"{wrapper}-factory"); - FindLink (graph, $"{wrapper}-factory", $"{wrapper}-conditional").Remove (); - FindLink (graph, "peer-metadata", $"{wrapper}-conditional").Remove (); - FindNode (graph, $"{wrapper}-conditional").Remove (); - } - - static XElement FindNode (XDocument graph, string id) - { - return graph.Descendants (DgmlNamespace + "Node").Single (node => node.Attribute ("Id")?.Value == id); - } - - static XElement FindLink (XDocument graph, string source, string target) - { - return graph.Descendants (DgmlNamespace + "Link").Single ( - link => link.Attribute ("Source")?.Value == source && link.Attribute ("Target")?.Value == target); - } - - static void AssertGraph (XDocument graph) - { - var path = Path.GetTempFileName (); - try { - graph.Save (path); - InterfaceCollectionTests.AssertCanonicalWrapperRooting (path); - } finally { - File.Delete (path); - } - } -} diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs index f609348f10e..cd7d56cf496 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; using System.Text; -using System.Xml; using NUnit.Framework; @@ -16,7 +14,6 @@ namespace Xamarin.Android.Build.Tests [Category ("UsesDevice")] public class InterfaceCollectionTests : DeviceTest { - const string DgmlNamespace = "http://schemas.microsoft.com/vs/2009/dgml"; const string ResultPrefix = "INTERFACE_COLLECTION_ROOTING_RESULT"; [Test] @@ -66,12 +63,6 @@ public void InterfaceCollectionFactoryRootsCanonicalWrappers () }, logcatPath, ActivityStartTimeoutInSeconds, onMonitoringStarted: () => StartActivityAndAssert (proj)); Assert.IsTrue (resultFound, $"The focused app did not report a result. See '{logcatPath}'."); StringAssert.Contains ($"{ResultPrefix} PASS {resultToken}", resultLine); - - var projectDirectory = Path.Combine (Root, builder.ProjectDirectory); - var dgmlFiles = Directory.GetFiles (projectDirectory, $"{proj.ProjectName}.scan.dgml.xml", SearchOption.AllDirectories); - Assert.AreEqual (1, dgmlFiles.Length, "The focused NativeAOT app should produce one scan dependency graph."); - AssertCanonicalWrapperRooting (dgmlFiles [0]); - TestContext.Out.WriteLine ($"Focused NativeAOT dependency graph: {dgmlFiles [0]}"); } finally { RunAdbCommand ($"uninstall {proj.PackageName}"); } @@ -100,133 +91,6 @@ void AssertGeneratedBindingsAreIsolated (ProjectBuilder builder, XamarinAndroidA "The raw JNI holder and concrete peers must not produce managed bindings that can root closed collection wrappers."); } - internal static void AssertCanonicalWrapperRooting (string dgmlFile) - { - var chains = new [] { - new RootingChain ( - "JavaList", - "Mono_Android_Java_Interop_SafeJavaCollectionFactory__CreateReferenceListFromJniHandle", - "Mono_Android_Android_Runtime_JavaList_1 constructed", - "__GenericDict_Mono_Android_Android_Runtime_JavaList_1", - "(__GenericDict_Mono_Android_Android_Runtime_JavaList_1, " + - "Mono_Android_Android_Runtime_JavaList_1___ctor_0)", - "Mono_Android_Android_Runtime_JavaList_1___ctor_0", - "JavaList`1..ctor(native int,JniHandleOwnership)"), - new RootingChain ( - "JavaCollection", - "Mono_Android_Java_Interop_SafeJavaCollectionFactory__CreateReferenceCollectionFromJniHandle", - "Mono_Android_Android_Runtime_JavaCollection_1 constructed", - "__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1", - "(__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1, " + - "Mono_Android_Android_Runtime_JavaCollection_1___ctor)", - "Mono_Android_Android_Runtime_JavaCollection_1___ctor", - "JavaCollection`1..ctor(native int,JniHandleOwnership)"), - new RootingChain ( - "JavaDictionary", - "Mono_Android_Java_Interop_SafeJavaCollectionFactory__CreateReferenceDictionaryFromJniHandle", - "Mono_Android_Android_Runtime_JavaDictionary_2 constructed", - "__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2", - "(__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2, " + - "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0)", - "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0", - "JavaDictionary`2..ctor(native int,JniHandleOwnership)"), - }; - var duplicateNodeIds = new List (); - var missingNodeIds = new List (); - var nodeIds = new HashSet (StringComparer.Ordinal); - var unexpectedCanonicalRoots = new List (); - - using (var reader = CreateDgmlReader (dgmlFile)) { - while (reader.Read ()) { - if (reader.NodeType != XmlNodeType.Element || - reader.LocalName != "Node" || - reader.NamespaceURI != DgmlNamespace) { - continue; - } - var id = reader.GetAttribute ("Id") ?? ""; - var label = reader.GetAttribute ("Label") ?? ""; - if (id.Length == 0) { - missingNodeIds.Add (label); - } else if (!nodeIds.Add (id)) { - duplicateNodeIds.Add ($"Id=\"{id}\" Label=\"{label}\""); - } - foreach (var chain in chains) { - chain.ObserveNode (id, label); - } - if (IsUnexpectedCanonicalReferenceConstructor (label)) { - unexpectedCanonicalRoots.Add (label); - } - } - } - - using (var reader = CreateDgmlReader (dgmlFile)) { - while (reader.Read ()) { - if (reader.NodeType != XmlNodeType.Element || - reader.LocalName != "Link" || - reader.NamespaceURI != DgmlNamespace) { - continue; - } - var source = reader.GetAttribute ("Source") ?? ""; - var target = reader.GetAttribute ("Target") ?? ""; - var reason = reader.GetAttribute ("Reason") ?? ""; - foreach (var chain in chains) { - chain.ObserveLink (source, target, reason); - } - } - } - - Assert.IsEmpty (missingNodeIds, "The NativeAOT dependency graph contained nodes without IDs."); - Assert.IsEmpty (duplicateNodeIds, "The NativeAOT dependency graph contained duplicate node IDs."); - Assert.IsEmpty ( - unexpectedCanonicalRoots, - "Only SafeJavaCollectionFactory's IJavaPeerable instantiations should root the reference-wrapper canonical constructors."); - foreach (var chain in chains) { - chain.AssertComplete (); - TestContext.Out.WriteLine ($"{chain.Name} canonical constructor rooted through SafeJavaCollectionFactory."); - } - } - - static XmlReader CreateDgmlReader (string dgmlFile) - { - return XmlReader.Create (dgmlFile, new XmlReaderSettings { - DtdProcessing = DtdProcessing.Prohibit, - XmlResolver = null, - }); - } - - static bool IsUnexpectedCanonicalReferenceConstructor (string label) - { - if (!label.Contains ("..ctor(native int,JniHandleOwnership) backed by ", StringComparison.Ordinal)) { - return false; - } - bool usesReferenceCanonicalCode = - label.Contains ("JavaList_1___ctor_0", StringComparison.Ordinal) || - label.Contains ("JavaCollection_1___ctor", StringComparison.Ordinal) || - label.Contains ("JavaDictionary_2___ctor_0", StringComparison.Ordinal); - if (!usesReferenceCanonicalCode) { - return false; - } - bool isExpectedRoot = - label == "[Mono.Android]Android.Runtime.JavaList`1..ctor(native int,JniHandleOwnership) " + - "backed by Mono_Android_Android_Runtime_JavaList_1___ctor_0" || - label == "[Mono.Android]Android.Runtime.JavaList`1..ctor(native int,JniHandleOwnership) " + - "backed by Mono_Android_Android_Runtime_JavaList_1___ctor_0" || - label == "[Mono.Android]Android.Runtime.JavaCollection`1..ctor(native int,JniHandleOwnership) " + - "backed by Mono_Android_Android_Runtime_JavaCollection_1___ctor" || - label == "[Mono.Android]Android.Runtime.JavaCollection`1..ctor(native int,JniHandleOwnership) " + - "backed by Mono_Android_Android_Runtime_JavaCollection_1___ctor" || - label == "[Mono.Android]Android.Runtime.JavaDictionary`2" + - "..ctor(native int,JniHandleOwnership) backed by " + - "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0" || - label == "[Mono.Android]Android.Runtime.JavaDictionary`2" + - "..ctor(native int,JniHandleOwnership) backed by " + - "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0"; - return !isExpectedRoot; - } - static string ReadFixture (string fileName) { return File.ReadAllText ( @@ -244,257 +108,5 @@ static string ReadRuntimeFixture (string fileName) return File.ReadAllText ( Path.Combine (XABuildPaths.TopDirectory, "tests", "Mono.Android-Tests", "Mono.Android-Tests", fileName)); } - - sealed class RootingChain - { - const string ReferenceTypeMetadata = "Type metadata: [Java.Interop]Java.Interop.IJavaPeerable"; - - readonly string constructorPattern; - readonly string canonicalConstructorPattern; - readonly string constructedTypePattern; - readonly string genericDictionaryPattern; - readonly string genericDictionaryDependencyPattern; - readonly string sourcePattern; - readonly string conditionalSourcePattern; - readonly List ambiguousNodeMatches = new (); - readonly HashSet observedNodeRoles = new (StringComparer.Ordinal); - readonly List unexpectedIncomingLinks = new (); - - string canonicalConstructorId = ""; - string constructedTypeId = ""; - string constructorId = ""; - string genericDictionaryId = ""; - string genericDictionaryDependencyId = ""; - string sourceId = ""; - string conditionalSourceId = ""; - string referenceTypeMetadataId = ""; - bool canonicalConstructorToDependency; - bool constructedTypeToGenericDictionary; - bool genericDictionaryToDependency; - bool genericDictionaryToConstructor; - bool sourceToConstructedType; - bool conditionalSourceToConstructedType; - bool sourceToConditionalSource; - bool metadataToConditionalSource; - - public RootingChain ( - string name, - string sourcePattern, - string constructedTypePattern, - string genericDictionaryPattern, - string genericDictionaryDependencyPattern, - string canonicalConstructorPattern, - string constructorPattern) - { - Name = name; - this.sourcePattern = sourcePattern; - conditionalSourcePattern = $"({sourcePattern}, {ReferenceTypeMetadata})"; - this.constructedTypePattern = constructedTypePattern; - this.genericDictionaryPattern = genericDictionaryPattern; - this.genericDictionaryDependencyPattern = genericDictionaryDependencyPattern; - this.canonicalConstructorPattern = canonicalConstructorPattern; - this.constructorPattern = constructorPattern; - } - - public string Name { get; } - - public void ObserveNode (string id, string label) - { - int matchedRoles = 0; - matchedRoles += ObserveNode ( - label == sourcePattern, - id, - label, - "SafeJavaCollectionFactory source", - ref sourceId) ? 1 : 0; - matchedRoles += ObserveNode ( - label == conditionalSourcePattern, - id, - label, - "conditional factory dependency", - ref conditionalSourceId) ? 1 : 0; - matchedRoles += ObserveNode ( - label == ReferenceTypeMetadata, - id, - label, - "IJavaPeerable type metadata", - ref referenceTypeMetadataId) ? 1 : 0; - matchedRoles += ObserveNode ( - IsConstructedTypeLabel (label, constructedTypePattern), - id, - label, - "IJavaPeerable constructed type", - ref constructedTypeId) ? 1 : 0; - matchedRoles += ObserveNode ( - label == genericDictionaryPattern, - id, - label, - "IJavaPeerable generic dictionary", - ref genericDictionaryId) ? 1 : 0; - matchedRoles += ObserveNode ( - label == genericDictionaryDependencyPattern, - id, - label, - "IJavaPeerable constructor dictionary dependency", - ref genericDictionaryDependencyId) ? 1 : 0; - matchedRoles += ObserveNode ( - label == canonicalConstructorPattern, - id, - label, - "canonical compiled constructor", - ref canonicalConstructorId) ? 1 : 0; - matchedRoles += ObserveNode ( - label == $"[Mono.Android]Android.Runtime.{constructorPattern} backed by {canonicalConstructorPattern}", - id, - label, - "IJavaPeerable activation constructor", - ref constructorId) ? 1 : 0; - if (matchedRoles > 1) { - ambiguousNodeMatches.Add ($"multiple roles: Id=\"{id}\" Label=\"{label}\""); - } - } - - public void ObserveLink (string source, string target, string reason) - { - sourceToConstructedType |= IsLink (source, target, reason, sourceId, constructedTypeId, "newobj"); - // ILC may represent the type guard as a conditional dependency instead of a direct newobj edge. - conditionalSourceToConstructedType |= IsLink (source, target, reason, conditionalSourceId, constructedTypeId, "newobj"); - sourceToConditionalSource |= IsLink (source, target, reason, sourceId, conditionalSourceId, "Primary"); - metadataToConditionalSource |= IsLink (source, target, reason, referenceTypeMetadataId, conditionalSourceId, "Secondary"); - constructedTypeToGenericDictionary |= IsLink (source, target, reason, constructedTypeId, genericDictionaryId, "reloc"); - genericDictionaryToDependency |= IsLink ( - source, - target, - reason, - genericDictionaryId, - genericDictionaryDependencyId, - "Primary"); - canonicalConstructorToDependency |= IsLink ( - source, - target, - reason, - canonicalConstructorId, - genericDictionaryDependencyId, - "Secondary"); - genericDictionaryToConstructor |= IsLink ( - source, - target, - reason, - genericDictionaryDependencyId, - constructorId, - "Generic dictionary dependency"); - - RejectUnexpectedIncoming (source, target, reason, constructedTypeId, sourceId, "newobj", conditionalSourceId, "newobj"); - RejectUnexpectedIncoming ( - source, target, reason, conditionalSourceId, sourceId, "Primary", referenceTypeMetadataId, "Secondary"); - RejectUnexpectedIncoming (source, target, reason, genericDictionaryId, constructedTypeId, "reloc"); - RejectUnexpectedIncoming ( - source, target, reason, genericDictionaryDependencyId, genericDictionaryId, "Primary", canonicalConstructorId, "Secondary"); - RejectUnexpectedIncoming ( - source, - target, - reason, - constructorId, - genericDictionaryDependencyId, - "Generic dictionary dependency"); - } - - public void AssertComplete () - { - Assert.IsEmpty (ambiguousNodeMatches, $"{Name} canonical constructor path had ambiguous node matches."); - Assert.IsNotEmpty (sourceId, $"{Name} SafeJavaCollectionFactory source node was not found."); - Assert.IsNotEmpty (constructedTypeId, $"{Name} IJavaPeerable constructed-type node was not found."); - Assert.IsNotEmpty (genericDictionaryId, $"{Name} IJavaPeerable generic dictionary node was not found."); - Assert.IsNotEmpty (genericDictionaryDependencyId, $"{Name} IJavaPeerable constructor dictionary dependency was not found."); - Assert.IsNotEmpty (canonicalConstructorId, $"{Name} canonical compiled constructor node was not found."); - Assert.IsNotEmpty (constructorId, $"{Name} IJavaPeerable activation constructor node was not found."); - Assert.IsTrue ( - sourceToConstructedType || conditionalSourceToConstructedType, - $"{Name} SafeJavaCollectionFactory newobj dependency was not found."); - if (conditionalSourceToConstructedType) { - Assert.IsTrue (sourceToConditionalSource, $"{Name} conditional factory primary dependency was not found."); - Assert.IsTrue (metadataToConditionalSource, $"{Name} conditional factory metadata dependency was not found."); - } - Assert.IsTrue (constructedTypeToGenericDictionary, $"{Name} constructed-type relocation dependency was not found."); - Assert.IsTrue (genericDictionaryToDependency, $"{Name} generic dictionary primary dependency was not found."); - Assert.IsTrue (canonicalConstructorToDependency, $"{Name} canonical constructor secondary dependency was not found."); - Assert.IsTrue (genericDictionaryToConstructor, $"{Name} generic dictionary constructor dependency was not found."); - Assert.IsEmpty (unexpectedIncomingLinks, $"{Name} canonical constructor path had an unexpected incoming dependency."); - } - - bool ObserveNode (bool matches, string id, string label, string role, ref string observedId) - { - if (!matches) { - return false; - } - if (!observedNodeRoles.Add (role)) { - ambiguousNodeMatches.Add ($"{role}: Id=\"{id}\" Label=\"{label}\""); - return true; - } - observedId = id; - return true; - } - - void RejectUnexpectedIncoming ( - string source, - string target, - string reason, - string expectedTarget, - string expectedSource, - string expectedReason, - string alternativeSource = "", - string alternativeReason = "") - { - if (IsIncomingLink (target, expectedTarget) && - !IsLink (source, target, reason, expectedSource, expectedTarget, expectedReason) && - !IsLink (source, target, reason, alternativeSource, expectedTarget, alternativeReason)) { - unexpectedIncomingLinks.Add (FormatLink (source, target, reason)); - } - } - - static bool IsIncomingLink (string actualTarget, string expectedTarget) - { - return expectedTarget.Length > 0 && actualTarget == expectedTarget; - } - - static bool IsLink ( - string actualSource, - string actualTarget, - string actualReason, - string expectedSource, - string expectedTarget, - string expectedReason) - { - return expectedSource.Length > 0 && - expectedTarget.Length > 0 && - actualSource == expectedSource && - actualTarget == expectedTarget && - actualReason == expectedReason; - } - - static string FormatLink (string source, string target, string reason) - { - return $"Source=\"{source}\" Target=\"{target}\" Reason=\"{reason}\""; - } - - static bool IsConstructedTypeLabel (string label, string constructedTypePattern) - { - if (!label.EndsWith (constructedTypePattern, StringComparison.Ordinal)) { - return false; - } - - int prefixLength = label.Length - constructedTypePattern.Length; - if (prefixLength <= "_ZTV".Length || - !label.StartsWith ("_ZTV", StringComparison.Ordinal)) { - return false; - } - for (int i = "_ZTV".Length; i < prefixLength; i++) { - if (label [i] < '0' || label [i] > '9') { - return false; - } - } - return true; - } - } } } diff --git a/tests/MSBuildDeviceIntegration/Tests/ScopedTypeMapR8Tests.cs b/tests/MSBuildDeviceIntegration/Tests/ScopedTypeMapR8Tests.cs new file mode 100644 index 00000000000..b53ca8f00c1 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/ScopedTypeMapR8Tests.cs @@ -0,0 +1,129 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests; + +[TestFixture] +[Category ("UsesDevice")] +public class ScopedTypeMapR8Tests : DeviceTest +{ + [TestCase ("llvm-ir")] + [TestCase ("trimmable")] + public void PreservesJniMembersAndShrinksJavaOnlyDependencies (string implementation) + { + var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (AndroidRuntime.CoreCLR, "scopedr8_" + implementation.Replace ("-", ""))) { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetRuntimeIdentifiers ([DeviceAbi]); + proj.SetProperty ("AndroidTypeMapImplementation", implementation); + proj.SetProperty ("AndroidLinkTool", "r8"); + proj.SetProperty ("AndroidR8ObfuscationMode", "disabled"); + proj.SetProperty ("_AndroidEnableTypemapR8Trimming", "true"); + proj.SetProperty ("TrimMode", "full"); + proj.SetProperty ("AndroidSdkDirectory", AndroidSdkResolver.GetAndroidSdkPath ()); + proj.SetProperty ("JavaSdkDirectory", AndroidSdkResolver.GetJavaSdkPath ()); + proj.SetDefaultTargetDevice (); + proj.AndroidJavaSources.Add (JavaSource ("ScopedBase.java", """ + package example; + public class ScopedBase { + public int inheritedField = 11; + private int privateField = 13; + public static int staticField = 17; + public int inheritedMethod() { return 19; } + protected int protectedMethod() { return 23; } + } + """)); + proj.AndroidJavaSources.Add (JavaSource ("ScopedContract.java", """ + package example; + public interface ScopedContract { + default int defaultMethod() { return 29; } + } + """)); + proj.AndroidJavaSources.Add (JavaSource ("ScopedPeer.java", """ + package example; + public class ScopedPeer extends ScopedBase implements ScopedContract { + public ScopedPeer() {} + public int keptMethod() { return Helper.used(); } + public int jniOnly() { return 3; } + } + class Helper { + static int used() { return 7; } + static int unused() { return UnusedDependency.value(); } + } + class UnusedDependency { + static int value() { return 42; } + } + """)); + var marker = "R8_SCOPED_MEMBERS_PASS " + Guid.NewGuid ().ToString ("N"); + proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", """ + using var peer = new Example.ScopedPeer (); + Require (peer.KeptMethod (), 7); + Require (((Example.IScopedContract) peer).DefaultMethod (), 29); + var klass = Android.Runtime.JNIEnv.GetObjectClass (peer.Handle); + try { + Require (Invoke ("jniOnly"), 3); + Require (Invoke ("inheritedMethod"), 19); + Require (Invoke ("protectedMethod"), 23); + Require (Invoke ("defaultMethod"), 29); + Require (ReadField ("inheritedField"), 11); + Require (ReadField ("privateField"), 13); + var field = Android.Runtime.JNIEnv.GetStaticFieldID (klass, "staticField", "I"); + Require (Android.Runtime.JNIEnv.GetStaticIntField (klass, field), 17); + Console.WriteLine ("${MARKER}"); + } finally { + Android.Runtime.JNIEnv.DeleteLocalRef (klass); + } + + int Invoke (string name) { + var method = Android.Runtime.JNIEnv.GetMethodID (klass, name, "()I"); + return Android.Runtime.JNIEnv.CallIntMethod (peer.Handle, method); + } + int ReadField (string name) { + var field = Android.Runtime.JNIEnv.GetFieldID (klass, name, "I"); + return Android.Runtime.JNIEnv.GetIntField (peer.Handle, field); + } + static void Require (int actual, int expected) { + if (actual != expected) { + throw new InvalidOperationException ($"JNI result {actual}, expected {expected}."); + } + } + """.Replace ("${MARKER}", marker, StringComparison.Ordinal)); + + using var builder = CreateApkBuilder (); + bool installed = false; + try { + installed = builder.Install (proj); + Assert.IsTrue (installed, "The scoped-retention app should install."); + var projectDirectory = Path.Combine (Root, builder.ProjectDirectory); + var dexFiles = Directory.GetFiles (Path.Combine (projectDirectory, proj.IntermediateOutputPath), "classes*.dex", SearchOption.AllDirectories); + Assert.IsNotEmpty (dexFiles); + Assert.IsTrue (dexFiles.Any (dex => DexUtils.ContainsClassWithMethod ("Lexample/ScopedPeer;", "keptMethod", "()I", dex, AndroidSdkPath)), + "The JNI entry point must survive, even when R8 inlines its Java-only helper."); + Assert.IsFalse (dexFiles.Any (dex => DexUtils.ContainsClassWithMethod ("Lexample/Helper;", "unused", "()I", dex, AndroidSdkPath)), + "Preserving JNI-facing peers must not keep unused members of Java-only dependencies."); + Assert.IsFalse (dexFiles.Any (dex => DexUtils.ContainsClass ("Lexample/UnusedDependency;", dex, AndroidSdkPath))); + Assert.IsTrue (MonitorAdbLogcat (line => line.Contains (marker, StringComparison.Ordinal), + Path.Combine (projectDirectory, "scoped-members-logcat.log"), ActivityStartTimeoutInSeconds, + onMonitoringStarted: () => StartActivityAndAssert (proj)), + "The app must successfully invoke preserved JNI methods and fields, including inherited members."); + } finally { + if (installed) { + RunAdbCommand ($"uninstall {proj.PackageName}"); + } + } + } + + static AndroidItem.AndroidJavaSource JavaSource (string name, string source) => new (name) { + Encoding = Encoding.ASCII, + TextContent = () => source, + Metadata = { + { "Bind", "True" }, + }, + }; +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromAssembliesTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromAssembliesTests.cs new file mode 100644 index 00000000000..73d262b8e23 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromAssembliesTests.cs @@ -0,0 +1,452 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Android.Tasks; +using Xamarin.Android.Tasks; +using Xunit; +using TaskItem = Microsoft.Build.Utilities.TaskItem; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ExtractTypeMapKeysFromAssembliesTests : IDisposable +{ + static readonly Version RuntimeVersion = new (11, 0, 0, 0); + readonly string directory = Path.Combine (Path.GetTempPath (), nameof (ExtractTypeMapKeysFromAssembliesTests), Guid.NewGuid ().ToString ("N")); + + public ExtractTypeMapKeysFromAssembliesTests () => Directory.CreateDirectory (directory); + + public void Dispose () => Directory.Delete (directory, recursive: true); + + static TypeMapAttributeData Entry (string key, string? target = null) => new () { + MapKey = key, + ProxyTypeReference = "System.Object, System.Runtime", + TargetTypeReference = target, + }; + + string Emit (string name, params TypeMapAttributeData [] entries) + { + string path = Path.Combine (directory, name + ".dll"); + Directory.CreateDirectory (Path.GetDirectoryName (path) ?? throw new InvalidOperationException ()); + var model = new TypeMapAssemblyData { + AssemblyName = Path.GetFileName (name), + ModuleName = Path.GetFileName (path), + }; + model.Entries.AddRange (entries); + using var stream = File.Create (path); + new TypeMapAssemblyEmitter (RuntimeVersion).Emit (model, stream); + return path; + } + + (ExtractTypeMapKeysFromAssemblies task, TypeMapTaskBuildEngine engine) CreateTask (params string [] inputs) + { + var engine = new TypeMapTaskBuildEngine (); + return (new ExtractTypeMapKeysFromAssemblies { + BuildEngine = engine, + LinkedAssemblies = inputs.Select (p => new TaskItem (p)).ToArray (), + OutputFile = Path.Combine (directory, "output", "keys.txt"), + }, engine); + } + + [Fact] + public void UnionsAllAssembliesAndRidsWithExactCanonicalEncoding () + { + string arm64 = Emit ("android-arm64/_Bindings.TypeMap", + Entry ("test/Zebra"), Entry ("test/Outer$Inner"), Entry ("test/Alias[0]"), Entry ("test/\u00e9clair")); + string x64 = Emit ("android-x64/_Bindings.TypeMap", + Entry ("test/Alpha", "System.String, System.Runtime"), Entry ("test/Outer$Inner"), Entry ("test/Alias[1]")); + var (task, _) = CreateTask (arm64, x64, arm64); + + Assert.True (task.Execute ()); + byte [] expected = new UTF8Encoding (false).GetBytes ("test/Alias\ntest/Alpha\ntest/Outer$Inner\ntest/Zebra\ntest/\u00e9clair\n"); + Assert.Equal (expected, File.ReadAllBytes (task.OutputFile)); + + File.SetLastWriteTimeUtc (task.OutputFile, new DateTime (2000, 1, 1)); + task.LinkedAssemblies = task.LinkedAssemblies.Reverse ().ToArray (); + Assert.True (task.Execute ()); + Assert.Equal (expected, File.ReadAllBytes (task.OutputFile)); + Assert.True (File.GetLastWriteTimeUtc (task.OutputFile).Year > 2000); + } + + [Theory] + [InlineData ("test/A[123]", "test/A")] + [InlineData ("test/A[000]", "test/A")] + [InlineData ("test/A[9999999999999999999999999999999]", "test/A")] + [InlineData ("test/Outer$Inner[2]", "test/Outer$Inner")] + [InlineData ("test/\u2160[1]", "test/\u2160")] + public void NormalizesOnlyFinalNonemptyAsciiDecimalAlias (string key, string expected) + { + var (task, _) = CreateTask (Emit ("Map", Entry (key))); + Assert.True (task.Execute ()); + Assert.Equal (expected + "\n", File.ReadAllText (task.OutputFile)); + } + + [Theory] + [InlineData ("[Ljava/lang/Object;", "java/lang/Object\n")] + [InlineData ("[[Ltest/Outer$Inner;", "test/Outer$Inner\n")] + [InlineData ("[Ltest/Outer$Inner;[000]", "test/Outer$Inner\n")] + [InlineData ("[B", "")] + [InlineData ("[[Z", "")] + [InlineData ("[I[0]", "")] + public void ArrayEntriesKeepOnlyTheirObjectElementClass (string key, string expected) + { + var (task, engine) = CreateTask (Emit ("Map", Entry (key))); + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal (new UTF8Encoding (false).GetBytes (expected), File.ReadAllBytes (task.OutputFile)); + } + + [Fact] + public void EmptyStubsAndOrdinaryManagedAssembliesContributeNoKeys () + { + string stub = EmitStub ("Stub"); + string fixtures = Path.Combine (AppContext.BaseDirectory, "TestFixtures.dll"); + var (task, _) = CreateTask (stub, fixtures); + Assert.True (task.Execute ()); + Assert.Empty (File.ReadAllBytes (task.OutputFile)); + + task.LinkedAssemblies = [new TaskItem (Emit ("Map", Entry ("test/Present")))]; + Assert.True (task.Execute ()); + task.LinkedAssemblies = [new TaskItem (stub)]; + Assert.True (task.Execute ()); + Assert.Empty (File.ReadAllBytes (task.OutputFile)); + } + + [Fact] + public void MissingAssemblyInputsFail () + { + var (task, engine) = CreateTask (); + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, e => e.Code == "XA4327"); + Assert.False (File.Exists (task.OutputFile)); + } + + [Fact] + public void IgnoresAliasArraysAssociationsAssemblyTargetsAndUnrelatedAttributes () + { + var model = new TypeMapAssemblyData { AssemblyName = "Map", ModuleName = "Map.dll" }; + model.Entries.Add (Entry ("test/Retained")); + model.AliasHolders.Add (new AliasHolderData { + Namespace = "_TypeMap.Aliases", TypeName = "StaleAliases", AliasKeys = ["test/Removed[0]", "test/Removed[1]"], + }); + model.Associations.Add (new TypeMapAssociationData { + SourceTypeReference = "Unrelated.Source, Unrelated", + AliasProxyTypeReference = "Unrelated.Proxy, Unrelated", + }); + string map = Path.Combine (directory, "Map.dll"); + using (var stream = File.Create (map)) { + new TypeMapAssemblyEmitter (RuntimeVersion).Emit (model, stream); + } + string root = Path.Combine (directory, "Root.dll"); + using (var stream = File.Create (root)) { + new RootTypeMapAssemblyGenerator (RuntimeVersion).Generate (["Map"], useSharedTypemapUniverse: false, stream); + } + var (task, _) = CreateTask (root, map); + Assert.True (task.Execute ()); + Assert.Equal ("test/Retained\n", File.ReadAllText (task.OutputFile)); + } + + [Theory] + [InlineData ("missing")] + [InlineData ("malformed")] + [InlineData ("zero-byte")] + [InlineData ("directory")] + public void BadRequiredAssemblyFailsWithoutWritingOutput (string kind) + { + string path = Path.Combine (directory, "bad.dll"); + if (kind == "malformed") { + File.WriteAllBytes (path, [1, 2, 3]); + } else if (kind == "zero-byte") { + File.WriteAllBytes (path, []); + } else if (kind == "directory") { + Directory.CreateDirectory (path); + } + var (task, engine) = CreateTask (Emit ("Valid", Entry ("test/Valid")), path); + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, e => e.Code == "XA4327" && e.Message != null && e.Message.Contains (path, StringComparison.Ordinal)); + Assert.False (File.Exists (task.OutputFile)); + } + + [Theory] + [InlineData ("")] + [InlineData ("test/Invalid\nName")] + [InlineData ("test/Invalid\rName")] + [InlineData ("test/Invalid\0Name")] + [InlineData ("test/A[]")] + [InlineData ("test/A[-1]")] + [InlineData ("test/A[1x]")] + [InlineData ("test/A[1]Extra")] + [InlineData ("test/A[1")] + [InlineData ("test/A[")] + [InlineData ("[0]")] + [InlineData ("test/A[\u0661]")] + [InlineData ("test/A[1][2]")] + [InlineData ("test.Invalid")] + [InlineData ("test/*")] + [InlineData ("test/")] + [InlineData ("/test")] + [InlineData ("test//Invalid")] + [InlineData ("test/Invalid\u200bName")] + [InlineData ("[L;")] + [InlineData ("[V")] + [InlineData ("[Ltest/A")] + [InlineData ("[[")] + public void InvalidKeyFails (string key) + { + var (task, engine) = CreateTask (Emit ("Map", Entry (key))); + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, e => e.Code == "XA4327"); + } + + [Fact] + public void NetmoduleIsNotAValidAssemblyInput () + { + var metadata = new MetadataBuilder (); + metadata.AddModule (0, metadata.GetOrAddString ("Invalid.netmodule"), metadata.GetOrAddGuid (Guid.NewGuid ()), default, default); + metadata.AddTypeDefinition (default, default, metadata.GetOrAddString (""), default, + MetadataTokens.FieldDefinitionHandle (1), MetadataTokens.MethodDefinitionHandle (1)); + var image = new BlobBuilder (); + new ManagedPEBuilder (new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), + new MetadataRootBuilder (metadata), new BlobBuilder ()).Serialize (image); + string path = Path.Combine (directory, "Invalid.netmodule"); + using (var stream = File.Create (path)) { + image.WriteContentTo (stream); + } + var (task, engine) = CreateTask (path); + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, e => e.Code == "XA4327"); + } + + [Fact] + public void OutputWriteFailureIsReported () + { + var (task, engine) = CreateTask (Emit ("Map", Entry ("test/Valid"))); + task.OutputFile = directory; + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, e => e.Code == "XA4327"); + } + + [Theory] + [InlineData ("prolog")] + [InlineData ("null-key")] + [InlineData ("null-type")] + [InlineData ("missing-type")] + [InlineData ("named-arguments")] + [InlineData ("trailing-bytes")] + [InlineData ("argument-count")] + [InlineData ("key-type")] + [InlineData ("type-parameter")] + [InlineData ("static-method")] + [InlineData ("method-name")] + public void MalformedTypeMapAttributeFails (string defect) + { + var (task, engine) = CreateTask (EmitAttribute (defect)); + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, e => e.Code == "XA4327"); + Assert.False (File.Exists (task.OutputFile)); + } + + [Theory] + [InlineData ("type-attribute")] + [InlineData ("other-namespace")] + public void OnlyAssemblyTypeMapAttributesAreEntries (string kind) + { + var (task, _) = CreateTask (EmitAttribute (kind)); + Assert.True (task.Execute ()); + Assert.Empty (File.ReadAllBytes (task.OutputFile)); + } + + string EmitAttribute (string kind) + { + var pe = new PEAssemblyBuilder (RuntimeVersion); + pe.EmitPreamble ("Attributes", "Attributes.dll"); + var metadata = pe.Metadata; + var systemType = metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); + var openAttribute = metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef, + metadata.GetOrAddString (kind == "other-namespace" ? "Unrelated" : "System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAttribute`1")); + var attributeType = pe.MakeGenericTypeSpec (openAttribute, systemType); + var constructor = pe.AddMemberRef (attributeType, kind == "method-name" ? "NotAConstructor" : ".ctor", s => + s.MethodSignature (isInstanceMethod: kind != "static-method").Parameters (kind == "argument-count" ? 1 : 2, + r => r.Void (), p => { + if (kind == "key-type") { + p.AddParameter ().Type ().Int32 (); + } else { + p.AddParameter ().Type ().String (); + } + if (kind != "argument-count") { + if (kind == "type-parameter") { + p.AddParameter ().Type ().String (); + } else { + p.AddParameter ().Type ().Type (systemType, isValueType: false); + } + } + })); + var value = new BlobBuilder (); + value.WriteUInt16 ((ushort) (kind == "prolog" ? 2 : 1)); + value.WriteSerializedString (kind == "null-key" ? null : "test/Key"); + if (kind != "missing-type") { + value.WriteSerializedString (kind == "null-type" ? null : "System.Object, System.Runtime"); + } + value.WriteUInt16 ((ushort) (kind == "named-arguments" ? 1 : 0)); + if (kind == "trailing-bytes") { + value.WriteByte (42); + } + EntityHandle parent = EntityHandle.AssemblyDefinition; + if (kind == "type-attribute") { + parent = metadata.AddTypeDefinition (TypeAttributes.Public, default, metadata.GetOrAddString ("NotAnAssembly"), + default, MetadataTokens.FieldDefinitionHandle (1), MetadataTokens.MethodDefinitionHandle (1)); + } + metadata.AddCustomAttribute (parent, constructor, metadata.GetOrAddBlob (value)); + string path = Path.Combine (directory, "Attributes.dll"); + using var stream = File.Create (path); + pe.WritePE (stream); + return path; + } + + [Fact] + public async Task RealILLinkRetainsOnlyLiveTypeMapAttributes () + { + // The emitter references these assemblies in unused MemberRefs even when the + // fixture contains no Android proxy IL. Resolve them without skipping errors. + EmitStub ("Mono.Android"); + EmitStub ("Java.Interop"); + EmitStub ("Mono.Android.Runtime"); + EmitTargets (); + string input = Emit ("_Bindings.TypeMap", + Entry ("test/Unconditional"), + Entry ("test/Surviving", "Targets.Surviving, Targets"), + Entry ("test/Removed", "Targets.Removed, Targets"), + Entry ("test/Alias[0]", "Targets.Surviving, Targets"), + Entry ("test/Alias[1]", "Targets.Removed, Targets")); + string root = EmitLinkerRoot ("_Bindings.TypeMap"); + var (before, _) = CreateTask (input); + Assert.True (before.Execute ()); + Assert.Contains ("test/Removed\n", File.ReadAllText (before.OutputFile), StringComparison.Ordinal); + + string linkedDirectory = Path.Combine (directory, "linked"); + await RunLinker (root, linkedDirectory); + string linked = Path.Combine (linkedDirectory, "_Bindings.TypeMap.dll"); + Assert.True (File.Exists (linked)); + var (after, _) = CreateTask (linked); + Assert.True (after.Execute ()); + Assert.Equal ("test/Alias\ntest/Surviving\ntest/Unconditional\n", File.ReadAllText (after.OutputFile)); + + using var pe = new PEReader (File.OpenRead (linked)); + var reader = pe.GetMetadataReader (); + var keys = new List (); + foreach (var handle in reader.GetAssemblyDefinition ().GetCustomAttributes ()) { + var attribute = reader.GetCustomAttribute (handle); + if (reader.GetCustomAttributeFullName (attribute, after.Log) != "System.Runtime.InteropServices.TypeMapAttribute`1") { + continue; + } + Assert.Equal (HandleKind.MemberReference, attribute.Constructor.Kind); + Assert.Equal (HandleKind.TypeSpecification, reader.GetMemberReference ((MemberReferenceHandle) attribute.Constructor).Parent.Kind); + var blob = reader.GetBlobReader (attribute.Value); + Assert.Equal (1, blob.ReadUInt16 ()); + keys.Add (blob.ReadSerializedString () ?? throw new InvalidOperationException ()); + } + Assert.Equal (new [] { "test/Alias[0]", "test/Surviving", "test/Unconditional" }, keys.OrderBy (k => k, StringComparer.Ordinal)); + } + + string EmitStub (string name) + { + string path = Path.Combine (directory, name + ".dll"); + using var stream = File.Create (path); + new TypeMapAssemblyGenerator (RuntimeVersion).GenerateEmpty (stream, name); + return path; + } + + void EmitTargets () + { + var pe = new PEAssemblyBuilder (RuntimeVersion); + pe.EmitPreamble ("Targets", "Targets.dll"); + var objectType = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + foreach (string name in new [] { "Surviving", "Removed" }) { + pe.Metadata.AddTypeDefinition (TypeAttributes.Public, pe.Metadata.GetOrAddString ("Targets"), pe.Metadata.GetOrAddString (name), + objectType, MetadataTokens.FieldDefinitionHandle (1), MetadataTokens.MethodDefinitionHandle (1)); + } + using var stream = File.Create (Path.Combine (directory, "Targets.dll")); + pe.WritePE (stream); + } + + string EmitLinkerRoot (string mapName) + { + var pe = new PEAssemblyBuilder (RuntimeVersion); + pe.EmitPreamble ("Root", "Root.dll"); + var metadata = pe.Metadata; + TypeReferenceHandle TypeRef (EntityHandle scope, string ns, string name) => + metadata.AddTypeReference (scope, metadata.GetOrAddString (ns), metadata.GetOrAddString (name)); + var anchor = TypeRef (pe.FindOrAddAssemblyRef (mapName), "", "__TypeMapAnchor"); + var attributeType = TypeRef (pe.SystemRuntimeInteropServicesRef, "System.Runtime.InteropServices", "TypeMapAssemblyTargetAttribute`1"); + var attributeCtor = pe.AddMemberRef (pe.MakeGenericTypeSpec (attributeType, anchor), ".ctor", + s => s.MethodSignature (isInstanceMethod: true).Parameters (1, r => r.Void (), p => p.AddParameter ().Type ().String ())); + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, attributeCtor, pe.BuildAttributeBlob (b => b.WriteSerializedString (mapName))); + var mappingType = TypeRef (pe.SystemRuntimeInteropServicesRef, "System.Runtime.InteropServices", "TypeMapping"); + var dictionaryType = TypeRef (pe.SystemRuntimeRef, "System.Collections.Generic", "IReadOnlyDictionary`2"); + var systemType = TypeRef (pe.SystemRuntimeRef, "System", "Type"); + var getMapping = pe.AddMemberRef (mappingType, "GetOrCreateExternalTypeMapping", + s => s.MethodSignature (genericParameterCount: 1).Parameters (0, r => { + var args = r.Type ().GenericInstantiation (dictionaryType, 2, isValueType: false); + args.AddArgument ().String (); + args.AddArgument ().Type (systemType, isValueType: false); + }, p => { })); + var methodSignature = new BlobBuilder (); + new BlobEncoder (methodSignature).MethodSpecificationSignature (1).AddArgument ().Type (anchor, isValueType: false); + var method = metadata.AddMethodSpecification (getMapping, metadata.GetOrAddBlob (methodSignature)); + metadata.AddTypeDefinition (TypeAttributes.Public, default, metadata.GetOrAddString ("Root"), + TypeRef (pe.SystemRuntimeRef, "System", "Object"), MetadataTokens.FieldDefinitionHandle (1), MetadataTokens.MethodDefinitionHandle (1)); + var survivingType = TypeRef (pe.FindOrAddAssemblyRef ("Targets"), "Targets", "Surviving"); + pe.EmitBody ("Run", MethodAttributes.Public | MethodAttributes.Static, + s => s.MethodSignature ().Parameters (0, r => r.Void (), p => { }), + il => { + il.LoadToken (survivingType); + il.PopValue (); + il.Call (method, 0, returnsValue: true); + il.PopValue (); + il.Return (); + }); + string path = Path.Combine (directory, "Root.dll"); + using var stream = File.Create (path); + pe.WritePE (stream); + return path; + } + + async Task RunLinker (string root, string output) + { + string linker = typeof (ExtractTypeMapKeysFromAssembliesTests).Assembly.GetCustomAttributes () + .Single (a => a.Key == "ILLinkPath").Value ?? throw new InvalidOperationException ("Missing ILLinkPath."); + string runtimeDirectory = Path.GetDirectoryName (typeof (object).Assembly.Location) ?? throw new InvalidOperationException (); + var start = new ProcessStartInfo (Environment.GetEnvironmentVariable ("DOTNET_HOST_PATH") ?? "dotnet") { + RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, + }; + foreach (string arg in new [] { + "exec", "--fx-version", Path.GetFileName (runtimeDirectory), linker, + "-a", Path.GetFileNameWithoutExtension (root), "-reference", root, "-d", directory, "-d", runtimeDirectory, "-out", output, + "--typemap-entry-assembly", "Root", "--skip-unresolved", "false", + }) { + start.ArgumentList.Add (arg); + } + using var process = Process.Start (start) ?? throw new InvalidOperationException ("Cannot start ILLink."); + var stdout = process.StandardOutput.ReadToEndAsync (); + var stderr = process.StandardError.ReadToEndAsync (); + using var timeout = new CancellationTokenSource (TimeSpan.FromMinutes (2)); + try { + await process.WaitForExitAsync (timeout.Token); + } catch (OperationCanceledException) { + process.Kill (entireProcessTree: true); + throw; + } + Assert.True (process.ExitCode == 0, await stdout + await stderr); + } + +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromLlvmIrArrayTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromLlvmIrArrayTests.cs new file mode 100644 index 00000000000..8dd512fcb9d --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromLlvmIrArrayTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; +using Microsoft.Android.Tasks; +using Microsoft.Build.Utilities; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ExtractTypeMapKeysFromLlvmIrArrayTests : IDisposable +{ + readonly string directory = Path.Combine (Path.GetTempPath (), "llvm-array-typemap-" + Guid.NewGuid ().ToString ("N")); + readonly TypeMapTaskBuildEngine engine = new (); + + public ExtractTypeMapKeysFromLlvmIrArrayTests () => Directory.CreateDirectory (directory); + + [Theory] + [InlineData ("java_type_names")] + [InlineData ("type_map_java_type_names")] + public void ArraysContributeOnlyReferenceElementClasses (string symbol) + { + var task = CreateTask (symbol, "[Ljava/lang/Object;", "[Z", "[B", "[C", "[S", "[I", "[J", "[F", "[D", + "[[[I", "test/Live", "[[Ltest/Outer$Inner;", "test/Outer$Inner"); + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("java/lang/Object\ntest/Live\ntest/Outer$Inner\n", File.ReadAllText (task.OutputFile)); + } + + [Fact] + public void PrimitiveArraysAloneProduceAValidEmptyClassSet () + { + var task = CreateTask ("java_type_names", "[B", "[[I", "[[[D"); + Assert.True (task.Execute ()); + Assert.Empty (File.ReadAllBytes (task.OutputFile)); + } + + [Theory] + [InlineData ("[")] + [InlineData ("[[")] + [InlineData ("[V")] + [InlineData ("[Q")] + [InlineData ("[Iextra")] + [InlineData ("[[I;")] + [InlineData ("[L;")] + [InlineData ("[Ljava.lang.Object;")] + [InlineData ("[Ljava/lang/Object")] + [InlineData ("[Ljava/lang/Object;;")] + [InlineData ("[Ltest/*;")] + [InlineData ("[Ltest/Foo[0];")] + [InlineData ("Ltest/Foo;")] + public void MalformedDescriptorsStillFail (string name) + { + var task = CreateTask ("java_type_names", name); + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, error => error.Code == "XA4327"); + Assert.False (File.Exists (task.OutputFile)); + } + + ExtractTypeMapKeysFromLlvmIr CreateTask (string symbol, params string [] names) + { + var bytes = Encoding.UTF8.GetBytes (string.Join ('\0', names) + '\0'); + var literal = new StringBuilder (); + foreach (var value in bytes) { + literal.Append ('\\').Append (value.ToString ("X2", CultureInfo.InvariantCulture)); + } + var source = Path.Combine (directory, "typemaps.ll"); + File.WriteAllText (source, $"@{symbol} = dso_local local_unnamed_addr constant [{bytes.Length} x i8] c\"{literal}\", align 1\n"); + return new ExtractTypeMapKeysFromLlvmIr { + BuildEngine = engine, + LlvmIrFiles = [new TaskItem (source)], + OutputFile = Path.Combine (directory, "classes.keys"), + }; + } + + public void Dispose () => Directory.Delete (directory, recursive: true); +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromNativeAotObjectTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromNativeAotObjectTests.cs new file mode 100644 index 00000000000..947a6dd7eec --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExtractTypeMapKeysFromNativeAotObjectTests.cs @@ -0,0 +1,801 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.Android.Tasks; +using Xunit; +using TaskItem = Microsoft.Build.Utilities.TaskItem; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ExtractTypeMapKeysFromNativeAotObjectTests : IDisposable +{ + const int SectionOffset = 128; + const int SymbolOffset = 17; + readonly string directory = Path.Combine (AppContext.BaseDirectory, nameof (ExtractTypeMapKeysFromNativeAotObjectTests), Guid.NewGuid ().ToString ("N")); + readonly Dictionary metadata = new (StringComparer.Ordinal); + + public ExtractTypeMapKeysFromNativeAotObjectTests () + { + Directory.CreateDirectory (directory); + File.WriteAllBytes (Path.Combine (directory, "llvm-readobj-placeholder"), []); + } + + public void Dispose () => Directory.Delete (directory, recursive: true); + + [Fact] + public void TaskLivesInModernBuildTasksAssembly () + { + Assert.Equal ("Microsoft.Android.Tasks.ExtractTypeMapKeysFromNativeAotObject", typeof (ExtractTypeMapKeysFromNativeAotObject).FullName); + Assert.Equal ("Microsoft.Android.Build.Tasks", typeof (ExtractTypeMapKeysFromNativeAotObject).Assembly.GetName ().Name); + } + + [Fact] + public void CapturedIlcArm64BlobContainsOnlyRetainedCanonicalKeys () + { + // Microsoft.DotNet.ILCompiler 11.0.0-rc.2.26461.115, Android ARM64, optimized with --scanreflection. + byte [] blob = Convert.FromBase64String ("AAIFXbUBGnNhbXBsZS9BbHdheXMEFnNhbXBsZS9MaXZlAkJzYW1wbGUvTGl2ZVsxMjM0NTY3ODkwMTIzNDU2Nzg5MF0CNGNvbS/DqXhhbXBsZS9QZWVyzpQkTmVzdGVkBBxzYW1wbGUvTGl2ZVswXQIAAgACD06t/lxt/qPIpx3+zIY="); + var (task, engine) = CreateTask (WriteObject ("captured-ilc-arm64", blob)); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + byte [] expected = new UTF8Encoding (false).GetBytes ("com/\u00e9xample/Peer\u0394$Nested\nsample/Always\nsample/Live\n"); + Assert.Equal (expected, File.ReadAllBytes (task.OutputFile)); + } + + [Fact] + public void ExtractsOnlySymbolPayloadAndUnionsEveryJavaGroupAndObject () + { + string arm64 = WriteObject ("arm64", NativeAotObjectTestFixture.CreateGroups ( + ["test/Zebra", "test/Outer$Inner", "test/Alias[0]", "test/\u00e9clair"], + ["test/FromOtherGroup", "test/Alias[1]", "test/\U00010400Peer"])); + string x64 = WriteObject ("x64", NativeAotObjectTestFixture.CreateGroups ( + ["test/Alpha", "test/Alias", "test/Outer$Inner"], + ["test/A\u0301", "test/a", "test/\u2160Peer"])); + Summary (x64) ["Format"] = "elf64-x86-64"; + Summary (x64) ["Arch"] = "x86_64"; + Symbol (arm64) ["Name"] = new JsonObject { ["Name"] = "Compilation_123___external_type_map__", ["Value"] = 42 }; + var (task, engine) = CreateTask (arm64, x64, arm64); + Directory.CreateDirectory (Path.GetDirectoryName (task.OutputFile) ?? throw new InvalidOperationException ()); + File.WriteAllText (task.OutputFile, "stale/sentinel\n"); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + byte [] expected = new UTF8Encoding (false).GetBytes ( + "test/Alias\ntest/Alpha\ntest/A\u0301\ntest/FromOtherGroup\ntest/Outer$Inner\ntest/Zebra\ntest/a\ntest/\u00e9clair\ntest/\u2160Peer\ntest/\U00010400Peer\n"); + Assert.Equal (expected, File.ReadAllBytes (task.OutputFile)); + + File.SetLastWriteTimeUtc (task.OutputFile, new DateTime (2000, 1, 1)); + var (reordered, reorderedEngine) = CreateTask (x64, arm64); + Assert.True (reordered.Execute ()); + Assert.Empty (reorderedEngine.Errors); + Assert.Equal (expected, File.ReadAllBytes (reordered.OutputFile)); + Assert.True (File.GetLastWriteTimeUtc (reordered.OutputFile).Year > 2000); + } + + [Theory] + [InlineData ("elf32-littlearm", "arm", "32bit")] + [InlineData ("elf64-littleaarch64", "aarch64", "64bit")] + [InlineData ("elf64-x86-64", "x86_64", "64bit")] + [InlineData ("elf32-i386", "i386", "32bit")] + public void AcceptsSupportedRelocatableElfArchitectures (string format, string arch, string addressSize) + { + string path = WriteObject ("map", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + Summary (path) ["Format"] = format; + Summary (path) ["Arch"] = arch; + Summary (path) ["AddressSize"] = addressSize; + var (task, engine) = CreateTask (path); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("test/Live\n", File.ReadAllText (task.OutputFile)); + } + + [Theory] + [InlineData ("test/A[123]", "test/A")] + [InlineData ("test/A[000]", "test/A")] + [InlineData ("test/A[9999999999999999999999999999999]", "test/A")] + [InlineData ("test/Outer$Inner[2]", "test/Outer$Inner")] + [InlineData ("test/\u2160[1]", "test/\u2160")] + public void RemovesOneTerminalNonemptyAsciiDecimalAlias (string key, string expected) + { + var (task, engine) = CreateTask (WriteObject ("map", NativeAotObjectTestFixture.CreateBlob (key))); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal (new UTF8Encoding (false).GetBytes (expected + "\n"), File.ReadAllBytes (task.OutputFile)); + } + + [Fact] + public void ObjectArraysContributeOnlyCanonicalElementClasses () + { + byte [] blob = NativeAotObjectTestFixture.CreateBlob ( + "[Ljava/lang/Object;", "[[Ltest/Outer$Inner;", "[Ltest/Outer$Inner;[000]", + "test/Outer$Inner", "[I[0]"); + var (task, engine) = CreateTask (WriteObject ("arrays", blob)); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("java/lang/Object\ntest/Outer$Inner\n", File.ReadAllText (task.OutputFile)); + } + + [Fact] + public void PrimitiveArraysContributeNoClasses () + { + byte [] blob = NativeAotObjectTestFixture.CreateBlob ("[Z", "[B", "[C", "[S", "[I", "[J", "[F", "[D", "[[I", "[[[D"); + var (task, engine) = CreateTask (WriteObject ("primitive-arrays", blob)); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.True (File.Exists (task.OutputFile)); + Assert.Empty (File.ReadAllBytes (task.OutputFile)); + } + + [Fact] + public void SelectsJavaGroupsByFixupsRatherThanKeyAppearance () + { + string path = WriteObject ("mixed-universes", NativeAotObjectTestFixture.CreateGroups ( + ["System.Collections.Generic.IDictionary`2[System.Char,System.Int32]", "foreign/LooksLikeJava"], + ["test/Shared", "test/Alias[0]"], + ["test/PerAssembly", "test/Alias[1]"])); + var (task, engine) = CreateTask (path); + task.UseGroupMetadata = true; + task.RelocationOutput = AddGroupMetadata (path, + "_ZTV43Mono_Android_Android_Runtime_JavaDictionary", + "_ZTV29Mono_Android_Java_Lang_Object", + "_ZTV37_Mono_Android_TypeMap___TypeMapAnchor"); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("test/Alias\ntest/PerAssembly\ntest/Shared\n", File.ReadAllText (task.OutputFile)); + Assert.Equal (".rela.rodata", task.RelocationSection); + Assert.Equal (12, task.RelocationEnd - task.RelocationStart); + } + + [Theory] + [InlineData ("aarch64", "R_AARCH64_PREL32", "SHT_RELA", ".rela.rodata")] + [InlineData ("arm", "R_ARM_REL32", "SHT_REL", ".rel.rodata")] + [InlineData ("x86_64", "R_X86_64_PC32", "SHT_RELA", ".rela.rodata")] + [InlineData ("i386", "R_386_PC32", "SHT_REL", ".rel.rodata")] + public void ResolvesGroupSlotsUsingTargetRelocationKind (string arch, string kind, string sectionType, string sectionName) + { + string path = WriteObject ("abi-group", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + Summary (path) ["Arch"] = arch; + var (task, engine) = CreateTask (path); + task.UseGroupMetadata = true; + task.RelocationOutput = AddGroupMetadata (path, "_ZTV29Mono_Android_Java_Lang_Object") + .Replace ("R_AARCH64_PREL32", kind, StringComparison.Ordinal); + var relocationSection = RelocationSection (path); + relocationSection ["Name"] = new JsonObject { ["Name"] = sectionName, ["Value"] = 0 }; + relocationSection ["Type"] = new JsonObject { ["Name"] = sectionType, ["Value"] = 0 }; + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("test/Live\n", File.ReadAllText (task.OutputFile)); + Assert.Equal (sectionName, task.RelocationSection); + } + + [Fact] + public void ResolvesCompilationPrefixedMapAndFixupSymbols () + { + string path = WriteObject ("prefixed", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + Symbol (path) ["Name"] = new JsonObject { ["Name"] = "Compilation_123___external_type_map__", ["Value"] = 0 }; + var (task, engine) = CreateTask (path); + task.UseGroupMetadata = true; + task.RelocationOutput = AddGroupMetadata (path, "Compilation_123__ZTV29Mono_Android_Java_Lang_Object"); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("test/Live\n", File.ReadAllText (task.OutputFile)); + } + + [Fact] + public void MalformedSelectedJavaKeyStillFails () + { + string path = WriteObject ("invalid-java-group", NativeAotObjectTestFixture.CreateGroups ( + ["System.Collections.Generic.IDictionary`2[System.Char,System.Int32]"], + ["test/Invalid[abc]"])); + var (task, engine) = CreateTask (path); + task.UseGroupMetadata = true; + task.RelocationOutput = AddGroupMetadata (path, + "_ZTV43Mono_Android_Android_Runtime_JavaDictionary", "_ZTV29Mono_Android_Java_Lang_Object"); + + AssertFailure (task, engine); + } + + [Theory] + [InlineData ("_ZTV43Mono_Android_Android_Runtime_JavaDictionary")] + [InlineData ("_ZTV29Mono_Android_Java_Lang_ObjectExtra")] + [InlineData ("_ZTV30ThirdParty_TypeMap___TypeMapAnchor")] + [InlineData ("_ZTV30_Other_TypeMap___TypeMapAnchorExtra")] + public void MissingRecognizedJavaGroupIsNotAnEmptySuccess (string groupSymbol) + { + string path = WriteObject ("foreign-only", NativeAotObjectTestFixture.CreateBlob ("test/LooksLikeJava")); + var (task, engine) = CreateTask (path); + task.UseGroupMetadata = true; + task.RelocationOutput = AddGroupMetadata (path, groupSymbol); + + AssertFailure (task, engine); + } + + [Theory] + [InlineData ("missing-fixups")] + [InlineData ("duplicate-fixups")] + [InlineData ("bad-fixup-size")] + [InlineData ("missing-relocation-section")] + [InlineData ("ambiguous-relocation-section")] + [InlineData ("wrong-target-section")] + [InlineData ("missing-relocation")] + [InlineData ("duplicate-relocation")] + [InlineData ("wrong-relocation-kind")] + [InlineData ("nonzero-addend")] + public void InvalidGroupMetadataFails (string defect) + { + string path = WriteObject ("invalid-group-metadata", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + var (task, engine) = CreateTask (path); + task.UseGroupMetadata = true; + task.RelocationOutput = AddGroupMetadata (path, "_ZTV29Mono_Android_Java_Lang_Object"); + var symbols = Array (Document (path) ["Symbols"]); + var sections = Array (Document (path) ["Sections"]); + switch (defect) { + case "missing-fixups": + symbols.RemoveAt (symbols.Count - 1); + break; + case "duplicate-fixups": + symbols.Add (symbols [symbols.Count - 1]?.DeepClone ()); + break; + case "bad-fixup-size": + Object (Object (symbols [symbols.Count - 1]) ["Symbol"]) ["Size"] = 3; + break; + case "missing-relocation-section": + sections.RemoveAt (sections.Count - 1); + break; + case "ambiguous-relocation-section": + var duplicate = Object (sections [sections.Count - 1]?.DeepClone ()); + Object (duplicate ["Section"]) ["Index"] = 4; + sections.Add (duplicate); + break; + case "wrong-target-section": + RelocationSection (path) ["Info"] = 2; + break; + case "missing-relocation": + task.RelocationOutput = ""; + break; + case "duplicate-relocation": + task.RelocationOutput += task.RelocationOutput; + break; + case "wrong-relocation-kind": + task.RelocationOutput = task.RelocationOutput.Replace ("R_AARCH64_PREL32", "R_AARCH64_ABS64", StringComparison.Ordinal); + break; + case "nonzero-addend": + task.RelocationOutput = task.RelocationOutput.TrimEnd () + "+0x4\n"; + break; + } + AssertFailure (task, engine); + } + + [Fact] + public void OutOfRangeGroupFixupFails () + { + byte [] group = NativeAotObjectTestFixture.CreateGroup ( + NativeAotObjectTestFixture.CreateTable ([NativeAotObjectTestFixture.CreateKey ("test/Live")]), typeIndex: 2); + string path = WriteObject ("bad-group-index", NativeAotObjectTestFixture.CreateTable ([group])); + var (task, engine) = CreateTask (path); + task.UseGroupMetadata = true; + task.RelocationOutput = AddGroupMetadata (path, "_ZTV29Mono_Android_Java_Lang_Object"); + + AssertFailure (task, engine); + Assert.Null (task.RelocationSection); + } + + [Fact] + public void EmptyOuterTableDoesNotRequireGroupRelocations () + { + var (task, engine) = CreateTask (WriteObject ("empty-groups", NativeAotObjectTestFixture.CreateGroups ())); + task.UseGroupMetadata = true; + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Empty (File.ReadAllBytes (task.OutputFile)); + Assert.Null (task.RelocationSection); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void EmptyOuterOrInnerTableOverwritesWithZeroByteFile (bool emptyOuter) + { + byte [] blob = emptyOuter ? NativeAotObjectTestFixture.CreateGroups () : NativeAotObjectTestFixture.CreateBlob (); + string path = WriteObject ("empty", blob); + var (task, engine) = CreateTask (path); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.True (File.Exists (task.OutputFile)); + Assert.Empty (File.ReadAllBytes (task.OutputFile)); + + File.WriteAllText (task.OutputFile, "stale/Key\n"); + for (int run = 0; run < 2; run++) { + File.SetLastWriteTimeUtc (task.OutputFile, new DateTime (2000, 1, 1)); + var (repeat, repeatEngine) = CreateTask (path); + Assert.True (repeat.Execute ()); + Assert.Empty (repeatEngine.Errors); + Assert.Empty (File.ReadAllBytes (repeat.OutputFile)); + Assert.True (File.GetLastWriteTimeUtc (repeat.OutputFile).Year > 2000); + } + } + + [Theory] + [InlineData (1, 0)] + [InlineData (1, 2)] + [InlineData (2, 1)] + [InlineData (4, 3)] + public void ReadsBucketIndexWidthsAndMultipleBuckets (int indexWidth, int bucketShift) + { + byte [] inner = NativeAotObjectTestFixture.CreateTable ( + new [] { "test/Z", "test/A", "test/M" }.Select (key => NativeAotObjectTestFixture.CreateKey (key)).ToArray (), + indexWidth, bucketShift); + byte [] group = NativeAotObjectTestFixture.CreateGroup (inner); + byte [] blob = NativeAotObjectTestFixture.CreateTable ([group, group], indexWidth, bucketShift); + var (task, engine) = CreateTask (WriteObject ("buckets", blob)); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("test/A\ntest/M\ntest/Z\n", File.ReadAllText (task.OutputFile)); + } + + [Theory] + [InlineData (1, false)] + [InlineData (2, false)] + [InlineData (3, false)] + [InlineData (4, false)] + [InlineData (5, false)] + [InlineData (1, true)] + [InlineData (2, true)] + [InlineData (3, true)] + [InlineData (4, true)] + [InlineData (5, true)] + public void RelativeOffsetsUseTheIntegerAddressAndCanPointBackwards (int relativeWidth, bool backwards) + { + byte [] inner = NativeAotObjectTestFixture.CreateTable ( + [NativeAotObjectTestFixture.CreateKey ("test/Live")], relativeWidth: relativeWidth, backwards: backwards); + byte [] blob = NativeAotObjectTestFixture.CreateTable ( + [NativeAotObjectTestFixture.CreateGroup (inner)], relativeWidth: relativeWidth, backwards: backwards); + var (task, engine) = CreateTask (WriteObject ("relative", blob)); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("test/Live\n", File.ReadAllText (task.OutputFile)); + } + + [Theory] + [InlineData (0u)] + [InlineData (128u)] + [InlineData (16384u)] + [InlineData (2097152u)] + [InlineData (268435456u)] + [InlineData (uint.MaxValue)] + public void ReadsAllUnsignedCompactIntegerWidths (uint typeIndex) + { + byte [] inner = NativeAotObjectTestFixture.CreateTable ([NativeAotObjectTestFixture.CreateKey ("test/Live", typeIndex)]); + byte [] blob = NativeAotObjectTestFixture.CreateTable ([NativeAotObjectTestFixture.CreateGroup (inner, typeIndex: typeIndex)]); + var (task, engine) = CreateTask (WriteObject ("indices", blob)); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal ("test/Live\n", File.ReadAllText (task.OutputFile)); + } + + [Theory] + [InlineData (127)] + [InlineData (128)] + [InlineData (16384)] + [InlineData (2097152)] + public void ReadsMultibyteStringLengthsWithoutTruncatingKeys (int byteLength) + { + string key = "test/" + new string ('A', byteLength - 5); + var (task, engine) = CreateTask (WriteObject ("length", NativeAotObjectTestFixture.CreateBlob (key))); + + Assert.True (task.Execute ()); + Assert.Empty (engine.Errors); + Assert.Equal (new UTF8Encoding (false).GetBytes (key + "\n"), File.ReadAllBytes (task.OutputFile)); + } + + [Fact] + public void MissingObjectInputsFail () + { + var (task, engine) = CreateTask (); + AssertFailure (task, engine); + } + + [Theory] + [InlineData ("missing")] + [InlineData ("directory")] + public void MissingOrUnreadableObjectFailsWithFileContext (string kind) + { + string path = Path.Combine (directory, "invalid.o"); + if (kind == "directory") { + Directory.CreateDirectory (path); + } + var (task, engine) = CreateTask (path); + AssertFailure (task, engine, path); + } + + [Theory] + [InlineData (0)] + [InlineData (128)] + public void TruncatedObjectFailsAgainstActualFileBounds (int length) + { + string path = WriteObject ("truncated", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + using (var stream = File.OpenWrite (path)) { + stream.SetLength (length); + } + var (task, engine) = CreateTask (path); + AssertFailure (task, engine, path); + } + + [Theory] + [InlineData ("")] + [InlineData ("missing-llvm-readobj")] + public void MissingToolFailsWithCodedError (string tool) + { + var engine = new TypeMapTaskBuildEngine (); + var task = new ExtractTypeMapKeysFromNativeAotObject { + BuildEngine = engine, + NativeObjectFiles = [new TaskItem (WriteObject ("map", NativeAotObjectTestFixture.CreateBlob ("test/Live")))], + LlvmReadObjPath = tool.Length == 0 ? "" : Path.Combine (directory, tool), + OutputFile = Path.Combine (directory, "keys.txt"), + }; + AssertFailure (task, engine, task.LlvmReadObjPath); + } + + [Theory] + [InlineData ("")] + [InlineData ("not JSON")] + [InlineData ("{}")] + [InlineData ("[]")] + [InlineData ("[{},{}]")] + [InlineData ("[null]")] + [InlineData ("[{}]")] + public void MalformedToolJsonFailsWithFileContext (string json) + { + string path = WriteObject ("map", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + var (task, engine) = CreateTask (path); + task.MetadataReader = _ => json; + AssertFailure (task, engine, path); + } + + [Theory] + [InlineData ("FileSummary")] + [InlineData ("ElfHeader")] + [InlineData ("Sections")] + [InlineData ("Symbols")] + public void MissingRequiredMetadataFails (string property) + { + string path = WriteObject ("map", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + Document (path).Remove (property); + var (task, engine) = CreateTask (path); + AssertFailure (task, engine, path); + } + + [Theory] + [InlineData ("format")] + [InlineData ("architecture")] + [InlineData ("dynamic")] + [InlineData ("executable")] + [InlineData ("big-endian")] + [InlineData ("missing-encoding")] + [InlineData ("no-symbol")] + [InlineData ("suffix-not-terminal")] + [InlineData ("undefined-symbol")] + [InlineData ("no-sections")] + [InlineData ("nobits-section")] + [InlineData ("duplicate-section")] + [InlineData ("negative-offset")] + [InlineData ("section-past-file")] + [InlineData ("section-overruns-file")] + [InlineData ("symbol-past-section")] + [InlineData ("symbol-overruns-section")] + [InlineData ("empty-symbol")] + [InlineData ("huge-symbol")] + [InlineData ("wrong-number-type")] + public void InvalidObjectMetadataFails (string defect) + { + string path = WriteObject ("map", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + var header = Object (Document (path) ["ElfHeader"]); + var symbols = Array (Document (path) ["Symbols"]); + var sections = Array (Document (path) ["Sections"]); + long fileLength = new FileInfo (path).Length; + switch (defect) { + case "format": Summary (path) ["Format"] = "Mach-O arm64"; break; + case "architecture": Summary (path) ["Arch"] = "riscv64"; break; + case "dynamic": header ["Type"] = "SharedObject (0x3)"; break; + case "executable": header ["Type"] = "Executable (0x2)"; break; + case "big-endian": Object (Object (header ["Ident"]) ["DataEncoding"]) ["Value"] = 2; break; + case "missing-encoding": Object (header ["Ident"]).Remove ("DataEncoding"); break; + case "no-symbol": symbols.RemoveAt (1); break; + case "suffix-not-terminal": Object (Symbol (path) ["Name"]) ["Name"] = "__external_type_map__unrelated"; break; + case "undefined-symbol": Object (Symbol (path) ["Section"]) ["Value"] = 0; break; + case "no-sections": sections.Clear (); break; + case "nobits-section": Object (Section (path) ["Type"]) ["Name"] = "SHT_NOBITS"; break; + case "duplicate-section": sections.Add (sections [1]?.DeepClone ()); break; + case "negative-offset": Section (path) ["Offset"] = -1; break; + case "section-past-file": Section (path) ["Offset"] = fileLength + 1; break; + case "section-overruns-file": Section (path) ["Size"] = fileLength; break; + case "symbol-past-section": Symbol (path) ["Value"] = fileLength; break; + case "symbol-overruns-section": Symbol (path) ["Size"] = fileLength; break; + case "empty-symbol": Symbol (path) ["Size"] = 0; break; + case "huge-symbol": Symbol (path) ["Size"] = long.MaxValue; break; + case "wrong-number-type": Symbol (path) ["Value"] = "17"; break; + default: throw new ArgumentException (defect, nameof (defect)); + } + var (task, engine) = CreateTask (path); + AssertFailure (task, engine, path); + } + + [Theory] + [MemberData (nameof (MalformedTables))] + public void MalformedOuterOrInnerTableFails (string defect, byte [] table) + { + foreach (bool inner in new [] { false, true }) { + byte [] blob = inner ? NativeAotObjectTestFixture.CreateTable ([NativeAotObjectTestFixture.CreateGroup (table)]) : table; + string path = WriteObject (defect + (inner ? "-inner" : "-outer"), blob); + var (task, engine) = CreateTask (path); + AssertFailure (task, engine, path); + } + } + + public static IEnumerable MalformedTables () + { + yield return ["missing-header", new byte [] { }]; + yield return ["invalid-index-width", new byte [] { 3 }]; + yield return ["invalid-bucket-count", new byte [] { 252 }]; + yield return ["truncated-indices", new byte [] { 0, 2 }]; + yield return ["indices-overlap-header", new byte [] { 0, 1, 1 }]; + yield return ["unordered-indices", new byte [] { 4, 3, 2, 3 }]; + yield return ["bucket-outside-blob", new byte [] { 0, 2, 250 }]; + yield return ["truncated-cell", new byte [] { 0, 2, 3, 42 }]; + yield return ["truncated-relative", new byte [] { 0, 2, 4, 42, 1 }]; + yield return ["cell-overruns-bucket", new byte [] { 0, 2, 4, 42, 15, 0, 0, 0, 0 }]; + yield return ["positive-relative-overflow", new byte [] { 0, 2, 8, 42, 15, 255, 255, 255, 127 }]; + yield return ["negative-relative-overflow", new byte [] { 0, 2, 8, 42, 15, 0, 0, 0, 128 }]; + yield return ["invalid-integer-tag", new byte [] { 0, 2, 4, 42, 31 }]; + } + + [Theory] + [InlineData (0u)] + [InlineData (2u)] + [InlineData (uint.MaxValue)] + public void InvalidGroupStateIsNotAnEmptyMap (uint state) + { + byte [] group = NativeAotObjectTestFixture.CreateGroup (NativeAotObjectTestFixture.CreateTable ([]), state); + var (task, engine) = CreateTask (WriteObject ("invalid-state", NativeAotObjectTestFixture.CreateTable ([group]))); + AssertFailure (task, engine); + } + + [Theory] + [InlineData (new byte [] { })] + [InlineData (new byte [] { 0 })] + [InlineData (new byte [] { 0, 2 })] + [InlineData (new byte [] { 31 })] + [InlineData (new byte [] { 0, 31 })] + public void TruncatedOrMalformedGroupTupleFails (byte [] group) + { + var (task, engine) = CreateTask (WriteObject ("invalid-group", NativeAotObjectTestFixture.CreateTable ([group]))); + AssertFailure (task, engine); + } + + [Theory] + [MemberData (nameof (MalformedKeyTuples))] + public void MalformedKeyTupleFails (string defect, byte [] tuple) + { + byte [] inner = NativeAotObjectTestFixture.CreateTable ([tuple]); + byte [] blob = NativeAotObjectTestFixture.CreateTable ([NativeAotObjectTestFixture.CreateGroup (inner)]); + var (task, engine) = CreateTask (WriteObject (defect, blob)); + AssertFailure (task, engine); + } + + public static IEnumerable MalformedKeyTuples () + { + yield return ["overflow-length", NativeAotObjectTestFixture.EncodeUnsigned (uint.MaxValue)]; + yield return ["invalid-length-tag", new byte [] { 31 }]; + yield return ["truncated-two-byte-length", new byte [] { 1 }]; + yield return ["truncated-three-byte-length", new byte [] { 3, 0 }]; + yield return ["truncated-four-byte-length", new byte [] { 7, 0, 0 }]; + yield return ["truncated-five-byte-length", new byte [] { 15, 0, 0, 0 }]; + yield return ["length-overruns-blob", new byte [] { 126, 65, 0 }]; + yield return ["missing-target-index", new byte [] { 2, 65 }]; + yield return ["invalid-target-index", new byte [] { 2, 65, 31 }]; + yield return ["overlong-utf8", new byte [] { 4, 192, 175, 0 }]; + yield return ["surrogate-utf8", new byte [] { 6, 237, 160, 128, 0 }]; + yield return ["incomplete-utf8", new byte [] { 4, 240, 144, 0 }]; + yield return ["invalid-utf8-continuation", new byte [] { 2, 128, 0 }]; + } + + [Theory] + [InlineData ("")] + [InlineData ("test/Invalid\nName")] + [InlineData ("test/Invalid\rName")] + [InlineData ("test/Invalid\0Name")] + [InlineData ("test/A[]")] + [InlineData ("test/A[-1]")] + [InlineData ("test/A[1x]")] + [InlineData ("test/A[1]Extra")] + [InlineData ("test/A[1")] + [InlineData ("test/A[")] + [InlineData ("[0]")] + [InlineData ("test/A[\u0661]")] + [InlineData ("test/A[1][2]")] + [InlineData ("test.Invalid")] + [InlineData ("test/*")] + [InlineData ("test/")] + [InlineData ("/test")] + [InlineData ("test//Invalid")] + [InlineData ("test/Invalid\u200bName")] + [InlineData ("[L;")] + [InlineData ("[V")] + [InlineData ("[Q")] + [InlineData ("[Iextra")] + [InlineData ("[Ljava.lang.Object;")] + [InlineData ("[Ljava/lang/Object")] + [InlineData ("[Ljava/lang/Object;;")] + [InlineData ("[Ltest/A[0];")] + [InlineData ("[[")] + public void InvalidCanonicalKeysOrAliasesFail (string key) + { + var (task, engine) = CreateTask (WriteObject ("invalid-key", NativeAotObjectTestFixture.CreateBlob (key))); + AssertFailure (task, engine); + } + + [Fact] + public void InvalidSecondObjectDoesNotWritePartialOutput () + { + string valid = WriteObject ("valid", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + string invalid = WriteObject ("invalid", [3]); + var (task, engine) = CreateTask (valid, invalid); + AssertFailure (task, engine, invalid); + } + + [Fact] + public void InvalidLaterGroupDoesNotWritePartialOutput () + { + byte [] valid = NativeAotObjectTestFixture.CreateGroup ( + NativeAotObjectTestFixture.CreateTable ([NativeAotObjectTestFixture.CreateKey ("test/Live")])); + byte [] invalid = NativeAotObjectTestFixture.CreateGroup (NativeAotObjectTestFixture.CreateTable ([]), state: 0); + var (task, engine) = CreateTask (WriteObject ("mixed-groups", NativeAotObjectTestFixture.CreateTable ([valid, invalid]))); + AssertFailure (task, engine); + } + + [Fact] + public void MetadataReadFailureIsReportedWithoutPartialOutput () + { + string path = WriteObject ("map", NativeAotObjectTestFixture.CreateBlob ("test/Live")); + var (task, engine) = CreateTask (path); + task.MetadataReader = _ => throw new IOException ("Could not read object metadata."); + AssertFailure (task, engine, path); + } + + [Fact] + public void OutputWriteFailureIsReported () + { + var (task, engine) = CreateTask (WriteObject ("map", NativeAotObjectTestFixture.CreateBlob ("test/Live"))); + task.OutputFile = directory; + AssertFailure (task, engine, directory); + } + + static void AssertFailure (ExtractTypeMapKeysFromNativeAotObject task, TypeMapTaskBuildEngine engine, string? file = null) + { + file ??= task.NativeObjectFiles.Length == 0 ? nameof (task.NativeObjectFiles) : task.NativeObjectFiles [0].ItemSpec; + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, error => error.Code == "XA4327" && + (file.Length == 0 || error.Message != null && error.Message.Contains (file, StringComparison.Ordinal))); + Assert.False (File.Exists (task.OutputFile)); + } + + (MetadataTask task, TypeMapTaskBuildEngine engine) CreateTask (params string [] paths) + { + var engine = new TypeMapTaskBuildEngine (); + return (new MetadataTask (path => metadata [path].ToJsonString ()) { + BuildEngine = engine, + NativeObjectFiles = paths.Select (path => new TaskItem (path)).ToArray (), + LlvmReadObjPath = Path.Combine (directory, "llvm-readobj-placeholder"), + OutputFile = Path.Combine (directory, "output", "keys.txt"), + }, engine); + } + + string WriteObject (string name, byte [] blob) + { + string path = Path.Combine (directory, name + ".o"); + byte [] bytes = new byte [SectionOffset + SymbolOffset + blob.Length + 64]; + Encoding.UTF8.GetBytes ("unrelated/Before\0__external_type_map__\0").CopyTo (bytes, 0); + blob.CopyTo (bytes, SectionOffset + SymbolOffset); + Encoding.UTF8.GetBytes ("unrelated/After\0").CopyTo (bytes, SectionOffset + SymbolOffset + blob.Length); + File.WriteAllBytes (path, bytes); + metadata [path] = Array (JsonNode.Parse ($$$$""" + [{ + "FileSummary": {"File":"fixture.o","Format":"elf64-littleaarch64","Arch":"aarch64","AddressSize":"64bit"}, + "ElfHeader": {"Type":"Relocatable (0x1)","Ident":{"DataEncoding":{"Name":"LittleEndian","Value":1}}}, + "Sections": [ + {"Section":{"Index":2,"Name":{"Name":".rodata","Value":9},"Type":{"Name":"SHT_PROGBITS","Value":1},"Offset":0,"Size":64}}, + {"Section":{"Index":1,"Name":{"Name":".rodata","Value":1},"Type":{"Name":"SHT_PROGBITS","Value":1},"Offset":{{{{SectionOffset}}}},"Size":{{{{SymbolOffset + blob.Length + 64}}}}}} + ], + "Symbols": [ + {"Symbol":{"Name":{"Name":"unrelated/Removed","Value":0},"Value":0,"Size":64,"Section":{"Name":".rodata","Value":2}}}, + {"Symbol":{"Name":{"Name":"__external_type_map__","Value":0},"Value":{{{{SymbolOffset}}}},"Size":{{{{blob.Length}}}},"Section":{"Name":".rodata","Value":1}}}, + {"Symbol":{"Name":{"Name":"__external_type_map__not_a_map","Value":0},"Value":0,"Size":64,"Section":{"Name":".rodata","Value":2}}} + ] + }] + """)); + return path; + } + + JsonObject Document (string path) => Object (metadata [path] [0]); + JsonObject Summary (string path) => Object (Document (path) ["FileSummary"]); + JsonObject Section (string path) => Object (Object (Array (Document (path) ["Sections"]) [1]) ["Section"]); + JsonObject Symbol (string path) => Object (Object (Array (Document (path) ["Symbols"]) [1]) ["Symbol"]); + JsonObject RelocationSection (string path) => Object (Object (Array (Document (path) ["Sections"]) [2]) ["Section"]); + + string AddGroupMetadata (string path, params string [] groupSymbols) + { + var symbol = Symbol (path); + long size = symbol ["Size"]?.GetValue () ?? throw new InvalidOperationException (); + string mapName = Object (symbol ["Name"]) ["Name"]?.GetValue () ?? throw new InvalidOperationException (); + string prefix = mapName.Substring (0, mapName.Length - "__external_type_map__".Length); + long value = SymbolOffset + size + 16; + Array (Document (path) ["Symbols"]).Add (new JsonObject { + ["Symbol"] = new JsonObject { + ["Name"] = new JsonObject { ["Name"] = prefix + "__external_CommonFixupsTable_references", ["Value"] = 0 }, + ["Value"] = value, + ["Size"] = groupSymbols.Length * 4, + ["Section"] = new JsonObject { ["Name"] = ".rodata", ["Value"] = 1 }, + }, + }); + Array (Document (path) ["Sections"]).Add (new JsonObject { + ["Section"] = new JsonObject { + ["Index"] = 3, + ["Name"] = new JsonObject { ["Name"] = ".rela.rodata", ["Value"] = 0 }, + ["Type"] = new JsonObject { ["Name"] = "SHT_RELA", ["Value"] = 4 }, + ["Info"] = 1, + }, + }); + var relocations = new StringBuilder ("RELOCATION RECORDS FOR [.rodata]:\nOFFSET TYPE VALUE\n"); + for (int i = 0; i < groupSymbols.Length; i++) { + relocations.Append ($"{value + i * 4:x16} R_AARCH64_PREL32 {groupSymbols [i]}\n"); + } + return relocations.ToString (); + } + + static JsonObject Object (JsonNode? node) => node as JsonObject ?? throw new InvalidOperationException ("Expected an object fixture."); + static JsonArray Array (JsonNode? node) => node as JsonArray ?? throw new InvalidOperationException ("Expected an array fixture."); + + sealed class MetadataTask (Func reader) : ExtractTypeMapKeysFromNativeAotObject + { + public Func MetadataReader { get; set; } = reader; + public bool UseGroupMetadata { get; set; } + public string RelocationOutput { get; set; } = ""; + public string? RelocationSection { get; private set; } + public long RelocationStart { get; private set; } + public long RelocationEnd { get; private set; } + + protected override Task ReadObjectMetadataAsync (string objectFile) => Task.FromResult (MetadataReader (objectFile)); + + // Parser fixtures model Java groups; separate relocation tests exercise group selection. + protected override Task> GetJavaTypeMapGroupsAsync ( + string objectFile, JsonElement fileMetadata, string mapSymbol, IReadOnlyCollection groupIndices, long fileLength) => + UseGroupMetadata + ? base.GetJavaTypeMapGroupsAsync (objectFile, fileMetadata, mapSymbol, groupIndices, fileLength) + : Task.FromResult (new HashSet (groupIndices)); + + protected override Task ReadObjectRelocationsAsync (string objectFile, string section, long start, long end) + { + RelocationSection = section; + RelocationStart = start; + RelocationEnd = end; + return Task.FromResult (RelocationOutput); + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/NativeAotObjectTestFixture.Tool.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/NativeAotObjectTestFixture.Tool.cs new file mode 100644 index 00000000000..92a8243f472 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/NativeAotObjectTestFixture.Tool.cs @@ -0,0 +1,75 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Xamarin.Android.Tools; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +static partial class NativeAotObjectTestFixture +{ + public static string WriteObject (string directory, string name, string llvmReadObjPath, params string [] keys) + => WriteObjectGroups (directory, name, llvmReadObjPath, ("_ZTV29Mono_Android_Java_Lang_Object", keys)); + + public static string WriteObjectGroups ( + string directory, string name, string llvmReadObjPath, params (string Symbol, string [] Keys) [] groups) + { + string? toolDirectory = Path.GetDirectoryName (llvmReadObjPath); + if (string.IsNullOrEmpty (toolDirectory)) { + throw new ArgumentException ("Expected a full path to llvm-readobj.", nameof (llvmReadObjPath)); + } + string clang = Path.Combine (toolDirectory, OperatingSystem.IsWindows () ? "clang.exe" : "clang"); + Directory.CreateDirectory (directory); + string objectPath = Path.ChangeExtension (Path.Combine (directory, name), ".o"); + string sourcePath = Path.ChangeExtension (objectPath, ".s"); + byte [] blob = CreateGroups (groups.Select (group => group.Keys).ToArray ()); + var source = new StringBuilder (""" + .section .rodata,"a",%progbits + .globl __external_type_map__ + .hidden __external_type_map__ + .type __external_type_map__,%object + __external_type_map__: + + """); + for (int i = 0; i < blob.Length; i += 32) { + source.Append (".byte "); + source.AppendJoin (",", blob.Skip (i).Take (32).Select (b => "0x" + b.ToString ("x2", CultureInfo.InvariantCulture))); + source.Append ('\n'); + } + source.Append (".size __external_type_map__, . - __external_type_map__\n"); + source.Append (""" + .balign 4 + .globl __external_CommonFixupsTable_references + .type __external_CommonFixupsTable_references,%object + __external_CommonFixupsTable_references: + + """); + foreach (var group in groups) { + source.Append (".long ").Append (group.Symbol).Append (" - .\n"); + } + source.Append (""" + .size __external_CommonFixupsTable_references, . - __external_CommonFixupsTable_references + .section .data,"aw",%progbits + + """); + foreach (string symbol in groups.Select (group => group.Symbol).Distinct (StringComparer.Ordinal)) { + source.Append (".globl ").Append (symbol).Append ('\n'); + source.Append (".type ").Append (symbol).Append (",%object\n"); + source.Append (symbol).Append (":\n.byte 0\n"); + source.Append (".size ").Append (symbol).Append (", 1\n"); + } + File.WriteAllText (sourcePath, source.ToString (), new UTF8Encoding (false)); + + using var stdout = new StringWriter (CultureInfo.InvariantCulture); + using var stderr = new StringWriter (CultureInfo.InvariantCulture); + var startInfo = ProcessUtils.CreateProcessStartInfo (clang, + "--target=aarch64-linux-android", "-c", "-x", "assembler", sourcePath, "-o", objectPath); + int exitCode = ProcessUtils.StartProcess (startInfo, stdout, stderr, CancellationToken.None).GetAwaiter ().GetResult (); + if (exitCode != 0) { + throw new InvalidOperationException ($"clang exited with code {exitCode}: {stderr}{stdout}"); + } + return objectPath; + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/NativeAotObjectTestFixture.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/NativeAotObjectTestFixture.cs new file mode 100644 index 00000000000..83166246ab5 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/NativeAotObjectTestFixture.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +internal static partial class NativeAotObjectTestFixture +{ + internal static byte [] CreateBlob (params string [] keys) => CreateGroups (keys); + + internal static byte [] CreateGroups (params string [] [] groups) => + CreateTable (groups.Select ((keys, index) => + CreateGroup (CreateTable (keys.Select (key => CreateKey (key)).ToArray ()), typeIndex: (uint) index)).ToArray ()); + + internal static byte [] CreateGroup (byte [] inner, uint state = 1, uint typeIndex = 0) => + [.. EncodeUnsigned (typeIndex), .. EncodeUnsigned (state), .. inner]; + + internal static byte [] CreateKey (string key, uint typeIndex = 0) + { + byte [] utf8 = new UTF8Encoding (false, true).GetBytes (key); + return [.. EncodeUnsigned ((uint) utf8.Length), .. utf8, .. EncodeUnsigned (typeIndex)]; + } + + internal static byte [] CreateTable (byte [] [] payloads, int indexWidth = 1, int bucketShift = 0, int relativeWidth = 5, bool backwards = false) + { + int bucketCount = 1 << bucketShift; + int indexTag = indexWidth switch { 1 => 0, 2 => 1, 4 => 2, _ => throw new ArgumentOutOfRangeException (nameof (indexWidth)) }; + var bytes = new List { (byte) ((bucketShift << 2) | indexTag) }; + bytes.AddRange (new byte [(bucketCount + 1) * indexWidth]); + var targets = new int [payloads.Length]; + var pointers = new List<(int address, int target)> (); + if (backwards) { + AppendPayloads (); + } + for (int bucket = 0; bucket <= bucketCount; bucket++) { + uint index = checked ((uint) (bytes.Count - 1)); + if (indexWidth < 4 && index >= 1u << (8 * indexWidth)) { + throw new ArgumentOutOfRangeException (nameof (indexWidth), "Fixture bucket offset does not fit."); + } + for (int i = 0; i < indexWidth; i++) { + bytes [1 + bucket * indexWidth + i] = (byte) (index >> (8 * i)); + } + if (bucket == bucketCount) { + break; + } + for (int entry = bucket; entry < payloads.Length; entry += bucketCount) { + bytes.Add ((byte) entry); + pointers.Add ((bytes.Count, entry)); + bytes.AddRange (new byte [relativeWidth]); + } + } + if (!backwards) { + AppendPayloads (); + } + foreach (var (address, target) in pointers) { + // NativeFormat offsets are relative to the integer's address, not its end. + int relative = targets [target] - address; + int bits = relativeWidth == 5 ? 32 : relativeWidth * 7; + if (bits < 32 && (relative < -(1L << (bits - 1)) || relative >= 1L << (bits - 1))) { + throw new ArgumentOutOfRangeException (nameof (relativeWidth), "Fixture relative offset does not fit."); + } + byte [] encoded = EncodeInteger (unchecked ((uint) relative), relativeWidth); + for (int i = 0; i < encoded.Length; i++) { + bytes [address + i] = encoded [i]; + } + } + return bytes.ToArray (); + + void AppendPayloads () + { + for (int i = 0; i < payloads.Length; i++) { + targets [i] = bytes.Count; + bytes.AddRange (payloads [i]); + } + } + } + + internal static byte [] EncodeUnsigned (uint value) + { + int width = value < 1u << 7 ? 1 : value < 1u << 14 ? 2 : value < 1u << 21 ? 3 : value < 1u << 28 ? 4 : 5; + return EncodeInteger (value, width); + } + + static byte [] EncodeInteger (uint value, int width) + { + var result = new byte [width]; + if (width == 5) { + result [0] = 15; + for (int i = 0; i < 4; i++) { + result [i + 1] = (byte) (value >> (8 * i)); + } + } else { + uint encoded = (value << width) | ((1u << (width - 1)) - 1); + for (int i = 0; i < width; i++) { + result [i] = (byte) (encoded >> (8 * i)); + } + } + return result; + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapProguardTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapProguardTests.cs new file mode 100644 index 00000000000..dd50a567cda --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapProguardTests.cs @@ -0,0 +1,619 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Android.Tasks; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class TypeMapProguardTests : IDisposable +{ + readonly string directory = Path.Combine (Path.GetTempPath (), "typemap-proguard-" + Guid.NewGuid ().ToString ("N")); + readonly TypeMapTaskBuildEngine engine = new (); + + public TypeMapProguardTests () => Directory.CreateDirectory (directory); + + [Fact] + public void GeneratorUnionsCanonicalKeysAndWritesOnlyClassRules () + { + var first = Write ("first.keys", "test/Outer$Inner\r\nandroid/app/Activity\r\n\r\ntest/Caf\u00e9\n"); + var second = Write ("second.keys", "android/app/Activity\ntest/\U00010428Peer\n"); + var task = CreateGenerator (first, second); + Assert.True (task.Execute ()); + var expected = "-keep class android.app.Activity\n-keep class test.Caf\u00e9\n-keep class test.Outer$Inner\n-keep class test.\U00010428Peer\n"; + Assert.Equal (new UTF8Encoding (false).GetBytes (expected), File.ReadAllBytes (task.OutputFile)); + } + + [Theory] + [InlineData ("CoreCLR", "true", "disabled", "proguard-android-optimize.txt")] + [InlineData ("CoreCLR", "true", "private-members", "proguard-android-optimize.txt")] + [InlineData ("CoreCLR", "false", "disabled", "proguard-android.txt")] + [InlineData ("CoreCLR", "false", "private-members", "proguard-android-optimize.txt")] + [InlineData ("NativeAOT", "true", "disabled", "proguard-android.txt")] + [InlineData ("NativeAOT", "true", "private-members", "proguard-android.txt")] + [InlineData ("MonoVM", "false", "disabled", "proguard-android.txt")] + public void PlatformConfigurationSeparatesCoreClrOptimizationFromObfuscation (string runtime, string typemap, string obfuscation, string expected) + { + var source = XDocument.Load (Path.Combine (RepositoryDirectory (), "src", "Xamarin.Android.Build.Tasks", "Xamarin.Android.Common.targets")); + var target = new XElement (source.Descendants ().Single (element => element.Name.LocalName == "Target" && (string?) element.Attribute ("Name") == "_CalculateProguardConfigurationFiles")); + foreach (var element in target.DescendantsAndSelf ()) { + element.Name = element.Name.LocalName; + } + var project = Path.Combine (directory, "configuration.proj"); + new XDocument (new XElement ("Project", + new XElement ("PropertyGroup", + new XElement ("_AndroidRuntime", runtime), + new XElement ("_AndroidUseTypeMapProguardConfiguration", typemap), + new XElement ("AndroidR8ObfuscationMode", obfuscation), + new XElement ("AndroidLinkTool", "r8"), + new XElement ("IntermediateOutputPath", "obj/")), + target, + new XElement ("Target", new XAttribute ("Name", "Build"), new XAttribute ("DependsOnTargets", "_CalculateProguardConfigurationFiles"), + new XElement ("WriteLinesToFile", new XAttribute ("File", "$(MSBuildProjectDirectory)/configurations.txt"), + new XAttribute ("Lines", "@(_ProguardConfiguration)"), new XAttribute ("Overwrite", "true"))))) + .Save (project); + Build (project); + var configurations = File.ReadAllLines (Path.Combine (directory, "configurations.txt")); + Assert.Single (configurations, path => Path.GetFileName (path).StartsWith ("proguard-android", StringComparison.Ordinal)); + Assert.Contains (configurations, path => Path.GetFileName (path) == expected); + Build (project, "-p:ProguardConfigFiles=custom.cfg"); + Assert.Equal (["custom.cfg"], File.ReadAllLines (Path.Combine (directory, "configurations.txt"))); + } + + [Fact] + public void MemberGeneratorScopesRulesToCanonicalKeys () + { + var task = new GenerateTypeMapMemberProguardConfiguration { + BuildEngine = engine, + TypeMapKeyFiles = [ + new TaskItem (Write ("first.keys", "test/Peer\ntest/Contract\n")), + new TaskItem (Write ("second.keys", "test/Peer\ntest/Base\n")), + ], + OutputFile = Path.Combine (directory, "members.cfg"), + }; + Assert.True (task.Execute ()); + Assert.Equal ("-keepclassmembers class test.Base { *; }\n-keepclassmembers class test.Contract { *; }\n-keepclassmembers class test.Peer { *; }\n", + File.ReadAllText (task.OutputFile)); + } + + [Fact] + public void MemberGeneratorRejectsInvalidKeysWithoutOverwritingOutput () + { + var output = Write ("members.cfg", "previous output"); + var task = new GenerateTypeMapMemberProguardConfiguration { + BuildEngine = engine, + TypeMapKeyFiles = [new TaskItem (Write ("invalid.keys", "test/Peer\ntest/*\n"))], + OutputFile = output, + }; + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, error => error.Code == "XA4328"); + Assert.Equal ("previous output", File.ReadAllText (output)); + } + + [Theory] + [InlineData ("test/*")] + [InlineData ("test/Foo { *; }")] + [InlineData ("test/Foo[1]")] + [InlineData ("test.Foo")] + [InlineData ("test//Foo")] + [InlineData ("/test/Foo")] + [InlineData ("test/Foo/")] + [InlineData ("test/Foo;")] + [InlineData ("test/Foo\n-keep class **")] + [InlineData ("\ufefftest/Foo")] + public void GeneratorRejectsRuleInjectionAndNoncanonicalKeys (string input) + { + var task = CreateGenerator (Write ("invalid.keys", input)); + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, error => error.Code == "XA4328"); + Assert.False (File.Exists (task.OutputFile)); + } + + [Fact] + public void EmptyMapOverwritesAndRefreshesRealOutput () + { + var task = CreateGenerator (Write ("empty.keys", "")); + Assert.True (task.Execute ()); + var old = DateTime.UtcNow.AddDays (-1); + File.SetLastWriteTimeUtc (task.OutputFile, old); + Assert.True (task.Execute ()); + Assert.Empty (File.ReadAllBytes (task.OutputFile)); + Assert.True (File.GetLastWriteTimeUtc (task.OutputFile) > old); + } + + [Fact] + public void MissingMapIsAnError () + { + var task = CreateGenerator (Path.Combine (directory, "missing.keys")); + Assert.False (task.Execute ()); + Assert.Contains (engine.Errors, error => error.Code == "XA4328"); + } + + [Fact] + public void LlvmTargetTracksAbiUnionInputListAndDeletedOutputs () + { + Write ("first.ll", "@java_type_names = dso_local local_unnamed_addr constant [10 x i8] c\"test/Live\\00\", align 1\n"); + Write ("second.ll", "@java_type_names = dso_local local_unnamed_addr constant [12 x i8] c\"test/Second\\00\", align 1\n"); + var project = CreateProject ("CoreCLR", "llvm-ir", + new XElement ("_TypeMapAssemblySource", new XAttribute ("Include", "$(MSBuildProjectDirectory)/first.ll")), + new XElement ("_TypeMapAssemblySource", new XAttribute ("Include", "$(MSBuildProjectDirectory)/second.ll"), + new XAttribute ("Condition", "'$(OneAbi)' != 'true'"))); + Build (project); + var keys = Path.Combine (directory, "obj", "typemap.keys.txt"); + var rules = Path.Combine (directory, "obj", "proguard", "proguard_project_references.cfg"); + var members = Path.Combine (directory, "obj", "proguard", "proguard_typemap_members.cfg"); + Assert.Equal ("-keep class test.Live\n-keep class test.Second\n", File.ReadAllText (rules)); + Assert.Equal ("-keepclassmembers class test.Live { *; }\n-keepclassmembers class test.Second { *; }\n", File.ReadAllText (members)); + var firstTime = File.GetLastWriteTimeUtc (keys); + var rulesTime = File.GetLastWriteTimeUtc (rules); + var membersTime = File.GetLastWriteTimeUtc (members); + Build (project); + Assert.Equal (firstTime, File.GetLastWriteTimeUtc (keys)); + Assert.Equal (rulesTime, File.GetLastWriteTimeUtc (rules)); + Assert.Equal (membersTime, File.GetLastWriteTimeUtc (members)); + Assert.Contains (members, File.ReadAllText (Path.Combine (directory, "writes.txt"))); + + Build (project, "-p:OneAbi=true"); + Assert.Equal ("test/Live\n", File.ReadAllText (keys)); + Assert.Equal ("-keep class test.Live\n", File.ReadAllText (rules)); + Assert.Equal ("-keepclassmembers class test.Live { *; }\n", File.ReadAllText (members)); + File.Delete (members); + Build (project, "-p:OneAbi=true"); + Assert.Equal ("-keepclassmembers class test.Live { *; }\n", File.ReadAllText (members)); + File.Delete (keys); + Build (project, "-p:OneAbi=true"); + Assert.Equal ("test/Live\n", File.ReadAllText (keys)); + File.Delete (rules); + Build (project, "-p:OneAbi=true"); + Assert.Equal ("-keep class test.Live\n", File.ReadAllText (rules)); + Write ("first.ll", "@java_type_names = dso_local local_unnamed_addr constant [13 x i8] c\"test/Changed\\00\", align 1\n"); + Build (project, "-p:OneAbi=true"); + Assert.Equal ("-keep class test.Changed\n", File.ReadAllText (rules)); + Assert.Equal ("-keepclassmembers class test.Changed { *; }\n", File.ReadAllText (members)); + + var writes = File.ReadAllText (Path.Combine (directory, "writes.txt")); + Assert.Contains ("typemap.keys.txt", writes); + Assert.Contains ("typemap.keys.inputs", writes); + Assert.Contains ("proguard_project_references.cfg", writes); + Assert.Contains ("proguard_typemap_members.cfg", writes); + } + + [Fact] + public void RelativeTaskAssemblyPathDoesNotInvalidateIncrementalOutputs () + { + Write ("first.ll", "@java_type_names = dso_local local_unnamed_addr constant [10 x i8] c\"test/Live\\00\", align 1\n"); + var project = CreateProject ("CoreCLR", "llvm-ir", + new XElement ("_TypeMapAssemblySource", new XAttribute ("Include", "$(MSBuildProjectDirectory)/first.ll"))); + var targetsDirectory = Path.Combine (RepositoryDirectory (), "src", "Xamarin.Android.Build.Tasks", "Microsoft.Android.Sdk", "targets"); + var assembly = Path.GetRelativePath (targetsDirectory, typeof (GenerateTypeMapProguardConfiguration).Assembly.Location); + var argument = "-p:_MicrosoftAndroidBuildTasksAssembly=" + assembly; + Build (project, argument); + var keys = Path.Combine (directory, "obj", "typemap.keys.txt"); + var members = Path.Combine (directory, "obj", "proguard", "proguard_typemap_members.cfg"); + var keysTime = File.GetLastWriteTimeUtc (keys); + var membersTime = File.GetLastWriteTimeUtc (members); + Build (project, argument); + Assert.Equal (keysTime, File.GetLastWriteTimeUtc (keys)); + Assert.Equal (membersTime, File.GetLastWriteTimeUtc (members)); + } + + [NativeAotObjectFact] + public void NativeObjectTargetUnionsRidsAndHonorsDisabledTrimming () + { + var first = WriteNativeObject ("first", "test/Live", "test/Outer$Inner[0]"); + var second = WriteNativeObject ("second", "test/Second", "test/Outer$Inner[1]"); + Write ("acw-map.txt", "App.Live, App;test.Live\nApp.Second, App;test.Second\nApp.Dead, App;test.Dead\n"); + var project = CreateProject ("NativeAOT", "trimmable", + NativeObjectItem ("first.so", first), + NativeObjectItem ("second.so", second)); + Build (project, "-p:_AndroidEnableTypemapR8Trimming=true"); + var rules = Path.Combine (directory, "obj", "proguard", "proguard_project_references.cfg"); + Assert.Equal ("-keep class test.Live\n-keep class test.Outer$Inner\n-keep class test.Second\n", File.ReadAllText (rules)); + Assert.False (File.Exists (Path.Combine (directory, "obj", "proguard", "proguard_typemap_members.cfg"))); + Build (project, "-p:_AndroidEnableTypemapR8Trimming=false"); + Assert.Contains ("legacy ACW configuration", File.ReadAllText (rules)); + File.Delete (second); + var output = Build (project, false, "-p:_AndroidEnableTypemapR8Trimming=true"); + Assert.Contains ("XA4327", output); + } + + [NativeAotObjectFact] + public void NativeObjectTargetSelectsJavaGroupsWithoutGuessingFromKeys () + { + var nativeObject = NativeAotObjectTestFixture.WriteObjectGroups ( + directory, "groups", NativeAotObjectIntegrationTools.LlvmReadObjPath, + ("_ZTV43Mono_Android_Android_Runtime_JavaDictionary", + ["System.Collections.Generic.IDictionary`2[System.Char,System.Int32]", "foreign/LooksLikeJava"]), + ("_ZTV29Mono_Android_Java_Lang_Object", + ["test/Live", "[Ljava/lang/Object;", "[I"])); + var project = CreateProject ("NativeAOT", "trimmable", NativeObjectItem ("app.so", nativeObject)); + Build (project, "-p:_AndroidEnableTypemapR8Trimming=true"); + var rules = Path.Combine (directory, "obj", "proguard", "proguard_project_references.cfg"); + Assert.Equal ("-keep class java.lang.Object\n-keep class test.Live\n", File.ReadAllText (rules)); + + NativeAotObjectTestFixture.WriteObjectGroups ( + directory, "groups", NativeAotObjectIntegrationTools.LlvmReadObjPath, + ("_ZTV29Mono_Android_Java_Lang_Object", ["test/*"])); + Assert.Contains ("XA4327", Build (project, false, "-p:_AndroidEnableTypemapR8Trimming=true")); + } + + [NativeAotObjectFact] + public void RuntimeSwitchCannotReuseAnotherRepresentation () + { + Write ("first.ll", "@java_type_names = dso_local local_unnamed_addr constant [10 x i8] c\"test/Llvm\\00\", align 1\n"); + var nativeObject = WriteNativeObject ("app", "test/Native"); + var project = CreateProject ("CoreCLR", "llvm-ir", + new XElement ("_TypeMapAssemblySource", new XAttribute ("Include", "$(MSBuildProjectDirectory)/first.ll")), + NativeObjectItem ("app.so", nativeObject)); + var rules = Path.Combine (directory, "obj", "proguard", "proguard_project_references.cfg"); + var members = Path.Combine (directory, "obj", "proguard", "proguard_typemap_members.cfg"); + Build (project); + Assert.Equal ("-keep class test.Llvm\n", File.ReadAllText (rules)); + Assert.Contains ("Members=" + members, File.ReadAllText (Path.Combine (directory, "writes.txt"))); + Build (project, "-p:_AndroidRuntime=NativeAOT", "-p:AndroidTypeMapImplementation=trimmable", "-p:_AndroidEnableTypemapR8Trimming=true"); + Assert.Equal ("-keep class test.Native\n", File.ReadAllText (rules)); + Assert.DoesNotContain ("Members=" + members, File.ReadAllText (Path.Combine (directory, "writes.txt"))); + Build (project); + Assert.Equal ("-keep class test.Llvm\n", File.ReadAllText (rules)); + Assert.Equal ("-keepclassmembers class test.Llvm { *; }\n", File.ReadAllText (members)); + } + + [Fact] + public void CompleteConfigurationOverridePreservesLegacyR8Policy () + { + Write ("first.ll", "@java_type_names = dso_local local_unnamed_addr constant [10 x i8] c\"test/Live\\00\", align 1\n"); + var project = CreateProject ("CoreCLR", "llvm-ir", + new XElement ("_TypeMapAssemblySource", new XAttribute ("Include", "$(MSBuildProjectDirectory)/first.ll"))); + Build (project); + Assert.Contains ("UseTypeMap=true", File.ReadAllText (Path.Combine (directory, "writes.txt"))); + Build (project, "-p:ProguardConfigFiles=custom.cfg"); + Assert.DoesNotContain ("UseTypeMap=true", File.ReadAllText (Path.Combine (directory, "writes.txt"))); + Assert.DoesNotContain ("Members=" + Path.Combine (directory, "obj"), File.ReadAllText (Path.Combine (directory, "writes.txt"))); + } + + [Theory] + [InlineData ("CoreCLR", "llvm-ir")] + [InlineData ("CoreCLR", "trimmable")] + [InlineData ("NativeAOT", "trimmable")] + public void DisabledPipelineNeedsNoTypemapInputsOrModernTaskAssembly (string runtime, string representation) + { + Write ("acw-map.txt", "App.Live, App;test.Live\n"); + var project = CreateProject (runtime, representation); + Build (project, "-p:_AndroidEnableTypemapR8Trimming=false", "-p:_MicrosoftAndroidBuildTasksAssembly=missing.dll"); + Assert.False (File.Exists (Path.Combine (directory, "obj", "typemap.keys.txt"))); + Assert.False (File.Exists (Path.Combine (directory, "obj", "proguard", "proguard_typemap_members.cfg"))); + Assert.DoesNotContain ("UseTypeMap=true", File.ReadAllText (Path.Combine (directory, "writes.txt"))); + if (runtime == "NativeAOT") { + Assert.Equal ("# Class roots are supplied by the legacy ACW configuration.", + File.ReadAllText (Path.Combine (directory, "obj", "proguard", "proguard_project_references.cfg")).Trim ()); + } + } + + [NativeAotObjectTheory] + [InlineData ("CoreCLR", "llvm-ir")] + [InlineData ("NativeAOT", "trimmable")] + public void EnablingDisablingAndUnsettingCannotReuseLegacyRules (string runtime, string representation) + { + Write ("first.ll", "@java_type_names = dso_local local_unnamed_addr constant [10 x i8] c\"test/Live\\00\", align 1\n"); + var nativeObject = WriteNativeObject ("app", "test/Live"); + Write ("acw-map.txt", "App.Live, App;test.Live\n"); + var project = CreateProject (runtime, representation, + new XElement ("_TypeMapAssemblySource", new XAttribute ("Include", "$(MSBuildProjectDirectory)/first.ll")), + NativeObjectItem ("app.so", nativeObject)); + var keys = Path.Combine (directory, "obj", "typemap.keys.txt"); + var rules = Path.Combine (directory, "obj", "proguard", "proguard_project_references.cfg"); + foreach (var enabled in new [] { "true", "" }) { + Build (project, "-p:_AndroidEnableTypemapR8Trimming=true"); + var keysTime = File.GetLastWriteTimeUtc (keys); + Build (project, "-p:_AndroidEnableTypemapR8Trimming=false"); + Assert.Equal (keysTime, File.GetLastWriteTimeUtc (keys)); + Assert.DoesNotContain ("UseTypeMap=true", File.ReadAllText (Path.Combine (directory, "writes.txt"))); + // The CoreCLR legacy producer runs in the RID inner build, outside this fixture. + if (runtime == "CoreCLR") { + File.WriteAllText (rules, "# legacy rules"); + } + Assert.DoesNotContain ("-keep class test.Live", File.ReadAllText (rules)); + Build (project, "-p:_AndroidEnableTypemapR8Trimming=" + enabled); + if (runtime == "NativeAOT" && enabled == "") { + Assert.Contains ("legacy ACW configuration", File.ReadAllText (rules)); + Assert.DoesNotContain ("UseTypeMap=true", File.ReadAllText (Path.Combine (directory, "writes.txt"))); + } else { + Assert.Equal ("-keep class test.Live\n", File.ReadAllText (rules)); + Assert.Contains ("UseTypeMap=true", File.ReadAllText (Path.Combine (directory, "writes.txt"))); + } + } + } + + [Theory] + [InlineData ("false", "false")] + [InlineData ("false", "true")] + [InlineData ("true", "false")] + [InlineData ("true", "true")] + public void TypemapInputsDoNotRequestGraphsOrChangeParallelism (string enabled, string diagnostics) + { + var project = CreateProject ("NativeAOT", "trimmable"); + var document = XDocument.Load (project); + var root = document.Root ?? throw new InvalidOperationException (); + root.Add (new XElement ("Import", new XAttribute ("Project", + Path.Combine (RepositoryDirectory (), "src", "Xamarin.Android.Build.Tasks", "Microsoft.Android.Sdk", "targets", "Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets")))); + root.Add (new XElement ("Target", new XAttribute ("Name", "_ReadGeneratedTrimmableTypeMapAssemblies"))); + root.Add (new XElement ("Target", new XAttribute ("Name", "Build"), + new XAttribute ("DependsOnTargets", "_AddTrimmableTypeMapAssembliesToIlc"), + new XElement ("WriteLinesToFile", new XAttribute ("File", "$(MSBuildProjectDirectory)/ilc.txt"), + new XAttribute ("Lines", "@(IlcArg);Diagnostics=$(IlcGenerateDgmlFile);Parallel=$(_AndroidBuildRuntimeIdentifiersInParallel)"), + new XAttribute ("Overwrite", "true")))); + document.Save (project); + Build (project, "-p:_AndroidEnableTypemapR8Trimming=" + enabled, "-p:IlcGenerateDgmlFile=" + diagnostics, "-p:Optimize=true"); + var lines = File.ReadAllLines (Path.Combine (directory, "ilc.txt")); + var output = string.Join ("\n", lines); + Assert.DoesNotContain ("--scandgmllog:", output); + Assert.DoesNotContain ("--dgmllog:", output); + Assert.Contains ("Parallel=", lines); + Assert.Contains ("Diagnostics=" + diagnostics, output); + } + + [NativeAotObjectFact] + public void UnoptimizedNativeAotReadsObjectWithoutGraphs () + { + var nativeObject = WriteNativeObject ("app", "test/Live"); + var project = CreateProject ("NativeAOT", "trimmable", + NativeObjectItem ("app.so", nativeObject)); + var keys = Path.Combine (directory, "obj", "typemap.keys.txt"); + var rules = Path.Combine (directory, "obj", "proguard", "proguard_project_references.cfg"); + Build (project, "-p:Optimize=false", "-p:_AndroidEnableTypemapR8Trimming=true"); + Assert.Equal ("test/Live\n", File.ReadAllText (keys)); + Assert.Equal ("-keep class test.Live\n", File.ReadAllText (rules)); + } + + [Fact] + public void NativeAotWithoutOptInDoesNotRequireObjectTools () + { + Write ("acw-map.txt", "App.Live, App;test.Live\n"); + var project = CreateProject ("NativeAOT", "trimmable"); + Build (project, "-p:_MicrosoftAndroidBuildTasksAssembly=missing.dll"); + Assert.False (File.Exists (Path.Combine (directory, "obj", "typemap.keys.txt"))); + Assert.DoesNotContain ("UseTypeMap=true", File.ReadAllText (Path.Combine (directory, "writes.txt"))); + } + + [Theory] + [InlineData ("NativeAOT", "true", "r8", "true", "true")] + [InlineData ("NativeAOT", "false", "r8", "true", "false")] + [InlineData ("NativeAOT", "", "r8", "true", "false")] + [InlineData ("NativeAOT", "true", "", "true", "false")] + [InlineData ("NativeAOT", "true", "r8", "false", "false")] + [InlineData ("CoreCLR", "true", "r8", "true", "false")] + public void NdkDependencyRequiresNativeObjectOptIn (string runtime, string enabled, string linkTool, string trimmed, string expected) + { + var common = XDocument.Load (Path.Combine (RepositoryDirectory (), "src", "Xamarin.Android.Build.Tasks", "Xamarin.Android.Common.targets")); + XNamespace ns = "http://schemas.microsoft.com/developer/msbuild/2003"; + var dependencyProperties = common.Root?.Elements (ns + "Target") + .Single (target => (string?) target.Attribute ("Name") == "GetAndroidDependencies") + .Element (ns + "PropertyGroup") ?? throw new InvalidOperationException (); + var path = Path.Combine (directory, "dependencies.proj"); + new XDocument (new XElement (ns + "Project", + new XElement (ns + "PropertyGroup", + new XElement (ns + "_AndroidRuntime", runtime), + new XElement (ns + "AndroidTypeMapImplementation", "trimmable"), + new XElement (ns + "_AndroidEnableTypemapR8Trimming", enabled), + new XElement (ns + "_AndroidUseWorkloadNativeLinker", "true"), + new XElement (ns + "PublishAot", "true"), + new XElement (ns + "PublishTrimmed", trimmed), + new XElement (ns + "AndroidLinkTool", linkTool)), + new XElement (ns + "Target", new XAttribute ("Name", "Build"), + new XElement (dependencyProperties), + new XElement (ns + "WriteLinesToFile", new XAttribute ("File", "$(MSBuildProjectDirectory)/ndk-required.txt"), + new XAttribute ("Lines", "$(_NdkRequired)"), new XAttribute ("Overwrite", "true"))))) + .Save (path); + Build (path); + Assert.Equal (expected, File.ReadAllText (Path.Combine (directory, "ndk-required.txt")).Trim ()); + } + + [Fact] + public void InnerBuildDoesNotGenerateOuterClassRules () + { + var project = CreateProject ("CoreCLR", "trimmable"); + Build (project, "-p:_ComputeFilesToPublishForRuntimeIdentifiers=true"); + Assert.False (File.Exists (Path.Combine (directory, "obj", "typemap.keys.txt"))); + } + + [Fact] + public void AssemblyTargetConsumesLinkedMetadataAcrossRidsAndEmptyStubs () + { + string EmitMap (string name, params string [] keys) + { + var path = Path.Combine (directory, name + ".dll"); + var model = new TypeMapAssemblyData { AssemblyName = name, ModuleName = name + ".dll" }; + foreach (var key in keys) { + model.Entries.Add (new TypeMapAttributeData { + MapKey = key, ProxyTypeReference = "System.Object, System.Runtime", + }); + } + using var stream = File.Create (path); + new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)).Emit (model, stream); + return path; + } + var first = EmitMap ("first", "test/Live", "test/Alias[0]"); + var second = EmitMap ("second", "test/Second", "test/Alias[1]"); + var stub = EmitMap ("stub"); + var project = CreateProject ("CoreCLR", "trimmable", + new XElement ("ResolvedFileToPublish", new XAttribute ("Include", "arm64/R2R/root.dll"), + new XAttribute ("AndroidTypeMapLinkedAssemblies", first + ";" + stub)), + new XElement ("ResolvedFileToPublish", new XAttribute ("Include", "x64/R2R/root.dll"), + new XAttribute ("AndroidTypeMapLinkedAssemblies", second)), + new XElement ("ResolvedFileToPublish", new XAttribute ("Include", "pretrim/unused.dll"))); + Build (project); + var rules = Path.Combine (directory, "obj", "proguard", "proguard_project_references.cfg"); + Assert.Equal ("-keep class test.Alias\n-keep class test.Live\n-keep class test.Second\n", File.ReadAllText (rules)); + File.Delete (second); + Assert.Contains ("XA4327", Build (project, expectSuccess: false)); + } + + [Theory] + [InlineData ("NativeAOT", "true", "custom-object/retained.o")] + [InlineData ("NativeAOT", "false", "custom-object/retained.o")] + [InlineData ("CoreCLR", "true", "obj/linked/_Binding.TypeMap.dll")] + public void InnerBuildReturnsExactProducerPaths (string runtime, string optimize, string expected) + { + var project = CreateProject (runtime, "trimmable"); + var document = XDocument.Load (project); + var root = document.Root ?? throw new InvalidOperationException (); + root.AddFirst (new XElement ("Import", new XAttribute ("Project", + Path.Combine (RepositoryDirectory (), "src", "Xamarin.Android.Build.Tasks", "Microsoft.Android.Sdk", "targets", "Microsoft.Android.Sdk.AssemblyResolution.targets")))); + root.Add (new XElement ("PropertyGroup", + new XElement ("RuntimeIdentifier", "android-x64"), + new XElement ("NativeIntermediateOutputPath", "$(MSBuildProjectDirectory)/custom-native/"), + new XElement ("NativeObject", "$(MSBuildProjectDirectory)/custom-object/retained.o"), + new XElement ("_NdkBinDir", "$(MSBuildProjectDirectory)/custom-ndk-bin/"), + new XElement ("TargetName", "App"), + new XElement ("Optimize", optimize), + new XElement ("_AndroidEnableTypemapR8Trimming", "true"), + new XElement ("_TypeMapAssemblyName", "_Microsoft.Android.TypeMaps"))); + foreach (var target in new [] { "BuildOnlySettings", "_CheckForInvalidConfigurationAndPlatform", "_FixupIntermediateAssembly", "_PatchNuGetReferenceMetadata", "ResolveReferences", "_AndroidAot" }) { + root.Add (new XElement ("Target", new XAttribute ("Name", target))); + } + root.Add (new XElement ("Target", new XAttribute ("Name", "ComputeFilesToPublish"), + new XElement ("ItemGroup", + new XElement ("_GeneratedTypeMapAssembliesFromList", new XAttribute ("Include", "generated/_Binding.TypeMap.dll")), + new XElement ("ResolvedFileToPublish", new XAttribute ("Include", "R2R/_Microsoft.Android.TypeMaps.dll"))))); + var metadata = runtime == "NativeAOT" ? "AndroidTypeMapNativeObject" : "AndroidTypeMapLinkedAssemblies"; + root.Add (new XElement ("Target", new XAttribute ("Name", "Build"), + new XAttribute ("DependsOnTargets", "_ComputeFilesToPublishForRuntimeIdentifiers"), + new XElement ("WriteLinesToFile", new XAttribute ("File", "$(MSBuildProjectDirectory)/producer.txt"), + new XAttribute ("Lines", $"@(ResolvedFileToPublish->'%({metadata})')"), + new XAttribute ("Overwrite", "true")), + new XElement ("WriteLinesToFile", new XAttribute ("File", "$(MSBuildProjectDirectory)/readobj.txt"), + new XAttribute ("Lines", "@(ResolvedFileToPublish->'%(AndroidTypeMapLlvmReadObjPath)')"), + new XAttribute ("Overwrite", "true")))); + document.Save (project); + Build (project); + Assert.Equal (Path.Combine (directory, expected).Replace ('\\', '/'), + File.ReadAllText (Path.Combine (directory, "producer.txt")).Trim ().Replace ('\\', '/')); + if (runtime == "NativeAOT") { + var executable = OperatingSystem.IsWindows () ? "llvm-readobj.exe" : "llvm-readobj"; + Assert.Equal (Path.Combine (directory, "custom-ndk-bin", executable).Replace ('\\', '/'), + File.ReadAllText (Path.Combine (directory, "readobj.txt")).Trim ().Replace ('\\', '/')); + } + } + + [Theory] + [InlineData ("MonoVM", "llvm-ir", "true", "r8")] + [InlineData ("CoreCLR", "llvm-ir", "false", "r8")] + [InlineData ("CoreCLR", "llvm-ir", "true", "")] + public void InactivePathsDoNotConsumeOrGenerateKeys (string runtime, string representation, string trimmed, string linkTool) + { + var project = CreateProject (runtime, representation); + Build (project, $"-p:PublishTrimmed={trimmed}", $"-p:AndroidLinkTool={linkTool}", "-p:_AndroidEnableTypemapR8Trimming=true"); + Assert.False (File.Exists (Path.Combine (directory, "obj", "typemap.keys.txt"))); + } + + GenerateTypeMapProguardConfiguration CreateGenerator (params string [] inputs) + { + var items = new ITaskItem [inputs.Length]; + for (int i = 0; i < inputs.Length; i++) { + items [i] = new TaskItem (inputs [i]); + } + return new GenerateTypeMapProguardConfiguration { + BuildEngine = engine, + TypeMapKeyFiles = items, + OutputFile = Path.Combine (directory, "rules", "classes.cfg"), + }; + } + + string CreateProject (string runtime, string representation, params XElement [] sourceItems) + { + var targets = Path.Combine (RepositoryDirectory (), "src", "Xamarin.Android.Build.Tasks", "Microsoft.Android.Sdk", "targets", "Microsoft.Android.Sdk.TypeMap.Proguard.targets"); + var path = Path.Combine (directory, "pipeline.proj"); + Directory.CreateDirectory (Path.Combine (directory, "obj")); + new XDocument ( + new XElement ("Project", + new XElement ("PropertyGroup", + new XElement ("_MicrosoftAndroidBuildTasksAssembly", typeof (GenerateTypeMapProguardConfiguration).Assembly.Location), + new XElement ("_XamarinAndroidBuildTasksAssembly", "unused-legacy-tasks.dll"), + new XElement ("_AndroidRuntime", runtime), + new XElement ("AndroidTypeMapImplementation", representation), + new XElement ("PublishTrimmed", "true"), + new XElement ("AndroidLinkTool", "r8"), + new XElement ("Optimize", "true"), + new XElement ("IntermediateOutputPath", "$(MSBuildProjectDirectory)/obj/"), + new XElement ("_AndroidBuildPropertiesCache", "$(MSBuildProjectDirectory)/obj/build.props.cache"), + new XElement ("_AcwMapFile", "$(MSBuildProjectDirectory)/acw-map.txt")), + new XElement ("Import", new XAttribute ("Project", targets)), + new XElement ("Target", new XAttribute ("Name", "_GenerateJavaStubs"), + new XElement ("ItemGroup", sourceItems)), + new XElement ("Target", new XAttribute ("Name", "_CalculateProguardConfigurationFiles")), + new XElement ("Target", new XAttribute ("Name", "_CreatePropertiesCache"), + new XElement ("WriteLinesToFile", new XAttribute ("File", "$(_AndroidBuildPropertiesCache)"), + new XAttribute ("Lines", "Enabled=$(_AndroidEnableTypemapR8Trimming)"), + new XAttribute ("Overwrite", "true"), new XAttribute ("WriteOnlyWhenDifferent", "true"))), + new XElement ("Target", new XAttribute ("Name", "Build"), + new XAttribute ("DependsOnTargets", "_CreatePropertiesCache;_CalculateProguardConfigurationFiles;_AndroidGenerateTypeMapProguardConfiguration"), + new XElement ("WriteLinesToFile", new XAttribute ("File", "$(MSBuildProjectDirectory)/writes.txt"), + new XAttribute ("Lines", "@(FileWrites);UseTypeMap=$(_AndroidUseTypeMapProguardConfiguration);@(_ProguardConfiguration->'Members=%(Identity)')"), new XAttribute ("Overwrite", "true"))))) + .Save (path); + return path; + } + + static string RepositoryDirectory () + { + var repository = new DirectoryInfo (AppContext.BaseDirectory); + while (repository != null && !File.Exists (Path.Combine (repository.FullName, "Configuration.props"))) { + repository = repository.Parent; + } + Assert.NotNull (repository); + return repository.FullName; + } + + string Build (string project, params string [] arguments) => Build (project, true, arguments); + + string Build (string project, bool expectSuccess, params string [] arguments) + { + var start = new ProcessStartInfo ("dotnet") { + WorkingDirectory = directory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + start.Environment ["DOTNET_CLI_USE_MSBUILD_SERVER"] = "0"; + foreach (var argument in new [] { "msbuild", project, "-t:Build", "-nologo", "-v:minimal", "-nr:false" }) { + start.ArgumentList.Add (argument); + } + foreach (var argument in arguments) { + start.ArgumentList.Add (argument); + } + using var process = Process.Start (start) ?? throw new InvalidOperationException ("Could not start MSBuild."); + var stdout = process.StandardOutput.ReadToEndAsync (); + var stderr = process.StandardError.ReadToEndAsync (); + bool completed = process.WaitForExit (120000); + if (!completed) { + process.Kill (entireProcessTree: true); + process.WaitForExit (); + } + var output = stdout.GetAwaiter ().GetResult () + stderr.GetAwaiter ().GetResult (); + Assert.True (completed, "MSBuild did not finish." + Environment.NewLine + output); + Assert.True (expectSuccess ? process.ExitCode == 0 : process.ExitCode != 0, output); + return output; + } + + string Write (string name, string content) + { + var path = Path.Combine (directory, name); + File.WriteAllText (path, content, new UTF8Encoding (false)); + return path; + } + + string WriteNativeObject (string name, params string [] keys) => + NativeAotObjectTestFixture.WriteObject (directory, name, NativeAotObjectIntegrationTools.LlvmReadObjPath, keys); + + static XElement NativeObjectItem (string output, string nativeObject) => + new ("ResolvedFileToPublish", new XAttribute ("Include", output), + new XAttribute ("AndroidTypeMapNativeObject", nativeObject), + new XAttribute ("AndroidTypeMapLlvmReadObjPath", NativeAotObjectIntegrationTools.LlvmReadObjPath), + new XAttribute ("AndroidTypeMapLlvmObjDumpPath", NativeAotObjectIntegrationTools.LlvmObjDumpPath)); + + public void Dispose () => Directory.Delete (directory, recursive: true); +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj index bb8aa7c1a02..efa468f4319 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -25,10 +25,12 @@ + + false @@ -44,6 +46,11 @@ + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NativeAotObjectIntegrationTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NativeAotObjectIntegrationTests.cs new file mode 100644 index 00000000000..73d7302386f --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NativeAotObjectIntegrationTests.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +static class NativeAotObjectIntegrationTools +{ + public static string LlvmReadObjPath => + typeof (NativeAotObjectIntegrationTools).Assembly.GetCustomAttributes () + .Single (attribute => attribute.Key == "NativeAotLlvmReadObjPath").Value ?? ""; + + public static string LlvmObjDumpPath => Path.Combine (Path.GetDirectoryName (LlvmReadObjPath) ?? "", + OperatingSystem.IsWindows () ? "llvm-objdump.exe" : "llvm-objdump"); + + public static string? SkipReason => File.Exists (LlvmReadObjPath) && File.Exists (LlvmObjDumpPath) + ? null + : "Set _NativeAotLlvmReadObjPath to the NDK llvm-readobj executable with adjacent llvm-objdump to run native-object integration tests."; +} + +sealed class NativeAotObjectFactAttribute : FactAttribute +{ + public NativeAotObjectFactAttribute () => Skip = NativeAotObjectIntegrationTools.SkipReason; +} + +sealed class NativeAotObjectTheoryAttribute : TheoryAttribute +{ + public NativeAotObjectTheoryAttribute () => Skip = NativeAotObjectIntegrationTools.SkipReason; +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TypeMapTaskBuildEngine.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TypeMapTaskBuildEngine.cs new file mode 100644 index 00000000000..d8dc9268678 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TypeMapTaskBuildEngine.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Framework; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +sealed class TypeMapTaskBuildEngine : IBuildEngine +{ + public List Errors { get; } = []; + public bool ContinueOnError => false; + public int LineNumberOfTaskNode => 0; + public int ColumnNumberOfTaskNode => 0; + public string ProjectFileOfTaskNode => ""; + public void LogErrorEvent (BuildErrorEventArgs e) => Errors.Add (e); + public void LogWarningEvent (BuildWarningEventArgs e) { } + public void LogMessageEvent (BuildMessageEventArgs e) { } + public void LogCustomEvent (CustomBuildEventArgs e) { } + public bool BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => throw new NotSupportedException (); +}