Skip to content

Commit 86260ed

Browse files
authored
[Xamarin.Android.Build.Tasks] Make all assemblies RID-specific (#8478)
Fixes: #8168 Context: #4337 Context: #8155 Context: 55e5c34 Context: 6836818 Context: 929e701 Context: c927026 Context: 2f19238 Issue #8155 noted a *fundamental* mismatch in expectations between the Classic Xamarin.Android packaging worldview and the .NET worldview: In Classic Xamarin.Android, all assemblies are presumed to be architecture agnostic ("AnyCPU"), while in .NET: 1. `System.Private.CoreLib.dll` was *always* an architecture-specific assembly (see #4337), and 2. The .NET Trimmer is extensible and can apply ABI-specific changes to IL which *effectively* results in an architecture-specific assembly (#8155). Meanwhile, there is no way of knowing that this is happening, and the trimmer doesn't mark the resulting assembly as architecture-specific. We long tried to "paper over" this difference, by trying to find -- and preserve the "nature" of -- architecture-agnostic assemblies (55e5c34, …). Unfortunately, all attempts at trying to preserve the concept of architecture-agnostic assemblies have failed; we're fighting against .NET tooling in attempting to do so. In commit 6836818 this came to a head: a long worked-on feature LLVM Marshal Methods (8bc7a3e) had to be disabled because of hangs within MAUI+Blazor Hybrid+.NET Android apps, and we suspect that treating an assembly as architecture-agnostic when it was "actually" architecture-specific is a plausible culprit. Bite the bullet: there is no longer such a thing as an architecture- agnostic assembly. Treat *all* assemblies as if they were architecture-specific. Additionally, alter assembly packaging so that instead of using `assemblies/assemblies*.blob` files (c927026), we instead store the assemblies within `lib/ABI` of the `.apk`/`.aab`. The Runtime config blob `rc.bin` is stored as `lib/ABI/libarc.bin.so`. When `$(AndroidUseAssemblyStore)`=true, assemblies will be stored within `lib/ABI/libassemblies.ABI.blob.so`, e.g. `lib/arm64-v8a/libassemblies.arm64-v8a.blob.so`. When `$(AndroidUseAssemblyStore)`=false and Fast Deployment is *not* used, then assemblies are stored individually within `lib/ABI` as compressed assembly data, with the following "name mangling" convention: * Regular assemblies: `lib_` + Assembly File Name + `.so` * Satellite assemblies: `lib-` + culture + `-` + Assembly File Name + `.so` For example, consider this selected `unzip -l` output: % unzip -l bin/Release/net9.0-android/*-Signed.apk | grep lib/arm64-v8a 723560 01-01-1981 01:01 lib/arm64-v8a/libSystem.IO.Compression.Native.so 70843 01-01-1981 01:01 lib/arm64-v8a/lib_Java.Interop.dll.so 157256 01-01-1981 01:01 lib/arm64-v8a/libaot-Java.Interop.dll.so 1512 01-01-1981 01:01 lib/arm64-v8a/libarc.bin.so * `libSystem.IO.Compression.Native.so` is a native shared library from .NET * `lib_Java.Interop.dll.so` is compressed assembly data for `Java.Interop.dll` * `libaot-Java.Interop.dll.so` contains Profiled AOT output for `Java.Interop.dll` * `libarc.bin.so` is the `rc.bin` file used by .NET runtime startup Additionally, note that Android limits the characters that can be used in native library filenames to the regex set `[-._A-Za-z0-9]`. TODO: No error checking is done to ensure that "Assembly File Name" stays within the limits of `[-.A-Za-z0-9]`, e.g. if you set `$(AssemblyName)=Emoji😅` *and `$(AndroidUseAssemblyStore)`=false, then we'll try to add `lib/arm64-v8a/lib_Emoji😅.dll.so`, which will fail at runtime. This works when `$(AndroidUseAssemblyStore)`=true, which is the default. Pros: * We're no longer fighting against .NET tooling features such as ILLink Substitutions. * While `.aab` files will get larger, we expect that the actual `.apk` files sent to Android devices from the Google Play Store will be *smaller*, as the Google Play Store would always preserve/transmit *all* `assemblies/assemblies*.blob` files, while now it will be able to remove `lib/ABI/*` for unsupported ABIs. Cons: * `.apk` files containing more than one ABI ***will get larger***, as there will no longer be "de-duping" of architecture-agnostic assembly data. We don't consider this a significant concern, as we believe `.aab` is the predominant packaging format. ~~ All assemblies are architecture-specific ~~ Assembly pre-processing changes so that every assembly ends up in every target architecture batch, regardless of whether its MVID differs from its brethren or not. This is done very early in the build process on our side, where we make sure that each assembly either has the `%(Abi)` metadata or is given one, and is placed in the corresponding batch. Further processing of those batches is "parallel", in that no code attempts to de-duplicate the batches. ~~ Impact on Fast Deployment, `$(IntermediateOutputPath)` ~~ The changes also required us to place all the assemblies in new locations on disk within `$(IntermediateOutputPath)` when building the application. (Related: 2f19238.) Assemblies are now placed in subdirectories named after either the target architecture/ABI or the .NET `$(RuntimeIdentifier)`, e.g. `obj/Release/netX.Y-android/android-arm64`. This, in turn, affects e.g. Fast Deployment as now the synchronized content is in the `…/.__override__/ABI` directory on device, instead of just in `…/.__override__`. ~~ File Formats ~~ The assembly store format (c927026) is updated to use the following structures: struct AssemblyStoreHeader { uint32_t magic; uint32_t version; uint32_t entry_count; // Number of assemblies in the store uint32_2 index_entry_count; uint32_t index_size; }; struct AssemblyStoreIndexEntry { intptr_t name_hash; // xxhash of assembly filename uint32_t descriptor_index; // index into `descriptors` array }; struct AssemblyStoreEntryDescriptor { uint32_t mapping_index; // index into an internal runtime array uint32_t data_offset; // index into `data` for assembly `.dll` uint32_t data_size; // size of assembly, in bytes uint32_t debug_data_offset; // index into `data` for assembly `.pdb`; 0 if not present uint32_t debug_data_size; // size of `.pdb`, in bytes; 0 if not present uint32_t config_data_offset; // index into `data` for assembly `.config`; 0 if not present uint32_t config_data_size; // size of `.config`, in bytes; 0 if not present }; struct AssemblyStoreAssemblyInfo { uint32_t length; // bytes uint8_t name[length]; }; `libassemblies.ABI.blob.so` has the following format, and is *not* a valid ELF file: AssemblyStoreHeader header {…}; AssemblyStoreIndexEntry index [header.index_entry_count]; AssemblyStoreAssemblyDescriptor descriptors [header.entry_count]; AssemblyStoreAssemblyInfo names [header.entry_count]; uint8_t data[];
1 parent ea52f50 commit 86260ed

File tree

88 files changed

+5091
-2980
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+5091
-2980
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<NuGetApiPackageVersion>5.4.0</NuGetApiPackageVersion>
4949
<LZ4PackageVersion>1.1.11</LZ4PackageVersion>
5050
<MonoOptionsVersion>6.12.0.148</MonoOptionsVersion>
51-
<SystemCollectionsImmutableVersion>6.0.0</SystemCollectionsImmutableVersion>
51+
<SystemCollectionsImmutableVersion>8.0.0</SystemCollectionsImmutableVersion>
5252
<SystemRuntimeCompilerServicesUnsafeVersion>6.0.0</SystemRuntimeCompilerServicesUnsafeVersion>
5353
<ELFSharpVersion>2.13.1</ELFSharpVersion>
5454
<HumanizerVersion>2.14.1</HumanizerVersion>

Xamarin.Android.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "decompress-assemblies", "to
109109
EndProject
110110
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tmt", "tools\tmt\tmt.csproj", "{1A273ED2-AE84-48E9-9C23-E978C2D0CB34}"
111111
EndProject
112-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "assembly-store-reader", "tools\assembly-store-reader\assembly-store-reader.csproj", "{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51}"
112+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "assembly-store-reader", "tools\assembly-store-reader-mk2\assembly-store-reader.csproj", "{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51}"
113113
EndProject
114114
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Java.Interop.Tools.JavaTypeSystem", "external\Java.Interop\src\Java.Interop.Tools.JavaTypeSystem\Java.Interop.Tools.JavaTypeSystem.csproj", "{4EFCED6E-9A6B-453A-94E4-CE4B736EC684}"
115115
EndProject

build-tools/scripts/TestApks.targets

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,14 @@
111111
Timeout="60000"
112112
/>
113113
<Xamarin.Android.Tools.BootstrapTasks.Adb
114-
Arguments="$(_AdbTarget) logcat -G 4M"
114+
Arguments="$(_AdbTarget) logcat -G 256M"
115+
IgnoreExitCode="True"
116+
ToolExe="$(AdbToolExe)"
117+
ToolPath="$(AdbToolPath)"
118+
Timeout="60000"
119+
/>
120+
<Xamarin.Android.Tools.BootstrapTasks.Adb
121+
Arguments="$(_AdbTarget) logcat -c"
115122
IgnoreExitCode="True"
116123
ToolExe="$(AdbToolExe)"
117124
ToolPath="$(AdbToolPath)"

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,14 @@ _ResolveAssemblies MSBuild target.
120120
<_ResolvedSymbolFiles Include="@(ResolvedFileToPublish)" Condition=" '%(ResolvedFileToPublish.Extension)' == '.pdb' " />
121121
<_ResolvedJavaLibraries Include="@(ResolvedFileToPublish)" Condition=" '%(ResolvedFileToPublish.Extension)' == '.jar' " />
122122
</ItemGroup>
123+
124+
<!-- All assemblies must be per-RID, thus no `->Distinct()` on `InputAssemblies` or `ResolvedSymbols` items -->
123125
<ProcessAssemblies
124126
RuntimeIdentifiers="@(_RIDs)"
125-
InputAssemblies="@(_ResolvedAssemblyFiles->Distinct())"
127+
DesignTimeBuild="$(DesignTimeBuild)"
128+
InputAssemblies="@(_ResolvedAssemblyFiles)"
126129
InputJavaLibraries="@(_ResolvedJavaLibraries->Distinct())"
127-
ResolvedSymbols="@(_ResolvedSymbolFiles->Distinct())"
130+
ResolvedSymbols="@(_ResolvedSymbolFiles)"
128131
AndroidIncludeDebugSymbols="$(AndroidIncludeDebugSymbols)"
129132
PublishTrimmed="$(PublishTrimmed)">
130133
<Output TaskParameter="OutputAssemblies" ItemName="_ProcessedAssemblies" />

src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs

Lines changed: 178 additions & 109 deletions
Large diffs are not rendered by default.

src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using Microsoft.Build.Framework;
55
using Microsoft.Android.Build.Tasks;
66

7+
using Xamarin.Android.Tools;
8+
79
namespace Xamarin.Android.Tasks
810
{
911
public class GenerateCompressedAssembliesNativeSourceFiles : AndroidTask
@@ -41,38 +43,54 @@ void GenerateCompressedAssemblySources ()
4143
return;
4244
}
4345

44-
var assemblies = new SortedDictionary<string, CompressedAssemblyInfo> (StringComparer.Ordinal);
45-
foreach (ITaskItem assembly in ResolvedAssemblies) {
46-
if (bool.TryParse (assembly.GetMetadata ("AndroidSkipAddToPackage"), out bool value) && value) {
47-
continue;
48-
}
46+
Dictionary<AndroidTargetArch, Dictionary<string, ITaskItem>> perArchAssemblies = MonoAndroidHelper.GetPerArchAssemblies (
47+
ResolvedAssemblies,
48+
SupportedAbis,
49+
validate: true,
50+
shouldSkip: (ITaskItem asm) => bool.TryParse (asm.GetMetadata ("AndroidSkipAddToPackage"), out bool value) && value
51+
);
52+
var archAssemblies = new Dictionary<AndroidTargetArch, Dictionary<string, CompressedAssemblyInfo>> ();
53+
var counters = new Dictionary<AndroidTargetArch, uint> ();
54+
55+
foreach (var kvpPerArch in perArchAssemblies) {
56+
AndroidTargetArch arch = kvpPerArch.Key;
57+
Dictionary<string, ITaskItem> resolvedArchAssemblies = kvpPerArch.Value;
58+
59+
foreach (var kvp in resolvedArchAssemblies) {
60+
ITaskItem assembly = kvp.Value;
61+
62+
if (!archAssemblies.TryGetValue (arch, out Dictionary<string, CompressedAssemblyInfo> assemblies)) {
63+
assemblies = new Dictionary<string, CompressedAssemblyInfo> (StringComparer.OrdinalIgnoreCase);
64+
archAssemblies.Add (arch, assemblies);
65+
}
4966

50-
var assemblyKey = CompressedAssemblyInfo.GetDictionaryKey (assembly);
51-
if (assemblies.ContainsKey (assemblyKey)) {
52-
Log.LogDebugMessage ($"Skipping duplicate assembly: {assembly.ItemSpec}");
53-
continue;
54-
}
67+
var assemblyKey = CompressedAssemblyInfo.GetDictionaryKey (assembly);
68+
if (assemblies.ContainsKey (assemblyKey)) {
69+
Log.LogDebugMessage ($"Skipping duplicate assembly: {assembly.ItemSpec} (arch {MonoAndroidHelper.GetAssemblyAbi(assembly)})");
70+
continue;
71+
}
5572

56-
var fi = new FileInfo (assembly.ItemSpec);
57-
if (!fi.Exists) {
58-
Log.LogError ($"Assembly {assembly.ItemSpec} does not exist");
59-
continue;
60-
}
73+
var fi = new FileInfo (assembly.ItemSpec);
74+
if (!fi.Exists) {
75+
Log.LogError ($"Assembly {assembly.ItemSpec} does not exist");
76+
continue;
77+
}
6178

62-
assemblies.Add (assemblyKey, new CompressedAssemblyInfo (checked((uint)fi.Length)));
63-
}
6479

65-
uint index = 0;
66-
foreach (var kvp in assemblies) {
67-
kvp.Value.DescriptorIndex = index++;
80+
if (!counters.TryGetValue (arch, out uint counter)) {
81+
counter = 0;
82+
}
83+
assemblies.Add (assemblyKey, new CompressedAssemblyInfo (checked((uint)fi.Length), counter++, arch, Path.GetFileNameWithoutExtension (assembly.ItemSpec)));
84+
counters[arch] = counter;
85+
}
6886
}
6987

7088
string key = CompressedAssemblyInfo.GetKey (ProjectFullPath);
7189
Log.LogDebugMessage ($"Storing compression assemblies info with key '{key}'");
72-
BuildEngine4.RegisterTaskObjectAssemblyLocal (key, assemblies, RegisteredTaskObjectLifetime.Build);
73-
Generate (assemblies);
90+
BuildEngine4.RegisterTaskObjectAssemblyLocal (key, archAssemblies, RegisteredTaskObjectLifetime.Build);
91+
Generate (archAssemblies);
7492

75-
void Generate (IDictionary<string, CompressedAssemblyInfo> dict)
93+
void Generate (Dictionary<AndroidTargetArch, Dictionary<string, CompressedAssemblyInfo>> dict)
7694
{
7795
var composer = new CompressedAssembliesNativeAssemblyGenerator (Log, dict);
7896
LLVMIR.LlvmIrModule compressedAssemblies = composer.Construct ();

0 commit comments

Comments
 (0)