Build an Android app. Task with an asterisk





Hello, Habr! In the summer, I spoke at the Summer Droid Meetup with a report on building an Android application. The video version can be found here: habr.com/ru/company/funcorp/blog/462825 . And for those who like to read more, I just wrote this article.



It's about what it is - an Android application. We will collect in different ways Hello, world !: start from the console and see what happens under the hood of build systems, then back to the past, remember Maven and learn about modern Bazel and Buck solutions. And finally, all this is comparable.



We thought about a possible change in the assembly system when we started a new project. It seemed to us that this is a good opportunity to look for some alternatives to Gradle. Moreover, it is easier to do this at the start than to translate an existing project. The following Gradle flaws pushed us to this step:





APK



First of all, remember what the Android application consists of: compiled code, resources and AndroidManifest.xml.







Sources are in the classes.dex file (there may be several files, depending on the size of the application) in a special dex format that the Android virtual machine can work with. Today it is ART, on older devices - Dalvik. In addition, you can find the lib folder, where the native sources are arranged in subfolders. They will be named depending on the target processor architecture, for example x86, arm, etc. If you use exoplayer, then you probably have lib. And the aidl folder, which contains interprocess communication interfaces. They will come in handy if you need to access a service running in another process. Such interfaces are used both in Android itself and inside GooglePlayServices.



Various non-compiled resources like pictures are in the res folder. All compiled resources, such as styles, lines, etc., are merged into a resource.arsc file. In the assets folder, as a rule, they put everything that does not fit into resources, for example, custom fonts.



In addition to all this, the APK contains AndroidManifest.xml. In it, we describe the various components of the application, such as Activity, Service, different permissions, etc. It lies in binary form, and in order to look inside, it will first have to be converted into a human-readable file.



CONSOLE



Now that we know what the application consists of, we can try to build Hello, world! from the console using the tools that the Android SDK provides. This is a pretty important step in understanding how build systems work, because they all rely on these utilities to one degree or another. Since the project is written in Kotlin, we need its compiler for the command line. It is easy to download separately.



The assembly of the application can be divided into the following steps:





build script
function preparedir() { rm -r -f $1 mkdir $1 } PROJ="src/main" LIBS="libs" LIBS_OUT_DIR="$LIBS/out" BUILD_TOOLS="$ANDROID_HOME/build-tools/28.0.3" ANDROID_JAR="$ANDROID_HOME/platforms/android-28/android.jar" DEBUG_KEYSTORE="$(echo ~)/.android/debug.keystore" GEN_DIR="build/generated" KOTLIN_OUT_DIR="$GEN_DIR/kotlin" DEX_OUT_DIR="$GEN_DIR/dex" OUT_DIR="out" libs_res="" libs_classes="" preparedir $LIBS_OUT_DIR aars=$(ls -p $LIBS | grep -v /) for filename in $aars; do DESTINATION=$LIBS_OUT_DIR/${filename%.*} echo "unpacking $filename into $DESTINATION" unzip -o -q $LIBS/$filename -d $DESTINATION libs_res="$libs_res -S $DESTINATION/res" libs_classes="$libs_classes:$DESTINATION/classes.jar" done preparedir $GEN_DIR $BUILD_TOOLS/aapt package -f -m \ -J $GEN_DIR \ -M $PROJ/AndroidManifest.xml \ -S $PROJ/res \ $libs_res \ -I $ANDROID_JAR --auto-add-overlay preparedir $KOTLIN_OUT_DIR compiledKotlin=$KOTLIN_OUT_DIR/compiled.jar kotlinc $PROJ/java $GEN_DIR -include-runtime \ -cp "$ANDROID_JAR$libs_classes"\ -d $compiledKotlin preparedir $DEX_OUT_DIR dex=$DEX_OUT_DIR/classes.dex $BUILD_TOOLS/dx --dex --output=$dex $compiledKotlin preparedir $OUT_DIR unaligned_apk=$OUT_DIR/unaligned.apk $BUILD_TOOLS/aapt package -f -m \ -F $unaligned_apk \ -M $PROJ/AndroidManifest.xml \ -S $PROJ/res \ $libs_res \ -I $ANDROID_JAR --auto-add-overlay cp $dex . $BUILD_TOOLS/aapt add $unaligned_apk classes.dex rm classes.dex aligned_apk=$OUT_DIR/aligned.apk $BUILD_TOOLS/zipalign -f 4 $unaligned_apk $aligned_apk $BUILD_TOOLS/apksigner sign --ks $DEBUG_KEYSTORE $aligned_apk
      
      







