TheCB4

Continous Integration of Android Emulator Docker Image

Continous Integration (CI) is important to reduce time to market while ensuring your app works as expected (through testing). I created this image in support of the One App Three Ways: Weather project.



This article will walk through the Docker file itself, using Gitlab as the continous integration platform, and as the image registry. There will be a sister article about setting up a Digital Ocean VM to run this image.

Android UI testing in a CI environment has proven to take a long time to get correct through a lot of trial and error. Referencing multple sites, I was able to put together my version of a docker image that allows me to run emulator tests in the gilab ci environment. The Gitlab Runner has to have kvm access. I can use gitlab to run CI for the image and host it in the gitlab registry as part of the project.

Dockerfile

The Dockerfile is split into three areas; dependencies, android sdk, creating a non-root user.

Dependencies

The dependencies took several iteration to ensure I was able to set up the emulator properly. This included KVM and QEMU. I also include git-secret for secrets management in the application that will run in the container.



LABEL Maintainer Cavelle Benjamin <cavelle@thecb4.io>
LABEL Name Swift LLVM
LABEL Release Alpha
LABEL Vendor TheCB4
LABEL Version 0.0.1


# https://gist.github.com/illuzor/988385c493d3f7ed7193a6e3ce001a68
ARG ANDROID_PLATFORM
ARG ANDROID_COMPILE_SDK
ARG ANDROID_BUILD_TOOLS
ARG ANDROID_SDK_TOOLS
ARG ANDROID_EMULATOR
ARG ANDROID_NDK
ARG CMAKE
ARG DEBIAN_FRONTEND
ARG DEBCONF_NONINTERACTIVE_SEEN


# locale and timezone
ENV LANG='en_US.UTF-8' \
  LANGUAGE='en_US:en' \
  LC_ALL='en_US.UTF-8' \
  ENV_TIMEZONE="America/New_York"
RUN echo '$ENV_TIMEZONE' > /etc/timezone && ln -fsn /usr/share/zoneinfo/$ENV_TIMEZONE /etc/localtime
RUN apt-get update -qq && \
  apt-get upgrade -qq && \
  DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends \
  locales tzdata ntp ntpstat ntpdate
RUN locale-gen $LANG && dpkg-reconfigure --frontend noninteractive tzdata


