diff --git a/Documentation/guides/extract-native-libraries.md b/Documentation/guides/extract-native-libraries.md new file mode 100644 index 00000000000..b5e7de01b7f --- /dev/null +++ b/Documentation/guides/extract-native-libraries.md @@ -0,0 +1,49 @@ +# Installation of Native Libraries + +An Android `.apk` file can contain native libraries for one or more +architectures. Historically, all native libraries for the target +device are extracted at `.apk` installation time. This would result +in *two* copies of native libraries on the target device: a set of +native libraries which are stored compressed within the `.apk`, and +a separate set of native libraries on the Android target filesystem. + +Starting with Android v6.0 (API-23), Android added an ability for +native libraries to be stored *uncompressed* within the `.apk` along +with the ability to load those native libraries *from* the `.apk` +without requiring a separate filesystem copy of the native libraries. + +Android Things *requires* this new mechanism; `.apk` files installed +on Android Things will no longer have *any* native libraries extracted. + +As a result, the `.apk` will be *larger*, because the native +libraries are stored uncompressed within the `.apk`, but the +install size will be *smaller*, because there isn't a second "copy" +of the native libraries (one compressed in the `.apk`, one outside +of the `.apk`). + +On Android versions older than Android v6.0, the native libraries +will continue to be extracted during `.apk` installation. + +In order to indicate to Android v6.0 and later that native libraries +do not need to be extracted, the +[`//application/@android:extractNativeLibs`][extractNativeLibs] +attribute within `AndroidManifest.xml` must be set to `false.` + +[extractNativeLibs]: https://developer.android.com/guide/topics/manifest/application-element#extractNativeLibs + +## OSS Implementation Details + +When `AndroidManifest.xml` contains an XML attribute matching +`//application[@android:extractNativeLibs='false']`, the +Xamarin.Android build system will do the following: + + 1. The `$(AndroidStoreUncompressedFileExtensions)` MSBuild property + will be automatically updated to contain the `.so` file + extension, causing native libraries to be stored uncompressed + within the `.apk`. + + 2. The `__XA_DSO_IN_APK` environment variable will be set within the + created `.apk` file with the value of `1`, indicating to + the app that native libraries should be loaded from the `.apk` + itself instead of from the filesystem. + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index 8ab08c7d297..fd5bcb37901 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; +using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using MonoDroid.Utils; @@ -68,6 +68,11 @@ public class GenerateJavaStubs : Task public string ApplicationJavaClass { get; set; } + /// + /// If specified, we need to cache the value of EmbeddedDSOsEnabled=True for incremental builds + /// + public string CacheFile { get; set; } + public override bool Execute () { try { @@ -223,6 +228,22 @@ void Run (DirectoryAssemblyResolver res) MonoAndroidHelper.CopyIfStreamChanged (stream, MergedAndroidManifestOutput); } + // Create the CacheFile if needed + if (!string.IsNullOrEmpty (CacheFile)) { + bool extractNativeLibraries = manifest.ExtractNativeLibraries (); + if (!extractNativeLibraries) { + //We need to write the value to a file, if _GenerateJavaStubs is skipped on incremental builds + var document = new XDocument ( + new XDeclaration ("1.0", "UTF-8", null), + new XElement ("Properties", new XElement (nameof (ReadJavaStubsCache.EmbeddedDSOsEnabled), "True")) + ); + document.SaveIfChanged (CacheFile); + } else { + //Delete the file otherwise, since we only need to specify when EmbeddedDSOsEnabled=True + File.Delete (CacheFile); + } + } + // Create additional runtime provider java sources. string providerTemplateFile = UseSharedRuntime ? "MonoRuntimeProvider.Shared.java" : "MonoRuntimeProvider.Bundled.java"; string providerTemplate = GetResource (providerTemplateFile); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ReadJavaStubsCache.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ReadJavaStubsCache.cs new file mode 100644 index 00000000000..e5b1e120db6 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ReadJavaStubsCache.cs @@ -0,0 +1,28 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.IO; +using System.Xml.Linq; + +namespace Xamarin.Android.Tasks +{ + public class ReadJavaStubsCache : Task + { + [Required] + public string CacheFile { get; set; } + + [Output] + public bool EmbeddedDSOsEnabled { get; set; } + + public override bool Execute () + { + if (File.Exists (CacheFile)) { + var doc = XDocument.Load (CacheFile); + string text = doc.Element ("Properties")?.Element (nameof (EmbeddedDSOsEnabled))?.Value; + if (bool.TryParse (text, out bool value)) + EmbeddedDSOsEnabled = value; + } + + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 873884083f0..80ace65b602 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -195,6 +195,53 @@ public void CheckIncludedNativeLibraries ([Values (true, false)] bool compressNa } } + [Test] + public void EmbeddedDSOs () + { + var proj = new XamarinAndroidApplicationProject (); + proj.AndroidManifest = $@" + + + + +"; + + using (var b = CreateApkBuilder (Path.Combine ("temp", TestName))) { + Assert.IsTrue (b.Build (proj), "first build should have succeeded"); + + var apk = Path.Combine (Root, b.ProjectDirectory, + proj.IntermediateOutputPath, "android", "bin", "UnnamedProject.UnnamedProject.apk"); + AssertEmbeddedDSOs (apk); + + //Delete the apk & build again + File.Delete (apk); + Assert.IsTrue (b.Build (proj), "second build should have succeeded"); + AssertEmbeddedDSOs (apk); + } + } + + void AssertEmbeddedDSOs (string apk) + { + FileAssert.Exists (apk); + + using (var zip = ZipHelper.OpenZip (apk)) { + foreach (var entry in zip) { + if (entry.FullName.EndsWith (".so")) { + Assert.AreEqual (entry.Size, entry.CompressedSize, $"`{entry.FullName}` should be uncompressed!"); + } else if (entry.FullName == "environment") { + using (var stream = new MemoryStream ()) { + entry.Extract (stream); + stream.Position = 0; + using (var reader = new StreamReader (stream)) { + string environment = reader.ReadToEnd (); + StringAssert.Contains ("__XA_DSO_IN_APK=1", environment, "`__XA_DSO_IN_APK=1` should be set via @(AndroidEnvironment)"); + } + } + } + } + } + } + [Test] public void ExplicitPackageNamingPolicy () { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs index aab7e8720d4..8524dccb6ed 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs @@ -644,6 +644,20 @@ bool IsMainLauncher (XElement intentFilter) intentFilter.Elements (entry.Key).Any (e => ((string) e.Attribute (attName) == entry.Value))); } + /// + /// Returns the value of //application/@android:extractNativeLibs. + /// + public bool ExtractNativeLibraries () + { + string text = app?.Attribute (androidNs + "extractNativeLibs")?.Value; + if (bool.TryParse (text, out bool value)) { + return value; + } + + // If android:extractNativeLibs is omitted, returns true. + return true; + } + XElement ActivityFromTypeDefinition (TypeDefinition type, string name, int targetSdkVersion) { if (name.StartsWith ("_")) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index f16e8a78db9..3218846fe5b 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -122,6 +122,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index bf4afabcdc2..89a4183e6bb 100755 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -91,6 +91,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + @@ -2285,7 +2286,7 @@ because xbuild doesn't support framework reference assemblies. + Outputs="$(_AndroidStampDirectory)_GenerateJavaStubs.stamp"> + AcwMapFile="$(_AcwMapFile)" + CacheFile="$(IntermediateOutputPath)javastubs.cache"> - + + + + + + + + + + + + + + + + .so;$(AndroidStoreUncompressedFileExtensions) + - + + - + + <_GetAddOnPlatformLibrariesDependsOn> + _GenerateJavaStubs; + _ReadJavaStubsCache; + _SetupEmbeddedDSOs; + + + + - diff --git a/tests/CodeGen-Binding/Xamarin.Android.McwGen-Tests/Jars/.gitignore b/tests/CodeGen-Binding/Xamarin.Android.McwGen-Tests/Jars/.gitignore new file mode 100644 index 00000000000..d392f0e82c4 --- /dev/null +++ b/tests/CodeGen-Binding/Xamarin.Android.McwGen-Tests/Jars/.gitignore @@ -0,0 +1 @@ +*.jar diff --git a/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj b/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj index 81d42105437..6c456feb415 100644 --- a/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj +++ b/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj @@ -18,11 +18,6 @@ true false armeabi-v7a;x86 - - - .so diff --git a/tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt b/tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt index 966b2ab27fb..ba117b60829 100644 --- a/tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt +++ b/tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt @@ -2,4 +2,3 @@ debug.mono.debug=1 MONO_LOG_LEVEL=debug MONO_LOG_MASK=asm MONO_XDEBUG=1 -__XA_DSO_IN_APK=1