OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Runs Closure compiler on a JavaScript file to check for errors.""" | 6 """Runs Closure compiler on a JavaScript file to check for errors.""" |
7 | 7 |
8 import argparse | 8 import argparse |
9 import os | 9 import os |
10 import re | 10 import re |
11 import subprocess | 11 import subprocess |
12 import sys | 12 import sys |
13 import tempfile | 13 import tempfile |
14 | 14 |
15 import build.inputs | 15 import build.inputs |
16 import processor | 16 import processor |
17 import error_filter | 17 import error_filter |
18 | 18 |
19 | 19 |
20 class Checker(object): | 20 class Checker(object): |
21 """Runs the Closure compiler on a given source file and returns the | 21 """Runs the Closure compiler on a given source file to typecheck it.""" |
22 success/errors.""" | |
23 | 22 |
24 _COMMON_CLOSURE_ARGS = [ | 23 _COMMON_CLOSURE_ARGS = [ |
25 "--accept_const_keyword", | 24 "--accept_const_keyword", |
26 "--jscomp_error=accessControls", | 25 "--jscomp_error=accessControls", |
27 "--jscomp_error=ambiguousFunctionDecl", | 26 "--jscomp_error=ambiguousFunctionDecl", |
28 "--jscomp_error=checkStructDictInheritance", | 27 "--jscomp_error=checkStructDictInheritance", |
29 "--jscomp_error=checkTypes", | 28 "--jscomp_error=checkTypes", |
30 "--jscomp_error=checkVars", | 29 "--jscomp_error=checkVars", |
31 "--jscomp_error=constantProperty", | 30 "--jscomp_error=constantProperty", |
32 "--jscomp_error=deprecated", | 31 "--jscomp_error=deprecated", |
33 "--jscomp_error=externsValidation", | 32 "--jscomp_error=externsValidation", |
34 "--jscomp_error=globalThis", | 33 "--jscomp_error=globalThis", |
35 "--jscomp_error=invalidCasts", | 34 "--jscomp_error=invalidCasts", |
36 "--jscomp_error=missingProperties", | 35 "--jscomp_error=missingProperties", |
37 "--jscomp_error=missingReturn", | 36 "--jscomp_error=missingReturn", |
38 "--jscomp_error=nonStandardJsDocs", | 37 "--jscomp_error=nonStandardJsDocs", |
39 "--jscomp_error=suspiciousCode", | 38 "--jscomp_error=suspiciousCode", |
40 "--jscomp_error=undefinedNames", | 39 "--jscomp_error=undefinedNames", |
41 "--jscomp_error=undefinedVars", | 40 "--jscomp_error=undefinedVars", |
42 "--jscomp_error=unknownDefines", | 41 "--jscomp_error=unknownDefines", |
43 "--jscomp_error=uselessCode", | 42 "--jscomp_error=uselessCode", |
44 "--jscomp_error=visibility", | 43 "--jscomp_error=visibility", |
45 "--language_in=ECMASCRIPT5_STRICT", | 44 "--language_in=ECMASCRIPT5_STRICT", |
46 "--summary_detail_level=3", | 45 "--summary_detail_level=3", |
47 ] | 46 ] |
48 | 47 |
49 # These are the extra flags used when compiling in 'strict' mode. | 48 # These are the extra flags used when compiling in strict mode. |
50 # Flags that are normally disabled are turned on for strict mode. | 49 # Flags that are normally disabled are turned on for strict mode. |
51 _STRICT_CLOSURE_ARGS = [ | 50 _STRICT_CLOSURE_ARGS = [ |
52 "--jscomp_error=reportUnknownTypes", | 51 "--jscomp_error=reportUnknownTypes", |
53 "--jscomp_error=duplicate", | 52 "--jscomp_error=duplicate", |
54 "--jscomp_error=misplacedTypeAnnotation", | 53 "--jscomp_error=misplacedTypeAnnotation", |
55 ] | 54 ] |
56 | 55 |
57 _DISABLED_CLOSURE_ARGS = [ | 56 _DISABLED_CLOSURE_ARGS = [ |
58 # TODO(dbeam): happens when the same file is <include>d multiple times. | 57 # TODO(dbeam): happens when the same file is <include>d multiple times. |
59 "--jscomp_off=duplicate", | 58 "--jscomp_off=duplicate", |
60 # TODO(fukino): happens when cr.defineProperty() has a type annotation. | 59 # TODO(fukino): happens when cr.defineProperty() has a type annotation. |
61 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562. | 60 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562. |
62 "--jscomp_off=misplacedTypeAnnotation", | 61 "--jscomp_off=misplacedTypeAnnotation", |
63 ] | 62 ] |
64 | 63 |
65 _JAR_COMMAND = [ | 64 _JAR_COMMAND = [ |
66 "java", | 65 "java", |
67 "-jar", | 66 "-jar", |
68 "-Xms1024m", | 67 "-Xms1024m", |
69 "-client", | 68 "-client", |
70 "-XX:+TieredCompilation" | 69 "-XX:+TieredCompilation" |
71 ] | 70 ] |
72 | 71 |
73 _found_java = False | |
74 | |
75 def __init__(self, verbose=False, strict=False): | 72 def __init__(self, verbose=False, strict=False): |
73 """ | |
74 Args: | |
75 verbose: Whether this class should output diagnostic messages. | |
76 strict: Whether the Closure Compiler should be invoked more strictly. | |
77 """ | |
76 current_dir = os.path.join(os.path.dirname(__file__)) | 78 current_dir = os.path.join(os.path.dirname(__file__)) |
77 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") | 79 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") |
78 self._temp_files = [] | 80 self._temp_files = [] |
79 self._verbose = verbose | 81 self._verbose = verbose |
80 self._strict = strict | 82 self._strict = strict |
81 self._error_filter = error_filter.PromiseErrorFilter() | 83 self._error_filter = error_filter.PromiseErrorFilter() |
82 | 84 |
83 def _clean_up(self): | 85 def _clean_up(self): |
86 """Deletes any temp files this class knows about.""" | |
84 if not self._temp_files: | 87 if not self._temp_files: |
85 return | 88 return |
86 | 89 |
87 self._debug("Deleting temporary files: %s" % ", ".join(self._temp_files)) | 90 self._log_debug("Deleting temp files: %s" % ", ".join(self._temp_files)) |
88 for f in self._temp_files: | 91 for f in self._temp_files: |
89 os.remove(f) | 92 os.remove(f) |
90 self._temp_files = [] | 93 self._temp_files = [] |
91 | 94 |
92 def _debug(self, msg, error=False): | 95 def _log_debug(self, msg, error=False): |
96 """Logs |msg| to stdout if --verbose/-v is passed when invoking this script. | |
rothwell
2015/03/20 19:24:31
is this syntax, |x| common in chromium?
Dan Beam
2015/03/21 01:35:43
yes, |var_names| or |_member_vars| or anything in
| |
97 | |
98 Args: | |
99 msg: A debug message to log. | |
100 """ | |
93 if self._verbose: | 101 if self._verbose: |
94 print "(INFO) %s" % msg | 102 print "(INFO) %s" % msg |
95 | 103 |
96 def _error(self, msg): | 104 def _log_error(self, msg): |
105 """Logs |msg| to stderr regardless of --flags. | |
106 | |
107 Args: | |
108 msg: An error message to log. | |
109 """ | |
97 print >> sys.stderr, "(ERROR) %s" % msg | 110 print >> sys.stderr, "(ERROR) %s" % msg |
98 self._clean_up() | |
99 | 111 |
100 def _common_args(self): | 112 def _common_args(self): |
101 """Returns an array of the common closure compiler args.""" | 113 """Returns an array of the common closure compiler args.""" |
102 if self._strict: | 114 if self._strict: |
103 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS | 115 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS |
104 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS | 116 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS |
105 | 117 |
106 def _run_command(self, cmd): | 118 def _run_jar(self, jar, args): |
107 """Runs a shell command. | 119 """Runs a .jar from the command line with arguments. |
108 | 120 |
109 Args: | 121 Args: |
110 cmd: A list of tokens to be joined into a shell command. | 122 jar: A file path to a .jar file |
111 | 123 args: A list of command line arguments to be passed when running the .jar. |
112 Return: | 124 |
113 True if the exit code was 0, else False. | 125 Return: |
114 """ | 126 (exit_code, stderr) The exit code of the command (e.g. 0 for success) and |
115 cmd_str = " ".join(cmd) | 127 the stderr collected while running |jar| (as a string). |
116 self._debug("Running command: %s" % cmd_str) | 128 """ |
129 shell_command = " ".join(self._JAR_COMMAND + [jar] + args) | |
130 self._log_debug("Running jar: %s" % shell_command) | |
117 | 131 |
118 devnull = open(os.devnull, "w") | 132 devnull = open(os.devnull, "w") |
119 return subprocess.Popen( | 133 kwargs = {"stdout": devnull, "stderr": subprocess.PIPE, "shell": True} |
120 cmd_str, stdout=devnull, stderr=subprocess.PIPE, shell=True) | 134 process = subprocess.Popen(shell_command, **kwargs) |
121 | 135 _, stderr = process.communicate() |
122 def _check_java_path(self): | 136 return process.returncode, stderr |
123 """Checks that `java` is on the system path.""" | 137 |
124 if not self._found_java: | 138 def _get_line_number(self, match): |
125 proc = self._run_command(["which", "java"]) | 139 """When chrome is built, it preprocesses its JavaScript from: |
126 proc.communicate() | 140 |
127 if proc.returncode == 0: | 141 <include src="blah.js"> |
128 self._found_java = True | 142 alert(1); |
129 else: | 143 |
130 self._error("Cannot find java (`which java` => %s)" % proc.returncode) | 144 to: |
131 | 145 |
132 return self._found_java | 146 /* contents of blah.js inlined */ |
133 | 147 alert(1); |
134 def _run_jar(self, jar, args=None): | 148 |
135 args = args or [] | 149 Because Closure Compiler requires this inlining already be done (as |
136 self._check_java_path() | 150 <include> isn't valid JavaScript), this script creates temporary files to |
137 return self._run_command(self._JAR_COMMAND + [jar] + args) | 151 expand all the <include>s. |
138 | 152 |
139 def _fix_line_number(self, match): | 153 When type errors are hit in temporary files, a developer doesn't know the |
140 """Changes a line number from /tmp/file:300 to /orig/file:100. | 154 original source location to fix. This method maps from /tmp/file:300 back to |
141 | 155 /original/source/file:100 so fixing errors is faster for developers. |
142 Args: | 156 |
143 match: A re.MatchObject from matching against a line number regex. | 157 Args: |
158 match: A re.MatchObject from matching against a line number regex. | |
144 | 159 |
145 Returns: | 160 Returns: |
146 The fixed up /file and :line number. | 161 The fixed up /file and :line number. |
147 """ | 162 """ |
148 real_file = self._processor.get_file_from_line(match.group(1)) | 163 real_file = self._processor.get_file_from_line(match.group(1)) |
149 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) | 164 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) |
150 | 165 |
166 def _filter_errors(self, errors): | |
167 """Removes some extraneous errors. For example, we ignore: | |
168 | |
169 Variable x first declared in /tmp/expanded/file | |
170 | |
171 Because it's just a duplicated error (it'll only ever show up 2+ times). | |
172 We also ignore Promose-based errors: | |
173 | |
174 found : function (VolumeInfo): (Promise<(DirectoryEntry|null)>|null) | |
175 required: (function (Promise<VolumeInfo>): ?|null|undefined) | |
176 | |
177 as templates don't work with Promises in all cases yet. See | |
178 https://github.com/google/closure-compiler/issues/715 for details. | |
179 | |
180 Args: | |
181 errors: A list of string errors extracted from Closure Compiler output. | |
182 | |
183 Return: | |
184 A slimmer, sleeker list of relevant errors (strings). | |
185 """ | |
186 first_declared_in = lambda e: " first declared in " not in e | |
187 return self._error_filter.filter(filter(first_declared_in, errors)) | |
188 | |
151 def _fix_up_error(self, error): | 189 def _fix_up_error(self, error): |
152 """Filter out irrelevant errors or fix line numbers. | 190 """Reverse the effects that funky <include> preprocessing steps have on |
153 | 191 errors messages. |
154 Args: | 192 |
155 error: A Closure compiler error (2 line string with error and source). | 193 Args: |
156 | 194 error: A Closure compiler error (2 line string with error and source). |
157 Return: | 195 |
158 The fixed up error string (blank if it should be ignored). | 196 Return: |
159 """ | 197 The fixed up error string. |
160 if " first declared in " in error: | 198 """ |
161 # Ignore "Variable x first declared in /same/file". | |
162 return "" | |
163 | |
164 expanded_file = self._expanded_file | 199 expanded_file = self._expanded_file |
165 fixed = re.sub("%s:(\d+)" % expanded_file, self._fix_line_number, error) | 200 fixed = re.sub("%s:(\d+)" % expanded_file, self._get_line_number, error) |
166 return fixed.replace(expanded_file, os.path.abspath(self._file_arg)) | 201 return fixed.replace(expanded_file, os.path.abspath(self._file_arg)) |
167 | 202 |
168 def _format_errors(self, errors): | 203 def _format_errors(self, errors): |
169 """Formats Closure compiler errors to easily spot compiler output.""" | 204 """Formats Closure compiler errors to easily spot compiler output. |
170 errors = filter(None, errors) | 205 |
206 Args: | |
207 errors: A list of strings extracted from the Closure compiler's output. | |
208 | |
209 Returns: | |
210 A formatted output string. | |
211 """ | |
171 contents = "\n## ".join("\n\n".join(errors).splitlines()) | 212 contents = "\n## ".join("\n\n".join(errors).splitlines()) |
172 return "## %s" % contents if contents else "" | 213 return "## %s" % contents if contents else "" |
173 | 214 |
174 def _create_temp_file(self, contents): | 215 def _create_temp_file(self, contents): |
216 """Creates an owned temporary file with |contents|. | |
217 | |
218 Args: | |
219 content: A string of the file contens to write to a temporary file. | |
220 | |
221 Return: | |
222 The filepath of the newly created, written, and closed temporary file. | |
223 """ | |
175 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: | 224 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: |
176 self._temp_files.append(tmp_file.name) | 225 self._temp_files.append(tmp_file.name) |
177 tmp_file.write(contents) | 226 tmp_file.write(contents) |
178 return tmp_file.name | 227 return tmp_file.name |
179 | 228 |
180 def run_js_check(self, sources, externs=None): | 229 def _run_js_check(self, sources, externs): |
181 if not self._check_java_path(): | 230 """Check |sources| for type errors. |
182 return 1, "" | 231 |
183 | 232 Args: |
233 sources: Files to check. | |
234 externs: @extern files that inform the compiler about custom globals. | |
235 | |
236 Returns: | |
237 A list of errors (strings) found by the Closure Compiler. | |
238 """ | |
184 args = ["--js=%s" % s for s in sources] | 239 args = ["--js=%s" % s for s in sources] |
185 if externs: | 240 args += ["--externs=%s" % e for e in externs] |
186 args += ["--externs=%s" % e for e in externs] | |
187 args_file_content = " %s" % " ".join(self._common_args() + args) | 241 args_file_content = " %s" % " ".join(self._common_args() + args) |
188 self._debug("Args: %s" % args_file_content.strip()) | 242 self._log_debug("Args: %s" % args_file_content.strip()) |
189 | 243 |
190 args_file = self._create_temp_file(args_file_content) | 244 args_file = self._create_temp_file(args_file_content) |
191 self._debug("Args file: %s" % args_file) | 245 self._log_debug("Args file: %s" % args_file) |
192 | 246 |
193 runner_args = ["--compiler-args-file=%s" % args_file] | 247 runner_args = ["--compiler-args-file=%s" % args_file] |
194 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) | 248 _, stderr = self._run_jar(self._runner_jar, runner_args) |
195 _, stderr = runner_cmd.communicate() | |
196 | 249 |
197 errors = stderr.strip().split("\n\n") | 250 errors = stderr.strip().split("\n\n") |
198 self._debug("Summary: %s" % errors.pop()) | 251 maybe_summary = errors.pop() |
199 | 252 |
200 self._clean_up() | 253 if re.search(".*error.*warning.*typed", maybe_summary): |
201 | 254 self._log_debug("Summary: %s" % maybe_summary) |
202 return errors, stderr | 255 else: |
203 | 256 # Not a summary. Running the jar failed. Bail. |
204 def check(self, source_file, depends=None, externs=None): | 257 self._log_error(stderr) |
205 """Closure compile a file and check for errors. | 258 self._clean_up() |
206 | 259 sys.exit(1) |
207 Args: | 260 |
208 source_file: A file to check. | 261 return errors |
209 depends: Other files that would be included with a <script> earlier in | 262 |
210 the page. | 263 def check(self, source_file, depends, externs): |
211 externs: @extern files that inform the compiler about custom globals. | 264 """Check |source_file| for type errors. |
212 | 265 |
213 Returns: | 266 Args: |
214 (has_errors, output) A boolean indicating if there were errors and the | 267 source_file: A file to check. |
215 Closure compiler output (as a string). | 268 depends: Files that |source_file| requires to run (e.g. earlier <script>). |
216 """ | 269 externs: @extern files that inform the compiler about custom globals. |
217 depends = depends or [] | 270 """ |
218 externs = externs or set() | 271 self._log_debug("FILE: %s" % source_file) |
219 | |
220 if not self._check_java_path(): | |
221 return 1, "" | |
222 | |
223 self._debug("FILE: %s" % source_file) | |
224 | 272 |
225 if source_file.endswith("_externs.js"): | 273 if source_file.endswith("_externs.js"): |
226 self._debug("Skipping externs: %s" % source_file) | 274 self._log_debug("Skipping externs: %s" % source_file) |
227 return | 275 return |
228 | 276 |
229 self._file_arg = source_file | 277 self._file_arg = source_file |
230 | 278 |
231 tmp_dir = tempfile.gettempdir() | 279 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir() |
232 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) | 280 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f) |
233 | 281 |
234 includes = [rel_path(f) for f in depends + [source_file]] | 282 includes = [rel_path(f) for f in depends + [source_file]] |
235 contents = ['<include src="%s">' % i for i in includes] | 283 contents = ['<include src="%s">' % i for i in includes] |
236 meta_file = self._create_temp_file("\n".join(contents)) | 284 meta_file = self._create_temp_file("\n".join(contents)) |
237 self._debug("Meta file: %s" % meta_file) | 285 self._log_debug("Meta file: %s" % meta_file) |
238 | 286 |
239 self._processor = processor.Processor(meta_file) | 287 self._processor = processor.Processor(meta_file) |
240 self._expanded_file = self._create_temp_file(self._processor.contents) | 288 self._expanded_file = self._create_temp_file(self._processor.contents) |
241 self._debug("Expanded file: %s" % self._expanded_file) | 289 self._log_debug("Expanded file: %s" % self._expanded_file) |
242 | 290 |
243 errors, stderr = self.run_js_check([self._expanded_file], externs) | 291 errors = self._run_js_check([self._expanded_file], externs) |
244 | 292 filtered_errors = self._filter_errors(errors) |
245 # Filter out false-positive promise chain errors. | 293 fixed_errors = map(self._fix_up_error, filtered_errors) |
246 # See https://github.com/google/closure-compiler/issues/715 for details. | 294 output = self._format_errors(fixed_errors) |
247 errors = self._error_filter.filter(errors); | 295 |
248 | |
249 output = self._format_errors(map(self._fix_up_error, errors)) | |
250 if errors: | 296 if errors: |
251 prefix = "\n" if output else "" | 297 prefix = "\n" if output else "" |
252 self._error("Error in: %s%s%s" % (source_file, prefix, output)) | 298 self._log_error("Error in: %s%s%s" % (source_file, prefix, output)) |
253 elif output: | 299 elif output: |
254 self._debug("Output: %s" % output) | 300 self._log_debug("Output: %s" % output) |
255 | 301 |
256 return bool(errors), output | 302 self._clean_up() |
257 | 303 |
258 def check_multiple(self, sources): | 304 def check_multiple(self, sources): |
259 """Closure compile a set of files and check for errors. | 305 """Closure compile a set of files and check for errors. |
260 | 306 |
261 Args: | 307 Args: |
262 sources: An array of files to check. | 308 sources: An array of files to check. |
263 | 309 """ |
264 Returns: | 310 self._run_js_check(sources, []) |
265 (has_errors, output) A boolean indicating if there were errors and the | 311 self._clean_up() |
266 Closure compiler output (as a string). | 312 |
267 """ | |
268 | |
269 errors, stderr = self.run_js_check(sources) | |
270 return bool(errors), stderr | |
271 | 313 |
272 if __name__ == "__main__": | 314 if __name__ == "__main__": |
273 parser = argparse.ArgumentParser( | 315 parser = argparse.ArgumentParser( |
274 description="Typecheck JavaScript using Closure compiler") | 316 description="Typecheck JavaScript using Closure compiler") |
275 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, | 317 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, |
276 help="Path to a source file to typecheck") | 318 help="Path to a source file to typecheck") |
277 single_file_group = parser.add_mutually_exclusive_group() | 319 single_file_group = parser.add_mutually_exclusive_group() |
278 single_file_group.add_argument("--single-file", dest="single_file", | 320 single_file_group.add_argument("--single-file", dest="single_file", |
279 action="store_true", | 321 action="store_true", |
280 help="Process each source file individually") | 322 help="Process each source file individually") |
(...skipping 10 matching lines...) Expand all Loading... | |
291 parser.add_argument("--success-stamp", | 333 parser.add_argument("--success-stamp", |
292 help="Timestamp file to update upon success") | 334 help="Timestamp file to update upon success") |
293 | 335 |
294 parser.set_defaults(single_file=True, strict=False) | 336 parser.set_defaults(single_file=True, strict=False) |
295 opts = parser.parse_args() | 337 opts = parser.parse_args() |
296 | 338 |
297 depends = opts.depends or [] | 339 depends = opts.depends or [] |
298 externs = opts.externs or set() | 340 externs = opts.externs or set() |
299 | 341 |
300 checker = Checker(verbose=opts.verbose, strict=opts.strict) | 342 checker = Checker(verbose=opts.verbose, strict=opts.strict) |
343 | |
301 if opts.single_file: | 344 if opts.single_file: |
302 for source in opts.sources: | 345 for source in opts.sources: |
303 depends, externs = build.inputs.resolve_recursive_dependencies( | 346 depends, externs = build.inputs.resolve_recursive_dependencies( |
304 source, | 347 source, depends, externs) |
305 depends, | 348 checker.check(source, depends, externs) |
306 externs) | |
307 has_errors, _ = checker.check(source, depends=depends, externs=externs) | |
308 if has_errors: | |
309 sys.exit(1) | |
310 | 349 |
311 if opts.out_file: | 350 if opts.out_file: |
312 out_dir = os.path.dirname(opts.out_file) | 351 out_dir = os.path.dirname(opts.out_file) |
313 if not os.path.exists(out_dir): | 352 if not os.path.exists(out_dir): |
314 os.makedirs(out_dir) | 353 os.makedirs(out_dir) |
315 # TODO(dbeam): write compiled file to |opts.out_file|. | 354 # Write a blank file so incremental builds work. Ninja compares the |
355 # modified time of output files to input files and rebuilds only if | |
356 # input files are newer. TODO(dbeam): write the compiled output instead | |
357 # when we're sure Closure compiling Chrome's JS doesn't break things. | |
316 open(opts.out_file, "w").write("") | 358 open(opts.out_file, "w").write("") |
317 else: | 359 else: |
318 has_errors, errors = checker.check_multiple(opts.sources) | 360 checker.check_multiple(opts.sources) |
319 if has_errors: | |
320 print errors | |
321 sys.exit(1) | |
322 | 361 |
323 if opts.success_stamp: | 362 if opts.success_stamp: |
324 with open(opts.success_stamp, 'w'): | 363 with open(opts.success_stamp, "w"): |
325 os.utime(opts.success_stamp, None) | 364 os.utime(opts.success_stamp, None) |
OLD | NEW |