Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/python2
      2 
      3 # Copyright 2014 Google Inc.
      4 #
      5 # Use of this source code is governed by a BSD-style license that can be
      6 # found in the LICENSE file.
      7 
      8 """Skia's Chromium DEPS roll script.
      9 
     10 This script:
     11 - searches through the last N Skia git commits to find out the hash that is
     12   associated with the SVN revision number.
     13 - creates a new branch in the Chromium tree, modifies the DEPS file to
     14   point at the given Skia commit, commits, uploads to Rietveld, and
     15   deletes the local copy of the branch.
     16 - creates a whitespace-only commit and uploads that to to Rietveld.
     17 - returns the Chromium tree to its previous state.
     18 
     19 To specify the location of the git executable, set the GIT_EXECUTABLE
     20 environment variable.
     21 
     22 Usage:
     23   %prog -c CHROMIUM_PATH -r REVISION [OPTIONAL_OPTIONS]
     24 """
     25 
     26 
     27 import optparse
     28 import os
     29 import re
     30 import shutil
     31 import sys
     32 import tempfile
     33 
     34 import fix_pythonpath # pylint: disable=W0611
     35 from common.py.utils import git_utils
     36 from common.py.utils import misc
     37 from common.py.utils import shell_utils
     38 
     39 
     40 DEFAULT_BOTS_LIST = [
     41   'android_clang_dbg',
     42   'android_dbg',
     43   'android_rel',
     44   'cros_daisy',
     45   'linux',
     46   'linux_asan',
     47   'linux_chromeos',
     48   'linux_chromeos_asan',
     49   'linux_chromium_gn_dbg',
     50   'linux_gpu',
     51   'linux_layout',
     52   'linux_layout_rel',
     53   'mac',
     54   'mac_asan',
     55   'mac_gpu',
     56   'mac_layout',
     57   'mac_layout_rel',
     58   'win',
     59   'win_gpu',
     60   'win_layout',
     61   'win_layout_rel',
     62 ]
     63 
     64 REGEXP_SKIA_REVISION = (
     65     r'^  "skia_revision": "(?P<revision>[0-9a-fA-F]{2,40})",$')
     66 
     67 
     68 class DepsRollConfig(object):
     69   """Contains configuration options for this module.
     70 
     71   Attributes:
     72       chromium_path: (string) path to a local chromium git repository.
     73       save_branches: (boolean) iff false, delete temporary branches.
     74       verbose: (boolean)  iff false, suppress the output from git-cl.
     75       skip_cl_upload: (boolean)
     76       cl_bot_list: (list of strings)
     77   """
     78 
     79   # pylint: disable=I0011,R0903,R0902
     80   def __init__(self, options=None):
     81     if not options:
     82       options = DepsRollConfig.GetOptionParser()
     83     # pylint: disable=I0011,E1103
     84     self.verbose = options.verbose
     85     self.save_branches = not options.delete_branches
     86     self.chromium_path = options.chromium_path
     87     self.skip_cl_upload = options.skip_cl_upload
     88     # Split and remove empty strigns from the bot list.
     89     self.cl_bot_list = [bot for bot in options.bots.split(',') if bot]
     90     self.default_branch_name = 'autogenerated_deps_roll_branch'
     91     self.reviewers_list = ','.join([
     92         # 'rmistry (at] google.com',
     93         # 'reed (at] google.com',
     94         # 'bsalomon (at] google.com',
     95         # 'robertphillips (at] google.com',
     96         ])
     97     self.cc_list = ','.join([
     98         # 'skia-team (at] google.com',
     99         ])
    100 
    101   @staticmethod
    102   def GetOptionParser():
    103     # pylint: disable=I0011,C0103
    104     """Returns an optparse.OptionParser object.
    105 
    106     Returns:
    107         An optparse.OptionParser object.
    108 
    109     Called by the main() function.
    110     """
    111     option_parser = optparse.OptionParser(usage=__doc__)
    112     # Anyone using this script on a regular basis should set the
    113     # CHROMIUM_CHECKOUT_PATH environment variable.
    114     option_parser.add_option(
    115         '-c', '--chromium_path', help='Path to local Chromium Git'
    116         ' repository checkout, defaults to CHROMIUM_CHECKOUT_PATH'
    117         ' if that environment variable is set.',
    118         default=os.environ.get('CHROMIUM_CHECKOUT_PATH'))
    119     option_parser.add_option(
    120         '-r', '--revision', default=None,
    121         help='The Skia Git commit hash.')
    122 
    123     option_parser.add_option(
    124         '', '--delete_branches', help='Delete the temporary branches',
    125         action='store_true', dest='delete_branches', default=False)
    126     option_parser.add_option(
    127         '', '--verbose', help='Do not suppress the output from `git cl`.',
    128         action='store_true', dest='verbose', default=False)
    129     option_parser.add_option(
    130         '', '--skip_cl_upload', help='Skip the cl upload step; useful'
    131         ' for testing.',
    132         action='store_true', default=False)
    133 
    134     default_bots_help = (
    135         'Comma-separated list of bots, defaults to a list of %d bots.'
    136         '  To skip `git cl try`, set this to an empty string.'
    137         % len(DEFAULT_BOTS_LIST))
    138     default_bots = ','.join(DEFAULT_BOTS_LIST)
    139     option_parser.add_option(
    140         '', '--bots', help=default_bots_help, default=default_bots)
    141 
    142     return option_parser
    143 
    144 
    145 class DepsRollError(Exception):
    146   """Exceptions specific to this module."""
    147   pass
    148 
    149 
    150 def change_skia_deps(revision, depspath):
    151   """Update the DEPS file.
    152 
    153   Modify the skia_revision entry in the given DEPS file.
    154 
    155   Args:
    156       revision: (string) Skia commit hash.
    157       depspath: (string) path to DEPS file.
    158   """
    159   temp_file = tempfile.NamedTemporaryFile(delete=False,
    160                                           prefix='skia_DEPS_ROLL_tmp_')
    161   try:
    162     deps_regex_rev = re.compile(REGEXP_SKIA_REVISION)
    163     deps_regex_rev_repl = '  "skia_revision": "%s",' % revision
    164 
    165     with open(depspath, 'r') as input_stream:
    166       for line in input_stream:
    167         line = deps_regex_rev.sub(deps_regex_rev_repl, line)
    168         temp_file.write(line)
    169   finally:
    170     temp_file.close()
    171   shutil.move(temp_file.name, depspath)
    172 
    173 
    174 def submit_tries(bots_to_run, dry_run=False):
    175   """Submit try requests for the current branch on the given bots.
    176 
    177   Args:
    178       bots_to_run: (list of strings) bots to request.
    179       dry_run: (bool) whether to actually submit the try request.
    180   """
    181   git_try = [
    182       git_utils.GIT, 'cl', 'try', '-m', 'tryserver.chromium']
    183   git_try.extend([arg for bot in bots_to_run for arg in ('-b', bot)])
    184 
    185   if dry_run:
    186     space = '   '
    187     print 'You should call:'
    188     print space, git_try
    189     print
    190   else:
    191     shell_utils.run(git_try)
    192 
    193 
    194 def roll_deps(config, revision):
    195   """Upload changed DEPS and a whitespace change.
    196 
    197   Given the correct git_hash, create two Reitveld issues.
    198 
    199   Args:
    200       config: (roll_deps.DepsRollConfig) object containing options.
    201       revision: (string) Skia Git hash.
    202 
    203   Returns:
    204       a tuple containing textual description of the two issues.
    205 
    206   Raises:
    207       OSError: failed to execute git or git-cl.
    208       subprocess.CalledProcessError: git returned unexpected status.
    209   """
    210   with misc.ChDir(config.chromium_path, verbose=config.verbose):
    211     git_utils.Fetch()
    212     output = shell_utils.run([git_utils.GIT, 'show', 'origin/master:DEPS'],
    213                              log_in_real_time=False).rstrip()
    214     match = re.search(REGEXP_SKIA_REVISION, output, flags=re.MULTILINE)
    215     old_revision = None
    216     if match:
    217       old_revision = match.group('revision')
    218     assert old_revision
    219 
    220     master_hash = git_utils.FullHash('origin/master').rstrip()
    221 
    222     # master_hash[8] gives each whitespace CL a unique name.
    223     branch = 'control_%s' % master_hash[:8]
    224     message = ('whitespace change %s\n\n'
    225                'Chromium base revision: %s\n\n'
    226                'This CL was created by Skia\'s roll_deps.py script.\n'
    227                ) % (master_hash[:8], master_hash[:8])
    228     with git_utils.GitBranch(branch, message,
    229                              delete_when_finished=not config.save_branches,
    230                              upload=not config.skip_cl_upload
    231                              ) as whitespace_branch:
    232       branch = git_utils.GetCurrentBranch()
    233       with open(os.path.join('build', 'whitespace_file.txt'), 'a') as f:
    234         f.write('\nCONTROL\n')
    235 
    236       control_url = whitespace_branch.commit_and_upload()
    237       if config.cl_bot_list:
    238         submit_tries(config.cl_bot_list, dry_run=config.skip_cl_upload)
    239       whitespace_cl = control_url
    240       if config.save_branches:
    241         whitespace_cl += '\n    branch: %s' % branch
    242 
    243     branch = 'roll_%s_%s' % (revision, master_hash[:8])
    244     message = (
    245         'roll skia DEPS to %s\n\n'
    246         'Chromium base revision: %s\n'
    247         'Old Skia revision: %s\n'
    248         'New Skia revision: %s\n'
    249         'Control CL: %s\n\n'
    250         'This CL was created by Skia\'s roll_deps.py script.\n\n'
    251         'Bypassing commit queue trybots:\n'
    252         'NOTRY=true\n'
    253         % (revision, master_hash[:8],
    254            old_revision[:8], revision[:8], control_url))
    255     with git_utils.GitBranch(branch, message,
    256                              delete_when_finished=not config.save_branches,
    257                              upload=not config.skip_cl_upload
    258                              ) as roll_branch:
    259       change_skia_deps(revision, 'DEPS')
    260       deps_url = roll_branch.commit_and_upload()
    261       if config.cl_bot_list:
    262         submit_tries(config.cl_bot_list, dry_run=config.skip_cl_upload)
    263       deps_cl = deps_url
    264       if config.save_branches:
    265         deps_cl += '\n    branch: %s' % branch
    266 
    267     return deps_cl, whitespace_cl
    268 
    269 
    270 def main(args):
    271   """main function; see module-level docstring and GetOptionParser help.
    272 
    273   Args:
    274       args: sys.argv[1:]-type argument list.
    275   """
    276   option_parser = DepsRollConfig.GetOptionParser()
    277   options = option_parser.parse_args(args)[0]
    278 
    279   if not options.revision:
    280     option_parser.error('Must specify a revision.')
    281   if not options.chromium_path:
    282     option_parser.error('Must specify chromium_path.')
    283   if not os.path.isdir(options.chromium_path):
    284     option_parser.error('chromium_path must be a directory.')
    285 
    286   config = DepsRollConfig(options)
    287   shell_utils.VERBOSE = options.verbose
    288   deps_issue, whitespace_issue = roll_deps(config, options.revision)
    289 
    290   if deps_issue and whitespace_issue:
    291     print 'DEPS roll:\n    %s\n' % deps_issue
    292     print 'Whitespace change:\n    %s\n' % whitespace_issue
    293   else:
    294     print >> sys.stderr, 'No issues created.'
    295 
    296 
    297 if __name__ == '__main__':
    298   main(sys.argv[1:])
    299