According to the figures, it turns out that a clean assembly takes 7 seconds, and the incremental assembly does not lag behind it, because we do not cache anything and rebuild everything every time.



Maven



It was developed by the guys at the Apache Software Foundation to build Java projects. Build configs for it are described in XML. The early revisions of Maven were collected by Ant, and now they have switched to the latest stable release.



Pros of Maven:





Cons Maven:





To build, we need to create pom.xml, which contains a description of our project. In the header, we indicate the basic information about the collected artifact, as well as the version of Kotlin.



build config pom.xml
 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>myapplication</artifactId> <version>1.0.0</version> <packaging>apk</packaging> <name>My Application</name> <properties> <kotlin.version>1.3.41</kotlin.version> </properties> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>com.google.android</groupId> <artifactId>android</artifactId> <version>4.1.1.4</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>${kotlin.version}</version> <executions> <execution> <id>compile</id> <phase>process-sources</phase> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>com.simpligility.maven.plugins</groupId> <artifactId>android-maven-plugin</artifactId> <extensions>true</extensions> <configuration> <sdk> <platform>28</platform> <buildTools>28.0.3</buildTools> </sdk> <failOnNonStandardStructure>false</failOnNonStandardStructure> </configuration> </plugin> </plugins> </build> </project>
      
      







In terms of numbers, everything is not very rosy. A clean assembly takes about 12 seconds, while an incremental one - 10. This means that Maven somehow reuses artifacts from previous assemblies, or, in my opinion, it is more likely that the plug-in for building an Android project prevents it from doing this



Now they are using all this, I think, first of all, the creators of the plugin are the guys from simpligility. More reliable information about this issue could not be found.



Bazel



Engineers in the bowels of Google invented Bazel to build their projects and relatively recently transferred it to open source. For the description of build-configs python-like Skylark or Starlark is used, both names have a place to be. It is assembled using its own latest stable release.



Pros of Bazel:





Cons of Bazel:





Conceptually, the basic Bazel config consists of WORKSPACE, where we describe all sorts of global things for a project, and BUILD, which contains directly targets for assembly.

Let's describe WORKSPACE. Since we have an Android project, the first thing we configure is the Android SDK. Also, a rule for unloading configs is imported here. Then, since the project is written in Kotlin, we must specify the rules for it. Here we do this, referring to a specific revision directly from the git repository.



WORKSPACE
 android_sdk_repository( name = "androidsdk", api_level = 28, build_tools_version = "28.0.3" ) load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # # KOTLIN RULES # RULES_KOTLIN_VERSION = "990fcc53689c8b58b3229c7f628f843a60cb9f5c" http_archive( name = "io_bazel_rules_kotlin", url = "https://github.com/bazelbuild/rules_kotlin/archive/%s.zip" % RULES_KOTLIN_VERSION, strip_prefix = "rules_kotlin-%s" % RULES_KOTLIN_VERSION ) load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kotlin_repositories", "kt_register_toolchains") kotlin_repositories() kt_register_toolchains()
      
      







Now let's get started on the BUILD.



First we import the rule for assembling Kotlin and describe what we want to collect. In our case, this is an Android application, so we use android_binary, where we set the manifest, minimum SDK, etc. Our application will depend on the source, so we mention them in deps and move on to what they are and where to find them. The code will also depend on the resources and the appcompat library. For resources, we use the usual target for assembling android sources, but we only assign resources to it without java classes. And we describe a couple of rules that import third-party libraries. It also mentions appcompat_core, which appcompat depends on.



BUILD
 load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") android_binary( name = "app", custom_package = "com.example.myapplication", manifest = "src/main/AndroidManifest.xml", manifest_values = { "minSdkVersion": "15", }, deps = [ ":lib", ], ) kt_android_library( name = "lib", srcs = glob(["src/main/java/**/*"]), deps = [ ":res", ":appcompat", ], ) android_library( name = "res", resource_files = glob(["src/main/res/**/*"]), manifest = "src/main/AndroidManifest.xml", custom_package = "com.example.myapplication", ) aar_import( name = "appcompat", aar = "libs/appcompat.aar", deps = [ ":appcompat_core", ] ) aar_import( name = "appcompat_core", aar = "libs/core.aar", )
      
      







