| #!/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() |