#!/usr/bin/env python """ Script that builds one or more release apks. Does the following things in several steps: Step 1: (e.g., --beta): - Runs the selected (clean) Gradle builds (e.g. beta, prod, custom) Step 2: (e.g., --beta --push): - Creates an annotated tag called 'releases/versionName' - Pushes the git tag to origin for history - TODO (Not implemented yet): Uploads certain bits to releases.mediawiki.org: r, beta To run 1) tell people on #wikimedia-mobile you're about to bump the version, so hold off on merging to main 2) git checkout main 3) git pull 4) git reset --hard 5) python scripts/make-release.py --prod Note: the apk file locations are printed to stdout 6) manual step: test the apk(s): adb install -r 7) python scripts/make-release.py --prod --push 8) compile release note of prod using git log --pretty=format:"%h | %cr | %s" --abbrev-commit --no-merges `git tag -l r/*|tail -1`.. 9) Upload prod apk to Play Store and to releases.mediawiki.org Requires the python module 'sh' and the environment variable ANDROID_HOME to run. Ensure you have a clean working directory before running as well. See also https://www.mediawiki.org/wiki/Wikimedia_Apps/Team/Release_process """ import argparse import glob import os import re import sh import subprocess import sys PATH_PREFIX = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) GRADLEW = './gradlew' VERSION_START = '2.7' def p(*path_fragments): """ Combine the path fragments with PATH_PREFIX as a base, and return the new full path """ return os.path.join(PATH_PREFIX, *path_fragments) def get_git_tag_name(target, version_name): """ Returns name used for creating the tag """ return target + '/' + version_name def git_tag(target, version_name): """ Creates an annotated git tag for this release """ sh.git.tag('-a', get_git_tag_name(target, version_name), '-m', target) def git_push_tag(target, version_name): """ Pushes the git tag to origin """ tag_name = get_git_tag_name(target, version_name) print('pushing tag ' + tag_name) sh.git.push('origin', tag_name) def make_release(flavors, custom_channel): sh.cd(PATH_PREFIX) args = [GRADLEW, '-q', 'clean', '-PcustomChannel=' + custom_channel] tasks = ['assemble{0}Release'.format(flavor.title()) for flavor in flavors] args += tasks subprocess.call(args) def make_bundle(flavors, custom_channel): sh.cd(PATH_PREFIX) args = [GRADLEW, '-q', 'clean', '-PcustomChannel=' + custom_channel] tasks = ['bundle{0}Release'.format(flavor.title()) for flavor in flavors] args += tasks subprocess.call(args) def get_output_apk_file_name(flavor): return 'app/build/outputs/apk/' + flavor + '/release/app-' + flavor + '-release.apk' def get_output_bundle_file_name(flavor): return 'app/build/outputs/bundle/' + flavor + 'Release/app-' + flavor + '-release.aab' def get_android_home(): android_home = os.environ['ANDROID_HOME'] if android_home: return android_home else: sys.exit('$ANDROID_HOME not set') def grep_from_build_file(property_name, regex): build_gradle_file_name = 'app/build.gradle' with open(build_gradle_file_name, "r") as build_file: for line in build_file: found = re.search(regex, line) if found: res = found.groups()[0] return res sys.exit("Could not find %s in %s" % (property_name, build_gradle_file_name)) def get_build_tools_version_from_build_file(): return grep_from_build_file('buildToolsVersion', r'buildToolsVersion\s+\'(\S+)\'') def get_version_code_from_build_file(): return grep_from_build_file('versionCode', r'versionCode\s+(\S+)') def get_version_name_from_apk(apk_file): aapt = '%s/build-tools/%s/aapt' % (get_android_home(), next(os.walk('%s/build-tools/' % get_android_home()))[1][0]) process = subprocess.check_output([aapt, 'dump', 'badging', apk_file]) found = re.search(r'versionName=\'(\S+)\'', str(process)) if found: apk_version_name = found.groups()[0] return apk_version_name else: sys.exit("Could not get version name from apk " + apk_file) def copy_apk(flavor, version_name): folder_path = 'releases' sh.mkdir("-p", folder_path) output_file = '%s/wikipedia-%s.apk' % (folder_path, version_name) sh.cp(get_output_apk_file_name(flavor), output_file) print(' apk: %s' % output_file) return output_file def copy_bundle(flavor, version_name): folder_path = 'releases' sh.mkdir("-p", folder_path) output_file = '%s/wikipedia-%s.aab' % (folder_path, version_name) sh.cp(get_output_bundle_file_name(flavor), output_file) print(' apk: %s' % output_file) def find_output_apk_for(label, version_code): folder_path = 'releases' file_pattern = '%s/wikipedia-%s.%s-%s-*.apk' % (folder_path, VERSION_START, version_code, label) apk_files = glob.glob(file_pattern) if len(apk_files) == 1: return apk_files[0] elif len(apk_files) == 0: sys.exit("Did not find apk files for %s" % file_pattern) else: sys.exit("Found too many(%d) files for %s" % (len(apk_files), file_pattern)) def main(): parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument('--alpha', help='Step 1: Alpha for testing.', action='store_true') group.add_argument('--beta', help='Step 1: Google Play Beta. git checkout BUMPTAG first!', action='store_true') group.add_argument('--prod', help='Step 1: Google Play stable.', action='store_true') parser.add_argument('--bundle', help='Build a bundle (AAB) in addition to APK.', action='store_true') group.add_argument('--channel', help='Step 1: Custom versionName&channel. OEMs w/ Play') group.add_argument('--app', help='Step 1: Custom versionName&channel. OEMs wout/ Play.') parser.add_argument('--push', help='Step 2: create&push git tag to origin.', action='store_true') args = parser.parse_args() custom_channel = 'ignore' if args.alpha: flavors = ['alpha'] targets = flavors elif args.beta: flavors = ['beta'] targets = flavors elif args.prod: flavors = ['prod'] targets = ['r'] elif args.channel: flavors = ['custom'] targets = [args.channel] custom_channel = args.channel elif args.app: flavors = ['custom'] targets = [args.app] custom_channel = args.app else: print('Error. Please specify --beta, --prod, etc.') sys.exit(-1) if args.push: if custom_channel == 'ignore': label = targets[0] else: label = custom_channel apk_file = find_output_apk_for(label, get_version_code_from_build_file()) version_name = get_version_name_from_apk(apk_file) for target in targets: git_tag(target, version_name) git_push_tag(target, version_name) else: folder_path = 'releases' sh.mkdir("-p", folder_path) print('Building APK: ' + str(flavors)) make_release(flavors, custom_channel) version_name = get_version_name_from_apk(get_output_apk_file_name(flavors[0])) print('Copying APK...') output_file = copy_apk(flavors[0], version_name) if args.bundle: print('Building bundle: ' + str(flavors)) make_bundle(flavors, custom_channel) print('Copying bundle...') copy_bundle(flavors[0], version_name) """ Remove the '.' to match the Samsung app store APK naming style. """ if custom_channel == 'samsung': os.rename(output_file, output_file.replace('.apk', '').replace('.', '-') + '.apk') print('Please test the APK. After that, run w/ --push flag, and release the tested APK.') print('A useful command for collecting the release notes:') print('git log --pretty=format:"%h | %cr | %s" --abbrev-commit --no-merges ' + '`git tag --list ' + targets[0] + '/* | tail -1`..') if __name__ == '__main__': main()