OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 | |
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. | |
6 | |
7 '''Produces various output formats from a set of JavaScript files with | |
8 closure style require/provide calls. | |
9 | |
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 output in various ways: | |
16 | |
17 - list: a plain list of files, one per line. | |
18 | |
19 - html: html <script> tags with src attributes containing paths. | |
20 | |
21 - bundle: a cocatenation of all the files, separated by newlines. | |
dmazzoni
2014/05/21 17:42:17
cocatenation -> concatenation
| |
22 | |
23 - compressed_bundle: A bundle where non-significant whitespace, including | |
24 comments, has been stripped. | |
25 ''' | |
26 | |
27 | |
28 import optparse | |
29 import os | |
30 import shutil | |
31 import sys | |
32 | |
33 _SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__)) | |
34 _CHROME_SOURCE = os.path.realpath( | |
35 os.path.join(_SCRIPT_DIR, *[os.path.pardir] * 6)) | |
36 sys.path.insert(0, os.path.join( | |
37 _CHROME_SOURCE, 'third_party/WebKit/Source/build/scripts')) | |
38 sys.path.insert(0, os.path.join( | |
39 _CHROME_SOURCE, ('chrome/third_party/chromevox/third_party/' + | |
40 'closure-library/closure/bin/build'))) | |
41 import depstree | |
42 import rjsmin | |
43 import source | |
44 import treescan | |
45 | |
46 | |
47 def Die(message): | |
48 '''Prints an error message and exit the program.''' | |
49 print >>sys.stderr, message | |
50 sys.exit(1) | |
51 | |
52 | |
53 class SourceWithPaths(source.Source): | |
54 '''A source.Source object with its relative input and output paths''' | |
55 | |
56 def __init__(self, content, in_path, out_path): | |
57 super(SourceWithPaths, self).__init__(content) | |
58 self._in_path = in_path | |
59 self._out_path = out_path | |
60 | |
61 def GetInPath(self): | |
62 return self._in_path | |
63 | |
64 def GetOutPath(self): | |
65 return self._out_path | |
66 | |
67 | |
68 class Bundle(): | |
69 '''An ordered list of sources without duplicates.''' | |
70 | |
71 def __init__(self): | |
72 self._added_paths = set() | |
73 self._added_sources = [] | |
74 | |
75 def Add(self, sources): | |
76 '''Appends one or more source objects the list if it doesn't already | |
77 exist. | |
78 | |
79 Args: | |
80 sources: A SourceWithPath or an iterable of such objects. | |
81 ''' | |
82 if isinstance(sources, SourceWithPaths): | |
83 sources = [sources] | |
84 for source in sources: | |
85 path = source.GetInPath() | |
86 if path not in self._added_paths: | |
87 self._added_paths.add(path) | |
88 self._added_sources.append(source) | |
89 | |
90 def GetOutPaths(self): | |
91 return (source.GetOutPath() for source in self._added_sources) | |
92 | |
93 def GetSources(self): | |
94 return self._added_sources | |
95 | |
96 def GetUncompressedSource(self): | |
97 return '\n'.join((s.GetSource() for s in self._added_sources)) | |
98 | |
99 def GetCompressedSource(self): | |
100 return rjsmin.jsmin(self.GetUncompressedSource()) | |
101 | |
102 | |
103 class PathRewriter(): | |
104 '''A list of simple path rewrite rules to map relative input paths to | |
105 relative output paths. | |
106 ''' | |
107 | |
108 def __init__(self, specs): | |
109 '''Args: | |
110 specs: A list of mappings, each consisting of the input prefix and | |
111 the corresponding output prefix separated by colons. | |
112 ''' | |
113 self._prefix_map = [] | |
114 for spec in specs: | |
115 parts = spec.split(':') | |
116 if len(parts) != 2: | |
117 Die('Invalid prefix rewrite spec %s' % spec) | |
118 if not parts[0].endswith('/'): | |
119 parts[0] += '/' | |
120 self._prefix_map.append(parts) | |
121 | |
122 def RewritePath(self, in_path): | |
123 '''Rewrites an input path according to the list of rules. | |
124 | |
125 Args: | |
126 in_path, str: The input path to rewrite. | |
127 Returns: | |
128 str: The corresponding output path. | |
129 ''' | |
130 for in_prefix, out_prefix in self._prefix_map: | |
131 if in_path.startswith(in_prefix): | |
132 return os.path.join(out_prefix, in_path[len(in_prefix):]) | |
133 return in_path | |
134 | |
135 | |
136 def ReadSources(options, args): | |
137 '''Reads all source specified on the command line, including sources | |
138 included by --root options. | |
139 ''' | |
140 | |
141 def EnsureSourceLoaded(in_path, sources, path_rewriter): | |
142 if in_path not in sources: | |
143 out_path = path_rewriter.RewritePath(in_path) | |
144 sources[in_path] = SourceWithPaths(source.GetFileContents(in_path), | |
145 in_path, out_path) | |
146 | |
147 # Only read the actual source file if we will do a dependency analysis or | |
148 # if we'll need it for the output. | |
149 need_source_text = (len(options.roots) > 0 or | |
150 options.mode in ('bundle', 'compressed_bundle')) | |
151 path_rewriter = PathRewriter(options.prefix_map) | |
152 sources = {} | |
153 for root in options.roots: | |
154 for name in treescan.ScanTreeForJsFiles(root): | |
155 EnsureSourceLoaded(name, sources, path_rewriter) | |
156 for path in args: | |
157 if need_source_text: | |
158 EnsureSourceLoaded(path, sources, path_rewriter) | |
159 else: | |
160 # Just add an empty representation of the source. | |
161 sources[path] = SourceWithPaths( | |
162 '', path, path_rewriter.RewritePath(path)) | |
163 return sources | |
164 | |
165 | |
166 def CalcDeps(bundle, sources, top_level): | |
167 '''Calculates dependencies for a set of top-level files. | |
168 | |
169 Args: | |
170 bundle: Bundle to add the sources to. | |
171 sources, dict: Mapping from input path to SourceWithPaths objects. | |
172 top_level, list: List of top-level input paths to calculate dependencies | |
173 for. | |
174 ''' | |
175 def GetBase(sources): | |
176 for source in sources.itervalues(): | |
177 if (os.path.basename(source.GetInPath()) == 'base.js' and | |
178 'goog' in source.provides): | |
179 return source | |
180 Die('goog.base not provided by any file') | |
181 | |
182 providers = [s for s in sources.itervalues() if len(s.provides) > 0] | |
183 deps = depstree.DepsTree(providers) | |
184 namespaces = [] | |
185 for path in top_level: | |
186 namespaces.extend(sources[path].requires) | |
187 # base.js is an implicit dependency that always goes first. | |
188 bundle.Add(GetBase(sources)) | |
189 bundle.Add(deps.GetDependencies(namespaces)) | |
190 | |
191 | |
192 def CopyFiles(sources, dest_dir): | |
193 '''Copies a list of sources to a destination directory.''' | |
dmazzoni
2014/05/21 17:42:17
I think this function should be called HardLinkOrC
| |
194 | |
195 def CopyOrLink(src, dst): | |
196 if not os.path.exists(os.path.dirname(dst)): | |
197 os.makedirs(os.path.dirname(dst)) | |
198 if os.path.exists(dst): | |
199 # Avoid clobbering the inode if source and destination refer to the | |
200 # same file already. | |
201 if os.path.samefile(src, dst): | |
202 return | |
203 os.unlink(dst) | |
204 try: | |
205 os.link(src, dst) | |
206 except: | |
207 shutil.copy(src, dst) | |
208 | |
209 for source in sources: | |
210 CopyOrLink(source.GetInPath(), | |
211 os.path.join(dest_dir, source.GetOutPath())) | |
212 | |
213 | |
214 def WriteOutput(bundle, format, out_file, dest_dir): | |
215 '''Writes output in the specified format. | |
216 | |
217 Args: | |
218 bundle: The ordered bundle iwth all sources already added. | |
219 format: Output format, one of list, html, bundle, compressed_bundle. | |
220 out_file: File object to receive the output. | |
221 dest_dir: Prepended to each path mentioned in the output, if aplicable. | |
dmazzoni
2014/05/21 17:42:17
aplicable -> applicable
| |
222 ''' | |
223 if format == 'list': | |
224 paths = bundle.GetOutPaths() | |
225 if dest_dir: | |
226 paths = (os.path.join(dest_dir, p) for p in paths) | |
227 paths = (os.path.normpath(p) for p in paths) | |
228 out_file.write('\n'.join(paths)) | |
229 elif format == 'html': | |
230 HTML_TEMPLATE = '<script src=\'%s\'>' | |
231 script_lines = (HTML_TEMPLATE % p for p in bundle.GetOutPaths()) | |
232 out_file.write('\n'.join(script_lines)) | |
233 elif format == 'bundle': | |
234 out_file.write(bundle.GetUncompressedSource()) | |
235 elif format == 'compressed_bundle': | |
236 out_file.write(bundle.GetCompressedSource()) | |
237 out_file.write('\n') | |
238 | |
239 | |
240 def CreateOptionParser(): | |
241 parser = optparse.OptionParser(description=__doc__) | |
242 parser.usage = '%prog [options] <top_level_file>...' | |
243 parser.add_option('-d', '--dest_dir', action='store', metavar='DIR', | |
244 help=('Destination directory. Used when translating ' + | |
245 'input paths to output paths and when copying ' | |
246 'files.')) | |
247 parser.add_option('-o', '--output_file', action='store', metavar='FILE', | |
248 help=('File to output result to for modes that output ' | |
249 'a single file.')) | |
250 parser.add_option('-r', '--root', dest='roots', action='append', default=[], | |
251 metavar='ROOT', | |
252 help='Roots of directory trees to scan for sources.') | |
253 parser.add_option('-w', '--rewrite_prefix', action='append', default=[], | |
254 dest='prefix_map', metavar='SPEC', | |
255 help=('Two path prefixes, separated by colons ' + | |
256 'specifying that a file whose (relative) path ' + | |
257 'name starts with the first prefix should have ' + | |
258 'that prefix replaced by the second prefix to ' + | |
259 'form a path relative to the output directory.')) | |
260 parser.add_option('-m', '--mode', type='choice', action='store', | |
261 choices=['list', 'html', 'bundle', | |
262 'compressed_bundle', 'copy'], | |
263 default='list', metavar='MODE', | |
264 help=("Otput mode. One of 'list', 'html', 'bundle', " + | |
265 "'compressed_bundle' or 'copy'.")) | |
266 return parser | |
267 | |
268 | |
269 def main(): | |
270 options, args = CreateOptionParser().parse_args() | |
271 if len(args) < 1: | |
272 Die('At least one top-level source file must be specified.') | |
273 sources = ReadSources(options, args) | |
274 bundle = Bundle() | |
275 if len(options.roots) > 0: | |
276 CalcDeps(bundle, sources, args) | |
277 bundle.Add((sources[name] for name in args)) | |
278 if options.mode == 'copy': | |
279 if options.dest_dir is None: | |
280 Die('Must specify --dest_dir when copying.') | |
281 CopyFiles(bundle.GetSources(), options.dest_dir) | |
282 else: | |
283 if options.output_file: | |
284 out_file = open(options.output_file, 'w') | |
285 else: | |
286 out_file = sys.stdout | |
287 try: | |
288 WriteOutput(bundle, options.mode, out_file, options.dest_dir) | |
289 finally: | |
290 if options.output_file: | |
291 out_file.close() | |
292 | |
293 | |
294 if __name__ == '__main__': | |
295 main() | |
OLD | NEW |