mobile/android/gradle/with_gecko_binaries.gradle
author Nick Alexander <nalexander@mozilla.com>
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 R.java 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 http://mozilla.org/MPL/2.0/. */

// We run fairly hard into a fundamental limitation of the Android Gradle
// plugin.  There are many bugs filed about this, but
// https://code.google.com/p/android/issues/detail?id=216978#c6 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
// http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Library-Publication)
// 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.
evaluationDependsOn(':omnijar')

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

    // See comment in :omnijar project regarding interface mismatches here.
    inputs.file(project(':omnijar').sourceSets.main.resources.srcDirs).skipWhenEmpty() 

    // 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(project.name) ? "fennec" : "geckoview"
    into("${project.buildDir}/generated/omnijar")
    from("${topobjdir}/dist/${omnijar_dir}/omni.ja",
         "${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) {
    into("${project.buildDir}/generated/jniLibs")
    from("${topobjdir}/dist/fennec/lib")
}

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) {
    into("${project.buildDir}/generated/assets")
    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 { it.name.capitalize() }
    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 = variant.buildType.name

    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${variant.name.capitalize()}JarArtifact"]
    } else {
        jarTask = tasks["jar${variant.name.capitalize()}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
    // https://github.com/evant/gradle-retrolambda/commit/15108c65ee43be499a1359d9d4f88b0851d46769
    // suggest that it is.  It certainly doesn't hurt.
    def prepareDependenciesTask = tasks.getByName("prepare${variant.name.capitalize()}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}${variant.name.capitalize()}")
    } else {
        wrapperTask = task("generateJNIWrappersFor${module}${variant.name.capitalize()}", 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${variant.name.capitalize()}"].dependsOn wrapperTask
    } else {
        tasks["assemble${variant.name.capitalize()}"].dependsOn wrapperTask
    }
}