# Install dependencies
RUN apt-get update -qq && apt-get upgrade -qq && DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends \
  sudo \
  ssl-cert openssl \
  ca-certificates \
  wget \
  curl \
  gpg \
  gpg-agent \
  zip \
  unzip \
  openjdk-11-jdk \
  python3 \
  git \
  git-secret \
  lsb-release \
  libc6 libdbus-1-3 libfontconfig1 libgcc1 \
  libpulse0 libtinfo5 libx11-6 libxcb1 libxdamage1 \
  libnss3 libxcomposite1 libxcursor1 libxi6 \
  libxext6 libxfixes3 zlib1g libgl1 pulseaudio socat \
  qemu-system-x86 libvirt-daemon-system libvirt-clients bridge-utils \
  mesa-vulkan-drivers \
  locales \
  qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools \
  && apt-get autoremove --purge \
	&& apt-get clean \
	&& rm -rf /var/lib/apt/lists/*

RUN rm -f /etc/ssl/certs/java/cacerts; \
    /var/lib/dpkg/info/ca-certificates-java.postinst configure






Android Command Line Tools

Installing the command line tools also took seeral iterations. Some of it was driven by out of date documentation and certain versions not being compatible with the tools and the APIs. Specifically I needed to download a particular version of the emulator when running sdkmanager install.



RUN wget --quiet --output-document=android-sdk-linux.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
RUN mkdir /usr/share/android-sdk-linux
RUN unzip -d /usr/share/android-sdk-linux/cmdline-tools android-sdk-linux.zip
RUN mv /usr/share/android-sdk-linux/cmdline-tools/cmdline-tools /usr/share/android-sdk-linux/cmdline-tools/latest
RUN rm -rf android-sdk-linux.zip

# Install Gradle
RUN cd /usr/local/ && \
	curl -L -O https://services.gradle.org/distributions/gradle-7.3.3-bin.zip && \
	unzip -qo gradle-7.3.3-bin.zip && \
	rm -rf gradle-7.3.3-bin.zip

RUN echo "$JAVA_HOME"
ENV ANDROID_SDK_ROOT="/usr/share/android-sdk-linux"
ENV ANDROID_HOME="/usr/share/android-sdk-linux"
ENV GRADLE_HOME "/usr/local/gradle-7.3.3"
ENV PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH:$GRADLE_HOME/bin"
ENV ADB_INSTALL_TIMEOUT=8


RUN yes "y" | sdkmanager --licenses
RUN echo y | sdkmanager --update --verbose --channel=3

RUN echo y | sdkmanager --install "platform-tools"
RUN echo y | sdkmanager --install "platforms;android-${ANDROID_COMPILE_SDK}"
RUN echo y | sdkmanager --install "build-tools;${ANDROID_BUILD_TOOLS}"
RUN echo y | sdkmanager --install "cmake;${CMAKE}"
RUN echo y | sdkmanager --install "emulator" --verbose --channel=3
RUN echo y | sdkmanager --install "system-images;android-${ANDROID_COMPILE_SDK};google_apis_playstore;${ANDROID_PLATFORM}"






Non-root user

Creating the non-root user was necessary due to the host configuration and ownership of files and certain commands around kvm not working because it was a root user. There



# Create and switch to non root user
RUN useradd -rm -d /home/thecb4 -s /bin/sh -g root -G sudo,kvm,libvirt -u 1001 thecb4
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER thecb4
WORKDIR /home/thecb4


ENV GRADLE_USER_HOME=/home/thecb4/.gradle GRADLE_OPTS=-Dorg.gradle.daemon=false
RUN mkdir -p $GRADLE_USER_HOME && echo "org.gradle.daemon=false" >> $GRADLE_USER_HOME/gradle.properties


RUN mkdir .android
COPY sources/advancedFeatures.ini .android/advancedFeatures.ini
COPY sources/emu-update-last-check.ini .android/emu-update-last-check.ini



With the advancedFeatures.ini file



Vulkan = off

GLDirectMem = on



The emu-update-last-check.ini is an empty file






The full docker file

LABEL Maintainer Cavelle Benjamin <cavelle@thecb4.io>
LABEL Name Swift LLVM
LABEL Release Alpha
LABEL Vendor TheCB4
LABEL Version 0.0.1


# https://gist.github.com/illuzor/988385c493d3f7ed7193a6e3ce001a68
ARG ANDROID_PLATFORM
ARG ANDROID_COMPILE_SDK
ARG ANDROID_BUILD_TOOLS
ARG ANDROID_SDK_TOOLS
ARG ANDROID_EMULATOR
ARG ANDROID_NDK
ARG CMAKE
ARG DEBIAN_FRONTEND
ARG DEBCONF_NONINTERACTIVE_SEEN


# locale and timezone
ENV LANG='en_US.UTF-8' \
  LANGUAGE='en_US:en' \
  LC_ALL='en_US.UTF-8' \
  ENV_TIMEZONE="America/New_York"
RUN echo '$ENV_TIMEZONE' > /etc/timezone && ln -fsn /usr/share/zoneinfo/$ENV_TIMEZONE /etc/localtime
RUN apt-get update -qq && \
  apt-get upgrade -qq && \
  DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends \
  locales tzdata ntp ntpstat ntpdate
RUN locale-gen $LANG && dpkg-reconfigure --frontend noninteractive tzdata


# Install dependencies
RUN apt-get update -qq && apt-get upgrade -qq && DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends \
  sudo \
  ssl-cert openssl \
  ca-certificates \
  wget \
  curl \
  gpg \
  gpg-agent \
  zip \
  unzip \
  openjdk-11-jdk \
  python3 \
  git \
  git-secret \
  lsb-release \
  libc6 libdbus-1-3 libfontconfig1 libgcc1 \
  libpulse0 libtinfo5 libx11-6 libxcb1 libxdamage1 \
  libnss3 libxcomposite1 libxcursor1 libxi6 \
  libxext6 libxfixes3 zlib1g libgl1 pulseaudio socat \
  qemu-system-x86 libvirt-daemon-system libvirt-clients bridge-utils \
  mesa-vulkan-drivers \
  locales \
  qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools \
  && apt-get autoremove --purge \
	&& apt-get clean \
	&& rm -rf /var/lib/apt/lists/*

RUN rm -f /etc/ssl/certs/java/cacerts; \
    /var/lib/dpkg/info/ca-certificates-java.postinst configure


# Install Android Command Line Tools
RUN wget --quiet --output-document=android-sdk-linux.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
RUN mkdir /usr/share/android-sdk-linux
RUN unzip -d /usr/share/android-sdk-linux/cmdline-tools android-sdk-linux.zip
RUN mv /usr/share/android-sdk-linux/cmdline-tools/cmdline-tools /usr/share/android-sdk-linux/cmdline-tools/latest
RUN rm -rf android-sdk-linux.zip

# Install Gradle
RUN cd /usr/local/ && \
	curl -L -O https://services.gradle.org/distributions/gradle-7.3.3-bin.zip && \
	unzip -qo gradle-7.3.3-bin.zip && \
	rm -rf gradle-7.3.3-bin.zip

RUN echo "$JAVA_HOME"
ENV ANDROID_SDK_ROOT="/usr/share/android-sdk-linux"
ENV ANDROID_HOME="/usr/share/android-sdk-linux"
ENV GRADLE_HOME "/usr/local/gradle-7.3.3"
ENV PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH:$GRADLE_HOME/bin"
ENV ADB_INSTALL_TIMEOUT=8


RUN yes "y" | sdkmanager --licenses
RUN echo y | sdkmanager --update --verbose --channel=3

RUN echo y | sdkmanager --install "platform-tools"
RUN echo y | sdkmanager --install "platforms;android-${ANDROID_COMPILE_SDK}"
RUN echo y | sdkmanager --install "build-tools;${ANDROID_BUILD_TOOLS}"
RUN echo y | sdkmanager --install "cmake;${CMAKE}"
RUN echo y | sdkmanager --install "emulator" --verbose --channel=3
RUN echo y | sdkmanager --install "system-images;android-${ANDROID_COMPILE_SDK};google_apis_playstore;${ANDROID_PLATFORM}"


# Create and switch to non root user
RUN useradd -rm -d /home/thecb4 -s /bin/sh -g root -G sudo,kvm,libvirt -u 1001 thecb4
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER thecb4
WORKDIR /home/thecb4


ENV GRADLE_USER_HOME=/home/thecb4/.gradle GRADLE_OPTS=-Dorg.gradle.daemon=false
RUN mkdir -p $GRADLE_USER_HOME && echo "org.gradle.daemon=false" >> $GRADLE_USER_HOME/gradle.properties


RUN mkdir .android
COPY sources/advancedFeatures.ini .android/advancedFeatures.ini
COPY sources/emu-update-last-check.ini .android/emu-update-last-check.ini






The Docker Compose file

I used docker compose to avoid a lot of command line work. This allows me to update any of the android settings from one place. I could also create multiple versions of the image.

version: "3.9"

services:
  android:
    image: registry.gitlab.com/thecb4-universe/private/docker/android-x86_64:latest
    build:
      context: .
      dockerfile: sources/android.dockerfile
      args:
        ANDROID_PLATFORM: "x86_64"
        ANDROID_COMPILE_SDK: "32"
        ANDROID_BUILD_TOOLS: "32.0.0"
        ANDROID_SDK_TOOLS: "8512546"
        ANDROID_EMULATOR: "31.2.10"
        ANDROID_NDK: "24.0.8215888"
        CMAKE: "3.18.1"
        DEBIAN_FRONTEND: noninteractive
        DEBCONF_NONINTERACTIVE_SEEN: true
    privileged: true






The gitlab-ci.yaml file

The gitlab ci file has two steps. The first is building the image. The second is scanning the image for security vulnerabilities. The image is built using docker-in-docker (dind) configuration on a Digital Ocean hosted Gitlab Runner. This is done to ensure KVM is available at the time of build. Before building and pushing to the registry, you have to log in to the gitlab registry using your CI variables.

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

services:
  - docker:dind


stages:
 - build
 - test

build:
  image: docker:latest
  variables:
    REPOSITORY: $CI_REGISTRY/thecb4-universe/private/docker/android-x86_64
    DOCKER_HOST: tcp://docker:2375
    DOCKER_DRIVER: overlay
    DOCKER_TLS_CERTDIR: ""
  stage: build
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker compose -f docker-compose-ci.yml build --force-rm --no-cache
    - docker push $REPOSITORY
  only:
    - main
  tags:
    - thecb4-universe
    - docker

container_scanning:
  needs: [build]
  variables:
    DOCKER_IMAGE: "$CI_REGISTRY/thecb4-universe/private/docker/android-x86_64"
    DOCKER_USER: $CI_REGISTRY_USER
    DOCKER_PASSWORD: $CI_REGISTRY_PASSWORD




Find me on Twitter and tell me all about it!

Tagged with: