As a way of familiarizing myself with the EBuild code and architecture, I decided to look into what would need to change to support App Bundles, and I figured I should log everything here for future reference, for whoever ends up working on this, even if it’s me.
Steps to get App Bundles working in EBuild:
- Register AAPT2 as an Android tool.
- Either package the latest
Bundletool.jar with EBuild or add logic to download it if absent.
- Tweak the
GenerateAndroidResources task to use
aapt2 compile and
aapt2 link when AAPT2 is available.
- Tweak the
AndroidPack task to use Bundletool to generate an App Bundle and APK when AAPT2 is available.
The larger to-do is supporting AAPT2, which is required for App Bundles. AAPT2 is a significant feature in its own right, since it enables incremental builds by diff-ing binaries of each app resource, which could greatly reduce build times for large Android projects.
A few things to note with AAPT2:
Min version: AAPT2 is available in SDK Build Tools 26.0.2 and above, so projects using less than 26.0.2 would need to update their build tools or revert to AAPT.
Mutually exclusive: I tried creating an App Bundle with AAPT2, using a dex file created through EBuild (which had compiled the
class files using an
R.java generated by AAPT) and it builds without issue but immediately crashes at runtime. I think this is the same issue that the docs refer to here:
If your app has a dependency on a third party library that was built using older versions of the Android SDK Build Tools, your app might crash at runtime without displaying any errors or warnings. This crash might occur because, during the library’s creation, the
R.java fields are declared
final and, as a result, all of the resource IDs are inlined in the library’s classes. AAPT2 relies on being able to re-assign IDs to library resources when building your app. If the library assumes the IDs to be
final and has them inlined in the library dex, there will be a runtime mismatch.
This is important because it means that if a project is trying to generate both an APK with AAPT and an App Bundle with AAPT2, EBuild cannot share intermediate resources between the two. The
R.java is incompatible, and the App Bundle also requires that app resources (drawables, layouts, strings, colors, etc) be in Google’s Protocol Buffer format, which APKs do not support. The format is easily acheived with a
--proto-format switch on
aapt2 link, but it means that the two cannot share these resources either.
Two phases: instead of AAPT’s single
aapt package command, AAPT2 has two phases.
aapt2 compile :: is run for every file in the app’s
res directory. This compile phase generates the protocol buffer binaries of the resource files, which are used for intermediary diffing and are added to the final App Bundle.
aapt2 link :: “merges all the intermediate files generated from the compilation phase such as resource tables, binary XML files, and processed PNG files and packages them into a single APK” (from docs). (Note this APK does not contain the dex files and is not an executable APK but rather an intermediary archive file in the build process, which for some reason they called an apk instead of a zip). The
link phase also generates the
App Bundles are not executable. An App Bundle is an archive that includes a BundleConfig.pb file (generated by
aapt2 link - lists all of the modules in the App Bundle) and a
zip file for each module. (Allowing multiple modules would be a good future feature, but for now EBuild could just put everything in the
base.zip module). The
zip file contains the app manifest, the dex files, the
assets folder and native files, and all of the app resources along with their resource tables in protocol buffer (binary) format (see image below, ignoring the “Dynamic Feature” sections for now).
To get an executable APK from an App Bundle, you have to run Google’s Bundletool utility. This is the same tool that Google runs on their servers to generate custom APKs from an uploaded App Bundle.
Note: Since an App Bundle is not executable, this means that if the project is running in Debug mode, it would have to generate a final APK to install on the debug device for testing. Bundletool does have a
build-apks command to generate an archive of all possible apks from the App Bundle, and an
install-apks command which will automatically install the correct APK for a device connected to the adb bridge. The tool is very easy to use, but I don’t know how well it would cooperate with the debugger as I’ve not tested that aspect of it. Bundletool does offer another command
bundletool build-apks --mode=universal which will generate a monolithic APK that can run on any device. This APK could then be installed via adb like Elements does now. This may be the most straightforward way to go about building/installing an APK for debugging.
The best solution for EBuild is probably to always use AAPT2 if it is available, and only use AAPT as a fallback if AAPT2 is not available. Under this architecture, if AAPT2 is not available then the EBuild flow would be exactly like it is now, using AAPT and building an APK with
apkbuilder. If AAPT2 is available, then EBuild would always build an App Bundle, and then optionally build an APK at the end if (a) the build is in Debug mode, or (b) the user has the project settings configured to generate an APK in Release mode. It is a bit longer process to get a simple APK for debugging, but I don’t think it would add much/any build time, since AAPT2 allows for faster incremental builds.
So… EBuild changes needed when using AAPT2:
- Recursively loop through
res folder and run
aapt2 compile for each file.
aapt2 link (note, every resource from the previous step is added as a command line argument in this command, so they should be added to an array as the loop runs for the compile phase).
- Extract resources from the intermediary APK created by
- Zip the extracted resources, dex files, manifest, assets, and jni files into a
- Build the App Bundle: run
bundletool build-bundle --modules=base.zip --output=[appname].aab
- Optionally generate an APK:
bundletool build-apks --mode=universal.