Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(100)

Side by Side Diff: build/get_syzygy_binaries.py

Issue 1236623002: Make get_syzygy_binaries.py resistant to cloud failures. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Cleanup. Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved. 2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """A utility script for downloading versioned Syzygy binaries.""" 6 """A utility script for downloading versioned Syzygy binaries."""
7 7
8 import cStringIO
9 import hashlib 8 import hashlib
10 import errno 9 import errno
11 import json 10 import json
12 import logging 11 import logging
13 import optparse 12 import optparse
14 import os 13 import os
15 import re 14 import re
16 import shutil 15 import shutil
17 import stat 16 import stat
18 import sys 17 import sys
19 import subprocess 18 import subprocess
20 import urllib2 19 import tempfile
20 import time
21 import zipfile 21 import zipfile
22 22
23 23
24 _LOGGER = logging.getLogger(os.path.basename(__file__)) 24 _LOGGER = logging.getLogger(os.path.basename(__file__))
25 25
26 # The URL where official builds are archived. 26 # The relative path where official builds are archived in their GS bucket.
27 _SYZYGY_ARCHIVE_URL = ('https://syzygy-archive.commondatastorage.googleapis.com' 27 _SYZYGY_ARCHIVE_PATH = ('/builds/official/%(revision)s')
28 '/builds/official/%(revision)s')
29 28
30 # A JSON file containing the state of the download directory. If this file and 29 # A JSON file containing the state of the download directory. If this file and
31 # directory state do not agree, then the binaries will be downloaded and 30 # directory state do not agree, then the binaries will be downloaded and
32 # installed again. 31 # installed again.
33 _STATE = '.state' 32 _STATE = '.state'
34 33
35 # This matches an integer (an SVN revision number) or a SHA1 value (a GIT hash). 34 # This matches an integer (an SVN revision number) or a SHA1 value (a GIT hash).
36 # The archive exclusively uses lowercase GIT hashes. 35 # The archive exclusively uses lowercase GIT hashes.
37 _REVISION_RE = re.compile('^(?:\d+|[a-f0-9]{40})$') 36 _REVISION_RE = re.compile('^(?:\d+|[a-f0-9]{40})$')
38 37
39 # This matches an MD5 hash. 38 # This matches an MD5 hash.
40 _MD5_RE = re.compile('^[a-f0-9]{32}$') 39 _MD5_RE = re.compile('^[a-f0-9]{32}$')
41 40
42 # List of reources to be downloaded and installed. These are tuples with the 41 # List of reources to be downloaded and installed. These are tuples with the
43 # following format: 42 # following format:
44 # (basename, logging name, relative installation path, extraction filter) 43 # (basename, logging name, relative installation path, extraction filter)
45 _RESOURCES = [ 44 _RESOURCES = [
46 ('benchmark.zip', 'benchmark', '', None), 45 ('benchmark.zip', 'benchmark', '', None),
47 ('binaries.zip', 'binaries', 'exe', None), 46 ('binaries.zip', 'binaries', 'exe', None),
48 ('symbols.zip', 'symbols', 'exe', 47 ('symbols.zip', 'symbols', 'exe',
49 lambda x: x.filename.endswith('.dll.pdb'))] 48 lambda x: x.filename.endswith('.dll.pdb'))]
50 49
51 50
52 def _Shell(*cmd, **kw):
53 """Runs |cmd|, returns the results from Popen(cmd).communicate()."""
54 _LOGGER.debug('Executing %s.', cmd)
55 prog = subprocess.Popen(cmd, shell=True, **kw)
56
57 stdout, stderr = prog.communicate()
58 if prog.returncode != 0:
59 raise RuntimeError('Command "%s" returned %d.' % (cmd, prog.returncode))
60 return (stdout, stderr)
61
62
63 def _LoadState(output_dir): 51 def _LoadState(output_dir):
64 """Loads the contents of the state file for a given |output_dir|, returning 52 """Loads the contents of the state file for a given |output_dir|, returning
65 None if it doesn't exist. 53 None if it doesn't exist.
66 """ 54 """
67 path = os.path.join(output_dir, _STATE) 55 path = os.path.join(output_dir, _STATE)
68 if not os.path.exists(path): 56 if not os.path.exists(path):
69 _LOGGER.debug('No state file found.') 57 _LOGGER.debug('No state file found.')
70 return None 58 return None
71 with open(path, 'rb') as f: 59 with open(path, 'rb') as f:
72 _LOGGER.debug('Reading state file: %s', path) 60 _LOGGER.debug('Reading state file: %s', path)
(...skipping 168 matching lines...) Expand 10 before | Expand all | Expand 10 after
241 dirs = sorted(dirs.keys(), key=lambda x: len(x), reverse=True) 229 dirs = sorted(dirs.keys(), key=lambda x: len(x), reverse=True)
242 for p in dirs: 230 for p in dirs:
243 if os.path.exists(p) and _DirIsEmpty(p): 231 if os.path.exists(p) and _DirIsEmpty(p):
244 _LOGGER.debug('Deleting empty directory "%s".', p) 232 _LOGGER.debug('Deleting empty directory "%s".', p)
245 if not dry_run: 233 if not dry_run:
246 _RmTree(p) 234 _RmTree(p)
247 235
248 return deleted 236 return deleted
249 237
250 238
251 def _Download(url): 239 def _FindGsUtil():
252 """Downloads the given URL and returns the contents as a string.""" 240 """Looks for depot_tools and returns the absolute path to gsutil.py."""
253 response = urllib2.urlopen(url) 241 for path in os.environ['PATH'].split(';'):
Nico 2015/07/14 17:58:41 os.pathsep instead of ;
chrisha 2015/07/14 18:05:06 Done.
254 if response.code != 200: 242 path = os.path.abspath(path)
255 raise RuntimeError('Failed to download "%s".' % url) 243 git_cl = os.path.join(path, 'git_cl.py')
256 return response.read() 244 gs_util = os.path.join(path, 'gsutil.py')
245 if os.path.exists(git_cl) and os.path.exists(gs_util):
246 return gs_util
247 return None
248
249
250 def _GsUtil(*cmd):
251 """Runs the given command in gsutil with exponential backoff and retries."""
252 gs_util = _FindGsUtil()
253 cmd = [sys.executable, gs_util] + list(cmd)
254
255 retries = 3
256 timeout = 4 # Seconds.
257 while True:
258 _LOGGER.debug('Running %s', cmd)
259 prog = subprocess.Popen(cmd, shell=True)
260 prog.communicate()
261
262 # Stop retrying on success.
263 if prog.returncode == 0:
264 return
265
266 # Raise a permanent failure if retries have been exhausted.
267 if retries == 0:
268 raise RuntimeError('Command "%s" returned %d.' % (cmd, prog.returncode))
269
270 _LOGGER.debug('Sleeping %d seconds and trying again.', timeout)
271 time.sleep(timeout)
272 retries -= 1
273 timeout *= 2
274
275
276 def _Download(resource):
277 """Downloads the given GS resource to a temporary file, returning its path."""
278 tmp = tempfile.mkstemp(suffix='syzygy_archive')
279 os.close(tmp[0])
280 url = 'gs://syzygy-archive' + resource
281 _GsUtil('cp', url, tmp[1])
282 return tmp[1]
257 283
258 284
259 def _InstallBinaries(options, deleted={}): 285 def _InstallBinaries(options, deleted={}):
260 """Installs Syzygy binaries. This assumes that the output directory has 286 """Installs Syzygy binaries. This assumes that the output directory has
261 already been cleaned, as it will refuse to overwrite existing files.""" 287 already been cleaned, as it will refuse to overwrite existing files."""
262 contents = {} 288 contents = {}
263 state = { 'revision': options.revision, 'contents': contents } 289 state = { 'revision': options.revision, 'contents': contents }
264 archive_url = _SYZYGY_ARCHIVE_URL % { 'revision': options.revision } 290 archive_path = _SYZYGY_ARCHIVE_PATH % { 'revision': options.revision }
265 if options.resources: 291 if options.resources:
266 resources = [(resource, resource, '', None) 292 resources = [(resource, resource, '', None)
267 for resource in options.resources] 293 for resource in options.resources]
268 else: 294 else:
269 resources = _RESOURCES 295 resources = _RESOURCES
270 for (base, name, subdir, filt) in resources: 296 for (base, name, subdir, filt) in resources:
271 # Create the output directory if it doesn't exist. 297 # Create the output directory if it doesn't exist.
272 fulldir = os.path.join(options.output_dir, subdir) 298 fulldir = os.path.join(options.output_dir, subdir)
273 if os.path.isfile(fulldir): 299 if os.path.isfile(fulldir):
274 raise Exception('File exists where a directory needs to be created: %s' % 300 raise Exception('File exists where a directory needs to be created: %s' %
275 fulldir) 301 fulldir)
276 if not os.path.exists(fulldir): 302 if not os.path.exists(fulldir):
277 _LOGGER.debug('Creating directory: %s', fulldir) 303 _LOGGER.debug('Creating directory: %s', fulldir)
278 if not options.dry_run: 304 if not options.dry_run:
279 os.makedirs(fulldir) 305 os.makedirs(fulldir)
280 306
281 # Download the archive. 307 # Download and read the archive.
282 url = archive_url + '/' + base 308 resource = archive_path + '/' + base
283 _LOGGER.debug('Retrieving %s archive at "%s".', name, url) 309 _LOGGER.debug('Retrieving %s archive at "%s".', name, resource)
284 data = _Download(url) 310 path = _Download(resource)
285 311
286 _LOGGER.debug('Unzipping %s archive.', name) 312 _LOGGER.debug('Unzipping %s archive.', name)
287 archive = zipfile.ZipFile(cStringIO.StringIO(data)) 313 archive = zipfile.ZipFile(open(path, 'rb'))
Nico 2015/07/14 17:58:41 nit: `with open(path, 'rb') as path:`, then you do
chrisha 2015/07/14 18:05:06 Done.
288 for entry in archive.infolist(): 314 for entry in archive.infolist():
289 if not filt or filt(entry): 315 if not filt or filt(entry):
290 fullpath = os.path.normpath(os.path.join(fulldir, entry.filename)) 316 fullpath = os.path.normpath(os.path.join(fulldir, entry.filename))
291 relpath = os.path.relpath(fullpath, options.output_dir) 317 relpath = os.path.relpath(fullpath, options.output_dir)
292 if os.path.exists(fullpath): 318 if os.path.exists(fullpath):
293 # If in a dry-run take into account the fact that the file *would* 319 # If in a dry-run take into account the fact that the file *would*
294 # have been deleted. 320 # have been deleted.
295 if options.dry_run and relpath in deleted: 321 if options.dry_run and relpath in deleted:
296 pass 322 pass
297 else: 323 else:
298 raise Exception('Path already exists: %s' % fullpath) 324 raise Exception('Path already exists: %s' % fullpath)
299 325
300 # Extract the file and update the state dictionary. 326 # Extract the file and update the state dictionary.
301 _LOGGER.debug('Extracting "%s".', fullpath) 327 _LOGGER.debug('Extracting "%s".', fullpath)
302 if not options.dry_run: 328 if not options.dry_run:
303 archive.extract(entry.filename, fulldir) 329 archive.extract(entry.filename, fulldir)
304 md5 = _Md5(fullpath) 330 md5 = _Md5(fullpath)
305 contents[relpath] = md5 331 contents[relpath] = md5
306 if sys.platform == 'cygwin': 332 if sys.platform == 'cygwin':
307 os.chmod(fullpath, os.stat(fullpath).st_mode | stat.S_IXUSR) 333 os.chmod(fullpath, os.stat(fullpath).st_mode | stat.S_IXUSR)
308 334
335 _LOGGER.debug('Removing temporary file "%s".', path)
336 archive.close()
337 os.remove(path)
338
309 return state 339 return state
310 340
311 341
312 def _ParseCommandLine(): 342 def _ParseCommandLine():
313 """Parses the command-line and returns an options structure.""" 343 """Parses the command-line and returns an options structure."""
314 option_parser = optparse.OptionParser() 344 option_parser = optparse.OptionParser()
315 option_parser.add_option('--dry-run', action='store_true', default=False, 345 option_parser.add_option('--dry-run', action='store_true', default=False,
316 help='If true then will simply list actions that would be performed.') 346 help='If true then will simply list actions that would be performed.')
317 option_parser.add_option('--force', action='store_true', default=False, 347 option_parser.add_option('--force', action='store_true', default=False,
318 help='Force an installation even if the binaries are up to date.') 348 help='Force an installation even if the binaries are up to date.')
349 option_parser.add_option('--no-cleanup', action='store_true', default=False,
350 help='Allow installation on non-Windows platforms, and skip the forced '
351 'cleanup step.')
319 option_parser.add_option('--output-dir', type='string', 352 option_parser.add_option('--output-dir', type='string',
320 help='The path where the binaries will be replaced. Existing binaries ' 353 help='The path where the binaries will be replaced. Existing binaries '
321 'will only be overwritten if not up to date.') 354 'will only be overwritten if not up to date.')
322 option_parser.add_option('--overwrite', action='store_true', default=False, 355 option_parser.add_option('--overwrite', action='store_true', default=False,
323 help='If specified then the installation will happily delete and rewrite ' 356 help='If specified then the installation will happily delete and rewrite '
324 'the entire output directory, blasting any local changes.') 357 'the entire output directory, blasting any local changes.')
325 option_parser.add_option('--revision', type='string', 358 option_parser.add_option('--revision', type='string',
326 help='The SVN revision or GIT hash associated with the required version.') 359 help='The SVN revision or GIT hash associated with the required version.')
327 option_parser.add_option('--revision-file', type='string', 360 option_parser.add_option('--revision-file', type='string',
328 help='A text file containing an SVN revision or GIT hash.') 361 help='A text file containing an SVN revision or GIT hash.')
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after
401 options = _ParseCommandLine() 434 options = _ParseCommandLine()
402 435
403 if options.dry_run: 436 if options.dry_run:
404 _LOGGER.debug('Performing a dry-run.') 437 _LOGGER.debug('Performing a dry-run.')
405 438
406 # We only care about Windows platforms, as the Syzygy binaries aren't used 439 # We only care about Windows platforms, as the Syzygy binaries aren't used
407 # elsewhere. However, there was a short period of time where this script 440 # elsewhere. However, there was a short period of time where this script
408 # wasn't gated on OS types, and those OSes downloaded and installed binaries. 441 # wasn't gated on OS types, and those OSes downloaded and installed binaries.
409 # This will cleanup orphaned files on those operating systems. 442 # This will cleanup orphaned files on those operating systems.
410 if sys.platform not in ('win32', 'cygwin'): 443 if sys.platform not in ('win32', 'cygwin'):
411 return _RemoveOrphanedFiles(options) 444 if options.no_cleanup:
445 _LOGGER.debug('Skipping usual cleanup for non-Windows platforms.')
446 else:
447 return _RemoveOrphanedFiles(options)
412 448
413 # Load the current installation state, and validate it against the 449 # Load the current installation state, and validate it against the
414 # requested installation. 450 # requested installation.
415 state, is_consistent = _GetCurrentState(options.revision, options.output_dir) 451 state, is_consistent = _GetCurrentState(options.revision, options.output_dir)
416 452
417 # Decide whether or not an install is necessary. 453 # Decide whether or not an install is necessary.
418 if options.force: 454 if options.force:
419 _LOGGER.debug('Forcing reinstall of binaries.') 455 _LOGGER.debug('Forcing reinstall of binaries.')
420 elif is_consistent: 456 elif is_consistent:
421 # Avoid doing any work if the contents of the directory are consistent. 457 # Avoid doing any work if the contents of the directory are consistent.
(...skipping 20 matching lines...) Expand all
442 # Install the new binaries. In a dry-run this will actually download the 478 # Install the new binaries. In a dry-run this will actually download the
443 # archives, but it won't write anything to disk. 479 # archives, but it won't write anything to disk.
444 state = _InstallBinaries(options, deleted) 480 state = _InstallBinaries(options, deleted)
445 481
446 # Build and save the state for the directory. 482 # Build and save the state for the directory.
447 _SaveState(options.output_dir, state, options.dry_run) 483 _SaveState(options.output_dir, state, options.dry_run)
448 484
449 485
450 if __name__ == '__main__': 486 if __name__ == '__main__':
451 main() 487 main()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698