Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 """Updates the Chrome reference builds.
      8 
      9 Usage:
     10   $ cd /tmp
     11   $ /path/to/update_reference_build.py -r <revision>
     12   $ cd reference_builds/reference_builds
     13   $ gcl change
     14   $ gcl upload <change>
     15   $ gcl commit <change>
     16 """
     17 
     18 import errno
     19 import logging
     20 import optparse
     21 import os
     22 import shutil
     23 import subprocess
     24 import sys
     25 import time
     26 import urllib
     27 import urllib2
     28 import zipfile
     29 
     30 
     31 class BuildUpdater(object):
     32   _PLATFORM_FILES_MAP = {
     33     'Win': [
     34         'chrome-win32.zip',
     35         'chrome-win32-syms.zip',
     36         'chrome-win32.test/_pyautolib.pyd',
     37         'chrome-win32.test/pyautolib.py',
     38     ],
     39     'Mac': [
     40       'chrome-mac.zip',
     41       'chrome-mac.test/_pyautolib.so',
     42       'chrome-mac.test/pyautolib.py',
     43     ],
     44     'Linux': [
     45         'chrome-linux.zip',
     46     ],
     47     'Linux_x64': [
     48         'chrome-linux.zip',
     49     ],
     50   }
     51 
     52   _PLATFORM_DEST_MAP = {
     53     'Linux': 'chrome_linux',
     54     'Linux_x64': 'chrome_linux64',
     55     'Win': 'chrome_win',
     56     'Mac': 'chrome_mac',
     57    }
     58 
     59   def __init__(self, options):
     60     self._platforms = options.platforms.split(',')
     61     self._revision = int(options.revision)
     62 
     63   @staticmethod
     64   def _GetCmdStatusAndOutput(args, cwd=None, shell=False):
     65     """Executes a subprocess and returns its exit code and output.
     66 
     67     Args:
     68       args: A string or a sequence of program arguments.
     69       cwd: If not None, the subprocess's current directory will be changed to
     70         |cwd| before it's executed.
     71       shell: Whether to execute args as a shell command.
     72 
     73     Returns:
     74       The tuple (exit code, output).
     75     """
     76     logging.info(str(args) + ' ' + (cwd or ''))
     77     p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
     78                          stderr=subprocess.PIPE, shell=shell)
     79     stdout, stderr = p.communicate()
     80     exit_code = p.returncode
     81     if stderr:
     82       logging.critical(stderr)
     83     logging.info(stdout)
     84     return (exit_code, stdout)
     85 
     86   def _GetBuildUrl(self, platform, revision, filename):
     87     URL_FMT = ('http://commondatastorage.googleapis.com/'
     88                'chromium-browser-snapshots/%s/%s/%s')
     89     return URL_FMT % (urllib.quote_plus(platform), revision, filename)
     90 
     91   def _FindBuildRevision(self, platform, revision, filename):
     92     MAX_REVISIONS_PER_BUILD = 100
     93     for revision_guess in xrange(revision, revision + MAX_REVISIONS_PER_BUILD):
     94       r = urllib2.Request(self._GetBuildUrl(platform, revision_guess, filename))
     95       r.get_method = lambda: 'HEAD'
     96       try:
     97         response = urllib2.urlopen(r)
     98         return revision_guess
     99       except urllib2.HTTPError, err:
    100         if err.code == 404:
    101           time.sleep(.1)
    102           continue
    103     return None
    104 
    105   def _DownloadBuilds(self):
    106     for platform in self._platforms:
    107       for f in BuildUpdater._PLATFORM_FILES_MAP[platform]:
    108         output = os.path.join('dl', platform,
    109                               '%s_%s_%s' % (platform, self._revision, f))
    110         if os.path.exists(output):
    111           logging.info('%s alread exists, skipping download' % output)
    112           continue
    113         build_revision = self._FindBuildRevision(platform, self._revision, f)
    114         if not build_revision:
    115           logging.critical('Failed to find %s build for r%s\n' % (
    116               platform, self._revision))
    117           sys.exit(1)
    118         dirname = os.path.dirname(output)
    119         if dirname and not os.path.exists(dirname):
    120           os.makedirs(dirname)
    121         url = self._GetBuildUrl(platform, build_revision, f)
    122         logging.info('Downloading %s, saving to %s' % (url, output))
    123         r = urllib2.urlopen(url)
    124         with file(output, 'wb') as f:
    125           f.write(r.read())
    126 
    127   def _FetchSvnRepos(self):
    128     if not os.path.exists('reference_builds'):
    129       os.makedirs('reference_builds')
    130     BuildUpdater._GetCmdStatusAndOutput(
    131         ['gclient', 'config',
    132          'svn://svn.chromium.org/chrome/trunk/deps/reference_builds'],
    133         'reference_builds')
    134     BuildUpdater._GetCmdStatusAndOutput(
    135         ['gclient', 'sync'], 'reference_builds')
    136 
    137   def _UnzipFile(self, dl_file, dest_dir):
    138     if not zipfile.is_zipfile(dl_file):
    139       return False
    140     logging.info('Opening %s' % dl_file)
    141     with zipfile.ZipFile(dl_file, 'r') as z:
    142       for content in z.namelist():
    143         dest = os.path.join(dest_dir, content[content.find('/')+1:])
    144         if not os.path.basename(dest):
    145           if not os.path.isdir(dest):
    146             os.makedirs(dest)
    147           continue
    148         with z.open(content) as unzipped_content:
    149           logging.info('Extracting %s to %s (%s)' % (content, dest, dl_file))
    150           with file(dest, 'wb') as dest_file:
    151             dest_file.write(unzipped_content.read())
    152           permissions = z.getinfo(content).external_attr >> 16
    153           if permissions:
    154             os.chmod(dest, permissions)
    155     return True
    156 
    157   def _ClearDir(self, dir):
    158     """Clears all files in |dir| except for hidden files and folders."""
    159     for root, dirs, files in os.walk(dir):
    160       # Skip hidden files and folders (like .svn and .git).
    161       files = [f for f in files if f[0] != '.']
    162       dirs[:] = [d for d in dirs if d[0] != '.']
    163 
    164       for f in files:
    165         os.remove(os.path.join(root, f))
    166 
    167   def _ExtractBuilds(self):
    168     for platform in self._platforms:
    169       if os.path.exists('tmp_unzip'):
    170         os.path.unlink('tmp_unzip')
    171       dest_dir = os.path.join('reference_builds', 'reference_builds',
    172                               BuildUpdater._PLATFORM_DEST_MAP[platform])
    173       self._ClearDir(dest_dir)
    174       for root, _, dl_files in os.walk(os.path.join('dl', platform)):
    175         for dl_file in dl_files:
    176           dl_file = os.path.join(root, dl_file)
    177           if not self._UnzipFile(dl_file, dest_dir):
    178             logging.info('Copying %s to %s' % (dl_file, dest_dir))
    179             shutil.copy(dl_file, dest_dir)
    180 
    181   def _SvnAddAndRemove(self):
    182     svn_dir = os.path.join('reference_builds', 'reference_builds')
    183     stat = BuildUpdater._GetCmdStatusAndOutput(['svn', 'stat'], svn_dir)[1]
    184     for line in stat.splitlines():
    185       action, filename = line.split(None, 1)
    186       if action == '?':
    187         BuildUpdater._GetCmdStatusAndOutput(
    188             ['svn', 'add', filename], svn_dir)
    189       elif action == '!':
    190         BuildUpdater._GetCmdStatusAndOutput(
    191             ['svn', 'delete', filename], svn_dir)
    192       filepath = os.path.join(svn_dir, filename)
    193       if not os.path.isdir(filepath) and os.access(filepath, os.X_OK):
    194         BuildUpdater._GetCmdStatusAndOutput(
    195             ['svn', 'propset', 'svn:executable', 'true', filename], svn_dir)
    196 
    197   def DownloadAndUpdateBuilds(self):
    198     self._DownloadBuilds()
    199     self._FetchSvnRepos()
    200     self._ExtractBuilds()
    201     self._SvnAddAndRemove()
    202 
    203 
    204 def ParseOptions(argv):
    205   parser = optparse.OptionParser()
    206   usage = 'usage: %prog <options>'
    207   parser.set_usage(usage)
    208   parser.add_option('-r', dest='revision',
    209                     help='Revision to pickup')
    210   parser.add_option('-p', dest='platforms',
    211                     default='Win,Mac,Linux,Linux_x64',
    212                     help='Comma separated list of platforms to download '
    213                          '(as defined by the chromium builders).')
    214   (options, _) = parser.parse_args(argv)
    215   if not options.revision:
    216     logging.critical('Must specify -r\n')
    217     sys.exit(1)
    218 
    219   return options
    220 
    221 
    222 def main(argv):
    223   logging.getLogger().setLevel(logging.DEBUG)
    224   options = ParseOptions(argv)
    225   b = BuildUpdater(options)
    226   b.DownloadAndUpdateBuilds()
    227   logging.info('Successfully updated reference builds. Move to '
    228                'reference_builds/reference_builds and make a change with gcl.')
    229 
    230 if __name__ == '__main__':
    231   sys.exit(main(sys.argv))
    232