Skip to content

Commit a954a33

Browse files
authored
Ignore split configs when bundle config moves shared libraries to base.apk (#8987)
Fixes: #8979 When bundle configuration uses standard settings for split configs, the per-ABI library directory (which contains all of our DSOs/assemblies/blobs etc) will be placed in a per-ABI split config file named split_config.{ARCH}.apk and we use the fact to optimize startup time. However, if a custom build config file with the following settings is found, Android bundletool doesn't create the per-ABI split config file, and so we need to search all the files in order to find shared libraries, assemblies/blobs etc: ``` { "optimizations": { "splitsConfig": { "splitDimension": [ { "value": "ABI", "negate": true } ], } } } ``` The presence or absence of split config files is checked in our Java startup code which will notice that split configs are present, but will not check (for performance reasons, to avoid string comparisons) whether the per-ABI split config is present. We, therefore, need to let our native runtime know in some inexpensive way that the split configs should be ignored and that the DSOs/assemblies/blobs should be searched for in the usual, non-split config, way. Since we know at build time whether this is the case, it's best to record the fact then and let the native runtime merely check a boolean flag instead of dynamic detection at each app startup.
1 parent 3f22ab6 commit a954a33

File tree

10 files changed

+391
-20
lines changed

10 files changed

+391
-20
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public class GeneratePackageManagerJava : AndroidTask
7979
public string TlsProvider { get; set; }
8080
public string AndroidSequencePointsMode { get; set; }
8181
public bool EnableSGenConcurrent { get; set; }
82+
public string? CustomBundleConfigFile { get; set; }
8283

8384
[Output]
8485
public string BuildId { get; set; }
@@ -357,6 +358,7 @@ void AddEnvironment ()
357358
JniRemappingReplacementTypeCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementTypeCount,
358359
JniRemappingReplacementMethodIndexEntryCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementMethodIndexEntryCount,
359360
MarshalMethodsEnabled = EnableMarshalMethods,
361+
IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (),
360362
};
361363
LLVMIR.LlvmIrModule appConfigModule = appConfigAsmGen.Construct ();
362364

@@ -440,6 +442,15 @@ string ValidAssemblerString (string s)
440442
}
441443
}
442444

445+
bool ShouldIgnoreSplitConfigs ()
446+
{
447+
if (String.IsNullOrEmpty (CustomBundleConfigFile)) {
448+
return false;
449+
}
450+
451+
return BundleConfigSplitConfigsChecker.ShouldIgnoreSplitConfigs (Log, CustomBundleConfigFile);
452+
}
453+
443454
void GetRequiredTokens (string assemblyFilePath, out int android_runtime_jnienv_class_token, out int jnienv_initialize_method_token, out int jnienv_registerjninatives_method_token)
444455
{
445456
using (var pe = new PEReader (File.OpenRead (assemblyFilePath))) {

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public EnvironmentFile (string path, string abi)
3636
}
3737
}
3838

39-
// This must be identical to the like-named structure in src/monodroid/jni/xamarin-app.h
39+
// This must be identical to the like-named structure in src/native/xamarin-app-stub/xamarin-app.hh
4040
public sealed class ApplicationConfig
4141
{
4242
public bool uses_mono_llvm;
@@ -49,6 +49,7 @@ public sealed class ApplicationConfig
4949
public bool have_runtime_config_blob;
5050
public bool have_assemblies_blob;
5151
public bool marshal_methods_enabled;
52+
public bool ignore_split_configs;
5253
public byte bound_stream_io_exception_type;
5354
public uint package_naming_policy;
5455
public uint environment_variable_count;
@@ -66,7 +67,7 @@ public sealed class ApplicationConfig
6667
public string android_package_name = String.Empty;
6768
}
6869

69-
const uint ApplicationConfigFieldCount = 25;
70+
const uint ApplicationConfigFieldCount = 26;
7071

7172
const string ApplicationConfigSymbolName = "application_config";
7273
const string AppEnvironmentVariablesSymbolName = "app_environment_variables";
@@ -255,77 +256,82 @@ static ApplicationConfig ReadApplicationConfig (EnvironmentFile envFile)
255256
ret.marshal_methods_enabled = ConvertFieldToBool ("marshal_methods_enabled", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
256257
break;
257258

258-
case 10: // bound_stream_io_exception_type: byte / .byte
259+
case 10: // ignore_split_configs: bool / .byte
260+
AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber);
261+
ret.ignore_split_configs = ConvertFieldToBool ("ignore_split_configs", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
262+
break;
263+
264+
case 11: // bound_stream_io_exception_type: byte / .byte
259265
AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber);
260266
ret.bound_stream_io_exception_type = ConvertFieldToByte ("bound_stream_io_exception_type", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
261267
break;
262268

263-
case 11: // package_naming_policy: uint32_t / .word | .long
269+
case 12: // package_naming_policy: uint32_t / .word | .long
264270
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
265271
ret.package_naming_policy = ConvertFieldToUInt32 ("package_naming_policy", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
266272
break;
267273

268-
case 12: // environment_variable_count: uint32_t / .word | .long
274+
case 13: // environment_variable_count: uint32_t / .word | .long
269275
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
270276
ret.environment_variable_count = ConvertFieldToUInt32 ("environment_variable_count", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
271277
break;
272278

273-
case 13: // system_property_count: uint32_t / .word | .long
279+
case 14: // system_property_count: uint32_t / .word | .long
274280
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
275281
ret.system_property_count = ConvertFieldToUInt32 ("system_property_count", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
276282
break;
277283

278-
case 14: // number_of_assemblies_in_apk: uint32_t / .word | .long
284+
case 15: // number_of_assemblies_in_apk: uint32_t / .word | .long
279285
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
280286
ret.number_of_assemblies_in_apk = ConvertFieldToUInt32 ("number_of_assemblies_in_apk", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
281287
break;
282288

283-
case 15: // bundled_assembly_name_width: uint32_t / .word | .long
289+
case 16: // bundled_assembly_name_width: uint32_t / .word | .long
284290
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
285291
ret.bundled_assembly_name_width = ConvertFieldToUInt32 ("bundled_assembly_name_width", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
286292
break;
287293

288-
case 16: // number_of_assembly_store_files: uint32_t / .word | .long
294+
case 17: // number_of_assembly_store_files: uint32_t / .word | .long
289295
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
290296
ret.number_of_assembly_store_files = ConvertFieldToUInt32 ("number_of_assembly_store_files", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
291297
break;
292298

293-
case 17: // number_of_dso_cache_entries: uint32_t / .word | .long
299+
case 18: // number_of_dso_cache_entries: uint32_t / .word | .long
294300
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
295301
ret.number_of_dso_cache_entries = ConvertFieldToUInt32 ("number_of_dso_cache_entries", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
296302
break;
297303

298-
case 18: // android_runtime_jnienv_class_token: uint32_t / .word | .long
304+
case 19: // android_runtime_jnienv_class_token: uint32_t / .word | .long
299305
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
300306
ret.number_of_dso_cache_entries = ConvertFieldToUInt32 ("android_runtime_jnienv_class_token", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
301307
break;
302308

303-
case 19: // jnienv_initialize_method_token: uint32_t / .word | .long
309+
case 20: // jnienv_initialize_method_token: uint32_t / .word | .long
304310
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
305311
ret.number_of_dso_cache_entries = ConvertFieldToUInt32 ("jnienv_initialize_method_token", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
306312
break;
307313

308-
case 20: // jnienv_registerjninatives_method_token: uint32_t / .word | .long
314+
case 21: // jnienv_registerjninatives_method_token: uint32_t / .word | .long
309315
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
310316
ret.number_of_dso_cache_entries = ConvertFieldToUInt32 ("jnienv_registerjninatives_method_token", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
311317
break;
312318

313-
case 21: // jni_remapping_replacement_type_count: uint32_t / .word | .long
319+
case 22: // jni_remapping_replacement_type_count: uint32_t / .word | .long
314320
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
315321
ret.jni_remapping_replacement_type_count = ConvertFieldToUInt32 ("jni_remapping_replacement_type_count", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
316322
break;
317323

318-
case 22: // jni_remapping_replacement_method_index_entry_count: uint32_t / .word | .long
324+
case 23: // jni_remapping_replacement_method_index_entry_count: uint32_t / .word | .long
319325
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
320326
ret.jni_remapping_replacement_method_index_entry_count = ConvertFieldToUInt32 ("jni_remapping_replacement_method_index_entry_count", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
321327
break;
322328

323-
case 23: // mono_components_mask: uint32_t / .word | .long
329+
case 24: // mono_components_mask: uint32_t / .word | .long
324330
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
325331
ret.mono_components_mask = ConvertFieldToUInt32 ("mono_components_mask", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
326332
break;
327333

328-
case 24: // android_package_name: string / [pointer type]
334+
case 25: // android_package_name: string / [pointer type]
329335
Assert.IsTrue (expectedPointerTypes.Contains (field [0]), $"Unexpected pointer field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
330336
pointers.Add (field [1].Trim ());
331337
break;

src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Xamarin.Android.Tasks
44
{
55
// Declaration order of fields and their types must correspond *exactly* to that in
6-
// src/monodroid/jni/xamarin-app.hh ApplicationConfig structure
6+
// src/native/xamarin-app-stub/xamarin-app.hh ApplicationConfig structure
77
//
88
// Type mappings:
99
//
@@ -34,6 +34,7 @@ sealed class ApplicationConfig
3434
public bool have_runtime_config_blob;
3535
public bool have_assemblies_blob;
3636
public bool marshal_methods_enabled;
37+
public bool ignore_split_configs;
3738
public byte bound_stream_io_exception_type;
3839
public uint package_naming_policy;
3940
public uint environment_variable_count;

src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ sealed class XamarinAndroidBundledAssembly
187187
public PackageNamingPolicy PackageNamingPolicy { get; set; }
188188
public List<ITaskItem> NativeLibraries { get; set; }
189189
public bool MarshalMethodsEnabled { get; set; }
190+
public bool IgnoreSplitConfigs { get; set; }
190191

191192
public ApplicationConfigNativeAssemblyGenerator (IDictionary<string, string> environmentVariables, IDictionary<string, string> systemProperties, TaskLoggingHelper log)
192193
: base (log)
@@ -229,6 +230,7 @@ protected override void Construct (LlvmIrModule module)
229230
have_runtime_config_blob = HaveRuntimeConfigBlob,
230231
have_assemblies_blob = HaveAssemblyStore,
231232
marshal_methods_enabled = MarshalMethodsEnabled,
233+
ignore_split_configs = IgnoreSplitConfigs,
232234
bound_stream_io_exception_type = (byte)BoundExceptionType,
233235
package_naming_policy = (uint)PackageNamingPolicy,
234236
environment_variable_count = (uint)(environmentVariables == null ? 0 : environmentVariables.Count * 2),
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
using System.Text.Json;
6+
7+
using Microsoft.Build.Utilities;
8+
9+
/// <para>
10+
/// When bundle configuration uses standard settings for split configs, the per-ABI library
11+
/// directory (which contains all of our DSOs/assemblies/blobs etc) will be placed in a per-ABI
12+
/// split config file named `split_config.{ARCH}.apk` and we use the fact to optimize startup
13+
/// time.
14+
/// </para>
15+
///
16+
/// <para>
17+
/// However, if a custom build config file with the following settings is found, Android bundletool
18+
/// doesn't create the per-ABI split config file, and so we need to search all the files in order
19+
/// to find shared libraries, assemblies/blobs etc:
20+
/// <code>
21+
/// {
22+
/// "optimizations": {
23+
/// "splitsConfig": {
24+
/// "splitDimension": [
25+
/// {
26+
/// "value": "ABI",
27+
/// "negate": true
28+
/// }
29+
/// ],
30+
/// }
31+
/// }
32+
/// }
33+
///</code></para>
34+
///
35+
/// <para>
36+
/// The presence or absence of split config files is checked in our Java startup code which will
37+
/// notice that split configs are present, but will not check (for performance reasons, to avoid
38+
/// string comparisons) whether the per-ABI split config is present. We, therefore, need to let
39+
/// our native runtime know in some inexpensive way that the split configs should be ignored and
40+
/// that the DSOs/assemblies/blobs should be searched for in the usual, non-split config, way.
41+
/// </para>
42+
///
43+
/// <para>
44+
/// Since we know at build time whether this is the case, it's best to record the fact then and
45+
/// let the native runtime merely check a boolean flag instead of dynamic detection at each app
46+
/// startup.
47+
/// </para>
48+
static class BundleConfigSplitConfigsChecker
49+
{
50+
enum BundleConfigObject
51+
{
52+
None,
53+
Root,
54+
Other,
55+
Optimizations,
56+
SplitsConfig,
57+
SplitDimension,
58+
}
59+
60+
ref struct Strings {
61+
public readonly ReadOnlySpan<byte> UTF8BOM;
62+
public readonly ReadOnlySpan<byte> ValuePropertyName;
63+
public readonly ReadOnlySpan<byte> NegatePropertyName;
64+
65+
public Strings ()
66+
{
67+
UTF8BOM = new byte[] { 0xEF, 0xBB, 0xBF };
68+
ValuePropertyName = Encoding.ASCII.GetBytes ("value");
69+
NegatePropertyName = Encoding.ASCII.GetBytes ("negate");
70+
}
71+
}
72+
73+
public static bool ShouldIgnoreSplitConfigs (TaskLoggingHelper log, string configFilePath)
74+
{
75+
try {
76+
return DoShouldIgnoreSplitConfigs (log, configFilePath);
77+
} catch (Exception ex) {
78+
log.LogWarning ($"Failed to process bundle config file '{configFilePath}', split config files will be ignored at run time.");
79+
log.LogWarningFromException (ex);
80+
return true;
81+
}
82+
}
83+
84+
static bool DoShouldIgnoreSplitConfigs (TaskLoggingHelper log, string configFilePath)
85+
{
86+
var options = new JsonReaderOptions {
87+
AllowTrailingCommas = true,
88+
CommentHandling = JsonCommentHandling.Skip
89+
};
90+
91+
Strings strings = new ();
92+
ReadOnlySpan<byte> json = File.ReadAllBytes (configFilePath);
93+
if (json.StartsWith (strings.UTF8BOM)) {
94+
json = json.Slice (strings.UTF8BOM.Length);
95+
}
96+
97+
var state = new Stack<BundleConfigObject> ();
98+
state.Push (BundleConfigObject.None);
99+
100+
bool? valueIsAbi = null;
101+
bool? negate = null;
102+
string? lastPropertyName = null;
103+
var reader = new Utf8JsonReader (json, options);
104+
while (reader.Read ()) {
105+
JsonTokenType tokenType = reader.TokenType;
106+
107+
switch (tokenType) {
108+
case JsonTokenType.StartObject:
109+
TransitionState (strings, reader, state, lastPropertyName);
110+
lastPropertyName = null;
111+
break;
112+
113+
case JsonTokenType.EndObject:
114+
if (state.Peek () != BundleConfigObject.None) {
115+
BundleConfigObject popped = state.Pop ();
116+
}
117+
lastPropertyName = null;
118+
break;
119+
120+
case JsonTokenType.PropertyName:
121+
lastPropertyName = reader.GetString ();
122+
if (state.Peek () == BundleConfigObject.SplitDimension) {
123+
CheckSplitDimensionProperty (reader, strings, ref valueIsAbi, ref negate);
124+
}
125+
break;
126+
}
127+
}
128+
129+
if (!valueIsAbi.HasValue || !negate.HasValue) {
130+
return false;
131+
}
132+
133+
return valueIsAbi.Value && negate.Value;
134+
}
135+
136+
static void CheckSplitDimensionProperty (Utf8JsonReader reader, Strings strings, ref bool? valueIsAbi, ref bool? negate)
137+
{
138+
if (!valueIsAbi.HasValue) {
139+
if (reader.ValueTextEquals (strings.ValuePropertyName)) {
140+
reader.Read ();
141+
string v = reader.GetString ();
142+
valueIsAbi = String.CompareOrdinal ("ABI", v) == 0;
143+
return;
144+
}
145+
}
146+
147+
if (negate.HasValue) {
148+
return;
149+
}
150+
151+
if (reader.ValueTextEquals (strings.NegatePropertyName)) {
152+
reader.Read ();
153+
negate = reader.GetBoolean ();
154+
}
155+
}
156+
157+
static void TransitionState (Strings strings, Utf8JsonReader reader, Stack<BundleConfigObject> state, string? objectName)
158+
{
159+
BundleConfigObject current = state.Peek ();
160+
if (current == BundleConfigObject.None) {
161+
state.Push (BundleConfigObject.Root);
162+
return;
163+
}
164+
165+
BundleConfigObject need = current switch {
166+
BundleConfigObject.Root => BundleConfigObject.Optimizations,
167+
BundleConfigObject.Optimizations => BundleConfigObject.SplitsConfig,
168+
BundleConfigObject.SplitsConfig => BundleConfigObject.SplitDimension,
169+
_ => BundleConfigObject.Other
170+
};
171+
172+
if (need == BundleConfigObject.Other) {
173+
state.Push (need);
174+
return;
175+
}
176+
177+
string needName = need switch {
178+
BundleConfigObject.Optimizations => "optimizations",
179+
BundleConfigObject.SplitsConfig => "splitsConfig",
180+
BundleConfigObject.SplitDimension => "splitDimension",
181+
_ => throw new InvalidOperationException ($"Internal error: unsupported state transition to '{need}'")
182+
};
183+
184+
if (!String.IsNullOrEmpty (objectName) && String.CompareOrdinal (needName, objectName) == 0) {
185+
state.Push (need);
186+
} else {
187+
state.Push (BundleConfigObject.Other);
188+
}
189+
}
190+
}

src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,7 @@ because xbuild doesn't support framework reference assemblies.
17231723
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)"
17241724
UseAssemblyStore="$(AndroidUseAssemblyStore)"
17251725
EnableMarshalMethods="$(_AndroidUseMarshalMethods)"
1726+
CustomBundleConfigFile="$(AndroidBundleConfigurationFile)"
17261727
>
17271728
<Output TaskParameter="BuildId" PropertyName="_XamarinBuildId" />
17281729
</GeneratePackageManagerJava>

src/native/monodroid/monodroid-glue.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1782,7 +1782,7 @@ Java_mono_android_Runtime_initInternal (JNIEnv *env, jclass klass, jstring lang,
17821782
assembliesJava,
17831783
apiLevel,
17841784
isEmulator,
1785-
haveSplitApks
1785+
application_config.ignore_split_configs ? false : haveSplitApks
17861786
);
17871787
}
17881788

0 commit comments

Comments
 (0)