1#!/usr/bin/env python 2# Copyright (c) 2015 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 6import argparse 7import logging 8import os 9import platform 10import shutil 11import subprocess 12import sys 13import re 14import json 15import tempfile 16 17 18_V8_DIR = os.path.abspath( 19 os.path.join(os.path.dirname(__file__), os.path.pardir, 'third_party', 20 'v8')) 21 22_JS_PARSER_DIR = os.path.abspath( 23 os.path.join(os.path.dirname(__file__), os.path.pardir, 'third_party', 24 'parse5', 'parse5.js')) 25 26 27_BOOTSTRAP_JS_DIR = os.path.abspath( 28 os.path.join(os.path.dirname(__file__), 'd8_bootstrap.js')) 29 30_BASE64_COMPAT_DIR = os.path.abspath( 31 os.path.join(os.path.dirname(__file__), 'base64_compat.js')) 32 33_PATH_UTILS_JS_DIR = os.path.abspath( 34 os.path.join(os.path.dirname(__file__), 'path_utils.js')) 35 36_HTML_IMPORTS_LOADER_JS_DIR = os.path.abspath( 37 os.path.join(os.path.dirname(__file__), 'html_imports_loader.js')) 38 39_HTML_TO_JS_GENERATOR_JS_DIR = os.path.abspath( 40 os.path.join(os.path.dirname(__file__), 'html_to_js_generator.js')) 41 42 43_BOOTSTRAP_JS_CONTENT = None 44 45 46def _ValidateSourcePaths(source_paths): 47 if source_paths is None: 48 return 49 for x in source_paths: 50 assert os.path.exists(x) 51 assert os.path.isdir(x) 52 assert os.path.isabs(x) 53 54 55def _EscapeJsString(s): 56 assert isinstance(s, str) 57 return json.dumps(s) 58 59def _RenderTemplateStringForJsSource(source, template, replacement_string): 60 return source.replace(template, _EscapeJsString(replacement_string)) 61 62 63def _GetBootStrapJsContent(source_paths): 64 assert isinstance(source_paths, list) 65 global _BOOTSTRAP_JS_CONTENT 66 if not _BOOTSTRAP_JS_CONTENT: 67 with open(_BOOTSTRAP_JS_DIR, 'r') as f: 68 _BOOTSTRAP_JS_CONTENT = f.read() 69 70 bsc = _BOOTSTRAP_JS_CONTENT 71 72 73 # Ensure that source paths are unique. 74 source_paths = list(set(source_paths)) 75 source_paths_string = '[%s]' % ( 76 ','.join(_EscapeJsString(s) for s in source_paths)) 77 bsc = bsc.replace('<%source_paths%>', source_paths_string) 78 bsc = _RenderTemplateStringForJsSource( 79 bsc, '<%current_working_directory%>', os.getcwd()) 80 bsc = _RenderTemplateStringForJsSource( 81 bsc, '<%path_utils_js_path%>', _PATH_UTILS_JS_DIR) 82 bsc = _RenderTemplateStringForJsSource( 83 bsc, '<%html_imports_loader_js_path%>', _HTML_IMPORTS_LOADER_JS_DIR) 84 bsc = _RenderTemplateStringForJsSource( 85 bsc, '<%html_to_js_generator_js_path%>', _HTML_TO_JS_GENERATOR_JS_DIR) 86 bsc = _RenderTemplateStringForJsSource( 87 bsc, '<%js_parser_path%>', _JS_PARSER_DIR) 88 bsc = _RenderTemplateStringForJsSource( 89 bsc, '<%base64_compat_path%>', _BASE64_COMPAT_DIR) 90 bsc += '\n//@ sourceURL=%s\n' % _BOOTSTRAP_JS_DIR 91 return bsc 92 93 94def _IsValidJsOrHTMLFile(parser, js_file_arg): 95 if not os.path.exists(js_file_arg): 96 parser.error('The file %s does not exist' % js_file_arg) 97 _, extension = os.path.splitext(js_file_arg) 98 if extension not in ('.js', '.html'): 99 parser.error('Input must be a JavaScript or HTML file') 100 return js_file_arg 101 102 103def _GetD8BinaryPathForPlatform(): 104 if platform.system() == 'Linux' and platform.machine() == 'x86_64': 105 return os.path.join(_V8_DIR, 'linux', 'x86_64', 'd8') 106 elif platform.system() == 'Darwin' and platform.machine() == 'x86_64': 107 return os.path.join(_V8_DIR, 'mac', 'x86_64', 'd8') 108 elif platform.system() == 'Windows' and platform.machine() == 'AMD64': 109 return os.path.join(_V8_DIR, 'win', 'AMD64', 'd8.exe') 110 else: 111 raise NotImplementedError( 112 'd8 binary for this platform (%s) and architecture (%s) is not yet' 113 ' supported' % (platform.system(), platform.machine())) 114 115 116class RunResult(object): 117 def __init__(self, returncode, stdout): 118 self.returncode = returncode 119 self.stdout = stdout 120 121 122def ExecuteFile(file_path, source_paths=None, js_args=None, v8_args=None, 123 stdout=subprocess.PIPE, stdin=subprocess.PIPE): 124 """Execute JavaScript program in |file_path|. 125 126 Args: 127 file_path: string file_path that contains path the .js or .html file to be 128 executed. 129 source_paths: the list of absolute paths containing code. All the imports 130 js_args: a list of string arguments to sent to the JS program. 131 132 Args stdout & stdin are the same as _RunFileWithD8. 133 134 Returns: 135 The string output from running the JS program. 136 """ 137 res = RunFile(file_path, source_paths, js_args, v8_args, stdout, stdin) 138 return res.stdout 139 140 141def RunFile(file_path, source_paths=None, js_args=None, v8_args=None, 142 stdout=subprocess.PIPE, stdin=subprocess.PIPE): 143 """Runs JavaScript program in |file_path|. 144 145 Args are same as ExecuteFile. 146 147 Returns: 148 A RunResult containing the program's output. 149 """ 150 assert os.path.isfile(file_path) 151 _ValidateSourcePaths(source_paths) 152 153 _, extension = os.path.splitext(file_path) 154 if not extension in ('.html', '.js'): 155 raise ValueError('Can only execute .js or .html file. File %s has ' 156 'unsupported file type: %s' % (file_path, extension)) 157 if source_paths is None: 158 source_paths = [os.path.dirname(file_path)] 159 160 abs_file_path_str = _EscapeJsString(os.path.abspath(file_path)) 161 162 try: 163 temp_dir = tempfile.mkdtemp() 164 temp_boostrap_file = os.path.join(temp_dir, '_tmp_boostrap.js') 165 with open(temp_boostrap_file, 'w') as f: 166 f.write(_GetBootStrapJsContent(source_paths)) 167 if extension == '.html': 168 f.write('\nHTMLImportsLoader.loadHTMLFile(%s, %s);' % 169 (abs_file_path_str, abs_file_path_str)) 170 else: 171 f.write('\nHTMLImportsLoader.loadFile(%s);' % abs_file_path_str) 172 return _RunFileWithD8(temp_boostrap_file, js_args, v8_args, stdout, stdin) 173 finally: 174 shutil.rmtree(temp_dir) 175 176 177def ExecuteJsString(js_string, source_paths=None, js_args=None, v8_args=None, 178 original_file_name=None, stdout=subprocess.PIPE, 179 stdin=subprocess.PIPE): 180 res = RunJsString(js_string, source_paths, js_args, v8_args, 181 original_file_name, stdout, stdin) 182 return res.stdout 183 184 185def RunJsString(js_string, source_paths=None, js_args=None, v8_args=None, 186 original_file_name=None, stdout=subprocess.PIPE, 187 stdin=subprocess.PIPE): 188 _ValidateSourcePaths(source_paths) 189 190 try: 191 temp_dir = tempfile.mkdtemp() 192 if original_file_name: 193 name = os.path.basename(original_file_name) 194 name, _ = os.path.splitext(name) 195 temp_file = os.path.join(temp_dir, '%s.js' % name) 196 else: 197 temp_file = os.path.join(temp_dir, 'temp_program.js') 198 with open(temp_file, 'w') as f: 199 f.write(js_string) 200 return RunFile(temp_file, source_paths, js_args, v8_args, stdout, stdin) 201 finally: 202 shutil.rmtree(temp_dir) 203 204 205def _RunFileWithD8(js_file_path, js_args, v8_args, stdout, stdin): 206 """ Execute the js_files with v8 engine and return the output of the program. 207 208 Args: 209 js_file_path: the string path of the js file to be run. 210 js_args: a list of arguments to passed to the |js_file_path| program. 211 v8_args: extra arguments to pass into d8. (for the full list of these 212 options, run d8 --help) 213 stdout: where to pipe the stdout of the executed program to. If 214 subprocess.PIPE is used, stdout will be returned in RunResult.out. 215 Otherwise RunResult.out is None 216 stdin: specify the executed program's input. 217 """ 218 if v8_args is None: 219 v8_args = [] 220 assert isinstance(v8_args, list) 221 args = [_GetD8BinaryPathForPlatform()] + v8_args 222 args.append(os.path.abspath(js_file_path)) 223 full_js_args = [args[0]] 224 if js_args: 225 full_js_args += js_args 226 227 args += ['--js_arguments'] + full_js_args 228 229 # Set stderr=None since d8 doesn't write into stderr anyway. 230 sp = subprocess.Popen(args, stdout=stdout, stderr=None, stdin=stdin) 231 out, _ = sp.communicate() 232 233 # On Windows, d8's print() method add the carriage return characters \r to 234 # newline, which make the output different from d8 on posix. We remove the 235 # extra \r's to make the output consistent with posix platforms. 236 if platform.system() == 'Windows' and out: 237 out = re.sub('\r+\n', '\n', out) 238 239 # d8 uses returncode 1 to indicate an uncaught exception, but 240 # _RunFileWithD8 needs to distingiush between that and quit(1). 241 # 242 # To fix this, d8_bootstrap.js monkeypatches D8's quit function to 243 # adds 1 to an intentioned nonzero quit. So, now, we have to undo this 244 # logic here in order to raise/return the right thing. 245 returncode = sp.returncode 246 if returncode == 0: 247 return RunResult(0, out) 248 elif returncode == 1: 249 if out: 250 raise RuntimeError( 251 'Exception raised when executing %s:\n%s' % (js_file_path, out)) 252 else: 253 raise RuntimeError( 254 'Exception raised when executing %s. ' 255 '(Error stack is dumped into stdout)' % js_file_path) 256 else: 257 return RunResult(returncode - 1, out) 258 259 260def main(): 261 parser = argparse.ArgumentParser( 262 description='Run JavaScript file with v8 engine') 263 parser.add_argument('file_name', help='input file', metavar='FILE', 264 type=lambda f: _IsValidJsOrHTMLFile(parser, f)) 265 parser.add_argument('--js_args', help='arguments for the js program', 266 nargs='+') 267 parser.add_argument('--source_paths', help='search path for the js program', 268 nargs='+', type=str) 269 270 args = parser.parse_args() 271 if args.source_paths: 272 args.source_paths = [os.path.abspath(x) for x in args.source_paths] 273 else: 274 args.source_paths = [os.path.abspath(os.path.dirname(args.file_name))] 275 logging.warning( 276 '--source_paths is not specified. Use %s for search path.' % 277 args.source_paths) 278 res = RunFile(args.file_name, source_paths=args.source_paths, 279 js_args=args.js_args, stdout=sys.stdout, stdin=sys.stdin) 280 return res.returncode 281