OLD | NEW |
---|---|
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import copy | 5 import copy |
6 import hashlib | 6 import hashlib |
7 import json | 7 import json |
8 import string | |
8 import sys | 9 import sys |
10 import urllib2 | |
9 | 11 |
10 MANIFEST_VERSION = 2 | 12 MANIFEST_VERSION = 2 |
11 | 13 |
12 # Some commonly-used key names. | 14 # Some commonly-used key names. |
13 ARCHIVES_KEY = 'archives' | 15 ARCHIVES_KEY = 'archives' |
14 BUNDLES_KEY = 'bundles' | 16 BUNDLES_KEY = 'bundles' |
15 NAME_KEY = 'name' | 17 NAME_KEY = 'name' |
16 REVISION_KEY = 'revision' | 18 REVISION_KEY = 'revision' |
17 VERSION_KEY = 'version' | 19 VERSION_KEY = 'version' |
18 | 20 |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
50 def DictToJSON(pydict): | 52 def DictToJSON(pydict): |
51 """Convert a dict to a JSON-formatted string.""" | 53 """Convert a dict to a JSON-formatted string.""" |
52 pretty_string = json.dumps(pydict, sort_keys=False, indent=2) | 54 pretty_string = json.dumps(pydict, sort_keys=False, indent=2) |
53 # json.dumps sometimes returns trailing whitespace and does not put | 55 # json.dumps sometimes returns trailing whitespace and does not put |
54 # a newline at the end. This code fixes these problems. | 56 # a newline at the end. This code fixes these problems. |
55 pretty_lines = pretty_string.split('\n') | 57 pretty_lines = pretty_string.split('\n') |
56 return '\n'.join([line.rstrip() for line in pretty_lines]) + '\n' | 58 return '\n'.join([line.rstrip() for line in pretty_lines]) + '\n' |
57 | 59 |
58 | 60 |
59 def DownloadAndComputeHash(from_stream, to_stream=None, progress_func=None): | 61 def DownloadAndComputeHash(from_stream, to_stream=None, progress_func=None): |
60 ''' Download the archive data from from-stream and generate sha1 and | 62 '''Download the archive data from from-stream and generate sha1 and |
61 size info. | 63 size info. |
62 | 64 |
63 Args: | 65 Args: |
64 from_stream: An input stream that supports read. | 66 from_stream: An input stream that supports read. |
65 to_stream: [optional] the data is written to to_stream if it is | 67 to_stream: [optional] the data is written to to_stream if it is |
66 provided. | 68 provided. |
67 progress_func: [optional] A function used to report download progress. If | 69 progress_func: [optional] A function used to report download progress. If |
68 provided, progress_func is called with progress=0 at the | 70 provided, progress_func is called with progress=0 at the |
69 beginning of the download, periodically with progress=1 | 71 beginning of the download, periodically with progress=1 |
70 during the download, and progress=100 at the end. | 72 during the download, and progress=100 at the end. |
71 | 73 |
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
139 raise Error('Archive "%s" has no checksum' % host_os) | 141 raise Error('Archive "%s" has no checksum' % host_os) |
140 elif not isinstance(checksum, dict): | 142 elif not isinstance(checksum, dict): |
141 raise Error('Archive "%s" has a checksum, but it is not a dict' % host_os) | 143 raise Error('Archive "%s" has a checksum, but it is not a dict' % host_os) |
142 elif not len(checksum): | 144 elif not len(checksum): |
143 raise Error('Archive "%s" has an empty checksum dict' % host_os) | 145 raise Error('Archive "%s" has an empty checksum dict' % host_os) |
144 # Verify that all key names are valid. | 146 # Verify that all key names are valid. |
145 for key in self: | 147 for key in self: |
146 if key not in VALID_ARCHIVE_KEYS: | 148 if key not in VALID_ARCHIVE_KEYS: |
147 raise Error('Archive "%s" has invalid attribute "%s"' % (host_os, key)) | 149 raise Error('Archive "%s" has invalid attribute "%s"' % (host_os, key)) |
148 | 150 |
151 def UpdateVitals(self, revision): | |
152 """Update the size and checksum information for this archive | |
153 based on the content currently at the URL. | |
154 | |
155 This allows the template mandifest to be maintained without | |
binji
2012/10/16 23:09:10
manifest
| |
156 the need to size and checksums to be present. | |
157 """ | |
158 template = string.Template(self['url']) | |
159 self['url'] = template.substitute({'revision': revision}) | |
160 from_stream = urllib2.urlopen(self['url']) | |
161 sha1_hash, size = DownloadAndComputeHash(from_stream) | |
162 self['size'] = size | |
163 self['checksum'] = { 'sha1': sha1_hash } | |
164 | |
149 def __getattr__(self, name): | 165 def __getattr__(self, name): |
150 """Retrieve values from this dict using attributes. | 166 """Retrieve values from this dict using attributes. |
151 | 167 |
152 This allows for foo.bar instead of foo['bar']. | 168 This allows for foo.bar instead of foo['bar']. |
153 | 169 |
154 Args: | 170 Args: |
155 name: the name of the key, 'bar' in the example above. | 171 name: the name of the key, 'bar' in the example above. |
156 Returns: | 172 Returns: |
157 The value associated with that key.""" | 173 The value associated with that key.""" |
158 if name not in VALID_ARCHIVE_KEYS: | 174 if name not in VALID_ARCHIVE_KEYS: |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
208 bundle: The other bundle. Must be a dict. | 224 bundle: The other bundle. Must be a dict. |
209 """ | 225 """ |
210 for k, v in bundle.iteritems(): | 226 for k, v in bundle.iteritems(): |
211 if k == ARCHIVES_KEY: | 227 if k == ARCHIVES_KEY: |
212 for archive in v: | 228 for archive in v: |
213 self.RemoveArchive(archive['host_os']) | 229 self.RemoveArchive(archive['host_os']) |
214 self.get(k, []).append(archive) | 230 self.get(k, []).append(archive) |
215 else: | 231 else: |
216 self[k] = v | 232 self[k] = v |
217 | 233 |
234 def __str__(self): | |
235 return self.GetDataAsString() | |
236 | |
218 def GetDataAsString(self): | 237 def GetDataAsString(self): |
219 """Returns the JSON bundle object, pretty-printed""" | 238 """Returns the JSON bundle object, pretty-printed""" |
220 return DictToJSON(self) | 239 return DictToJSON(self) |
221 | 240 |
222 def LoadDataFromString(self, json_string): | 241 def LoadDataFromString(self, json_string): |
223 """Load a JSON bundle string. Raises an exception if json_string | 242 """Load a JSON bundle string. Raises an exception if json_string |
224 is not well-formed JSON. | 243 is not well-formed JSON. |
225 | 244 |
226 Args: | 245 Args: |
227 json_string: a JSON-formatted string containing the bundle | 246 json_string: a JSON-formatted string containing the bundle |
(...skipping 10 matching lines...) Expand all Loading... | |
238 if key == ARCHIVES_KEY: | 257 if key == ARCHIVES_KEY: |
239 archives = [] | 258 archives = [] |
240 for a in value: | 259 for a in value: |
241 new_archive = Archive(a['host_os']) | 260 new_archive = Archive(a['host_os']) |
242 new_archive.CopyFrom(a) | 261 new_archive.CopyFrom(a) |
243 archives.append(new_archive) | 262 archives.append(new_archive) |
244 self[ARCHIVES_KEY] = archives | 263 self[ARCHIVES_KEY] = archives |
245 else: | 264 else: |
246 self[key] = value | 265 self[key] = value |
247 | 266 |
248 def Validate(self): | 267 def Validate(self, add_missing_info=False): |
249 """Validate the content of the bundle. Raise an Error if an invalid or | 268 """Validate the content of the bundle. Raise an Error if an invalid or |
250 missing field is found. """ | 269 missing field is found. """ |
251 # Check required fields. | 270 # Check required fields. |
252 if not self.get(NAME_KEY, None): | 271 if not self.get(NAME_KEY): |
253 raise Error('Bundle has no name') | 272 raise Error('Bundle has no name') |
254 if self.get(REVISION_KEY, None) == None: | 273 if self.get(REVISION_KEY) == None: |
255 raise Error('Bundle "%s" is missing a revision number' % self[NAME_KEY]) | 274 raise Error('Bundle "%s" is missing a revision number' % self[NAME_KEY]) |
256 if self.get(VERSION_KEY, None) == None: | 275 if self.get(VERSION_KEY) == None: |
257 raise Error('Bundle "%s" is missing a version number' % self[NAME_KEY]) | 276 raise Error('Bundle "%s" is missing a version number' % self[NAME_KEY]) |
258 if not self.get('description', None): | 277 if not self.get('description'): |
259 raise Error('Bundle "%s" is missing a description' % self[NAME_KEY]) | 278 raise Error('Bundle "%s" is missing a description' % self[NAME_KEY]) |
260 if not self.get('stability', None): | 279 if not self.get('stability'): |
261 raise Error('Bundle "%s" is missing stability info' % self[NAME_KEY]) | 280 raise Error('Bundle "%s" is missing stability info' % self[NAME_KEY]) |
262 if self.get('recommended', None) == None: | 281 if self.get('recommended') == None: |
263 raise Error('Bundle "%s" is missing the recommended field' % | 282 raise Error('Bundle "%s" is missing the recommended field' % |
264 self[NAME_KEY]) | 283 self[NAME_KEY]) |
265 # Check specific values | 284 # Check specific values |
266 if self['stability'] not in STABILITY_LITERALS: | 285 if self['stability'] not in STABILITY_LITERALS: |
267 raise Error('Bundle "%s" has invalid stability field: "%s"' % | 286 raise Error('Bundle "%s" has invalid stability field: "%s"' % |
268 (self[NAME_KEY], self['stability'])) | 287 (self[NAME_KEY], self['stability'])) |
269 if self['recommended'] not in YES_NO_LITERALS: | 288 if self['recommended'] not in YES_NO_LITERALS: |
270 raise Error( | 289 raise Error( |
271 'Bundle "%s" has invalid recommended field: "%s"' % | 290 'Bundle "%s" has invalid recommended field: "%s"' % |
272 (self[NAME_KEY], self['recommended'])) | 291 (self[NAME_KEY], self['recommended'])) |
273 # Verify that all key names are valid. | 292 # Verify that all key names are valid. |
274 for key in self: | 293 for key in self: |
275 if key not in VALID_BUNDLES_KEYS: | 294 if key not in VALID_BUNDLES_KEYS: |
276 raise Error('Bundle "%s" has invalid attribute "%s"' % | 295 raise Error('Bundle "%s" has invalid attribute "%s"' % |
277 (self[NAME_KEY], key)) | 296 (self[NAME_KEY], key)) |
278 # Validate the archives | 297 # Validate the archives |
279 for archive in self[ARCHIVES_KEY]: | 298 for archive in self[ARCHIVES_KEY]: |
299 if add_missing_info and 'size' not in archive: | |
300 archive.UpdateVitals(self[REVISION_KEY]) | |
280 archive.Validate() | 301 archive.Validate() |
281 | 302 |
303 | |
binji
2012/10/16 23:09:10
no newline necessary here
| |
282 def GetArchive(self, host_os_name): | 304 def GetArchive(self, host_os_name): |
283 """Retrieve the archive for the given host os. | 305 """Retrieve the archive for the given host os. |
284 | 306 |
285 Args: | 307 Args: |
286 host_os_name: name of host os whose archive must be retrieved. | 308 host_os_name: name of host os whose archive must be retrieved. |
287 Return: | 309 Return: |
288 An Archive instance or None if it doesn't exist.""" | 310 An Archive instance or None if it doesn't exist.""" |
289 for archive in self[ARCHIVES_KEY]: | 311 for archive in self[ARCHIVES_KEY]: |
290 if archive.host_os == host_os_name or archive.host_os == 'all': | 312 if archive.host_os == host_os_name or archive.host_os == 'all': |
291 return archive | 313 return archive |
(...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
380 For ease of unit-testing, this class should not contain any file I/O. | 402 For ease of unit-testing, this class should not contain any file I/O. |
381 """ | 403 """ |
382 | 404 |
383 def __init__(self): | 405 def __init__(self): |
384 """Create a new SDKManifest object with default contents""" | 406 """Create a new SDKManifest object with default contents""" |
385 self._manifest_data = { | 407 self._manifest_data = { |
386 "manifest_version": MANIFEST_VERSION, | 408 "manifest_version": MANIFEST_VERSION, |
387 "bundles": [], | 409 "bundles": [], |
388 } | 410 } |
389 | 411 |
390 def Validate(self): | 412 def Validate(self, add_missing_info=False): |
391 """Validate the Manifest file and raises an exception for problems""" | 413 """Validate the Manifest file and raises an exception for problems""" |
392 # Validate the manifest top level | 414 # Validate the manifest top level |
393 if self._manifest_data["manifest_version"] > MANIFEST_VERSION: | 415 if self._manifest_data["manifest_version"] > MANIFEST_VERSION: |
394 raise Error("Manifest version too high: %s" % | 416 raise Error("Manifest version too high: %s" % |
395 self._manifest_data["manifest_version"]) | 417 self._manifest_data["manifest_version"]) |
396 # Verify that all key names are valid. | 418 # Verify that all key names are valid. |
397 for key in self._manifest_data: | 419 for key in self._manifest_data: |
398 if key not in VALID_MANIFEST_KEYS: | 420 if key not in VALID_MANIFEST_KEYS: |
399 raise Error('Manifest has invalid attribute "%s"' % key) | 421 raise Error('Manifest has invalid attribute "%s"' % key) |
400 # Validate each bundle | 422 # Validate each bundle |
401 for bundle in self._manifest_data[BUNDLES_KEY]: | 423 for bundle in self._manifest_data[BUNDLES_KEY]: |
402 bundle.Validate() | 424 bundle.Validate(add_missing_info) |
403 | 425 |
404 def GetBundle(self, name): | 426 def GetBundle(self, name): |
405 """Get a bundle from the array of bundles. | 427 """Get a bundle from the array of bundles. |
406 | 428 |
407 Args: | 429 Args: |
408 name: the name of the bundle to return. | 430 name: the name of the bundle to return. |
409 Return: | 431 Return: |
410 The first bundle with the given name, or None if it is not found.""" | 432 The first bundle with the given name, or None if it is not found.""" |
411 if not BUNDLES_KEY in self._manifest_data: | 433 if not BUNDLES_KEY in self._manifest_data: |
412 return None | 434 return None |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
450 Returns: | 472 Returns: |
451 True if Bundle needs to be updated. | 473 True if Bundle needs to be updated. |
452 """ | 474 """ |
453 if NAME_KEY not in bundle: | 475 if NAME_KEY not in bundle: |
454 raise KeyError("Bundle must have a 'name' key.") | 476 raise KeyError("Bundle must have a 'name' key.") |
455 local_bundle = self.GetBundle(bundle[NAME_KEY]) | 477 local_bundle = self.GetBundle(bundle[NAME_KEY]) |
456 return (local_bundle == None) or ( | 478 return (local_bundle == None) or ( |
457 (local_bundle[VERSION_KEY], local_bundle[REVISION_KEY]) < | 479 (local_bundle[VERSION_KEY], local_bundle[REVISION_KEY]) < |
458 (bundle[VERSION_KEY], bundle[REVISION_KEY])) | 480 (bundle[VERSION_KEY], bundle[REVISION_KEY])) |
459 | 481 |
460 def MergeBundle(self, bundle, allow_existing = True): | 482 def MergeBundle(self, bundle, allow_existing=True): |
461 """Merge a Bundle into this manifest. | 483 """Merge a Bundle into this manifest. |
462 | 484 |
463 The new bundle is added if not present, or merged into the existing bundle. | 485 The new bundle is added if not present, or merged into the existing bundle. |
464 | 486 |
465 Args: | 487 Args: |
466 bundle: The bundle to merge. | 488 bundle: The bundle to merge. |
467 """ | 489 """ |
468 if NAME_KEY not in bundle: | 490 if NAME_KEY not in bundle: |
469 raise KeyError("Bundle must have a 'name' key.") | 491 raise KeyError("Bundle must have a 'name' key.") |
470 local_bundle = self.GetBundle(bundle.name) | 492 local_bundle = self.GetBundle(bundle.name) |
471 if not local_bundle: | 493 if not local_bundle: |
472 self.SetBundle(bundle) | 494 self.SetBundle(bundle) |
473 else: | 495 else: |
474 if not allow_existing: | 496 if not allow_existing: |
475 raise Error('cannot merge manifest bundle \'%s\', it already exists' | 497 raise Error('cannot merge manifest bundle \'%s\', it already exists' |
476 % bundle.name) | 498 % bundle.name) |
477 local_bundle.MergeWithBundle(bundle) | 499 local_bundle.MergeWithBundle(bundle) |
478 | 500 |
479 def MergeManifest(self, manifest): | 501 def MergeManifest(self, manifest): |
480 '''Merge another manifest into this manifest, disallowing overiding. | 502 '''Merge another manifest into this manifest, disallowing overiding. |
481 | 503 |
482 Args | 504 Args |
483 manifest: The manifest to merge. | 505 manifest: The manifest to merge. |
484 ''' | 506 ''' |
485 for bundle in manifest.GetBundles(): | 507 for bundle in manifest.GetBundles(): |
486 self.MergeBundle(bundle, allow_existing = False) | 508 self.MergeBundle(bundle, allow_existing=False) |
487 | 509 |
488 def FilterBundles(self, predicate): | 510 def FilterBundles(self, predicate): |
489 """Filter the list of bundles by |predicate|. | 511 """Filter the list of bundles by |predicate|. |
490 | 512 |
491 For all bundles in this manifest, if predicate(bundle) is False, the bundle | 513 For all bundles in this manifest, if predicate(bundle) is False, the bundle |
492 is removed from the manifest. | 514 is removed from the manifest. |
493 | 515 |
494 Args: | 516 Args: |
495 predicate: a function that take a bundle and returns whether True to keep | 517 predicate: a function that take a bundle and returns whether True to keep |
496 it or False to remove it. | 518 it or False to remove it. |
497 """ | 519 """ |
498 self._manifest_data[BUNDLES_KEY] = filter(predicate, self.GetBundles()) | 520 self._manifest_data[BUNDLES_KEY] = filter(predicate, self.GetBundles()) |
499 | 521 |
500 def LoadDataFromString(self, json_string): | 522 def LoadDataFromString(self, json_string, add_missing_info=False): |
501 """Load a JSON manifest string. Raises an exception if json_string | 523 """Load a JSON manifest string. Raises an exception if json_string |
502 is not well-formed JSON. | 524 is not well-formed JSON. |
503 | 525 |
504 Args: | 526 Args: |
505 json_string: a JSON-formatted string containing the previous manifest | 527 json_string: a JSON-formatted string containing the previous manifest |
506 all_hosts: True indicates that we should load bundles for all hosts. | 528 all_hosts: True indicates that we should load bundles for all hosts. |
507 False (default) says to only load bundles for the current host""" | 529 False (default) says to only load bundles for the current host""" |
508 new_manifest = json.loads(json_string) | 530 new_manifest = json.loads(json_string) |
509 for key, value in new_manifest.items(): | 531 for key, value in new_manifest.items(): |
510 if key == BUNDLES_KEY: | 532 if key == BUNDLES_KEY: |
511 # Remap each bundle in |value| to a Bundle instance | 533 # Remap each bundle in |value| to a Bundle instance |
512 bundles = [] | 534 bundles = [] |
513 for b in value: | 535 for b in value: |
514 new_bundle = Bundle(b[NAME_KEY]) | 536 new_bundle = Bundle(b[NAME_KEY]) |
515 new_bundle.CopyFrom(b) | 537 new_bundle.CopyFrom(b) |
516 bundles.append(new_bundle) | 538 bundles.append(new_bundle) |
517 self._manifest_data[key] = bundles | 539 self._manifest_data[key] = bundles |
518 else: | 540 else: |
519 self._manifest_data[key] = value | 541 self._manifest_data[key] = value |
520 self.Validate() | 542 self.Validate(add_missing_info) |
543 | |
544 def __str__(self): | |
545 return self.GetDataAsString() | |
521 | 546 |
522 def GetDataAsString(self): | 547 def GetDataAsString(self): |
523 """Returns the current JSON manifest object, pretty-printed""" | 548 """Returns the current JSON manifest object, pretty-printed""" |
524 return DictToJSON(self._manifest_data) | 549 return DictToJSON(self._manifest_data) |
OLD | NEW |