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
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
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.
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')))
54 '''Prints an error message and exit the program.'''
55 print >>sys
.stderr
, message
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
75 '''An ordered list of sources without duplicates.'''
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
86 sources: A SourceWithPath or an iterable of such objects.
88 if isinstance(sources
, SourceWithPaths
):
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
)
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
=[]):
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
= []
124 parts
= spec
.split(':')
126 Die('Invalid prefix rewrite spec %s' % spec
)
127 if not parts
[0].endswith('/') and 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.
136 in_path, str: The input path to rewrite.
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
):])
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
),
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
163 for name
in treescan
.ScanTreeForJsFiles(root
):
164 if any((r
.search(name
) for r
in exclude
)):
166 EnsureSourceLoaded(name
, sources
)
167 for path
in source_files
:
169 EnsureSourceLoaded(path
, sources
)
171 # Just add an empty representation of the source.
172 sources
[path
] = SourceWithPaths(
173 '', path
, path_rewriter
.RewritePath(path
))
177 def _GetBase(sources
):
178 '''Gets the closure base.js file if present among the sources.
181 sources: Dictionary with input path names as keys and SourceWithPaths
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
):
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.
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
202 providers
= [s
for s
in sources
.itervalues() if len(s
.provides
) > 0]
203 deps
= depstree
.DepsTree(providers
)
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.
216 sources: Dictionary with input paths names as keys and SourcWithPaths
219 base
= _GetBase(sources
)
220 new_content
, count
= re
.subn('^var COMPILED = false;$',
221 'var COMPILED = true;',
226 Die('COMPILED var assignment not found in %s' % base
.GetInPath())
227 sources
[base
.GetInPath()] = SourceWithPaths(
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
):
243 shutil
.copy(src
, dst
)
245 for source
in sources
:
246 LinkOrCopyOneFile(source
.GetInPath(),
247 os
.path
.join(dest_dir
, source
.GetOutPath()))
250 def WriteOutput(bundle
, format
, out_file
, dest_dir
):
251 '''Writes output in the specified format.
254 bundle: The ordered bundle iwth all sources already added.
255 format: Output format, one of list, html, bundle, compressed_bundle.
256 out_file: File object to receive the output.
257 dest_dir: Prepended to each path mentioned in the output, if applicable.
260 paths
= bundle
.GetOutPaths()
262 paths
= (os
.path
.join(dest_dir
, p
) for p
in paths
)
263 paths
= (os
.path
.normpath(p
) for p
in paths
)
264 out_file
.write('\n'.join(paths
))
265 elif format
== 'html':
266 HTML_TEMPLATE
= '<script src=\'%s\'>'
267 script_lines
= (HTML_TEMPLATE
% p
for p
in bundle
.GetOutPaths())
268 out_file
.write('\n'.join(script_lines
))
269 elif format
== 'bundle':
270 out_file
.write(bundle
.GetUncompressedSource())
271 elif format
== 'compressed_bundle':
272 out_file
.write(bundle
.GetCompressedSource())
276 def WriteStampfile(stampfile
):
277 '''Writes a stamp file.
280 stampfile, string: name of stamp file to touch
282 with
open(stampfile
, 'w') as file:
283 os
.utime(stampfile
, None)
286 def WriteDepfile(depfile
, outfile
, infiles
):
290 depfile, string: name of dep file to write
291 outfile, string: Name of output file to use as the target in the generated
293 infiles, list: File names to list as dependencies in the .d file.
295 content
= '%s: %s' % (outfile
, ' '.join(infiles
))
296 open(depfile
, 'w').write(content
)
299 def CreateOptionParser():
300 parser
= optparse
.OptionParser(description
=__doc__
)
301 parser
.usage
= '%prog [options] <top_level_file>...'
302 parser
.add_option('-d', '--dest_dir', action
='store', metavar
='DIR',
303 help=('Destination directory. Used when translating ' +
304 'input paths to output paths and when copying '
306 parser
.add_option('-o', '--output_file', action
='store', metavar
='FILE',
307 help=('File to output result to for modes that output '
309 parser
.add_option('-r', '--root', dest
='roots', action
='append', default
=[],
311 help='Roots of directory trees to scan for sources.')
312 parser
.add_option('-M', '--module', dest
='modules', action
='append',
313 default
=[], metavar
='FILENAME',
314 help='Source modules to load')
315 parser
.add_option('-w', '--rewrite_prefix', action
='append', default
=[],
316 dest
='prefix_map', metavar
='SPEC',
317 help=('Two path prefixes, separated by colons ' +
318 'specifying that a file whose (relative) path ' +
319 'name starts with the first prefix should have ' +
320 'that prefix replaced by the second prefix to ' +
321 'form a path relative to the output directory.'))
322 parser
.add_option('-m', '--mode', type='choice', action
='store',
323 choices
=['list', 'html', 'bundle',
324 'compressed_bundle', 'copy'],
325 default
='list', metavar
='MODE',
326 help=("Otput mode. One of 'list', 'html', 'bundle', " +
327 "'compressed_bundle' or 'copy'."))
328 parser
.add_option('-x', '--exclude', action
='append', default
=[],
329 help=('Exclude files whose full path contains a match for '
330 'the given regular expression. Does not apply to '
331 'filenames given as arguments or with the '
333 parser
.add_option('--depfile', metavar
='FILENAME',
334 help='Store .d style dependencies in FILENAME')
335 parser
.add_option('--stampfile', metavar
='FILENAME',
336 help='Write empty stamp file')
341 options
, args
= CreateOptionParser().parse_args()
343 Die('At least one top-level source file must be specified.')
344 if options
.depfile
and not options
.output_file
:
345 Die('--depfile requires an output file')
346 will_output_source_text
= options
.mode
in ('bundle', 'compressed_bundle')
347 path_rewriter
= PathRewriter(options
.prefix_map
)
348 exclude
= [re
.compile(r
) for r
in options
.exclude
]
349 sources
= ReadSources(options
.roots
, options
.modules
+ args
,
350 will_output_source_text
or len(options
.modules
) > 0,
351 path_rewriter
, exclude
)
352 if will_output_source_text
:
353 _MarkAsCompiled(sources
)
355 if len(options
.roots
) > 0 or len(options
.modules
) > 0:
356 CalcDeps(bundle
, sources
, args
)
357 bundle
.Add((sources
[name
] for name
in args
))
358 if options
.mode
== 'copy':
359 if options
.dest_dir
is None:
360 Die('Must specify --dest_dir when copying.')
361 LinkOrCopyFiles(bundle
.GetSources(), options
.dest_dir
)
363 if options
.output_file
:
364 out_file
= open(options
.output_file
, 'w')
366 out_file
= sys
.stdout
368 WriteOutput(bundle
, options
.mode
, out_file
, options
.dest_dir
)
370 if options
.output_file
:
372 if options
.stampfile
:
373 WriteStampfile(options
.stampfile
)
375 WriteDepfile(options
.depfile
, options
.output_file
, bundle
.GetInPaths())
377 if __name__
== '__main__':