diff --git a/.github/workflows/build_apk.yml b/.github/workflows/build_apk.yml index 051fb388..f5870775 100644 --- a/.github/workflows/build_apk.yml +++ b/.github/workflows/build_apk.yml @@ -7,6 +7,11 @@ on: tar-url: description: 'URL for Kolibri tar file' required: true + release: + description: 'Is this a release asset?' + required: false + type: boolean + default: false workflow_call: inputs: tar-file-name: @@ -16,16 +21,50 @@ on: description: 'A ref for this workflow to check out its own repo' required: true type: string + release: + description: 'Is this a release asset?' + required: false + type: boolean + default: false + secrets: + # A base64-encoded keystore that contains the key used to sign the app + # for development builds. + KOLIBRI_ANDROID_APP_DEVELOPER_KEYSTORE: + required: false + # The password for the keystore. + KOLIBRI_ANDROID_APP_DEVELOPER_KEYSTORE_PASSWORD: + required: false + # The password for the key in the keystore. + KOLIBRI_ANDROID_APP_DEVELOPER_KEYALIAS_PASSWORD: + required: false + # A base64-encoded keystore that contains the key used to sign the app + # for production builds. + KOLIBRI_ANDROID_APP_PRODUCTION_KEYSTORE: + required: false + # The password for the keystore. + KOLIBRI_ANDROID_APP_PRODUCTION_KEYSTORE_PASSWORD: + required: false + # The password for the key in the keystore. + KOLIBRI_ANDROID_APP_PRODUCTION_KEYALIAS_PASSWORD: + required: false + KOLIBRI_ANDROID_PLAY_STORE_API_SERVICE_ACCOUNT_JSON: + required: false outputs: apk-file-name: description: "APK file name" value: ${{ jobs.build_apk.outputs.apk-file-name }} + version-code: + description: "Version code" + value: ${{ jobs.build_apk.outputs.version-code }} jobs: build_apk: runs-on: ubuntu-latest + env: + SERVICE_ACCOUNT_JSON: '${{ secrets.KOLIBRI_ANDROID_PLAY_STORE_API_SERVICE_ACCOUNT_JSON }}' outputs: apk-file-name: ${{ steps.get-apk-filename.outputs.apk-file-name }} + version-code: ${{ steps.get-version-code.outputs.version-code}} steps: - uses: actions/checkout@v3 if: ${{ !inputs.ref }} @@ -84,12 +123,34 @@ jobs: run: pip install -r requirements.txt - name: Ensure that Android SDK dependencies are installed run: make setup - - name: Build the app + - name: Build the aab + if: ${{ inputs.release == true || github.event.inputs.release == 'true' }} + env: + RELEASE_KEYALIAS: LE_RELEASE_KEY + RELEASE_KEYSTORE_PASSWD: ${{ secrets.KOLIBRI_ANDROID_APP_PRODUCTION_KEYSTORE_PASSWORD }} + RELEASE_KEYALIAS_PASSWD: ${{ secrets.KOLIBRI_ANDROID_APP_PRODUCTION_KEYALIAS_PASSWORD }} run: | - make kolibri.apk.unsigned + echo -n "${{ secrets.KOLIBRI_ANDROID_APP_PRODUCTION_KEYSTORE }}" | base64 --decode --output production.keystore + echo "RELEASE_KEYSTORE=$(realpath production.keystore)" >> "$GITHUB_ENV" + make kolibri.aab + # Upload to Play Store - this will also download the universal APK into the dist folder + make playstore-upload + - name: Build the apk + if: ${{ inputs.release != true && github.event.inputs.release != 'true' }} + env: + RELEASE_KEYALIAS: LE_DEV_KEY + RELEASE_KEYSTORE_PASSWD: ${{ secrets.KOLIBRI_ANDROID_APP_DEVELOPER_KEYSTORE_PASSWORD }} + RELEASE_KEYALIAS_PASSWD: ${{ secrets.KOLIBRI_ANDROID_APP_DEVELOPER_KEYALIAS_PASSWORD }} + run: | + echo -n "${{ secrets.KOLIBRI_ANDROID_APP_DEVELOPER_KEYSTORE }}" | base64 --decode --output development.keystore + echo "RELEASE_KEYSTORE=$(realpath development.keystore)" >> "$GITHUB_ENV" + make kolibri.apk - name: Get APK filename id: get-apk-filename run: echo "apk-file-name=$(ls dist | grep .apk | cat)" >> $GITHUB_OUTPUT + - name: Get versionCode + id: get-version-code + run: echo "version-code=$(cat .version-code)" >> $GITHUB_OUTPUT - uses: actions/upload-artifact@v3 with: name: ${{ steps.get-apk-filename.outputs.apk-file-name }} diff --git a/.github/workflows/release_apk.yml b/.github/workflows/release_apk.yml new file mode 100644 index 00000000..06d5ef0f --- /dev/null +++ b/.github/workflows/release_apk.yml @@ -0,0 +1,51 @@ +name: Release Android App + +on: + workflow_dispatch: + # Inputs the workflow accepts. + inputs: + version-code: + description: 'The version code to promote to the open testing track' + required: true + type: string + workflow_call: + inputs: + version-code: + description: 'The version code to promote to the open testing track' + required: true + type: string + ref: + description: 'A ref for this workflow to check out its own repo' + required: true + type: string + secrets: + KOLIBRI_ANDROID_PLAY_STORE_API_SERVICE_ACCOUNT_JSON: + required: false + +jobs: + release_apk: + runs-on: ubuntu-latest + env: + SERVICE_ACCOUNT_JSON: '${{ secrets.KOLIBRI_ANDROID_PLAY_STORE_API_SERVICE_ACCOUNT_JSON }}' + steps: + - uses: actions/checkout@v3 + if: ${{ !inputs.ref }} + - uses: actions/checkout@v3 + if: ${{ inputs.ref }} + with: + repository: learningequality/kolibri-android-installer + ref: ${{ inputs.ref }} + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: 3.9 + - uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install dependencies + run: pip install -r requirements.txt + - name: Release APK + run: python scripts/play_store_api.py release "${{ inputs.version-code }}" diff --git a/.gitignore b/.gitignore index 9208e1eb..f382af05 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ android_root/ *.apk .env +.version-code +*.keystore diff --git a/Makefile b/Makefile index 5ca66530..315ce507 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,7 @@ get-tar: clean-tar $(eval TARFILE = $(shell echo "${DLFILE}" | sed "s/\?.*//")) [ "${DLFILE}" = "${TARFILE}" ] || mv "${DLFILE}" "${TARFILE}" +.PHONY: install-tar # Extract the tar file install-tar: clean $(eval TARFILE = $(shell echo ""tar/kolibri*.tar.gz"" | sed "s/tar\///")) @@ -89,6 +90,7 @@ install-tar: clean rm -rf python-for-android/build/python-installs/kolibri/*/kolibri* | true rm -rf python-for-android/build/other_builds/kolibri | true +.PHONY: create-strings create-strings: python scripts/create_strings.py @@ -109,8 +111,8 @@ p4a_android_distro: needs-android-dirs check-android-clean # Update the python-for-android project bootstrap, discarding any changes that are made to committed files # this should be the usually run command in normal workflows. .PHONY: p4a_android_project -p4a_android_project: install-tar p4a_android_distro create-strings needs-version - $(P4A) bootstrap $(ARCH_OPTIONS) --version=$(APK_VERSION) --numeric-version=$(BUILD_NUMBER) +p4a_android_project: install-tar p4a_android_distro create-strings + $(P4A) bootstrap $(ARCH_OPTIONS) --version="None" --numeric-version=1 # Stash any changes to our python-for-android directory @git stash push --quiet --include-untracked -- python-for-android $(MAKE) write-version @@ -119,30 +121,27 @@ p4a_android_project: install-tar p4a_android_distro create-strings needs-version # this command should only be run when it is known there is an update from the upstream p4a bootstrap # that is needed, although it will probably normally be easier to manually vendor the changes. .PHONY: update_project_from_p4a -update_project_from_p4a: install-tar p4a_android_distro create-strings needs-version - $(P4A) bootstrap $(ARCH_OPTIONS) --version=$(APK_VERSION) --numeric-version=$(BUILD_NUMBER) - -.PHONY: needs-version -needs-version: - $(eval APK_VERSION ?= $(shell python3 scripts/version.py apk_version)) - $(eval BUILD_NUMBER ?= $(shell python3 scripts/version.py build_number)) +update_project_from_p4a: install-tar p4a_android_distro create-strings + $(P4A) bootstrap $(ARCH_OPTIONS) --version="None" --numeric-version=1 +.version-code: + python3 scripts/version.py set_version_code .PHONY: write-version -write-version: needs-version - python3 scripts/version.py write_version +write-version: .version-code + python3 scripts/version.py write_version_properties .PHONY: kolibri.apk # Build the signed version of the apk kolibri.apk: p4a_android_project - $(MAKE) guard-P4A_RELEASE_KEYSTORE - $(MAKE) guard-P4A_RELEASE_KEYALIAS - $(MAKE) guard-P4A_RELEASE_KEYSTORE_PASSWD - $(MAKE) guard-P4A_RELEASE_KEYALIAS_PASSWD + $(MAKE) guard-RELEASE_KEYSTORE + $(MAKE) guard-RELEASE_KEYALIAS + $(MAKE) guard-RELEASE_KEYSTORE_PASSWD + $(MAKE) guard-RELEASE_KEYALIAS_PASSWD @echo "--- :android: Build APK" cd python-for-android/dists/kolibri && ./gradlew clean assembleRelease mkdir -p dist - cp python-for-android/dists/kolibri/build/outputs/apk/release/kolibri-release.apk dist/kolibri-release-$(APK_VERSION).apk + cp python-for-android/dists/kolibri/build/outputs/apk/release/*.apk dist/ .PHONY: kolibri.apk.unsigned # Build the unsigned debug version of the apk @@ -150,19 +149,25 @@ kolibri.apk.unsigned: p4a_android_project @echo "--- :android: Build APK (unsigned)" cd python-for-android/dists/kolibri && ./gradlew clean assembleDebug mkdir -p dist - cp python-for-android/dists/kolibri/build/outputs/apk/debug/kolibri-debug.apk dist/kolibri-debug-$(APK_VERSION).apk + cp python-for-android/dists/kolibri/build/outputs/apk/debug/*.apk dist/ .PHONY: kolibri.aab # Build the signed version of the aab kolibri.aab: p4a_android_project - $(MAKE) guard-P4A_RELEASE_KEYSTORE - $(MAKE) guard-P4A_RELEASE_KEYALIAS - $(MAKE) guard-P4A_RELEASE_KEYSTORE_PASSWD - $(MAKE) guard-P4A_RELEASE_KEYALIAS_PASSWD + $(MAKE) guard-RELEASE_KEYSTORE + $(MAKE) guard-RELEASE_KEYALIAS + $(MAKE) guard-RELEASE_KEYSTORE_PASSWD + $(MAKE) guard-RELEASE_KEYALIAS_PASSWD @echo "--- :android: Build AAB" cd python-for-android/dists/kolibri && ./gradlew clean bundleRelease mkdir -p dist - cp python-for-android/dists/kolibri/build/outputs/bundle/release/kolibri-release.aab dist/kolibri-release-$(APK_VERSION).aab + cp python-for-android/dists/kolibri/build/outputs/bundle/release/*.aab dist/ + +.PHONY: playstore-upload +# Upload the aab to the play store +playstore-upload: + python3 scripts/play_store_api.py upload + # DOCKER BUILD diff --git a/python-for-android/dists/kolibri/build.gradle b/python-for-android/dists/kolibri/build.gradle index 54758c19..c9476b5c 100644 --- a/python-for-android/dists/kolibri/build.gradle +++ b/python-for-android/dists/kolibri/build.gradle @@ -39,6 +39,12 @@ android { def code = versionProps['VERSION_CODE'].toInteger() def name = versionProps['VERSION_NAME'] + // If we are doing a debug build, we'll end up with -debug-debug + // so we strip that here. + // For release builds, we'll either have -dev-release + // or -official-release so it is more informative. + def nameNoDebug = name.replace("-debug", "") + defaultConfig { minSdkVersion 23 targetSdkVersion 31 @@ -46,27 +52,32 @@ android { versionName name manifestPlaceholders = [:] multiDexEnabled true + setProperty("archivesBaseName", "kolibri-$nameNoDebug") } - - packagingOptions { + packagingOptions { jniLibs { useLegacyPackaging = true } - doNotStrip '**/*.so' - - } - - - - + exclude 'lib/**/gdbserver' + exclude 'lib/**/gdb.setup' + } + signingConfigs { + release { + storeFile file(System.getenv("RELEASE_KEYSTORE") ?: "empty") + keyAlias System.getenv("RELEASE_KEYALIAS") ?: "" + storePassword System.getenv("RELEASE_KEYSTORE_PASSWD") ?: "" + keyPassword System.getenv("RELEASE_KEYALIAS_PASSWD") ?: "" + } + } buildTypes { debug { + debuggable true } release { - + signingConfig signingConfigs.release } } diff --git a/requirements.txt b/requirements.txt index 8bdadf78..5de6322e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ cython~=0.29 virtualenv git+https://github.com/learningequality/python-for-android@4a3c74caf67cad4495f2352ae56ba2b0f1b266c5#egg=python-for-android +google-api-python-client==2.96.0 +google-auth==2.22.0 +google-auth-httplib2==0.1.0 diff --git a/scripts/create_strings.py b/scripts/create_strings.py index 3803b055..f807918a 100644 --- a/scripts/create_strings.py +++ b/scripts/create_strings.py @@ -43,7 +43,7 @@ def generate_loading_pages(output_dir): "--output-dir", output_dir, "--version-text", - apk_version(), + apk_version().replace("-official", ""), ) diff --git a/scripts/play_store_api.py b/scripts/play_store_api.py new file mode 100644 index 00000000..65c24495 --- /dev/null +++ b/scripts/play_store_api.py @@ -0,0 +1,251 @@ +import glob +import json +import mimetypes +import os +import socket +import sys +import time + +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +from googleapiclient.http import MediaIoBaseDownload + + +# Register mimetypes for aab and apk files +mimetypes.add_type("application/octet-stream", ".apk") +mimetypes.add_type("application/octet-stream", ".aab") + +# Set a timeout of 7 days for all requests. +socket.setdefaulttimeout(7 * 24 * 60 * 60) + +SCOPES = ["https://www.googleapis.com/auth/androidpublisher"] + + +def package_name(): + return os.environ.get("PACKAGE_NAME", "org.learningequality.Kolibri") + + +def _get_credentials(): + if "SERVICE_ACCOUNT_JSON" not in os.environ: + raise RuntimeError("SERVICE_ACCOUNT_JSON environment variable not set.") + + try: + SERVICE_ACCOUNT_JSON = json.loads(os.environ["SERVICE_ACCOUNT_JSON"]) + except ValueError: + raise RuntimeError( + "SERVICE_ACCOUNT_JSON environment variable is not valid JSON." + ) + + return service_account.Credentials.from_service_account_info( + SERVICE_ACCOUNT_JSON, scopes=SCOPES + ) + + +def _get_service(): + return build("androidpublisher", "v3", credentials=_get_credentials()) + + +def _create_edit(service): + return service.edits().insert(body={}, packageName=package_name()).execute()["id"] + + +def get_latest_version_code(): + service = _get_service() + edit_id = _create_edit(service) + tracks = ( + service.edits() + .tracks() + .list(editId=edit_id, packageName=package_name()) + .execute()["tracks"] + ) + versionCode = 0 + for track in tracks: + for release in track["releases"]: + for vc in release.get("versionCodes", []): + versionCode = max(versionCode, int(vc)) + # Clean up after ourselves! + service.edits().delete(editId=edit_id, packageName=package_name()).execute() + if versionCode == 0: + raise RuntimeError( + "No version code found - unless this app has never been released before, this is indicative of an error." + ) + return versionCode + + +def upload_dist_aab(): + from version import apk_version + + service = _get_service() + edit_id = _create_edit(service) + aabs = glob.glob( + os.path.join( + os.path.dirname(__file__), + "../dist/*.aab", + ) + ) + if len(aabs) != 1: + raise RuntimeError( + "Expected exactly one aab file in dist, found {}".format(len(aabs)) + ) + aab_path = aabs[0] + + print("Uploading AAB: {}".format(aab_path)) + + bundle_upload = ( + service.edits() + .bundles() + .upload(editId=edit_id, packageName=package_name(), media_body=aab_path) + .execute() + ) + + versionCode = bundle_upload["versionCode"] + + print("AAB with version code: {} successfully uploaded".format(versionCode)) + + # Assign APK to closed testing track. + track_response = ( + service.edits() + .tracks() + .update( + editId=edit_id, + track="internal", + packageName=package_name(), + body={ + "releases": [ + { + "name": apk_version(), + "versionCodes": [versionCode], + "status": "completed", + } + ] + }, + ) + .execute() + ) + + print( + "Track {} is set with releases: {}".format( + track_response["track"], str(track_response["releases"]) + ) + ) + + # Commit changes for edit. + commit_request = ( + service.edits().commit(editId=edit_id, packageName=package_name()).execute() + ) + + print("Edit id {} has been committed".format(commit_request["id"])) + + universal_apk_id = None + while universal_apk_id is None: + try: + generated_apk_response = ( + service.generatedapks() + .list(packageName=package_name(), versionCode=versionCode) + .execute() + ) + + universal_apk_id = generated_apk_response["generatedApks"][0][ + "generatedUniversalApk" + ]["downloadId"] + # Ensure that it has been generated by checking that this endpoint + # returns a success code. + service.generatedapks().download( + packageName=package_name(), + versionCode=versionCode, + downloadId=universal_apk_id, + ).execute() + except (HttpError, IndexError, KeyError): + print("Waiting for universal APK to be generated...") + time.sleep(15) + continue + + print("Universal APK generated with download ID: {}".format(universal_apk_id)) + + downloaded_attempts = 0 + + while downloaded_attempts < 10: + try: + # Download the universal APK. + downloader_request = service.generatedapks().download_media( + packageName=package_name(), + versionCode=versionCode, + downloadId=universal_apk_id, + ) + + filename = "kolibri-{}-release-universal.apk".format(apk_version()) + + filepath = os.path.join(os.path.dirname(__file__), "../dist", filename) + + with open(filepath, "wb") as f: + downloader = MediaIoBaseDownload( + f, downloader_request, chunksize=1024 * 1024 + ) + done = False + while not done: + status, done = downloader.next_chunk() + if status: + print( + "Universal APK download in progress: {}%.".format( + int(status.progress() * 100) + ) + ) + break + except Exception as e: + downloaded_attempts += 1 + print("Download failed with error: {}. Retrying...".format(e)) + + print("Universal APK downloaded to {}".format(filepath)) + + +def release_app(version_code): + service = _get_service() + edit_id = _create_edit(service) + internal_track = ( + service.edits() + .tracks() + .get(track="internal", editId=edit_id, packageName=package_name) + .execute() + ) + release = None + for r in internal_track["releases"]: + if version_code in r["versionCodes"]: + release = r + break + else: + raise RuntimeError( + "Version code {} not found in internal track.".format(version_code) + ) + + # Assign this release to the open testing track + service.edits().tracks().update( + editId=edit_id, + track="beta", + packageName=package_name(), + body={"releases": [release]}, + ).execute() + + print("Open testing track updated with release: {}".format(str(release))) + + # Commit changes for edit. + commit_response = ( + service.edits().commit(editId=edit_id, packageName=package_name()).execute() + ) + + print("Edit id {} has been committed".format(commit_response["id"])) + print("App version {} has been promoted to open testing.".format(version_code)) + + +if __name__ == "__main__": + if sys.argv[1] == "upload": + upload_dist_aab() + elif sys.argv[1] == "release": + try: + release_app(sys.argv[2]) + except IndexError: + raise RuntimeError( + "You must specify the version code of the release to promote to production." + ) + else: + raise RuntimeError("Unknown command {}".format(sys.argv[1])) diff --git a/scripts/version.py b/scripts/version.py index 9a1831ce..88823af6 100644 --- a/scripts/version.py +++ b/scripts/version.py @@ -1,8 +1,20 @@ import os -import subprocess import sys from datetime import datetime +from play_store_api import get_latest_version_code + + +android_installer_version = "0.1.0" + + +BUILD_TYPE_DEBUG = "debug" +BUILD_TYPE_DEV = "dev" +BUILD_TYPE_OFFICIAL = "official" + + +VERSION_CODE_FILE = os.path.join(os.path.dirname(__file__), "../.version-code") + def kolibri_version(): """ @@ -13,43 +25,13 @@ def kolibri_version(): return kolibri.__version__ -def commit_hash(): - """ - Returns the number of commits of the Kolibri Android repo. Returns 0 if something fails. - TODO hash, unless there's a tag. Use alias to annotate - """ - repo_dir = os.path.dirname(os.path.abspath(__file__)) - p = subprocess.Popen( - "git rev-parse --short HEAD", - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - cwd=repo_dir, - universal_newlines=True, - ) - return p.communicate()[0].rstrip() - - -def git_tag(): - repo_dir = os.path.dirname(os.path.abspath(__file__)) - p = subprocess.Popen( - "git tag --points-at {}".format(commit_hash()), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - cwd=repo_dir, - universal_newlines=True, - ) - return p.communicate()[0].rstrip() - - def build_type(): - key_alias = os.getenv("P4A_RELEASE_KEYALIAS", "unknown") + key_alias = os.getenv("RELEASE_KEYALIAS", "") if key_alias == "LE_DEV_KEY": - return "dev" + return BUILD_TYPE_DEV if key_alias == "LE_RELEASE_KEY": - return "official" - return key_alias + return BUILD_TYPE_OFFICIAL + return BUILD_TYPE_DEBUG def apk_version(): @@ -57,32 +39,49 @@ def apk_version(): Returns the version to be used for the Kolibri Android app. Schema: [kolibri version]-[android installer version or githash]-[build signature type] """ - android_version_indicator = git_tag() or commit_hash() - return "{}-{}-{}".format(kolibri_version(), android_version_indicator, build_type()) + return "{}-{}-{}".format(kolibri_version(), android_installer_version, build_type()) -def build_number(): +def _generate_build_number(): """ - Returns the build number for the apk. This is the mechanism used to understand whether one - build is newer than another. Uses buildkite build number with time as local dev backup + Generates the build number - this should not be called more than once per build. """ - + if build_type() == BUILD_TYPE_OFFICIAL: + return get_latest_version_code() + 1 + # build_base_number is no longer strictly needed, but keeping here to remind us + # why our versionCodes are so high - also, we use this base build number to + # make sure the time based build number is not higher than the maximum value + # of 2100000000, so it still serves a purpose! + # It also has the additional advantage of ensuring that the build number + # for the dev build is always lower than the build number for the official build. + # Meaning we shouldn't accidentally release a development build. # Patch, due to a build error. # Envar was not being passed into the container this runs in, and the # build submitted to the play store ended up using the dev build number. # We can't go backwards. So we're adding to the one submitted at first. build_base_number = 2008998000 + return int(datetime.now().strftime("%y%m%d%H%M")) - build_base_number - buildkite_build_number = os.getenv("BUILDKITE_BUILD_NUMBER") - if buildkite_build_number is not None: - build_number = build_base_number + 2 * int(buildkite_build_number) - return str(build_number) +def write_build_number(): + """ + Writes the build number to a file. + """ + with open(VERSION_CODE_FILE, "w") as f: + f.write(str(_generate_build_number())) - alt_build_number = ( - int(datetime.now().strftime("%y%m%d%H%M")) - build_base_number - ) * 2 - return alt_build_number + +def build_number(): + """ + Returns the build number for the apk. See the functions above for how the file this is + read from is generated. + """ + try: + with open(VERSION_CODE_FILE, "r") as f: + return int(f.read()) + except Exception as e: + print("Improper version code file, have you generated a version code?") + raise e def fileoutput(): @@ -103,9 +102,9 @@ def fileoutput(): if __name__ == "__main__": - if sys.argv[1] == "apk_version": - print(apk_version()) - elif sys.argv[1] == "build_number": - print(build_number()) - elif sys.argv[1] == "write_version": + if sys.argv[1] == "set_version_code": + write_build_number() + elif sys.argv[1] == "write_version_properties": fileoutput() + else: + raise RuntimeError("Unknown command {}".format(sys.argv[1]))