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