Even with all the current and upcoming changes in the Android compiler toolchains and build systems used for both applications and the platform, one common issue continues to come up amongst Android platform developer community: What is the best way to develop platform applications given the current state of the tools?
The Problem
With the introduction of the Gradle build system (used by Android Studio), the
default project structure for an application has deviated from the platform's
internal build system (based on Android.mk
files). This creates a problem if
you are working on internal applications destined to be packaged with a custom
system image.
Which build system do you choose? Either way you go, some mapping is required. The default project structure for a platform package (application) looks something like this:
PlatformAppPackage/
aidl/
assets/
AndroidManifest.xml
Android.mk
java/
jni/
res/
Where the contents of the Android.mk
file might include:
Android.mk
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
# Reference java/ and aidl/ source files
# res/ and assets/ are discovered automatically
LOCAL_SRC_FILES := \
$(call all-java-files-under, java) \
$(call all-named-files-under, *.aidl, aidl)
LOCAL_PACKAGE_NAME := MySystemApplication
LOCAL_SDK_VERSION := current
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)
#######################################
include $(CLEAR_VARS)
LOCAL_SRC_FILES:= \
$(call all-cpp-files-under, jni)
LOCAL_MODULE := mypackage_jni
include $(BUILD_SHARED_LIBRARY)
Hacking Android.mk
Most folks are typically inclined to migrate the entire project structure to a
Gradle-based project, so that Android Studio will recognize the sources and the
developers can more easily work with the code. Then the necessary changes are made
to the Android.mk
files to support building with the platform. This would take
the above project and make it look more like this:
GradleAppPackage/
Android.mk // <1>
app/
Android.mk // <1>
build.gradle // <4>
src/
Android.mk // <1>
main/
aidl/
assets/
Android.mk // <2>
AndroidManifest.xml
java/
jni/
res/
build.gradle // <3>
settings.gradle // <5>
<1> Required intermediate makefile to allow build to find real makefile <2> Real makefile used to compile the package <3> Top-level Gradle build file to declare project dependencies <4> Gradle module file used to compile the package <5> Gradle project file allowing top-level to reference the module
The first thing you may notice is that it's makefiles all the way down! What's worse, all those intermediate makefiles look like this:
include $(call all-subdir-makefiles)
Their only purpose is to allow the build system to find the actual makefile that
lives in a subdirectory. Without these, the module might be skipped by the build
system depending on what other directories are around it. The reason for this is
that the build implicitly looks for the res/
and assets/
directories in the
local path.
The build.gradle
files in this case are standard fare that Android Studio would
generate:
build.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
...
}
}
app/build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
applicationId "..."
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
...
}
dependencies {
...
}
Alternate Android.mk
One alternative to remove the excess is to map the implicit dependencies in the
Android.mk
file. We can replace the above Gradle structure with the following:
GradleAppPackage/
Android.mk
app/
build.gradle
src/
main/
aidl/
assets/
AndroidManifest.xml
java/
jni/
res/
build.gradle
settings.gradle
If we modify the base Android.mk
file to look more like this:
Android.mk
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
# Reference java/ and aidl/ source files
LOCAL_SRC_FILES := \
$(call all-java-files-under, app/src/main/java) \
$(call all-named-files-under, *.aidl, app/src/main/aidl)
# Re-map res/ and assets/ directly
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/app/src/main/res
LOCAL_ASSET_DIR := $(LOCAL_PATH)/app/src/main/assets
LOCAL_PACKAGE_NAME := MySystemApplication
LOCAL_SDK_VERSION := current
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)
#######################################
include $(CLEAR_VARS)
# Re-map native code path
LOCAL_SRC_FILES:= \
$(call all-cpp-files-under, app/src/main/jni)
LOCAL_MODULE := mypackage_jni
include $(BUILD_SHARED_LIBRARY)
One advantage to this approach is that you could leverage the product flavors feature of a Gradle build, and potentially map different build targets to each of the variants.
Hacking Gradle
The other option is to keep the source files in the structure that Android.mk
prefers and create a build.gradle
that maps the appropriate directories using
source sets.
This was a common technique in the early days of migrating ADT-based projects to
the Gradle build system before Android Studio came with an importer that moved
all the files around automatically for you. Since then, using the sourceSets
attribute has become somewhat of a lost art amongst Android folks.
In this case, the source tree would look like the following:
PlatformAppPackage/
aidl/
assets/
AndroidManifest.xml
Android.mk
*build.gradle*
java/
jni/
res/
Awesome! We only had to add one file! Let's see what it looks like:
build.gradle
//Top-level buildscript information
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
//App module build information
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
targetSdkVersion 23
...
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['java']
resources.srcDirs = ['java']
aidl.srcDirs = ['aidl']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
}
You may notice that this has the contents of both previous build.gradle
files
condensed into one. If we have no intention of supporting multiple modules in the
same build project, this is perfectly legal.
The key here, though, is the sourceSets
attribute. This maps the source files
and directories from their current locations into the Gradle build without requiring
us to move them to the locations where Gradle would implicitly search.
The advantage to this approach is that it requires the least amount of change to map an existing platform project into allowing Gradle builds.
Summary
So, which method should you choose? I suppose it depends on where you are coming from. If you are integrating an existing Studio project into platform builds, the first option may be less work for you. If the reverse is true, the second approach may make more sense. If you are starting from scratch, pick your favorite.
If you were to corner me at a conference and ask that question, my personal preference
would be to use sourceSets
in Gradle to map a standard project. If your application
is intended for platform builds, this creates consistency between your project and
the rest of the source tree. Gradle functionality is only provided as an extension
to make developer lives easier.