1#!/usr/bin/env python 2# Copyright (c) 2017 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Script that uploads the specified Skia Gerrit change to Android. 7 8This script does the following: 9* Downloads the repo tool. 10* Inits and checks out the bare-minimum required Android checkout. 11* Sets the required git config options in external/skia. 12* Cherry-picks the specified Skia patch. 13* Modifies the change subject to append a "Test:" line required for presubmits. 14* Uploads the Skia change to Android's Gerrit instance. 15 16After the change is uploaded to Android, developers can trigger TH and download 17binaries (if required) after runs complete. 18 19The script re-uses the workdir when it is run again. To start from a clean slate 20delete the workdir. 21 22Timings: 23* ~1m15s when using an empty/non-existent workdir for the first time. 24* ~15s when using a workdir previously populated by the script. 25 26Example usage: 27 $ python upload_to_android.py -w /repos/testing -c 44200 28""" 29 30import argparse 31import getpass 32import json 33import os 34import subprocess 35import stat 36import urllib2 37 38 39REPO_TOOL_URL = 'https://storage.googleapis.com/git-repo-downloads/repo' 40SKIA_PATH_IN_ANDROID = os.path.join('external', 'skia') 41ANDROID_REPO_URL = 'https://googleplex-android.googlesource.com' 42REPO_BRANCH_NAME = 'experiment' 43SKIA_GERRIT_INSTANCE = 'https://skia-review.googlesource.com' 44SK_USER_CONFIG_PATH = os.path.join('include', 'config', 'SkUserConfig.h') 45 46 47def get_change_details(change_num): 48 response = urllib2.urlopen('%s/changes/%s/detail?o=ALL_REVISIONS' % ( 49 SKIA_GERRIT_INSTANCE, change_num), timeout=5) 50 content = response.read() 51 # Remove the first line which contains ")]}'\n". 52 return json.loads(content[5:]) 53 54 55def init_work_dir(work_dir): 56 if not os.path.isdir(work_dir): 57 print 'Creating %s' % work_dir 58 os.makedirs(work_dir) 59 60 # Ensure the repo tool exists in the work_dir. 61 repo_dir = os.path.join(work_dir, 'bin') 62 repo_binary = os.path.join(repo_dir, 'repo') 63 if not os.path.isdir(repo_dir): 64 print 'Creating %s' % repo_dir 65 os.makedirs(repo_dir) 66 if not os.path.exists(repo_binary): 67 print 'Downloading %s from %s' % (repo_binary, REPO_TOOL_URL) 68 response = urllib2.urlopen(REPO_TOOL_URL, timeout=5) 69 content = response.read() 70 with open(repo_binary, 'w') as f: 71 f.write(content) 72 # Set executable bit. 73 st = os.stat(repo_binary) 74 os.chmod(repo_binary, st.st_mode | stat.S_IEXEC) 75 76 # Create android-repo directory in the work_dir. 77 android_dir = os.path.join(work_dir, 'android-repo') 78 if not os.path.isdir(android_dir): 79 print 'Creating %s' % android_dir 80 os.makedirs(android_dir) 81 82 print """ 83 84About to run repo init. If it hangs asking you to run glogin then please: 85* Exit the script (ctrl-c). 86* Run 'glogin'. 87* Re-run the script. 88 89""" 90 os.chdir(android_dir) 91 subprocess.check_call( 92 '%s init -u %s/a/platform/manifest -g "all,-notdefault,-darwin" ' 93 '-b master --depth=1' 94 % (repo_binary, ANDROID_REPO_URL), shell=True) 95 96 print 'Syncing the Android checkout at %s' % android_dir 97 subprocess.check_call('%s sync %s tools/repohooks -j 32 -c' % ( 98 repo_binary, SKIA_PATH_IN_ANDROID), shell=True) 99 100 # Set the necessary git config options. 101 os.chdir(SKIA_PATH_IN_ANDROID) 102 subprocess.check_call( 103 'git config remote.goog.review %s/' % ANDROID_REPO_URL, shell=True) 104 subprocess.check_call( 105 'git config review.%s/.autoupload true' % ANDROID_REPO_URL, shell=True) 106 subprocess.check_call( 107 'git config user.email %s@google.com' % getpass.getuser(), shell=True) 108 109 return repo_binary 110 111 112class Modifier: 113 def modify(self): 114 raise NotImplementedError 115 def get_user_msg(self): 116 raise NotImplementedError 117 118 119class FetchModifier(Modifier): 120 def __init__(self, change_num, debug): 121 self.change_num = change_num 122 self.debug = debug 123 124 def modify(self): 125 # Download and cherry-pick the patch. 126 change_details = get_change_details(self.change_num) 127 latest_patchset = len(change_details['revisions']) 128 mod = int(self.change_num) % 100 129 download_ref = 'refs/changes/%s/%s/%s' % ( 130 str(mod).zfill(2), self.change_num, latest_patchset) 131 subprocess.check_call( 132 'git fetch https://skia.googlesource.com/skia %s' % download_ref, 133 shell=True) 134 subprocess.check_call('git cherry-pick FETCH_HEAD', shell=True) 135 136 if self.debug: 137 # Add SK_DEBUG to SkUserConfig.h. 138 with open(SK_USER_CONFIG_PATH, 'a') as f: 139 f.write('#ifndef SK_DEBUG\n') 140 f.write('#define SK_DEBUG\n') 141 f.write('#endif//SK_DEBUG\n') 142 subprocess.check_call('git add %s' % SK_USER_CONFIG_PATH, shell=True) 143 144 # Amend the commit message to add a prefix that makes it clear that the 145 # change should not be submitted and a "Test:" line which is required by 146 # Android presubmit checks. 147 original_commit_message = change_details['subject'] 148 new_commit_message = ( 149 # Intentionally breaking up the below string because some presubmits 150 # complain about it. 151 '[DO ' + 'NOT ' + 'SUBMIT] %s\n\n' 152 'Test: Presubmit checks will test this change.' % ( 153 original_commit_message)) 154 155 subprocess.check_call('git commit --amend -m "%s"' % new_commit_message, 156 shell=True) 157 158 def get_user_msg(self): 159 return """ 160 161Open the above URL and trigger TH by checking 'Presubmit-Ready'. 162You can download binaries (if required) from the TH link after it completes. 163""" 164 165 166# Add a legacy flag if it doesn't exist, or remove it if it exists. 167class AndroidLegacyFlagModifier(Modifier): 168 def __init__(self, flag): 169 self.flag = flag 170 self.verb = "Unknown" 171 172 def modify(self): 173 flag_line = " #define %s\n" % self.flag 174 175 config_file = os.path.join('include', 'config', 'SkUserConfigManual.h') 176 177 with open(config_file) as f: 178 lines = f.readlines() 179 180 if flag_line not in lines: 181 lines.insert( 182 lines.index("#endif // SkUserConfigManual_DEFINED\n"), flag_line) 183 verb = "Add" 184 else: 185 lines.remove(flag_line) 186 verb = "Remove" 187 188 with open(config_file, 'w') as f: 189 for line in lines: 190 f.write(line) 191 192 subprocess.check_call('git add %s' % config_file, shell=True) 193 message = '%s %s\n\nTest: Presubmit checks will test this change.' % ( 194 verb, self.flag) 195 196 subprocess.check_call('git commit -m "%s"' % message, shell=True) 197 198 def get_user_msg(self): 199 return """ 200 201 Please open the above URL to review and land the change. 202""" 203 204 205def upload_to_android(work_dir, modifier): 206 repo_binary = init_work_dir(work_dir) 207 208 # Create repo branch. 209 subprocess.check_call('%s start %s .' % (repo_binary, REPO_BRANCH_NAME), 210 shell=True) 211 try: 212 modifier.modify() 213 214 # Upload to Android Gerrit. 215 subprocess.check_call('%s upload --verify' % repo_binary, shell=True) 216 217 print modifier.get_user_msg() 218 finally: 219 # Abandon repo branch. 220 subprocess.call('%s abandon %s' % (repo_binary, REPO_BRANCH_NAME), 221 shell=True) 222 223 224def main(): 225 parser = argparse.ArgumentParser() 226 parser.add_argument( 227 '--work-dir', '-w', required=True, 228 help='Directory where an Android checkout will be created (if it does ' 229 'not already exist). Note: ~1GB space will be used.') 230 parser.add_argument( 231 '--change-num', '-c', required=True, 232 help='The skia-rev Gerrit change number that should be patched into ' 233 'Android.') 234 parser.add_argument( 235 '--debug', '-d', action='store_true', default=False, 236 help='Adds SK_DEBUG to SkUserConfig.h.') 237 args = parser.parse_args() 238 upload_to_android(args.work_dir, FetchModifier(args.change_num, args.debug)) 239 240 241if __name__ == '__main__': 242 main() 243