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

Side by Side Diff: third_party/closure_compiler/compile.py

Issue 2895773002: (fix landed)Revert of Closure: remove older, original GYP compilation system (Closed)
Patch Set: Created 3 years, 7 months 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
OLDNEW
(Empty)
1 #!/usr/bin/python
2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Runs Closure compiler on JavaScript files to check for errors and produce
7 minified output."""
8
9 import argparse
10 import os
11 import re
12 import subprocess
13 import sys
14 import tempfile
15
16 import build.inputs
17 import processor
18 import error_filter
19
20
21 _CURRENT_DIR = os.path.join(os.path.dirname(__file__))
22
23
24 class Checker(object):
25 """Runs the Closure compiler on given source files to typecheck them
26 and produce minified output."""
27
28 _JAR_COMMAND = [
29 "java",
30 "-jar",
31 "-Xms1024m",
32 "-client",
33 "-XX:+TieredCompilation"
34 ]
35
36 _MAP_FILE_FORMAT = "%s.map"
37
38 def __init__(self, verbose=False):
39 """
40 Args:
41 verbose: Whether this class should output diagnostic messages.
42 """
43 self._compiler_jar = os.path.join(_CURRENT_DIR, "compiler", "compiler.jar")
44 self._temp_files = []
45 self._verbose = verbose
46 self._error_filter = error_filter.PromiseErrorFilter()
47
48 def _nuke_temp_files(self):
49 """Deletes any temp files this class knows about."""
50 if not self._temp_files:
51 return
52
53 self._log_debug("Deleting temp files: %s" % ", ".join(self._temp_files))
54 for f in self._temp_files:
55 os.remove(f)
56 self._temp_files = []
57
58 def _log_debug(self, msg, error=False):
59 """Logs |msg| to stdout if --verbose/-v is passed when invoking this script.
60
61 Args:
62 msg: A debug message to log.
63 """
64 if self._verbose:
65 print "(INFO) %s" % msg
66
67 def _log_error(self, msg):
68 """Logs |msg| to stderr regardless of --flags.
69
70 Args:
71 msg: An error message to log.
72 """
73 print >> sys.stderr, "(ERROR) %s" % msg
74
75 def _run_jar(self, jar, args):
76 """Runs a .jar from the command line with arguments.
77
78 Args:
79 jar: A file path to a .jar file
80 args: A list of command line arguments to be passed when running the .jar.
81
82 Return:
83 (exit_code, stderr) The exit code of the command (e.g. 0 for success) and
84 the stderr collected while running |jar| (as a string).
85 """
86 shell_command = " ".join(self._JAR_COMMAND + [jar] + args)
87 self._log_debug("Running jar: %s" % shell_command)
88
89 devnull = open(os.devnull, "w")
90 kwargs = {"stdout": devnull, "stderr": subprocess.PIPE, "shell": True}
91 process = subprocess.Popen(shell_command, **kwargs)
92 _, stderr = process.communicate()
93 return process.returncode, stderr
94
95 def _get_line_number(self, match):
96 """When chrome is built, it preprocesses its JavaScript from:
97
98 <include src="blah.js">
99 alert(1);
100
101 to:
102
103 /* contents of blah.js inlined */
104 alert(1);
105
106 Because Closure Compiler requires this inlining already be done (as
107 <include> isn't valid JavaScript), this script creates temporary files to
108 expand all the <include>s.
109
110 When type errors are hit in temporary files, a developer doesn't know the
111 original source location to fix. This method maps from /tmp/file:300 back to
112 /original/source/file:100 so fixing errors is faster for developers.
113
114 Args:
115 match: A re.MatchObject from matching against a line number regex.
116
117 Returns:
118 The fixed up /file and :line number.
119 """
120 real_file = self._processor.get_file_from_line(match.group(1))
121 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number)
122
123 def _filter_errors(self, errors):
124 """Removes some extraneous errors. For example, we ignore:
125
126 Variable x first declared in /tmp/expanded/file
127
128 Because it's just a duplicated error (it'll only ever show up 2+ times).
129 We also ignore Promose-based errors:
130
131 found : function (VolumeInfo): (Promise<(DirectoryEntry|null)>|null)
132 required: (function (Promise<VolumeInfo>): ?|null|undefined)
133
134 as templates don't work with Promises in all cases yet. See
135 https://github.com/google/closure-compiler/issues/715 for details.
136
137 Args:
138 errors: A list of string errors extracted from Closure Compiler output.
139
140 Return:
141 A slimmer, sleeker list of relevant errors (strings).
142 """
143 first_declared_in = lambda e: " first declared in " not in e
144 return self._error_filter.filter(filter(first_declared_in, errors))
145
146 def _clean_up_error(self, error):
147 """Reverse the effects that funky <include> preprocessing steps have on
148 errors messages.
149
150 Args:
151 error: A Closure compiler error (2 line string with error and source).
152
153 Return:
154 The fixed up error string.
155 """
156 expanded_file = self._expanded_file
157 fixed = re.sub("%s:(\d+)" % expanded_file, self._get_line_number, error)
158 return fixed.replace(expanded_file, os.path.abspath(self._file_arg))
159
160 def _format_errors(self, errors):
161 """Formats Closure compiler errors to easily spot compiler output.
162
163 Args:
164 errors: A list of strings extracted from the Closure compiler's output.
165
166 Returns:
167 A formatted output string.
168 """
169 contents = "\n## ".join("\n\n".join(errors).splitlines())
170 return "## %s" % contents if contents else ""
171
172 def _create_temp_file(self, contents):
173 """Creates an owned temporary file with |contents|.
174
175 Args:
176 content: A string of the file contens to write to a temporary file.
177
178 Return:
179 The filepath of the newly created, written, and closed temporary file.
180 """
181 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file:
182 self._temp_files.append(tmp_file.name)
183 tmp_file.write(contents)
184 return tmp_file.name
185
186 def _run_js_check(self, sources, out_file, externs=None,
187 closure_args=None):
188 """Check |sources| for type errors.
189
190 Args:
191 sources: Files to check.
192 out_file: A file where the compiled output is written to.
193 externs: @extern files that inform the compiler about custom globals.
194 closure_args: Arguments passed directly to the Closure compiler.
195
196 Returns:
197 (errors, stderr) A parsed list of errors (strings) found by the compiler
198 and the raw stderr (as a string).
199 """
200 args = ["--js=%s" % s for s in sources]
201
202 assert out_file
203
204 out_dir = os.path.dirname(out_file)
205 if not os.path.exists(out_dir):
206 os.makedirs(out_dir)
207
208 checks_only = 'checks_only' in closure_args
209
210 if not checks_only:
211 args += ["--js_output_file=%s" % out_file]
212 args += ["--create_source_map=%s" % (self._MAP_FILE_FORMAT % out_file)]
213
214 args += ["--externs=%s" % e for e in externs or []]
215
216 closure_args = closure_args or []
217 closure_args += ["summary_detail_level=3", "continue_after_errors"]
218 args += ["--%s" % arg for arg in closure_args]
219
220 self._log_debug("Args: %s" % " ".join(args))
221
222 _, stderr = self._run_jar(self._compiler_jar, args)
223
224 errors = stderr.strip().split("\n\n")
225 maybe_summary = errors.pop()
226
227 if re.search(".*error.*warning.*typed", maybe_summary):
228 self._log_debug("Summary: %s" % maybe_summary)
229 else:
230 # Not a summary. Running the jar failed. Bail.
231 self._log_error(stderr)
232 self._nuke_temp_files()
233 sys.exit(1)
234
235 if errors:
236 if os.path.exists(out_file):
237 os.remove(out_file)
238 if os.path.exists(self._MAP_FILE_FORMAT % out_file):
239 os.remove(self._MAP_FILE_FORMAT % out_file)
240 elif checks_only:
241 # Compile succeeded but --checks_only disables --js_output_file from
242 # actually writing a file. Write a file ourselves so incremental builds
243 # still work.
244 with open(out_file, 'w') as f:
245 f.write('')
246
247 return errors, stderr
248
249 def check(self, source_file, out_file=None, depends=None, externs=None,
250 closure_args=None):
251 """Closure compiler |source_file| while checking for errors.
252
253 Args:
254 source_file: A file to check.
255 out_file: A file where the compiled output is written to.
256 depends: Files that |source_file| requires to run (e.g. earlier <script>).
257 externs: @extern files that inform the compiler about custom globals.
258 closure_args: Arguments passed directly to the Closure compiler.
259
260 Returns:
261 (found_errors, stderr) A boolean indicating whether errors were found and
262 the raw Closure compiler stderr (as a string).
263 """
264 self._log_debug("FILE: %s" % source_file)
265
266 if source_file.endswith("_externs.js"):
267 self._log_debug("Skipping externs: %s" % source_file)
268 return
269
270 self._file_arg = source_file
271
272 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir()
273 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f)
274
275 depends = depends or []
276 includes = [rel_path(f) for f in depends + [source_file]]
277 contents = ['<include src="%s">' % i for i in includes]
278 meta_file = self._create_temp_file("\n".join(contents))
279 self._log_debug("Meta file: %s" % meta_file)
280
281 self._processor = processor.Processor(meta_file)
282 self._expanded_file = self._create_temp_file(self._processor.contents)
283 self._log_debug("Expanded file: %s" % self._expanded_file)
284
285 errors, stderr = self._run_js_check([self._expanded_file],
286 out_file=out_file, externs=externs,
287 closure_args=closure_args)
288 filtered_errors = self._filter_errors(errors)
289 cleaned_errors = map(self._clean_up_error, filtered_errors)
290 output = self._format_errors(cleaned_errors)
291
292 if cleaned_errors:
293 prefix = "\n" if output else ""
294 self._log_error("Error in: %s%s%s" % (source_file, prefix, output))
295 elif output:
296 self._log_debug("Output: %s" % output)
297
298 self._nuke_temp_files()
299 return bool(cleaned_errors), stderr
300
301 def check_multiple(self, sources, out_file=None, externs=None,
302 closure_args=None):
303 """Closure compile a set of files and check for errors.
304
305 Args:
306 sources: An array of files to check.
307 out_file: A file where the compiled output is written to.
308 externs: @extern files that inform the compiler about custom globals.
309 closure_args: Arguments passed directly to the Closure compiler.
310
311 Returns:
312 (found_errors, stderr) A boolean indicating whether errors were found and
313 the raw Closure Compiler stderr (as a string).
314 """
315 errors, stderr = self._run_js_check(sources, out_file=out_file,
316 externs=externs,
317 closure_args=closure_args)
318 self._nuke_temp_files()
319 return bool(errors), stderr
320
321
322 if __name__ == "__main__":
323 parser = argparse.ArgumentParser(
324 description="Typecheck JavaScript using Closure compiler")
325 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE,
326 help="Path to a source file to typecheck")
327 single_file_group = parser.add_mutually_exclusive_group()
328 single_file_group.add_argument("--single_file", dest="single_file",
329 action="store_true",
330 help="Process each source file individually")
331 # TODO(twellington): remove --no_single_file and use len(opts.sources).
332 single_file_group.add_argument("--no_single_file", dest="single_file",
333 action="store_false",
334 help="Process all source files as a group")
335 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE)
336 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE)
337 parser.add_argument("-o", "--out_file", required=True,
338 help="A file where the compiled output is written to")
339 parser.add_argument("-c", "--closure_args", nargs=argparse.ZERO_OR_MORE,
340 help="Arguments passed directly to the Closure compiler")
341 parser.add_argument("-v", "--verbose", action="store_true",
342 help="Show more information as this script runs")
343
344 parser.set_defaults(single_file=True)
345 opts = parser.parse_args()
346
347 depends = opts.depends or []
348 # TODO(devlin): should we run normpath() on this first and/or do this for
349 # depends as well?
350 externs = set(opts.externs or [])
351 sources = set(opts.sources)
352
353 externs.add(os.path.join(_CURRENT_DIR, "externs", "polymer-1.0.js"))
354
355 checker = Checker(verbose=opts.verbose)
356 if opts.single_file:
357 for source in sources:
358 # Normalize source to the current directory.
359 source = os.path.normpath(os.path.join(os.getcwd(), source))
360 depends, externs = build.inputs.resolve_recursive_dependencies(
361 source, depends, externs)
362
363 found_errors, _ = checker.check(source, out_file=opts.out_file,
364 depends=depends, externs=externs,
365 closure_args=opts.closure_args)
366 if found_errors:
367 sys.exit(1)
368 else:
369 found_errors, stderr = checker.check_multiple(
370 sources,
371 out_file=opts.out_file,
372 externs=externs,
373 closure_args=opts.closure_args)
374 if found_errors:
375 print stderr
376 sys.exit(1)
OLDNEW
« no previous file with comments | « third_party/closure_compiler/build/inputs.py ('k') | third_party/closure_compiler/compile_js.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698