Running Android builds — Part 4, React Native DevOps Guide

Tyler Pate
10 min readNov 15, 2018

--

TLDR: Configure a React Native Android project for three build types: development, staging, and release. Customize build behavior for repeatability through gradle script edits, then streamline flows using Fastlane. Use Jenkins to automate Fastlane build flows.

Intro | Setup | Pre-Build | iOS | Android | Jenkins | CodePush | Testing

Android by sterlic / CC BY 2.0

Hello Readers!

Congrats for making it to the 4th installment in the React Native DevOps Guide! I hope you are benefiting from all of the tips and tricks I learned while leading the mobile team at Earn.com (previously 21, Inc).

If you have read Running iOS Builds already, you will be in great shape to power through this tutorial. If you are coming in fresh to the 4th article, I recommend reading the concepts sections from the previous articles. This will help you understand the design decisions presented here — concepts continue to build through each article.

Synopsis

Concepts

  • Build speed optimization
  • Staging configuration
  • Node path
  • ExtraPropertiesExtension parameters
  • Input excludes
  • Fastlane

Walkthrough

  • Edit ~/.gradle/gradle.properties
  • Edit android/gradle.properties
  • Edit android/app/build.gradle
  • Update $PROJECT_ROOT/fastlane to support Android
  • Test builds via CLI
  • Update Jenkins jobs
  • Test builds via Jenkins

System overview

Concepts — Android

Now that we have all three build configurations and schemes set up for iOS builds, we will mirror the flows for Android. I ran into a lot of problems with build speed and final minute errors with Android builds when I was starting out with React Native. I previously released an article on Android outlining some of my findings. This article updates those recommendations, and fits them in the context of the Jenkins CI system we are developing.

Build speed optimization

The Java VM needs to have enough memory to cruise along smoothly during build processes.

If the VM runs out of memory during the build process, the entire build will fail. In the context of a Jenkins agent, a failed Android build that is back-to-back with an iOS build will cause the entire job to fail, wasting ~15 minutes. You’ll have to re-run both builds for Jenkins to be happy. If you have an overzealous boss or you are on a deadline, this kind of wasted time is terrible.

In my previous Android article, I recommended an 8G heap size. Since then, I’ve revised my recommendation to a more sane 4G size.

I recommend explicitly defining the max heap size in 2 locations, android/app/build.gradle and android/gradle.properties.

// android/app/build.gradleandroid {
...
dexOptions {
preDexLibraries false
javaMaxHeapSize "4g"
}
...
}

In android/gradle.properties, there are a few other parameters to tweak to optimize build performance. Set org.gradle.parallel and org.gradle.configureondemand to false. This ensures proper bundling behavior during Bundle[Type]JSAndAssets. Setting the daemon flag to true will speed up future builds as well.

You may be using a 3rd party library that implements a depreciated NDK. If so, you will need to set useDeprecatedNdk to true.

// android/gradle.propertiesandroid.useDeprecatedNdk=trueorg.gradle.daemon=true
org.gradle.parallel=false
org.gradle.configureondemand=false
org.gradle.jvmargs=-Xmx4096M -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

Node path

Setting theNODE_BINARY variable in your user-space gradle.properties file allows you to reference the NODE_BINARY variable in your project level build.gradle file. We will use the NODE_BINARY variable when customizing ExtraPropertiesExtension parameters.

// ~/.gradle/gradle.propertiesNODE_BINARY=[YOUR_HOME_HERE]/.nvm/versions/node/v8.11.3/bin/node

If you use react-native-checkmate to perform pre-build checks (see part 2 for more info), you can set the nodePath check to true to verify that NODE_BINARY variable has been set.

Staging buildType

By default, react-native init will generate an Android project with two buildTypes. Keeping Android and iOS deployment flows the same, we will add a staging buildType to the build.gradle script.

Setting an applicationIdSuffix for each build type allows multiple .apk files to coexist on the same simulator without being overwritten. We add .dev and .staging suffixes to the build type configurations to aid development.

