Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(817)

Side by Side Diff: chrome/common/extensions/docs/build/directory.py

Issue 4106007: Only generate zips for Chrome extension samples whose contents have changed. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/chrome/common/extensions/docs
Patch Set: Reapply patch after unicode fixes. Created 10 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Class for parsing metadata about extension samples.""" 6 """Class for parsing metadata about extension samples."""
7 7
8 import locale 8 import locale
9 import os 9 import os
10 import os.path 10 import os.path
(...skipping 207 matching lines...) Expand 10 before | Expand all | Expand 10 after
218 218
219 manifest_data = {'samples': samples, 'api': api_method_dict} 219 manifest_data = {'samples': samples, 'api': api_method_dict}
220 return manifest_data 220 return manifest_data
221 221
222 def writeToFile(self, path): 222 def writeToFile(self, path):
223 """ Writes the contents of this manifest file as a JSON-encoded text file. 223 """ Writes the contents of this manifest file as a JSON-encoded text file.
224 224
225 Args: 225 Args:
226 path: The path to write the samples manifest file to. 226 path: The path to write the samples manifest file to.
227 """ 227 """
228 228 manifest_text = json.dumps(self._manifest_data, indent=2, sort_keys=True)
229 manifest_text = json.dumps(self._manifest_data, indent=2)
230 output_path = os.path.realpath(path) 229 output_path = os.path.realpath(path)
231 try: 230 try:
232 output_file = open(output_path, 'w') 231 output_file = open(output_path, 'w')
233 except IOError, msg: 232 except IOError, msg:
234 raise Exception("Failed to write the samples manifest file." 233 raise Exception("Failed to write the samples manifest file."
235 "The specific error was: %s." % msg) 234 "The specific error was: %s." % msg)
236 output_file.write(manifest_text) 235 output_file.write(manifest_text)
237 output_file.close() 236 output_file.close()
238 237
239 def writeZippedSamples(self): 238 def writeZippedSamples(self):
240 """ For each sample in the current manifest, create a zip file with the 239 """ For each sample in the current manifest, create a zip file with the
241 sample contents in the sample's parent directory. """ 240 sample contents in the sample's parent directory if not zip exists, or
241 update the zip file if the sample has been updated.
242 242
243 Returns:
244 A set of paths representing zip files which have been modified.
245 """
246 modified_paths = []
243 for sample in self._manifest_data['samples']: 247 for sample in self._manifest_data['samples']:
244 sample.write_zip() 248 path = sample.write_zip()
249 if path:
250 modified_paths.append(path)
251 return modified_paths
245 252
246 class Sample(dict): 253 class Sample(dict):
247 """ Represents metadata about a Chrome extension sample. 254 """ Represents metadata about a Chrome extension sample.
248 255
249 Extends dict so that it can be easily JSON serialized. 256 Extends dict so that it can be easily JSON serialized.
250 """ 257 """
251 258
252 def __init__(self, manifest_path, api_methods, base_dir): 259 def __init__(self, manifest_path, api_methods, base_dir):
253 """ Initializes a Sample instance given a path to a manifest. 260 """ Initializes a Sample instance given a path to a manifest.
254 261
255 Args: 262 Args:
256 manifest_path: A filesystem path to a manifest file. 263 manifest_path: A filesystem path to a manifest file.
257 api_methods: A list of strings containing all possible Chrome extension 264 api_methods: A list of strings containing all possible Chrome extension
258 API calls. 265 API calls.
259 base_dir: The base directory where this sample will be referenced from - 266 base_dir: The base directory where this sample will be referenced from -
260 paths will be made relative to this directory. 267 paths will be made relative to this directory.
261 """ 268 """
262 self._base_dir = base_dir 269 self._base_dir = base_dir
263 self._manifest_path = manifest_path 270 self._manifest_path = manifest_path
264 self._manifest = parse_json_file(self._manifest_path) 271 self._manifest = parse_json_file(self._manifest_path)
265 self._locale_data = self._parse_locale_data() 272 self._locale_data = self._parse_locale_data()
266 273
267 # The following properties will be serialized when converting this object 274 # The following calls set data which will be serialized when converting
268 # to JSON. 275 # this object to JSON.
276 source_data = self._parse_source_data(api_methods)
277 self['api_calls'] = source_data['api_calls']
278 self['source_files'] = source_data['source_files']
279 self['source_hash'] = source_data['source_hash']
269 280
270 self['api_calls'] = self._parse_api_calls(api_methods)
271 self['name'] = self._parse_name() 281 self['name'] = self._parse_name()
272 self['description'] = self._parse_description() 282 self['description'] = self._parse_description()
273 self['icon'] = self._parse_icon() 283 self['icon'] = self._parse_icon()
274 self['features'] = self._parse_features() 284 self['features'] = self._parse_features()
275 self['protocols'] = self._parse_protocols() 285 self['protocols'] = self._parse_protocols()
276 self['path'] = self._get_relative_path() 286 self['path'] = self._get_relative_path()
277 self['search_string'] = self._get_search_string() 287 self['search_string'] = self._get_search_string()
278 self['source_files'] = self._parse_source_files()
279 self['id'] = hashlib.sha1(self['path']).hexdigest() 288 self['id'] = hashlib.sha1(self['path']).hexdigest()
280 self['zip_path'] = self._get_relative_zip_path() 289 self['zip_path'] = self._get_relative_zip_path()
281 290
282 _FEATURE_ATTRIBUTES = ( 291 _FEATURE_ATTRIBUTES = (
283 'browser_action', 292 'browser_action',
284 'page_action', 293 'page_action',
285 'background_page', 294 'background_page',
286 'options_page', 295 'options_page',
287 'plugins', 296 'plugins',
288 'theme', 297 'theme',
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after
414 """ Returns the filename to be used for a generated zip of the sample. 423 """ Returns the filename to be used for a generated zip of the sample.
415 424
416 Returns: 425 Returns:
417 A string in the form of "<dirname>.zip" where <dirname> is the name 426 A string in the form of "<dirname>.zip" where <dirname> is the name
418 of the directory containing this sample's manifest.json. 427 of the directory containing this sample's manifest.json.
419 """ 428 """
420 sample_path = os.path.realpath(os.path.dirname(self._manifest_path)) 429 sample_path = os.path.realpath(os.path.dirname(self._manifest_path))
421 sample_dirname = os.path.basename(sample_path) 430 sample_dirname = os.path.basename(sample_path)
422 return "%s.zip" % sample_dirname 431 return "%s.zip" % sample_dirname
423 432
424 def _parse_api_calls(self, api_methods):
425 """ Returns a list of Chrome extension API calls the sample makes.
426
427 Parses any *.html and *.js files in the sample directory and matches them
428 against the supplied list of all available API methods, returning methods
429 which show up in the sample code.
430
431 Args:
432 api_methods: A list of strings containing the potential
433 API calls the and the extension sample could be making.
434
435 Returns:
436 A set of every member of api_methods that appears in any *.html or *.js
437 file contained in this sample's directory (or subdirectories).
438
439 Raises:
440 Exception: If any of the *.html or *.js files cannot be read.
441 """
442 api_calls = set()
443 extension_dir_path = os.path.dirname(self._manifest_path)
444 for root, dirs, files in sorted_walk(extension_dir_path):
445 for file in files:
446 if file[-5:] == '.html' or file[-3:] == '.js':
447 path = os.path.join(root, file)
448 try:
449 code_file = open(path, "r")
450 except IOError, msg:
451 raise Exception("Failed to read %s: %s" % (path, msg))
452 code_contents = unicode(code_file.read(), errors="replace")
453 code_file.close()
454
455 for method in api_methods:
456 if (code_contents.find(method) > -1):
457 api_calls.add(method)
458 return sorted(api_calls)
459
460 def _parse_source_files(self):
461 """ Returns a list of paths to source files present in the extenion.
462
463 Returns:
464 A list of paths relative to the manifest file directory.
465 """
466 source_paths = []
467 base_path = os.path.realpath(os.path.dirname(self._manifest_path))
468 for root, directories, files in sorted_walk(base_path):
469 if '.svn' in directories:
470 directories.remove('.svn') # Don't go into SVN metadata directories
471
472 for file_name in files:
473 ext = os.path.splitext(file_name)[1]
474 if ext in self._SOURCE_FILE_EXTENSIONS:
475 path = os.path.realpath(os.path.join(root, file_name))
476 path = path.replace(base_path, '')[1:]
477 source_paths.append(path)
478 return sorted(source_paths)
479
480 def _parse_description(self): 433 def _parse_description(self):
481 """ Returns a localized description of the extension. 434 """ Returns a localized description of the extension.
482 435
483 Returns: 436 Returns:
484 A localized version of the sample's description. 437 A localized version of the sample's description.
485 """ 438 """
486 return self._get_localized_manifest_value('description') 439 return self._get_localized_manifest_value('description')
487 440
488 def _parse_features(self): 441 def _parse_features(self):
489 """ Returns a list of features the sample uses. 442 """ Returns a list of features the sample uses.
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after
570 A list of every unique protocol listed in the manifest's permssions. 523 A list of every unique protocol listed in the manifest's permssions.
571 """ 524 """
572 protocols = [] 525 protocols = []
573 if self._manifest.has_key('permissions'): 526 if self._manifest.has_key('permissions'):
574 for permission in self._manifest['permissions']: 527 for permission in self._manifest['permissions']:
575 split = permission.split('://') 528 split = permission.split('://')
576 if (len(split) == 2) and (split[0] not in protocols): 529 if (len(split) == 2) and (split[0] not in protocols):
577 protocols.append(split[0] + "://") 530 protocols.append(split[0] + "://")
578 return protocols 531 return protocols
579 532
533 def _parse_source_data(self, api_methods):
534 """ Iterates over the sample's source files and parses data from them.
535
536 Parses any files in the sample directory with known source extensions
537 (as defined in self._SOURCE_FILE_EXTENSIONS). For each file, this method:
538
539 1. Stores a relative path from the manifest.json directory to the file.
540 2. Searches through the contents of the file for chrome.* API calls.
541 3. Calculates a SHA1 digest for the contents of the file.
542
543 Args:
544 api_methods: A list of strings containing the potential
545 API calls the and the extension sample could be making.
546
547 Raises:
548 Exception: If any of the source files cannot be read.
549
550 Returns:
551 A dictionary containing the keys/values:
552 'api_calls' A sorted list of API calls the sample makes.
553 'source_files' A sorted list of paths to files the extension uses.
554 'source_hash' A hash of the individual file hashes.
555 """
556 data = {}
557 source_paths = []
558 source_hashes = []
559 api_calls = set()
560 base_path = os.path.realpath(os.path.dirname(self._manifest_path))
561 for root, directories, files in sorted_walk(base_path):
562 if '.svn' in directories:
563 directories.remove('.svn') # Don't go into SVN metadata directories
564
565 for file_name in files:
566 ext = os.path.splitext(file_name)[1]
567 if ext in self._SOURCE_FILE_EXTENSIONS:
568 # Add the file path to the list of source paths.
569 fullpath = os.path.realpath(os.path.join(root, file_name))
570 path = fullpath.replace(base_path, '')[1:]
571 source_paths.append(path)
572
573 # Read the contents and parse out API calls.
574 try:
575 code_file = open(fullpath, "r")
576 except IOError, msg:
577 raise Exception("Failed to read %s: %s" % (fullpath, msg))
578 code_contents = unicode(code_file.read(), errors="replace")
579 code_file.close()
580 for method in api_methods:
581 if (code_contents.find(method) > -1):
582 api_calls.add(method)
583
584 # Get a hash of the file contents for zip file generation.
585 hash = hashlib.sha1(code_contents.encode("ascii", "replace"))
586 source_hashes.append(hash.hexdigest())
587
588 data['api_calls'] = sorted(api_calls)
589 data['source_files'] = sorted(source_paths)
590 data['source_hash'] = hashlib.sha1(''.join(source_hashes)).hexdigest()
591 return data
592
580 def _uses_background(self): 593 def _uses_background(self):
581 """ Returns true if the extension defines a background page. """ 594 """ Returns true if the extension defines a background page. """
582 return self._manifest.has_key('background_page') 595 return self._manifest.has_key('background_page')
583 596
584 def _uses_browser_action(self): 597 def _uses_browser_action(self):
585 """ Returns true if the extension defines a browser action. """ 598 """ Returns true if the extension defines a browser action. """
586 return self._manifest.has_key('browser_action') 599 return self._manifest.has_key('browser_action')
587 600
588 def _uses_content_scripts(self): 601 def _uses_content_scripts(self):
589 """ Returns true if the extension uses content scripts. """ 602 """ Returns true if the extension uses content scripts. """
(...skipping 21 matching lines...) Expand all
611 return self._manifest.has_key('app') 624 return self._manifest.has_key('app')
612 625
613 def write_zip(self): 626 def write_zip(self):
614 """ Writes a zip file containing all of the files in this Sample's dir.""" 627 """ Writes a zip file containing all of the files in this Sample's dir."""
615 sample_path = os.path.realpath(os.path.dirname(self._manifest_path)) 628 sample_path = os.path.realpath(os.path.dirname(self._manifest_path))
616 sample_dirname = os.path.basename(sample_path) 629 sample_dirname = os.path.basename(sample_path)
617 sample_parentpath = os.path.dirname(sample_path) 630 sample_parentpath = os.path.dirname(sample_path)
618 631
619 zip_filename = self._get_zip_filename() 632 zip_filename = self._get_zip_filename()
620 zip_path = os.path.join(sample_parentpath, zip_filename) 633 zip_path = os.path.join(sample_parentpath, zip_filename)
634 zip_manifest_path = os.path.join(sample_dirname, 'manifest.json')
635
636 zipfile.ZipFile.debug = 3
637
638 if os.path.isfile(zip_path):
639 try:
640 old_zip_file = zipfile.ZipFile(zip_path, 'r')
641 except IOError, msg:
642 raise Exception("Could not read zip at %s: %s" % (zip_path, msg))
643
644 try:
645 info = old_zip_file.getinfo(zip_manifest_path)
646 hash = info.comment
647 if hash == self['source_hash']:
648 return None # Hashes match - no need to generate file
649 except KeyError, msg:
650 pass # The old zip file doesn't contain a hash - overwrite
651 finally:
652 old_zip_file.close()
653
621 zip_file = zipfile.ZipFile(zip_path, 'w') 654 zip_file = zipfile.ZipFile(zip_path, 'w')
622 655
623 try: 656 try:
624 for root, dirs, files in sorted_walk(sample_path): 657 for root, dirs, files in sorted_walk(sample_path):
625 if '.svn' in dirs: 658 if '.svn' in dirs:
626 dirs.remove('.svn') 659 dirs.remove('.svn')
627 for file in files: 660 for file in files:
628 # Absolute path to the file to be added. 661 # Absolute path to the file to be added.
629 abspath = os.path.realpath(os.path.join(root, file)) 662 abspath = os.path.realpath(os.path.join(root, file))
630 # Relative path to store the file in under the zip. 663 # Relative path to store the file in under the zip.
631 relpath = sample_dirname + abspath.replace(sample_path, "") 664 relpath = sample_dirname + abspath.replace(sample_path, "")
665
632 zip_file.write(abspath, relpath) 666 zip_file.write(abspath, relpath)
667 if file == 'manifest.json':
668 info = zip_file.getinfo(zip_manifest_path)
669 info.comment = self['source_hash']
633 except RuntimeError, msg: 670 except RuntimeError, msg:
634 raise Exception("Could not write zip at " % zip_path) 671 raise Exception("Could not write zip at %s: %s" % (zip_path, msg))
635 finally: 672 finally:
636 zip_file.close() 673 zip_file.close()
674
675 return self._get_relative_zip_path()
OLDNEW
« no previous file with comments | « chrome/common/extensions/docs/build/build.py ('k') | chrome/common/extensions/docs/examples/api/bookmarks/basic.zip » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698