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 |
| 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 |
282 def GetArchive(self, host_os_name): | 303 def GetArchive(self, host_os_name): |
283 """Retrieve the archive for the given host os. | 304 """Retrieve the archive for the given host os. |
284 | 305 |
285 Args: | 306 Args: |
286 host_os_name: name of host os whose archive must be retrieved. | 307 host_os_name: name of host os whose archive must be retrieved. |
287 Return: | 308 Return: |
288 An Archive instance or None if it doesn't exist.""" | 309 An Archive instance or None if it doesn't exist.""" |
289 for archive in self[ARCHIVES_KEY]: | 310 for archive in self[ARCHIVES_KEY]: |
(...skipping 90 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. | 401 For ease of unit-testing, this class should not contain any file I/O. |
381 """ | 402 """ |
382 | 403 |
383 def __init__(self): | 404 def __init__(self): |
384 """Create a new SDKManifest object with default contents""" | 405 """Create a new SDKManifest object with default contents""" |
385 self._manifest_data = { | 406 self._manifest_data = { |
386 "manifest_version": MANIFEST_VERSION, | 407 "manifest_version": MANIFEST_VERSION, |
387 "bundles": [], | 408 "bundles": [], |
388 } | 409 } |
389 | 410 |
390 def Validate(self): | 411 def Validate(self, add_missing_info=False): |
391 """Validate the Manifest file and raises an exception for problems""" | 412 """Validate the Manifest file and raises an exception for problems""" |
392 # Validate the manifest top level | 413 # Validate the manifest top level |
393 if self._manifest_data["manifest_version"] > MANIFEST_VERSION: | 414 if self._manifest_data["manifest_version"] > MANIFEST_VERSION: |
394 raise Error("Manifest version too high: %s" % | 415 raise Error("Manifest version too high: %s" % |
395 self._manifest_data["manifest_version"]) | 416 self._manifest_data["manifest_version"]) |
396 # Verify that all key names are valid. | 417 # Verify that all key names are valid. |
397 for key in self._manifest_data: | 418 for key in self._manifest_data: |
398 if key not in VALID_MANIFEST_KEYS: | 419 if key not in VALID_MANIFEST_KEYS: |
399 raise Error('Manifest has invalid attribute "%s"' % key) | 420 raise Error('Manifest has invalid attribute "%s"' % key) |
400 # Validate each bundle | 421 # Validate each bundle |
401 for bundle in self._manifest_data[BUNDLES_KEY]: | 422 for bundle in self._manifest_data[BUNDLES_KEY]: |
402 bundle.Validate() | 423 bundle.Validate(add_missing_info) |
403 | 424 |
404 def GetBundle(self, name): | 425 def GetBundle(self, name): |
405 """Get a bundle from the array of bundles. | 426 """Get a bundle from the array of bundles. |
406 | 427 |
407 Args: | 428 Args: |
408 name: the name of the bundle to return. | 429 name: the name of the bundle to return. |
409 Return: | 430 Return: |
410 The first bundle with the given name, or None if it is not found.""" | 431 The first bundle with the given name, or None if it is not found.""" |
411 if not BUNDLES_KEY in self._manifest_data: | 432 if not BUNDLES_KEY in self._manifest_data: |
412 return None | 433 return None |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
450 Returns: | 471 Returns: |
451 True if Bundle needs to be updated. | 472 True if Bundle needs to be updated. |
452 """ | 473 """ |
453 if NAME_KEY not in bundle: | 474 if NAME_KEY not in bundle: |
454 raise KeyError("Bundle must have a 'name' key.") | 475 raise KeyError("Bundle must have a 'name' key.") |
455 local_bundle = self.GetBundle(bundle[NAME_KEY]) | 476 local_bundle = self.GetBundle(bundle[NAME_KEY]) |
456 return (local_bundle == None) or ( | 477 return (local_bundle == None) or ( |
457 (local_bundle[VERSION_KEY], local_bundle[REVISION_KEY]) < | 478 (local_bundle[VERSION_KEY], local_bundle[REVISION_KEY]) < |
458 (bundle[VERSION_KEY], bundle[REVISION_KEY])) | 479 (bundle[VERSION_KEY], bundle[REVISION_KEY])) |
459 | 480 |
460 def MergeBundle(self, bundle, allow_existing = True): | 481 def MergeBundle(self, bundle, allow_existing=True): |
461 """Merge a Bundle into this manifest. | 482 """Merge a Bundle into this manifest. |
462 | 483 |
463 The new bundle is added if not present, or merged into the existing bundle. | 484 The new bundle is added if not present, or merged into the existing bundle. |
464 | 485 |
465 Args: | 486 Args: |
466 bundle: The bundle to merge. | 487 bundle: The bundle to merge. |
467 """ | 488 """ |
468 if NAME_KEY not in bundle: | 489 if NAME_KEY not in bundle: |
469 raise KeyError("Bundle must have a 'name' key.") | 490 raise KeyError("Bundle must have a 'name' key.") |
470 local_bundle = self.GetBundle(bundle.name) | 491 local_bundle = self.GetBundle(bundle.name) |
471 if not local_bundle: | 492 if not local_bundle: |
472 self.SetBundle(bundle) | 493 self.SetBundle(bundle) |
473 else: | 494 else: |
474 if not allow_existing: | 495 if not allow_existing: |
475 raise Error('cannot merge manifest bundle \'%s\', it already exists' | 496 raise Error('cannot merge manifest bundle \'%s\', it already exists' |
476 % bundle.name) | 497 % bundle.name) |
477 local_bundle.MergeWithBundle(bundle) | 498 local_bundle.MergeWithBundle(bundle) |
478 | 499 |
479 def MergeManifest(self, manifest): | 500 def MergeManifest(self, manifest): |
480 '''Merge another manifest into this manifest, disallowing overiding. | 501 '''Merge another manifest into this manifest, disallowing overiding. |
481 | 502 |
482 Args | 503 Args |
483 manifest: The manifest to merge. | 504 manifest: The manifest to merge. |
484 ''' | 505 ''' |
485 for bundle in manifest.GetBundles(): | 506 for bundle in manifest.GetBundles(): |
486 self.MergeBundle(bundle, allow_existing = False) | 507 self.MergeBundle(bundle, allow_existing=False) |
487 | 508 |
488 def FilterBundles(self, predicate): | 509 def FilterBundles(self, predicate): |
489 """Filter the list of bundles by |predicate|. | 510 """Filter the list of bundles by |predicate|. |
490 | 511 |
491 For all bundles in this manifest, if predicate(bundle) is False, the bundle | 512 For all bundles in this manifest, if predicate(bundle) is False, the bundle |
492 is removed from the manifest. | 513 is removed from the manifest. |
493 | 514 |
494 Args: | 515 Args: |
495 predicate: a function that take a bundle and returns whether True to keep | 516 predicate: a function that take a bundle and returns whether True to keep |
496 it or False to remove it. | 517 it or False to remove it. |
497 """ | 518 """ |
498 self._manifest_data[BUNDLES_KEY] = filter(predicate, self.GetBundles()) | 519 self._manifest_data[BUNDLES_KEY] = filter(predicate, self.GetBundles()) |
499 | 520 |
500 def LoadDataFromString(self, json_string): | 521 def LoadDataFromString(self, json_string, add_missing_info=False): |
501 """Load a JSON manifest string. Raises an exception if json_string | 522 """Load a JSON manifest string. Raises an exception if json_string |
502 is not well-formed JSON. | 523 is not well-formed JSON. |
503 | 524 |
504 Args: | 525 Args: |
505 json_string: a JSON-formatted string containing the previous manifest | 526 json_string: a JSON-formatted string containing the previous manifest |
506 all_hosts: True indicates that we should load bundles for all hosts. | 527 all_hosts: True indicates that we should load bundles for all hosts. |
507 False (default) says to only load bundles for the current host""" | 528 False (default) says to only load bundles for the current host""" |
508 new_manifest = json.loads(json_string) | 529 new_manifest = json.loads(json_string) |
509 for key, value in new_manifest.items(): | 530 for key, value in new_manifest.items(): |
510 if key == BUNDLES_KEY: | 531 if key == BUNDLES_KEY: |
511 # Remap each bundle in |value| to a Bundle instance | 532 # Remap each bundle in |value| to a Bundle instance |
512 bundles = [] | 533 bundles = [] |
513 for b in value: | 534 for b in value: |
514 new_bundle = Bundle(b[NAME_KEY]) | 535 new_bundle = Bundle(b[NAME_KEY]) |
515 new_bundle.CopyFrom(b) | 536 new_bundle.CopyFrom(b) |
516 bundles.append(new_bundle) | 537 bundles.append(new_bundle) |
517 self._manifest_data[key] = bundles | 538 self._manifest_data[key] = bundles |
518 else: | 539 else: |
519 self._manifest_data[key] = value | 540 self._manifest_data[key] = value |
520 self.Validate() | 541 self.Validate(add_missing_info) |
| 542 |
| 543 def __str__(self): |
| 544 return self.GetDataAsString() |
521 | 545 |
522 def GetDataAsString(self): | 546 def GetDataAsString(self): |
523 """Returns the current JSON manifest object, pretty-printed""" | 547 """Returns the current JSON manifest object, pretty-printed""" |
524 return DictToJSON(self._manifest_data) | 548 return DictToJSON(self._manifest_data) |
OLD | NEW |