diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..1f3263e
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,26 @@
+name: Image Publishing
+
+on:
+ workflow_dispatch:
+
+jobs:
+ publish-images:
+ runs-on: ubuntu-latest
+ permissions:
+ packages: write
+ contents: read
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ - name: Setup Java
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: '11'
+ - name: Login to Docker Hub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKER_LOGIN_USER }}
+ password: ${{ secrets.DOCKER_LOGIN_TOKEN }}
+ - name: Publish Project
+ run: ./gradlew run --args="$(pwd)/configuration.toml"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d298ff7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+
+outputs
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..595baf1
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,4 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+sonarlint
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..4bec4ea
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..11b335b
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..9534be1
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinScripting.xml b/.idea/kotlinScripting.xml
new file mode 100644
index 0000000..bc444de
--- /dev/null
+++ b/.idea/kotlinScripting.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..d5d35ec
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0490d8c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,153 @@
+# Android Emulator Test Image
+
+[](https://opensource.org/licenses/Apache-2.0)
+
+This is a simple Java app to generate and publish my DockerImages.
+The DockerImages I am planning to use with `/dev/kvm` enabled Containers so I can run my tests androidTests on Emulators in my CI with acceptable speed.
+
+## To Use
+The images are published [here on DockerHub](https://hub.docker.com/repository/docker/fknives/android-test-img).
+
+> The Images are not yet tested, I consider testing them in the future (once I figure out how).
+
+To use as a base image: `FROM fknives/android-test-img:`
+
+To use in CI: `image: fknives/android-test-img:1.0.0`
+
+*Feel free to use or suggest alternatives.*
+
+## To Generate
+
+To generate the Java program is used. (For ease of maintenance, since Kotlin is the language I use the most)
+To run I use the following command: `./gradlew run --args="$(pwd)/configuration.toml`
+
+A standalone JAR could be generated if needed.
+
+> Note: GitHub Action is created to run the script.
+
+## Contains
+The Generated images contain Android BuildTools, SDKs, Gradle for easy Android builds and creation of Emulators.
+
+It contains a `androidemulatorstart` script, which based on the `$EMULATOR_API_LEVEL` ENV variable creates an Emulator then boots it.
+The script can be found [here](./main/src/main/resources/startemulator).
+
+## Configuration
+There are a couple of things to configure in the images, described here with the format and example:
+```toml
+# UPDATE: build tools versions can be found via sdkmanager --list and filtering it. Example: `sdkmanager --list | grep -o "build-tools;[0-9.][0-9.]*" | sort | uniq`
+# build tools installed into every image
+buildTools = ["33.0.0", "32.0.0", "31.0.0", "30.0.3", "30.0.2"]
+# UPDATE: sdk versions can be found via sdkmanager --list and filtering it. Example: `sdkmanager --list | grep -o "android-[0-9.][0-9.]*" | sort | uniq`
+# sdks installed into every image
+sdks = [30, 31, 32, 33]
+# UPDATE: latest command lines version here: https://developer.android.com/studio#command-tools
+androidCommandlineTools = "8512546_latest"
+# UPDATE: gradle version can be found in projects gradle-wrapper.properties
+gradleVersion = "7.3.3"
+# folder to save generated images into
+output = "outputs"
+
+# additional variations
+[variations]
+# apiLevel variation create separate image with emulator sdk installed and emulator created
+# this will create two additional images like fknives/android-test-img:1.0.0-api-21, fknives/android-test-img:1.0.0-api-25
+# This images depend on fknives/android-test-img:1.0.0 and add an additional SDK and create the emulator itself.
+apiLevels = [21,25]
+
+# the docker image description
+# /:
+# Example: fknives/android-test-img:1.0.0, fknives/android-test-img:1.0.0-api-21
+[image]
+repository = "fknives"
+namespace = "android-test-img"
+tagPrefix = "1.0.0"
+```
+
+## Example Dockerfile
+
+Dockerfile
+
+```Dockerfile
+FROM openjdk:11-jdk-slim
+# Generated on 2022-08-25
+
+# installing usual required tools
+# build-essential, ruby and bundler is needed for fastlane
+RUN apt-get update && apt-get install -y \
+ sudo \
+ wget \
+ curl \
+ unzip \
+ android-sdk \
+ build-essential \
+ ruby-full && \
+ gem install bundler
+
+# versions
+# latest command lines version here: https://developer.android.com/studio#command-tools
+ENV ANDROID_COMMAND_LINES_TOOLS_VERSION "8512546_latest"
+# gradle version can be found in projects gradle-wrapper.properties
+ENV GRADLE_VERSION "7.3.3"
+
+# set homes and paths
+ENV ANDROID_HOME "/usr/lib/android-sdk"
+ENV ANDROID_SDK_ROOT $ANDROID_HOME
+ENV CMDLINE_TOOLS_ROOT "${ANDROID_HOME}/cmdline-tools/latest/bin"
+ENV AVD_HOME "/root/.android/avd"
+ENV PATH "$ANDROID_HOME/cmdline-tools/latest/bin:${PATH}"
+ENV PATH "$ANDROID_HOME/emulator:${PATH}"
+ENV PATH "/usr/local/gradle-${GRADLE_VERSION}/bin:${PATH}"
+
+WORKDIR /root/
+
+# install gradle
+RUN curl -sSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip && \
+ unzip -d /usr/local /tmp/gradle.zip && \
+ rm -rf /tmp/gradle.zip && \
+ echo "1" | gradle init && \
+ /root/gradlew && \
+ rm -rf /root/* && \
+ rm /root/.gitignore
+
+# install command line tools
+RUN wget https://dl.google.com/android/repository/commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ unzip commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip -d cmdline-tools && \
+ rm commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ mv cmdline-tools/cmdline-tools/ cmdline-tools/latest && \
+ mv cmdline-tools $ANDROID_HOME/
+
+# install emulator and setup sdkmanager
+RUN yes | sdkmanager --licenses > /dev/null && \
+ sdkmanager --install emulator --channel=0 > /dev/null
+
+# support libraries and google play services
+RUN echo y | sdkmanager "extras;android;m2repository" && \
+ echo y | sdkmanager "extras;google;m2repository" && \
+ echo y | sdkmanager "extras;google;google_play_services"
+
+# build tools versions and sdk versions can be found via sdkmanager --list and filtering it
+# install build-tools
+# example: `echo y | sdkmanager "build-tools;33.0.0"`
+RUN echo y | sdkmanager "build-tools;33.0.0" && \
+ echo y | sdkmanager "build-tools;32.0.0" && \
+ echo y | sdkmanager "build-tools;31.0.0" && \
+ echo y | sdkmanager "build-tools;30.0.3" && \
+ echo y | sdkmanager "build-tools;30.0.2"
+
+# install sdks
+# example: `echo y | sdkmanager "platforms;android-33"`
+RUN echo y | sdkmanager "platforms;android-30" && \
+ echo y | sdkmanager "platforms;android-31" && \
+ echo y | sdkmanager "platforms;android-32" && \
+ echo y | sdkmanager "platforms;android-33"
+
+# copy script to install sdk, create emulator and boot it
+COPY ./startemulator /usr/local/bin/androidemulatorstart
+RUN chmod 744 /usr/local/bin/androidemulatorstart
+
+CMD ["/bin/bash"]
+```
+
+
+## License
+[License file](./LICENSE)
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..5c1ae9e
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,10 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("org.jetbrains.kotlin.jvm").version("1.6.21").apply(false)
+}
+
+tasks {
+ register("clean"){
+ delete(rootProject.buildDir)
+ }
+}
\ No newline at end of file
diff --git a/configuration.toml b/configuration.toml
new file mode 100644
index 0000000..9893cf7
--- /dev/null
+++ b/configuration.toml
@@ -0,0 +1,25 @@
+# UPDATE: build tools versions can be found via sdkmanager --list and filtering it. Example: `sdkmanager --list | grep -o "build-tools;[0-9.][0-9.]*" | sort | uniq`
+# build tools installed into every image
+buildTools = ["33.0.0", "32.0.0", "31.0.0", "30.0.3", "30.0.2"]
+# UPDATE: sdk versions can be found via sdkmanager --list and filtering it. Example: `sdkmanager --list | grep -o "android-[0-9.][0-9.]*" | sort | uniq`
+# sdks installed into every image
+sdks = [30, 31, 32, 33]
+# UPDATE: latest command lines version here: https://developer.android.com/studio#command-tools
+androidCommandlineTools = "8512546_latest"
+# UPDATE: gradle version can be found in projects gradle-wrapper.properties
+gradleVersion = "7.3.3"
+# folder to save generated images into
+output = "outputs-1.0.0"
+
+# additional variations
+[variations]
+# apiLevel variation create separate image with emulator sdk installed and emulator created
+apiLevels = []
+
+# the docker image description
+# /:
+# Example: fknives/android-test-img:1.0.0, fknives/android-test-img:1.0.0-api-21
+[image]
+repository = "fknives"
+namespace = "android-test-img"
+tagPrefix = "1.0.0"
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..107a766
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,11 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..2e8ad7e
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Aug 23 17:18:09 EEST 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/main/.gitignore b/main/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/main/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/main/build.gradle.kts b/main/build.gradle.kts
new file mode 100644
index 0000000..d20e6c8
--- /dev/null
+++ b/main/build.gradle.kts
@@ -0,0 +1,31 @@
+plugins {
+ id("org.jetbrains.kotlin.jvm")
+ id("application")
+}
+
+application {
+ mainClass.set("org.fnives.android.test.dockerfile.Main")
+}
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+tasks.test {
+ useJUnitPlatform()
+ testLogging {
+ setEvents(setOf("started", "passed", "skipped", "failed"))
+ setExceptionFormat("full")
+ showStandardStreams = true
+ }
+}
+
+dependencies {
+ val tomlParserVersion = "1.1.0"
+ implementation("cc.ekblad:4koma:$tomlParserVersion")
+
+ val testingJunitJupiterVersion = "5.7.0"
+ val testingJunitJupiterRuntimeVersion = "5.3.1"
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:$testingJunitJupiterVersion")
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$testingJunitJupiterRuntimeVersion")
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/Main.kt b/main/src/main/java/org/fnives/android/test/dockerfile/Main.kt
new file mode 100644
index 0000000..6e9b393
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/Main.kt
@@ -0,0 +1,65 @@
+package org.fnives.android.test.dockerfile
+
+import org.fnives.android.test.dockerfile.configuration.Configuration
+import org.fnives.android.test.dockerfile.configuration.ImageConfiguration
+import org.fnives.android.test.dockerfile.di.ServiceLocatorHolder.ServiceLocator
+import java.io.File
+
+/**
+ * Step into the Script.
+ *
+ * This script is intended to:
+ * - Generate Dockerfiles from the given configuration.
+ * - Create DockerImages from the generated Dockerfiles
+ * - Push those images up to DockerHub based on the configucation.
+ *
+ * Needs a single argument to the path of the configuration.toml file.
+ *
+ * Usually run like: ./gradlew run --args="$(pwd)/configuration.toml
+ *
+ * **Docker needs to be installed**
+ */
+object Main {
+
+ @JvmStatic
+ @Throws(IllegalArgumentException::class)
+ fun main(args: Array) {
+ val configuration = getConfigurationFromArgument(args)
+
+ val configuredServiceLocator = ServiceLocator.loadConfiguration(configuration)
+ val imageConfigurations = listOf(ImageConfiguration(configuration))
+ .plus(configuration.apiLevelVariations.map(::ImageConfiguration))
+
+ configuredServiceLocator.copyScript().invoke()
+ imageConfigurations.map(configuredServiceLocator.contentGenerator()::create)
+ .map(configuredServiceLocator.imageWriter()::write)
+ .forEach(configuredServiceLocator.imageBuildAndPush()::buildPushClean)
+ }
+
+ @Throws(IllegalArgumentException::class)
+ private fun getConfigurationFromArgument(args: Array): Configuration {
+ val configFile = getFileFromArgument(args)
+
+ return try {
+ configFile.inputStream().use(ServiceLocator.parser()::invoke)
+ } catch (illegalArgumentException: IllegalArgumentException) {
+ throw IllegalArgumentException("Unparseable file at given path: ${configFile.absolutePath}", illegalArgumentException)
+ }
+ }
+
+ @Throws(IllegalArgumentException::class)
+ private fun getFileFromArgument(args: Array): File {
+ require(args.isNotEmpty()) {
+ "Required argument is missing! Required argument is the config.toml file path and just that!"
+ }
+ require(args.size < 2) {
+ "More arguments than expected! Required argument is the config.toml file path and just that!"
+ }
+ val configFile = File(args[0])
+ require(configFile.exists()) {
+ "Can't find file at given path: ${configFile.absolutePath}"
+ }
+
+ return configFile
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/configuration/Configuration.kt b/main/src/main/java/org/fnives/android/test/dockerfile/configuration/Configuration.kt
new file mode 100644
index 0000000..56e8da2
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/configuration/Configuration.kt
@@ -0,0 +1,28 @@
+package org.fnives.android.test.dockerfile.configuration
+
+import org.fnives.android.test.dockerfile.generator.DockerFileContent
+
+/**
+ * Describes all the configurations which can be set inside the [toml][https://toml.io/en/] file.
+ *
+ * @param buildTools android build tools installed into the DockerImages
+ * @param sdks AndroidSDKs installed into the DockerImages
+ * @param commandlineTools AndroidCommandLinesTools installed into the DockerImages
+ * @param gradleVersion GradleVersion to be installed into the DockerImages
+ * @param output the folder which will contain the generated Dockerfiles
+ * @param dockerRepository the repository in DockerHub to push into.
+ * @param dockerNamespace the namespace inside the [dockerRepository] defined on DockerHub.
+ * @param dockerTagPrefix the prefix which should be added to all DockerImage tags when creating [DockerFileContent]
+ * @param apiLevelVariations the specific api levels that should have their own image. This is used so everything is ready for the Emulator for a specific API version.
+ */
+data class Configuration(
+ val buildTools: Set,
+ val sdks: Set,
+ val commandlineTools: String,
+ val gradleVersion: String,
+ val output: String,
+ val dockerRepository: String,
+ val dockerNamespace: String,
+ val dockerTagPrefix: String,
+ val apiLevelVariations: Set = emptySet()
+)
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ConfigurationParser.kt b/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ConfigurationParser.kt
new file mode 100644
index 0000000..48589e0
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ConfigurationParser.kt
@@ -0,0 +1,55 @@
+package org.fnives.android.test.dockerfile.configuration
+
+import cc.ekblad.toml.TomlMapper
+import cc.ekblad.toml.decode
+import cc.ekblad.toml.tomlMapper
+import java.io.InputStream
+
+/**
+ * Parses the given [toml][https://toml.io/en/] file into a [Configuration] to be usable by the other parts of the app.
+ */
+class ConfigurationParser(private val mapper: TomlMapper = tomlMapper {}) {
+
+ @Throws(IllegalArgumentException::class)
+ fun invoke(content: InputStream): Configuration {
+ val raw = try {
+ mapper.decode(content)
+ } catch (cause: Throwable) {
+ throw IllegalArgumentException("Couldn't process configuration", cause)
+ }
+ return raw.convert()
+ }
+
+ private fun RawConfiguration.convert(): Configuration = Configuration(
+ buildTools = buildTools,
+ sdks = sdks,
+ commandlineTools = androidCommandlineTools,
+ gradleVersion = gradleVersion,
+ output = output,
+ apiLevelVariations = variations?.apiLevels.orEmpty(),
+ dockerNamespace = image.namespace,
+ dockerRepository = image.repository,
+ dockerTagPrefix = image.tagPrefix
+ )
+
+ private data class RawConfiguration(
+ val buildTools: Set,
+ val sdks: Set,
+ val androidCommandlineTools: String,
+ val gradleVersion: String,
+ val output: String,
+ val variations: Variations?,
+ val image: Image
+ ) {
+
+ class Variations(
+ val apiLevels: Set = emptySet()
+ )
+
+ class Image(
+ val namespace: String,
+ val repository: String,
+ val tagPrefix: String,
+ )
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ImageConfiguration.kt b/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ImageConfiguration.kt
new file mode 100644
index 0000000..ffdda1b
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ImageConfiguration.kt
@@ -0,0 +1,17 @@
+package org.fnives.android.test.dockerfile.configuration
+
+/**
+ * Describes the specific that should be used to fill out the Dockerfile templates.
+ */
+sealed class ImageConfiguration {
+ data class Generic(
+ val buildTools: Set,
+ val sdks: Set,
+ val commandlineTools: String,
+ val gradleVersion: String
+ ) : ImageConfiguration()
+
+ data class ApiVersion(
+ val version: Int
+ ) : ImageConfiguration()
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ImageConfigurationConvert.kt b/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ImageConfigurationConvert.kt
new file mode 100644
index 0000000..c818384
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/configuration/ImageConfigurationConvert.kt
@@ -0,0 +1,17 @@
+package org.fnives.android.test.dockerfile.configuration
+
+/**
+ * Utility function to ease the creation of a generic [ImageConfiguration]
+ */
+fun ImageConfiguration(configuration: Configuration): ImageConfiguration =
+ ImageConfiguration.Generic(
+ buildTools = configuration.buildTools,
+ sdks = configuration.sdks,
+ commandlineTools = configuration.commandlineTools,
+ gradleVersion = configuration.gradleVersion,
+ )
+
+/**
+ * Utility function to ease the creation of a specific [ImageConfiguration], that will depend on a Generic one.
+ */
+fun ImageConfiguration(version: Int): ImageConfiguration = ImageConfiguration.ApiVersion(version)
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/di/ConfiguredServiceLocator.kt b/main/src/main/java/org/fnives/android/test/dockerfile/di/ConfiguredServiceLocator.kt
new file mode 100644
index 0000000..946b4ff
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/di/ConfiguredServiceLocator.kt
@@ -0,0 +1,16 @@
+package org.fnives.android.test.dockerfile.di
+
+import org.fnives.android.test.dockerfile.generator.DockerFileContentGenerator
+import org.fnives.android.test.dockerfile.push.DockerBuildAndPush
+import org.fnives.android.test.dockerfile.write.DockerFileWriter
+import org.fnives.android.test.dockerfile.write.ScriptCopier
+
+/**
+ * Gives access to the Services that depend on [Configuration][org.fnives.android.test.dockerfile.configuration.Configuration] and takes care of their creation and instances.
+ */
+interface ConfiguredServiceLocator {
+ fun contentGenerator(): DockerFileContentGenerator
+ fun imageWriter(): DockerFileWriter
+ fun imageBuildAndPush(): DockerBuildAndPush
+ fun copyScript(): ScriptCopier
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/di/DefaultConfiguredServiceLocator.kt b/main/src/main/java/org/fnives/android/test/dockerfile/di/DefaultConfiguredServiceLocator.kt
new file mode 100644
index 0000000..8ea158e
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/di/DefaultConfiguredServiceLocator.kt
@@ -0,0 +1,61 @@
+package org.fnives.android.test.dockerfile.di
+
+import org.fnives.android.test.dockerfile.configuration.Configuration
+import org.fnives.android.test.dockerfile.generator.DockerFileContentConfig
+import org.fnives.android.test.dockerfile.generator.DockerFileContentGenerator
+import org.fnives.android.test.dockerfile.push.DockerBuildAndPush
+import org.fnives.android.test.dockerfile.push.PushConfig
+import org.fnives.android.test.dockerfile.util.resourceFileContent
+import org.fnives.android.test.dockerfile.util.clock.Clock
+import org.fnives.android.test.dockerfile.write.DockerFileWriter
+import org.fnives.android.test.dockerfile.write.ScriptCopier
+import java.io.File
+
+/** Syntax Sugar */
+fun ConfiguredServiceLocator(configuration: Configuration): ConfiguredServiceLocator =
+ DefaultConfiguredServiceLocator(configuration)
+
+/**
+ * Actual [ConfiguredServiceLocator] keeping references and creating the Services that are dependent on [Configuration].
+ */
+class DefaultConfiguredServiceLocator(private val configuration: Configuration) : ConfiguredServiceLocator {
+ private val pushConfig
+ get() = PushConfig(
+ dockerRepository = configuration.dockerRepository,
+ dockerNamespace = configuration.dockerNamespace,
+ output = configuration.output
+ )
+ private val outputFolder get() = File(configuration.output)
+
+ private val processRunner get() = ServiceLocatorHolder.ServiceLocator.processRunner()
+ private val clock: Clock get() = ServiceLocatorHolder.ServiceLocator.clock()
+ private val scriptName: String get() = ServiceLocatorHolder.ServiceLocator.scriptToCopy()
+
+ private val imageBuildAndPush by lazy { DockerBuildAndPush(config = pushConfig, processRunner = processRunner) }
+ private val imageWriter by lazy { DockerFileWriter(outputFolder = outputFolder) }
+ private val copyScript by lazy { ScriptCopier(outputFolder = outputFolder, scriptName = scriptName) }
+
+ private val dockerFileContentConfig
+ get() = DockerFileContentConfig(
+ templateGeneric = resourceFileContent("Dockerfile.template"),
+ templateSpecific = resourceFileContent("Dockerfile.api.template"),
+ dockerRepository = configuration.dockerRepository,
+ dockerNamespace = configuration.dockerNamespace,
+ dockerTagPrefix = configuration.dockerTagPrefix,
+ )
+
+ private val contentGenerator by lazy {
+ DockerFileContentGenerator(
+ config = dockerFileContentConfig,
+ clock = clock
+ )
+ }
+
+ override fun contentGenerator(): DockerFileContentGenerator = contentGenerator
+
+ override fun imageWriter(): DockerFileWriter = imageWriter
+
+ override fun imageBuildAndPush(): DockerBuildAndPush = imageBuildAndPush
+
+ override fun copyScript(): ScriptCopier = copyScript
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/di/DefaultServiceLocator.kt b/main/src/main/java/org/fnives/android/test/dockerfile/di/DefaultServiceLocator.kt
new file mode 100644
index 0000000..e9d49a2
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/di/DefaultServiceLocator.kt
@@ -0,0 +1,28 @@
+package org.fnives.android.test.dockerfile.di
+
+import org.fnives.android.test.dockerfile.configuration.Configuration
+import org.fnives.android.test.dockerfile.configuration.ConfigurationParser
+import org.fnives.android.test.dockerfile.push.ProcessRunner
+import org.fnives.android.test.dockerfile.push.SystemProcessRunner
+import org.fnives.android.test.dockerfile.util.clock.Clock
+import org.fnives.android.test.dockerfile.util.clock.SystemCock
+
+/**
+ * Actual [ServiceLocator] keeping references and creating the Services.
+ */
+object DefaultServiceLocator : ServiceLocator {
+
+ private val parser by lazy { ConfigurationParser() }
+ private val processRunner by lazy { SystemProcessRunner() }
+
+ override fun loadConfiguration(configuration: Configuration): ConfiguredServiceLocator =
+ ConfiguredServiceLocator(configuration)
+
+ override fun parser(): ConfigurationParser = parser
+
+ override fun clock(): Clock = SystemCock
+
+ override fun scriptToCopy(): String = "startemulator"
+
+ override fun processRunner(): ProcessRunner = processRunner
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/di/ServiceLocator.kt b/main/src/main/java/org/fnives/android/test/dockerfile/di/ServiceLocator.kt
new file mode 100644
index 0000000..7701406
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/di/ServiceLocator.kt
@@ -0,0 +1,17 @@
+package org.fnives.android.test.dockerfile.di
+
+import org.fnives.android.test.dockerfile.configuration.Configuration
+import org.fnives.android.test.dockerfile.configuration.ConfigurationParser
+import org.fnives.android.test.dockerfile.push.ProcessRunner
+import org.fnives.android.test.dockerfile.util.clock.Clock
+
+/**
+ * Gives access to the Services and takes care of their creation and instances.
+ */
+interface ServiceLocator {
+ fun parser(): ConfigurationParser
+ fun clock(): Clock
+ fun scriptToCopy(): String
+ fun processRunner(): ProcessRunner
+ fun loadConfiguration(configuration: Configuration): ConfiguredServiceLocator
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/di/ServiceLocatorHolder.kt b/main/src/main/java/org/fnives/android/test/dockerfile/di/ServiceLocatorHolder.kt
new file mode 100644
index 0000000..f85c931
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/di/ServiceLocatorHolder.kt
@@ -0,0 +1,32 @@
+package org.fnives.android.test.dockerfile.di
+
+/**
+ * Access point to [ServiceLocator][org.fnives.android.test.dockerfile.di.ServiceLocator] so it can be [swap]ed out in tests.
+ */
+object ServiceLocatorHolder {
+
+ /**
+ * Syntax Sugar
+ */
+ val ServiceLocator get() = get()
+
+ val default: ServiceLocator = DefaultServiceLocator
+ private var actual: ServiceLocator = default
+
+ /**
+ * Change the [ServiceLocator][org.fnives.android.test.dockerfile.di.ServiceLocator] returned in [get] and used.
+ */
+ fun swap(serviceLocator: ServiceLocator) {
+ actual = serviceLocator
+ }
+
+ /**
+ * Reset back to [default] [ServiceLocator][org.fnives.android.test.dockerfile.di.ServiceLocator] to be returned in [get]
+ */
+ fun reset() = swap(default)
+
+ /**
+ * Returns the actual [ServiceLocator][org.fnives.android.test.dockerfile.di.ServiceLocator]
+ */
+ fun get() = actual
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContent.kt b/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContent.kt
new file mode 100644
index 0000000..c5467b9
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContent.kt
@@ -0,0 +1,14 @@
+package org.fnives.android.test.dockerfile.generator
+
+/**
+ * Describes the content and necessary information to create a Dockerfile and DockerImage from that.
+ *
+ * @param name the name of the Dockerfile to be created
+ * @param tag the id of the DockerImage to be created
+ * @param content the commands which should be in the Dockerfile
+ */
+data class DockerFileContent(
+ val name: String,
+ val tag: String,
+ val content: String
+)
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContentConfig.kt b/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContentConfig.kt
new file mode 100644
index 0000000..4207f64
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContentConfig.kt
@@ -0,0 +1,18 @@
+package org.fnives.android.test.dockerfile.generator
+
+/**
+ * Configuration for the [DockerFileContentGenerator]
+ *
+ * @param templateGeneric the Dockerfile template content for a Generic image
+ * @param templateSpecific the Dockerfile template content for a Specific image, which extends the Generic and adds additional installs.
+ * @param dockerRepository the repository in DockerHub to push into.
+ * @param dockerNamespace the namespace inside the [dockerRepository] defined on DockerHub.
+ * @param dockerTagPrefix the prefix which should be added to all DockerImage tags when creating [DockerFileContent]
+ */
+data class DockerFileContentConfig(
+ val templateGeneric: String,
+ val templateSpecific: String,
+ val dockerRepository: String,
+ val dockerNamespace: String,
+ val dockerTagPrefix: String,
+)
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContentGenerator.kt b/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContentGenerator.kt
new file mode 100644
index 0000000..8ba8ab8
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/generator/DockerFileContentGenerator.kt
@@ -0,0 +1,103 @@
+package org.fnives.android.test.dockerfile.generator
+
+import org.fnives.android.test.dockerfile.configuration.ImageConfiguration
+import org.fnives.android.test.dockerfile.util.clock.Clock
+import java.text.SimpleDateFormat
+import java.util.Date
+
+/**
+ * Creates [DockerFileContent] from an [ImageConfiguration] based on the [DockerFileContentConfig].
+ *
+ * Uses the templates of [DockerFileContentConfig] and replaces the specifics described in the [ImageConfiguration].
+ * The templates have keywords which are used to replace with the specific commands defined by [ImageConfiguration]
+ */
+class DockerFileContentGenerator(
+ private val config: DockerFileContentConfig,
+ private val clock: Clock
+) {
+
+ fun create(imageConfiguration: ImageConfiguration): DockerFileContent {
+ val dockerfileName = dockerFileName(imageConfiguration.version())
+ val tag = "${config.dockerRepository}/${config.dockerNamespace}:${config.dockerTagPrefix}${tagSuffix(imageConfiguration.version())}"
+ val genericTag = "${config.dockerRepository}/${config.dockerNamespace}:${config.dockerTagPrefix}${tagSuffix(null)}"
+
+ val content = when (imageConfiguration) {
+ is ImageConfiguration.Generic ->
+ config.templateGeneric.setCommandLineToolsVersion(imageConfiguration.commandlineTools)
+ .setGradleVersion(imageConfiguration.gradleVersion)
+ .setInstalledSDKs(imageConfiguration.sdks)
+ .setInstalledBuildTools(imageConfiguration.buildTools)
+ .setDateStamp()
+ is ImageConfiguration.ApiVersion ->
+ config.templateSpecific.setFromTag(genericTag)
+ .setENVAPILevel(imageConfiguration.version)
+ .setDateStamp()
+ }
+
+ return DockerFileContent(
+ name = dockerfileName,
+ content = content,
+ tag = tag
+ )
+ }
+
+ private fun String.setCommandLineToolsVersion(version: String): String =
+ replace(COMMAND_LINES_KEY, version)
+
+ private fun String.setGradleVersion(version: String): String =
+ replace(GRADLE_VERSION_KEY, version)
+
+ private fun String.setInstalledSDKs(sdks: Set): String {
+ val sdkInstallCommand = sdks.joinToString(" && \\\n ") { getSdkInstallCommand(it) }
+
+ return replace(SDK_INSTALL_COMMANDS_KEY, sdkInstallCommand)
+ }
+
+ private fun String.setInstalledBuildTools(buildTools: Set): String {
+ val buildToolsInstallCommand = buildTools.joinToString(" && \\\n ") { getBuildToolsInstallCommand(it) }
+
+ return replace(BUILD_TOOLS_INSTALL_COMMANDS_KEY, buildToolsInstallCommand)
+ }
+
+ private fun dockerFileName(version: Int?): String =
+ if (version == null) {
+ "Dockerfile"
+ } else {
+ "Dockerfile-api-$version"
+ }
+
+ private fun tagSuffix(version: Int?): String = if (version == null) "" else "-api-$version"
+
+ private fun String.setFromTag(tagPrefix: String): String {
+ val tag = tagPrefix + tagSuffix(null)
+
+ return replace(TAG_KEY, tag)
+ }
+
+ private fun String.setENVAPILevel(apiLevel: Int): String = replace(SET_API_LEVEL, apiLevel.toString())
+
+ private fun String.setDateStamp(): String {
+ val dateStamp = SimpleDateFormat("yyyy-MM-dd").format(Date(clock.currentTimeMillis()))
+
+ return replace(DATE_STAMP_KEY, "Generated on $dateStamp")
+ }
+
+ companion object {
+ private const val COMMAND_LINES_KEY = "{{COMMAND_LINE_TOOLS}}"
+ private const val GRADLE_VERSION_KEY = "{{GRADLE_VERSION}}"
+ private const val SDK_INSTALL_COMMANDS_KEY = "{{SDK_INSTALL_COMMANDS}}"
+ private const val BUILD_TOOLS_INSTALL_COMMANDS_KEY = "{{BUILD_TOOLS_INSTALL_COMMANDS}}"
+ private const val TAG_KEY = "{{IMG-TAG}}"
+ private const val SET_API_LEVEL = "{{API_LEVEL}}"
+ private const val DATE_STAMP_KEY = "{{GENERATED_AT}}"
+
+ private fun getSdkInstallCommand(sdk: Int) = "echo y | sdkmanager \"platforms;android-$sdk\""
+
+ private fun getBuildToolsInstallCommand(buildTool: String) = "echo y | sdkmanager \"build-tools;$buildTool\""
+
+ private fun ImageConfiguration.version() = when (this) {
+ is ImageConfiguration.ApiVersion -> version
+ is ImageConfiguration.Generic -> null
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/push/DockerBuildAndPush.kt b/main/src/main/java/org/fnives/android/test/dockerfile/push/DockerBuildAndPush.kt
new file mode 100644
index 0000000..6963ac5
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/push/DockerBuildAndPush.kt
@@ -0,0 +1,38 @@
+package org.fnives.android.test.dockerfile.push
+
+import org.fnives.android.test.dockerfile.write.DockerFileDescriptor
+import java.io.File
+
+/**
+ * Runs the necessary docker commands to create, push and cleanup the DockerImage from the given Dockerfile.
+ */
+class DockerBuildAndPush(
+ private val config: PushConfig,
+ private val processRunner: ProcessRunner
+) {
+
+ private val workDir get() = File(config.output)
+
+ fun build(dockerFileDescriptor: DockerFileDescriptor): String {
+ "docker build -t ${dockerFileDescriptor.tag} -f ${dockerFileDescriptor.filePath} .".runCommand(workDir)
+
+ return dockerFileDescriptor.tag
+ }
+
+ fun push(id: String) {
+ "docker push $id".runCommand(workDir)
+ }
+
+ fun removeLocalImage(id: String) {
+ "docker image rm -f $id".runCommand(workDir)
+ }
+
+ fun buildPushClean(dockerFileDescriptor: DockerFileDescriptor) {
+ val id = build(dockerFileDescriptor)
+ push(id)
+ removeLocalImage(id)
+ }
+
+ private fun String.runCommand(workingDir: File) =
+ processRunner.run(workingDirectory = workingDir, command = this)
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/push/ProcessRunner.kt b/main/src/main/java/org/fnives/android/test/dockerfile/push/ProcessRunner.kt
new file mode 100644
index 0000000..6c06210
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/push/ProcessRunner.kt
@@ -0,0 +1,16 @@
+package org.fnives.android.test.dockerfile.push
+
+import java.io.File
+import java.util.concurrent.TimeoutException
+
+/**
+ * Process Runner is able to run a given command in the System process.
+ */
+interface ProcessRunner {
+
+ /**
+ * Runs the given [command] as a System process in [workingDirectory].
+ */
+ @Throws(TimeoutException::class)
+ fun run(workingDirectory: File, command: String)
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/push/PushConfig.kt b/main/src/main/java/org/fnives/android/test/dockerfile/push/PushConfig.kt
new file mode 100644
index 0000000..43f8ced
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/push/PushConfig.kt
@@ -0,0 +1,14 @@
+package org.fnives.android.test.dockerfile.push
+
+/**
+ * Configuration for [DockerBuildAndPush].
+ *
+ * @param dockerRepository the repository in DockerHub to push into.
+ * @param dockerNamespace the namespace inside the [dockerRepository] defined on DockerHub.
+ * @param output the path to the output folder.
+ */
+data class PushConfig(
+ val dockerRepository: String,
+ val dockerNamespace: String,
+ val output: String,
+)
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/push/SystemProcessRunner.kt b/main/src/main/java/org/fnives/android/test/dockerfile/push/SystemProcessRunner.kt
new file mode 100644
index 0000000..7530ec7
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/push/SystemProcessRunner.kt
@@ -0,0 +1,26 @@
+package org.fnives.android.test.dockerfile.push
+
+import java.io.File
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+/**
+ * Actual [ProcessRunner] which converts the command into the Process then starts and awaits it.
+ */
+class SystemProcessRunner(
+ private val timeout: Long = 60,
+ private val timeoutUnit: TimeUnit = TimeUnit.MINUTES
+) : ProcessRunner {
+
+ override fun run(workingDirectory: File, command: String) {
+ val result = ProcessBuilder(*command.split(" ").toTypedArray())
+ .directory(workingDirectory)
+ .redirectOutput(ProcessBuilder.Redirect.INHERIT)
+ .redirectError(ProcessBuilder.Redirect.INHERIT)
+ .start()
+ .waitFor(timeout, timeoutUnit)
+ if (!result) {
+ throw TimeoutException("The command have not finished in $timeout ${timeoutUnit}! `$command`")
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/util/ResourceUtil.kt b/main/src/main/java/org/fnives/android/test/dockerfile/util/ResourceUtil.kt
new file mode 100644
index 0000000..26c5ab3
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/util/ResourceUtil.kt
@@ -0,0 +1,24 @@
+package org.fnives.android.test.dockerfile.util
+
+import java.io.BufferedReader
+import java.io.InputStream
+import java.io.InputStreamReader
+
+/**
+ * Helper function to read contents of files baked into the resources folder
+ */
+internal fun Any.resourceFileContent(filePath: String): String = try {
+ BufferedReader(InputStreamReader(resourceFileStream(filePath)))
+ .readLines().joinToString("\n")
+} catch (nullPointerException: NullPointerException) {
+ throw IllegalArgumentException("$filePath file not found!", nullPointerException)
+}
+
+/**
+ * Helper function to access contents of files baked into the resources folder
+ */
+internal fun Any.resourceFileStream(filePath: String): InputStream = try {
+ this.javaClass.classLoader.getResourceAsStream(filePath)!!
+} catch (nullPointerException: NullPointerException) {
+ throw IllegalArgumentException("$filePath file not found!", nullPointerException)
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/util/clock/Clock.kt b/main/src/main/java/org/fnives/android/test/dockerfile/util/clock/Clock.kt
new file mode 100644
index 0000000..460ecc8
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/util/clock/Clock.kt
@@ -0,0 +1,9 @@
+package org.fnives.android.test.dockerfile.util.clock
+
+/**
+ * Simple Utility interface providing the [currentTimeMillis].
+ */
+interface Clock {
+
+ fun currentTimeMillis(): Long
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/util/clock/SystemCock.kt b/main/src/main/java/org/fnives/android/test/dockerfile/util/clock/SystemCock.kt
new file mode 100644
index 0000000..033c652
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/util/clock/SystemCock.kt
@@ -0,0 +1,9 @@
+package org.fnives.android.test.dockerfile.util.clock
+
+/**
+ * Actual [Clock] returning the System time.
+ */
+object SystemCock : Clock {
+
+ override fun currentTimeMillis(): Long = System.currentTimeMillis()
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/write/DockerFileDescriptor.kt b/main/src/main/java/org/fnives/android/test/dockerfile/write/DockerFileDescriptor.kt
new file mode 100644
index 0000000..fd00eaa
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/write/DockerFileDescriptor.kt
@@ -0,0 +1,8 @@
+package org.fnives.android.test.dockerfile.write
+
+/**
+ * Describes [DockerFileContent][org.fnives.android.test.dockerfile.generator.DockerFileContent] which was written into a file.
+ * @param filePath is the absolut path of the File
+ * @param tag is the id of the DockerImage.
+ */
+data class DockerFileDescriptor(val filePath: String, val tag: String)
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/write/DockerFileWriter.kt b/main/src/main/java/org/fnives/android/test/dockerfile/write/DockerFileWriter.kt
new file mode 100644
index 0000000..4192166
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/write/DockerFileWriter.kt
@@ -0,0 +1,21 @@
+package org.fnives.android.test.dockerfile.write
+
+import org.fnives.android.test.dockerfile.generator.DockerFileContent
+import java.io.File
+
+/**
+ * Simple configured class which can write a [DockerFileContent] into a file
+ * within the given [outputFolder] configuration, returns [DockerFileDescriptor] describing the written file.
+ */
+class DockerFileWriter(private val outputFolder: File) {
+
+ fun write(dockerFile: DockerFileContent): DockerFileDescriptor {
+ outputFolder.mkdirs()
+
+ val file = File(outputFolder, dockerFile.name)
+ file.createNewFile()
+ file.writeText(dockerFile.content)
+
+ return DockerFileDescriptor(filePath = file.absolutePath, tag = dockerFile.tag)
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/org/fnives/android/test/dockerfile/write/ScriptCopier.kt b/main/src/main/java/org/fnives/android/test/dockerfile/write/ScriptCopier.kt
new file mode 100644
index 0000000..782b202
--- /dev/null
+++ b/main/src/main/java/org/fnives/android/test/dockerfile/write/ScriptCopier.kt
@@ -0,0 +1,16 @@
+package org.fnives.android.test.dockerfile.write
+
+import org.fnives.android.test.dockerfile.util.resourceFileStream
+import java.io.File
+
+class ScriptCopier(private val outputFolder: File, private val scriptName: String) {
+
+ fun invoke() {
+ outputFolder.mkdirs()
+ val outputScript = File(outputFolder, scriptName)
+ outputScript.createNewFile()
+ outputScript.outputStream().use {
+ resourceFileStream(scriptName).copyTo(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/resources/Dockerfile.api.template b/main/src/main/resources/Dockerfile.api.template
new file mode 100644
index 0000000..40a3a82
--- /dev/null
+++ b/main/src/main/resources/Dockerfile.api.template
@@ -0,0 +1,8 @@
+FROM {{IMG-TAG}}
+# {{GENERATED_AT}}
+
+ENV EMULATOR_API_LEVEL {{API_LEVEL}}
+
+RUN /usr/local/bin/androidemulatorstart --buildOnly
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/main/src/main/resources/Dockerfile.template b/main/src/main/resources/Dockerfile.template
new file mode 100644
index 0000000..e313d5a
--- /dev/null
+++ b/main/src/main/resources/Dockerfile.template
@@ -0,0 +1,71 @@
+FROM openjdk:11-jdk-slim
+# {{GENERATED_AT}}
+
+# installing usual required tools
+# build-essential, ruby and bundler is needed for fastlane
+RUN apt-get update && apt-get install -y \
+ sudo \
+ wget \
+ curl \
+ unzip \
+ android-sdk \
+ build-essential \
+ ruby-full && \
+ gem install bundler
+
+# versions
+# latest command lines version here: https://developer.android.com/studio#command-tools
+ENV ANDROID_COMMAND_LINES_TOOLS_VERSION "{{COMMAND_LINE_TOOLS}}"
+# gradle version can be found in projects gradle-wrapper.properties
+ENV GRADLE_VERSION "{{GRADLE_VERSION}}"
+
+# set homes and paths
+ENV ANDROID_HOME "/usr/lib/android-sdk"
+ENV ANDROID_SDK_ROOT $ANDROID_HOME
+ENV CMDLINE_TOOLS_ROOT "${ANDROID_HOME}/cmdline-tools/latest/bin"
+ENV AVD_HOME "/root/.android/avd"
+ENV PATH "$ANDROID_HOME/cmdline-tools/latest/bin:${PATH}"
+ENV PATH "$ANDROID_HOME/emulator:${PATH}"
+ENV PATH "/usr/local/gradle-${GRADLE_VERSION}/bin:${PATH}"
+
+WORKDIR /root/
+
+# install gradle
+RUN curl -sSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip && \
+ unzip -d /usr/local /tmp/gradle.zip && \
+ rm -rf /tmp/gradle.zip && \
+ echo "1" | gradle init && \
+ /root/gradlew && \
+ rm -rf /root/* && \
+ rm /root/.gitignore
+
+# install command line tools
+RUN wget https://dl.google.com/android/repository/commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ unzip commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip -d cmdline-tools && \
+ rm commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ mv cmdline-tools/cmdline-tools/ cmdline-tools/latest && \
+ mv cmdline-tools $ANDROID_HOME/
+
+# install emulator and setup sdkmanager
+RUN yes | sdkmanager --licenses > /dev/null && \
+ sdkmanager --install emulator --channel=0 > /dev/null
+
+# support libraries and google play services
+RUN echo y | sdkmanager "extras;android;m2repository" && \
+ echo y | sdkmanager "extras;google;m2repository" && \
+ echo y | sdkmanager "extras;google;google_play_services"
+
+# build tools versions and sdk versions can be found via sdkmanager --list and filtering it
+# install build-tools
+# example: `echo y | sdkmanager "build-tools;33.0.0"`
+RUN {{BUILD_TOOLS_INSTALL_COMMANDS}}
+
+# install sdks
+# example: `echo y | sdkmanager "platforms;android-33"`
+RUN {{SDK_INSTALL_COMMANDS}}
+
+# copy script to install sdk, create emulator and boot it
+COPY ./startemulator /usr/local/bin/androidemulatorstart
+RUN chmod 744 /usr/local/bin/androidemulatorstart
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/main/src/main/resources/startemulator b/main/src/main/resources/startemulator
new file mode 100644
index 0000000..a846bf6
--- /dev/null
+++ b/main/src/main/resources/startemulator
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+if [[ "$1" == "--help" ]]
+then
+ echo "Creates default emulator and starts it."
+ echo "API level and Target can be customized, for more customization use the avdmanager directly!"
+ echo "API_level is read from EMULATOR_API_LEVEL variable"
+ echo "target is read from EMULATOR_TARGET variable or defaults to target"
+ echo "--buildOnly parameter means the emulator will be created, but not started"
+ exit 0;
+fi
+
+if [[ -z $EMULATOR_API_LEVEL ]]
+then
+ echo "EMULATOR_API_LEVEL not set!" >&2
+ exit 1;
+fi
+
+if [[ -z $EMULATOR_TARGET ]]
+then
+ if [[ $EMULATOR_API_LEVEL -ge 32 ]]
+ then
+ echo "EMULATOR_TARGET not set using \"google_apis\", \"default\" doesn't exists above 32"
+ EMULATOR_TARGET="google_apis"
+ else
+ echo "EMULATOR_TARGET not set using \"default\"" >&2
+ EMULATOR_TARGET="default"
+ fi
+fi
+
+if [[ -z $AVD_HOME ]]
+then
+ AVD_HOME="$HOME/.android/avd"
+ echo "AVD_HOME not set, using fallback: $AVD_HOME"
+fi
+
+if [[ "$1" == "--buildOnly" ]]
+then
+ echo "INFO: --buildOnly found, emulator will be created but not started!" >&2
+fi
+
+echo "INFO: installing sdk" >&2
+sdkmanager --install "system-images;android-$EMULATOR_API_LEVEL;$EMULATOR_TARGET;x86_64" --channel=0 > /dev/null 2> /dev/null
+
+
+echo "INFO: creating emulator" >&2
+EMULATOR_NAME="test_$EMULATOR_API_LEVEL"
+echo "no" | avdmanager create avd -n "$EMULATOR_NAME" --abi "$EMULATOR_TARGET/x86_64" --package "system-images;android-$EMULATOR_API_LEVEL;$EMULATOR_TARGET;x86_64"
+
+echo "INFO: config of emulator:" >&2
+cat $AVD_HOME/$EMULATOR_NAME.avd/config.ini >&2
+echo "INFO: modifying config of emulator..." >&2
+echo 'disk.dataPartition.size=2G\n' >> $AVD_HOME/$EMULATOR_NAME.avd/config.ini
+
+if [[ "$1" == "--buildOnly" ]]
+then
+ echo "INFO: emulator created!" >&2
+ exit 0;
+fi
+
+echo "INFO: checking devices, expecting nothing..." >&2
+adb devices
+
+echo "INFO: starting emulator..." >&2
+emulator -avd $EMULATOR_NAME -no-snapshot-save -no-window -gpu off -noaudio -no-boot-anim -camera-back none > emulator.log &
+
+echo "INFO: waiting for emulator..." >&2
+sleep 30s
+
+echo "INFO: emulator logs" >&2
+cat emulator.log
+
+echo "INFO: checking devices..." >&2
+adb devices
+echo "INFO: waiting for emulator to be booted..." >&2
+BOOT_WAIT_LOG_MESSAGE="INFO: still waiting for emulator to boot"
+SECONDS_PASSED=0;
+BOOT_COMPLETE=`adb shell getprop sys.boot_completed`
+while [[ -z $BOOT_COMPLETE ]];
+do
+ sleep $BOOT_DELAY_IN_SECONDS;
+ SECONDS_PASSED=$(( $SECONDS_PASSED+$BOOT_DELAY_IN_SECONDS ));
+ echo "INFO: still waiting for emulator to boot ($SECONDS_PASSED seconds passed)"; >&2
+ BOOT_COMPLETE=`adb shell getprop sys.boot_completed`
+done;
+echo "INFO: emulator booted." >&2
+# alternative: https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator
+
+echo "INFO: clearing animations..." >&2
+adb shell settings put global window_animation_scale 0.0
+adb shell settings put global transition_animation_scale 0.0
+adb shell settings put global animator_duration_scale 0.0
+
+echo "INFO: waiting for emulator have sdk property..." >&2
+SDK_VERSION=`adb shell getprop ro.build.version.sdk`
+while [[ -z $SDK_VERSION ]];
+do
+ sleep $BOOT_DELAY_IN_SECONDS;
+ echo "INFO: still waiting for emulator to have SDK property"; >&2
+ SDK_VERSION=`adb shell getprop ro.build.version.sdk`
+done;
+
+echo "INFO: unlocking screen..." >&2
+adb shell input keyevent 82
+echo "INFO: emulator ready!" >&2
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/configuration/ConfigurationParserTest.kt b/main/src/test/java/org/fnives/android/test/dockerfile/configuration/ConfigurationParserTest.kt
new file mode 100644
index 0000000..259316f
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/configuration/ConfigurationParserTest.kt
@@ -0,0 +1,56 @@
+package org.fnives.android.test.dockerfile.configuration
+
+import org.fnives.android.test.dockerfile.util.resourceFileAsInputStream
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class ConfigurationParserTest {
+
+ private lateinit var parser: ConfigurationParser
+
+ @BeforeEach
+ fun setUp() {
+ parser = ConfigurationParser()
+ }
+
+ @Test
+ fun example() {
+ val expectedConfiguration = Configuration(
+ buildTools = setOf("33.0.0", "32.0.0", "31.0.0", "30.0.3", "30.0.2"),
+ sdks = setOf(30, 31, 32, 33),
+ commandlineTools = "8512546_latest",
+ gradleVersion = "7.3.3",
+ output = "outputs",
+ dockerRepository = "fknives",
+ dockerNamespace = "android-test-img",
+ dockerTagPrefix = "1.0.0",
+ apiLevelVariations = setOf(21, 22, 23)
+ )
+ val rawConfiguration = resourceFileAsInputStream("example_configuration.toml")
+
+ val actual = parser.invoke(rawConfiguration)
+
+ Assertions.assertEquals(expectedConfiguration, actual)
+ }
+
+ @Test
+ fun singleConfiguration() {
+ val expectedConfiguration = Configuration(
+ buildTools = setOf("33.0.0", "32.0.0", "31.0.0"),
+ sdks = setOf(32, 33),
+ commandlineTools = "8512546_latest",
+ gradleVersion = "7.3.3",
+ output = "outputs",
+ dockerRepository = "fknives",
+ dockerNamespace = "android-test-img",
+ dockerTagPrefix = "1.0.0",
+ apiLevelVariations = emptySet()
+ )
+ val rawConfiguration = resourceFileAsInputStream("single_configuration.toml")
+
+ val actual = parser.invoke(rawConfiguration)
+
+ Assertions.assertEquals(expectedConfiguration, actual)
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/generator/DockerFileContentGeneratorTest.kt b/main/src/test/java/org/fnives/android/test/dockerfile/generator/DockerFileContentGeneratorTest.kt
new file mode 100644
index 0000000..173bf01
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/generator/DockerFileContentGeneratorTest.kt
@@ -0,0 +1,60 @@
+package org.fnives.android.test.dockerfile.generator
+
+import org.fnives.android.test.dockerfile.configuration.ImageConfiguration
+import org.fnives.android.test.dockerfile.util.FixedClock
+import org.fnives.android.test.dockerfile.util.readResourceFile
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class DockerFileContentGeneratorTest {
+
+ private lateinit var generator: DockerFileContentGenerator
+
+ @BeforeEach
+ fun setup() {
+ generator = DockerFileContentGenerator(
+ config = DockerFileContentConfig(
+ templateGeneric = readResourceFile("Dockerfile.template"),
+ templateSpecific = readResourceFile("Dockerfile.api.template"),
+ dockerRepository = "fknives",
+ dockerTagPrefix = "1.0",
+ dockerNamespace = "ati"
+ ),
+ clock = FixedClock(year = 2021, month = 11, dayOfMonth = 15)
+ )
+ }
+
+ @Test
+ fun generic() {
+ val expected = DockerFileContent(
+ content = readResourceFile("expected_generic_dockerfile"),
+ name = "Dockerfile",
+ tag = "fknives/ati:1.0"
+ )
+ val config = ImageConfiguration.Generic(
+ buildTools = setOf("30.0.0", "32.0.0", "31.0.0", "30.0.3", "30.0.2"),
+ sdks = setOf(33, 32, 31, 30),
+ commandlineTools = "8512546_latest",
+ gradleVersion = "7.3.3"
+ )
+
+ val actual = generator.create(config)
+
+ Assertions.assertEquals(expected, actual)
+ }
+
+ @Test
+ fun api27() {
+ val expected = DockerFileContent(
+ content = readResourceFile("expected_api_27_dockerfile"),
+ name = "Dockerfile-api-27",
+ tag = "fknives/ati:1.0-api-27"
+ )
+ val config = ImageConfiguration.ApiVersion(27)
+
+ val actual = generator.create(config)
+
+ Assertions.assertEquals(expected, actual)
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/integration/IntegrationTest.kt b/main/src/test/java/org/fnives/android/test/dockerfile/integration/IntegrationTest.kt
new file mode 100644
index 0000000..0277a8e
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/integration/IntegrationTest.kt
@@ -0,0 +1,148 @@
+package org.fnives.android.test.dockerfile.integration
+
+import org.fnives.android.test.dockerfile.Main
+import org.fnives.android.test.dockerfile.di.ServiceLocatorHolder
+import org.fnives.android.test.dockerfile.util.CapturingProcessRunner
+import org.fnives.android.test.dockerfile.util.FixedClock
+import org.fnives.android.test.dockerfile.util.OverrideableClockServiceLocator
+import org.fnives.android.test.dockerfile.util.assertCollectionsEquals
+import org.fnives.android.test.dockerfile.util.readResourceFile
+import org.fnives.android.test.dockerfile.util.workDirAbsolutePathWithCommands
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.io.File
+
+class IntegrationTest {
+
+ @TempDir
+ lateinit var tempDir: File
+
+ private lateinit var capturingProcessRunner: CapturingProcessRunner
+
+ @BeforeEach
+ fun setup() {
+ capturingProcessRunner = CapturingProcessRunner()
+ val mockServiceLocator = OverrideableClockServiceLocator()
+ mockServiceLocator.clock = FixedClock(year = 2016, month = 5, dayOfMonth = 28)
+ mockServiceLocator.processRunner = capturingProcessRunner
+
+ ServiceLocatorHolder.swap(mockServiceLocator)
+ }
+
+ @AfterEach
+ fun tearDown() {
+ ServiceLocatorHolder.reset()
+ }
+
+ @Test
+ fun noArgumentsGivesError() {
+ val expectedMessage = "Required argument is missing! Required argument is the config.toml file path and just that!"
+
+ val actual = Assertions.assertThrows(IllegalArgumentException::class.java) {
+ Main.main(emptyArray())
+ }
+
+ Assertions.assertEquals(expectedMessage, actual.message)
+ }
+
+ @Test
+ fun tooManyArgumentsGivesError() {
+ val expectedMessage = "More arguments than expected! Required argument is the config.toml file path and just that!"
+
+ val actual = Assertions.assertThrows(IllegalArgumentException::class.java) {
+ Main.main(arrayOf("", ""))
+ }
+
+ Assertions.assertEquals(expectedMessage, actual.message)
+ }
+
+ @Test
+ fun nonExistentConfigurationFileGivesError() {
+ val nonExistentFile = File(tempDir, "alma")
+ val actual = Assertions.assertThrows(IllegalArgumentException::class.java) {
+ Main.main(arrayOf(nonExistentFile.absolutePath))
+ }
+
+ Assertions.assertEquals("Can't find file at given path: ${nonExistentFile.absolutePath}", actual.message)
+ }
+
+ @Test
+ fun cannotParseConfigFile() {
+ val file = File(tempDir, "alma")
+
+ file.createNewFile()
+ val actual = Assertions.assertThrows(IllegalArgumentException::class.java) {
+ Main.main(arrayOf(file.absolutePath))
+ }
+
+ Assertions.assertEquals("Unparseable file at given path: ${file.absolutePath}", actual.message)
+ Assertions.assertNotNull(actual.cause)
+ }
+
+ @Test
+ fun singleConfiguration() {
+ val file = File(tempDir, "config.toml")
+ val outputDir = File(tempDir, "output")
+ outputDir.mkdir()
+ val updatedConfiguration = readResourceFile(resourceName = "single_configuration.toml")
+ .replace("output = \"outputs\"", "output = \"${outputDir.absolutePath}\"")
+ file.writeText(updatedConfiguration)
+
+ val expectedDockerCommands = listOf(
+ outputDir.absolutePath to "docker build -t fknives/android-test-img:1.0.0 -f ${File(outputDir, "Dockerfile").absolutePath} .",
+ outputDir.absolutePath to "docker push fknives/android-test-img:1.0.0",
+ outputDir.absolutePath to "docker image rm -f fknives/android-test-img:1.0.0"
+ )
+ val expectedDockerFileContent = readResourceFile("expected_single_configuration.Dockerfile")
+ val expectedScriptContent = readResourceFile("startemulator")
+
+ Main.main(arrayOf(file.absolutePath))
+ val actualDockerfileContent = File(outputDir, "Dockerfile").readText()
+ val actualScriptFileContent = File(outputDir, "startemulator").readText()
+
+ assertCollectionsEquals(expectedDockerCommands, capturingProcessRunner.workDirAbsolutePathWithCommands)
+ Assertions.assertEquals(expectedDockerFileContent, actualDockerfileContent)
+ Assertions.assertEquals(expectedScriptContent, actualScriptFileContent)
+ }
+
+ @Test
+ fun multiApiConfiguration() {
+ val file = File(tempDir, "config.toml")
+ val outputDir = File(tempDir, "output")
+ outputDir.mkdir()
+ val updatedConfiguration = readResourceFile(resourceName = "multi_api_configuration.toml")
+ .replace("output = \"outputs\"", "output = \"${outputDir.absolutePath}\"")
+ file.writeText(updatedConfiguration)
+
+ val expectedDockerCommands = listOf(
+ outputDir.absolutePath to "docker build -t fknives2/android-test-img-multi:1.0.1 -f ${File(outputDir, "Dockerfile").absolutePath} .",
+ outputDir.absolutePath to "docker push fknives2/android-test-img-multi:1.0.1",
+ outputDir.absolutePath to "docker image rm -f fknives2/android-test-img-multi:1.0.1",
+ outputDir.absolutePath to "docker build -t fknives2/android-test-img-multi:1.0.1-api-22 -f ${File(outputDir, "Dockerfile-api-22").absolutePath} .",
+ outputDir.absolutePath to "docker push fknives2/android-test-img-multi:1.0.1-api-22",
+ outputDir.absolutePath to "docker image rm -f fknives2/android-test-img-multi:1.0.1-api-22",
+ outputDir.absolutePath to "docker build -t fknives2/android-test-img-multi:1.0.1-api-30 -f ${File(outputDir, "Dockerfile-api-30").absolutePath} .",
+ outputDir.absolutePath to "docker push fknives2/android-test-img-multi:1.0.1-api-30",
+ outputDir.absolutePath to "docker image rm -f fknives2/android-test-img-multi:1.0.1-api-30"
+ )
+ val expectedDockerFileContent = readResourceFile("expected_multi_api_configuration.Dockerfile")
+ val expectedDocker22FileContent = readResourceFile("expected_multi_api_configuration.22.Dockerfile")
+ val expectedDocker30FileContent = readResourceFile("expected_multi_api_configuration.30.Dockerfile")
+ val expectedScriptContent = readResourceFile("startemulator")
+
+ Main.main(arrayOf(file.absolutePath))
+ val actualDockerfileContent = File(outputDir, "Dockerfile").readText()
+ val actualDocker22fileContent = File(outputDir, "Dockerfile-api-22").readText()
+ val actualDocker30fileContent = File(outputDir, "Dockerfile-api-30").readText()
+ val actualScriptFileContent = File(outputDir, "startemulator").readText()
+
+ assertCollectionsEquals(expectedDockerCommands, capturingProcessRunner.workDirAbsolutePathWithCommands)
+ Assertions.assertEquals(expectedDockerFileContent, actualDockerfileContent)
+ Assertions.assertEquals(expectedDocker22FileContent, actualDocker22fileContent)
+ Assertions.assertEquals(expectedDocker30FileContent, actualDocker30fileContent)
+ Assertions.assertEquals(expectedScriptContent, actualScriptFileContent)
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/push/DockerBuildAndPushTest.kt b/main/src/test/java/org/fnives/android/test/dockerfile/push/DockerBuildAndPushTest.kt
new file mode 100644
index 0000000..22262ba
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/push/DockerBuildAndPushTest.kt
@@ -0,0 +1,96 @@
+package org.fnives.android.test.dockerfile.push
+
+import org.fnives.android.test.dockerfile.util.CapturingProcessRunner
+import org.fnives.android.test.dockerfile.util.assertCollectionsEquals
+import org.fnives.android.test.dockerfile.util.workDirAbsolutePathWithCommands
+import org.fnives.android.test.dockerfile.write.DockerFileDescriptor
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.io.File
+
+internal class DockerBuildAndPushTest {
+
+ private lateinit var sut: DockerBuildAndPush
+ private lateinit var capturingProcessRunner: CapturingProcessRunner
+ private lateinit var expectedFolder: String
+
+ @BeforeEach
+ fun setup() {
+ capturingProcessRunner = CapturingProcessRunner(
+ defaultReturnValue = true,
+ fallbackToDefault = true
+ )
+ val folderName = "folder"
+ expectedFolder = File(folderName).absolutePath
+ sut = DockerBuildAndPush(
+ config = PushConfig(
+ dockerRepository = "repo",
+ dockerNamespace = "name",
+ output = folderName
+ ),
+ processRunner = capturingProcessRunner
+ )
+ }
+
+ @Test
+ fun verifyBuildCommand() {
+ val expected = listOf(expectedFolder to "docker build -t 123 -f /banan/alma .")
+
+ sut.build(DockerFileDescriptor("/banan/alma", "123"))
+ val actual = capturingProcessRunner.workDirAbsolutePathWithCommands
+
+ assertCollectionsEquals(expected, actual)
+ }
+
+ @Test
+ fun verifyPushCommand() {
+ val expected = listOf(expectedFolder to "docker push id")
+
+ sut.push("id")
+ val actual = capturingProcessRunner.workDirAbsolutePathWithCommands
+
+ assertCollectionsEquals(expected, actual)
+ }
+
+ @Test
+ fun verifyCleanCommand() {
+ val expected = listOf(expectedFolder to "docker image rm -f id")
+
+ sut.removeLocalImage("id")
+ val actual = capturingProcessRunner.workDirAbsolutePathWithCommands
+
+ assertCollectionsEquals(expected, actual)
+ }
+
+ @Test
+ fun verifyBuildPushClean() {
+ val expected = listOf(
+ expectedFolder to "docker build -t 123 -f banan/alma .",
+ expectedFolder to "docker push 123",
+ expectedFolder to "docker image rm -f 123"
+ )
+
+ sut.buildPushClean(DockerFileDescriptor("banan/alma", "123"))
+ val actual = capturingProcessRunner.workDirAbsolutePathWithCommands
+
+ assertCollectionsEquals(expected, actual)
+ }
+
+ @Test
+ fun verifyMultipleBuildPushClean() {
+ val expected = listOf(
+ expectedFolder to "docker build -t 123 -f banan/alma .",
+ expectedFolder to "docker push 123",
+ expectedFolder to "docker image rm -f 123",
+ expectedFolder to "docker build -t fknives/421:512.3 -f banan/alma2 .",
+ expectedFolder to "docker push fknives/421:512.3",
+ expectedFolder to "docker image rm -f fknives/421:512.3"
+ )
+
+ sut.buildPushClean(DockerFileDescriptor("banan/alma", "123"))
+ sut.buildPushClean(DockerFileDescriptor("banan/alma2", "fknives/421:512.3"))
+ val actual = capturingProcessRunner.workDirAbsolutePathWithCommands
+
+ assertCollectionsEquals(expected, actual)
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/util/AssertUtil.kt b/main/src/test/java/org/fnives/android/test/dockerfile/util/AssertUtil.kt
new file mode 100644
index 0000000..7bd293a
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/util/AssertUtil.kt
@@ -0,0 +1,7 @@
+package org.fnives.android.test.dockerfile.util
+
+import org.junit.jupiter.api.Assertions
+
+inline fun assertCollectionsEquals(expected: Collection, actual: Collection) {
+ Assertions.assertArrayEquals(expected.toTypedArray(), actual.toTypedArray())
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/util/CapturingProcessRunner.kt b/main/src/test/java/org/fnives/android/test/dockerfile/util/CapturingProcessRunner.kt
new file mode 100644
index 0000000..df7b549
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/util/CapturingProcessRunner.kt
@@ -0,0 +1,17 @@
+package org.fnives.android.test.dockerfile.util
+
+import org.fnives.android.test.dockerfile.push.ProcessRunner
+import java.io.File
+
+class CapturingProcessRunner : ProcessRunner {
+
+ private val _receivedCommands = mutableListOf>()
+ val receivedCommands get() = _receivedCommands.toList()
+
+ override fun run(workingDirectory: File, command: String) {
+ _receivedCommands.add(workingDirectory to command)
+ }
+}
+
+val CapturingProcessRunner.workDirAbsolutePathWithCommands: List>
+ get() = receivedCommands.map { (workdir, command) -> workdir.absolutePath to command }
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/util/FixedClock.kt b/main/src/test/java/org/fnives/android/test/dockerfile/util/FixedClock.kt
new file mode 100644
index 0000000..d6e94c7
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/util/FixedClock.kt
@@ -0,0 +1,18 @@
+package org.fnives.android.test.dockerfile.util
+
+import org.fnives.android.test.dockerfile.util.clock.Clock
+import java.util.Calendar
+
+class FixedClock(val timeMillis: Long) : Clock {
+
+ override fun currentTimeMillis(): Long = timeMillis
+}
+
+fun FixedClock(year: Int, month: Int, dayOfMonth: Int): Clock {
+ val calendar = Calendar.getInstance()
+ calendar.set(Calendar.YEAR, year)
+ calendar.set(Calendar.MONTH, month - 1 + Calendar.JANUARY)
+ calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
+
+ return FixedClock(calendar.timeInMillis)
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/util/OverrideableClockServiceLocator.kt b/main/src/test/java/org/fnives/android/test/dockerfile/util/OverrideableClockServiceLocator.kt
new file mode 100644
index 0000000..1cfa230
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/util/OverrideableClockServiceLocator.kt
@@ -0,0 +1,32 @@
+package org.fnives.android.test.dockerfile.util
+
+import org.fnives.android.test.dockerfile.configuration.Configuration
+import org.fnives.android.test.dockerfile.configuration.ConfigurationParser
+import org.fnives.android.test.dockerfile.di.ConfiguredServiceLocator
+import org.fnives.android.test.dockerfile.di.ServiceLocator
+import org.fnives.android.test.dockerfile.di.ServiceLocatorHolder
+import org.fnives.android.test.dockerfile.push.ProcessRunner
+import org.fnives.android.test.dockerfile.util.clock.Clock
+
+class OverrideableClockServiceLocator(private val delegate: ServiceLocator = ServiceLocatorHolder.default) : ServiceLocator by delegate {
+
+ var clock: Clock? = null
+ var configuredServiceLocator: ConfiguredServiceLocator? = null
+ var configurationParser: ConfigurationParser? = null
+ var processRunner: ProcessRunner? = null
+ var scriptToCopy: String? = null
+
+ override fun clock(): Clock = clock ?: delegate.clock()
+
+ override fun loadConfiguration(configuration: Configuration): ConfiguredServiceLocator =
+ configuredServiceLocator ?: delegate.loadConfiguration(configuration)
+
+ override fun parser(): ConfigurationParser =
+ configurationParser ?: delegate.parser()
+
+ override fun processRunner(): ProcessRunner =
+ processRunner ?: delegate.processRunner()
+
+ override fun scriptToCopy(): String =
+ scriptToCopy ?: delegate.scriptToCopy()
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/util/ResourceUtil.kt b/main/src/test/java/org/fnives/android/test/dockerfile/util/ResourceUtil.kt
new file mode 100644
index 0000000..0360f82
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/util/ResourceUtil.kt
@@ -0,0 +1,19 @@
+package org.fnives.android.test.dockerfile.util
+
+import java.io.BufferedReader
+import java.io.File
+import java.io.InputStream
+import java.io.InputStreamReader
+
+internal fun Any.readResourceFile(resourceName: String): String = try {
+ BufferedReader(InputStreamReader(resourceFileAsInputStream(resourceName)))
+ .readLines().joinToString("\n")
+} catch (nullPointerException: NullPointerException) {
+ throw IllegalArgumentException("$resourceName file not found!", nullPointerException)
+}
+
+internal fun Any.resourceFileAsInputStream(resourceName: String): InputStream = try {
+ this.javaClass.classLoader.getResourceAsStream(resourceName)!!
+} catch (nullPointerException: NullPointerException) {
+ throw IllegalArgumentException("$resourceName file not found!", nullPointerException)
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/write/DockerFileWriterTest.kt b/main/src/test/java/org/fnives/android/test/dockerfile/write/DockerFileWriterTest.kt
new file mode 100644
index 0000000..7cf6289
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/write/DockerFileWriterTest.kt
@@ -0,0 +1,47 @@
+package org.fnives.android.test.dockerfile.write
+
+import org.fnives.android.test.dockerfile.generator.DockerFileContent
+import org.fnives.android.test.dockerfile.util.assertCollectionsEquals
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.io.File
+
+internal class DockerFileWriterTest {
+
+ @TempDir
+ lateinit var tempDirectory: File
+
+ @Test
+ fun createsFolder() {
+ val folderToCreate = File(tempDirectory, "childFolder")
+ val sut = DockerFileWriter(outputFolder = folderToCreate)
+
+ sut.write(DockerFileContent(name = "alma", content = "-", tag = "123"))
+
+ Assertions.assertTrue(folderToCreate.exists()) { "Temp folder does not exist!" }
+ Assertions.assertFalse(folderToCreate.isFile) { "Temp folder is a file not a Folder!" }
+ }
+
+ @Test
+ fun writesFileProperly() {
+ val sut = DockerFileWriter(outputFolder = tempDirectory)
+ val expected = listOf("123", "abc")
+
+ sut.write(DockerFileContent(name = "alma", content = "123\nabc", tag = "123"))
+ val actual = File(tempDirectory, "alma")
+
+ assertCollectionsEquals(expected, actual.readLines())
+ }
+
+ @Test
+ fun assertReturnsCorrectDescriptor() {
+ val sut = DockerFileWriter(outputFolder = tempDirectory)
+ val expectedFile = File(tempDirectory, "alma")
+ val expected = DockerFileDescriptor(filePath = expectedFile.absolutePath, tag = "123")
+
+ val actual = sut.write(DockerFileContent(name = "alma", content = "123\nabc", tag = "123"))
+
+ Assertions.assertEquals(expected, actual)
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/java/org/fnives/android/test/dockerfile/write/ScriptCopierTest.kt b/main/src/test/java/org/fnives/android/test/dockerfile/write/ScriptCopierTest.kt
new file mode 100644
index 0000000..8187f5b
--- /dev/null
+++ b/main/src/test/java/org/fnives/android/test/dockerfile/write/ScriptCopierTest.kt
@@ -0,0 +1,53 @@
+package org.fnives.android.test.dockerfile.write
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.io.File
+
+internal class ScriptCopierTest {
+
+ @TempDir
+ lateinit var tempDirectory: File
+
+ @Test
+ fun copyExample() {
+ val sut = ScriptCopier(
+ outputFolder = tempDirectory,
+ scriptName = "example_to_copy"
+ )
+
+ val actual = File(tempDirectory, "example_to_copy")
+ sut.invoke()
+
+ val expected = arrayOf("123", "ab", "456")
+ Assertions.assertArrayEquals(expected, actual.readLines().toTypedArray())
+ }
+
+ @Test
+ fun checkActualCopyIsNotEmpty() {
+ val sut = ScriptCopier(
+ outputFolder = tempDirectory,
+ scriptName = "startemulator"
+ )
+
+ val actual = File(tempDirectory, "startemulator")
+ sut.invoke()
+
+ Assertions.assertTrue(actual.readLines().isNotEmpty()) { "Content is empty when trying to copy startemulator" }
+ }
+
+ @Test
+ fun createsFolder() {
+ val folderToCreate = File(tempDirectory, "childFolder")
+ val sut = ScriptCopier(
+ outputFolder = folderToCreate,
+ scriptName = "example_to_copy"
+ )
+
+ sut.invoke()
+
+ Assertions.assertTrue(folderToCreate.exists()) { "Temp folder does not exist!" }
+ Assertions.assertFalse(folderToCreate.isFile) { "Temp folder is a file not a Folder!" }
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/resources/example_configuration.toml b/main/src/test/resources/example_configuration.toml
new file mode 100644
index 0000000..68420e8
--- /dev/null
+++ b/main/src/test/resources/example_configuration.toml
@@ -0,0 +1,11 @@
+buildTools = ["33.0.0", "32.0.0", "31.0.0", "30.0.3", "30.0.2"]
+sdks = [30, 31, 32, 33]
+androidCommandlineTools = "8512546_latest"
+gradleVersion = "7.3.3"
+output = "outputs"
+[variations]
+apiLevels = [21, 22, 23]
+[image]
+repository = "fknives"
+namespace = "android-test-img"
+tagPrefix = "1.0.0"
diff --git a/main/src/test/resources/example_to_copy b/main/src/test/resources/example_to_copy
new file mode 100644
index 0000000..815fc93
--- /dev/null
+++ b/main/src/test/resources/example_to_copy
@@ -0,0 +1,3 @@
+123
+ab
+456
\ No newline at end of file
diff --git a/main/src/test/resources/expected_api_27_dockerfile b/main/src/test/resources/expected_api_27_dockerfile
new file mode 100644
index 0000000..e22d77d
--- /dev/null
+++ b/main/src/test/resources/expected_api_27_dockerfile
@@ -0,0 +1,8 @@
+FROM fknives/ati:1.0
+# Generated on 2021-11-15
+
+ENV EMULATOR_API_LEVEL 27
+
+RUN /usr/local/bin/androidemulatorstart --buildOnly
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/main/src/test/resources/expected_generic_dockerfile b/main/src/test/resources/expected_generic_dockerfile
new file mode 100644
index 0000000..1187507
--- /dev/null
+++ b/main/src/test/resources/expected_generic_dockerfile
@@ -0,0 +1,78 @@
+FROM openjdk:11-jdk-slim
+# Generated on 2021-11-15
+
+# installing usual required tools
+# build-essential, ruby and bundler is needed for fastlane
+RUN apt-get update && apt-get install -y \
+ sudo \
+ wget \
+ curl \
+ unzip \
+ android-sdk \
+ build-essential \
+ ruby-full && \
+ gem install bundler
+
+# versions
+# latest command lines version here: https://developer.android.com/studio#command-tools
+ENV ANDROID_COMMAND_LINES_TOOLS_VERSION "8512546_latest"
+# gradle version can be found in projects gradle-wrapper.properties
+ENV GRADLE_VERSION "7.3.3"
+
+# set homes and paths
+ENV ANDROID_HOME "/usr/lib/android-sdk"
+ENV ANDROID_SDK_ROOT $ANDROID_HOME
+ENV CMDLINE_TOOLS_ROOT "${ANDROID_HOME}/cmdline-tools/latest/bin"
+ENV AVD_HOME "/root/.android/avd"
+ENV PATH "$ANDROID_HOME/cmdline-tools/latest/bin:${PATH}"
+ENV PATH "$ANDROID_HOME/emulator:${PATH}"
+ENV PATH "/usr/local/gradle-${GRADLE_VERSION}/bin:${PATH}"
+
+WORKDIR /root/
+
+# install gradle
+RUN curl -sSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip && \
+ unzip -d /usr/local /tmp/gradle.zip && \
+ rm -rf /tmp/gradle.zip && \
+ echo "1" | gradle init && \
+ /root/gradlew && \
+ rm -rf /root/* && \
+ rm /root/.gitignore
+
+# install command line tools
+RUN wget https://dl.google.com/android/repository/commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ unzip commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip -d cmdline-tools && \
+ rm commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ mv cmdline-tools/cmdline-tools/ cmdline-tools/latest && \
+ mv cmdline-tools $ANDROID_HOME/
+
+# install emulator and setup sdkmanager
+RUN yes | sdkmanager --licenses > /dev/null && \
+ sdkmanager --install emulator --channel=0 > /dev/null
+
+# support libraries and google play services
+RUN echo y | sdkmanager "extras;android;m2repository" && \
+ echo y | sdkmanager "extras;google;m2repository" && \
+ echo y | sdkmanager "extras;google;google_play_services"
+
+# build tools versions and sdk versions can be found via sdkmanager --list and filtering it
+# install build-tools
+# example: `echo y | sdkmanager "build-tools;33.0.0"`
+RUN echo y | sdkmanager "build-tools;30.0.0" && \
+ echo y | sdkmanager "build-tools;32.0.0" && \
+ echo y | sdkmanager "build-tools;31.0.0" && \
+ echo y | sdkmanager "build-tools;30.0.3" && \
+ echo y | sdkmanager "build-tools;30.0.2"
+
+# install sdks
+# example: `echo y | sdkmanager "platforms;android-33"`
+RUN echo y | sdkmanager "platforms;android-33" && \
+ echo y | sdkmanager "platforms;android-32" && \
+ echo y | sdkmanager "platforms;android-31" && \
+ echo y | sdkmanager "platforms;android-30"
+
+# copy script to install sdk, create emulator and boot it
+COPY ./startemulator /usr/local/bin/androidemulatorstart
+RUN chmod 744 /usr/local/bin/androidemulatorstart
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/main/src/test/resources/expected_multi_api_configuration.22.Dockerfile b/main/src/test/resources/expected_multi_api_configuration.22.Dockerfile
new file mode 100644
index 0000000..8caadc9
--- /dev/null
+++ b/main/src/test/resources/expected_multi_api_configuration.22.Dockerfile
@@ -0,0 +1,8 @@
+FROM fknives2/android-test-img-multi:1.0.1
+# Generated on 2016-05-28
+
+ENV EMULATOR_API_LEVEL 22
+
+RUN /usr/local/bin/androidemulatorstart --buildOnly
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/main/src/test/resources/expected_multi_api_configuration.30.Dockerfile b/main/src/test/resources/expected_multi_api_configuration.30.Dockerfile
new file mode 100644
index 0000000..0d4f6d1
--- /dev/null
+++ b/main/src/test/resources/expected_multi_api_configuration.30.Dockerfile
@@ -0,0 +1,8 @@
+FROM fknives2/android-test-img-multi:1.0.1
+# Generated on 2016-05-28
+
+ENV EMULATOR_API_LEVEL 30
+
+RUN /usr/local/bin/androidemulatorstart --buildOnly
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/main/src/test/resources/expected_multi_api_configuration.Dockerfile b/main/src/test/resources/expected_multi_api_configuration.Dockerfile
new file mode 100644
index 0000000..04600d3
--- /dev/null
+++ b/main/src/test/resources/expected_multi_api_configuration.Dockerfile
@@ -0,0 +1,73 @@
+FROM openjdk:11-jdk-slim
+# Generated on 2016-05-28
+
+# installing usual required tools
+# build-essential, ruby and bundler is needed for fastlane
+RUN apt-get update && apt-get install -y \
+ sudo \
+ wget \
+ curl \
+ unzip \
+ android-sdk \
+ build-essential \
+ ruby-full && \
+ gem install bundler
+
+# versions
+# latest command lines version here: https://developer.android.com/studio#command-tools
+ENV ANDROID_COMMAND_LINES_TOOLS_VERSION "8512546_latest"
+# gradle version can be found in projects gradle-wrapper.properties
+ENV GRADLE_VERSION "7.3.3"
+
+# set homes and paths
+ENV ANDROID_HOME "/usr/lib/android-sdk"
+ENV ANDROID_SDK_ROOT $ANDROID_HOME
+ENV CMDLINE_TOOLS_ROOT "${ANDROID_HOME}/cmdline-tools/latest/bin"
+ENV AVD_HOME "/root/.android/avd"
+ENV PATH "$ANDROID_HOME/cmdline-tools/latest/bin:${PATH}"
+ENV PATH "$ANDROID_HOME/emulator:${PATH}"
+ENV PATH "/usr/local/gradle-${GRADLE_VERSION}/bin:${PATH}"
+
+WORKDIR /root/
+
+# install gradle
+RUN curl -sSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip && \
+ unzip -d /usr/local /tmp/gradle.zip && \
+ rm -rf /tmp/gradle.zip && \
+ echo "1" | gradle init && \
+ /root/gradlew && \
+ rm -rf /root/* && \
+ rm /root/.gitignore
+
+# install command line tools
+RUN wget https://dl.google.com/android/repository/commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ unzip commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip -d cmdline-tools && \
+ rm commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ mv cmdline-tools/cmdline-tools/ cmdline-tools/latest && \
+ mv cmdline-tools $ANDROID_HOME/
+
+# install emulator and setup sdkmanager
+RUN yes | sdkmanager --licenses > /dev/null && \
+ sdkmanager --install emulator --channel=0 > /dev/null
+
+# support libraries and google play services
+RUN echo y | sdkmanager "extras;android;m2repository" && \
+ echo y | sdkmanager "extras;google;m2repository" && \
+ echo y | sdkmanager "extras;google;google_play_services"
+
+# build tools versions and sdk versions can be found via sdkmanager --list and filtering it
+# install build-tools
+# example: `echo y | sdkmanager "build-tools;33.0.0"`
+RUN echo y | sdkmanager "build-tools;33.0.0" && \
+ echo y | sdkmanager "build-tools;32.0.0"
+
+# install sdks
+# example: `echo y | sdkmanager "platforms;android-33"`
+RUN echo y | sdkmanager "platforms;android-30" && \
+ echo y | sdkmanager "platforms;android-31"
+
+# copy script to install sdk, create emulator and boot it
+COPY ./startemulator /usr/local/bin/androidemulatorstart
+RUN chmod 744 /usr/local/bin/androidemulatorstart
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/main/src/test/resources/expected_single_configuration.Dockerfile b/main/src/test/resources/expected_single_configuration.Dockerfile
new file mode 100644
index 0000000..9c1f950
--- /dev/null
+++ b/main/src/test/resources/expected_single_configuration.Dockerfile
@@ -0,0 +1,74 @@
+FROM openjdk:11-jdk-slim
+# Generated on 2016-05-28
+
+# installing usual required tools
+# build-essential, ruby and bundler is needed for fastlane
+RUN apt-get update && apt-get install -y \
+ sudo \
+ wget \
+ curl \
+ unzip \
+ android-sdk \
+ build-essential \
+ ruby-full && \
+ gem install bundler
+
+# versions
+# latest command lines version here: https://developer.android.com/studio#command-tools
+ENV ANDROID_COMMAND_LINES_TOOLS_VERSION "8512546_latest"
+# gradle version can be found in projects gradle-wrapper.properties
+ENV GRADLE_VERSION "7.3.3"
+
+# set homes and paths
+ENV ANDROID_HOME "/usr/lib/android-sdk"
+ENV ANDROID_SDK_ROOT $ANDROID_HOME
+ENV CMDLINE_TOOLS_ROOT "${ANDROID_HOME}/cmdline-tools/latest/bin"
+ENV AVD_HOME "/root/.android/avd"
+ENV PATH "$ANDROID_HOME/cmdline-tools/latest/bin:${PATH}"
+ENV PATH "$ANDROID_HOME/emulator:${PATH}"
+ENV PATH "/usr/local/gradle-${GRADLE_VERSION}/bin:${PATH}"
+
+WORKDIR /root/
+
+# install gradle
+RUN curl -sSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip && \
+ unzip -d /usr/local /tmp/gradle.zip && \
+ rm -rf /tmp/gradle.zip && \
+ echo "1" | gradle init && \
+ /root/gradlew && \
+ rm -rf /root/* && \
+ rm /root/.gitignore
+
+# install command line tools
+RUN wget https://dl.google.com/android/repository/commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ unzip commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip -d cmdline-tools && \
+ rm commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ mv cmdline-tools/cmdline-tools/ cmdline-tools/latest && \
+ mv cmdline-tools $ANDROID_HOME/
+
+# install emulator and setup sdkmanager
+RUN yes | sdkmanager --licenses > /dev/null && \
+ sdkmanager --install emulator --channel=0 > /dev/null
+
+# support libraries and google play services
+RUN echo y | sdkmanager "extras;android;m2repository" && \
+ echo y | sdkmanager "extras;google;m2repository" && \
+ echo y | sdkmanager "extras;google;google_play_services"
+
+# build tools versions and sdk versions can be found via sdkmanager --list and filtering it
+# install build-tools
+# example: `echo y | sdkmanager "build-tools;33.0.0"`
+RUN echo y | sdkmanager "build-tools;33.0.0" && \
+ echo y | sdkmanager "build-tools;32.0.0" && \
+ echo y | sdkmanager "build-tools;31.0.0"
+
+# install sdks
+# example: `echo y | sdkmanager "platforms;android-33"`
+RUN echo y | sdkmanager "platforms;android-32" && \
+ echo y | sdkmanager "platforms;android-33"
+
+# copy script to install sdk, create emulator and boot it
+COPY ./startemulator /usr/local/bin/androidemulatorstart
+RUN chmod 744 /usr/local/bin/androidemulatorstart
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/main/src/test/resources/multi_api_configuration.toml b/main/src/test/resources/multi_api_configuration.toml
new file mode 100644
index 0000000..496c7f4
--- /dev/null
+++ b/main/src/test/resources/multi_api_configuration.toml
@@ -0,0 +1,11 @@
+buildTools = ["33.0.0", "32.0.0"]
+sdks = [30, 31]
+androidCommandlineTools = "8512546_latest"
+gradleVersion = "7.3.3"
+output = "outputs"
+[variations]
+apiLevels = [22, 30]
+[image]
+repository = "fknives2"
+namespace = "android-test-img-multi"
+tagPrefix = "1.0.1"
diff --git a/main/src/test/resources/single_configuration.toml b/main/src/test/resources/single_configuration.toml
new file mode 100644
index 0000000..09bdae4
--- /dev/null
+++ b/main/src/test/resources/single_configuration.toml
@@ -0,0 +1,9 @@
+buildTools = ["33.0.0", "32.0.0", "31.0.0"]
+sdks = [32, 33]
+androidCommandlineTools = "8512546_latest"
+gradleVersion = "7.3.3"
+output = "outputs"
+[image]
+repository = "fknives"
+namespace = "android-test-img"
+tagPrefix = "1.0.0"
diff --git a/outputs-1.0.0/Dockerfile b/outputs-1.0.0/Dockerfile
new file mode 100644
index 0000000..705e5a9
--- /dev/null
+++ b/outputs-1.0.0/Dockerfile
@@ -0,0 +1,78 @@
+FROM openjdk:11-jdk-slim
+# Generated on 2022-08-26
+
+# installing usual required tools
+# build-essential, ruby and bundler is needed for fastlane
+RUN apt-get update && apt-get install -y \
+ sudo \
+ wget \
+ curl \
+ unzip \
+ android-sdk \
+ build-essential \
+ ruby-full && \
+ gem install bundler
+
+# versions
+# latest command lines version here: https://developer.android.com/studio#command-tools
+ENV ANDROID_COMMAND_LINES_TOOLS_VERSION "8512546_latest"
+# gradle version can be found in projects gradle-wrapper.properties
+ENV GRADLE_VERSION "7.3.3"
+
+# set homes and paths
+ENV ANDROID_HOME "/usr/lib/android-sdk"
+ENV ANDROID_SDK_ROOT $ANDROID_HOME
+ENV CMDLINE_TOOLS_ROOT "${ANDROID_HOME}/cmdline-tools/latest/bin"
+ENV AVD_HOME "/root/.android/avd"
+ENV PATH "$ANDROID_HOME/cmdline-tools/latest/bin:${PATH}"
+ENV PATH "$ANDROID_HOME/emulator:${PATH}"
+ENV PATH "/usr/local/gradle-${GRADLE_VERSION}/bin:${PATH}"
+
+WORKDIR /root/
+
+# install gradle
+RUN curl -sSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip && \
+ unzip -d /usr/local /tmp/gradle.zip && \
+ rm -rf /tmp/gradle.zip && \
+ echo "1" | gradle init && \
+ /root/gradlew && \
+ rm -rf /root/* && \
+ rm /root/.gitignore
+
+# install command line tools
+RUN wget https://dl.google.com/android/repository/commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ unzip commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip -d cmdline-tools && \
+ rm commandlinetools-linux-$ANDROID_COMMAND_LINES_TOOLS_VERSION.zip && \
+ mv cmdline-tools/cmdline-tools/ cmdline-tools/latest && \
+ mv cmdline-tools $ANDROID_HOME/
+
+# install emulator and setup sdkmanager
+RUN yes | sdkmanager --licenses > /dev/null && \
+ sdkmanager --install emulator --channel=0 > /dev/null
+
+# support libraries and google play services
+RUN echo y | sdkmanager "extras;android;m2repository" && \
+ echo y | sdkmanager "extras;google;m2repository" && \
+ echo y | sdkmanager "extras;google;google_play_services"
+
+# build tools versions and sdk versions can be found via sdkmanager --list and filtering it
+# install build-tools
+# example: `echo y | sdkmanager "build-tools;33.0.0"`
+RUN echo y | sdkmanager "build-tools;33.0.0" && \
+ echo y | sdkmanager "build-tools;32.0.0" && \
+ echo y | sdkmanager "build-tools;31.0.0" && \
+ echo y | sdkmanager "build-tools;30.0.3" && \
+ echo y | sdkmanager "build-tools;30.0.2"
+
+# install sdks
+# example: `echo y | sdkmanager "platforms;android-33"`
+RUN echo y | sdkmanager "platforms;android-30" && \
+ echo y | sdkmanager "platforms;android-31" && \
+ echo y | sdkmanager "platforms;android-32" && \
+ echo y | sdkmanager "platforms;android-33"
+
+# copy script to install sdk, create emulator and boot it
+COPY ./startemulator /usr/local/bin/androidemulatorstart
+RUN chmod 744 /usr/local/bin/androidemulatorstart
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/outputs-1.0.0/startemulator b/outputs-1.0.0/startemulator
new file mode 100644
index 0000000..a846bf6
--- /dev/null
+++ b/outputs-1.0.0/startemulator
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+if [[ "$1" == "--help" ]]
+then
+ echo "Creates default emulator and starts it."
+ echo "API level and Target can be customized, for more customization use the avdmanager directly!"
+ echo "API_level is read from EMULATOR_API_LEVEL variable"
+ echo "target is read from EMULATOR_TARGET variable or defaults to target"
+ echo "--buildOnly parameter means the emulator will be created, but not started"
+ exit 0;
+fi
+
+if [[ -z $EMULATOR_API_LEVEL ]]
+then
+ echo "EMULATOR_API_LEVEL not set!" >&2
+ exit 1;
+fi
+
+if [[ -z $EMULATOR_TARGET ]]
+then
+ if [[ $EMULATOR_API_LEVEL -ge 32 ]]
+ then
+ echo "EMULATOR_TARGET not set using \"google_apis\", \"default\" doesn't exists above 32"
+ EMULATOR_TARGET="google_apis"
+ else
+ echo "EMULATOR_TARGET not set using \"default\"" >&2
+ EMULATOR_TARGET="default"
+ fi
+fi
+
+if [[ -z $AVD_HOME ]]
+then
+ AVD_HOME="$HOME/.android/avd"
+ echo "AVD_HOME not set, using fallback: $AVD_HOME"
+fi
+
+if [[ "$1" == "--buildOnly" ]]
+then
+ echo "INFO: --buildOnly found, emulator will be created but not started!" >&2
+fi
+
+echo "INFO: installing sdk" >&2
+sdkmanager --install "system-images;android-$EMULATOR_API_LEVEL;$EMULATOR_TARGET;x86_64" --channel=0 > /dev/null 2> /dev/null
+
+
+echo "INFO: creating emulator" >&2
+EMULATOR_NAME="test_$EMULATOR_API_LEVEL"
+echo "no" | avdmanager create avd -n "$EMULATOR_NAME" --abi "$EMULATOR_TARGET/x86_64" --package "system-images;android-$EMULATOR_API_LEVEL;$EMULATOR_TARGET;x86_64"
+
+echo "INFO: config of emulator:" >&2
+cat $AVD_HOME/$EMULATOR_NAME.avd/config.ini >&2
+echo "INFO: modifying config of emulator..." >&2
+echo 'disk.dataPartition.size=2G\n' >> $AVD_HOME/$EMULATOR_NAME.avd/config.ini
+
+if [[ "$1" == "--buildOnly" ]]
+then
+ echo "INFO: emulator created!" >&2
+ exit 0;
+fi
+
+echo "INFO: checking devices, expecting nothing..." >&2
+adb devices
+
+echo "INFO: starting emulator..." >&2
+emulator -avd $EMULATOR_NAME -no-snapshot-save -no-window -gpu off -noaudio -no-boot-anim -camera-back none > emulator.log &
+
+echo "INFO: waiting for emulator..." >&2
+sleep 30s
+
+echo "INFO: emulator logs" >&2
+cat emulator.log
+
+echo "INFO: checking devices..." >&2
+adb devices
+echo "INFO: waiting for emulator to be booted..." >&2
+BOOT_WAIT_LOG_MESSAGE="INFO: still waiting for emulator to boot"
+SECONDS_PASSED=0;
+BOOT_COMPLETE=`adb shell getprop sys.boot_completed`
+while [[ -z $BOOT_COMPLETE ]];
+do
+ sleep $BOOT_DELAY_IN_SECONDS;
+ SECONDS_PASSED=$(( $SECONDS_PASSED+$BOOT_DELAY_IN_SECONDS ));
+ echo "INFO: still waiting for emulator to boot ($SECONDS_PASSED seconds passed)"; >&2
+ BOOT_COMPLETE=`adb shell getprop sys.boot_completed`
+done;
+echo "INFO: emulator booted." >&2
+# alternative: https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator
+
+echo "INFO: clearing animations..." >&2
+adb shell settings put global window_animation_scale 0.0
+adb shell settings put global transition_animation_scale 0.0
+adb shell settings put global animator_duration_scale 0.0
+
+echo "INFO: waiting for emulator have sdk property..." >&2
+SDK_VERSION=`adb shell getprop ro.build.version.sdk`
+while [[ -z $SDK_VERSION ]];
+do
+ sleep $BOOT_DELAY_IN_SECONDS;
+ echo "INFO: still waiting for emulator to have SDK property"; >&2
+ SDK_VERSION=`adb shell getprop ro.build.version.sdk`
+done;
+
+echo "INFO: unlocking screen..." >&2
+adb shell input keyevent 82
+echo "INFO: emulator ready!" >&2
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..8b2a085
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,19 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+
+ repositories {
+ google()
+ mavenCentral()
+ maven { url = uri("https://jitpack.io") }
+ }
+}
+rootProject.name = "AndroidTestDockerfile"
+
+include(":main")
\ No newline at end of file