// android/app/build.gradlebuildTypes {
debug {
applicationIdSuffix ".dev"
}
staging {
...
signingConfig signingConfigs.release
applicationIdSuffix ".staging"
}
release {
...
signingConfig signingConfigs.release
}
}

ExtraPropertiesExtension parameters

In addition to modifying the buildTypes configuration, we also have to tell react packager to set the bundle and dev mode flags for the new staging buildType. This modification is similar in outcome to using a custom packager script with iOS.

Taking a look at node_modules/react-native/react.gradle , we see dev mode and bundling behavior set by:

  • config parameters
  • release in the buildType name.
// node_modules/react-native/react.gradledef devEnabled = !(config."devDisabledIn${targetName}" ||
targetName.toLowerCase().contains("release"))
...enabled config."bundleIn${targetName}" ||
config."bundleIn${buildTypeName.capitalize()}"
?:
targetName.toLowerCase().contains("release")

We need to set config parameters that tell the packager what to do with assets and JS in this build. We will edit project.ext.react properties below to ensure correct behavior.

Input excludes

One potential snag in Android builds involve unexpected files present in the project root directory. By adding an inputExcludes string array to project.ext.react object, we can easily exclude directories and files that don’t belong in the bundle.

One of the major excludes I implemented was excluding CodePush bundle output. As part of my CodePush flows, I auto-generated a JS bundle and committed it to a sub-folder in the project root directory. The bundle was used as part of a post-build script to upload new sourcemaps to our event / crash tracker.

On subsequent builds, the packager would try to integrate the old JS bundle, which ended up in a lot of failed builds and head scratching on my part. As soon as I excluded the js_build directory, I started getting consistent build behavior again.

Bringing all changes into build.gradle

The NODE_BINARY parameter set in~/.gradle/gradle.properties is used to define the react packager node executable path. For each of the build types, I explicitly define the asset and code destinations.

// android/app/build.gradleproject.ext.react = [
entryFile: "index.js",
nodeExecutableAndArgs: hasProperty(’NODE_BINARY’)?[NODE_BINARY]: ["node"],
bundleInDebug: false,
bundleInStaging: true,
bundleInRelease: true,
jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
jsBundleDirStaging: "$buildDir/intermediates/assets/staging",
resourcesDirStaging:
"$buildDir/intermediates/res/merged/staging",
jsBundleDirRelease: "$buildDir/intermediates/assets/release",
resourcesDirRelease:
"$buildDir/intermediates/res/merged/release",
devDisabledInDebug: false,
devDisabledInStaging: true,
devDisabledInRelease: true,
inputExcludes: ["ios/**", "__tests__/**", "js_build/**"]
];

Updates to Android SDK

React Native 0.56 included a whole lot of build system upgrades, touching iOS, Android, and Node. Android as a whole sorely needed modernization with respect to React Native, as the specified build tools and SDK versions were seriously out of date.

Since 0.56, the Android toolchain is now:

  • Gradle 3.5
  • SDK 26
  • Fresco 1.9.0
  • OkHttp 3.10.0,
  • NDK target API 16.

If you are running React Native 0.55 or less, an upgrade is required to comply with Play Store requirements. I’m a big fan of the build system upgrades as it relates to overall build speed. Read more from the React Native blog here.

Concepts — Fastlane

We will build on the Fastlane flows established in the iOS tutorial. For deeper discussion on overall Fastlane flow, lanes, and options, follow the link above.

In general, we will create a 1:1 copy of the iOS lanes/options and apply them to Android. Google does not require provisioning profiles, so Android lanes have fewer options and are less complex.

Lanes

There are 3 major lanes for Android, potentially more if you use CodePush in your project.

  • dev: simulator or device, debug = true
  • staging: simulator or device, debug = false
  • release: simulator or device, debug = false, suitable for Play Store

Options

