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 |