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 To update Chromium (unofficial) reference build, use the -r option and give a
     10 Chromium SVN revision number, like 228977. To update a Chrome (official) build,
     11 use the -v option and give a Chrome version number like 30.0.1595.0.
     12 
     13 If you're getting a Chrome build, you can give the flag --gs to fetch from
     14 Google Storage. Otherwise, it will be fetched from go/chrome_official_builds.
     15 
     16 Before running this script, you should first verify that you are authenticated
     17 for SVN. You can do this by running:
     18   $ svn ls svn://svn.chromium.org/chrome/trunk/deps/reference_builds
     19 You may need to get your SVN password from https://chromium-access.appspot.com/.
     20 
     21 Usage:
     22   $ cd /tmp
     23   $ /path/to/update_reference_build.py --gs -v <version>
     24   $ cd reference_builds/reference_builds
     25   $ gcl change
     26   $ gcl upload <change>
     27   $ gcl commit <change>
     28 """
     29 
     30 import logging
     31 import optparse
     32 import os
     33 import shutil
     34 import subprocess
     35 import sys
     36 import time
     37 import urllib
     38 import urllib2
     39 import zipfile
     40 
     41 # Example chromium build location:
     42 # gs://chromium-browser-snapshots/Linux/228977/chrome-linux.zip
     43 CHROMIUM_URL_FMT = ('http://commondatastorage.googleapis.com/'
     44                     'chromium-browser-snapshots/%s/%s/%s')
     45 
     46 # Chrome official build storage
     47 # https://wiki.corp.google.com/twiki/bin/view/Main/ChromeOfficialBuilds
     48 
     49 # Internal Google archive of official Chrome builds, example:
     50 # https://goto.google.com/chrome_official_builds/
     51 # 32.0.1677.0/precise32bit/chrome-precise32bit.zip
     52 CHROME_INTERNAL_URL_FMT = ('http://master.chrome.corp.google.com/'
     53                            'official_builds/%s/%s/%s')
     54 
     55 # Google storage location (no public web URL's), example:
     56 # gs://chrome-archive/30/30.0.1595.0/precise32bit/chrome-precise32bit.zip
     57 CHROME_GS_URL_FMT = ('gs://chrome-archive/%s/%s/%s/%s')
     58 
     59 
     60 class BuildUpdater(object):
     61   _PLATFORM_FILES_MAP = {
     62       'Win': [
     63           'chrome-win32.zip',
     64           'chrome-win32-syms.zip',
     65       ],
     66       'Mac': [
     67           'chrome-mac.zip',
     68       ],
     69       'Linux': [
     70           'chrome-linux.zip',
     71       ],
     72       'Linux_x64': [
     73           'chrome-linux.zip',
     74       ],
     75   }
     76 
     77   _CHROME_PLATFORM_FILES_MAP = {
     78       'Win': [
     79           'chrome-win32.zip',
     80           'chrome-win32-syms.zip',
     81       ],
     82       'Mac': [
     83           'chrome-mac.zip',
     84       ],
     85       'Linux': [
     86           'chrome-precise32bit.zip',
     87       ],
     88       'Linux_x64': [
     89           'chrome-precise64bit.zip',
     90       ],
     91   }
     92 
     93   # Map of platform names to gs:// Chrome build names.
     94   _BUILD_PLATFORM_MAP = {
     95       'Linux': 'precise32bit',
     96       'Linux_x64': 'precise64bit',
     97       'Win': 'win',
     98       'Mac': 'mac',
     99   }
    100 
    101   _PLATFORM_DEST_MAP = {
    102       'Linux': 'chrome_linux',
    103       'Linux_x64': 'chrome_linux64',
    104       'Win': 'chrome_win',
    105       'Mac': 'chrome_mac',
    106   }
    107 
    108   def __init__(self, options):
    109     self._platforms = options.platforms.split(',')
    110     self._version_or_revision = options.version or int(options.revision)
    111     self._use_official_version = bool(options.version)
    112     self._use_gs = options.use_gs
    113 
    114   @staticmethod
    115   def _GetCmdStatusAndOutput(args, cwd=None, shell=False):
    116     """Executes a subprocess and returns its exit code and output.
    117 
    118     Args:
    119       args: A string or a sequence of program arguments.
    120       cwd: If not None, the subprocess's current directory will be changed to
    121         |cwd| before it's executed.
    122       shell: Whether to execute args as a shell command.
    123 
    124     Returns:
    125       The tuple (exit code, output).
    126     """
    127     logging.info(str(args) + ' ' + (cwd or ''))
    128     p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
    129                          stderr=subprocess.PIPE, shell=shell)
    130     stdout, stderr = p.communicate()
    131     exit_code = p.returncode
    132     if stderr:
    133       logging.critical(stderr)
    134     logging.info(stdout)
    135     return (exit_code, stdout)
    136 
    137   def _GetBuildUrl(self, platform, version_or_revision, filename):
    138     """Returns the URL for fetching one file.
    139 
    140     Args:
    141       platform: Platform name, must be a key in |self._BUILD_PLATFORM_MAP|.
    142       version_or_revision: Either an SVN revision, e.g. 234567, or a Chrome
    143           version number, e.g. 30.0.1600.1.
    144       filename: Name of the file to fetch.
    145 
    146     Returns:
    147       The URL for fetching a file. This may be a GS or HTTP URL.
    148     """
    149     if self._use_official_version:
    150       # Chrome Google storage bucket.
    151       version = version_or_revision
    152       if self._use_gs:
    153         release = version[:version.find('.')]
    154         return (CHROME_GS_URL_FMT % (
    155             release,
    156             version,
    157             self._BUILD_PLATFORM_MAP[platform],
    158             filename))
    159       # Chrome internal archive.
    160       return (CHROME_INTERNAL_URL_FMT % (
    161           version,
    162           self._BUILD_PLATFORM_MAP[platform],
    163           filename))
    164     # Chromium archive.
    165     revision = version_or_revision
    166     return CHROMIUM_URL_FMT % (urllib.quote_plus(platform), revision, filename)
    167 
    168   def _FindBuildVersionOrRevision(
    169       self, platform, version_or_revision, filename):
    170     """Searches for a version or revision where a filename can be found.
    171 
    172     Args:
    173       platform: Platform name.
    174       version_or_revision: Either Chrome version or Chromium revision.
    175       filename: Filename to look for.
    176 
    177     Returns:
    178       A version or revision where the file could be found, or None.
    179     """
    180     # TODO(shadi): Iterate over official versions to find a valid one.
    181     if self._use_official_version:
    182       version = version_or_revision
    183       return (version
    184               if self._DoesBuildExist(platform, version, filename) else None)
    185 
    186     revision = version_or_revision
    187     MAX_REVISIONS_PER_BUILD = 100
    188     for revision_guess in xrange(revision, revision + MAX_REVISIONS_PER_BUILD):
    189       if self._DoesBuildExist(platform, revision_guess, filename):
    190         return revision_guess
    191       else:
    192         time.sleep(.1)
    193     return None
    194 
    195   def _DoesBuildExist(self, platform, version, filename):
    196     """Checks whether a file can be found for the given Chrome version.
    197 
    198     Args:
    199       platform: Platform name.
    200       version: Chrome version number, e.g. 30.0.1600.1.
    201       filename: Filename to look for.
    202 
    203     Returns:
    204       True if the file could be found, False otherwise.
    205     """
    206     url = self._GetBuildUrl(platform, version, filename)
    207     if self._use_gs:
    208       return self._DoesGSFileExist(url)
    209 
    210     request = urllib2.Request(url)
    211     request.get_method = lambda: 'HEAD'
    212     try:
    213       urllib2.urlopen(request)
    214       return True
    215     except urllib2.HTTPError, err:
    216       if err.code == 404:
    217         return False
    218 
    219   def _DoesGSFileExist(self, gs_file_name):
    220     """Returns True if the GS file can be found, False otherwise."""
    221     exit_code = BuildUpdater._GetCmdStatusAndOutput(
    222         ['gsutil', 'ls', gs_file_name])[0]
    223     return not exit_code
    224 
    225   def _GetPlatformFiles(self, platform):
    226     """Returns a list of filenames to fetch for the given platform."""
    227     if self._use_official_version:
    228       return BuildUpdater._CHROME_PLATFORM_FILES_MAP[platform]
    229     return BuildUpdater._PLATFORM_FILES_MAP[platform]
    230 
    231   def _DownloadBuilds(self):
    232     for platform in self._platforms:
    233       for filename in self._GetPlatformFiles(platform):
    234         output = os.path.join('dl', platform,
    235                               '%s_%s_%s' % (platform,
    236                                             self._version_or_revision,
    237                                             filename))
    238         if os.path.exists(output):
    239           logging.info('%s alread exists, skipping download', output)
    240           continue
    241         version_or_revision = self._FindBuildVersionOrRevision(
    242             platform, self._version_or_revision, filename)
    243         if not version_or_revision:
    244           logging.critical('Failed to find %s build for r%s\n', platform,
    245                            self._version_or_revision)
    246           sys.exit(1)
    247         dirname = os.path.dirname(output)
    248         if dirname and not os.path.exists(dirname):
    249           os.makedirs(dirname)
    250         url = self._GetBuildUrl(platform, version_or_revision, filename)
    251         self._DownloadFile(url, output)
    252 
    253   def _DownloadFile(self, url, output):
    254     logging.info('Downloading %s, saving to %s', url, output)
    255     if self._use_official_version and self._use_gs:
    256       BuildUpdater._GetCmdStatusAndOutput(['gsutil', 'cp', url, output])
    257     else:
    258       response = urllib2.urlopen(url)
    259       with file(output, 'wb') as f:
    260         f.write(response.read())
    261 
    262   def _FetchSvnRepos(self):
    263     if not os.path.exists('reference_builds'):
    264       os.makedirs('reference_builds')
    265     BuildUpdater._GetCmdStatusAndOutput(
    266         ['gclient', 'config',
    267          'svn://svn.chromium.org/chrome/trunk/deps/reference_builds'],
    268         'reference_builds')
    269     BuildUpdater._GetCmdStatusAndOutput(
    270         ['gclient', 'sync'], 'reference_builds')
    271 
    272   def _UnzipFile(self, dl_file, dest_dir):
    273     """Unzips a file if it is a zip file.
    274 
    275     Args:
    276       dl_file: The downloaded file to unzip.
    277       dest_dir: The destination directory to unzip to.
    278 
    279     Returns:
    280       True if the file was unzipped. False if it wasn't a zip file.
    281     """
    282     if not zipfile.is_zipfile(dl_file):
    283       return False
    284     logging.info('Opening %s', dl_file)
    285     with zipfile.ZipFile(dl_file, 'r') as z:
    286       for content in z.namelist():
    287         dest = os.path.join(dest_dir, content[content.find('/')+1:])
    288         # Create dest parent dir if it does not exist.
    289         if not os.path.isdir(os.path.dirname(dest)):
    290           os.makedirs(os.path.dirname(dest))
    291         # If dest is just a dir listing, do nothing.
    292         if not os.path.basename(dest):
    293           continue
    294         if not os.path.isdir(os.path.dirname(dest)):
    295           os.makedirs(os.path.dirname(dest))
    296         with z.open(content) as unzipped_content:
    297           logging.info('Extracting %s to %s (%s)', content, dest, dl_file)
    298           with file(dest, 'wb') as dest_file:
    299             dest_file.write(unzipped_content.read())
    300           permissions = z.getinfo(content).external_attr >> 16
    301           if permissions:
    302             os.chmod(dest, permissions)
    303     return True
    304 
    305   def _ClearDir(self, dir):
    306     """Clears all files in |dir| except for hidden files and folders."""
    307     for root, dirs, files in os.walk(dir):
    308       # Skip hidden files and folders (like .svn and .git).
    309       files = [f for f in files if f[0] != '.']
    310       dirs[:] = [d for d in dirs if d[0] != '.']
    311 
    312       for f in files:
    313         os.remove(os.path.join(root, f))
    314 
    315   def _ExtractBuilds(self):
    316     for platform in self._platforms:
    317       if os.path.exists('tmp_unzip'):
    318         os.path.unlink('tmp_unzip')
    319       dest_dir = os.path.join('reference_builds', 'reference_builds',
    320                               BuildUpdater._PLATFORM_DEST_MAP[platform])
    321       self._ClearDir(dest_dir)
    322       for root, _, dl_files in os.walk(os.path.join('dl', platform)):
    323         for dl_file in dl_files:
    324           dl_file = os.path.join(root, dl_file)
    325           if not self._UnzipFile(dl_file, dest_dir):
    326             logging.info('Copying %s to %s', dl_file, dest_dir)
    327             shutil.copy(dl_file, dest_dir)
    328 
    329   def _SvnAddAndRemove(self):
    330     svn_dir = os.path.join('reference_builds', 'reference_builds')
    331     # List all changes without ignoring any files.
    332     stat = BuildUpdater._GetCmdStatusAndOutput(['svn', 'stat', '--no-ignore'],
    333                                                svn_dir)[1]
    334     for line in stat.splitlines():
    335       action, filename = line.split(None, 1)
    336       # Add new and ignored files.
    337       if action == '?' or action == 'I':
    338         BuildUpdater._GetCmdStatusAndOutput(
    339             ['svn', 'add', filename], svn_dir)
    340       elif action == '!':
    341         BuildUpdater._GetCmdStatusAndOutput(
    342             ['svn', 'delete', filename], svn_dir)
    343       filepath = os.path.join(svn_dir, filename)
    344       if not os.path.isdir(filepath) and os.access(filepath, os.X_OK):
    345         BuildUpdater._GetCmdStatusAndOutput(
    346             ['svn', 'propset', 'svn:executable', 'true', filename], svn_dir)
    347 
    348   def DownloadAndUpdateBuilds(self):
    349     self._DownloadBuilds()
    350     self._FetchSvnRepos()
    351     self._ExtractBuilds()
    352     self._SvnAddAndRemove()
    353 
    354 
    355 def ParseOptions(argv):
    356   parser = optparse.OptionParser()
    357   usage = 'usage: %prog <options>'
    358   parser.set_usage(usage)
    359   parser.add_option('-v', dest='version',
    360                     help='Chrome official version to pick up '
    361                          '(e.g. 30.0.1600.1).')
    362   parser.add_option('--gs', dest='use_gs', action='store_true', default=False,
    363                     help='Use Google storage for official builds. Used with -b '
    364                          'option. Default is false (i.e. use internal storage.')
    365   parser.add_option('-p', dest='platforms',
    366                     default='Win,Mac,Linux,Linux_x64',
    367                     help='Comma separated list of platforms to download '
    368                          '(as defined by the chromium builders).')
    369   parser.add_option('-r', dest='revision',
    370                     help='Chromium revision to pick up (e.g. 234567).')
    371 
    372   (options, _) = parser.parse_args(argv)
    373   if not options.revision and not options.version:
    374     logging.critical('Must specify either -r or -v.\n')
    375     sys.exit(1)
    376   if options.revision and options.version:
    377     logging.critical('Must specify either -r or -v but not both.\n')
    378     sys.exit(1)
    379   if options.use_gs and not options.version:
    380     logging.critical('Can only use --gs with -v option.\n')
    381     sys.exit(1)
    382 
    383   return options
    384 
    385 
    386 def main(argv):
    387   logging.getLogger().setLevel(logging.DEBUG)
    388   options = ParseOptions(argv)
    389   b = BuildUpdater(options)
    390   b.DownloadAndUpdateBuilds()
    391   logging.info('Successfully updated reference builds. Move to '
    392                'reference_builds/reference_builds and make a change with gcl.')
    393 
    394 if __name__ == '__main__':
    395   sys.exit(main(sys.argv))
    396