Reland #3: Ensure WebView notifies desktop automation on creation, destruction, and...
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / tools / jsbundler.py
blobf2c15c7ab6af01399a1303c1e4ec346e2313de29
1 #!/usr/bin/env python
3 # Copyright 2014 The Chromium Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
7 '''Produces various output formats from a set of JavaScript files with
8 closure style require/provide calls.
10 Scans one or more directory trees for JavaScript files. Then, from a
11 given list of top-level files, sorts all required input files topologically.
12 The top-level files are appended to the sorted list in the order specified
13 on the command line. If no root directories are specified, the source
14 files are assumed to be ordered already and no dependency analysis is
15 performed. The resulting file list can then be used in one of the following
16 ways:
18 - list: a plain list of files, one per line is output.
20 - html: a series of html <script> tags with src attributes containing paths
21 is output.
23 - bundle: a concatenation of all the files, separated by newlines is output.
25 - compressed_bundle: A bundle where non-significant whitespace, including
26 comments, has been stripped is output.
28 - copy: the files are copied, or hard linked if possible, to the destination
29 directory. In this case, no output is generated.
30 '''
33 import optparse
34 import os
35 import re
36 import shutil
37 import sys
39 _SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__))
40 _CHROME_SOURCE = os.path.realpath(
41 os.path.join(_SCRIPT_DIR, *[os.path.pardir] * 6))
42 sys.path.insert(0, os.path.join(
43 _CHROME_SOURCE, 'third_party/WebKit/Source/build/scripts'))
44 sys.path.insert(0, os.path.join(
45 _CHROME_SOURCE, ('chrome/third_party/chromevox/third_party/' +
46 'closure-library/closure/bin/build')))
47 import depstree
48 import rjsmin
49 import source
50 import treescan
53 def Die(message):
54 '''Prints an error message and exit the program.'''
55 print >>sys.stderr, message
56 sys.exit(1)
59 class SourceWithPaths(source.Source):
60 '''A source.Source object with its relative input and output paths'''
62 def __init__(self, content, in_path, out_path):
63 super(SourceWithPaths, self).__init__(content)
64 self._in_path = in_path
65 self._out_path = out_path
67 def GetInPath(self):
68 return self._in_path
70 def GetOutPath(self):
71 return self._out_path
74 class Bundle():
75 '''An ordered list of sources without duplicates.'''
77 def __init__(self):
78 self._added_paths = set()
79 self._added_sources = []
81 def Add(self, sources):
82 '''Appends one or more source objects the list if it doesn't already
83 exist.
85 Args:
86 sources: A SourceWithPath or an iterable of such objects.
87 '''
88 if isinstance(sources, SourceWithPaths):
89 sources = [sources]
90 for source in sources:
91 path = source.GetInPath()
92 if path not in self._added_paths:
93 self._added_paths.add(path)
94 self._added_sources.append(source)
96 def GetInPaths(self):
97 return (source.GetInPath() for source in self._added_sources)
99 def GetOutPaths(self):
100 return (source.GetOutPath() for source in self._added_sources)
102 def GetSources(self):
103 return self._added_sources
105 def GetUncompressedSource(self):
106 return '\n'.join((s.GetSource() for s in self._added_sources))
108 def GetCompressedSource(self):
109 return rjsmin.jsmin(self.GetUncompressedSource())
112 class PathRewriter():
113 '''A list of simple path rewrite rules to map relative input paths to
114 relative output paths.
117 def __init__(self, specs=[]):
118 '''Args:
119 specs: A list of mappings, each consisting of the input prefix and
120 the corresponding output prefix separated by colons.
122 self._prefix_map = []
123 for spec in specs:
124 parts = spec.split(':')
125 if len(parts) != 2:
126 Die('Invalid prefix rewrite spec %s' % spec)
127 if not parts[0].endswith('/') and parts[0] != '':
128 parts[0] += '/'
129 self._prefix_map.append(parts)
130 self._prefix_map.sort(reverse=True)
132 def RewritePath(self, in_path):
133 '''Rewrites an input path according to the list of rules.
135 Args:
136 in_path, str: The input path to rewrite.
137 Returns:
138 str: The corresponding output path.
140 for in_prefix, out_prefix in self._prefix_map:
141 if in_path.startswith(in_prefix):
142 return os.path.join(out_prefix, in_path[len(in_prefix):])
143 return in_path
146 def ReadSources(roots=[], source_files=[], need_source_text=False,
147 path_rewriter=PathRewriter(), exclude=[]):
148 '''Reads all source specified on the command line, including sources
149 included by --root options.
152 def EnsureSourceLoaded(in_path, sources):
153 if in_path not in sources:
154 out_path = path_rewriter.RewritePath(in_path)
155 sources[in_path] = SourceWithPaths(source.GetFileContents(in_path),
156 in_path, out_path)
158 # Only read the actual source file if we will do a dependency analysis or
159 # the caller asks for it.
160 need_source_text = need_source_text or len(roots) > 0
161 sources = {}
162 for root in roots:
163 for name in treescan.ScanTreeForJsFiles(root):
164 if any((r.search(name) for r in exclude)):
165 continue
166 EnsureSourceLoaded(name, sources)
167 for path in source_files:
168 if need_source_text:
169 EnsureSourceLoaded(path, sources)
170 else:
171 # Just add an empty representation of the source.
172 sources[path] = SourceWithPaths(
173 '', path, path_rewriter.RewritePath(path))
174 return sources
177 def _GetBase(sources):
178 '''Gets the closure base.js file if present among the sources.
180 Args:
181 sources: Dictionary with input path names as keys and SourceWithPaths
182 as values.
183 Returns:
184 SourceWithPath: The source file providing the goog namespace.
186 for source in sources.itervalues():
187 if (os.path.basename(source.GetInPath()) == 'base.js' and
188 'goog' in source.provides):
189 return source
190 Die('goog.base not provided by any file.')
193 def CalcDeps(bundle, sources, top_level):
194 '''Calculates dependencies for a set of top-level files.
196 Args:
197 bundle: Bundle to add the sources to.
198 sources, dict: Mapping from input path to SourceWithPaths objects.
199 top_level, list: List of top-level input paths to calculate dependencies
200 for.
202 providers = [s for s in sources.itervalues() if len(s.provides) > 0]
203 deps = depstree.DepsTree(providers)
204 namespaces = []
205 for path in top_level:
206 namespaces.extend(sources[path].requires)
207 # base.js is an implicit dependency that always goes first.
208 bundle.Add(_GetBase(sources))
209 bundle.Add(deps.GetDependencies(namespaces))
212 def _MarkAsCompiled(sources):
213 '''Sets COMPILED to true in the Closure base.js source.
215 Args:
216 sources: Dictionary with input paths names as keys and SourcWithPaths
217 objects as values.
219 base = _GetBase(sources)
220 new_content, count = re.subn('^var COMPILED = false;$',
221 'var COMPILED = true;',
222 base.GetSource(),
223 count=1,
224 flags=re.MULTILINE)
225 if count != 1:
226 Die('COMPILED var assignment not found in %s' % base.GetInPath())
227 sources[base.GetInPath()] = SourceWithPaths(
228 new_content,
229 base.GetInPath(),
230 base.GetOutPath())
232 def LinkOrCopyFiles(sources, dest_dir):
233 '''Copies a list of sources to a destination directory.'''
235 def LinkOrCopyOneFile(src, dst):
236 if not os.path.exists(os.path.dirname(dst)):
237 os.makedirs(os.path.dirname(dst))
238 if os.path.exists(dst):
239 # Avoid clobbering the inode if source and destination refer to the
240 # same file already.
241 if os.path.samefile(src, dst):
242 return
243 os.unlink(dst)
244 try:
245 os.link(src, dst)
246 except:
247 shutil.copy(src, dst)
249 for source in sources:
250 LinkOrCopyOneFile(source.GetInPath(),
251 os.path.join(dest_dir, source.GetOutPath()))
254 def WriteOutput(bundle, format, out_file, dest_dir):
255 '''Writes output in the specified format.
257 Args:
258 bundle: The ordered bundle iwth all sources already added.
259 format: Output format, one of list, html, bundle, compressed_bundle.
260 out_file: File object to receive the output.
261 dest_dir: Prepended to each path mentioned in the output, if applicable.
263 if format == 'list':
264 paths = bundle.GetOutPaths()
265 if dest_dir:
266 paths = (os.path.join(dest_dir, p) for p in paths)
267 paths = (os.path.normpath(p) for p in paths)
268 out_file.write('\n'.join(paths))
269 elif format == 'html':
270 HTML_TEMPLATE = '<script src=\'%s\'>'
271 script_lines = (HTML_TEMPLATE % p for p in bundle.GetOutPaths())
272 out_file.write('\n'.join(script_lines))
273 elif format == 'bundle':
274 out_file.write(bundle.GetUncompressedSource())
275 elif format == 'compressed_bundle':
276 out_file.write(bundle.GetCompressedSource())
277 out_file.write('\n')
280 def CreateOptionParser():
281 parser = optparse.OptionParser(description=__doc__)
282 parser.usage = '%prog [options] <top_level_file>...'
283 parser.add_option('-d', '--dest_dir', action='store', metavar='DIR',
284 help=('Destination directory. Used when translating ' +
285 'input paths to output paths and when copying '
286 'files.'))
287 parser.add_option('-o', '--output_file', action='store', metavar='FILE',
288 help=('File to output result to for modes that output '
289 'a single file.'))
290 parser.add_option('-r', '--root', dest='roots', action='append', default=[],
291 metavar='ROOT',
292 help='Roots of directory trees to scan for sources.')
293 parser.add_option('-w', '--rewrite_prefix', action='append', default=[],
294 dest='prefix_map', metavar='SPEC',
295 help=('Two path prefixes, separated by colons ' +
296 'specifying that a file whose (relative) path ' +
297 'name starts with the first prefix should have ' +
298 'that prefix replaced by the second prefix to ' +
299 'form a path relative to the output directory.'))
300 parser.add_option('-m', '--mode', type='choice', action='store',
301 choices=['list', 'html', 'bundle',
302 'compressed_bundle', 'copy'],
303 default='list', metavar='MODE',
304 help=("Otput mode. One of 'list', 'html', 'bundle', " +
305 "'compressed_bundle' or 'copy'."))
306 parser.add_option('-x', '--exclude', action='append', default=[],
307 help=('Exclude files whose full path contains a match for '
308 'the given regular expression. Does not apply to '
309 'filenames given as arguments.'))
310 return parser
313 def main():
314 options, args = CreateOptionParser().parse_args()
315 if len(args) < 1:
316 Die('At least one top-level source file must be specified.')
317 will_output_source_text = options.mode in ('bundle', 'compressed_bundle')
318 path_rewriter = PathRewriter(options.prefix_map)
319 exclude = [re.compile(r) for r in options.exclude]
320 sources = ReadSources(options.roots, args, will_output_source_text,
321 path_rewriter, exclude)
322 if will_output_source_text:
323 _MarkAsCompiled(sources)
324 bundle = Bundle()
325 if len(options.roots) > 0:
326 CalcDeps(bundle, sources, args)
327 bundle.Add((sources[name] for name in args))
328 if options.mode == 'copy':
329 if options.dest_dir is None:
330 Die('Must specify --dest_dir when copying.')
331 LinkOrCopyFiles(bundle.GetSources(), options.dest_dir)
332 else:
333 if options.output_file:
334 out_file = open(options.output_file, 'w')
335 else:
336 out_file = sys.stdout
337 try:
338 WriteOutput(bundle, options.mode, out_file, options.dest_dir)
339 finally:
340 if options.output_file:
341 out_file.close()
344 if __name__ == '__main__':
345 main()