Options let us adjust lane variables at execution time. This is a critical component of interoperability with Jenkins. Options, passed into fastlane CLI calls via Jenkins build parameters, will let us produce a variety of products from the 3 main Android lanes listed above.

  • skip_before (bool): skip before_all lane. Used with back-to-back fastlane calls in Jenkins jobs for build time optimization.
  • badge (bool): generate badge overlay with version, revision, and dev or beta flags on the app image
  • clean (bool): clean project before build actions
  • install (bool): install app to running simulator as a post-build action
  • play_store (bool): submit app to Play Store after successful releasebuild
  • hook (bool): send a webhook success/fail message to defined webhook endpoint (in fastfile)
  • channel (string): define the channel name for Slack webhook integration

Play Store Metadata

You will need to make changes to the Appfile to match your Android project information. There is no Deliverfile equivalent for Android, so editing project metadata before submission will happen on the Google Play Developer Console.

Fastlane + Jenkins

Xcode and Android Studio provide seemingly unlimited customization, then we pare complexity down with Fastlane, and finally streamline repeatable operations with Jenkins jobs.

We created 3 Jenkins jobs from the last tutorial, mobile-compile, mobile-staging, and mobile-production. To add Android support to these jobs, we just need to mirror the iOS commands as new operations in the same jobs. Let’s show how to complete the mobile-compile job for both iOS and Android.

// mobile-compile#!/usr/local/bin/zsh
source $HOME/.zshrc
fastlane ios staging simulator:true
fastlane android staging skip_before:true

Super simple — only thing added was the last line, fastlane android staging skip_before:true.

There is a new option:skip_before. This option will bypass the before_all lane in Fastlane execution. Since we already ran through yarn, checkmate, and versioning with the iOS build, there is no need to repeat for the Android build. This is a speed optimization for build job execution time.

Walkthrough

I recommend cloning the companion repository for comparison and reference while you work through this tutorial. The sample project used here is called myCoolApp.

  1. Define node path in ~/.gradle/gradle.properties

Open ~/.gradle/gradle.properties with your favorite editor. AddNODE_BINARY to the file, and set it to your NVM node executable.

NODE_BINARY=[YOUR_HOME_HERE]/.nvm/versions/node/v8.11.3/bin/node

2. Update build settings in android/gradle.properties

Open $PROJECT_ROOT/android/gradle.properties. Update to match the code snippet below and save the file. Note, useDepreciatedNdk may not be required for your project. There are multiple 3rd party libraries that use this setting.

// android/gradle.propertiesandroid.useDeprecatedNdk=trueorg.gradle.daemon=true
org.gradle.parallel=false
org.gradle.configureondemand=false
org.gradle.jvmargs=-Xmx4096M -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

3. Add staging build type to android/app/build.gradle

Now, let’s edit $PROEJCT_ROOT/android/app/build.gradle. We will make changes to this file in steps 3 and 4, so leave it open after making the first set of changes.

First, add the staging buildType by copying the release buildType and renaming the code block. minifyEnabled and proguardFiles options can be omitted for the staging build type, depending on your needs for the staging build. If you are shooting for a 1:1 copy of a release build, then copy the release configuration.

Next, add applicationIdSuffix to all three buildTypes as shown below.

// android/app/build.gradlebuildTypes {
debug {
applicationIdSuffix ".dev"
}

staging {
...
signingConfig signingConfigs.release
applicationIdSuffix ".staging"
}

release {
...
signingConfig signingConfigs.release
}
}

4. ExtraPropertiesExtension Parameters

Next, we will add project.ext.react ExtraPropertiesExtension parameters.

If you are starting from a fresh project, you will see a long comment block near the top of your build.gradle script. I have deleted this block in the sample repository as I am implementing all of the available parameters.

Feel free to copy and paste the code block below to your script, but make sure to understand what changes are made and why.

Parameters

  • node executable: update to use NODE_BINARY env variable set in ~/.gradle/settings.gradle
  • input excludes: paths to ignore during the metro bundler call
  • add staging: add parameters to support staging buildType
  • explicitly define all paths and options: This is a personal preference. I like to have all resource and JS paths explicitly defined in this section for peace of mind.
