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