2 # Copyright (c) 2012 Google Inc. All rights reserved.
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 from modular_build
import read_file
, write_file
32 import os
.path
as path
33 import generate_injected_script_externs
34 import generate_protocol_externs
42 import simplejson
as json
47 if len(sys
.argv
) == 2 and sys
.argv
[1] == '--help':
48 print("Usage: %s [module_names]" % path
.basename(sys
.argv
[0]))
49 print(" module_names list of modules for which the Closure compilation should run.")
50 print(" If absent, the entire frontend will be compiled.")
53 is_cygwin
= sys
.platform
== 'cygwin'
57 return subprocess
.Popen(arguments
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.STDOUT
)
59 def to_platform_path(filepath
):
62 return re
.sub(r
'^/cygdrive/(\w)', '\\1:', filepath
)
65 def to_platform_path_exact(filepath
):
68 output
, _
= popen(['cygpath', '-w', filepath
]).communicate()
69 # pylint: disable=E1103
70 return output
.strip().replace('\\', '\\\\')
72 scripts_path
= path
.dirname(path
.abspath(__file__
))
73 devtools_path
= path
.dirname(scripts_path
)
74 inspector_path
= path
.join(path
.dirname(devtools_path
), 'core', 'inspector')
75 devtools_frontend_path
= path
.join(devtools_path
, 'front_end')
76 patched_es6_externs_file
= to_platform_path(path
.join(devtools_frontend_path
, 'es6.js'))
77 global_externs_file
= to_platform_path(path
.join(devtools_frontend_path
, 'externs.js'))
78 protocol_externs_file
= path
.join(devtools_frontend_path
, 'protocol_externs.js')
79 injected_script_source_name
= path
.join(inspector_path
, 'InjectedScriptSource.js')
80 injected_script_externs_file
= path
.join(inspector_path
, 'injected_script_externs.js')
82 jsmodule_name_prefix
= 'jsmodule_'
83 runtime_module_name
= '_runtime'
85 type_checked_jsdoc_tags_list
= ['param', 'return', 'type', 'enum']
86 type_checked_jsdoc_tags_or
= '|'.join(type_checked_jsdoc_tags_list
)
88 # Basic regex for invalid JsDoc types: an object type name ([A-Z][A-Za-z0-9.]+[A-Za-z0-9]) not preceded by '!', '?', ':' (this, new), or '.' (object property).
89 invalid_type_regex
= re
.compile(r
'@(?:' + type_checked_jsdoc_tags_or
+ r
')\s*\{.*(?<![!?:.A-Za-z0-9])([A-Z][A-Za-z0-9.]+[A-Za-z0-9])[^/]*\}')
90 invalid_type_designator_regex
= re
.compile(r
'@(?:' + type_checked_jsdoc_tags_or
+ r
')\s*.*(?<![{: ])([?!])=?\}')
91 invalid_non_object_type_regex
= re
.compile(r
'@(?:' + type_checked_jsdoc_tags_or
+ r
')\s*\{.*(![a-z]+)[^/]*\}')
92 error_warning_regex
= re
.compile(r
'WARNING|ERROR')
93 loaded_css_regex
= re
.compile(r
'(?:registerRequiredCSS|WebInspector\.View\.createStyleElement)\s*\(\s*"(.+)"\s*\)')
95 java_build_regex
= re
.compile(r
'^\w+ version "(\d+)\.(\d+)')
98 generate_protocol_externs
.generate_protocol_externs(protocol_externs_file
, path
.join(devtools_path
, 'protocol.json'))
101 # Based on http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python.
104 return path
.isfile(fpath
) and os
.access(fpath
, os
.X_OK
)
106 fpath
, fname
= path
.split(program
)
111 for part
in os
.environ
["PATH"].split(os
.pathsep
):
112 part
= part
.strip('"')
113 exe_file
= path
.join(part
, program
)
119 def log_error(message
):
120 print 'ERROR: ' + message
122 def error_excepthook(exctype
, value
, traceback
):
124 sys
.__excepthook
__(exctype
, value
, traceback
)
125 sys
.excepthook
= error_excepthook
127 application_descriptors
= ['inspector.json', 'toolbox.json']
128 loader
= modular_build
.DescriptorLoader(devtools_frontend_path
)
129 descriptors
= loader
.load_applications(application_descriptors
)
130 modules_by_name
= descriptors
.modules
133 def hasErrors(output
):
134 return re
.search(error_warning_regex
, output
) != None
137 def verify_jsdoc_extra(additional_files
):
138 files
= [to_platform_path(file) for file in descriptors
.all_compiled_files() + additional_files
]
139 file_list
= tempfile
.NamedTemporaryFile(mode
='wt', delete
=False)
141 file_list
.write('\n'.join(files
))
144 return popen(java_exec
+ ['-jar', jsdoc_validator_jar
, '--files-list-name', to_platform_path_exact(file_list
.name
)]), file_list
147 def verify_jsdoc(additional_files
):
149 return descriptors
.all_compiled_files() + additional_files
152 for full_file_name
in file_list():
154 with
open(full_file_name
, 'r') as sourceFile
:
155 for line
in sourceFile
:
160 if verify_jsdoc_line(full_file_name
, lineIndex
, line
):
165 def verify_jsdoc_line(fileName
, lineIndex
, line
):
166 def print_error(message
, errorPosition
):
167 print '%s:%s: ERROR - %s%s%s%s%s%s' % (fileName
, lineIndex
, message
, os
.linesep
, line
, os
.linesep
, ' ' * errorPosition
+ '^', os
.linesep
)
171 match
= re
.search(invalid_type_regex
, line
)
173 print_error('Type "%s" nullability not marked explicitly with "?" (nullable) or "!" (non-nullable)' % match
.group(1), match
.start(1))
176 match
= re
.search(invalid_non_object_type_regex
, line
)
178 print_error('Non-object type explicitly marked with "!" (non-nullable), which is the default and should be omitted', match
.start(1))
181 match
= re
.search(invalid_type_designator_regex
, line
)
183 print_error('Type nullability indicator misplaced, should precede type', match
.start(1))
186 match
= re
.search(loaded_css_regex
, line
)
188 file = path
.join(devtools_frontend_path
, match
.group(1))
189 exists
= known_css
.get(file)
191 exists
= path
.isfile(file)
192 known_css
[file] = exists
194 print_error('Dynamically loaded CSS stylesheet is missing in the source tree', match
.start(1))
203 has_server_jvm
= True
204 java_path
= which('java')
206 java_path
= which('java.exe')
209 print 'NOTE: No Java executable found in $PATH.'
213 java_version_out
, _
= popen([java_path
, '-version']).communicate()
214 # pylint: disable=E1103
215 match
= re
.search(java_build_regex
, java_version_out
)
217 major
= int(match
.group(1))
218 minor
= int(match
.group(2))
219 is_ok
= major
>= required_major
and minor
>= required_minor
221 exec_command
= [java_path
, '-Xms1024m', '-server', '-XX:+TieredCompilation']
222 check_server_proc
= popen(exec_command
+ ['-version'])
223 check_server_proc
.communicate()
224 if check_server_proc
.returncode
!= 0:
225 # Not all Java installs have server JVMs.
226 exec_command
= exec_command
.remove('-server')
227 has_server_jvm
= False
230 print 'NOTE: Java executable version %d.%d or above not found in $PATH.' % (required_major
, required_minor
)
232 print 'Java executable: %s%s' % (java_path
, '' if has_server_jvm
else ' (no server JVM)')
235 java_exec
= find_java()
237 closure_compiler_jar
= to_platform_path(path
.join(scripts_path
, 'closure', 'compiler.jar'))
238 closure_runner_jar
= to_platform_path(path
.join(scripts_path
, 'compiler-runner', 'closure-runner.jar'))
239 jsdoc_validator_jar
= to_platform_path(path
.join(scripts_path
, 'jsdoc-validator', 'jsdoc-validator.jar'))
241 modules_dir
= tempfile
.mkdtemp()
242 common_closure_args
= [
243 '--summary_detail_level', '3',
244 '--jscomp_error', 'visibility',
245 '--compilation_level', 'SIMPLE_OPTIMIZATIONS',
246 '--warning_level', 'VERBOSE',
247 '--language_in=ES6_STRICT',
248 '--language_out=ES5_STRICT',
249 '--extra_annotation_name', 'suppressReceiverCheck',
250 '--extra_annotation_name', 'suppressGlobalPropertiesCheck',
251 '--module_output_path_prefix', to_platform_path_exact(modules_dir
+ path
.sep
)
254 worker_modules_by_name
= {}
255 dependents_by_module_name
= {}
257 for module_name
in descriptors
.application
:
258 module
= descriptors
.modules
[module_name
]
259 if descriptors
.application
[module_name
].get('type', None) == 'worker':
260 worker_modules_by_name
[module_name
] = module
261 for dep
in module
.get('dependencies', []):
262 list = dependents_by_module_name
.get(dep
)
265 dependents_by_module_name
[dep
] = list
266 list.append(module_name
)
269 def check_conditional_dependencies():
270 for name
in modules_by_name
:
271 for dep_name
in modules_by_name
[name
].get('dependencies', []):
272 dependency
= modules_by_name
[dep_name
]
273 if dependency
.get('experiment') or dependency
.get('condition'):
274 log_error('Module "%s" may not depend on the conditional module "%s"' % (name
, dep_name
))
277 check_conditional_dependencies()
280 def verify_worker_modules():
281 for name
in modules_by_name
:
282 for dependency
in modules_by_name
[name
].get('dependencies', []):
283 if dependency
in worker_modules_by_name
:
284 log_error('Module "%s" may not depend on the worker module "%s"' % (name
, dependency
))
287 verify_worker_modules()
290 def check_duplicate_files():
292 def check_module(module
, seen_files
, seen_modules
):
293 name
= module
['name']
294 seen_modules
[name
] = True
295 for dep_name
in module
.get('dependencies', []):
296 if not dep_name
in seen_modules
:
297 check_module(modules_by_name
[dep_name
], seen_files
, seen_modules
)
298 for source
in module
.get('scripts', []):
299 referencing_module
= seen_files
.get(source
)
300 if referencing_module
:
301 log_error('Duplicate use of %s in "%s" (previously seen in "%s")' % (source
, name
, referencing_module
))
302 seen_files
[source
] = name
304 for module_name
in worker_modules_by_name
:
305 check_module(worker_modules_by_name
[module_name
], {}, {})
307 print 'Checking duplicate files across modules...'
308 check_duplicate_files()
311 def module_arg(module_name
):
312 return ' --module ' + jsmodule_name_prefix
+ module_name
315 def modules_to_check():
316 if len(sys
.argv
) == 1:
317 return descriptors
.sorted_modules()
318 print 'Compiling only these modules: %s' % sys
.argv
[1:]
319 return [module
for module
in descriptors
.sorted_modules() if module
in set(sys
.argv
[1:])]
322 def dump_module(name
, recursively
, processed_modules
):
323 if name
in processed_modules
:
325 processed_modules
[name
] = True
326 module
= modules_by_name
[name
]
327 skipped_scripts
= set(module
.get('skip_compilation', []))
330 dependencies
= module
.get('dependencies', [])
332 for dependency
in dependencies
:
333 command
+= dump_module(dependency
, recursively
, processed_modules
)
334 command
+= module_arg(name
) + ':'
335 filtered_scripts
= descriptors
.module_compiled_files(name
)
336 command
+= str(len(filtered_scripts
))
337 firstDependency
= True
338 for dependency
in dependencies
+ [runtime_module_name
]:
343 firstDependency
= False
344 command
+= jsmodule_name_prefix
+ dependency
345 for script
in filtered_scripts
:
346 command
+= ' --js ' + to_platform_path(path
.join(devtools_frontend_path
, name
, script
))
349 print 'Compiling frontend...'
351 compiler_args_file
= tempfile
.NamedTemporaryFile(mode
='wt', delete
=False)
353 platform_protocol_externs_file
= to_platform_path(protocol_externs_file
)
354 runtime_js_path
= to_platform_path(path
.join(devtools_frontend_path
, 'Runtime.js'))
355 checked_modules
= modules_to_check()
356 for name
in checked_modules
:
357 closure_args
= ' '.join(common_closure_args
)
358 closure_args
+= ' --externs ' + to_platform_path(patched_es6_externs_file
)
359 closure_args
+= ' --externs ' + to_platform_path(global_externs_file
)
360 closure_args
+= ' --externs ' + platform_protocol_externs_file
361 runtime_module
= module_arg(runtime_module_name
) + ':1 --js ' + runtime_js_path
362 closure_args
+= runtime_module
+ dump_module(name
, True, {})
363 compiler_args_file
.write('%s %s%s' % (name
, closure_args
, os
.linesep
))
365 compiler_args_file
.close()
367 modular_compiler_proc
= popen(java_exec
+ ['-jar', closure_runner_jar
, '--compiler-args-file', to_platform_path_exact(compiler_args_file
.name
)])
370 def unclosure_injected_script(sourceFileName
, outFileName
):
372 source
= read_file(sourceFileName
)
374 def replace_function(matchobj
):
375 return re
.sub(r
'@param', 'param', matchobj
.group(1) or '') + '\n//' + matchobj
.group(2)
377 # Comment out the closure function and its jsdocs
378 source
= re
.sub(r
'(/\*\*(?:[\s\n]*\*\s*@param[^\n]+\n)+\s*\*/\s*)?\n(\(function)', replace_function
, source
, count
=1)
380 # Comment out its return statement
381 source
= re
.sub(r
'\n(\s*return\s+[^;]+;\s*\n\}\)\s*)$', '\n/*\\1*/', source
)
383 # Replace the "var Object" override with a "self.Object" one
384 source
= re
.sub(r
'\nvar Object =', '\nself.Object =', source
, count
=1)
386 write_file(outFileName
, source
)
388 injectedScriptSourceTmpFile
= to_platform_path(path
.join(inspector_path
, 'InjectedScriptSourceTmp.js'))
390 unclosure_injected_script(injected_script_source_name
, injectedScriptSourceTmpFile
)
392 print 'Compiling InjectedScriptSource.js...'
394 spawned_compiler_command
= java_exec
+ [
397 ] + common_closure_args
399 command
= spawned_compiler_command
+ [
400 '--externs', to_platform_path_exact(injected_script_externs_file
),
401 '--externs', to_platform_path_exact(protocol_externs_file
),
402 '--module', jsmodule_name_prefix
+ 'injected_script' + ':1',
403 '--js', to_platform_path(injectedScriptSourceTmpFile
)
406 injectedScriptCompileProc
= popen(command
)
408 print 'Compiling devtools.js...'
410 command
= spawned_compiler_command
+ [
411 '--externs', to_platform_path(global_externs_file
),
412 '--externs', to_platform_path(path
.join(devtools_frontend_path
, 'host', 'InspectorFrontendHostAPI.js')),
413 '--module', jsmodule_name_prefix
+ 'devtools_js' + ':1',
414 '--js', to_platform_path(path
.join(devtools_frontend_path
, 'devtools.js'))
416 devtoolsJSCompileProc
= popen(command
)
418 print 'Verifying JSDoc comments...'
419 additional_jsdoc_check_files
= [injectedScriptSourceTmpFile
]
420 errors_found |
= verify_jsdoc(additional_jsdoc_check_files
)
421 jsdocValidatorProc
, jsdocValidatorFileList
= verify_jsdoc_extra(additional_jsdoc_check_files
)
423 print 'Validating InjectedScriptSource.js...'
424 injectedscript_check_script_path
= path
.join(scripts_path
, "check_injected_script_source.py")
425 validateInjectedScriptProc
= popen([sys
.executable
, injectedscript_check_script_path
, injected_script_source_name
])
429 (jsdocValidatorOut
, _
) = jsdocValidatorProc
.communicate()
430 if jsdocValidatorOut
:
431 print ('JSDoc validator output:%s%s' % (os
.linesep
, jsdocValidatorOut
))
434 os
.remove(jsdocValidatorFileList
.name
)
436 (moduleCompileOut
, _
) = modular_compiler_proc
.communicate()
437 print 'Modular compilation output:'
439 start_module_regex
= re
.compile(r
'^@@ START_MODULE:(.+) @@$')
440 end_module_regex
= re
.compile(r
'^@@ END_MODULE @@$')
446 def skip_dependents(module_name
):
447 for skipped_module
in dependents_by_module_name
.get(module_name
, []):
448 skipped_modules
[skipped_module
] = True
450 has_module_output
= False
452 # pylint: disable=E1103
453 for line
in moduleCompileOut
.splitlines():
455 match
= re
.search(start_module_regex
, line
)
459 has_module_output
= True
460 module_error_count
= 0
462 module_name
= match
.group(1)
463 skip_module
= skipped_modules
.get(module_name
)
465 skip_dependents(module_name
)
467 match
= re
.search(end_module_regex
, line
)
470 module_output
.append(line
)
473 module_error_count
+= 1
474 skip_dependents(module_name
)
479 print 'Skipping module %s...' % module_name
480 elif not module_error_count
:
481 print 'Module %s compiled successfully: %s' % (module_name
, module_output
[0])
483 print 'Module %s compile failed: %s errors%s' % (module_name
, module_error_count
, os
.linesep
)
484 print os
.linesep
.join(module_output
)
486 if not has_module_output
:
487 print moduleCompileOut
490 print 'Total Closure errors: %d%s' % (error_count
, os
.linesep
)
493 (injectedScriptCompileOut
, _
) = injectedScriptCompileProc
.communicate()
494 print 'InjectedScriptSource.js compilation output:%s' % os
.linesep
, injectedScriptCompileOut
495 errors_found |
= hasErrors(injectedScriptCompileOut
)
497 (devtoolsJSCompileOut
, _
) = devtoolsJSCompileProc
.communicate()
498 print 'devtools.js compilation output:%s' % os
.linesep
, devtoolsJSCompileOut
499 errors_found |
= hasErrors(devtoolsJSCompileOut
)
501 (validateInjectedScriptOut
, _
) = validateInjectedScriptProc
.communicate()
502 print 'Validate InjectedScriptSource.js output:%s' % os
.linesep
, (validateInjectedScriptOut
if validateInjectedScriptOut
else '<empty>')
503 errors_found |
= hasErrors(validateInjectedScriptOut
)
506 print 'ERRORS DETECTED'
508 os
.remove(injectedScriptSourceTmpFile
)
509 os
.remove(compiler_args_file
.name
)
510 os
.remove(protocol_externs_file
)
511 shutil
.rmtree(modules_dir
, True)