| Index: platform_tools/android/bin/download_utils.py
|
| diff --git a/platform_tools/android/bin/download_utils.py b/platform_tools/android/bin/download_utils.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..298ba9a863aa8c68badb2ab8a1fe0d3ce8c955a0
|
| --- /dev/null
|
| +++ b/platform_tools/android/bin/download_utils.py
|
| @@ -0,0 +1,323 @@
|
| +#!/usr/bin/python
|
| +# Copyright (c) 2012 The Native Client Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""A library to assist automatically downloading files.
|
| +
|
| +This library is used by scripts that download tarballs, zipfiles, etc. as part
|
| +of the build process.
|
| +"""
|
| +
|
| +import hashlib
|
| +import http_download
|
| +import os.path
|
| +import re
|
| +import shutil
|
| +import sys
|
| +import time
|
| +import urllib2
|
| +
|
| +SOURCE_STAMP = 'SOURCE_URL'
|
| +HASH_STAMP = 'SOURCE_SHA1'
|
| +
|
| +
|
| +# Designed to handle more general inputs than sys.platform because the platform
|
| +# name may come from the command line.
|
| +PLATFORM_COLLAPSE = {
|
| + 'windows': 'windows',
|
| + 'win32': 'windows',
|
| + 'cygwin': 'windows',
|
| + 'linux': 'linux',
|
| + 'linux2': 'linux',
|
| + 'linux3': 'linux',
|
| + 'darwin': 'mac',
|
| + 'mac': 'mac',
|
| +}
|
| +
|
| +ARCH_COLLAPSE = {
|
| + 'i386' : 'x86',
|
| + 'i686' : 'x86',
|
| + 'x86_64': 'x86',
|
| + 'armv7l': 'arm',
|
| +}
|
| +
|
| +
|
| +class HashError(Exception):
|
| + def __init__(self, download_url, expected_hash, actual_hash):
|
| + self.download_url = download_url
|
| + self.expected_hash = expected_hash
|
| + self.actual_hash = actual_hash
|
| +
|
| + def __str__(self):
|
| + return 'Got hash "%s" but expected hash "%s" for "%s"' % (
|
| + self.actual_hash, self.expected_hash, self.download_url)
|
| +
|
| +
|
| +def PlatformName(name=None):
|
| + if name is None:
|
| + name = sys.platform
|
| + return PLATFORM_COLLAPSE[name]
|
| +
|
| +def ArchName(name=None):
|
| + if name is None:
|
| + if PlatformName() == 'windows':
|
| + # TODO(pdox): Figure out how to auto-detect 32-bit vs 64-bit Windows.
|
| + name = 'i386'
|
| + else:
|
| + import platform
|
| + name = platform.machine()
|
| + return ARCH_COLLAPSE[name]
|
| +
|
| +def EnsureFileCanBeWritten(filename):
|
| + directory = os.path.dirname(filename)
|
| + if not os.path.exists(directory):
|
| + os.makedirs(directory)
|
| +
|
| +
|
| +def WriteData(filename, data):
|
| + EnsureFileCanBeWritten(filename)
|
| + f = open(filename, 'wb')
|
| + f.write(data)
|
| + f.close()
|
| +
|
| +
|
| +def WriteDataFromStream(filename, stream, chunk_size, verbose=True):
|
| + EnsureFileCanBeWritten(filename)
|
| + dst = open(filename, 'wb')
|
| + try:
|
| + while True:
|
| + data = stream.read(chunk_size)
|
| + if len(data) == 0:
|
| + break
|
| + dst.write(data)
|
| + if verbose:
|
| + # Indicate that we're still writing.
|
| + sys.stdout.write('.')
|
| + sys.stdout.flush()
|
| + finally:
|
| + if verbose:
|
| + sys.stdout.write('\n')
|
| + dst.close()
|
| +
|
| +
|
| +def DoesStampMatch(stampfile, expected, index):
|
| + try:
|
| + f = open(stampfile, 'r')
|
| + stamp = f.read()
|
| + f.close()
|
| + if stamp.split('\n')[index] == expected:
|
| + return "already up-to-date."
|
| + elif stamp.startswith('manual'):
|
| + return "manual override."
|
| + return False
|
| + except IOError:
|
| + return False
|
| +
|
| +
|
| +def WriteStamp(stampfile, data):
|
| + EnsureFileCanBeWritten(stampfile)
|
| + f = open(stampfile, 'w')
|
| + f.write(data)
|
| + f.close()
|
| +
|
| +
|
| +def StampIsCurrent(path, stamp_name, stamp_contents, min_time=None, index=0):
|
| + stampfile = os.path.join(path, stamp_name)
|
| +
|
| + # Check if the stampfile is older than the minimum last mod time
|
| + if min_time:
|
| + try:
|
| + stamp_time = os.stat(stampfile).st_mtime
|
| + if stamp_time <= min_time:
|
| + return False
|
| + except OSError:
|
| + return False
|
| +
|
| + return DoesStampMatch(stampfile, stamp_contents, index)
|
| +
|
| +
|
| +def WriteSourceStamp(path, url):
|
| + stampfile = os.path.join(path, SOURCE_STAMP)
|
| + WriteStamp(stampfile, url)
|
| +
|
| +def WriteHashStamp(path, hash_val):
|
| + hash_stampfile = os.path.join(path, HASH_STAMP)
|
| + WriteStamp(hash_stampfile, hash_val)
|
| +
|
| +
|
| +def Retry(op, *args):
|
| + # Windows seems to be prone to having commands that delete files or
|
| + # directories fail. We currently do not have a complete understanding why,
|
| + # and as a workaround we simply retry the command a few times.
|
| + # It appears that file locks are hanging around longer than they should. This
|
| + # may be a secondary effect of processes hanging around longer than they
|
| + # should. This may be because when we kill a browser sel_ldr does not exit
|
| + # immediately, etc.
|
| + # Virus checkers can also accidently prevent files from being deleted, but
|
| + # that shouldn't be a problem on the bots.
|
| + if sys.platform in ('win32', 'cygwin'):
|
| + count = 0
|
| + while True:
|
| + try:
|
| + op(*args)
|
| + break
|
| + except Exception:
|
| + sys.stdout.write("FAILED: %s %s\n" % (op.__name__, repr(args)))
|
| + count += 1
|
| + if count < 5:
|
| + sys.stdout.write("RETRY: %s %s\n" % (op.__name__, repr(args)))
|
| + time.sleep(pow(2, count))
|
| + else:
|
| + # Don't mask the exception.
|
| + raise
|
| + else:
|
| + op(*args)
|
| +
|
| +
|
| +def MoveDirCleanly(src, dst):
|
| + RemoveDir(dst)
|
| + MoveDir(src, dst)
|
| +
|
| +
|
| +def MoveDir(src, dst):
|
| + Retry(shutil.move, src, dst)
|
| +
|
| +
|
| +def RemoveDir(path):
|
| + if os.path.exists(path):
|
| + Retry(shutil.rmtree, path)
|
| +
|
| +
|
| +def RemoveFile(path):
|
| + if os.path.exists(path):
|
| + Retry(os.unlink, path)
|
| +
|
| +
|
| +def _HashFileHandle(fh):
|
| + """sha1 of a file like object.
|
| +
|
| + Arguments:
|
| + fh: file handle like object to hash.
|
| + Returns:
|
| + sha1 as a string.
|
| + """
|
| + hasher = hashlib.sha1()
|
| + try:
|
| + while True:
|
| + data = fh.read(4096)
|
| + if not data:
|
| + break
|
| + hasher.update(data)
|
| + finally:
|
| + fh.close()
|
| + return hasher.hexdigest()
|
| +
|
| +
|
| +def HashFile(filename):
|
| + """sha1 a file on disk.
|
| +
|
| + Arguments:
|
| + filename: filename to hash.
|
| + Returns:
|
| + sha1 as a string.
|
| + """
|
| + fh = open(filename, 'rb')
|
| + return _HashFileHandle(fh)
|
| +
|
| +
|
| +def HashUrlByDownloading(url):
|
| + """sha1 the data at an url.
|
| +
|
| + Arguments:
|
| + url: url to download from.
|
| + Returns:
|
| + sha1 of the data at the url.
|
| + """
|
| + try:
|
| + fh = urllib2.urlopen(url)
|
| + except:
|
| + sys.stderr.write("Failed fetching URL: %s\n" % url)
|
| + raise
|
| + return _HashFileHandle(fh)
|
| +
|
| +
|
| +# Attempts to get the SHA1 hash of a file given a URL by looking for
|
| +# an adjacent file with a ".sha1hash" suffix. This saves having to
|
| +# download a large tarball just to get its hash. Otherwise, we fall
|
| +# back to downloading the main file.
|
| +def HashUrl(url):
|
| + hash_url = '%s.sha1hash' % url
|
| + try:
|
| + fh = urllib2.urlopen(hash_url)
|
| + data = fh.read(100)
|
| + fh.close()
|
| + except urllib2.HTTPError, exn:
|
| + if exn.code == 404:
|
| + return HashUrlByDownloading(url)
|
| + raise
|
| + else:
|
| + if not re.match('[0-9a-f]{40}\n?$', data):
|
| + raise AssertionError('Bad SHA1 hash file: %r' % data)
|
| + return data.strip()
|
| +
|
| +
|
| +def SyncURL(url, filename=None, stamp_dir=None, min_time=None,
|
| + hash_val=None, keep=False, verbose=False, stamp_index=0):
|
| + """Synchronize a destination file with a URL
|
| +
|
| + if the URL does not match the URL stamp, then we must re-download it.
|
| +
|
| + Arugments:
|
| + url: the url which will to compare against and download
|
| + filename: the file to create on download
|
| + path: the download path
|
| + stamp_dir: the filename containing the URL stamp to check against
|
| + hash_val: if set, the expected hash which must be matched
|
| + verbose: prints out status as it runs
|
| + stamp_index: index within the stamp file to check.
|
| + Returns:
|
| + True if the file is replaced
|
| + False if the file is not replaced
|
| + Exception:
|
| + HashError: if the hash does not match
|
| + """
|
| +
|
| + assert url and filename
|
| +
|
| + # If we are not keeping the tarball, or we already have it, we can
|
| + # skip downloading it for this reason. If we are keeping it,
|
| + # it must exist.
|
| + if keep:
|
| + tarball_ok = os.path.isfile(filename)
|
| + else:
|
| + tarball_ok = True
|
| +
|
| + # If we don't need the tarball and the stamp_file matches the url, then
|
| + # we must be up to date. If the URL differs but the recorded hash matches
|
| + # the one we'll insist the tarball has, then that's good enough too.
|
| + # TODO(mcgrathr): Download the .sha1sum file first to compare with
|
| + # the cached hash, in case --file-hash options weren't used.
|
| + if tarball_ok and stamp_dir is not None:
|
| + if StampIsCurrent(stamp_dir, SOURCE_STAMP, url, min_time):
|
| + if verbose:
|
| + print '%s is already up to date.' % filename
|
| + return False
|
| + if (hash_val is not None and
|
| + StampIsCurrent(stamp_dir, HASH_STAMP, hash_val, min_time, stamp_index)):
|
| + if verbose:
|
| + print '%s is identical to the up to date file.' % filename
|
| + return False
|
| +
|
| + if verbose:
|
| + print 'Updating %s\n\tfrom %s.' % (filename, url)
|
| + EnsureFileCanBeWritten(filename)
|
| + http_download.HttpDownload(url, filename)
|
| +
|
| + if hash_val:
|
| + tar_hash = HashFile(filename)
|
| + if hash_val != tar_hash:
|
| + raise HashError(actual_hash=tar_hash, expected_hash=hash_val,
|
| + download_url=url)
|
| +
|
| + return True
|
|
|