blob: e49765bb2de84f724bee00d717d5ac2eacd45f59 [file] [log] [blame]
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import hashlib
import copy
import logging
import os
import subprocess
import sys
import urlparse
import urllib2
import command_common
import download
from sdk_update_common import Error
import sdk_update_common
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PARENT_DIR = os.path.dirname(SCRIPT_DIR)
sys.path.append(PARENT_DIR)
try:
import cygtar
except ImportError:
# Try to find this in the Chromium repo.
CHROME_SRC_DIR = os.path.abspath(
os.path.join(PARENT_DIR, '..', '..', '..', '..'))
sys.path.append(os.path.join(CHROME_SRC_DIR, 'native_client', 'build'))
import cygtar
RECOMMENDED = 'recommended'
SDK_TOOLS = 'sdk_tools'
HTTP_CONTENT_LENGTH = 'Content-Length' # HTTP Header field for content length
DEFAULT_CACHE_SIZE = 512 * 1024 * 1024 # 1/2 Gb cache by default
class UpdateDelegate(object):
def BundleDirectoryExists(self, bundle_name):
raise NotImplementedError()
def DownloadToFile(self, url, dest_filename):
raise NotImplementedError()
def ExtractArchives(self, archives, extract_dir, rename_from_dir,
rename_to_dir):
raise NotImplementedError()
class RealUpdateDelegate(UpdateDelegate):
def __init__(self, user_data_dir, install_dir, cfg):
UpdateDelegate.__init__(self)
self.archive_cache = os.path.join(user_data_dir, 'archives')
self.install_dir = install_dir
self.cache_max = getattr(cfg, 'cache_max', DEFAULT_CACHE_SIZE)
def BundleDirectoryExists(self, bundle_name):
bundle_path = os.path.join(self.install_dir, bundle_name)
return os.path.isdir(bundle_path)
def VerifyDownload(self, filename, archive):
"""Verify that a local filename in the cache matches the given
online archive.
Returns True if both size and sha1 match, False otherwise.
"""
filename = os.path.join(self.archive_cache, filename)
if not os.path.exists(filename):
logging.info('File does not exist: %s.' % filename)
return False
size = os.path.getsize(filename)
if size != archive.size:
logging.info('File size does not match (%d vs %d): %s.' % (size,
archive.size, filename))
return False
sha1_hash = hashlib.sha1()
with open(filename) as f:
sha1_hash.update(f.read())
if sha1_hash.hexdigest() != archive.GetChecksum():
logging.info('File hash does not match: %s.' % filename)
return False
return True
def BytesUsedInCache(self):
"""Determine number of bytes currently be in local archive cache."""
total = 0
for root, _, files in os.walk(self.archive_cache):
for filename in files:
total += os.path.getsize(os.path.join(root, filename))
return total
def CleanupCache(self):
"""Remove archives from the local filesystem cache until the
total size is below cache_max.
This is done my deleting the oldest archive files until the
condition is satisfied. If cache_max is zero then the entire
cache will be removed.
"""
used = self.BytesUsedInCache()
logging.info('Cache usage: %d / %d' % (used, self.cache_max))
if used <= self.cache_max:
return
clean_bytes = used - self.cache_max
logging.info('Clearing %d bytes in archive cache' % clean_bytes)
file_timestamps = []
for root, _, files in os.walk(self.archive_cache):
for filename in files:
fullname = os.path.join(root, filename)
file_timestamps.append((os.path.getmtime(fullname), fullname))
file_timestamps.sort()
while clean_bytes > 0:
assert(file_timestamps)
filename_to_remove = file_timestamps[0][1]
clean_bytes -= os.path.getsize(filename_to_remove)
logging.info('Removing from cache: %s' % filename_to_remove)
os.remove(filename_to_remove)
# Also remove resulting empty parent directory structure
while True:
filename_to_remove = os.path.dirname(filename_to_remove)
if not os.listdir(filename_to_remove):
os.rmdir(filename_to_remove)
else:
break
file_timestamps = file_timestamps[1:]
def DownloadToFile(self, url, dest_filename):
dest_path = os.path.join(self.archive_cache, dest_filename)
sdk_update_common.MakeDirs(os.path.dirname(dest_path))
out_stream = None
url_stream = None
try:
out_stream = open(dest_path, 'wb')
url_stream = download.UrlOpen(url)
content_length = int(url_stream.info()[HTTP_CONTENT_LENGTH])
progress = download.MakeProgressFunction(content_length)
sha1, size = download.DownloadAndComputeHash(url_stream, out_stream,
progress)
return sha1, size
except urllib2.URLError as e:
raise Error('Unable to read from URL "%s".\n %s' % (url, e))
except IOError as e:
raise Error('Unable to write to file "%s".\n %s' % (dest_filename, e))
finally:
if url_stream:
url_stream.close()
if out_stream:
out_stream.close()
def ExtractArchives(self, archives, extract_dir, rename_from_dir,
rename_to_dir):
tar_file = None
extract_path = os.path.join(self.install_dir, extract_dir)
rename_from_path = os.path.join(self.install_dir, rename_from_dir)
rename_to_path = os.path.join(self.install_dir, rename_to_dir)
# Extract to extract_dir, usually "<bundle name>_update".
# This way if the extraction fails, we haven't blown away the old bundle
# (if it exists).
sdk_update_common.RemoveDir(extract_path)
sdk_update_common.MakeDirs(extract_path)
curpath = os.getcwd()
tar_file = None
try:
try:
logging.info('Changing the directory to %s' % (extract_path,))
os.chdir(extract_path)
except Exception as e:
raise Error('Unable to chdir into "%s".\n %s' % (extract_path, e))
for i, archive in enumerate(archives):
archive_path = os.path.join(self.archive_cache, archive)
if len(archives) > 1:
print '(file %d/%d - "%s")' % (
i + 1, len(archives), os.path.basename(archive_path))
logging.info('Extracting to %s' % (extract_path,))
if sys.platform == 'win32':
try:
logging.info('Opening file %s (%d/%d).' % (archive_path, i + 1,
len(archives)))
try:
tar_file = cygtar.CygTar(archive_path, 'r', verbose=True)
except Exception as e:
raise Error("Can't open archive '%s'.\n %s" % (archive_path, e))
tar_file.Extract()
finally:
if tar_file:
tar_file.Close()
else:
try:
subprocess.check_call(['tar', 'xf', archive_path])
except subprocess.CalledProcessError:
raise Error('Error extracting archive: %s' % archive_path)
logging.info('Changing the directory to %s' % (curpath,))
os.chdir(curpath)
logging.info('Renaming %s->%s' % (rename_from_path, rename_to_path))
sdk_update_common.RenameDir(rename_from_path, rename_to_path)
finally:
# Change the directory back so we can remove the update directory.
os.chdir(curpath)
# Clean up the ..._update directory.
try:
sdk_update_common.RemoveDir(extract_path)
except Exception as e:
logging.error('Failed to remove directory \"%s\". %s' % (
extract_path, e))
def Update(delegate, remote_manifest, local_manifest, bundle_names, force):
valid_bundles = set([bundle.name for bundle in remote_manifest.GetBundles()])
requested_bundles = _GetRequestedBundleNamesFromArgs(remote_manifest,
bundle_names)
invalid_bundles = requested_bundles - valid_bundles
if invalid_bundles:
logging.warn('Ignoring unknown bundle(s): %s' % (
', '.join(invalid_bundles)))
requested_bundles -= invalid_bundles
if SDK_TOOLS in requested_bundles:
logging.warn('Updating sdk_tools happens automatically. '
'Ignoring manual update request.')
requested_bundles.discard(SDK_TOOLS)
if requested_bundles:
for bundle_name in requested_bundles:
logging.info('Trying to update %s' % (bundle_name,))
UpdateBundleIfNeeded(delegate, remote_manifest, local_manifest,
bundle_name, force)
else:
logging.warn('No bundles to update.')
def Reinstall(delegate, local_manifest, bundle_names):
valid_bundles, invalid_bundles = \
command_common.GetValidBundles(local_manifest, bundle_names)
if invalid_bundles:
logging.warn('Unknown bundle(s): %s\n' % (', '.join(invalid_bundles)))
if not valid_bundles:
logging.warn('No bundles to reinstall.')
return
for bundle_name in valid_bundles:
bundle = copy.deepcopy(local_manifest.GetBundle(bundle_name))
# HACK(binji): There was a bug where we'd merge the bundles from the old
# archive and the new archive when updating. As a result, some users may
# have a cache manifest that contains duplicate archives. Remove all
# archives with the same basename except for the most recent.
# Because the archives are added to a list, we know the most recent is at
# the end.
archives = {}
for archive in bundle.GetArchives():
url = archive.url
path = urlparse.urlparse(url)[2]
basename = os.path.basename(path)
archives[basename] = archive
# Update the bundle with these new archives.
bundle.RemoveAllArchives()
for _, archive in archives.iteritems():
bundle.AddArchive(archive)
_UpdateBundle(delegate, bundle, local_manifest)
def UpdateBundleIfNeeded(delegate, remote_manifest, local_manifest,
bundle_name, force):
bundle = remote_manifest.GetBundle(bundle_name)
if bundle:
if _BundleNeedsUpdate(delegate, local_manifest, bundle):
# TODO(binji): It would be nicer to detect whether the user has any
# modifications to the bundle. If not, we could update with impunity.
if not force and delegate.BundleDirectoryExists(bundle_name):
print ('%s already exists, but has an update available.\n'
'Run update with the --force option to overwrite the '
'existing directory.\nWarning: This will overwrite any '
'modifications you have made within this directory.'
% (bundle_name,))
return
_UpdateBundle(delegate, bundle, local_manifest)
else:
print '%s is already up-to-date.' % (bundle.name,)
else:
logging.error('Bundle %s does not exist.' % (bundle_name,))
def _GetRequestedBundleNamesFromArgs(remote_manifest, requested_bundles):
requested_bundles = set(requested_bundles)
if RECOMMENDED in requested_bundles:
requested_bundles.discard(RECOMMENDED)
requested_bundles |= set(_GetRecommendedBundleNames(remote_manifest))
return requested_bundles
def _GetRecommendedBundleNames(remote_manifest):
result = []
for bundle in remote_manifest.GetBundles():
if bundle.recommended == 'yes' and bundle.name != SDK_TOOLS:
result.append(bundle.name)
return result
def _BundleNeedsUpdate(delegate, local_manifest, bundle):
# Always update the bundle if the directory doesn't exist;
# the user may have deleted it.
if not delegate.BundleDirectoryExists(bundle.name):
return True
return local_manifest.BundleNeedsUpdate(bundle)
def _UpdateBundle(delegate, bundle, local_manifest):
archives = bundle.GetHostOSArchives()
if not archives:
logging.warn('Bundle %s does not exist for this platform.' % (bundle.name,))
return
archive_filenames = []
shown_banner = False
for i, archive in enumerate(archives):
archive_filename = _GetFilenameFromURL(archive.url)
archive_filename = os.path.join(bundle.name, archive_filename)
if not delegate.VerifyDownload(archive_filename, archive):
if not shown_banner:
shown_banner = True
print 'Downloading bundle %s' % (bundle.name,)
if len(archives) > 1:
print '(file %d/%d - "%s")' % (
i + 1, len(archives), os.path.basename(archive.url))
sha1, size = delegate.DownloadToFile(archive.url, archive_filename)
_ValidateArchive(archive, sha1, size)
archive_filenames.append(archive_filename)
print 'Updating bundle %s to version %s, revision %s' % (
bundle.name, bundle.version, bundle.revision)
extract_dir = bundle.name + '_update'
repath_dir = bundle.get('repath', None)
if repath_dir:
# If repath is specified:
# The files are extracted to nacl_sdk/<bundle.name>_update/<repath>/...
# The destination directory is nacl_sdk/<bundle.name>/...
rename_from_dir = os.path.join(extract_dir, repath_dir)
else:
# If no repath is specified:
# The files are extracted to nacl_sdk/<bundle.name>_update/...
# The destination directory is nacl_sdk/<bundle.name>/...
rename_from_dir = extract_dir
rename_to_dir = bundle.name
delegate.ExtractArchives(archive_filenames, extract_dir, rename_from_dir,
rename_to_dir)
logging.info('Updating local manifest to include bundle %s' % (bundle.name))
local_manifest.RemoveBundle(bundle.name)
local_manifest.SetBundle(bundle)
delegate.CleanupCache()
def _GetFilenameFromURL(url):
path = urlparse.urlparse(url)[2]
return os.path.basename(path)
def _ValidateArchive(archive, actual_sha1, actual_size):
if actual_size != archive.size:
raise Error('Size mismatch on "%s". Expected %s but got %s bytes' % (
archive.url, archive.size, actual_size))
if actual_sha1 != archive.GetChecksum():
raise Error('SHA1 checksum mismatch on "%s". Expected %s but got %s' % (
archive.url, archive.GetChecksum(), actual_sha1))