├── .env ├── Dockerfile ├── LICENSE ├── README.md ├── build-image.sh ├── docker-compose.yml ├── packages.txt └── scripts ├── android-wait-for-emulator ├── commands-to-run-in-privileged-mode.sh └── create-snapshot.sh /.env: -------------------------------------------------------------------------------- 1 | ANDROID_HOME=/sdk 2 | AVD_NAME=myavd 3 | SNAPSHOT_NAME=myemulator 4 | VERSION_COMPILE_VERSION=24 5 | VERSION_SDK_TOOLS=4333796 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | LABEL maintainer="Yorick van Zweeden" 3 | 4 | # Variables taken from variables.env 5 | ARG AVD_NAME 6 | ARG ANDROID_HOME 7 | ARG VERSION_COMPILE_VERSION 8 | ARG VERSION_SDK_TOOLS 9 | 10 | # Expect requires tzdata, which requires a timezone specified 11 | RUN ln -fs /usr/share/zoneinfo/Europe/Amsterdam /etc/localtime 12 | 13 | RUN apt-get -qq update && \ 14 | apt-get install -qqy --no-install-recommends \ 15 | bridge-utils \ 16 | bzip2 \ 17 | curl \ 18 | # expect: Passing commands to telnet 19 | expect \ 20 | git-core \ 21 | html2text \ 22 | lib32gcc1 \ 23 | lib32ncurses5 \ 24 | lib32stdc++6 \ 25 | lib32z1 \ 26 | libc6-i386 \ 27 | libqt5svg5 \ 28 | libqt5widgets5 \ 29 | # libvirt-bin: Virtualisation for emulator 30 | libvirt-bin \ 31 | openjdk-8-jdk \ 32 | # qemu-kvm: Hardware acceleration for emulator 33 | qemu-kvm \ 34 | # telnet: Communicating with emulator 35 | telnet \ 36 | # ubuntu-vm-builder: Building VM for emulator 37 | ubuntu-vm-builder \ 38 | unzip \ 39 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 40 | 41 | # Configurating Java 42 | RUN rm -f /etc/ssl/certs/java/cacerts; \ 43 | /var/lib/dpkg/info/ca-certificates-java.postinst configure 44 | 45 | # Downloading SDK-tools (AVDManager, SDKManager, etc) 46 | RUN curl -s https://dl.google.com/android/repository/sdk-tools-linux-"${VERSION_SDK_TOOLS}".zip > /sdk.zip && \ 47 | unzip /sdk.zip -d /sdk && \ 48 | rm -v /sdk.zip 49 | 50 | # Add Android licences instead of acceptance 51 | RUN mkdir -p $ANDROID_HOME/licenses/ \ 52 | && echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > $ANDROID_HOME/licenses/android-sdk-license \ 53 | && echo "84831b9409646a918e30573bab4c9c91346d8abd" > $ANDROID_HOME/licenses/android-sdk-preview-license 54 | 55 | # Download packages 56 | ADD packages.txt /sdk 57 | RUN mkdir -p /root/.android && \ 58 | touch /root/.android/repositories.cfg && \ 59 | ${ANDROID_HOME}/tools/bin/sdkmanager --update 60 | RUN while read -r package; do PACKAGES="${PACKAGES}${package} "; done < /sdk/packages.txt && \ 61 | ${ANDROID_HOME}/tools/bin/sdkmanager ${PACKAGES} 62 | 63 | # Download system image for compiled version (separate statement for build cache) 64 | RUN echo y | ${ANDROID_HOME}/tools/bin/sdkmanager "system-images;android-${VERSION_COMPILE_VERSION};google_apis;x86_64" 65 | 66 | # Create AVD 67 | RUN mkdir ~/.android/avd && \ 68 | echo no | ${ANDROID_HOME}/tools/bin/avdmanager create avd -n ${AVD_NAME} -k "system-images;android-${VERSION_COMPILE_VERSION};google_apis;x86_64" 69 | 70 | # Copy scripts to container for running the emulator and creating a snapshot 71 | COPY scripts/* / -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yorick van Zweeden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android CI 2 | ### Continous Integration (CI) for Android apps with an emulator 3 | By using snapshots, the emulator in this image is able to boot in under 15 seconds. This Docker image contains the Android SDK, emulator packages and an AVD with a snapshot. 4 | 5 | # Image 6 | ```yml 7 | image: yorickvanzweeden/android-ci:latest 8 | ``` 9 | 10 | Specifications 11 | * Build-tools: 28.0.2 12 | * Platform: 24 13 | * System-image: android-24;google_apis;x86_64 14 | 15 | # Sample GitLab usage 16 | *.gitlab-ci.yml* 17 | 18 | ```yml 19 | image: yorickvanzweeden/android-ci:latest 20 | 21 | before_script: 22 | - export GRADLE_USER_HOME=`pwd`/.gradle 23 | - chmod +x ./gradlew 24 | 25 | cache: 26 | key: "$CI_COMMIT_REF_NAME" 27 | paths: 28 | - .gradle/ 29 | 30 | stages: 31 | - build 32 | - test_instrumented 33 | 34 | build: 35 | stage: build 36 | script: 37 | - ./gradlew assembleDebug 38 | artifacts: 39 | expire_in: 1 week 40 | paths: 41 | - app/build/outputs/apk/ 42 | 43 | instrumentedTests: 44 | stage: test_instrumented 45 | only: 46 | - scheduled 47 | script: 48 | # Running emulator 49 | - /sdk/emulator/emulator -avd ${AVD_NAME} -no-window -no-audio -snapshot ${SNAPSHOT_NAME} & 50 | 51 | # Wait for emulator 52 | - ./android-wait-for-emulator 53 | 54 | # Run instrumented Android tests 55 | - /sdk/platform-tools/adb shell input keyevent 82 56 | - ./gradlew cAT 57 | ``` 58 | 59 | 60 | # How it works 61 | In order to have an emulator running in a Docker container, the Docker container must run with a `privileged` flag. This flag is required for KVM (Linux kernel virtualization). Docker build does not contain a `privileged` flag and therefore docker-compose is used with a bash script. The bash script is allowed to run in privileged mode and start the emulator. In order to save a snapshot, an expect script is fired that sets up a telnet connection. In Gitlab/other CI tools, the emulator may start from the snapshot instead of a cold boot. This reduces the startup time of the emulator to about 15 seconds. 62 | 63 | If you want to create your own image, you should adjust the `.env` file to your needs and run the `build-image.sh` script. The name of the AVD and snapshot used are stored in the variables `AVD_NAME` and `SNAPSHOT_NAME`. 64 | -------------------------------------------------------------------------------- /build-image.sh: -------------------------------------------------------------------------------- 1 | echo "Building image" 2 | docker-compose build 3 | 4 | echo "Running container" 5 | docker-compose up 6 | 7 | echo "Committing image" 8 | docker commit -c 'ENTRYPOINT [""]' emulator yorickvanzweeden/android-ci:latest 9 | 10 | echo "Pushing image to dockerhub" 11 | docker push yorickvanzweeden/android-ci:latest -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | your_service: 5 | container_name: emulator 6 | image: "emulator" 7 | build: 8 | # Dockerfile location 9 | context: . 10 | args: 11 | - AVD_NAME=${AVD_NAME} 12 | - ANDROID_HOME=${ANDROID_HOME} 13 | - VERSION_COMPILE_VERSION=${VERSION_COMPILE_VERSION} 14 | - VERSION_SDK_TOOLS=${VERSION_SDK_TOOLS} 15 | 16 | # Run scripts in container in privileged mode 17 | entrypoint: /commands-to-run-in-privileged-mode.sh ${AVD_NAME} ${SNAPSHOT_NAME} 18 | privileged: true 19 | env_file: 20 | - .env -------------------------------------------------------------------------------- /packages.txt: -------------------------------------------------------------------------------- 1 | build-tools;28.0.2 2 | emulator 3 | extras;android;m2repository 4 | extras;google;m2repository 5 | extras;google;google_play_services 6 | extras;m2repository;com;android;support;constraint;constraint-layout;1.0.2 7 | extras;m2repository;com;android;support;constraint;constraint-layout-solver;1.0.2 8 | platform-tools 9 | platforms;android-24 10 | platforms;android-28 -------------------------------------------------------------------------------- /scripts/android-wait-for-emulator: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Originally written by Ralf Kistner , but placed in the public domain 4 | 5 | set +e 6 | 7 | bootanim="" 8 | failcounter=0 9 | timeout_in_sec=360 10 | 11 | until [[ "$bootanim" =~ "stopped" ]]; do 12 | bootanim=`/sdk/platform-tools/adb -e shell getprop init.svc.bootanim 2>&1 &` 13 | if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline" 14 | || "$bootanim" =~ "running" ]]; then 15 | echo "Waiting for emulator to start" 16 | 17 | let "failcounter += 1" 18 | if [[ $failcounter -gt timeout_in_sec ]]; then 19 | echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator" 20 | exit 1 21 | fi 22 | 23 | elif [[ "$bootanim" =~ "no emulators found" ]]; then 24 | echo "No emulators were found" 25 | exit 1 26 | fi 27 | sleep 1 28 | done 29 | 30 | echo "Emulator is ready" 31 | -------------------------------------------------------------------------------- /scripts/commands-to-run-in-privileged-mode.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "#############################" 3 | echo " Running privileged commands" 4 | echo "#############################" 5 | 6 | echo "## Running emulator and saving snapshot" 7 | /sdk/emulator/emulator -avd "$1" -no-window -no-audio -no-snapshot-save & 8 | 9 | echo "## Waiting for emulator to come online" 10 | ./android-wait-for-emulator 11 | 12 | echo "## Making sure the emulator has fully booted" 13 | sleep 15 14 | 15 | echo "## Obtaining auth token" 16 | token="$(cat ~/.emulator_console_auth_token)" 17 | snapshot_name="$2" 18 | port="5554" 19 | 20 | echo "## Creating snapshot" 21 | ./create-snapshot.sh $token $snapshot_name $port 22 | 23 | echo "## Waiting until state has been saved" 24 | 25 | while [[ -e ~/.android/avd/"$1".avd/hardware-qemu.ini.lock ]]; do 26 | echo "State has not been fully saved" 27 | sleep 1 28 | done 29 | -------------------------------------------------------------------------------- /scripts/create-snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | # Get token argument 4 | set token [lindex $argv 0]; 5 | 6 | # Get token argument 7 | set snapshot_name [lindex $argv 1]; 8 | 9 | # Get port argument or set to 5554 10 | set port [lindex $argv 2] 11 | if {$port == ""} { 12 | set port "5554" 13 | } 14 | 15 | # Spawn telnet 16 | spawn telnet localhost $port 17 | 18 | # Wait till OK is shown 19 | expect "OK" 20 | 21 | # Authenticate 22 | send "auth $token\r" 23 | 24 | # Wait till OK is shown 25 | expect "OK" 26 | 27 | # Wait till OK is shown 28 | send "avd snapshot save $snapshot_name\r" 29 | 30 | # Wait till OK is shown 31 | expect "OK" 32 | 33 | # Kill emulator 34 | send "kill\r" 35 | 36 | # Wait till OK is shown 37 | expect "OK" --------------------------------------------------------------------------------