Skip to content

Commit 2e4a6ab

Browse files
jonathanpeppersjonpryor
authored andcommitted
[Xamarin.Android.Build.Tasks] support Embedded DSOs (#2579)
Fixes: #2415 Context: #2154 Currently, in order to activate the embedded Dynamic Shared Object (DSO) support, one has to perform the following actions manually (see also 95ca102): 1. Add the `android:extractNativeLibs="false"` attribute to the `<application>` element within `Properties/AndroidManifest.xml`. 2. Add the following property to the project file: <AndroidStoreUncompressedFileExtensions>.so</AndroidStoreUncompressedFileExtensions> 3. Add an android environment file to the project with a line containing: __XA_DSO_IN_APK=1 Instead of requiring these three separate steps, the presence of the `//application[@android:extractNativeLibs='false']` attribute within `AndroidManifest.xml` should instead be used as the "feature" toggle; when `extractNativeLibs` is false, then `$(AndroidStoreUncompressedFileExtensions)` should be automatically updated and the `__XA_DSO_IN_APK` environment variable should automatically exported: 1. Append the value `.so` to `$(AndroidStoreUncompressedFileExtensions)`. Both of the `<Aapt/>` and `<BuildApk/>` MSBuild tasks use this property. 2. Create a new file within `$(IntermediateOutputPath)` containing the string `__XA_DSO_IN_APK=1`, and add this new file to `@(AndroidEnvironment)` so that `__XA_DSO_IN_APK` is set. The problem here is with incremental builds; if the `_GenerateJavaStubs` target is skipped, we still need to know if `//application/@android:extractNativeLibs` is set. To make this work, I added a `CacheFile` property on the `<GenerateJavaStubs/>` MSBuild task. It writes a simple XML document to `$(IntermediateOutputPath)javastubs.cache`, such as: <Properties> <EmbeddedDSOsEnabled>True</EmbeddedDSOsEnabled> </Properties> The file will not exist at all unless the value is `True`, which will prevent this feature from impacting build times. I also added an MSBuild test to verify these changes are happening. I also updated `tests/EmbeddedDSOs` to rely on the new functionality. Finally, I updated the `_GenerateJavaStubs` target to follow the "stamp" file convention of using `$(_AndroidStampDirectory)_GenerateJavaStubs.stamp`.
1 parent b90d3ab commit 2e4a6ab

File tree

10 files changed

+201
-13
lines changed

10 files changed

+201
-13
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Installation of Native Libraries
2+
3+
An Android `.apk` file can contain native libraries for one or more
4+
architectures. Historically, all native libraries for the target
5+
device are extracted at `.apk` installation time. This would result
6+
in *two* copies of native libraries on the target device: a set of
7+
native libraries which are stored compressed within the `.apk`, and
8+
a separate set of native libraries on the Android target filesystem.
9+
10+
Starting with Android v6.0 (API-23), Android added an ability for
11+
native libraries to be stored *uncompressed* within the `.apk` along
12+
with the ability to load those native libraries *from* the `.apk`
13+
without requiring a separate filesystem copy of the native libraries.
14+
15+
Android Things *requires* this new mechanism; `.apk` files installed
16+
on Android Things will no longer have *any* native libraries extracted.
17+
18+
As a result, the `.apk` will be *larger*, because the native
19+
libraries are stored uncompressed within the `.apk`, but the
20+
install size will be *smaller*, because there isn't a second "copy"
21+
of the native libraries (one compressed in the `.apk`, one outside
22+
of the `.apk`).
23+
24+
On Android versions older than Android v6.0, the native libraries
25+
will continue to be extracted during `.apk` installation.
26+
27+
In order to indicate to Android v6.0 and later that native libraries
28+
do not need to be extracted, the
29+
[`//application/@android:extractNativeLibs`][extractNativeLibs]
30+
attribute within `AndroidManifest.xml` must be set to `false.`
31+
32+
[extractNativeLibs]: https://developer.android.com/guide/topics/manifest/application-element#extractNativeLibs
33+
34+
## OSS Implementation Details
35+
36+
When `AndroidManifest.xml` contains an XML attribute matching
37+
`//application[@android:extractNativeLibs='false']`, the
38+
Xamarin.Android build system will do the following:
39+
40+
1. The `$(AndroidStoreUncompressedFileExtensions)` MSBuild property
41+
will be automatically updated to contain the `.so` file
42+
extension, causing native libraries to be stored uncompressed
43+
within the `.apk`.
44+
45+
2. The `__XA_DSO_IN_APK` environment variable will be set within the
46+
created `.apk` file with the value of `1`, indicating to
47+
the app that native libraries should be loaded from the `.apk`
48+
itself instead of from the filesystem.
49+

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
using System;
44
using System.Collections.Generic;
5-
using System.Diagnostics;
65
using System.IO;
76
using System.Linq;
7+
using System.Xml.Linq;
88
using Microsoft.Build.Framework;
99
using Microsoft.Build.Utilities;
1010
using MonoDroid.Utils;
@@ -68,6 +68,11 @@ public class GenerateJavaStubs : Task
6868

6969
public string ApplicationJavaClass { get; set; }
7070

71+
/// <summary>
72+
/// If specified, we need to cache the value of EmbeddedDSOsEnabled=True for incremental builds
73+
/// </summary>
74+
public string CacheFile { get; set; }
75+
7176
public override bool Execute ()
7277
{
7378
try {
@@ -223,6 +228,22 @@ void Run (DirectoryAssemblyResolver res)
223228
MonoAndroidHelper.CopyIfStreamChanged (stream, MergedAndroidManifestOutput);
224229
}
225230

231+
// Create the CacheFile if needed
232+
if (!string.IsNullOrEmpty (CacheFile)) {
233+
bool extractNativeLibraries = manifest.ExtractNativeLibraries ();
234+
if (!extractNativeLibraries) {
235+
//We need to write the value to a file, if _GenerateJavaStubs is skipped on incremental builds
236+
var document = new XDocument (
237+
new XDeclaration ("1.0", "UTF-8", null),
238+
new XElement ("Properties", new XElement (nameof (ReadJavaStubsCache.EmbeddedDSOsEnabled), "True"))
239+
);
240+
document.SaveIfChanged (CacheFile);
241+
} else {
242+
//Delete the file otherwise, since we only need to specify when EmbeddedDSOsEnabled=True
243+
File.Delete (CacheFile);
244+
}
245+
}
246+
226247
// Create additional runtime provider java sources.
227248
string providerTemplateFile = UseSharedRuntime ? "MonoRuntimeProvider.Shared.java" : "MonoRuntimeProvider.Bundled.java";
228249
string providerTemplate = GetResource<JavaCallableWrapperGenerator> (providerTemplateFile);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Microsoft.Build.Framework;
2+
using Microsoft.Build.Utilities;
3+
using System.IO;
4+
using System.Xml.Linq;
5+
6+
namespace Xamarin.Android.Tasks
7+
{
8+
public class ReadJavaStubsCache : Task
9+
{
10+
[Required]
11+
public string CacheFile { get; set; }
12+
13+
[Output]
14+
public bool EmbeddedDSOsEnabled { get; set; }
15+
16+
public override bool Execute ()
17+
{
18+
if (File.Exists (CacheFile)) {
19+
var doc = XDocument.Load (CacheFile);
20+
string text = doc.Element ("Properties")?.Element (nameof (EmbeddedDSOsEnabled))?.Value;
21+
if (bool.TryParse (text, out bool value))
22+
EmbeddedDSOsEnabled = value;
23+
}
24+
25+
return !Log.HasLoggedErrors;
26+
}
27+
}
28+
}

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,53 @@ public void CheckIncludedNativeLibraries ([Values (true, false)] bool compressNa
202202
}
203203
}
204204

205+
[Test]
206+
public void EmbeddedDSOs ()
207+
{
208+
var proj = new XamarinAndroidApplicationProject ();
209+
proj.AndroidManifest = $@"<?xml version=""1.0"" encoding=""utf-8""?>
210+
<manifest xmlns:android=""http://schemas.android.com/apk/res/android"" android:versionCode=""1"" android:versionName=""1.0"" package=""{proj.PackageName}"">
211+
<uses-sdk />
212+
<application android:label=""{proj.ProjectName}"" android:extractNativeLibs=""false"">
213+
</application>
214+
</manifest>";
215+
216+
using (var b = CreateApkBuilder (Path.Combine ("temp", TestName))) {
217+
Assert.IsTrue (b.Build (proj), "first build should have succeeded");
218+
219+
var apk = Path.Combine (Root, b.ProjectDirectory,
220+
proj.IntermediateOutputPath, "android", "bin", "UnnamedProject.UnnamedProject.apk");
221+
AssertEmbeddedDSOs (apk);
222+
223+
//Delete the apk & build again
224+
File.Delete (apk);
225+
Assert.IsTrue (b.Build (proj), "second build should have succeeded");
226+
AssertEmbeddedDSOs (apk);
227+
}
228+
}
229+
230+
void AssertEmbeddedDSOs (string apk)
231+
{
232+
FileAssert.Exists (apk);
233+
234+
using (var zip = ZipHelper.OpenZip (apk)) {
235+
foreach (var entry in zip) {
236+
if (entry.FullName.EndsWith (".so")) {
237+
Assert.AreEqual (entry.Size, entry.CompressedSize, $"`{entry.FullName}` should be uncompressed!");
238+
} else if (entry.FullName == "environment") {
239+
using (var stream = new MemoryStream ()) {
240+
entry.Extract (stream);
241+
stream.Position = 0;
242+
using (var reader = new StreamReader (stream)) {
243+
string environment = reader.ReadToEnd ();
244+
StringAssert.Contains ("__XA_DSO_IN_APK=1", environment, "`__XA_DSO_IN_APK=1` should be set via @(AndroidEnvironment)");
245+
}
246+
}
247+
}
248+
}
249+
}
250+
}
251+
205252
[Test]
206253
public void ExplicitPackageNamingPolicy ()
207254
{

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,20 @@ bool IsMainLauncher (XElement intentFilter)
644644
intentFilter.Elements (entry.Key).Any (e => ((string) e.Attribute (attName) == entry.Value)));
645645
}
646646

647+
/// <summary>
648+
/// Returns the value of //application/@android:extractNativeLibs.
649+
/// </summary>
650+
public bool ExtractNativeLibraries ()
651+
{
652+
string text = app?.Attribute (androidNs + "extractNativeLibs")?.Value;
653+
if (bool.TryParse (text, out bool value)) {
654+
return value;
655+
}
656+
657+
// If android:extractNativeLibs is omitted, returns true.
658+
return true;
659+
}
660+
647661
XElement ActivityFromTypeDefinition (TypeDefinition type, string name, int targetSdkVersion)
648662
{
649663
if (name.StartsWith ("_"))

src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
<Compile Include="Tasks\JarToXml.cs" />
123123
<Compile Include="Tasks\GetJavaPlatformJar.cs" />
124124
<Compile Include="Tasks\GetFilesThatExist.cs" />
125+
<Compile Include="Tasks\ReadJavaStubsCache.cs" />
125126
<Compile Include="Tasks\RemoveRegisterAttribute.cs" />
126127
<Compile Include="Tasks\GetMonoPlatformJar.cs" />
127128
<Compile Include="Tasks\GeneratePackageManagerJava.cs" />

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

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
9191
<UsingTask TaskName="Xamarin.Android.Tasks.GetExtraPackages" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
9292
<UsingTask TaskName="Xamarin.Android.Tasks.CopyGeneratedJavaResourceClasses" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
9393
<UsingTask TaskName="Xamarin.Android.Tasks.ResolveLibraryProjectImports" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
94+
<UsingTask TaskName="Xamarin.Android.Tasks.ReadJavaStubsCache" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
9495
<UsingTask TaskName="Xamarin.Android.Tasks.ReadLibraryProjectImportsCache" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
9596
<UsingTask TaskName="Xamarin.Android.Tasks.ScanAssemblies" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
9697
<UsingTask TaskName="Xamarin.Android.Tasks.CheckProjectItems" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
@@ -2284,7 +2285,7 @@ because xbuild doesn't support framework reference assemblies.
22842285
<Target Name="_GenerateJavaStubs"
22852286
DependsOnTargets="_SetLatestTargetFrameworkVersion;_PrepareAssemblies;$(_AfterPrepareAssemblies)"
22862287
Inputs="$(MSBuildAllProjects);@(_ResolvedAssemblies);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache);@(_AndroidResourceDest)"
2287-
Outputs="$(IntermediateOutputPath)_javastubs.stamp">
2288+
Outputs="$(_AndroidStampDirectory)_GenerateJavaStubs.stamp">
22882289
<GenerateJavaStubs
22892290
ResolvedAssemblies="@(_ResolvedAssemblies)"
22902291
ResolvedUserAssemblies="@(_ResolvedUserAssemblies)"
@@ -2308,7 +2309,8 @@ because xbuild doesn't support framework reference assemblies.
23082309
PackageNamingPolicy="$(AndroidPackageNamingPolicy)"
23092310
ApplicationJavaClass="$(AndroidApplicationJavaClass)"
23102311
FrameworkDirectories="$(_XATargetFrameworkDirectories);$(_XATargetFrameworkDirectories)Facades"
2311-
AcwMapFile="$(_AcwMapFile)">
2312+
AcwMapFile="$(_AcwMapFile)"
2313+
CacheFile="$(IntermediateOutputPath)javastubs.cache">
23122314
</GenerateJavaStubs>
23132315
<ConvertCustomView
23142316
Condition="Exists('$(_CustomViewMapFile)')"
@@ -2317,13 +2319,45 @@ because xbuild doesn't support framework reference assemblies.
23172319
ResourceDirectories="$(MonoAndroidResDirIntermediate);@(LibraryResourceDirectories)"
23182320
ResourceNameCaseMap="$(_AndroidResourceNameCaseMap)"
23192321
/>
2320-
<Touch Files="$(IntermediateOutputPath)_javastubs.stamp" AlwaysCreate="True" />
2322+
<Touch Files="$(_AndroidStampDirectory)_GenerateJavaStubs.stamp" AlwaysCreate="True" />
2323+
<ItemGroup>
2324+
<FileWrites Include="$(IntermediateOutputPath)javastubs.cache" Condition="Exists('$(IntermediateOutputPath)javastubs.cache')" />
2325+
</ItemGroup>
2326+
</Target>
2327+
2328+
<Target Name="_ReadJavaStubsCache">
2329+
<ReadJavaStubsCache CacheFile="$(IntermediateOutputPath)javastubs.cache">
2330+
<Output TaskParameter="EmbeddedDSOsEnabled" PropertyName="_EmbeddedDSOsEnabled" />
2331+
</ReadJavaStubsCache>
2332+
</Target>
2333+
2334+
<Target Name="_SetupEmbeddedDSOs"
2335+
Condition=" '$(_EmbeddedDSOsEnabled)' == 'True' "
2336+
Inputs="$(IntermediateOutputPath)javastubs.cache"
2337+
Outputs="$(IntermediateOutputPath)dsoenvironment.txt">
2338+
<WriteLinesToFile
2339+
File="$(IntermediateOutputPath)dsoenvironment.txt"
2340+
Lines="__XA_DSO_IN_APK=1"
2341+
Overwrite="True"
2342+
/>
2343+
<PropertyGroup>
2344+
<AndroidStoreUncompressedFileExtensions>.so;$(AndroidStoreUncompressedFileExtensions)</AndroidStoreUncompressedFileExtensions>
2345+
</PropertyGroup>
23212346
<ItemGroup>
2322-
<FileWrites Include="$(IntermediateOutputPath)_javastubs.stamp" />
2347+
<AndroidEnvironment Include="$(IntermediateOutputPath)dsoenvironment.txt" />
2348+
<FileWrites Include="$(IntermediateOutputPath)dsoenvironment.txt" />
23232349
</ItemGroup>
23242350
</Target>
23252351

2326-
<Target Name="_GetAddOnPlatformLibraries" DependsOnTargets="_GenerateJavaStubs">
2352+
<PropertyGroup>
2353+
<_GetAddOnPlatformLibrariesDependsOn>
2354+
_GenerateJavaStubs;
2355+
_ReadJavaStubsCache;
2356+
_SetupEmbeddedDSOs;
2357+
</_GetAddOnPlatformLibrariesDependsOn>
2358+
</PropertyGroup>
2359+
2360+
<Target Name="_GetAddOnPlatformLibraries" DependsOnTargets="$(_GetAddOnPlatformLibrariesDependsOn)">
23272361
<GetAddOnPlatformLibraries
23282362
AndroidSdkPlatform="$(_AndroidApiLevel)"
23292363
AndroidSdkDir="$(_AndroidSdkDirectory)"
@@ -3195,7 +3229,6 @@ because xbuild doesn't support framework reference assemblies.
31953229
<Delete Files="$(MonoAndroidIntermediate)__AndroidNativeLibraries__.zip" />
31963230
<Delete Files="$(MonoAndroidIntermediate)stub_application_data.txt" />
31973231
<Delete Files="$(IntermediateOutputPath)_javac.stamp" />
3198-
<Delete Files="$(IntermediateOutputPath)_javastubs.stamp" />
31993232
<Delete Files="$(_AndroidResFlagFile)" />
32003233
<Delete Files="$(_AndroidLinkFlag)" />
32013234
<Delete Files="$(_AndroidComponentResgenFlagFile)" />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.jar

tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,6 @@
1818
<AndroidUseLatestPlatformSdk>true</AndroidUseLatestPlatformSdk>
1919
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
2020
<AndroidSupportedAbis>armeabi-v7a;x86</AndroidSupportedAbis>
21-
22-
<!-- This should be removed once XA build tasks are updated to support embedded DSOs.
23-
The same applies to the `__XA_DSO_IN_APK=1` line in Environment.txt
24-
-->
25-
<AndroidStoreUncompressedFileExtensions>.so</AndroidStoreUncompressedFileExtensions>
2621
</PropertyGroup>
2722

2823
<PropertyGroup Condition=" '$(UnitTestsMode)' == 'true' ">

tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@ debug.mono.debug=1
22
MONO_LOG_LEVEL=debug
33
MONO_LOG_MASK=asm
44
MONO_XDEBUG=1
5-
__XA_DSO_IN_APK=1

0 commit comments

Comments
 (0)