├── .gitignore ├── .mainframer ├── config ├── ignore ├── localignore └── remoteignore ├── Dockerfile ├── LICENSE ├── README.md ├── mainframer-android-docker.sh ├── mainframer-init.sh └── mainframer.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/macos 2 | 3 | ### macOS ### 4 | *.DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | # End of https://www.gitignore.io/api/macos 32 | -------------------------------------------------------------------------------- /.mainframer/config: -------------------------------------------------------------------------------- 1 | remote_machine=user@host 2 | 3 | android_target_sdk=25 4 | android_build_tools=25.0.2 5 | android_sdk_tools=25.2.5 6 | -------------------------------------------------------------------------------- /.mainframer/ignore: -------------------------------------------------------------------------------- 1 | # .gradle folder contains machine specific loc and bin files, no need to sync it between machines. 2 | .gradle 3 | 4 | # Synching .git or other VCS folders is very bad idea, they are usually very heavy and not required for the build. 5 | .git 6 | 7 | # Synching IDE specific folders is really not needed for remote build. 8 | /.idea 9 | 10 | # Synching local.properties usually very bad idea because they contain machine specific properties like paths to SDKs and so on. 11 | /local.properties 12 | 13 | # Syncing captures from Android Studio is usually not required. 14 | /captures 15 | -------------------------------------------------------------------------------- /.mainframer/localignore: -------------------------------------------------------------------------------- 1 | # Usually you don't need to sync build folder to remote machine since it will have one as result of build. 2 | build 3 | -------------------------------------------------------------------------------- /.mainframer/remoteignore: -------------------------------------------------------------------------------- 1 | # Usually you don't need to sync sources back from remote to local machine. 2 | # Btw, this syntax supports multimodule Gradle projects. 3 | src 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk 2 | 3 | ENV ANDROID_HOME=${PWD}/android-sdk-linux 4 | ENV PATH=${PATH}:${ANDROID_HOME}/platform-tools 5 | 6 | RUN apt-get --quiet update --yes && \ 7 | apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 build-essential file usbutils openssh-client && \ 8 | apt-get autoremove --yes && \ 9 | apt-get clean && \ 10 | rm -rf /var/lib/apt/lists/* 11 | 12 | ARG ANDROID_TARGET_SDK 13 | ARG ANDROID_BUILD_TOOLS 14 | ARG ANDROID_SDK_TOOLS 15 | 16 | RUN wget -nv --output-document=android-sdk.zip https://dl.google.com/android/repository/tools_r${ANDROID_SDK_TOOLS}-linux.zip && \ 17 | unzip -qo android-sdk.zip -d android-sdk-linux && \ 18 | rm android-sdk.zip && \ 19 | mkdir -p ~/.gradle && \ 20 | echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties && \ 21 | echo y | ${ANDROID_HOME}/tools/android --silent update sdk --no-ui --all --filter android-${ANDROID_TARGET_SDK} && \ 22 | echo y | ${ANDROID_HOME}/tools/android --silent update sdk --no-ui --all --filter platform-tools && \ 23 | echo y | ${ANDROID_HOME}/tools/android --silent update sdk --no-ui --all --filter build-tools-${ANDROID_BUILD_TOOLS} && \ 24 | mkdir -p ${ANDROID_HOME}/licenses/ && \ 25 | echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > ${ANDROID_HOME}/licenses/android-sdk-license && \ 26 | echo "84831b9409646a918e30573bab4c9c91346d8abd" > ${ANDROID_HOME}/licenses/android-sdk-preview-license 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andreas Backx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mainframer Docker proof of concept 2 | 3 | mainframer-android-docker is a proof of concept that shows how [mainframer](https://github.com/gojuno/mainframer) could be used with Docker. It makes it easy to run everything remotely with almost no setup required besides SSH and Docker. 4 | 5 | This example is specifically made for Android. It can be easily changed in order to work for other build systems. Simply change the `Dockerfile` to what is required for a build and change the command passed to mainframer used in `mainframer-android-docker.sh`'s `run` function. 6 | 7 | ## Configuration 8 | 9 | See [mainframer](https://github.com/gojuno/mainframer) for an explanation on how its configuration works. You need to add following settings to `.mainframer/config`: 10 | 11 | ``` 12 | android_target_sdk=25 13 | android_build_tools=25.0.2 14 | android_sdk_tools=25.2.5 15 | ``` 16 | 17 | > Note: Because of how the Dockerfile gets the Android SDK Tools version, version 25.2.5 seems to be the latest available version when using `https://dl.google.com/android/repository/tools_r${ANDROID_SDK_TOOLS}-linux.zip`. This needs to be resolved. 18 | 19 | ## Usage 20 | 21 | ### Build docker image 22 | 23 | The Docker image needs to be built before you can get started. It also needs to be rebuilt every time the version of the target sdk, build tools, or sdk tools changes. 24 | 25 | ``` 26 | ./mainframer-android-docker.sh build 27 | ``` 28 | 29 | ### Run gradle 30 | 31 | The `./mainframer-android-docker.sh run` command simply executes the Gradle wrapper in the container and passes it the arguments that were passed to the script. 32 | 33 | ``` 34 | ./mainframer-android-docker.sh run TASK_NAME 35 | ``` 36 | 37 | Example: 38 | 39 | ``` 40 | ./mainframer-android-docker.sh run :app:assembleDebug 41 | ``` 42 | 43 | ## Thank you 44 | 45 | A quick thank you to [Ian Thomas](https://github.com/toxicbakery) and [Eddie Ringle](https://github.com/eddieringle) for providing me with the Dockerfile. 46 | -------------------------------------------------------------------------------- /mainframer-android-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | COMMAND=$1 6 | 7 | IMAGE_NAME="mainframer-android-docker" 8 | 9 | function executeRemotely { 10 | if [ "$#" -ne 1 ]; then 11 | echo "A command must be passed to executeRemotely." 12 | exit 0 13 | fi 14 | 15 | BUILD_COMMAND=$1 16 | BUILD_COMMAND_SUCCESSFUL="false" 17 | 18 | echo "Running '$BUILD_COMMAND' on $REMOTE_MACHINE." 19 | echo "" 20 | 21 | set +e 22 | if ssh "$REMOTE_MACHINE" "echo 'set -e && cd '$PROJECT_DIR_ON_REMOTE_MACHINE' && echo \"$BUILD_COMMAND\" && echo "" && $BUILD_COMMAND' | bash" ; then 23 | BUILD_COMMAND_SUCCESSFUL="true" 24 | fi 25 | set -e 26 | echo "" 27 | 28 | if [ "$BUILD_COMMAND_SUCCESSFUL" == "true" ]; then 29 | echo "Execution done." 30 | else 31 | echo "Execution failed." 32 | exit 1 33 | fi 34 | } 35 | 36 | function build { 37 | TARGET_SDK_CONFIG_PROPERTY="android_target_sdk" 38 | BUILD_TOOLS_CONFIG_PROPERTY="android_build_tools" 39 | SDK_TOOLS_CONFIG_PROPERTY="android_sdk_tools" 40 | 41 | source ./mainframer-init.sh 42 | 43 | TARGET_SDK=$(readConfigProperty "$TARGET_SDK_CONFIG_PROPERTY") 44 | BUILD_TOOLS=$(readConfigProperty "$BUILD_TOOLS_CONFIG_PROPERTY") 45 | SDK_TOOLS=$(readConfigProperty "$SDK_TOOLS_CONFIG_PROPERTY") 46 | 47 | echo "Building..." 48 | echo "Target SDK: $TARGET_SDK" 49 | echo "Build tools: $BUILD_TOOLS" 50 | echo "SDK tools: $SDK_TOOLS" 51 | echo "" 52 | 53 | syncBeforeRemoteCommand 54 | executeRemotely "docker build -t $IMAGE_NAME . --build-arg ANDROID_TARGET_SDK=$TARGET_SDK --build-arg ANDROID_BUILD_TOOLS=$BUILD_TOOLS --build-arg ANDROID_SDK_TOOLS=$SDK_TOOLS" 55 | } 56 | 57 | function run { 58 | echo "Running" 59 | source ./mainframer-init.sh 60 | ./mainframer.sh "docker run -i --rm -v android:/root/.android -v sdk:/android-sdk-linux -v gradle:/root/.gradle -v $PROJECT_DIR_ON_REMOTE_MACHINE:/project:rw --name $IMAGE_NAME $IMAGE_NAME /project/gradlew ${@:2} -p /project" 61 | } 62 | 63 | function usage { 64 | echo "Usage: $0 {build|run}" 65 | } 66 | 67 | 68 | case "$COMMAND" in 69 | build) 70 | build $@ 71 | exit 0 72 | ;; 73 | 74 | run) 75 | run $@ 76 | exit 0 77 | ;; 78 | 79 | *) 80 | usage 81 | 82 | esac 83 | 84 | exit 1 85 | -------------------------------------------------------------------------------- /mainframer-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Juno, Inc. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | START_TIME="$(date +%s)" 20 | 21 | PROJECT_DIR="$(pwd)" 22 | PROJECT_DIR_NAME="$( basename "$PROJECT_DIR" )" 23 | PROJECT_DIR_ON_REMOTE_MACHINE="/home/andreas/mainframer/$PROJECT_DIR_NAME" 24 | 25 | CONFIG_DIR="$PROJECT_DIR/.mainframer" 26 | CONFIG_FILE="$CONFIG_DIR/config" 27 | COMMON_IGNORE_FILE="$CONFIG_DIR/ignore" 28 | LOCAL_IGNORE_FILE="$CONFIG_DIR/localignore" 29 | REMOTE_IGNORE_FILE="$CONFIG_DIR/remoteignore" 30 | 31 | function readConfigProperty { 32 | grep "^${1}=" "$CONFIG_FILE" | cut -d'=' -f2 33 | } 34 | 35 | REMOTE_MACHINE_CONFIG_PROPERTY="remote_machine" 36 | LOCAL_COMPRESS_LEVEL_CONFIG_PROPERTY="local_compression_level" 37 | REMOTE_COMPRESS_LEVEL_CONFIG_PROPERTY="remote_compression_level" 38 | 39 | if [ ! -f "$CONFIG_FILE" ]; then 40 | echo "Please create and fill $CONFIG_FILE." 41 | exit 1 42 | fi 43 | 44 | REMOTE_MACHINE=$(readConfigProperty "$REMOTE_MACHINE_CONFIG_PROPERTY") 45 | LOCAL_COMPRESS_LEVEL=$(readConfigProperty "$LOCAL_COMPRESS_LEVEL_CONFIG_PROPERTY") 46 | REMOTE_COMPRESS_LEVEL=$(readConfigProperty "$REMOTE_COMPRESS_LEVEL_CONFIG_PROPERTY") 47 | 48 | if [ -z "$REMOTE_MACHINE" ]; then 49 | echo "Please specify \"$REMOTE_MACHINE_CONFIG_PROPERTY\" in $CONFIG_FILE." 50 | exit 1 51 | fi 52 | 53 | if [ -z "$LOCAL_COMPRESS_LEVEL" ]; then 54 | LOCAL_COMPRESS_LEVEL=1 55 | fi 56 | 57 | if [ -z "$REMOTE_COMPRESS_LEVEL" ]; then 58 | REMOTE_COMPRESS_LEVEL=1 59 | fi 60 | 61 | function formatTime { 62 | local time=$1 63 | 64 | local hours=$((time / 3600)) 65 | local minutes=$(((time % 3600) / 60)) 66 | local seconds=$((time % 60)) 67 | 68 | if [ "$hours" -eq "1" ]; then HOURS_LABEL="hour"; else HOURS_LABEL="hours"; fi 69 | if [ "$minutes" -eq "1" ]; then MINUTES_LABEL="minute"; else MINUTES_LABEL="minutes"; fi 70 | if [ "$seconds" -eq "1" ]; then SECONDS_LABEL="second"; else SECONDS_LABEL="seconds"; fi 71 | 72 | (( hours > 0 )) && printf "%d $HOURS_LABEL " ${hours} 73 | (( minutes > 0 )) && printf "%d $MINUTES_LABEL " ${minutes} 74 | (( seconds >= 0 )) && printf "%d $SECONDS_LABEL" ${seconds} 75 | } 76 | 77 | function syncBeforeRemoteCommand { 78 | echo "Sync local → remote machine..." 79 | startTime="$(date +%s)" 80 | 81 | COMMAND="rsync --archive --delete --rsync-path=\"mkdir -p \"$PROJECT_DIR_ON_REMOTE_MACHINE\" && rsync\" --compress-level=$LOCAL_COMPRESS_LEVEL " 82 | 83 | if [ -f "$COMMON_IGNORE_FILE" ]; then 84 | COMMAND+="--exclude-from='$COMMON_IGNORE_FILE' " 85 | fi 86 | 87 | if [ -f "$LOCAL_IGNORE_FILE" ]; then 88 | COMMAND+="--exclude-from='$LOCAL_IGNORE_FILE' " 89 | fi 90 | 91 | COMMAND+="--rsh ssh ./ $REMOTE_MACHINE:'$PROJECT_DIR_ON_REMOTE_MACHINE'" 92 | 93 | eval "$COMMAND" 94 | 95 | endTime="$(date +%s)" 96 | echo "Sync done: took $(formatTime $((endTime-startTime)))." 97 | echo "" 98 | } 99 | 100 | function executeRemoteCommand { 101 | echo "Executing command on remote machine…" 102 | echo "" 103 | startTime="$(date +%s)" 104 | 105 | set +e 106 | if ssh "$REMOTE_MACHINE" "echo 'set -e && cd '$PROJECT_DIR_ON_REMOTE_MACHINE' && echo \"$REMOTE_COMMAND\" && echo "" && $REMOTE_COMMAND' | bash" ; then 107 | REMOTE_COMMAND_SUCCESSFUL="true" 108 | fi 109 | set -e 110 | 111 | endTime="$(date +%s)" 112 | echo "" 113 | 114 | duration="$((endTime-startTime))" 115 | 116 | if [ "$REMOTE_COMMAND_SUCCESSFUL" == "true" ]; then 117 | echo "Execution done: took $(formatTime $duration)." 118 | else 119 | echo "Execution failed: took $(formatTime $duration)." 120 | fi 121 | 122 | echo "" 123 | } 124 | 125 | function syncAfterRemoteCommand { 126 | echo "Sync remote → local machine…" 127 | startTime="$(date +%s)" 128 | 129 | COMMAND="rsync --archive --delete --compress-level=$REMOTE_COMPRESS_LEVEL " 130 | 131 | if [ -f "$COMMON_IGNORE_FILE" ]; then 132 | COMMAND+="--exclude-from='$COMMON_IGNORE_FILE' " 133 | fi 134 | 135 | if [ -f "$REMOTE_IGNORE_FILE" ]; then 136 | COMMAND+="--exclude-from='$REMOTE_IGNORE_FILE' " 137 | fi 138 | 139 | COMMAND+="--rsh ssh $REMOTE_MACHINE:'$PROJECT_DIR_ON_REMOTE_MACHINE'/ ./" 140 | eval "$COMMAND" 141 | 142 | endTime="$(date +%s)" 143 | echo "Sync done: took $(formatTime $((endTime-startTime)))." 144 | } 145 | -------------------------------------------------------------------------------- /mainframer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Juno, Inc. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | echo ":: mainframer v2.1.0" 20 | echo "" 21 | 22 | source ./mainframer-init.sh 23 | 24 | REMOTE_COMMAND="$*" 25 | REMOTE_COMMAND_SUCCESSFUL="false" 26 | 27 | if [ -z "$REMOTE_COMMAND" ]; then 28 | echo "Please pass remote command." 29 | exit 1 30 | fi 31 | 32 | pushd "$PROJECT_DIR" > /dev/null 33 | 34 | syncBeforeRemoteCommand 35 | executeRemoteCommand 36 | syncAfterRemoteCommand 37 | 38 | popd > /dev/null 39 | 40 | FINISH_TIME="$(date +%s)" 41 | echo "" 42 | 43 | DURATION="$((FINISH_TIME-START_TIME))" 44 | 45 | if [ "$REMOTE_COMMAND_SUCCESSFUL" == "true" ]; then 46 | echo "Success: took $(formatTime $DURATION)." 47 | else 48 | echo "Failure: took $(formatTime $DURATION)." 49 | exit 1 50 | fi 51 | --------------------------------------------------------------------------------