Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore split configs when bundle config moves shared libraries to base.apk #8987

Merged
merged 6 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public class GeneratePackageManagerJava : AndroidTask
public string TlsProvider { get; set; }
public string AndroidSequencePointsMode { get; set; }
public bool EnableSGenConcurrent { get; set; }
public string? CustomBundleConfigFile { get; set; }

[Output]
public string BuildId { get; set; }
Expand Down Expand Up @@ -357,6 +358,7 @@ void AddEnvironment ()
JniRemappingReplacementTypeCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementTypeCount,
JniRemappingReplacementMethodIndexEntryCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementMethodIndexEntryCount,
MarshalMethodsEnabled = EnableMarshalMethods,
IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (),
};
LLVMIR.LlvmIrModule appConfigModule = appConfigAsmGen.Construct ();

Expand Down Expand Up @@ -440,6 +442,15 @@ string ValidAssemblerString (string s)
}
}

bool ShouldIgnoreSplitConfigs ()
{
if (String.IsNullOrEmpty (CustomBundleConfigFile)) {
dellis1972 marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

return BundleConfigSplitConfigsChecker.ShouldIgnoreSplitConfigs (Log, CustomBundleConfigFile);
}

void GetRequiredTokens (string assemblyFilePath, out int android_runtime_jnienv_class_token, out int jnienv_initialize_method_token, out int jnienv_registerjninatives_method_token)
{
using (var pe = new PEReader (File.OpenRead (assemblyFilePath))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public EnvironmentFile (string path, string abi)
}
}

// This must be identical to the like-named structure in src/monodroid/jni/xamarin-app.h
// This must be identical to the like-named structure in src/native/xamarin-app-stub/xamarin-app.hh
public sealed class ApplicationConfig
{
public bool uses_mono_llvm;
Expand All @@ -49,6 +49,7 @@ public sealed class ApplicationConfig
public bool have_runtime_config_blob;
public bool have_assemblies_blob;
public bool marshal_methods_enabled;
public bool ignore_split_configs;
public byte bound_stream_io_exception_type;
public uint package_naming_policy;
public uint environment_variable_count;
Expand All @@ -66,7 +67,7 @@ public sealed class ApplicationConfig
public string android_package_name = String.Empty;
}

const uint ApplicationConfigFieldCount = 25;
const uint ApplicationConfigFieldCount = 26;

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

case 10: // bound_stream_io_exception_type: byte / .byte
case 10: // ignore_split_configs: bool / .byte
AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber);
ret.ignore_split_configs = ConvertFieldToBool ("ignore_split_configs", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
break;

case 11: // bound_stream_io_exception_type: byte / .byte
AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber);
ret.bound_stream_io_exception_type = ConvertFieldToByte ("bound_stream_io_exception_type", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
break;

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

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

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

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

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

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

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

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

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

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

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

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

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

case 24: // android_package_name: string / [pointer type]
case 25: // android_package_name: string / [pointer type]
Assert.IsTrue (expectedPointerTypes.Contains (field [0]), $"Unexpected pointer field type in '{envFile.Path}:{item.LineNumber}': {field [0]}");
pointers.Add (field [1].Trim ());
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Xamarin.Android.Tasks
{
// Declaration order of fields and their types must correspond *exactly* to that in
// src/monodroid/jni/xamarin-app.hh ApplicationConfig structure
// src/native/xamarin-app-stub/xamarin-app.hh ApplicationConfig structure
//
// Type mappings:
//
Expand Down Expand Up @@ -34,6 +34,7 @@ sealed class ApplicationConfig
public bool have_runtime_config_blob;
public bool have_assemblies_blob;
public bool marshal_methods_enabled;
public bool ignore_split_configs;
public byte bound_stream_io_exception_type;
public uint package_naming_policy;
public uint environment_variable_count;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ sealed class XamarinAndroidBundledAssembly
public PackageNamingPolicy PackageNamingPolicy { get; set; }
public List<ITaskItem> NativeLibraries { get; set; }
public bool MarshalMethodsEnabled { get; set; }
public bool IgnoreSplitConfigs { get; set; }

public ApplicationConfigNativeAssemblyGenerator (IDictionary<string, string> environmentVariables, IDictionary<string, string> systemProperties, TaskLoggingHelper log)
: base (log)
Expand Down Expand Up @@ -229,6 +230,7 @@ protected override void Construct (LlvmIrModule module)
have_runtime_config_blob = HaveRuntimeConfigBlob,
have_assemblies_blob = HaveAssemblyStore,
marshal_methods_enabled = MarshalMethodsEnabled,
ignore_split_configs = IgnoreSplitConfigs,
bound_stream_io_exception_type = (byte)BoundExceptionType,
package_naming_policy = (uint)PackageNamingPolicy,
environment_variable_count = (uint)(environmentVariables == null ? 0 : environmentVariables.Count * 2),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;

using Microsoft.Build.Utilities;

/// <para>
/// 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.
/// </para>
///
/// <para>
/// 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:
/// <code>
/// {
/// "optimizations": {
/// "splitsConfig": {
/// "splitDimension": [
/// {
/// "value": "ABI",
/// "negate": true
/// }
/// ],
/// }
/// }
/// }
///</code></para>
///
/// <para>
/// 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.
/// </para>
///
/// <para>
/// 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.
/// </para>
static class BundleConfigSplitConfigsChecker
{
enum BundleConfigObject
{
None,
Root,
Other,
Optimizations,
SplitsConfig,
SplitDimension,
}

ref struct Strings {
public readonly ReadOnlySpan<byte> UTF8BOM;
public readonly ReadOnlySpan<byte> ValuePropertyName;
public readonly ReadOnlySpan<byte> NegatePropertyName;

public Strings ()
{
UTF8BOM = new byte[] { 0xEF, 0xBB, 0xBF };
ValuePropertyName = Encoding.ASCII.GetBytes ("value");
NegatePropertyName = Encoding.ASCII.GetBytes ("negate");
}
}

public static bool ShouldIgnoreSplitConfigs (TaskLoggingHelper log, string configFilePath)
{
try {
return DoShouldIgnoreSplitConfigs (log, configFilePath);
} catch (Exception ex) {
log.LogWarning ($"Failed to process bundle config file '{configFilePath}', split config files will be ignored at run time.");
log.LogWarningFromException (ex);
return true;
}
}

static bool DoShouldIgnoreSplitConfigs (TaskLoggingHelper log, string configFilePath)
{
var options = new JsonReaderOptions {
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip
};

Strings strings = new ();
ReadOnlySpan<byte> json = File.ReadAllBytes (configFilePath);
if (json.StartsWith (strings.UTF8BOM)) {
json = json.Slice (strings.UTF8BOM.Length);
}

var state = new Stack<BundleConfigObject> ();
state.Push (BundleConfigObject.None);

bool? valueIsAbi = null;
bool? negate = null;
string? lastPropertyName = null;
var reader = new Utf8JsonReader (json, options);
while (reader.Read ()) {
JsonTokenType tokenType = reader.TokenType;

switch (tokenType) {
case JsonTokenType.StartObject:
TransitionState (strings, reader, state, lastPropertyName);
lastPropertyName = null;
break;

case JsonTokenType.EndObject:
if (state.Peek () != BundleConfigObject.None) {
BundleConfigObject popped = state.Pop ();
}
lastPropertyName = null;
break;

case JsonTokenType.PropertyName:
lastPropertyName = reader.GetString ();
if (state.Peek () == BundleConfigObject.SplitDimension) {
CheckSplitDimensionProperty (reader, strings, ref valueIsAbi, ref negate);
}
break;
}
}

if (!valueIsAbi.HasValue || !negate.HasValue) {
return false;
}

return valueIsAbi.Value && negate.Value;
}

static void CheckSplitDimensionProperty (Utf8JsonReader reader, Strings strings, ref bool? valueIsAbi, ref bool? negate)
{
if (!valueIsAbi.HasValue) {
if (reader.ValueTextEquals (strings.ValuePropertyName)) {
reader.Read ();
string v = reader.GetString ();
valueIsAbi = String.CompareOrdinal ("ABI", v) == 0;
return;
}
}

if (negate.HasValue) {
return;
}

if (reader.ValueTextEquals (strings.NegatePropertyName)) {
reader.Read ();
negate = reader.GetBoolean ();
}
}

static void TransitionState (Strings strings, Utf8JsonReader reader, Stack<BundleConfigObject> state, string? objectName)
{
BundleConfigObject current = state.Peek ();
if (current == BundleConfigObject.None) {
state.Push (BundleConfigObject.Root);
return;
}

BundleConfigObject need = current switch {
BundleConfigObject.Root => BundleConfigObject.Optimizations,
BundleConfigObject.Optimizations => BundleConfigObject.SplitsConfig,
BundleConfigObject.SplitsConfig => BundleConfigObject.SplitDimension,
_ => BundleConfigObject.Other
};

if (need == BundleConfigObject.Other) {
state.Push (need);
return;
}

string needName = need switch {
BundleConfigObject.Optimizations => "optimizations",
BundleConfigObject.SplitsConfig => "splitsConfig",
BundleConfigObject.SplitDimension => "splitDimension",
_ => throw new InvalidOperationException ($"Internal error: unsupported state transition to '{need}'")
};

if (!String.IsNullOrEmpty (objectName) && String.CompareOrdinal (needName, objectName) == 0) {
state.Push (need);
} else {
state.Push (BundleConfigObject.Other);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,7 @@ because xbuild doesn't support framework reference assemblies.
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)"
UseAssemblyStore="$(AndroidUseAssemblyStore)"
EnableMarshalMethods="$(_AndroidUseMarshalMethods)"
CustomBundleConfigFile="$(AndroidBundleConfigurationFile)"
>
<Output TaskParameter="BuildId" PropertyName="_XamarinBuildId" />
</GeneratePackageManagerJava>
Expand Down
2 changes: 1 addition & 1 deletion src/native/monodroid/monodroid-glue.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1782,7 +1782,7 @@ Java_mono_android_Runtime_initInternal (JNIEnv *env, jclass klass, jstring lang,
assembliesJava,
apiLevel,
isEmulator,
haveSplitApks
application_config.ignore_split_configs ? false : haveSplitApks
);
}

Expand Down
Loading