Bug 1411654 - Part 4: Use flavorDimensions to simplify {with,without}GeckoBinaries logic. r=maliu draft
authorNick Alexander <nalexander@mozilla.com>
Thu, 09 Nov 2017 16:47:05 -0800
changeset 749697 0a5854cf80fa746c7c29624100de0ecc36315324
parent 749696 896a8618fee1b990df0c9c830306f39d02d7bd73
child 749698 7f41e2dd90ba5ab358fd064c38796bf0c30aa09d
push id97470
push usernalexander@mozilla.com
push dateWed, 31 Jan 2018 21:14:34 +0000
reviewersmaliu
bugs1411654
milestone60.0a1
Bug 1411654 - Part 4: Use flavorDimensions to simplify {with,without}GeckoBinaries logic. r=maliu MozReview-Commit-ID: 2rbsP6A0BY0
mobile/android/app/build.gradle
mobile/android/geckoview/build.gradle
mobile/android/geckoview_example/build.gradle
mobile/android/gradle.configure
mobile/android/gradle/debug_level.gradle
mobile/android/gradle/product_flavors.gradle
mobile/android/gradle/with_gecko_binaries.gradle
taskcluster/ci/build/android.yml
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -1,15 +1,17 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/app"
 
 apply plugin: 'com.android.application'
 apply plugin: 'checkstyle'
 apply plugin: 'com.getkeepsafe.dexcount'
 apply plugin: 'findbugs'
 
+apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
+
 dexcount {
     format = "tree"
 }
 
 android {
     compileSdkVersion project.ext.compileSdkVersion
 
     defaultConfig {
@@ -86,45 +88,23 @@ android {
     // The "audience" flavour dimension distinguishes between _local_ builds (intended for
     // development) and _official_ builds (intended for testing in automation and to ship in one of
     // the Fennec distribution channels).
     //
     // The "skin" flavor dimension distinguishes between different user interfaces.  We sometimes
     // want to develop significant new user interface pieces in-tree that don't ship (even in the
     // Nightly channel) while under development.  A new "skin" flavour allows us to develop such
     // pieces in Gradle without changing the mainline configuration.
-    flavorDimensions "audience", "skin"
+
+    project.configureProductFlavors.delegate = it
+    project.configureProductFlavors()
+
+    flavorDimensions "audience", "geckoBinaries", "minApi", "skin"
 
     productFlavors {
-        // For API 21+ - with pre-dexing, this will be faster for local development.
-        local {
-            dimension "audience"
-
-            // For pre-dexing, setting `minSdkVersion 21` allows the Android gradle plugin to
-            // pre-DEX each module and produce an APK that can be tested on
-            // Android Lollipop without time consuming DEX merging processes.
-            minSdkVersion 21
-            dexOptions {
-                preDexLibraries true
-            }
-        }
-        // For API < 21 - does not support pre-dexing because local development
-        // is slow in that case.
-        localOld {
-            dimension "audience"
-        }
-
-        // Automation builds.  We use "official" rather than "automation" to drive these builds down
-        // the list of configurations that Android Studio offers, thereby making it _not_ the
-        // default.  This avoids a common issue with "omni.ja" not being packed into the default APK
-        // built and deployed by Android Studio.
-        official {
-             dimension "audience"
-        }
-
         // Since Firefox 57, the mobile user interface has followed the Photon design.
         // Before Firefox 57, the user interface followed the Australis design.
         photon {
             dimension "skin"
         }
     }
 
     sourceSets {
@@ -256,25 +236,20 @@ dependencies {
 
     if (mozconfig.substs.MOZ_ANDROID_GCM) {
         implementation "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
         implementation "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
         implementation "com.google.android.gms:play-services-gcm:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
         implementation "com.google.android.gms:play-services-measurement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
     }
 
-    // Include LeakCanary in most gradle based builds. LeakCanary adds about 5k methods, so we disable
-    // it for the (non-proguarded, non-predex) localOld builds to allow space for other libraries.
-    // Gradle based tests include the no-op version.  Mach based builds only include the no-op version
-    // of this library.
-    // It doesn't seem like there is a non-trivial way to be conditional on 'localOld', so instead we explicitly
-    // define a version of leakcanary for every flavor:
+    // Include LeakCanary in local builds, but not in official builds.  Mach
+    // builds target the official audience, so LeakCanary will not be included
+    // in any Mach build.
     localImplementation 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
-    localOldImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
-    officialImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
     officialImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
     testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
 
     implementation project(path: ':geckoview')
     implementation project(path: ':thirdparty')
 
     testImplementation 'junit:junit:4.12'
     testImplementation 'org.robolectric:robolectric:3.5.1'
@@ -366,28 +341,25 @@ android.applicationVariants.all { varian
     // re-entrant.  Official builds are driven by the moz.build system and
     // should never be re-entrant in this way.
     if (!((variant.productFlavors*.name).contains('official'))) {
         syncPreprocessedJava.dependsOn rootProject.generateCodeAndResources
         syncPreprocessedRes.dependsOn rootProject.generateCodeAndResources
         rewriteManifestPackage.dependsOn rootProject.generateCodeAndResources
     }
 
-    // Official automation builds don't include Gecko binaries, since those binaries are not
-    // produced until after build time (at package time).  official Therefore, automation builds
-    // include the Gecko binaries into the APK at package time.  The "withGeckoBinaries" variant of
-    // the :geckoview project also does this.  (It does what it says on the tin!)  For notes on this
-    // approach, see mobile/android/gradle/with_gecko_binaries.gradle.
-
-    // Like 'local' or 'localOld'.
-    def audienceDimension = variant.productFlavors[0].name
-
-    // :app uses :geckoview:release and handles it's own Gecko binary inclusion,
-    // even though this would be most naturally done in the :geckoview project.
-    if (!audienceDimension.equals('official')) {
+    // When driven from moz.build via |mach build|, Gradle does not require or
+    // use Gecko binaries.  It's only |mach package| that packs the Gecko
+    // binaries into the resulting APK.  The "withoutGeckoBinaries" variants
+    // handle this.  When driven from Android Studio or Gradle, the
+    // "withGeckoBinaries" variants handle packing the Gecko binaries into the
+    // resulting APK (for on-device deployment).  They also update the Omnijars
+    // as necessary, smoothing out the edit-compile-test development cycle.
+    // They do what they say on the tin!
+    if ((variant.productFlavors*.name).contains('withGeckoBinaries')) {
         configureVariantWithGeckoBinaries(variant)
     }
 }
 
 android.applicationVariants.all { variant ->
     configureApplicationVariantWithJNIWrappers(variant, "Fennec")
 }
 
--- a/mobile/android/geckoview/build.gradle
+++ b/mobile/android/geckoview/build.gradle
@@ -1,12 +1,14 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
 
 apply plugin: 'com.android.library'
 
+apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
+
 // This converts MOZ_APP_VERSION into an integer
 // version code.
 //
 // We take something like 58.1.2a1 and come out with 5800102
 // This gives us 3 digits for the major number, and 2 digits
 // each for the minor and build number. Beta and Release
 def computeVersionCode() {
     String appVersion = mozconfig.substs.MOZ_APP_VERSION
@@ -94,24 +96,18 @@ android {
 
         // Official corresponds, roughly, to whether this build is performed on
         // Mozilla's continuous integration infrastructure. You should disable
         // developer-only functionality when this flag is set.
         // This makes no sense for GeckoView and should be removed as soon as possible.
         buildConfigField 'boolean', 'MOZILLA_OFFICIAL', mozconfig.substs.MOZILLA_OFFICIAL ? 'true' : 'false';
     }
 
-    buildTypes {
-        withGeckoBinaries {
-            initWith release
-        }
-        withoutGeckoBinaries { // For clarity and consistency throughout the tree.
-            initWith release
-        }
-    }
+    project.configureProductFlavors.delegate = it
+    project.configureProductFlavors()
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
 
     dexOptions {
         javaMaxHeapSize "2g"
@@ -152,24 +148,19 @@ android {
 dependencies {
     implementation "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
     implementation "com.android.support:palette-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
 }
 
 apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
 
 android.libraryVariants.all { variant ->
-    // Like 'debug', 'release', or 'withGeckoBinaries'.
-    def buildType = variant.buildType.name
-
-    // It would be most natural for :geckoview to always include the Gecko
-    // binaries, but that's difficult; see the notes in
-    // mobile/android/gradle/with_gecko_binaries.gradle.  Instead :app uses
-    // :geckoview:release and handles it's own Gecko binary inclusion.
-    if (buildType.equals('withGeckoBinaries')) {
+    // See the notes in mobile/android/app/build.gradle for details on including
+    // Gecko binaries and the Omnijar.
+    if ((variant.productFlavors*.name).contains('withGeckoBinaries')) {
         configureVariantWithGeckoBinaries(variant)
     }
 
     // Javadoc and Sources JAR configuration cribbed from
     // https://github.com/mapbox/mapbox-gl-native/blob/d169ea55c1cfa85cd8bf19f94c5f023569f71810/platform/android/MapboxGLAndroidSDK/build.gradle#L85
     // informed by
     // https://code.tutsplus.com/tutorials/creating-and-publishing-an-android-library--cms-24582,
     // and amended from numerous Stackoverflow posts.
@@ -195,22 +186,22 @@ android.libraryVariants.all { variant ->
         options.docTitle = "GeckoView ${mozconfig.substs.MOZ_APP_VERSION} API"
         options.header = "GeckoView ${mozconfig.substs.MOZ_APP_VERSION} API"
         options.noTimestamp = true
         options.noIndex = true
         options.noQualifiers = ['java.lang']
         options.tags = ['hide:a:']
     }
 
-    task "javadocJar${name.capitalize()}"(type: Jar, dependsOn: javadoc) {
+    def javadocJar = task("javadocJar${name.capitalize()}", type: Jar, dependsOn: javadoc) {
         classifier = 'javadoc'
         from javadoc.destinationDir
     }
 
-    task "sourcesJar${name.capitalize()}"(type: Jar) {
+    def sourcesJar = task("sourcesJar${name.capitalize()}", type: Jar) {
         classifier 'sources'
         description = "Generate Javadoc for build variant $name"
         destinationDir = new File(destinationDir, variant.baseName)
         from files(variant.javaCompile.source)
     }
 }
 
 android.libraryVariants.all { variant ->
@@ -238,32 +229,32 @@ uploadArchives {
 }
 
 // This is all related to the withGeckoBinaries approach; see
 // mobile/android/gradle/with_gecko_binaries.gradle.
 afterEvaluate {
     // The bundle tasks are only present when the particular configuration is
     // being built, so this task might not exist.  (This is due to the way the
     // Android Gradle plugin defines things during configuration.)
-    def bundleWithGeckoBinaries = tasks.findByName('bundleWithGeckoBinaries')
+    def bundleWithGeckoBinaries = tasks.findByName('bundleOfficialWithGeckoBinariesNoMinApiRelease')
     if (!bundleWithGeckoBinaries) {
         return
     }
 
     // Remove default configuration, which is the release configuration, when
     // we're actually building withGeckoBinaries.  This makes `gradle install`
     // install the withGeckoBinaries artifacts, not the release artifacts (which
     // are withoutGeckoBinaries and not suitable for distribution.)
     def Configuration archivesConfig = project.getConfigurations().getByName('archives')
     archivesConfig.artifacts.removeAll { it.extension.equals('aar') }
 
     artifacts {
         // Instead of default (release) configuration, publish one with Gecko binaries.
-        archives bundleWithGeckoBinaries
+        archives bundleOfficialWithGeckoBinariesNoMinApiRelease
         // Javadoc and sources for developer ergononomics.
-        archives javadocJarWithGeckoBinaries
-        archives sourcesJarWithGeckoBinaries
+        archives javadocJarOfficialWithGeckoBinariesNoMinApiRelease
+        archives sourcesJarOfficialWithGeckoBinariesNoMinApiRelease
     }
 }
 
 // Bug 1353055 - Strip 'vars' debugging information to agree with moz.build.
 apply from: "${topsrcdir}/mobile/android/gradle/debug_level.gradle"
 android.libraryVariants.all configureVariantDebugLevel
--- a/mobile/android/geckoview_example/build.gradle
+++ b/mobile/android/geckoview_example/build.gradle
@@ -1,64 +1,44 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/geckoview_example"
 
 apply plugin: 'com.android.application'
 
+apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
+
 android {
     compileSdkVersion project.ext.compileSdkVersion
 
     defaultConfig {
         targetSdkVersion project.ext.targetSdkVersion
         minSdkVersion project.ext.minSdkVersion
         manifestPlaceholders = project.ext.manifestPlaceholders
 
         applicationId "org.mozilla.geckoview_example"
         versionCode 1
         versionName "1.0"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
 
-    // This is extremely frustrating, but the only way to do it automation for
-    // now.  Without this, we only get a "debugAndroidTest" configuration; we
-    // have no "withoutGeckoBinariesAndroidTest" configuration.
-    testBuildType "withoutGeckoBinaries"
     buildTypes {
         release {
             minifyEnabled false
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
-        withGeckoBinaries { // For consistency with :geckoview project in Task Cluster invocations.
-            initWith debug
-        }
-        withoutGeckoBinaries { // Logical negation of withGeckoBinaries.
-            initWith debug
-        }
     }
+
+    project.configureProductFlavors.delegate = it
+    project.configureProductFlavors()
 }
 
 dependencies {
     testImplementation 'junit:junit:4.12'
 
     implementation 'com.android.support:support-annotations:23.4.0'
 
     androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'
     androidTestImplementation 'com.android.support.test:runner:0.5'
     // Not defining this library again results in test-app assuming 23.1.1, and the following errors:
     // "Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.4.0) and test app (23.1.1) differ."
     androidTestImplementation 'com.android.support:support-annotations:23.4.0'
 
     implementation project(path: ':geckoview')
 }
-
-apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
-
-android.applicationVariants.all { variant ->
-    // Like 'debug', 'release', or 'withoutGeckoBinaries'.
-    def buildType = variant.buildType.name
-
-    // It would be most natural for :geckoview to always include the Gecko
-    // binaries, but that's difficult; see the notes in
-    // mobile/android/gradle/with_gecko_binaries.gradle.  Instead we handle our
-    // own Gecko binary inclusion.
-    if (!buildType.equals('withoutGeckoBinaries')) {
-        configureVariantWithGeckoBinaries(variant)
-    }
-}
--- a/mobile/android/gradle.configure
+++ b/mobile/android/gradle.configure
@@ -41,30 +41,33 @@ set_config('GRADLE', gradle)
 def gradle_android_build_config():
     def capitalize(s):
         # str.capitalize lower cases trailing letters.
         if s:
             return s[0].upper() + s[1:]
         else:
             return s
 
-    # It's not really possible to abstract the GeckoView details just yet; post
-    # Android-Gradle plugin 3.0+, the configurations can be more sensible and
-    # we'll do this work.
     def variant(productFlavors, buildType):
         return namespace(
             productFlavors=productFlavors,
             buildType=buildType,
             # Like 'OfficialWithoutGeckoBinariesPhotonDebug'
             name = ''.join(capitalize(t) for t in chain(productFlavors, (buildType, )))
         )
 
     return namespace(
         app=namespace(
-            variant=variant(('official', 'photon'), 'debug'),
+            variant=variant(('official', 'withoutGeckoBinaries', 'noMinApi', 'photon'), 'debug'),
+        ),
+        geckoview=namespace(
+            variant=variant(('official', 'withGeckoBinaries', 'noMinApi'), 'release'),
+        ),
+        geckoview_example=namespace(
+            variant=variant(('official', 'withGeckoBinaries', 'noMinApi'), 'debug'),
         ),
     )
 
 
 @depends(gradle_android_build_config)
 def gradle_android_app_variant_name(build_config):
     '''Like "officialPhotonDebug".'''
     def uncapitalize(s):
@@ -77,17 +80,17 @@ def gradle_android_app_variant_name(buil
 
 set_config('GRADLE_ANDROID_APP_VARIANT_NAME', gradle_android_app_variant_name)
 
 
 @depends(gradle_android_build_config)
 def gradle_android_app_tasks(build_config):
     '''Gradle tasks run by |mach android assemble-app|.'''
     return [
-        'geckoview:generateJNIWrappersForGeneratedRelease',
+        'geckoview:generateJNIWrappersForGenerated{geckoview.variant.name}'.format(geckoview=build_config.geckoview),
         'app:generateJNIWrappersForFennec{app.variant.name}'.format(app=build_config.app),
         'app:assemble{app.variant.name}'.format(app=build_config.app),
         'app:assemble{app.variant.name}AndroidTest'.format(app=build_config.app),
     ]
 
 set_config('GRADLE_ANDROID_APP_TASKS', gradle_android_app_tasks)
 
 
@@ -170,19 +173,19 @@ def gradle_android_findbugs_tasks(build_
 
 set_config('GRADLE_ANDROID_FINDBUGS_TASKS', gradle_android_findbugs_tasks)
 
 
 @depends(gradle_android_build_config)
 def gradle_android_archive_geckoview_tasks(build_config):
     '''Gradle tasks run by |mach android archive-geckoview|.'''
     return [
-        'geckoview:assembleWithGeckoBinaries',
-        'geckoview_example:assembleWithGeckoBinaries',
-        'geckoview_example:assembleWithGeckoBinariesAndroidTest',
+        'geckoview:assemble{geckoview.variant.name}'.format(geckoview=build_config.geckoview),
+        'geckoview_example:assemble{geckoview_example.variant.name}'.format(geckoview_example=build_config.geckoview_example),
+        'geckoview_example:assemble{geckoview_example.variant.name}AndroidTest'.format(geckoview_example=build_config.geckoview_example),
         'geckoview:uploadArchives',
     ]
 
 set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS', gradle_android_archive_geckoview_tasks)
 
 
 @depends(
     gradle_android_app_tasks,
--- a/mobile/android/gradle/debug_level.gradle
+++ b/mobile/android/gradle/debug_level.gradle
@@ -1,20 +1,17 @@
 /* -*- Mode: Groovy; 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/. */
 
 // Bug 1353055 - Strip 'vars' debugging information to agree with moz.build.
 ext.configureVariantDebugLevel = { variant ->
-    // Like 'debug', 'release', or 'withGeckoBinaries'.
+    // Like 'debug' or 'release'.
     def buildType = variant.buildType.name
 
-    // For :app, like 'local', 'localOld', or 'official'.  For other projects, null.
-    def audienceDimension = variant.productFlavors[0]?.name
-
     // The default is 'lines,source,vars', which includes debugging information
     // that is quite large: roughly 500kb for Fennec.  Therefore we remove
     // 'vars' unless we're producing a debug build, where it is useful.
-    if (!'debug'.equals(buildType) || 'official'.equals(audienceDimension)) {
+    if (!'debug'.equals(buildType) || (variant.productFlavors*.name).contains('official')) {
         variant.javaCompile.options.debugOptions.debugLevel = 'lines,source'
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle/product_flavors.gradle
@@ -0,0 +1,48 @@
+/* -*- Mode: Groovy; 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/. */
+
+ext.configureProductFlavors = {
+    flavorDimensions "audience", "geckoBinaries", "minApi"
+    productFlavors {
+        local {
+            dimension "audience"
+        }
+
+        // Automation builds.  We use "official" rather than "automation" to drive these builds down
+        // the list of configurations that Android Studio offers, thereby making it _not_ the
+        // default.  This avoids a common issue with "omni.ja" not being packed into the default APK
+        // built and deployed by Android Studio.
+        official {
+             dimension "audience"
+        }
+
+        withGeckoBinaries {
+            dimension "geckoBinaries"
+        }
+
+        withoutGeckoBinaries {
+            dimension "geckoBinaries"
+        }
+
+        // For API 21+ - with pre-dexing, this will be faster for local development.
+        minApi21 {
+            dimension "minApi"
+
+            // For pre-dexing, setting `minSdkVersion 21` allows the Android gradle plugin to
+            // pre-DEX each module and produce an APK that can be tested on
+            // Android Lollipop without time consuming DEX merging processes.
+            minSdkVersion 21
+            dexOptions {
+                preDexLibraries true
+            }
+        }
+
+        // For API < 21 - does not support pre-dexing because local development
+        // is slow in that case.
+        noMinApi {
+            dimension "minApi"
+        }
+    }
+}
--- a/mobile/android/gradle/with_gecko_binaries.gradle
+++ b/mobile/android/gradle/with_gecko_binaries.gradle
@@ -1,51 +1,27 @@
 /* -*- Mode: Groovy; 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) {
+task buildOmnijars(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.
+    // Produce both the Fennec and the GeckoView omnijars.
     outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
+    outputs.file "${topobjdir}/dist/geckoview/assets/omni.ja"
 
     workingDir "${topobjdir}"
 
     commandLine mozconfig.substs.GMAKE
     args '-C'
     args "${topobjdir}/mobile/android/base"
     args 'gradle-omnijar'
 
@@ -55,80 +31,79 @@ task buildOmnijar(type:Exec) {
     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.
+ext.configureVariantWithGeckoBinaries = { variant ->
+    // :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'
-    }
-}
+    def distDir = "${topobjdir}/dist/${omnijar_dir}"
 
-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?")
+    def syncOmnijarFromDistDir = task("syncOmnijarFromDistDirFor${variant.name.capitalize()}", type: Sync) {
+        doFirst {
+            if (source.empty) {
+                throw new GradleException("Required omnijar not found in ${source.asPath}.  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?")
+        into("${project.buildDir}/moz.build/src/${variant.name}/omnijar")
+        from("${distDir}/omni.ja",
+             "${distDir}/assets/omni.ja") {
+            // Throw an exception if we find multiple, potentially conflicting omni.ja files.
+            duplicatesStrategy 'fail'
         }
     }
-}
-
-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)
+    def syncLibsFromDistDir = task("syncLibsFromDistDirFor${variant.name.capitalize()}", type: Sync) {
+        doFirst {
+            if (source.empty) {
+                throw new GradleException("Required JNI libraries not found in ${source.asPath}.  Have you built and packaged?")
+            }
+        }
+
+        into("${project.buildDir}/moz.build/src/${variant.name}/jniLibs")
+        from("${distDir}/lib")
     }
 
-    // Like 'debug' or 'release'.
-    def buildType = variant.buildType.name
+    def syncAssetsFromDistDir = task("syncAssetsFromDistDirFor${variant.name.capitalize()}", type: Sync) {
+        doFirst {
+            if (source.empty) {
+                throw new GradleException("Required assets not found in ${source.asPath}.  Have you built and packaged?")
+            }
+        }
+
+        into("${project.buildDir}/moz.build/src/${variant.name}/assets")
+        from("${distDir}/assets") {
+            exclude 'omni.ja'
+        }
+    }
 
-    syncOmnijarFromDistDir.dependsOn buildOmnijar
-    def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
-    generateAssetsTask.dependsOn syncOmnijarFromDistDir
-    generateAssetsTask.dependsOn syncLibsFromDistDir
-    generateAssetsTask.dependsOn syncAssetsFromDistDir
+    // Local (read, not 'official') builds want to reflect developer changes to
+    // the Omnijar sources.  To do this, the Gradle build calls out to the
+    // moz.build system, which can be re-entrant.  Official builds are driven by
+    // the moz.build system and should never be re-entrant in this way.
+    if (!((variant.productFlavors*.name).contains('official'))) {
+        syncOmnijarFromDistDir.dependsOn buildOmnijars
+    }
 
-    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
+    def assetGenTask = tasks.findByName("generate${variant.name.capitalize()}Assets")
+    if ((variant.productFlavors*.name).contains('withGeckoBinaries')) {
+        assetGenTask.dependsOn syncOmnijarFromDistDir
+        assetGenTask.dependsOn syncLibsFromDistDir
+        assetGenTask.dependsOn syncAssetsFromDistDir
+
+        android.sourceSets."${variant.name}".assets.srcDir syncOmnijarFromDistDir.destinationDir
+        android.sourceSets."${variant.name}".assets.srcDir syncAssetsFromDistDir.destinationDir
+        android.sourceSets."${variant.name}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
+    }
 }
 
 ext.configureLibraryVariantWithJNIWrappers = { variant, module ->
     // Library variants have two essentially independent transform* tasks:
     //
     // - ...WithSyncLibJars... is used by assemble* and bundle*
     // - ...WithPrepareIntermediateJars... is used by consuming applications.
     //
--- a/taskcluster/ci/build/android.yml
+++ b/taskcluster/ci/build/android.yml
@@ -16,17 +16,17 @@ android-api-16/debug:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -68,17 +68,17 @@ android-x86/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -125,17 +125,17 @@ android-x86-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -173,17 +173,17 @@ android-api-16/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -274,17 +274,17 @@ android-api-16-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -327,17 +327,17 @@ android-x86-old-id/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -383,17 +383,17 @@ android-x86-old-id-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -432,17 +432,17 @@ android-api-16-old-id/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -483,17 +483,17 @@ android-api-16-old-id-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -532,17 +532,17 @@ android-api-16-gradle/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -578,17 +578,17 @@ android-aarch64/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -630,17 +630,17 @@ android-aarch64-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config: