Index: chrome/browser/resources/chromeos/chromevox/tools/jsbundler.py |
diff --git a/chrome/browser/resources/chromeos/chromevox/tools/jsbundler.py b/chrome/browser/resources/chromeos/chromevox/tools/jsbundler.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..051813e4066897685c432dbb05ce9b678180b5ed |
--- /dev/null |
+++ b/chrome/browser/resources/chromeos/chromevox/tools/jsbundler.py |
@@ -0,0 +1,300 @@ |
+#!/usr/bin/env python |
+ |
+# Copyright 2014 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. |
+ |
+'''Produces various output formats from a set of JavaScript files with |
+closure style require/provide calls. |
+ |
+Scans one or more directory trees for JavaScript files. Then, from a |
+given list of top-level files, sorts all required input files topologically. |
+The top-level files are appended to the sorted list in the order specified |
+on the command line. If no root directories are specified, the source |
+files are assumed to be ordered already and no dependency analysis is |
+performed. The resulting file list can then be used in one of the following |
+ways: |
+ |
+- list: a plain list of files, one per line is output. |
+ |
+- html: a series of html <script> tags with src attributes containing paths |
+ is output. |
+ |
+- bundle: a concatenation of all the files, separated by newlines is output. |
+ |
+- compressed_bundle: A bundle where non-significant whitespace, including |
+ comments, has been stripped is output. |
+ |
+- copy: the files are copied, or hard linked if possible, to the destination |
+ directory. In this case, no output is generated. |
+''' |
+ |
+ |
+import optparse |
+import os |
+import shutil |
+import sys |
+ |
+_SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__)) |
+_CHROME_SOURCE = os.path.realpath( |
+ os.path.join(_SCRIPT_DIR, *[os.path.pardir] * 6)) |
+sys.path.insert(0, os.path.join( |
+ _CHROME_SOURCE, 'third_party/WebKit/Source/build/scripts')) |
+sys.path.insert(0, os.path.join( |
+ _CHROME_SOURCE, ('chrome/third_party/chromevox/third_party/' + |
+ 'closure-library/closure/bin/build'))) |
+import depstree |
+import rjsmin |
+import source |
+import treescan |
+ |
+ |
+def Die(message): |
+ '''Prints an error message and exit the program.''' |
+ print >>sys.stderr, message |
+ sys.exit(1) |
+ |
+ |
+class SourceWithPaths(source.Source): |
+ '''A source.Source object with its relative input and output paths''' |
+ |
+ def __init__(self, content, in_path, out_path): |
+ super(SourceWithPaths, self).__init__(content) |
+ self._in_path = in_path |
+ self._out_path = out_path |
+ |
+ def GetInPath(self): |
+ return self._in_path |
+ |
+ def GetOutPath(self): |
+ return self._out_path |
+ |
+ |
+class Bundle(): |
+ '''An ordered list of sources without duplicates.''' |
+ |
+ def __init__(self): |
+ self._added_paths = set() |
+ self._added_sources = [] |
+ |
+ def Add(self, sources): |
+ '''Appends one or more source objects the list if it doesn't already |
+ exist. |
+ |
+ Args: |
+ sources: A SourceWithPath or an iterable of such objects. |
+ ''' |
+ if isinstance(sources, SourceWithPaths): |
+ sources = [sources] |
+ for source in sources: |
+ path = source.GetInPath() |
+ if path not in self._added_paths: |
+ self._added_paths.add(path) |
+ self._added_sources.append(source) |
+ |
+ def GetOutPaths(self): |
+ return (source.GetOutPath() for source in self._added_sources) |
+ |
+ def GetSources(self): |
+ return self._added_sources |
+ |
+ def GetUncompressedSource(self): |
+ return '\n'.join((s.GetSource() for s in self._added_sources)) |
+ |
+ def GetCompressedSource(self): |
+ return rjsmin.jsmin(self.GetUncompressedSource()) |
+ |
+ |
+class PathRewriter(): |
+ '''A list of simple path rewrite rules to map relative input paths to |
+ relative output paths. |
+ ''' |
+ |
+ def __init__(self, specs): |
+ '''Args: |
+ specs: A list of mappings, each consisting of the input prefix and |
+ the corresponding output prefix separated by colons. |
+ ''' |
+ self._prefix_map = [] |
+ for spec in specs: |
+ parts = spec.split(':') |
+ if len(parts) != 2: |
+ Die('Invalid prefix rewrite spec %s' % spec) |
+ if not parts[0].endswith('/'): |
+ parts[0] += '/' |
+ self._prefix_map.append(parts) |
+ |
+ def RewritePath(self, in_path): |
+ '''Rewrites an input path according to the list of rules. |
+ |
+ Args: |
+ in_path, str: The input path to rewrite. |
+ Returns: |
+ str: The corresponding output path. |
+ ''' |
+ for in_prefix, out_prefix in self._prefix_map: |
+ if in_path.startswith(in_prefix): |
+ return os.path.join(out_prefix, in_path[len(in_prefix):]) |
+ return in_path |
+ |
+ |
+def ReadSources(options, args): |
+ '''Reads all source specified on the command line, including sources |
+ included by --root options. |
+ ''' |
+ |
+ def EnsureSourceLoaded(in_path, sources, path_rewriter): |
+ if in_path not in sources: |
+ out_path = path_rewriter.RewritePath(in_path) |
+ sources[in_path] = SourceWithPaths(source.GetFileContents(in_path), |
+ in_path, out_path) |
+ |
+ # Only read the actual source file if we will do a dependency analysis or |
+ # if we'll need it for the output. |
+ need_source_text = (len(options.roots) > 0 or |
+ options.mode in ('bundle', 'compressed_bundle')) |
+ path_rewriter = PathRewriter(options.prefix_map) |
+ sources = {} |
+ for root in options.roots: |
+ for name in treescan.ScanTreeForJsFiles(root): |
+ EnsureSourceLoaded(name, sources, path_rewriter) |
+ for path in args: |
+ if need_source_text: |
+ EnsureSourceLoaded(path, sources, path_rewriter) |
+ else: |
+ # Just add an empty representation of the source. |
+ sources[path] = SourceWithPaths( |
+ '', path, path_rewriter.RewritePath(path)) |
+ return sources |
+ |
+ |
+def CalcDeps(bundle, sources, top_level): |
+ '''Calculates dependencies for a set of top-level files. |
+ |
+ Args: |
+ bundle: Bundle to add the sources to. |
+ sources, dict: Mapping from input path to SourceWithPaths objects. |
+ top_level, list: List of top-level input paths to calculate dependencies |
+ for. |
+ ''' |
+ def GetBase(sources): |
+ for source in sources.itervalues(): |
+ if (os.path.basename(source.GetInPath()) == 'base.js' and |
+ 'goog' in source.provides): |
+ return source |
+ Die('goog.base not provided by any file') |
+ |
+ providers = [s for s in sources.itervalues() if len(s.provides) > 0] |
+ deps = depstree.DepsTree(providers) |
+ namespaces = [] |
+ for path in top_level: |
+ namespaces.extend(sources[path].requires) |
+ # base.js is an implicit dependency that always goes first. |
+ bundle.Add(GetBase(sources)) |
+ bundle.Add(deps.GetDependencies(namespaces)) |
+ |
+ |
+def LinkOrCopyFiles(sources, dest_dir): |
+ '''Copies a list of sources to a destination directory.''' |
+ |
+ def LinkOrCopyOneFile(src, dst): |
+ if not os.path.exists(os.path.dirname(dst)): |
+ os.makedirs(os.path.dirname(dst)) |
+ if os.path.exists(dst): |
+ # Avoid clobbering the inode if source and destination refer to the |
+ # same file already. |
+ if os.path.samefile(src, dst): |
+ return |
+ os.unlink(dst) |
+ try: |
+ os.link(src, dst) |
+ except: |
+ shutil.copy(src, dst) |
+ |
+ for source in sources: |
+ LinkOrCopyOneFile(source.GetInPath(), |
+ os.path.join(dest_dir, source.GetOutPath())) |
+ |
+ |
+def WriteOutput(bundle, format, out_file, dest_dir): |
+ '''Writes output in the specified format. |
+ |
+ Args: |
+ bundle: The ordered bundle iwth all sources already added. |
+ format: Output format, one of list, html, bundle, compressed_bundle. |
+ out_file: File object to receive the output. |
+ dest_dir: Prepended to each path mentioned in the output, if applicable. |
+ ''' |
+ if format == 'list': |
+ paths = bundle.GetOutPaths() |
+ if dest_dir: |
+ paths = (os.path.join(dest_dir, p) for p in paths) |
+ paths = (os.path.normpath(p) for p in paths) |
+ out_file.write('\n'.join(paths)) |
+ elif format == 'html': |
+ HTML_TEMPLATE = '<script src=\'%s\'>' |
+ script_lines = (HTML_TEMPLATE % p for p in bundle.GetOutPaths()) |
+ out_file.write('\n'.join(script_lines)) |
+ elif format == 'bundle': |
+ out_file.write(bundle.GetUncompressedSource()) |
+ elif format == 'compressed_bundle': |
+ out_file.write(bundle.GetCompressedSource()) |
+ out_file.write('\n') |
+ |
+ |
+def CreateOptionParser(): |
+ parser = optparse.OptionParser(description=__doc__) |
+ parser.usage = '%prog [options] <top_level_file>...' |
+ parser.add_option('-d', '--dest_dir', action='store', metavar='DIR', |
+ help=('Destination directory. Used when translating ' + |
+ 'input paths to output paths and when copying ' |
+ 'files.')) |
+ parser.add_option('-o', '--output_file', action='store', metavar='FILE', |
+ help=('File to output result to for modes that output ' |
+ 'a single file.')) |
+ parser.add_option('-r', '--root', dest='roots', action='append', default=[], |
+ metavar='ROOT', |
+ help='Roots of directory trees to scan for sources.') |
+ parser.add_option('-w', '--rewrite_prefix', action='append', default=[], |
+ dest='prefix_map', metavar='SPEC', |
+ help=('Two path prefixes, separated by colons ' + |
+ 'specifying that a file whose (relative) path ' + |
+ 'name starts with the first prefix should have ' + |
+ 'that prefix replaced by the second prefix to ' + |
+ 'form a path relative to the output directory.')) |
+ parser.add_option('-m', '--mode', type='choice', action='store', |
+ choices=['list', 'html', 'bundle', |
+ 'compressed_bundle', 'copy'], |
+ default='list', metavar='MODE', |
+ help=("Otput mode. One of 'list', 'html', 'bundle', " + |
+ "'compressed_bundle' or 'copy'.")) |
+ return parser |
+ |
+ |
+def main(): |
+ options, args = CreateOptionParser().parse_args() |
+ if len(args) < 1: |
+ Die('At least one top-level source file must be specified.') |
+ sources = ReadSources(options, args) |
+ bundle = Bundle() |
+ if len(options.roots) > 0: |
+ CalcDeps(bundle, sources, args) |
+ bundle.Add((sources[name] for name in args)) |
+ if options.mode == 'copy': |
+ if options.dest_dir is None: |
+ Die('Must specify --dest_dir when copying.') |
+ LinkOrCopyFiles(bundle.GetSources(), options.dest_dir) |
+ else: |
+ if options.output_file: |
+ out_file = open(options.output_file, 'w') |
+ else: |
+ out_file = sys.stdout |
+ try: |
+ WriteOutput(bundle, options.mode, out_file, options.dest_dir) |
+ finally: |
+ if options.output_file: |
+ out_file.close() |
+ |
+ |
+if __name__ == '__main__': |
+ main() |