| Index: build/get_syzygy_binaries.py
|
| diff --git a/build/get_syzygy_binaries.py b/build/get_syzygy_binaries.py
|
| index 2577c7cb7fde733b974ebf66e7c11fe1cd9777ba..2a6469ffaa6de0f3184085d54c2c6626d246c153 100755
|
| --- a/build/get_syzygy_binaries.py
|
| +++ b/build/get_syzygy_binaries.py
|
| @@ -5,7 +5,6 @@
|
|
|
| """A utility script for downloading versioned Syzygy binaries."""
|
|
|
| -import cStringIO
|
| import hashlib
|
| import errno
|
| import json
|
| @@ -17,15 +16,15 @@ import shutil
|
| import stat
|
| import sys
|
| import subprocess
|
| -import urllib2
|
| +import tempfile
|
| +import time
|
| import zipfile
|
|
|
|
|
| _LOGGER = logging.getLogger(os.path.basename(__file__))
|
|
|
| -# The URL where official builds are archived.
|
| -_SYZYGY_ARCHIVE_URL = ('https://syzygy-archive.commondatastorage.googleapis.com'
|
| - '/builds/official/%(revision)s')
|
| +# The relative path where official builds are archived in their GS bucket.
|
| +_SYZYGY_ARCHIVE_PATH = ('/builds/official/%(revision)s')
|
|
|
| # A JSON file containing the state of the download directory. If this file and
|
| # directory state do not agree, then the binaries will be downloaded and
|
| @@ -49,17 +48,6 @@ _RESOURCES = [
|
| lambda x: x.filename.endswith('.dll.pdb'))]
|
|
|
|
|
| -def _Shell(*cmd, **kw):
|
| - """Runs |cmd|, returns the results from Popen(cmd).communicate()."""
|
| - _LOGGER.debug('Executing %s.', cmd)
|
| - prog = subprocess.Popen(cmd, shell=True, **kw)
|
| -
|
| - stdout, stderr = prog.communicate()
|
| - if prog.returncode != 0:
|
| - raise RuntimeError('Command "%s" returned %d.' % (cmd, prog.returncode))
|
| - return (stdout, stderr)
|
| -
|
| -
|
| def _LoadState(output_dir):
|
| """Loads the contents of the state file for a given |output_dir|, returning
|
| None if it doesn't exist.
|
| @@ -248,12 +236,50 @@ def _CleanState(output_dir, state, dry_run=False):
|
| return deleted
|
|
|
|
|
| -def _Download(url):
|
| - """Downloads the given URL and returns the contents as a string."""
|
| - response = urllib2.urlopen(url)
|
| - if response.code != 200:
|
| - raise RuntimeError('Failed to download "%s".' % url)
|
| - return response.read()
|
| +def _FindGsUtil():
|
| + """Looks for depot_tools and returns the absolute path to gsutil.py."""
|
| + for path in os.environ['PATH'].split(os.pathsep):
|
| + path = os.path.abspath(path)
|
| + git_cl = os.path.join(path, 'git_cl.py')
|
| + gs_util = os.path.join(path, 'gsutil.py')
|
| + if os.path.exists(git_cl) and os.path.exists(gs_util):
|
| + return gs_util
|
| + return None
|
| +
|
| +
|
| +def _GsUtil(*cmd):
|
| + """Runs the given command in gsutil with exponential backoff and retries."""
|
| + gs_util = _FindGsUtil()
|
| + cmd = [sys.executable, gs_util] + list(cmd)
|
| +
|
| + retries = 3
|
| + timeout = 4 # Seconds.
|
| + while True:
|
| + _LOGGER.debug('Running %s', cmd)
|
| + prog = subprocess.Popen(cmd, shell=True)
|
| + prog.communicate()
|
| +
|
| + # Stop retrying on success.
|
| + if prog.returncode == 0:
|
| + return
|
| +
|
| + # Raise a permanent failure if retries have been exhausted.
|
| + if retries == 0:
|
| + raise RuntimeError('Command "%s" returned %d.' % (cmd, prog.returncode))
|
| +
|
| + _LOGGER.debug('Sleeping %d seconds and trying again.', timeout)
|
| + time.sleep(timeout)
|
| + retries -= 1
|
| + timeout *= 2
|
| +
|
| +
|
| +def _Download(resource):
|
| + """Downloads the given GS resource to a temporary file, returning its path."""
|
| + tmp = tempfile.mkstemp(suffix='syzygy_archive')
|
| + os.close(tmp[0])
|
| + url = 'gs://syzygy-archive' + resource
|
| + _GsUtil('cp', url, tmp[1])
|
| + return tmp[1]
|
|
|
|
|
| def _InstallBinaries(options, deleted={}):
|
| @@ -261,7 +287,7 @@ def _InstallBinaries(options, deleted={}):
|
| already been cleaned, as it will refuse to overwrite existing files."""
|
| contents = {}
|
| state = { 'revision': options.revision, 'contents': contents }
|
| - archive_url = _SYZYGY_ARCHIVE_URL % { 'revision': options.revision }
|
| + archive_path = _SYZYGY_ARCHIVE_PATH % { 'revision': options.revision }
|
| if options.resources:
|
| resources = [(resource, resource, '', None)
|
| for resource in options.resources]
|
| @@ -278,33 +304,37 @@ def _InstallBinaries(options, deleted={}):
|
| if not options.dry_run:
|
| os.makedirs(fulldir)
|
|
|
| - # Download the archive.
|
| - url = archive_url + '/' + base
|
| - _LOGGER.debug('Retrieving %s archive at "%s".', name, url)
|
| - data = _Download(url)
|
| + # Download and read the archive.
|
| + resource = archive_path + '/' + base
|
| + _LOGGER.debug('Retrieving %s archive at "%s".', name, resource)
|
| + path = _Download(resource)
|
|
|
| _LOGGER.debug('Unzipping %s archive.', name)
|
| - archive = zipfile.ZipFile(cStringIO.StringIO(data))
|
| - for entry in archive.infolist():
|
| - if not filt or filt(entry):
|
| - fullpath = os.path.normpath(os.path.join(fulldir, entry.filename))
|
| - relpath = os.path.relpath(fullpath, options.output_dir)
|
| - if os.path.exists(fullpath):
|
| - # If in a dry-run take into account the fact that the file *would*
|
| - # have been deleted.
|
| - if options.dry_run and relpath in deleted:
|
| - pass
|
| - else:
|
| - raise Exception('Path already exists: %s' % fullpath)
|
| -
|
| - # Extract the file and update the state dictionary.
|
| - _LOGGER.debug('Extracting "%s".', fullpath)
|
| - if not options.dry_run:
|
| - archive.extract(entry.filename, fulldir)
|
| - md5 = _Md5(fullpath)
|
| - contents[relpath] = md5
|
| - if sys.platform == 'cygwin':
|
| - os.chmod(fullpath, os.stat(fullpath).st_mode | stat.S_IXUSR)
|
| + with open(path, 'rb') as data:
|
| + archive = zipfile.ZipFile(data)
|
| + for entry in archive.infolist():
|
| + if not filt or filt(entry):
|
| + fullpath = os.path.normpath(os.path.join(fulldir, entry.filename))
|
| + relpath = os.path.relpath(fullpath, options.output_dir)
|
| + if os.path.exists(fullpath):
|
| + # If in a dry-run take into account the fact that the file *would*
|
| + # have been deleted.
|
| + if options.dry_run and relpath in deleted:
|
| + pass
|
| + else:
|
| + raise Exception('Path already exists: %s' % fullpath)
|
| +
|
| + # Extract the file and update the state dictionary.
|
| + _LOGGER.debug('Extracting "%s".', fullpath)
|
| + if not options.dry_run:
|
| + archive.extract(entry.filename, fulldir)
|
| + md5 = _Md5(fullpath)
|
| + contents[relpath] = md5
|
| + if sys.platform == 'cygwin':
|
| + os.chmod(fullpath, os.stat(fullpath).st_mode | stat.S_IXUSR)
|
| +
|
| + _LOGGER.debug('Removing temporary file "%s".', path)
|
| + os.remove(path)
|
|
|
| return state
|
|
|
| @@ -316,6 +346,9 @@ def _ParseCommandLine():
|
| help='If true then will simply list actions that would be performed.')
|
| option_parser.add_option('--force', action='store_true', default=False,
|
| help='Force an installation even if the binaries are up to date.')
|
| + option_parser.add_option('--no-cleanup', action='store_true', default=False,
|
| + help='Allow installation on non-Windows platforms, and skip the forced '
|
| + 'cleanup step.')
|
| option_parser.add_option('--output-dir', type='string',
|
| help='The path where the binaries will be replaced. Existing binaries '
|
| 'will only be overwritten if not up to date.')
|
| @@ -408,7 +441,10 @@ def main():
|
| # wasn't gated on OS types, and those OSes downloaded and installed binaries.
|
| # This will cleanup orphaned files on those operating systems.
|
| if sys.platform not in ('win32', 'cygwin'):
|
| - return _RemoveOrphanedFiles(options)
|
| + if options.no_cleanup:
|
| + _LOGGER.debug('Skipping usual cleanup for non-Windows platforms.')
|
| + else:
|
| + return _RemoveOrphanedFiles(options)
|
|
|
| # Load the current installation state, and validate it against the
|
| # requested installation.
|
|
|