author Nick Alexander <>
Mon, 23 Oct 2017 13:24:58 -0700
changeset 440947 34f83aab44e6c0a68a448c3c10f61267c0eb649e
parent 440930 0de94b56338e29e469b762fe9e98f2b6e7b99c49
child 441054 a73e202ca31d30bc2b96418fa81d1096f8dc731e
permissions -rw-r--r--
Bug 1411688 - Part 1: Make --with-gradle handle single-locale repacks. r=snorp Single-locale repacks do the following: Download existing APK; unzip APK; update l10n resources; |mach package| with IS_LANGUAGE_REPACK=1. This is pretty hard to accommodate, but we can try. The key issues here are to recognize when IS_LANGUAGE_REPACK=1 and not ask for l10n resources (in particular, strings.xml) to be generated. We do need to include the freshly built classes.dex when repackaging, because newer Gradle/aapt doesn't preserve the IDs. MozReview-Commit-ID: 9FvQtmPOUjg

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at */

// We run fairly hard into a fundamental limitation of the Android Gradle
// plugin.  There are many bugs filed about this, but
// is a reason one.
// The issue is that we need fine-grained control over when to include Gecko's
// binary libraries into the GeckoView AAR and the Fennec APK, and that's hard
// to achieve.  In particular:
// * :app:official* wants :geckoview to not include Gecko binaries (official
// *  automation build, before package)
// * :geckoview:withLibraries wants :geckoview to include Gecko binaries
// * (automation build, after package)
// * non-:app:official* wants :geckoview to include Gecko binaries (local
// * build, always after package)
// publishNonDefault (see
// is intended to address this, but doesn't handle our case.  That option always
// builds *all* configurations, which fails when the required Gecko binaries
// don't exist (automation build, before package).  So instead, we make both
// :app and :geckoview both know how to include the Gecko binaries, and use a
// non-default, non-published :geckoview:withGeckoBinaries configuration to
// handle automation's needs.  Simple, right?

// The omnijar inputs are listed as resource directory inputs to a dummy JAR.
// That arrangement labels them nicely in IntelliJ.  See the comment in the
// :omnijar project for more context.

task buildOmnijar(type:Exec) {
    dependsOn rootProject.generateCodeAndResources

    // See comment in :omnijar project regarding interface mismatches here.

    // Produce a single output file.
    outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"

    workingDir "${topobjdir}"

    commandLine mozconfig.substs.GMAKE
    args '-C'
    args "${topobjdir}/mobile/android/base"
    args 'gradle-omnijar'

    // Only show the output if something went wrong.
    ignoreExitValue = true
    standardOutput = new ByteArrayOutputStream()
    errorOutput = standardOutput
    doLast {
        if (execResult.exitValue != 0) {
            throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")

task syncOmnijarFromDistDir(type: Sync) {
    // :app needs the full Fennec omni.ja, whereas other projects need the GeckoView-specific omni.ja.
    def omnijar_dir = "app".equals( ? "fennec" : "geckoview"
         "${topobjdir}/dist/${omnijar_dir}/assets/omni.ja") {
        // Throw an exception if we find multiple, potentially conflicting omni.ja files.
        duplicatesStrategy 'fail'

task checkLibsExistInDistDir {
    doLast {
        if (syncLibsFromDistDir.source.empty) {
            throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib.  Have you built and packaged?")

task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {

task checkAssetsExistInDistDir {
    doLast {
        if (syncAssetsFromDistDir.source.empty) {
            throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets.  Have you built and packaged?")

task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
    from("${topobjdir}/dist/fennec/assets") {
        exclude 'omni.ja'

ext.configureVariantWithGeckoBinaries = { variant ->
    // Like 'localPhoton' or 'localOldPhoton'; may be null.
    def productFlavor = ""
    def productFlavorNames = variant.productFlavors.collect { }
    if (!productFlavorNames.isEmpty()) {
        productFlavor = productFlavorNames.join()
        // Groovy's `uncapitilize` is not yet available.
        def c = productFlavor.toCharArray()
        c[0] = Character.toLowerCase(c[0])
        productFlavor = new String(c)

    // Like 'debug' or 'release'.
    def buildType =

    syncOmnijarFromDistDir.dependsOn buildOmnijar
    def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
    generateAssetsTask.dependsOn syncOmnijarFromDistDir
    generateAssetsTask.dependsOn syncLibsFromDistDir
    generateAssetsTask.dependsOn syncAssetsFromDistDir

    def sourceSet = productFlavor ? "${productFlavor}${buildType.capitalize()}" : buildType
    android.sourceSets."${sourceSet}".assets.srcDir syncOmnijarFromDistDir.destinationDir
    android.sourceSets."${sourceSet}".assets.srcDir syncAssetsFromDistDir.destinationDir
    android.sourceSets."${sourceSet}".jniLibs.srcDir syncLibsFromDistDir.destinationDir

ext.configureVariantWithJNIWrappers = { variant, module ->
    def jarTask
    if (module == 'Generated') {
        jarTask = tasks["package${}JarArtifact"]
    } else {
        jarTask = tasks["jar${}Classes"]

    if (jarTask.outputs.files.size() != 1) {
        throw new GradleException("Jar task output multiple files other than one single jar")

    // At configuration time, the classpath of dependencies with internal_impl
    // JAR files may not be correct.  This manifests in
    // 'Exception in thread "main" java.lang.NoClassDefFoundError: android/support/v4/app/ActivityCompatApi23$RequestPermissionsRequestCodeValidator'
    // when running |mach gradle clean app:generateJNI...|.  We work around this
    // by configuring the classpath at evaluation-time, not configuration-time.
    // The specific dependency on the `prepareDependencies` task may not be
    // necessary, but commits like
    // suggest that it is.  It certainly doesn't hurt.
    def prepareDependenciesTask = tasks.getByName("prepare${}Dependencies")

    def wrapperTask
    if (System.env.IS_LANGUAGE_REPACK == '1') {
        // Single-locale l10n repacks set `IS_LANGUAGE_REPACK=1` and don't
        // really have a build environment.
        wrapperTask = task("generateJNIWrappersFor${module}${}")
    } else {
        wrapperTask = task("generateJNIWrappersFor${module}${}", type: JavaExec) {
            classpath "${topobjdir}/build/annotationProcessors/annotationProcessors.jar"
            // Configure the classpath at evaluation-time, not at
            // configuration-time: see above comment.
            doFirst {
                classpath variant.javaCompile.classpath
                // Include android.jar.
                classpath variant.javaCompile.options.bootClasspath
            main = 'org.mozilla.gecko.annotationProcessors.AnnotationProcessor'
            args module
            args jarTask.outputs.files.iterator().next()
            workingDir "${topobjdir}/mobile/android/base"
            dependsOn jarTask
            dependsOn prepareDependenciesTask

    if (module == 'Generated') {
        tasks["bundle${}"].dependsOn wrapperTask
    } else {
        tasks["assemble${}"].dependsOn wrapperTask