#!/usr/bin/env python # Copyright (c) 2017 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Script that uploads the specified Skia Gerrit change to Android. This script does the following: * Downloads the repo tool. * Inits and checks out the bare-minimum required Android checkout. * Sets the required git config options in external/skia. * Cherry-picks the specified Skia patch. * Modifies the change subject to append a "Test:" line required for presubmits. * Uploads the Skia change to Android's Gerrit instance. After the change is uploaded to Android, developers can trigger TH and download binaries (if required) after runs complete. The script re-uses the workdir when it is run again. To start from a clean slate delete the workdir. Timings: * ~1m15s when using an empty/non-existent workdir for the first time. * ~15s when using a workdir previously populated by the script. Example usage: $ python upload_to_android.py -w /repos/testing -c 44200 """ import argparse import getpass import json import os import subprocess import stat import urllib2 REPO_TOOL_URL = 'https://storage.googleapis.com/git-repo-downloads/repo' SKIA_PATH_IN_ANDROID = os.path.join('external', 'skia') ANDROID_REPO_URL = 'https://googleplex-android.googlesource.com' REPO_BRANCH_NAME = 'experiment' SKIA_GERRIT_INSTANCE = 'https://skia-review.googlesource.com' SK_USER_CONFIG_PATH = os.path.join('include', 'config', 'SkUserConfig.h') def get_change_details(change_num): response = urllib2.urlopen('%s/changes/%s/detail?o=ALL_REVISIONS' % ( SKIA_GERRIT_INSTANCE, change_num), timeout=5) content = response.read() # Remove the first line which contains ")]}'\n". return json.loads(content[5:]) def init_work_dir(work_dir): if not os.path.isdir(work_dir): print 'Creating %s' % work_dir os.makedirs(work_dir) # Ensure the repo tool exists in the work_dir. repo_dir = os.path.join(work_dir, 'bin') repo_binary = os.path.join(repo_dir, 'repo') if not os.path.isdir(repo_dir): print 'Creating %s' % repo_dir os.makedirs(repo_dir) if not os.path.exists(repo_binary): print 'Downloading %s from %s' % (repo_binary, REPO_TOOL_URL) response = urllib2.urlopen(REPO_TOOL_URL, timeout=5) content = response.read() with open(repo_binary, 'w') as f: f.write(content) # Set executable bit. st = os.stat(repo_binary) os.chmod(repo_binary, st.st_mode | stat.S_IEXEC) # Create android-repo directory in the work_dir. android_dir = os.path.join(work_dir, 'android-repo') if not os.path.isdir(android_dir): print 'Creating %s' % android_dir os.makedirs(android_dir) print """ About to run repo init. If it hangs asking you to run glogin then please: * Exit the script (ctrl-c). * Run 'glogin'. * Re-run the script. """ os.chdir(android_dir) subprocess.check_call( '%s init -u %s/a/platform/manifest -g "all,-notdefault,-darwin" ' '-b master --depth=1' % (repo_binary, ANDROID_REPO_URL), shell=True) print 'Syncing the Android checkout at %s' % android_dir subprocess.check_call('%s sync %s tools/repohooks -j 32 -c' % ( repo_binary, SKIA_PATH_IN_ANDROID), shell=True) # Set the necessary git config options. os.chdir(SKIA_PATH_IN_ANDROID) subprocess.check_call( 'git config remote.goog.review %s/' % ANDROID_REPO_URL, shell=True) subprocess.check_call( 'git config review.%s/.autoupload true' % ANDROID_REPO_URL, shell=True) subprocess.check_call( 'git config user.email %s@google.com' % getpass.getuser(), shell=True) return repo_binary class Modifier: def modify(self): raise NotImplementedError def get_user_msg(self): raise NotImplementedError class FetchModifier(Modifier): def __init__(self, change_num, debug): self.change_num = change_num self.debug = debug def modify(self): # Download and cherry-pick the patch. change_details = get_change_details(self.change_num) latest_patchset = len(change_details['revisions']) mod = int(self.change_num) % 100 download_ref = 'refs/changes/%s/%s/%s' % ( str(mod).zfill(2), self.change_num, latest_patchset) subprocess.check_call( 'git fetch https://skia.googlesource.com/skia %s' % download_ref, shell=True) subprocess.check_call('git cherry-pick FETCH_HEAD', shell=True) if self.debug: # Add SK_DEBUG to SkUserConfig.h. with open(SK_USER_CONFIG_PATH, 'a') as f: f.write('#ifndef SK_DEBUG\n') f.write('#define SK_DEBUG\n') f.write('#endif//SK_DEBUG\n') subprocess.check_call('git add %s' % SK_USER_CONFIG_PATH, shell=True) # Amend the commit message to add a prefix that makes it clear that the # change should not be submitted and a "Test:" line which is required by # Android presubmit checks. original_commit_message = change_details['subject'] new_commit_message = ( # Intentionally breaking up the below string because some presubmits # complain about it. '[DO ' + 'NOT ' + 'SUBMIT] %s\n\n' 'Test: Presubmit checks will test this change.' % ( original_commit_message)) subprocess.check_call('git commit --amend -m "%s"' % new_commit_message, shell=True) def get_user_msg(self): return """ Open the above URL and trigger TH by checking 'Presubmit-Ready'. You can download binaries (if required) from the TH link after it completes. """ # Add a legacy flag if it doesn't exist, or remove it if it exists. class AndroidLegacyFlagModifier(Modifier): def __init__(self, flag): self.flag = flag self.verb = "Unknown" def modify(self): flag_line = " #define %s\n" % self.flag config_file = os.path.join('include', 'config', 'SkUserConfigManual.h') with open(config_file) as f: lines = f.readlines() if flag_line not in lines: lines.insert( lines.index("#endif // SkUserConfigManual_DEFINED\n"), flag_line) verb = "Add" else: lines.remove(flag_line) verb = "Remove" with open(config_file, 'w') as f: for line in lines: f.write(line) subprocess.check_call('git add %s' % config_file, shell=True) message = '%s %s\n\nTest: Presubmit checks will test this change.' % ( verb, self.flag) subprocess.check_call('git commit -m "%s"' % message, shell=True) def get_user_msg(self): return """ Please open the above URL to review and land the change. """ def upload_to_android(work_dir, modifier): repo_binary = init_work_dir(work_dir) # Create repo branch. subprocess.check_call('%s start %s .' % (repo_binary, REPO_BRANCH_NAME), shell=True) try: modifier.modify() # Upload to Android Gerrit. subprocess.check_call('%s upload --verify' % repo_binary, shell=True) print modifier.get_user_msg() finally: # Abandon repo branch. subprocess.call('%s abandon %s' % (repo_binary, REPO_BRANCH_NAME), shell=True) def main(): parser = argparse.ArgumentParser() parser.add_argument( '--work-dir', '-w', required=True, help='Directory where an Android checkout will be created (if it does ' 'not already exist). Note: ~1GB space will be used.') parser.add_argument( '--change-num', '-c', required=True, help='The skia-rev Gerrit change number that should be patched into ' 'Android.') parser.add_argument( '--debug', '-d', action='store_true', default=False, help='Adds SK_DEBUG to SkUserConfig.h.') args = parser.parse_args() upload_to_android(args.work_dir, FetchModifier(args.change_num, args.debug)) if __name__ == '__main__': main()