summaryrefslogtreecommitdiffstats
path: root/native_client_sdk
diff options
context:
space:
mode:
authorbinji@chromium.org <binji@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-04-19 20:10:27 +0000
committerbinji@chromium.org <binji@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-04-19 20:10:27 +0000
commit7cf05f46c0c7475308bf733b03945a3cbc576731 (patch)
tree1dcabead490f2792ce3b36d00d2d769489dcf634 /native_client_sdk
parentcc1aa09291edf788b518e7b1342ac52e8082c240 (diff)
downloadchromium_src-7cf05f46c0c7475308bf733b03945a3cbc576731.zip
chromium_src-7cf05f46c0c7475308bf733b03945a3cbc576731.tar.gz
chromium_src-7cf05f46c0c7475308bf733b03945a3cbc576731.tar.bz2
[NaCl SDK] extract manifest manipulation classes into manifest_util.py
BUG=none TEST=none Review URL: https://chromiumcodereview.appspot.com/10031003 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@133048 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'native_client_sdk')
-rwxr-xr-xnative_client_sdk/src/build_tools/build_sdk.py3
-rw-r--r--native_client_sdk/src/build_tools/manifest_util.py353
-rwxr-xr-xnative_client_sdk/src/build_tools/sdk_tools/sdk_update.py605
3 files changed, 465 insertions, 496 deletions
diff --git a/native_client_sdk/src/build_tools/build_sdk.py b/native_client_sdk/src/build_tools/build_sdk.py
index 6b6068a..a0df2a1 100755
--- a/native_client_sdk/src/build_tools/build_sdk.py
+++ b/native_client_sdk/src/build_tools/build_sdk.py
@@ -408,6 +408,7 @@ UPDATER_FILES = [
# SDK tools
('build_tools/sdk_tools/cacerts.txt', 'nacl_sdk/sdk_tools/cacerts.txt'),
('build_tools/sdk_tools/sdk_update.py', 'nacl_sdk/sdk_tools/sdk_update.py'),
+ ('build_tools/manifest_util.py', 'nacl_sdk/sdk_tools/manifest_util.py'),
('build_tools/sdk_tools/third_party/__init__.py',
'nacl_sdk/sdk_tools/third_party/__init__.py'),
('build_tools/sdk_tools/third_party/fancy_urllib/__init__.py',
@@ -565,7 +566,7 @@ def main(args):
buildbot_common.Run(['make', 'all', '-j8'],
cwd=os.path.abspath(dirnode), shell=True)
-# Build SDK Tools
+ # Build SDK Tools
if not skip_update:
BuildUpdater()
diff --git a/native_client_sdk/src/build_tools/manifest_util.py b/native_client_sdk/src/build_tools/manifest_util.py
new file mode 100644
index 0000000..b2c8263
--- /dev/null
+++ b/native_client_sdk/src/build_tools/manifest_util.py
@@ -0,0 +1,353 @@
+#!/usr/bin/env python
+# 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 json
+
+MANIFEST_VERSION = 2
+
+# Some commonly-used key names.
+ARCHIVES_KEY = 'archives'
+BUNDLES_KEY = 'bundles'
+NAME_KEY = 'name'
+REVISION_KEY = 'revision'
+VERSION_KEY = 'version'
+
+# Valid values for the archive.host_os field
+HOST_OS_LITERALS = frozenset(['mac', 'win', 'linux', 'all'])
+
+# Valid keys for various sdk objects, used for validation.
+VALID_ARCHIVE_KEYS = frozenset(['host_os', 'size', 'checksum', 'url'])
+
+# Valid values for bundle.stability field
+STABILITY_LITERALS = [
+ 'obsolete', 'post_stable', 'stable', 'beta', 'dev', 'canary']
+
+# Valid values for bundle-recommended field.
+YES_NO_LITERALS = ['yes', 'no']
+VALID_BUNDLES_KEYS = frozenset([
+ ARCHIVES_KEY, NAME_KEY, VERSION_KEY, REVISION_KEY,
+ 'description', 'desc_url', 'stability', 'recommended', 'repath',
+ ])
+
+VALID_MANIFEST_KEYS = frozenset(['manifest_version', BUNDLES_KEY])
+
+
+class Error(Exception):
+ """Generic error/exception for manifest_util module"""
+ pass
+
+
+class Archive(dict):
+ """A placeholder for sdk archive information. We derive Archive from
+ dict so that it is easily serializable. """
+
+ def __init__(self, host_os_name):
+ """ Create a new archive for the given host-os name. """
+ self['host_os'] = host_os_name
+
+ def CopyFrom(self, dict):
+ """Update the content of the archive by copying values from the given
+ dictionary.
+
+ Args:
+ dict: The dictionary whose values must be copied to the archive."""
+ for key, value in dict.items():
+ self[key] = value
+
+ def Validate(self):
+ """Validate the content of the archive object. Raise an Error if
+ an invalid or missing field is found.
+
+ Returns: True if self is a valid bundle.
+ """
+ host_os = self.get('host_os', None)
+ if host_os and host_os not in HOST_OS_LITERALS:
+ raise Error('Invalid host-os name in archive')
+ # Ensure host_os has a valid string. We'll use it for pretty printing.
+ if not host_os:
+ host_os = 'all (default)'
+ if not self.get('url', None):
+ raise Error('Archive "%s" has no URL' % host_os)
+ # Verify that all key names are valid.
+ for key, val in self.iteritems():
+ if key not in VALID_ARCHIVE_KEYS:
+ raise Error('Archive "%s" has invalid attribute "%s"' % (host_os, key))
+
+ @property
+ def url(self):
+ """Returns the URL of this archive"""
+ return self['url']
+
+ @property
+ def size(self):
+ """Returns the size of this archive, in bytes"""
+ return self['size']
+
+ @property
+ def host_os(self):
+ """Returns the host OS of this archive"""
+ return self['host_os']
+
+ def GetChecksum(self, type='sha1'):
+ """Returns a given cryptographic checksum of the archive"""
+ return self['checksum'][type]
+
+
+class Bundle(dict):
+ """A placeholder for sdk bundle information. We derive Bundle from
+ dict so that it is easily serializable."""
+
+ def __init__(self, obj):
+ """ Create a new bundle with the given bundle name."""
+ if isinstance(obj, str) or isinstance(obj, unicode):
+ dict.__init__(self, [(ARCHIVES_KEY, []), (NAME_KEY, obj)])
+ else:
+ dict.__init__(self, obj)
+
+ def MergeWithBundle(self, bundle):
+ """Merge this bundle with |bundle|.
+
+ Merges dict in |bundle| with this one in such a way that keys are not
+ duplicated: the values of the keys in |bundle| take precedence in the
+ returned dictionary.
+
+ Any keys in either the symlink or links dictionaries that also exist in
+ either of the files or dicts sets are removed from the latter, meaning that
+ symlinks or links which overlap file or directory entries take precedence.
+
+ Args:
+ bundle: The other bundle. Must be a dict.
+ Returns:
+ A dict which is the result of merging the two Bundles.
+ """
+ return Bundle(self.items() + bundle.items())
+
+ def CopyFrom(self, dict):
+ """Update the content of the bundle by copying values from the given
+ dictionary.
+
+ Args:
+ dict: The dictionary whose values must be copied to the bundle."""
+ for key, value in dict.items():
+ if key == ARCHIVES_KEY:
+ archives = []
+ for a in value:
+ new_archive = Archive(a['host_os'])
+ new_archive.CopyFrom(a)
+ archives.append(new_archive)
+ self[ARCHIVES_KEY] = archives
+ else:
+ self[key] = value
+
+ def Validate(self):
+ """Validate the content of the bundle. Raise an Error if an invalid or
+ missing field is found. """
+ # Check required fields.
+ if not self.get(NAME_KEY, None):
+ raise Error('Bundle has no name')
+ if self.get(REVISION_KEY, None) == None:
+ raise Error('Bundle "%s" is missing a revision number' % self[NAME_KEY])
+ if self.get(VERSION_KEY, None) == None:
+ raise Error('Bundle "%s" is missing a version number' % self[NAME_KEY])
+ if not self.get('description', None):
+ raise Error('Bundle "%s" is missing a description' % self[NAME_KEY])
+ if not self.get('stability', None):
+ raise Error('Bundle "%s" is missing stability info' % self[NAME_KEY])
+ if self.get('recommended', None) == None:
+ raise Error('Bundle "%s" is missing the recommended field' %
+ self[NAME_KEY])
+ # Check specific values
+ if self['stability'] not in STABILITY_LITERALS:
+ raise Error('Bundle "%s" has invalid stability field: "%s"' %
+ (self[NAME_KEY], self['stability']))
+ if self['recommended'] not in YES_NO_LITERALS:
+ raise Error(
+ 'Bundle "%s" has invalid recommended field: "%s"' %
+ (self[NAME_KEY], self['recommended']))
+ # Verify that all key names are valid.
+ for key, val in self.iteritems():
+ if key not in VALID_BUNDLES_KEYS:
+ raise Error('Bundle "%s" has invalid attribute "%s"' %
+ (self[NAME_KEY], key))
+ # Validate the archives
+ for archive in self[ARCHIVES_KEY]:
+ archive.Validate()
+
+ def GetArchive(self, host_os_name):
+ """Retrieve the archive for the given host os.
+
+ Args:
+ host_os_name: name of host os whose archive must be retrieved.
+ Return:
+ An Archive instance or None if it doesn't exist."""
+ for archive in self[ARCHIVES_KEY]:
+ if archive.host_os == host_os_name:
+ return archive
+ return None
+
+ def GetArchives(self):
+ """Returns all the archives in this bundle"""
+ return self[ARCHIVES_KEY]
+
+ @property
+ def name(self):
+ """Returns the name of this bundle"""
+ return self[NAME_KEY]
+
+ @property
+ def version(self):
+ """Returns the version of this bundle"""
+ return self[VERSION_KEY]
+
+ @property
+ def revision(self):
+ """Returns the revision of this bundle"""
+ return self[REVISION_KEY]
+
+ @property
+ def recommended(self):
+ """Returns whether this bundle is recommended"""
+ return self['recommended']
+
+
+class SDKManifest(object):
+ """This class contains utilities for manipulation an SDK manifest string
+
+ For ease of unit-testing, this class should not contain any file I/O.
+ """
+
+ def __init__(self):
+ """Create a new SDKManifest object with default contents"""
+ self._manifest_data = {
+ "manifest_version": MANIFEST_VERSION,
+ "bundles": [],
+ }
+
+ def Validate(self):
+ """Validate the Manifest file and raises an exception for problems"""
+ # Validate the manifest top level
+ if self._manifest_data["manifest_version"] > MANIFEST_VERSION:
+ raise Error("Manifest version too high: %s" %
+ self._manifest_data["manifest_version"])
+ # Verify that all key names are valid.
+ for key, val in self._manifest_data.iteritems():
+ if key not in VALID_MANIFEST_KEYS:
+ raise Error('Manifest has invalid attribute "%s"' % key)
+ # Validate each bundle
+ for bundle in self._manifest_data[BUNDLES_KEY]:
+ bundle.Validate()
+
+ def GetBundle(self, name):
+ """Get a bundle from the array of bundles.
+
+ Args:
+ name: the name of the bundle to return.
+ Return:
+ The first bundle with the given name, or None if it is not found."""
+ if not BUNDLES_KEY in self._manifest_data:
+ return None
+ bundles = [bundle for bundle in self._manifest_data[BUNDLES_KEY]
+ if bundle[NAME_KEY] == name]
+ if len(bundles) > 1:
+ WarningPrint("More than one bundle with name '%s' exists." % name)
+ return bundles[0] if len(bundles) > 0 else None
+
+ def GetBundles(self):
+ """Return all the bundles in the manifest."""
+ return self._manifest_data[BUNDLES_KEY]
+
+ def SetBundle(self, new_bundle):
+ """Replace named bundle. Add if absent.
+
+ Args:
+ bundle: The bundle.
+ """
+ name = new_bundle[NAME_KEY]
+ if not BUNDLES_KEY in self._manifest_data:
+ self._manifest_data[BUNDLES_KEY] = []
+ bundles = self._manifest_data[BUNDLES_KEY]
+ # Delete any bundles from the list, then add the new one. This has the
+ # effect of replacing the bundle if it already exists. It also removes all
+ # duplicate bundles.
+ for i, bundle in enumerate(bundles):
+ if bundle[NAME_KEY] == name:
+ del bundles[i]
+ bundles.append(new_bundle)
+
+ def BundleNeedsUpdate(self, bundle):
+ """Decides if a bundle needs to be updated.
+
+ A bundle needs to be updated if it is not installed (doesn't exist in this
+ manifest file) or if its revision is later than the revision in this file.
+
+ Args:
+ bundle: The Bundle to test.
+ Returns:
+ True if Bundle needs to be updated.
+ """
+ if NAME_KEY not in bundle:
+ raise KeyError("Bundle must have a 'name' key.")
+ local_bundle = self.GetBundle(bundle[NAME_KEY])
+ return (local_bundle == None) or (
+ (local_bundle[VERSION_KEY], local_bundle[REVISION_KEY]) <
+ (bundle[VERSION_KEY], bundle[REVISION_KEY]))
+
+ def MergeBundle(self, bundle):
+ """Merge a Bundle into this manifest.
+
+ The new bundle is added if not present, or merged into the existing bundle.
+
+ Args:
+ bundle: The bundle to merge.
+ """
+ if NAME_KEY not in bundle:
+ raise KeyError("Bundle must have a 'name' key.")
+ local_bundle = self.GetBundle(bundle.name)
+ if not local_bundle:
+ self.SetBundle(bundle)
+ else:
+ self.SetBundle(local_bundle.MergeWithBundle(bundle))
+
+ def FilterBundles(self, predicate):
+ """Filter the list of bundles by |predicate|.
+
+ For all bundles in this manifest, if predicate(bundle) is False, the bundle
+ is removed from the manifest.
+
+ Args:
+ predicate: a function that take a bundle and returns whether True to keep
+ it or False to remove it.
+ """
+ self._manifest_data[BUNDLES_KEY] = filter(predicate, self.GetBundles())
+
+ def LoadManifestString(self, json_string):
+ """Load a JSON manifest string. Raises an exception if json_string
+ is not well-formed JSON.
+
+ Args:
+ json_string: a JSON-formatted string containing the previous manifest
+ all_hosts: True indicates that we should load bundles for all hosts.
+ False (default) says to only load bundles for the current host"""
+ new_manifest = json.loads(json_string)
+ for key, value in new_manifest.items():
+ if key == BUNDLES_KEY:
+ # Remap each bundle in |value| to a Bundle instance
+ bundles = []
+ for b in value:
+ new_bundle = Bundle(b[NAME_KEY])
+ new_bundle.CopyFrom(b)
+ bundles.append(new_bundle)
+ self._manifest_data[key] = bundles
+ else:
+ self._manifest_data[key] = value
+ self.Validate()
+
+ def GetManifestString(self):
+ """Returns the current JSON manifest object, pretty-printed"""
+ pretty_string = json.dumps(self._manifest_data, sort_keys=False, indent=2)
+ # json.dumps sometimes returns trailing whitespace and does not put
+ # a newline at the end. This code fixes these problems.
+ pretty_lines = pretty_string.split('\n')
+ return '\n'.join([line.rstrip() for line in pretty_lines]) + '\n'
diff --git a/native_client_sdk/src/build_tools/sdk_tools/sdk_update.py b/native_client_sdk/src/build_tools/sdk_tools/sdk_update.py
index 4cab827..878e21e 100755
--- a/native_client_sdk/src/build_tools/sdk_tools/sdk_update.py
+++ b/native_client_sdk/src/build_tools/sdk_tools/sdk_update.py
@@ -11,6 +11,7 @@ import errno
import exceptions
import hashlib
import json
+import manifest_util
import optparse
import os
import shutil
@@ -28,7 +29,7 @@ import urlparse
# Bump the MINOR_REV every time you check this file in.
MAJOR_REV = 2
-MINOR_REV = 16
+MINOR_REV = 17
GLOBAL_HELP = '''Usage: naclsdk [options] command [command_options]
@@ -53,28 +54,6 @@ USER_DATA_DIR='sdk_cache'
HTTP_CONTENT_LENGTH = 'Content-Length' # HTTP Header field for content length
-# Some commonly-used key names.
-ARCHIVES_KEY = 'archives'
-BUNDLES_KEY = 'bundles'
-NAME_KEY = 'name'
-REVISION_KEY = 'revision'
-VERSION_KEY = 'version'
-
-# Valid values for bundle.stability field
-STABILITY_LITERALS = [
- 'obsolete', 'post_stable', 'stable', 'beta', 'dev', 'canary']
-# Valid values for the archive.host_os field
-HOST_OS_LITERALS = frozenset(['mac', 'win', 'linux', 'all'])
-# Valid values for bundle-recommended field.
-YES_NO_LITERALS = ['yes', 'no']
-# Valid keys for various sdk objects, used for validation.
-VALID_ARCHIVE_KEYS = frozenset(['host_os', 'size', 'checksum', 'url'])
-VALID_BUNDLES_KEYS = frozenset([
- ARCHIVES_KEY, NAME_KEY, VERSION_KEY, REVISION_KEY,
- 'description', 'desc_url', 'stability', 'recommended', 'repath',
- ])
-VALID_MANIFEST_KEYS = frozenset(['manifest_version', BUNDLES_KEY])
-
#------------------------------------------------------------------------------
# General Utilities
@@ -311,455 +290,96 @@ def DownloadAndComputeHash(from_stream, to_stream=None, progress_func=None):
return sha1_hash.hexdigest(), size
-class Archive(dict):
- ''' A placeholder for sdk archive information. We derive Archive from
- dict so that it is easily serializable. '''
- def __init__(self, host_os_name):
- ''' Create a new archive for the given host-os name. '''
- self['host_os'] = host_os_name
-
- def CopyFrom(self, dict):
- ''' Update the content of the archive by copying values from the given
- dictionary.
-
- Args:
- dict: The dictionary whose values must be copied to the archive.'''
- for key, value in dict.items():
- self[key] = value
-
- def Validate(self):
- ''' Validate the content of the archive object. Raise an Error if
- an invalid or missing field is found.
- Returns: True if self is a valid bundle.
- '''
- host_os = self.get('host_os', None)
- if host_os and host_os not in HOST_OS_LITERALS:
- raise Error('Invalid host-os name in archive')
- # Ensure host_os has a valid string. We'll use it for pretty printing.
- if not host_os:
- host_os = 'all (default)'
- if not self.get('url', None):
- raise Error('Archive "%s" has no URL' % host_os)
- # Verify that all key names are valid.
- for key, val in self.iteritems():
- if key not in VALID_ARCHIVE_KEYS:
- raise Error('Archive "%s" has invalid attribute "%s"' % (host_os, key))
-
- def _OpenURLStream(self):
- ''' Open a file-like stream for the archives's url. Raises an Error if the
- url can't be opened.
-
- Return:
- A file-like object from which the archive's data can be read.'''
- try:
- url_stream = UrlOpen(self['url'])
- except urllib2.URLError:
- raise Error('Cannot open "%s" for archive %s' %
- (self['url'], self['host_os']))
-
- return url_stream
-
- def ComputeSha1AndSize(self):
- ''' Compute the sha1 hash and size of the archive's data. Raises
- an Error if the url can't be opened.
-
- Return:
- A tuple (sha1, size) with the sha1 hash and data size respectively.'''
- stream = None
- sha1 = None
- size = 0
- try:
- print 'Scanning archive to generate sha1 and size info:'
- stream = self._OpenURLStream()
- content_length = int(stream.info()[HTTP_CONTENT_LENGTH])
- sha1, size = DownloadAndComputeHash(from_stream=stream,
- progress_func=ShowProgress)
- if size != content_length:
- raise Error('Download size mismatch for %s.\n'
- 'Expected %s bytes but got %s' %
- (self['url'], content_length, size))
- finally:
- if stream: stream.close()
- return sha1, size
-
- def DownloadToFile(self, dest_path):
- ''' Download the archive's data to a file at dest_path. As a side effect,
- computes the sha1 hash and data size, both returned as a tuple. Raises
- an Error if the url can't be opened, or an IOError exception if
- dest_path can't be opened.
-
- Args:
- dest_path: Path for the file that will receive the data.
- Return:
- A tuple (sha1, size) with the sha1 hash and data size respectively.'''
- sha1 = None
- size = 0
- with open(dest_path, 'wb') as to_stream:
- from_stream = None
- try:
- from_stream = self._OpenURLStream()
- content_length = int(from_stream.info()[HTTP_CONTENT_LENGTH])
- progress_function = ProgressFunction(
- content_length).GetProgressFunction()
- InfoPrint('Downloading %s' % self['url'])
- sha1, size = DownloadAndComputeHash(
- from_stream,
- to_stream=to_stream,
- progress_func=progress_function)
- if size != content_length:
- raise Error('Download size mismatch for %s.\n'
- 'Expected %s bytes but got %s' %
- (self['url'], content_length, size))
- finally:
- if from_stream: from_stream.close()
- return sha1, size
-
- def Update(self, url):
- ''' Update the archive with the new url. Automatically update the
- archive's size and checksum fields. Raises an Error if the url is
- is invalid. '''
- self['url'] = url
- sha1, size = self.ComputeSha1AndSize()
- self['size'] = size
- self['checksum'] = {'sha1': sha1}
-
- def GetUrl(self):
- '''Returns the URL of this Archive'''
- return self['url']
-
- def GetSize(self):
- '''Returns the size of this archive, in bytes'''
- return int(self['size'])
-
- def GetChecksum(self, type='sha1'):
- '''Returns a given cryptographic checksum of the archive'''
- return self['checksum'][type]
-
-
-class Bundle(dict):
- ''' A placeholder for sdk bundle information. We derive Bundle from
- dict so that it is easily serializable.'''
- def __init__(self, obj):
- ''' Create a new bundle with the given bundle name.'''
- if isinstance(obj, str) or isinstance(obj, unicode):
- dict.__init__(self, [(ARCHIVES_KEY, []), (NAME_KEY, obj)])
- else:
- dict.__init__(self, obj)
-
- def MergeWithBundle(self, bundle):
- '''Merge this bundle with |bundle|.
-
- Merges dict in |bundle| with this one in such a way that keys are not
- duplicated: the values of the keys in |bundle| take precedence in the
- returned dictionary.
-
- Any keys in either the symlink or links dictionaries that also exist in
- either of the files or dicts sets are removed from the latter, meaning that
- symlinks or links which overlap file or directory entries take precedence.
-
- Args:
- bundle: The other bundle. Must be a dict.
- Returns:
- A dict which is the result of merging the two Bundles.
- '''
- return Bundle(self.items() + bundle.items())
-
- def CopyFrom(self, dict):
- ''' Update the content of the bundle by copying values from the given
- dictionary.
-
- Args:
- dict: The dictionary whose values must be copied to the bundle.'''
- for key, value in dict.items():
- if key == ARCHIVES_KEY:
- archives = []
- for a in value:
- new_archive = Archive(a['host_os'])
- new_archive.CopyFrom(a)
- archives.append(new_archive)
- self[ARCHIVES_KEY] = archives
- else:
- self[key] = value
-
- def Validate(self):
- ''' Validate the content of the bundle. Raise an Error if an invalid or
- missing field is found. '''
- # Check required fields.
- if not self.get(NAME_KEY, None):
- raise Error('Bundle has no name')
- if self.get(REVISION_KEY, None) == None:
- raise Error('Bundle "%s" is missing a revision number' % self[NAME_KEY])
- if self.get(VERSION_KEY, None) == None:
- raise Error('Bundle "%s" is missing a version number' % self[NAME_KEY])
- if not self.get('description', None):
- raise Error('Bundle "%s" is missing a description' % self[NAME_KEY])
- if not self.get('stability', None):
- raise Error('Bundle "%s" is missing stability info' % self[NAME_KEY])
- if self.get('recommended', None) == None:
- raise Error('Bundle "%s" is missing the recommended field' %
- self[NAME_KEY])
- # Check specific values
- if self['stability'] not in STABILITY_LITERALS:
- raise Error('Bundle "%s" has invalid stability field: "%s"' %
- (self[NAME_KEY], self['stability']))
- if self['recommended'] not in YES_NO_LITERALS:
- raise Error(
- 'Bundle "%s" has invalid recommended field: "%s"' %
- (self[NAME_KEY], self['recommended']))
- # Verify that all key names are valid.
- for key, val in self.iteritems():
- if key not in VALID_BUNDLES_KEYS:
- raise Error('Bundle "%s" has invalid attribute "%s"' %
- (self[NAME_KEY], key))
- # Validate the archives
- for archive in self[ARCHIVES_KEY]:
- archive.Validate()
-
- def GetArchive(self, host_os_name):
- ''' Retrieve the archive for the given host os.
-
- Args:
- host_os_name: name of host os whose archive must be retrieved.
- Return:
- An Archive instance or None if it doesn't exist.'''
- for archive in self[ARCHIVES_KEY]:
- if archive['host_os'] == host_os_name:
- return archive
- return None
-
- def UpdateArchive(self, host_os, url):
- ''' Update or create the archive for host_os with the new url.
- Automatically updates the archive size and checksum info by downloading
- the data from the given archive. Raises an Error if the url is invalid.
-
- Args:
- host_os: name of host os whose archive must be updated or created.
- url: the new url for the archive.'''
- archive = self.GetArchive(host_os)
- if not archive:
- archive = Archive(host_os_name=host_os)
- self[ARCHIVES_KEY].append(archive)
- archive.Update(url)
-
- def GetArchives(self):
- '''Returns all the archives in this bundle'''
- return self[ARCHIVES_KEY]
-
-
-class SDKManifest(object):
- '''This class contains utilities for manipulation an SDK manifest string
-
- For ease of unit-testing, this class should not contain any file I/O.
- '''
-
- def __init__(self):
- '''Create a new SDKManifest object with default contents'''
- self.MANIFEST_VERSION = MAJOR_REV
- self._manifest_data = {
- "manifest_version": self.MANIFEST_VERSION,
- "bundles": [],
- }
-
- def _ValidateManifest(self):
- '''Validate the Manifest file and raises an exception for problems'''
- # Validate the manifest top level
- if self._manifest_data["manifest_version"] > self.MANIFEST_VERSION:
- raise Error("Manifest version too high: %s" %
- self._manifest_data["manifest_version"])
- # Verify that all key names are valid.
- for key, val in self._manifest_data.iteritems():
- if key not in VALID_MANIFEST_KEYS:
- raise Error('Manifest has invalid attribute "%s"' % key)
- # Validate each bundle
- for bundle in self._manifest_data[BUNDLES_KEY]:
- bundle.Validate()
-
- def GetBundle(self, name):
- ''' Get a bundle from the array of bundles.
-
- Args:
- name: the name of the bundle to return.
- Return:
- The first bundle with the given name, or None if it is not found.'''
- if not BUNDLES_KEY in self._manifest_data:
- return None
- bundles = filter(lambda b: b[NAME_KEY] == name,
- self._manifest_data[BUNDLES_KEY])
- if len(bundles) > 1:
- WarningPrint("More than one bundle with name '%s' exists." % name)
- return bundles[0] if len(bundles) > 0 else None
-
- def SetBundle(self, new_bundle):
- '''Replace named bundle. Add if absent.
+def LoadManifestFromFile(path):
+ '''Returns a manifest loaded from the JSON file at |path|.
- Args:
- name: Name of the bundle to replace or add.
- bundle: The bundle.
- '''
- name = new_bundle[NAME_KEY]
- if not BUNDLES_KEY in self._manifest_data:
- self._manifest_data[BUNDLES_KEY] = []
- bundles = self._manifest_data[BUNDLES_KEY]
- # Delete any bundles from the list, then add the new one. This has the
- # effect of replacing the bundle if it already exists. It also removes all
- # duplicate bundles.
- for i, bundle in enumerate(bundles):
- if bundle[NAME_KEY] == name:
- del bundles[i]
- bundles.append(new_bundle)
-
- def LoadManifestString(self, json_string, all_hosts=False):
- ''' Load a JSON manifest string. Raises an exception if json_string
- is not well-formed JSON.
+ If the path does not exist or is invalid, returns an empty manifest.'''
+ if not os.path.exists(path):
+ return manifest_util.SDKManifest()
- Args:
- json_string: a JSON-formatted string containing the previous manifest
- all_hosts: True indicates that we should load bundles for all hosts.
- False (default) says to only load bundles for the current host'''
- new_manifest = json.loads(json_string)
- for key, value in new_manifest.items():
- if key == BUNDLES_KEY:
- # Remap each bundle in |value| to a Bundle instance
- bundles = []
- for b in value:
- new_bundle = Bundle(b[NAME_KEY])
- new_bundle.CopyFrom(b)
- # Only add this archive if it's supported on this platform.
- # However, the sdk_tools bundle might not have an archive entry,
- # but is still always valid.
- if (all_hosts or new_bundle.GetArchive(GetHostOS()) or
- b[NAME_KEY] == 'sdk_tools'):
- bundles.append(new_bundle)
- self._manifest_data[key] = bundles
- else:
- self._manifest_data[key] = value
- self._ValidateManifest()
+ with open(path, 'r') as f:
+ json_string = f.read()
+ if not json_string:
+ return manifest_util.SDKManifest()
- def GetManifestString(self):
- '''Returns the current JSON manifest object, pretty-printed'''
- pretty_string = json.dumps(self._manifest_data, sort_keys=False, indent=2)
- # json.dumps sometimes returns trailing whitespace and does not put
- # a newline at the end. This code fixes these problems.
- pretty_lines = pretty_string.split('\n')
- return '\n'.join([line.rstrip() for line in pretty_lines]) + '\n'
+ manifest = manifest_util.SDKManifest()
+ manifest.LoadManifestString(json_string)
+ return manifest
-class SDKManifestFile(object):
- ''' This class provides basic file I/O support for manifest objects.'''
+def LoadManifestFromURL(url):
+ '''Returns a manifest loaded from |url|.'''
+ try:
+ url_stream = UrlOpen(url)
+ except urllib2.URLError as e:
+ raise Error('Unable to open %s. [%s]' % (url, e))
- def __init__(self, json_filepath):
- '''Create a new SDKManifest object with default contents.
+ manifest_stream = cStringIO.StringIO()
+ sha1, size = DownloadAndComputeHash(
+ url_stream, manifest_stream)
+ manifest = manifest_util.SDKManifest()
+ manifest.LoadManifestString(manifest_stream.getvalue())
- If |json_filepath| is specified, and it exists, its contents are loaded and
- used to initialize the internal manifest.
+ def BundleFilter(bundle):
+ # Only add this bundle if it's supported on this platform.
+ return bundle.GetArchive(GetHostOS())
- Args:
- json_filepath: path to jason file to read/write, or None to write a new
- manifest file to stdout.
- '''
- self._json_filepath = json_filepath
- self._manifest = SDKManifest()
- if self._json_filepath:
- self._LoadFile()
-
- def _LoadFile(self):
- '''Load the manifest from the JSON file.
-
- This function returns quietly if the file doesn't exit.
- '''
- if not os.path.exists(self._json_filepath):
- return
+ manifest.FilterBundles(BundleFilter)
+ return manifest
- with open(self._json_filepath, 'r') as f:
- json_string = f.read()
- if json_string:
- self._manifest.LoadManifestString(json_string, all_hosts=True)
-
- def WriteFile(self):
- '''Write the json data to the file. If not file name was specified, the
- data is written to stdout.'''
- json_string = self._manifest.GetManifestString()
- if not self._json_filepath:
- # No file is specified; print the json data to stdout
- sys.stdout.write(json_string)
- else:
- # Write the JSON data to a temp file.
- temp_file_name = None
- # TODO(dspringer): Use file locks here so that multiple sdk_updates can
- # run at the same time.
- with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
- f.write(json_string)
- temp_file_name = f.name
- # Move the temp file to the actual file.
- if os.path.exists(self._json_filepath):
- os.remove(self._json_filepath)
- shutil.move(temp_file_name, self._json_filepath)
-
- def GetBundles(self):
- '''Return all the bundles in |_manifest|'''
- return self._manifest._manifest_data[BUNDLES_KEY]
-
- def GetBundleNamed(self, name):
- '''Return the first bundle named |name| or None if it doesn't exist'''
- return self._manifest.GetBundle(name)
-
- def BundleNeedsUpdate(self, bundle):
- '''Decides if a bundle needs to be updated.
-
- A bundle needs to be updated if it is not installed (doesn't exist in this
- manifest file) or if its revision is later than the revision in this file.
- Args:
- bundle: The Bundle to test.
- Returns:
- True if Bundle needs to be updated.
- '''
- if NAME_KEY not in bundle:
- raise KeyError("Bundle must have a 'name' key.")
- local_bundle = self.GetBundleNamed(bundle[NAME_KEY])
- return (local_bundle == None) or (
- (local_bundle[VERSION_KEY], local_bundle[REVISION_KEY]) <
- (bundle[VERSION_KEY], bundle[REVISION_KEY]))
-
- def MergeBundle(self, bundle):
- '''Merge a Bundle into this manifest.
-
- The new bundle is added if not present, or merged into the existing bundle.
+def WriteManifestToFile(manifest, path):
+ '''Write |manifest| to a JSON file at |path|.'''
+ json_string = manifest.GetManifestString()
- Args:
- bundle: The bundle to merge.
- '''
- if NAME_KEY not in bundle:
- raise KeyError("Bundle must have a 'name' key.")
- local_bundle = self.GetBundleNamed(bundle[NAME_KEY])
- if not local_bundle:
- self._manifest.SetBundle(bundle)
- else:
- self._manifest.SetBundle(local_bundle.MergeWithBundle(bundle))
+ # Write the JSON data to a temp file.
+ temp_file_name = None
+ # TODO(dspringer): Use file locks here so that multiple sdk_updates can
+ # run at the same time.
+ with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
+ f.write(json_string)
+ temp_file_name = f.name
+ # Move the temp file to the actual file.
+ if os.path.exists(path):
+ os.remove(path)
+ shutil.move(temp_file_name, path)
-class ManifestTools(object):
- '''Wrapper class for supporting the SDK manifest file'''
+def DownloadArchiveToFile(archive, dest_path):
+ '''Download the archive's data to a file at dest_path.
- def __init__(self, options):
- self._options = options
- self._manifest = SDKManifest()
+ As a side effect, computes the sha1 hash and data size, both returned as a
+ tuple. Raises an Error if the url can't be opened, or an IOError exception if
+ dest_path can't be opened.
- def LoadManifest(self):
- DebugPrint("Running LoadManifest")
+ Args:
+ dest_path: Path for the file that will receive the data.
+ Return:
+ A tuple (sha1, size) with the sha1 hash and data size respectively.'''
+ sha1 = None
+ size = 0
+ with open(dest_path, 'wb') as to_stream:
+ from_stream = None
try:
- # TODO(mball): Add certificate validation on the server
- url_stream = UrlOpen(self._options.manifest_url)
- except urllib2.URLError, e:
- raise Error('Unable to open %s. [%s]' % (self._options.manifest_url, e))
-
- manifest_stream = cStringIO.StringIO()
- sha1, size = DownloadAndComputeHash(
- url_stream, manifest_stream)
- self._manifest.LoadManifestString(manifest_stream.getvalue())
-
- def GetBundles(self):
- return self._manifest._manifest_data[BUNDLES_KEY]
+ from_stream = UrlOpen(archive.url)
+ except urllib2.URLError:
+ raise Error('Cannot open "%s" for archive %s' %
+ (archive.url, archive.host_os))
+ try:
+ content_length = int(from_stream.info()[HTTP_CONTENT_LENGTH])
+ progress_function = ProgressFunction(content_length).GetProgressFunction()
+ InfoPrint('Downloading %s' % archive.url)
+ sha1, size = DownloadAndComputeHash(
+ from_stream,
+ to_stream=to_stream,
+ progress_func=progress_function)
+ if size != content_length:
+ raise Error('Download size mismatch for %s.\n'
+ 'Expected %s bytes but got %s' %
+ (archive.url, content_length, size))
+ finally:
+ if from_stream: from_stream.close()
+ return sha1, size
#------------------------------------------------------------------------------
@@ -772,26 +392,23 @@ def List(options, argv):
Lists the available SDK bundles that are available for download.'''
def PrintBundles(bundles):
for bundle in bundles:
- InfoPrint(' %s' % bundle[NAME_KEY])
+ InfoPrint(' %s' % bundle.name)
for key, value in bundle.iteritems():
- if key not in [ARCHIVES_KEY, NAME_KEY]:
+ if key not in (manifest_util.ARCHIVES_KEY, manifest_util.NAME_KEY):
InfoPrint(' %s: %s' % (key, value))
DebugPrint("Running List command with: %s, %s" %(options, argv))
parser = optparse.OptionParser(usage=List.__doc__)
(list_options, args) = parser.parse_args(argv)
- tools = ManifestTools(options)
- tools.LoadManifest()
- bundles = tools.GetBundles()
+ manifest = LoadManifestFromURL(options.manifest_url)
InfoPrint('Available bundles:')
- PrintBundles(bundles)
+ PrintBundles(manifest.GetBundles())
# Print the local information.
- local_manifest = SDKManifestFile(os.path.join(options.user_data_dir,
- options.manifest_filename))
- bundles = local_manifest.GetBundles()
+ manifest_path = os.path.join(options.user_data_dir, options.manifest_filename)
+ local_manifest = LoadManifestFromFile(manifest_path)
InfoPrint('\nCurrently installed bundles:')
- PrintBundles(bundles)
+ PrintBundles(local_manifest.GetBundles())
def Update(options, argv):
@@ -831,16 +448,24 @@ def Update(options, argv):
(update_options, args) = parser.parse_args(argv)
if len(args) == 0:
args = [RECOMMENDED]
- tools = ManifestTools(options)
- tools.LoadManifest()
- bundles = tools.GetBundles()
- local_manifest = SDKManifestFile(os.path.join(options.user_data_dir,
- options.manifest_filename))
+ manifest = LoadManifestFromURL(options.manifest_url)
+ bundles = manifest.GetBundles()
+ local_manifest_path = os.path.join(options.user_data_dir,
+ options.manifest_filename)
+ local_manifest = LoadManifestFromFile(local_manifest_path)
+
+ # Validate the arg list against the available bundle names. Raises an
+ # error if any invalid bundle names or args are detected.
+ valid_args = set([ALL, RECOMMENDED] + [bundle.name for bundle in bundles])
+ bad_args = set(args) - valid_args
+ if len(bad_args) > 0:
+ raise Error("Unrecognized bundle name or argument: '%s'" %
+ ', '.join(bad_args))
+
for bundle in bundles:
- bundle_name = bundle[NAME_KEY]
- bundle_path = os.path.join(options.sdk_root_dir, bundle_name)
+ bundle_path = os.path.join(options.sdk_root_dir, bundle.name)
bundle_update_path = '%s_update' % bundle_path
- if not (bundle_name in args or
+ if not (bundle.name in args or
ALL in args or (RECOMMENDED in args and
bundle[RECOMMENDED] == 'yes')):
continue
@@ -849,18 +474,17 @@ def Update(options, argv):
archive = bundle.GetArchive(GetHostOS())
(scheme, host, path, _, _, _) = urlparse.urlparse(archive['url'])
dest_filename = os.path.join(options.user_data_dir, path.split('/')[-1])
- sha1, size = archive.DownloadToFile(os.path.join(options.user_data_dir,
- dest_filename))
- if sha1 != archive['checksum']['sha1']:
+ sha1, size = DownloadArchiveToFile(archive, dest_filename)
+ if sha1 != archive.GetChecksum():
raise Error("SHA1 checksum mismatch on '%s'. Expected %s but got %s" %
- (bundle_name, archive['checksum']['sha1'], sha1))
- if size != archive['size']:
+ (bundle.name, archive.GetChecksum(), sha1))
+ if size != archive.size:
raise Error("Size mismatch on Archive. Expected %s but got %s bytes" %
- (archive['size'], size))
+ (archive.size, size))
InfoPrint('Updating bundle %s to version %s, revision %s' % (
- (bundle_name, bundle[VERSION_KEY], bundle[REVISION_KEY])))
+ (bundle.name, bundle.version, bundle.revision)))
ExtractInstaller(dest_filename, bundle_update_path)
- if bundle_name != SDK_TOOLS:
+ if bundle.name != SDK_TOOLS:
repath = bundle.get('repath', None)
if repath:
bundle_move_path = os.path.join(bundle_update_path, repath)
@@ -871,32 +495,23 @@ def Update(options, argv):
RemoveDir(bundle_update_path)
os.remove(dest_filename)
local_manifest.MergeBundle(bundle)
- local_manifest.WriteFile()
+ WriteManifestToFile(local_manifest, local_manifest_path)
# Test revision numbers, update the bundle accordingly.
# TODO(dspringer): The local file should be refreshed from disk each
# iteration thought this loop so that multiple sdk_updates can run at the
# same time.
if local_manifest.BundleNeedsUpdate(bundle):
if (not update_options.force and os.path.exists(bundle_path) and
- bundle_name != SDK_TOOLS):
+ bundle.name != SDK_TOOLS):
WarningPrint('%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)
+ % bundle.name)
else:
UpdateBundle()
else:
- InfoPrint('%s is already up-to-date.' % bundle_name)
-
- # Validate the arg list against the available bundle names. Raises an
- # error if any invalid bundle names or args are detected.
- valid_args = set([ALL, RECOMMENDED] +
- [bundle[NAME_KEY] for bundle in bundles])
- bad_args = set(args) - valid_args
- if len(bad_args) > 0:
- raise Error("Unrecognized bundle name or argument: '%s'" %
- ', '.join(bad_args))
+ InfoPrint('%s is already up-to-date.' % bundle.name)
#------------------------------------------------------------------------------