Preparing for a Gradle-Only Build

Before Dropbox could remove its custom BMBF build system for Android, the team had to replicate two of its most useful properties in pure Gradle: guaranteed layered dependency order and minimal boilerplate. Both were addressed with shared Gradle configuration in buildSrc and the root project.

Enforcing layered dependencies

Dropbox organizes modules into layers based on their direct subdirectory under dbx/. Layers provide a quick sense of module scope and enforce high-level dependency constraints. They are ordered from top to bottom, where higher layers are more specific and narrow, while lower layers are more general and broad.

Layer NameLayer Description
productModules relating to a single Product (eg Paper or Dropbox). Modules in this layer will typically be under a subdirectory specifying which product they’re part of.
coreDropbox-related modules that are shared between multiple products. For example, Stormcrow (our gating library) lives in this layer.
baseNon-Dropbox-specific modules that are common utilities. For instance, our HTTP libraries are in this layer.
externalCode not written at Dropbox which cannot be pulled in as a library binary.

For example, dbx/core/stormcrow sits in the core layer. Stormcrow is a Dropbox-specific concept used by both DBApp and Paper. Being in core, it cannot depend on a product module, but it may depend on other core, base, and external modules.

Under BMBF, the layered verifier was a Python script invoked by a Gradle task on every app build, with no practical caching. The team rewrote it as a Kotlin verifier inside buildSrc. That made the code maintainable by any mobile engineer and gave them Gradle UP-TO-DATE checks for free.

Cutting boilerplate with common.gradle

Creating a new module previously meant copying a block of Gradle boilerplate from an existing one. Dropbox eliminated that by adding a common Gradle file applied to all subprojects. With that in place, an engineer creating a module only needs to declare its dependencies. Because build.gradle files are no longer autogenerated, engineers can also define custom logic that the old build system could not express.

common.gradle

subprojects { Project project ->
    project.apply from: xplatRoot.absolutePath + "/tools/gradle/test_results_formatter.gradle"
    project.plugins.withId('com.android.library') {
        project.apply plugin: 'kotlin-android'

        if (project.hasProperty('apply_jacoco_plugin') {
            // Runs in CI or when a local dev enables this property
            project.apply from: xplatRoot.absolutePath + "/tools/gradle/jacoco_test_coverage.gradle"
        }

        project.android {
            compileSdkVersion androidCompileSdkVersion
            buildToolsVersion androidBuildToolsVersion

            lintOptions {
                ignore 'MissingTranslation'
            }

            defaultConfig {
                minSdkVersion androidMinSdkVersion
                targetSdkVersion androidTargetSdkVersion
                testInstrumentationRunner 'com.dropbox.base.test.runner.DbxBaseTestRunner'
                // This is needed for when we build this as a standalone target,
                // which will typically be in tests.
                multiDexEnabled true
            }

            compileOptions {
                sourceCompatibility androidSourceCompatibility
                targetCompatibility androidTargetCompatibility
            }
        }
    }

A simple build.gradle file now suffices for a new module, and any shared changes can be made in a single location.

build.gradle

apply plugin: 'com.android.library'

dependencies {
    api project(':dbx:base:analytics_gen')
    implementation project(':dbx:base:error')
    implementation project(':dbx:base:json')
    implementation project(':dbx:base:oxygen')
    implementation commonlibs.guava
}

Shipping the migration

With the foundation ready, Dropbox removed BMBF's autogeneration of Gradle build files and checked in simplified versions based on the new common code. Because this was a high-impact change, it was scheduled immediately after a release, and the repository was frozen until the change landed.

The migration team kept a back-channel for engineers who had recently created modules, allowing them to force-migrate their BMBF config files. The transition was smooth, largely because of deliberate overcommunication: emails, Slack messages, and weekly cross-functional mobile meetings kept everyone informed in advance. The team also reserved the following week to handle issues that surfaced.

Cleaning up the directory structure

The migration itself unlocked a larger cleanup opportunity. BMBF used a non-standard directory layout, with modules outside the Android project root, which caused overhead in settings.gradle and was the biggest time sink in builds. Modules lived under /xplat/dbx/ while Android projects lived under /xplat/android/.

BMBF structure (Before)Gradle default structure (After)
java/srcsrc/main/java
android/src/AndroidManifest.xmlsrc/main/AndroidManifest.xml
jvm_test/srctest/main/java
android_test/srcandroidTest/main/java

Rather than hand-migrating 75 modules or pushing the work onto product teams, Dropbox wrote a 500-line Python migration script. The script determined which modules needed to move, relocated the code into the proper directories, updated imports in source files, and fixed project dependencies in the build.gradle files.

Keeping BMBF-lite alive

BMBF was not removed entirely because it still handled code generation for Djinni, Stormcrow gating, and analytics ADL files. Previously, that generation ran on every single build regardless of whether inputs changed. The team introduced Watchman to monitor source directories and wire those checks into a Gradle task's @Input and @Output annotations, letting Gradle skip work when nothing changed.

watchman.gradle

/*
 * Copyright (c) 2019, Dropbox, Inc. All rights reserved.
 */

// Watchman generates a json file to depict the xplat file structure filtered on "bmbf source" files
task watchmanCheckIfCodegenNeeded(type:Exec) {
    File outputJson = new File(project.buildDir, "changed-files.json")
    File watchmanJson = new File(xplatRoot, "tools/watchman/watchman-bmbf.json")

    workingDir xplatRoot
    commandLine "bash", "-c", "watchman watch-project $xplatRoot"
    commandLine "bash", "-c", "watchman -j < $watchmanJson.absolutePath"

    doFirst {
        standardOutput new ByteArrayOutputStream()
    }

    doLast {
        // Remove this piece of data that changes on every run (even with no modifications to the files)
        def filteredText = standardOutput.toString().replaceFirst(".*\"clock\".*\n", "")
        if (outputJson.exists()) {
            outputJson.delete()
        }
        outputJson << filteredText
        logger.lifecycle("Watchman query for BMBF files done: " + outputJson)
    }

    // Save the json as the output so other tasks can reference it easily
    outputs.files { outputJson }
    // Always run this task
    outputs.upToDateWhen { false }
}

Those changes, plus a few minor improvements, cut P50 local build times by 20 percent.

Why Gradle

Dropbox's custom build system served both platforms for years, but its maintenance costs eventually outweighed the benefits of shared patterns. Splitting iOS and Android onto platform-specific build systems let each side take advantage of tools and practices aligned with its ecosystem. Gradle brought the Android build closer to industry standards while leaving room to revisit Bazel later. The change was also delivered with minimal disruption and without burdening engineers with extra boilerplate.