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