1#!/usr/bin/env python 2# 3# Copyright 2006-2008 the V8 project authors. 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 6# met: 7# 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 12# disclaimer in the documentation and/or other materials provided 13# with the distribution. 14# * Neither the name of Google Inc. nor the names of its 15# contributors may be used to endorse or promote products derived 16# from this software without specific prior written permission. 17# 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. 29 30""" 31This is a utility for converting JavaScript source code into uint16_t[], 32that are used for embedding JavaScript code into the Node.js binary. 33""" 34import argparse 35import os 36import re 37import functools 38import codecs 39import utils 40 41def ReadFile(filename): 42 if is_verbose: 43 print(filename) 44 with codecs.open(filename, "r", "utf-8") as f: 45 lines = f.read() 46 return lines 47 48 49TEMPLATE = """ 50#include "env-inl.h" 51#include "node_native_module.h" 52#include "node_internals.h" 53 54namespace node {{ 55 56namespace native_module {{ 57 58{0} 59 60void NativeModuleLoader::LoadJavaScriptSource() {{ 61 {1} 62}} 63 64UnionBytes NativeModuleLoader::GetConfig() {{ 65 return UnionBytes(config_raw, {2}); // config.gypi 66}} 67 68}} // namespace native_module 69 70}} // namespace node 71""" 72 73ONE_BYTE_STRING = """ 74static const uint8_t {0}[] = {{ 75{1} 76}}; 77""" 78 79TWO_BYTE_STRING = """ 80static const uint16_t {0}[] = {{ 81{1} 82}}; 83""" 84 85INITIALIZER = 'source_.emplace("{0}", UnionBytes{{{1}, {2}}});' 86 87CONFIG_GYPI_ID = 'config_raw' 88 89SLUGGER_RE =re.compile('[.\-/]') 90 91is_verbose = False 92 93def GetDefinition(var, source, step=30): 94 template = ONE_BYTE_STRING 95 code_points = [ord(c) for c in source] 96 if any(c > 127 for c in code_points): 97 template = TWO_BYTE_STRING 98 # Treat non-ASCII as UTF-8 and encode as UTF-16 Little Endian. 99 encoded_source = bytearray(source, 'utf-16le') 100 code_points = [ 101 encoded_source[i] + (encoded_source[i + 1] * 256) 102 for i in range(0, len(encoded_source), 2) 103 ] 104 105 # For easier debugging, align to the common 3 char for code-points. 106 elements_s = ['%3s' % x for x in code_points] 107 # Put no more then `step` code-points in a line. 108 slices = [elements_s[i:i + step] for i in range(0, len(elements_s), step)] 109 lines = [','.join(s) for s in slices] 110 array_content = ',\n'.join(lines) 111 definition = template.format(var, array_content) 112 113 return definition, len(code_points) 114 115 116def AddModule(filename, definitions, initializers): 117 code = ReadFile(filename) 118 name = NormalizeFileName(filename) 119 slug = SLUGGER_RE.sub('_', name) 120 var = slug + '_raw' 121 definition, size = GetDefinition(var, code) 122 initializer = INITIALIZER.format(name, var, size) 123 definitions.append(definition) 124 initializers.append(initializer) 125 126def NormalizeFileName(filename): 127 split = filename.split('/') 128 if split[0] == 'deps': 129 split = ['internal'] + split 130 else: # `lib/**/*.js` so drop the 'lib' part 131 split = split[1:] 132 if len(split): 133 filename = '/'.join(split) 134 return os.path.splitext(filename)[0] 135 136 137def JS2C(source_files, target): 138 # Build source code lines 139 definitions = [] 140 initializers = [] 141 142 for filename in source_files['.js']: 143 AddModule(filename, definitions, initializers) 144 145 config_def, config_size = handle_config_gypi(source_files['config.gypi']) 146 definitions.append(config_def) 147 148 # Emit result 149 definitions = ''.join(definitions) 150 initializers = '\n '.join(initializers) 151 out = TEMPLATE.format(definitions, initializers, config_size) 152 write_if_chaged(out, target) 153 154 155def handle_config_gypi(config_filename): 156 # if its a gypi file we're going to want it as json 157 # later on anyway, so get it out of the way now 158 config = ReadFile(config_filename) 159 config = jsonify(config) 160 config_def, config_size = GetDefinition(CONFIG_GYPI_ID, config) 161 return config_def, config_size 162 163 164def jsonify(config): 165 # 1. string comments 166 config = re.sub(r'#.*?\n', '', config) 167 # 2. join multiline strings 168 config = re.sub(r"'$\s+'", '', config, flags=re.M) 169 # 3. normalize string literals from ' into " 170 config = re.sub('\'', '"', config) 171 # 4. turn pseudo-booleans strings into Booleans 172 config = re.sub('"true"', 'true', config) 173 config = re.sub('"false"', 'false', config) 174 return config 175 176 177def write_if_chaged(content, target): 178 if os.path.exists(target): 179 with open(target, 'rt') as existing: 180 old_content = existing.read() 181 else: 182 old_content = '' 183 if old_content == content: 184 os.utime(target, None) 185 return 186 with open(target, "wt") as output: 187 output.write(content) 188 189 190def SourceFileByExt(files_by_ext, filename): 191 """ 192 :type files_by_ext: dict 193 :type filename: str 194 :rtype: dict 195 """ 196 ext = os.path.splitext(filename)[-1] 197 files_by_ext.setdefault(ext, []).append(filename) 198 return files_by_ext 199 200def main(): 201 parser = argparse.ArgumentParser( 202 description='Convert code files into `uint16_t[]`s', 203 fromfile_prefix_chars='@' 204 ) 205 parser.add_argument('--target', help='output file') 206 parser.add_argument( 207 '--directory', 208 default=None, 209 help='input file directory') 210 parser.add_argument('--verbose', action='store_true', help='output file') 211 parser.add_argument('sources', nargs='*', help='input files') 212 options = parser.parse_args() 213 global is_verbose 214 is_verbose = options.verbose 215 sources = options.sources 216 if options.directory is not None: 217 js_files = utils.SearchFiles(options.directory, 'js') 218 mjs_files = utils.SearchFiles(options.directory, 'mjs') 219 sources = js_files + mjs_files + options.sources 220 221 source_files = functools.reduce(SourceFileByExt, sources, {}) 222 223 # Should have exactly 2 types: `.js`, and `.gypi` 224 assert len(source_files) == 2 225 # Currently config.gypi is the only `.gypi` file allowed 226 assert source_files['.gypi'] == ['config.gypi'] 227 source_files['config.gypi'] = source_files.pop('.gypi')[0] 228 JS2C(source_files, options.target) 229 230 231if __name__ == "__main__": 232 main() 233