[ kalizi.dev ]

Lock Your Dependencies With Docker: How This Saved My Old Project React Native Android Build

Photo by Kelly Sikkema on Unsplash

“Programming is about managing complexity: the complexity of the problem, laid upon the complexity of the machine. Because of this complexity, most of our programming projects fail.” ~ Bruce Eckel

We all have that nightmare, an old project coming back from the grave for some reason.

Some days ago, I got a call from a customer, that never paid for maintenance, asking for “a simple edit”. And it was easy, I just had to edit a regular expression and trigger a build.

And there, lying behind that simple edit, waiting, was the Android Studio Build. Once I pressed that button, all my problems started.

The last project commit was from September ’19, more than a year ago. And according to npm repositories, for that project were: 20 major updates, 12 minor updates and 6 patches.

Updating everything wasn’t an option: the customer didn’t want to pay for that. So I had to find a way to build the project in the fastest possible way.

Rebuilding September ’19 environment

This was the challenge, rebuild the exact setup I had on September ’19 without breaking my current setup.

Docker: Package Software into Standardized Units for Development, Shipment and Deployment

Recently, less than 3 months ago, I started using docker. I really wanted to learn how Docker and containerization work, but I didn’t have enough time, so I embraced the philosophy “use first, learn later”.

I never considered using it for Android development, but… why not?

I’m not a dockerist, so I started searching on Google and I found an interesting Github repository from react-native-community.

I grabbed the Dockerfile from the repository and put that on my project.

That was a starting point. Now I needed tweaking:

  • I didn’t need Buck, so I just removed it.
  • I didn’t need the NDK, trashed that too.
  • Dependency versions weren’t the ones I needed, so I changed them.

After these tweaks, I didn’t change anything.

FROM ubuntu:20.04

# Original Dockefile: https://github.com/react-native-community/docker-android/blob/master/Dockerfile

ENV DEBIAN_FRONTEND=noninteractive

# https://github.com/react-native-community/docker-android
# set default build arguments
ARG SDK_VERSION=commandlinetools-linux-6609375_latest.zip
ARG ANDROID_BUILD_VERSION=28
ARG ANDROID_TOOLS_VERSION=28.0.3
ARG NODE_VERSION=10.x

# set default environment variables
ENV ADB_INSTALL_TIMEOUT=10
ENV ANDROID_HOME=/opt/android
ENV ANDROID_SDK_HOME=${ANDROID_HOME}
ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

ENV PATH=${ANDROID_HOME}/cmdline-tools/tools/bin:${ANDROID_HOME}/emulator:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:/opt/buck/bin/:${PATH}

# Install system dependencies
RUN apt update -qq && apt install -qq -y --no-install-recommends \
        apt-transport-https \
        curl \
        file \
        gcc \
        git \
        g++ \
        gnupg2 \
        libc++1-10 \
        libgl1 \
        libtcmalloc-minimal4 \
        make \
        openjdk-8-jdk-headless \
        openssh-client \
        python3 \
        python3-distutils \
        rsync \
        ruby \
        ruby-dev \
        tzdata \
        unzip \
        sudo \
        ninja-build \
        zip \
    && gem install bundler \
    && rm -rf /var/lib/apt/lists/*;

# Refresh keys to prevent invalid signature 
RUN apt-key adv --refresh-keys --keyserver keyserver.ubuntu.com

# install nodejs and yarn packages from nodesource and yarn apt sources
RUN curl -sL https://deb.nodesource.com/setup_${NODE_VERSION} | bash - \
	&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
    && apt-get update -qq \
    && apt-get install -qq -y --no-install-recommends nodejs yarn \
    && rm -rf /var/lib/apt/lists/*

# Full reference at https://dl.google.com/android/repository/repository2-1.xml
# download and unpack android
RUN curl -sS https://dl.google.com/android/repository/${SDK_VERSION} -o /tmp/sdk.zip \
    && mkdir -p ${ANDROID_HOME}/cmdline-tools \
    && unzip -q -d ${ANDROID_HOME}/cmdline-tools /tmp/sdk.zip \
    && rm /tmp/sdk.zip \
    && yes | sdkmanager --licenses \
    && yes | sdkmanager "platform-tools" \
        "emulator" \
        "platforms;android-$ANDROID_BUILD_VERSION" \
        "build-tools;$ANDROID_TOOLS_VERSION" \
        "cmake;3.10.2.4988404" \
        "system-images;android-21;google_apis;armeabi-v7a" \
    && rm -rf ${ANDROID_HOME}/.android

View on GitHub Gist

Now build the container and run it!

docker build -t project-android-build .

In my environment, 4 cores and 2048MB of RAM on WSL2, it took 816.3s.

docker run --name=running_container -v F:\Projects\my-app:/app -it project-android-build

I had my September ’19 environment!

It’s time for a build

Everything was set up and ready. I needed to install dependencies with yarn install and to trigger the build for my project

cd android && ./gradlew clean && cd .. && npx jetify && cd android && ./gradlew bundleRelease

If you’re lucky as I was, you’ll finally have your bundle ready.

A final thought on Docker

Photo by Ugur Peker on Unsplash

Well, if I have to summarize what I think in just a few words I would say “It’s amazing”.

Docker provides a simple throwaway development environment that you can spin up, use anytime you need it and finally, thrash and spin up again when needed. It’s amazing because even if the project keeps collecting dust for a while, the next time I need to edit another regex, I can just spin the container up again and rebuild. I can even delete the image and rebuild it if every mirror stays up. That’s, in my opinion, the real power of docker, set your environment, change your project!