Gradle DSL from developer perspective

Mar 26, 2016


Gradle is the official build tool for Android. Compared with Maven, it’s more dynamic so developers can easily define their customized building logics. It could easy for anyone to be lost in the new Gradle world --- DSK, task re-ordering/injection, incremental build, Gradle demo --- there are a lot for new features and concepts to learn. From an Android developer's point of view, understanding the configuration language of Gradle is the most important. So let's take a deep look into Gradle DSL.

The Gradle system can be divided into two structural parts: Gradle runtime and Gradle plugins. Gradle runtime is the libs and executables inside the Gradle installation dir (also the Gradlew scripts and the libs inside gradle/wrapper folder if you are using a Gradle wrapper). Runtime works as a low-level service which start up the build system and reads the configuration files (build.gradle, settings.gradle). It also loads plugins mentioned in the configuration files. The concrete build logic of tasks are mostly defined by plugins. For Android, the logic of how to build an apk/aar is declared inside the Google android plugin.

Every Android developer could already know the content of build.gradle and settings.gradle under your project. In this article we call the set of Gradle configration files "Gradle DSL". There are multiple confiugrations like dependencies, android, publish etc. The configuration language of Gradle is Groovy. The below configuration will be used thoroughly in this article:

apply plugin: 'com.android.library'
buildscript {
    repositories {
        mavenLocal()
        maven { url "http://somewhere.com/mvn/repository"}
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.1'
    }
}

android {
  compileSdkVersion 23
    buildToolsVersion "23.0.2"

  sourceSets {
    main {
      mainifest.srcFile "AndroidManifest.xml"
      java.srcDirs = ['src']
      aidl.srcDirs = ['src']
      res.srcDirs = ['res']
      assets.srcDirs = ['assets']
    }
  }
}

How does Gradle plugin loaded? Where the configuration block "android" is defined?

The "apply" in the 1st line specifies the name of plugin to be loaded. The "buildscript" block (line 7-15) specifies the repository to fine the plugin jar file. In our example we define two repositories: local Maven repo and a public Maven repo. The "classpath" line (line 13) specifies the GAV coordinate for the plugin: group=com.android.tools.build, artifact=gradle, version=1.3.1. With GAV, Gradle runtime could locate and download the plugin artifact from Maven repository. By extracting gradle-1.3.1.jar, we can find there are plenty of .properties files inside --- one of them (file name: com.android.library.properties) defines the implementation class of plugin 'com.android.library':
implementation-class=com.android.build.gradle.LibraryPlugin
Gradle runtime will load the class and execute the menthod apply(Project). The core logic of LibraryPlugin.apply() is at its supper class, which is com.android.build.gradle.BasePlugin.apply():
protected void apply(Project project) {
        ......

        SpanRecorders.record(project, ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSTION_CREATION) {
            createExtension()
        }

        SpanRecorders.record(project, ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION) {
            createTasks()
        }
}
We take a deep look into the method createExtension(). Extension is a conception in Gradle. In fact, one extension defines how a configuration block in DSL is parsed. The implementation of createExtension() is like following:
private void createExtension() {
    ......
  extension = project.extensions.create('android', getExtensionClass(),
                (ProjectInternal) project, instantiator, androidBuilder, sdkHandler,
                buildTypeContainer, productFlavorContainer, signingConfigContainer,
                extraModelInfo, isLibrary())
    ......
}
The first parameter "android" of project.extensions.create() is relate to the configuration block with the same name in Gradle DSL. Here the plugin links the String "android" in DSL to a class returned from getExtensionClass(), which is the second parameter of project.extensions.create(). LibraryPlugin.getExtensionClass() is as follows:
public Class<? extends BaseExtension> getExtensionClass() {
    return LibraryExtension.class
}
So far, "android" in DSL is mapped to Extension class com.android.build.gradle.LibraryExtension. When Gradle reads every sub-configuration entry in "android{}", it tries to locate the related field or method in LibraryExtension class.

How configuration entries like "compileSdkVersion" works?

When I was a newbie in the Gradle world, the only way to know the configuration entries are by reading Gradle documents. However, documents are far from perfect so it's not easy to know how to config all the entries. Frankly speaking, if you get to know a little bit how Gradle plugin works, you can query all the configration entries available by reading a little code. Let's fall back to the previous sample. com.android.build.gradle.LibraryExtension derives from BaseExtension. Take a look at the definition of BaseExtension:
void compileSdkVersion(String version) {
    checkWritability()
    this.target = version
}

void compileSdkVersion(int apiLevel) {
    compileSdkVersion("android-" + apiLevel)
}
In fact, the line "compileSdkVersion 23" in DSL, maps to the invokation of method BaseExtension.compileSdkVersion(int). For every configuration name "abcd", Gradle tries to set the value in the related Extension class: - abcd() method - setAbcd() method - field abcd So you can also config it like "compileSdkVersion 'android-23'". After the version of SDK is set, Gradle will generate the path to the build-tools. You could also see there are bunch of other properties defined in BaseExtension like: useLibrary(String), buildTypes(Action), manifestOptions(Action), signingConfigs(Action) --- they are all related to the entries in DSL.

About the dynamic

The mechanism of loading configurations is a kind of static way --- Gradle reads DSL and set related properties. But the power of Gradle is far more than this. For example, we can insert any code at the begining and end of a task:
tasks.whenTaskAdded { task ->
  if(task.name.startsWith(merge) && task.name.endsWith("Assets")){
    task.doLast {
      def buildType = task.name.substring(5,task.name.length() - 6).toLowerCase()
            if(project.android.signingConfigs.getAt(buildType).getSignOnline()){
              org.apache.commons.io.FileUtils.copyFileToDirectory(task.project.file("env/assets.release/SE.bin"),task.outputDir)
            }else{
              org.apache.commons.io.FileUtils.copyFileToDirectory(task.project.file("env/assets.debug/SE.bin"),task.outputDir)
            }
    }
  }
}
Here we insert a copying logic at the end of tasks whose name starts with "merge" and ends with "Assets". In the example the file SE.bin is copied to the output folder of take mergeXXXAssets, so it will be included in the assets folder of apk. "afterEvaluate {}" could also implement the same effect:
afterEvaluate {
    // set parameter for dex task: —-multi-dex和—main-dex-list
   tasks.matching {
      it.name.equals('dexDebug')
   }.each { dx ->
      if (dx.additionalParameters == null) {
         dx.additionalParameters = ['--multi-dex', "--main-dex-list=" + mianDexFilePath]
      } else {
         dx.additionalParameters += '--multi-dex'
         dx.additionalParameters += "--main-dex-list=" + mianDexFilePath
      }
   }
}
The delegate object of "afterEvaluate" is the object of Project, as it is defined in the top level of DSL. It's a shortcut syntax, which is the same as "project.afterEvaluate{}". If it is written inside a task, the default delegate is the task object. As there is no method nor field named "afterEvaluate", Gradle will report the error.

Summary

I hope you understand the essential of Gradle DSL is the mappings to Extension classes, which are loaded through Gradle plugins. Gradle is initially designed to provide a powerful way to extend the build logic through such mechanism. Indeed it's much agile than XML by Maven. However, with this mechanism the learning curve is much sharp than other build tools, especially at start time.