15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Copyright (c) 2012 The Chromium Authors. All rights reserved.
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# found in the LICENSE file.
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)import hashlib
62a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import copy
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import logging
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import os
9c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)import subprocess
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import sys
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import urlparse
125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import urllib2
135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import command_common
152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import download
162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)from sdk_update_common import Error
172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import sdk_update_common
182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)PARENT_DIR = os.path.dirname(SCRIPT_DIR)
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)sys.path.append(PARENT_DIR)
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)try:
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  import cygtar
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)except ImportError:
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Try to find this in the Chromium repo.
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  CHROME_SRC_DIR = os.path.abspath(
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      os.path.join(PARENT_DIR, '..', '..', '..', '..'))
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  sys.path.append(os.path.join(CHROME_SRC_DIR, 'native_client', 'build'))
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  import cygtar
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)RECOMMENDED = 'recommended'
335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)SDK_TOOLS = 'sdk_tools'
345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)HTTP_CONTENT_LENGTH = 'Content-Length'  # HTTP Header field for content length
35c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)DEFAULT_CACHE_SIZE = 512 * 1024 * 1024  # 1/2 Gb cache by default
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class UpdateDelegate(object):
395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def BundleDirectoryExists(self, bundle_name):
405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    raise NotImplementedError()
415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def DownloadToFile(self, url, dest_filename):
435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    raise NotImplementedError()
445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  def ExtractArchives(self, archives, extract_dir, rename_from_dir,
462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                      rename_to_dir):
475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    raise NotImplementedError()
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class RealUpdateDelegate(UpdateDelegate):
51c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  def __init__(self, user_data_dir, install_dir, cfg):
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    UpdateDelegate.__init__(self)
53c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    self.archive_cache = os.path.join(user_data_dir, 'archives')
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.install_dir = install_dir
55c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    self.cache_max = getattr(cfg, 'cache_max', DEFAULT_CACHE_SIZE)
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def BundleDirectoryExists(self, bundle_name):
585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    bundle_path = os.path.join(self.install_dir, bundle_name)
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return os.path.isdir(bundle_path)
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
61c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  def VerifyDownload(self, filename, archive):
62c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    """Verify that a local filename in the cache matches the given
63c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    online archive.
64c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
65c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    Returns True if both size and sha1 match, False otherwise.
66c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    """
67c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    filename = os.path.join(self.archive_cache, filename)
68c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    if not os.path.exists(filename):
69c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      logging.info('File does not exist: %s.' % filename)
70c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      return False
71c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    size = os.path.getsize(filename)
72c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    if size != archive.size:
73c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      logging.info('File size does not match (%d vs %d): %s.' % (size,
74c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)          archive.size, filename))
75c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      return False
76c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    sha1_hash = hashlib.sha1()
77c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    with open(filename) as f:
78c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      sha1_hash.update(f.read())
79c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    if sha1_hash.hexdigest() != archive.GetChecksum():
80c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      logging.info('File hash does not match: %s.' % filename)
81c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      return False
82c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    return True
83c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
84c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  def BytesUsedInCache(self):
85c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    """Determine number of bytes currently be in local archive cache."""
86c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    total = 0
87c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    for root, _, files in os.walk(self.archive_cache):
88c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      for filename in files:
89c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        total += os.path.getsize(os.path.join(root, filename))
90c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    return total
91c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
92c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  def CleanupCache(self):
93c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    """Remove archives from the local filesystem cache until the
94c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    total size is below cache_max.
95c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
96c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    This is done my deleting the oldest archive files until the
97c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    condition is satisfied.  If cache_max is zero then the entire
98c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    cache will be removed.
99c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    """
100c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    used = self.BytesUsedInCache()
101c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    logging.info('Cache usage: %d / %d' % (used, self.cache_max))
102c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    if used <= self.cache_max:
103c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      return
104c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    clean_bytes = used - self.cache_max
105c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
106c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    logging.info('Clearing %d bytes in archive cache' % clean_bytes)
107c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    file_timestamps = []
108c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    for root, _, files in os.walk(self.archive_cache):
109c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      for filename in files:
110c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        fullname = os.path.join(root, filename)
111c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        file_timestamps.append((os.path.getmtime(fullname), fullname))
112c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
113c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    file_timestamps.sort()
114c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    while clean_bytes > 0:
115c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      assert(file_timestamps)
116c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      filename_to_remove = file_timestamps[0][1]
117c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      clean_bytes -= os.path.getsize(filename_to_remove)
118c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      logging.info('Removing from cache: %s' % filename_to_remove)
119c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      os.remove(filename_to_remove)
120c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      # Also remove resulting empty parent directory structure
121c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      while True:
122c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        filename_to_remove = os.path.dirname(filename_to_remove)
123c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        if not os.listdir(filename_to_remove):
124c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)          os.rmdir(filename_to_remove)
125c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        else:
126c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)          break
127c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      file_timestamps = file_timestamps[1:]
128c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def DownloadToFile(self, url, dest_filename):
130c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    dest_path = os.path.join(self.archive_cache, dest_filename)
131c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    sdk_update_common.MakeDirs(os.path.dirname(dest_path))
132c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
1335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    out_stream = None
1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    url_stream = None
1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    try:
1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      out_stream = open(dest_path, 'wb')
1375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      url_stream = download.UrlOpen(url)
1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      content_length = int(url_stream.info()[HTTP_CONTENT_LENGTH])
1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      progress = download.MakeProgressFunction(content_length)
1405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      sha1, size = download.DownloadAndComputeHash(url_stream, out_stream,
1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                                   progress)
1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return sha1, size
1435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    except urllib2.URLError as e:
1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      raise Error('Unable to read from URL "%s".\n  %s' % (url, e))
1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    except IOError as e:
1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      raise Error('Unable to write to file "%s".\n  %s' % (dest_filename, e))
1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    finally:
1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if url_stream:
1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        url_stream.close()
1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if out_stream:
1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        out_stream.close()
1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  def ExtractArchives(self, archives, extract_dir, rename_from_dir,
1542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                      rename_to_dir):
1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    tar_file = None
1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    extract_path = os.path.join(self.install_dir, extract_dir)
1585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    rename_from_path = os.path.join(self.install_dir, rename_from_dir)
1595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    rename_to_path = os.path.join(self.install_dir, rename_to_dir)
1605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # Extract to extract_dir, usually "<bundle name>_update".
1625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # This way if the extraction fails, we haven't blown away the old bundle
1635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # (if it exists).
1645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    sdk_update_common.RemoveDir(extract_path)
1655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    sdk_update_common.MakeDirs(extract_path)
1665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    curpath = os.getcwd()
1675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    tar_file = None
1685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    try:
1705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      try:
1715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        logging.info('Changing the directory to %s' % (extract_path,))
1725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        os.chdir(extract_path)
1735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      except Exception as e:
1745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        raise Error('Unable to chdir into "%s".\n  %s' % (extract_path, e))
1755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      for i, archive in enumerate(archives):
177c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        archive_path = os.path.join(self.archive_cache, archive)
1782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
179c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        if len(archives) > 1:
180c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)          print '(file %d/%d - "%s")' % (
181c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)             i + 1, len(archives), os.path.basename(archive_path))
182c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        logging.info('Extracting to %s' % (extract_path,))
183c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
184c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        if sys.platform == 'win32':
185c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)          try:
186c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            logging.info('Opening file %s (%d/%d).' % (archive_path, i + 1,
187c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)                len(archives)))
188c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            try:
189c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)              tar_file = cygtar.CygTar(archive_path, 'r', verbose=True)
190c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            except Exception as e:
191c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)              raise Error("Can't open archive '%s'.\n  %s" % (archive_path, e))
192c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
193c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            tar_file.Extract()
194c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)          finally:
195c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            if tar_file:
196c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)              tar_file.Close()
197c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        else:
1982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          try:
199c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            subprocess.check_call(['tar', 'xf', archive_path])
200c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)          except subprocess.CalledProcessError:
201c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            raise Error('Error extracting archive: %s' % archive_path)
2025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      logging.info('Changing the directory to %s' % (curpath,))
2045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      os.chdir(curpath)
2055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      logging.info('Renaming %s->%s' % (rename_from_path, rename_to_path))
2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      sdk_update_common.RenameDir(rename_from_path, rename_to_path)
2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    finally:
2095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # Change the directory back so we can remove the update directory.
2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      os.chdir(curpath)
2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # Clean up the ..._update directory.
2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      try:
2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        sdk_update_common.RemoveDir(extract_path)
2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      except Exception as e:
2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        logging.error('Failed to remove directory \"%s\".  %s' % (
2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            extract_path, e))
2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def Update(delegate, remote_manifest, local_manifest, bundle_names, force):
2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  valid_bundles = set([bundle.name for bundle in remote_manifest.GetBundles()])
2222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  requested_bundles = _GetRequestedBundleNamesFromArgs(remote_manifest,
2232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                                                       bundle_names)
2245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  invalid_bundles = requested_bundles - valid_bundles
2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if invalid_bundles:
2265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    logging.warn('Ignoring unknown bundle(s): %s' % (
2275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        ', '.join(invalid_bundles)))
2285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    requested_bundles -= invalid_bundles
2295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if SDK_TOOLS in requested_bundles:
2315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    logging.warn('Updating sdk_tools happens automatically. '
2325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                 'Ignoring manual update request.')
2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    requested_bundles.discard(SDK_TOOLS)
2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if requested_bundles:
2365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for bundle_name in requested_bundles:
2375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      logging.info('Trying to update %s' % (bundle_name,))
2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      UpdateBundleIfNeeded(delegate, remote_manifest, local_manifest,
2395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          bundle_name, force)
2405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  else:
2415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    logging.warn('No bundles to update.')
2425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)def Reinstall(delegate, local_manifest, bundle_names):
2452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  valid_bundles, invalid_bundles = \
2462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      command_common.GetValidBundles(local_manifest, bundle_names)
2472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  if invalid_bundles:
2482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    logging.warn('Unknown bundle(s): %s\n' % (', '.join(invalid_bundles)))
2492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  if not valid_bundles:
2512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    logging.warn('No bundles to reinstall.')
2522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    return
2532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  for bundle_name in valid_bundles:
2552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    bundle = copy.deepcopy(local_manifest.GetBundle(bundle_name))
25690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
25790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    # HACK(binji): There was a bug where we'd merge the bundles from the old
25890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    # archive and the new archive when updating. As a result, some users may
25990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    # have a cache manifest that contains duplicate archives. Remove all
26090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    # archives with the same basename except for the most recent.
26190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    # Because the archives are added to a list, we know the most recent is at
26290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    # the end.
26390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    archives = {}
26490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    for archive in bundle.GetArchives():
26590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      url = archive.url
26690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      path = urlparse.urlparse(url)[2]
26790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      basename = os.path.basename(path)
26890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      archives[basename] = archive
26990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
27090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    # Update the bundle with these new archives.
27190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    bundle.RemoveAllArchives()
27290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    for _, archive in archives.iteritems():
27390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      bundle.AddArchive(archive)
27490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
2752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    _UpdateBundle(delegate, bundle, local_manifest)
2762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def UpdateBundleIfNeeded(delegate, remote_manifest, local_manifest,
2795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                         bundle_name, force):
2805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  bundle = remote_manifest.GetBundle(bundle_name)
2815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if bundle:
2825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if _BundleNeedsUpdate(delegate, local_manifest, bundle):
2835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # TODO(binji): It would be nicer to detect whether the user has any
2845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # modifications to the bundle. If not, we could update with impunity.
2855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if not force and delegate.BundleDirectoryExists(bundle_name):
2865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        print ('%s already exists, but has an update available.\n'
2875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            'Run update with the --force option to overwrite the '
2885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            'existing directory.\nWarning: This will overwrite any '
2895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            'modifications you have made within this directory.'
2905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            % (bundle_name,))
2915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return
2925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      _UpdateBundle(delegate, bundle, local_manifest)
2945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    else:
2955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      print '%s is already up-to-date.' % (bundle.name,)
2965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  else:
2975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    logging.error('Bundle %s does not exist.' % (bundle_name,))
2985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)def _GetRequestedBundleNamesFromArgs(remote_manifest, requested_bundles):
3015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  requested_bundles = set(requested_bundles)
3025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if RECOMMENDED in requested_bundles:
3035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    requested_bundles.discard(RECOMMENDED)
3042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    requested_bundles |= set(_GetRecommendedBundleNames(remote_manifest))
3055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return requested_bundles
3075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)def _GetRecommendedBundleNames(remote_manifest):
3102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  result = []
3112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  for bundle in remote_manifest.GetBundles():
3122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if bundle.recommended == 'yes' and bundle.name != SDK_TOOLS:
3132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      result.append(bundle.name)
3142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  return result
3155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _BundleNeedsUpdate(delegate, local_manifest, bundle):
3185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Always update the bundle if the directory doesn't exist;
3195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # the user may have deleted it.
3205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if not delegate.BundleDirectoryExists(bundle.name):
3215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return True
3225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return local_manifest.BundleNeedsUpdate(bundle)
3245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _UpdateBundle(delegate, bundle, local_manifest):
3272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  archives = bundle.GetHostOSArchives()
3282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  if not archives:
3295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    logging.warn('Bundle %s does not exist for this platform.' % (bundle.name,))
3305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return
3315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  archive_filenames = []
3332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
334c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  shown_banner = False
3352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  for i, archive in enumerate(archives):
3362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    archive_filename = _GetFilenameFromURL(archive.url)
337c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    archive_filename = os.path.join(bundle.name, archive_filename)
338c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
339c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    if not delegate.VerifyDownload(archive_filename, archive):
340c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      if not shown_banner:
341c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        shown_banner = True
342c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        print 'Downloading bundle %s' % (bundle.name,)
343c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      if len(archives) > 1:
344c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        print '(file %d/%d - "%s")' % (
345c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            i + 1, len(archives), os.path.basename(archive.url))
346c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      sha1, size = delegate.DownloadToFile(archive.url, archive_filename)
347c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      _ValidateArchive(archive, sha1, size)
3482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    archive_filenames.append(archive_filename)
3505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  print 'Updating bundle %s to version %s, revision %s' % (
3525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      bundle.name, bundle.version, bundle.revision)
3535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  extract_dir = bundle.name + '_update'
3545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  repath_dir = bundle.get('repath', None)
3565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if repath_dir:
3575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # If repath is specified:
3585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # The files are extracted to nacl_sdk/<bundle.name>_update/<repath>/...
3592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    # The destination directory is nacl_sdk/<bundle.name>/...
3605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    rename_from_dir = os.path.join(extract_dir, repath_dir)
3615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  else:
3625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # If no repath is specified:
3635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # The files are extracted to nacl_sdk/<bundle.name>_update/...
3645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # The destination directory is nacl_sdk/<bundle.name>/...
3655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    rename_from_dir = extract_dir
3665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  rename_to_dir = bundle.name
3682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  delegate.ExtractArchives(archive_filenames, extract_dir, rename_from_dir,
3702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                           rename_to_dir)
3715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  logging.info('Updating local manifest to include bundle %s' % (bundle.name))
37390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  local_manifest.RemoveBundle(bundle.name)
37490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  local_manifest.SetBundle(bundle)
375c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  delegate.CleanupCache()
3765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _GetFilenameFromURL(url):
379c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  path = urlparse.urlparse(url)[2]
380c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  return os.path.basename(path)
3815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _ValidateArchive(archive, actual_sha1, actual_size):
3845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if actual_size != archive.size:
3855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    raise Error('Size mismatch on "%s".  Expected %s but got %s bytes' % (
3865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        archive.name, archive.size, actual_size))
387c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)  if actual_sha1 != archive.GetChecksum():
388c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    raise Error('SHA1 checksum mismatch on "%s".  Expected %s but got %s' % (
389c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        archive.name, archive.GetChecksum(), actual_sha1))
390