In numbers for such a small project, everything looks sad. More than half a minute to a clean build Hello, world! - lots of. Incremental build time is also far from perfect.



Bazel is used by its creators (Google) for some of their projects, including server ones, as well as Dropbox and Huawei, which collect mobile applications for them. And the notorious Dagger 2 is also going to Bazel.



Buck



It was invented by defectors from Google to Facebook. He used Python to describe the configs, and then migrated to the Skylark mentioned today. He is going, all of a sudden, using the Ant system.



Buck Pros:





Cons Buck:





So, what does the assembly config for Hello, world! through buck? Here we describe one configuration file, where we indicate that we want to build an Android project that will be signed with a debug key. The application will likewise depend on the source - lib in the deps array. Next comes the target with signature settings. I am using a debit key that comes with the Android SDK. Immediately after it is a target that will collect the source of Kotlin for us. Like Bazel, it depends on resources and compatibility libraries.



We describe them. There is a separate target for resources in Buck, so bikes are not useful. Following are the rules for downloaded third-party libraries.



BUILD
 android_binary( name = 'app', manifest = 'src/main/AndroidManifest.xml', manifest_entries = { 'min_sdk_version': 15, }, keystore = ':debug_keystore', deps = [ ':lib', ], ) keystore( name = 'debug_keystore', store = 'debug.keystore', properties = 'debug.keystore.properties', ) android_library( name = 'lib', srcs = glob(['src/main/java/*.kt']), deps = [ ':res', ':compat', ':compat_core', ], language = 'kotlin', ) android_resource( name = 'res', res = "src/main/res", package = 'com.example.myapplication', ) android_prebuilt_aar( name = 'compat', aar = "libs/appcompat.aar", ) android_prebuilt_aar( name = 'compat_core', aar = "libs/core.aar", )
      
      







This whole thing is going very briskly. A clean assembly takes a little more than 7 seconds, while an incremental assembly takes completely invisible 200 milliseconds. I think this is a very good result.



This is what Facebook does. In addition to their flagship application, they collect Facebook Messenger for them. And Uber, who made the plugin for Gradle and Airbnb with Lyft.



findings



Now that we’ve talked about each build system, we can compare them with each other using the example Hello, world! The console assembly pleases with its stability. The execution time of the script from the terminal can be considered a reference for the assembly of clean builds, because third-party costs for parsing scripts are minimal here. In this case, I would call Maven an obvious outsider for an extremely insignificant increase in incremental assembly. Bazel parses configs for a very long time and initializes: there is an idea that it somehow caches the initialization results, because the incremental build it runs is much faster than clean. Buck is the undisputed leader of this collection. Very fast both clean and incremental assembly.







Now compare the pros and cons. I will not include Maven in the comparison, because it clearly loses to Gradle and is almost never used in the market. I unite Buck and Bazel, because they have approximately the same advantages and disadvantages.



So, about Gradle:





About Buck / Bazel:





Do not forget about the cons.



Gradle pays for its simplicity by being slow and inefficient.

Buck / Bazel, on the contrary, because of its speed, suffers from the need to describe the build process in configs in more detail. Well, since they appeared on the market relatively recently, there are not many documentation and various cheat sheets.



iFUNNY



Perhaps you have a question how we collect iFunny. Just like many - using Gradle. And there are reasons for this:



  1. It is not yet clear what gain in assembly speed this will give us. A clean build of iFunny takes almost 3 minutes, and incremental - about a minute, which is actually not very long.
  2. Buck or Bazel build configs are more difficult to maintain. In the case of Buck, you also need to monitor the relevance of the connected libraries and the libraries on which they depend.
  3. It is banal expensive to transfer an existing project from Gradle to Buck / Bazel, especially in conditions of incomprehensible profit.


If your project is going to take more than 45 minutes and there are about 20 people in the Android development team, then it makes sense to think about changing the build system. If you and your friend are sawing a startup, then use Gradle and drop these thoughts.







I will be glad to discuss the prospects of Gradle alternatives in the comments!

Link to the project



All Articles