// android/app/build.gradleproject.ext.react = [
entryFile: "index.js",
nodeExecutableAndArgs: hasProperty('NODE_BINARY')?[NODE_BINARY]:["node"],
bundleInDebug: false,
bundleInStaging: true,
bundleInRelease: true,
jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
jsBundleDirStaging: "$buildDir/intermediates/assets/staging",
resourcesDirStaging:
"$buildDir/intermediates/res/merged/staging",
jsBundleDirRelease: "$buildDir/intermediates/assets/release",
resourcesDirRelease:
"$buildDir/intermediates/res/merged/release",
devDisabledInDebug: false,
devDisabledInStaging: true,
devDisabledInRelease: true,
inputExcludes: ["ios/**", "__tests__/**", "js_build/**"]
];

5. Appfile

Update Appfile, located at$PROJECT_ROOT/fastlane/Appfile, with your app package name and json_key_file path.

If you don’t have a json credentials file, take a look at collect your Google credentials for a how-to guide on getting it. You will need a proper developer account.

// fastlane/Appfilepackage_name ''
json_key_file ''

6. Add Android support to Fastfile

Clone the react-native-fastlane-boilerplate and update your project’s Fastfile with the new Android lanes.

We will create 3 lanes for Android, mirroring the iOS capabilities in our Fastfile. There are fewer CLI options to pass with Android builds.

Before modifying the Android lanes, there is a build time optimization to add to the before_all lane. Add do |lane, options| after before_all in the Fastfile. Then, add an option parsing block which takes in a CLI option or an environment variable, provides a default value, and adds SKIP_BEFORE to a parsed_options hash.

Finally, wrap the full before_all execution block with a unless statement. This will prevent the pre-build checks from running if SKIP_BEFORE has been set. When running back to back iOS and Android builds, the before_all lane will execute twice without this optimization.

// Fastfilebefore_all do |lane, options|
parsed_options = {
:skip_before => handle_env_and_options(
ENV['SKIP_BEFORE'],
options[:skip_before],
false, // isString
false // default value, str or bool
)
}
unless parsed_options[:skip_before]
...
end
end
// Note: handle_env_and_options() definition in fastlane/Common
// file in react-native-fastlane-boilerplate

7. CLI Testing

Before moving on to the Jenkins steps, make sure you are successful in running the following commands from the CLI. If you run into errors, debug these before attempting to take the full setup to your Jenkins agent.

fastlane android dev clean:true

fastlane android staging clean:true

fastlane android release clean:true play_store:false

Extra features

  • Install the built product directly to your simulator.
    Open an Android Simulator and leave it running, then run the command below. This will uninstall any bundles with a matching bundle id, then will install and launch your newly built binary.
    fastlane android dev clean:true install:true
  • Badge the app icons before building
    Add the badge:true flag to your build command. Your icons will be badged with either dev or beta, depending on if you run a dev or staging build.
    fastlane android dev clean:true install:true badge:true

8. Update Jenkins jobs

Add $PLAY_STORE and $SKIP_BEFORE boolean build variables for Android builds.

Add fastlane commands to run Android builds after iOS builds for each job. Use the new build parameters as shown below.

#### mobile-compile ####
#!/usr/local/bin/zsh
source $HOME/.zshrc
fastlane ios dev clean:$CLEAN simulator:$SIMULATOR
fastlane android dev clean:$CLEAN skip_before:true
#### mobile-staging ####
#!/usr/local/bin/zsh
source $HOME/.zshrc
fastlane ios staging clean:$CLEAN simulator:$SIMULATOR
fastlane android staging clean:$CLEAN skip_before:true
#### mobile-production ####
#!/usr/local/bin/zsh
source $HOME/.zshrc
fastlane ios release app_store:$APP_STORE
fastlane android release play_store:$PLAY_STORE skip_before:true

9. Test Jenkins jobs

Run through all of your Jenkins jobs — mobile-compile, mobile-staging, and mobile-production. Make sure that everything works as expected on your Jenkins agent.

Once you have validated the jobs, continue on to part 5 (Coming soon!), where I will feature recipes for getting the most out of your Jenkins agent.

--

--

Tyler Pate

Former Lead Mobile Developer @ Earn.com, acquired by Coinbase. Stuff I love: React Native, DevOps, JS, 3D Printing, Skiing, Motorcycles, Music