Skip to content

Commit 14db6b7

Browse files
committed
Add a "single" source/header file generation and build mode
Introduce a new way of building: before any file is compiled all source files are combined into a single C file and all header files into a single H file (per subdir). This new approach makes it possible to quickly integrate JerryScript into other projects: ``` $ gcc -o demo demo.c jerryscript.c jerryscript-port-default.c -lm ``` To use the source generator run: ``` $ cmake -Bbuild_dir -H. -DENABLE_ALL_IN_ONE_SOURCE=ON $ make -C build_dir generate-single-source ``` This will create the following files in the `build_dir`: * jerryscript.c * jerryscript.h * jerryscript-config.h * jerryscript-port-default.c * jerryscript-port-default.h JerryScript-DCO-1.0-Signed-off-by: Peter Gal [email protected]
1 parent f3c8eee commit 14db6b7

File tree

4 files changed

+330
-0
lines changed

4 files changed

+330
-0
lines changed

jerry-core/CMakeLists.txt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,56 @@ if(ENABLE_ALL_IN_ONE)
150150
set(SOURCE_CORE_FILES ${ALL_IN_FILE})
151151
endif()
152152

153+
# "Single" JerryScript source/header build.
154+
# The process will create the following files:
155+
# * jerryscript.c
156+
# * jerryscript.h
157+
# * jerryscript-config.h
158+
if(ENABLE_ALL_IN_ONE_SOURCE)
159+
# Create a default configuration
160+
set(JERRYSCRIPT_CONFIG_H "${CMAKE_BINARY_DIR}/jerryscript-config.h")
161+
set(JERRYSCRIPT_SOURCE_CONFIG_H "${CMAKE_CURRENT_SOURCE_DIR}/config.h")
162+
add_custom_command(OUTPUT ${JERRYSCRIPT_CONFIG_H}
163+
COMMAND ${CMAKE_COMMAND} -E copy ${JERRYSCRIPT_SOURCE_CONFIG_H} ${JERRYSCRIPT_CONFIG_H}
164+
DEPENDS ${JERRYSCRIPT_SOURCE_CONFIG_H})
165+
166+
# Create single C file
167+
file(GLOB HEADER_CORE_FILES *.h)
168+
set(ALL_IN_FILE "${CMAKE_BINARY_DIR}/jerryscript.c")
169+
set(ALL_IN_FILE_H "${CMAKE_BINARY_DIR}/jerryscript.h")
170+
add_custom_command(OUTPUT ${ALL_IN_FILE}
171+
COMMAND python ${CMAKE_SOURCE_DIR}/tools/srcmerger.py
172+
--base-dir ${CMAKE_CURRENT_SOURCE_DIR}
173+
--input ${CMAKE_CURRENT_SOURCE_DIR}/api/jerry.c
174+
--output ${ALL_IN_FILE}
175+
--append-c-files
176+
--remove-include jerryscript.h
177+
--remove-include jerryscript-port.h
178+
--remove-include jerryscript-compiler.h
179+
--remove-include jerryscript-core.h
180+
--remove-include jerryscript-debugger.h
181+
--remove-include jerryscript-debugger-transport.h
182+
--remove-include jerryscript-port.h
183+
--remove-include jerryscript-snapshot.h
184+
--remove-include config.h
185+
--push-include jerryscript.h
186+
DEPENDS ${SOURCE_CORE_FILES} ${ALL_IN_FILE_H} ${JERRYSCRIPT_CONFIG_H}
187+
)
188+
add_custom_command(OUTPUT ${ALL_IN_FILE_H}
189+
COMMAND python ${CMAKE_SOURCE_DIR}/tools/srcmerger.py
190+
--base-dir ${CMAKE_CURRENT_SOURCE_DIR}
191+
--input ${CMAKE_CURRENT_SOURCE_DIR}/include/jerryscript.h
192+
--output ${ALL_IN_FILE_H}
193+
--remove-include config.h
194+
--push-include jerryscript-config.h
195+
DEPENDS ${HEADER_CORE_FILES} ${JERRYSCRIPT_CONFIG_H}
196+
)
197+
add_custom_target(generate-single-source-jerry DEPENDS ${ALL_IN_FILE} ${ALL_IN_FILE_H})
198+
add_custom_target(generate-single-source DEPENDS generate-single-source-jerry)
199+
200+
set(SOURCE_CORE_FILES ${ALL_IN_FILE} ${ALL_IN_FILE_H})
201+
endif()
202+
153203
# Third-party
154204
# Valgrind
155205
set(INCLUDE_THIRD_PARTY_VALGRIND "${CMAKE_SOURCE_DIR}/third-party/valgrind")

jerry-port/default/CMakeLists.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,43 @@ set(INCLUDE_PORT_DEFAULT "${CMAKE_CURRENT_SOURCE_DIR}/include")
2222
# Source directories
2323
file(GLOB SOURCE_PORT_DEFAULT *.c)
2424

25+
# "Single" JerryScript source/header build.
26+
# The process will create the following files:
27+
# * jerryscript-port-default.c
28+
# * jerryscript-port-default.h
29+
if(ENABLE_ALL_IN_ONE_SOURCE)
30+
file(GLOB HEADER_PORT_DEFAULT *.h)
31+
set(ALL_IN_FILE "${CMAKE_BINARY_DIR}/jerryscript-port-default.c")
32+
set(ALL_IN_FILE_H "${CMAKE_BINARY_DIR}/jerryscript-port-default.h")
33+
add_custom_command(OUTPUT ${ALL_IN_FILE}
34+
COMMAND python ${CMAKE_SOURCE_DIR}/tools/srcmerger.py
35+
--base-dir ${CMAKE_CURRENT_SOURCE_DIR}
36+
--output ${ALL_IN_FILE}
37+
--append-c-files
38+
--remove-include jerryscript-port.h
39+
--remove-include jerryscript-port-default.h
40+
--remove-include jerryscript-debugger.h
41+
--push-include jerryscript.h
42+
--push-include jerryscript-port-default.h
43+
DEPENDS ${SOURCE_PORT_DEFAULT} ${ALL_IN_FILE_H}
44+
)
45+
46+
add_custom_command(OUTPUT ${ALL_IN_FILE_H}
47+
COMMAND python ${CMAKE_SOURCE_DIR}/tools/srcmerger.py
48+
--base-dir ${CMAKE_CURRENT_SOURCE_DIR}/
49+
--input ${CMAKE_CURRENT_SOURCE_DIR}/include/jerryscript-port-default.h
50+
--output ${ALL_IN_FILE_H}
51+
--remove-include jerryscript-port.h
52+
--remove-include jerryscript.h
53+
--push-include jerryscript.h
54+
DEPENDS ${HEADER_PORT_DEFAULT}
55+
)
56+
add_custom_target(generate-single-source-port DEPENDS ${ALL_IN_FILE} ${ALL_IN_FILE_H})
57+
add_dependencies(generate-single-source generate-single-source-port)
58+
59+
set(SOURCE_PORT_DEFAULT ${ALL_IN_FILE} ${ALL_IN_FILE_H})
60+
endif()
61+
2562
# Define _BSD_SOURCE and _DEFAULT_SOURCE
2663
# (should only be necessary if we used compiler default libc but not checking that)
2764
set(DEFINES_PORT_DEFAULT _BSD_SOURCE _DEFAULT_SOURCE)

tools/run-tests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ def skip_if(condition, desc):
160160
['--regexp-recursion-limit=1000']),
161161
Options('buildoption_test-vm_recursion_limit',
162162
OPTIONS_VM_RECURSION_LIMIT),
163+
Options('buildoption_test-single-source',
164+
['--cmake-param=-DENABLE_ALL_IN_ONE_SOURCE=ON']),
163165
]
164166

165167
def get_arguments():

tools/srcmerger.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright JS Foundation and other contributors, http://js.foundation
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
from __future__ import print_function
17+
18+
import argparse
19+
import fnmatch
20+
import logging
21+
import os
22+
import re
23+
import sys
24+
25+
26+
class SourceMerger(object):
27+
# pylint: disable=too-many-instance-attributes
28+
29+
_RE_INCLUDE = re.compile(r'\s*#include ("|<)(.*?)("|>)\n$')
30+
31+
def __init__(self, h_files, extra_includes=None, remove_includes=None):
32+
self._log = logging.getLogger('sourcemerger')
33+
self._last_builtin = None
34+
self._processed = []
35+
self._output = []
36+
self._h_files = h_files
37+
self._extra_includes = extra_includes or []
38+
self._remove_includes = remove_includes
39+
# The copyright will be loaded from the first input file
40+
self._copyright = {'lines': [], 'loaded': False}
41+
42+
def _process_non_include(self, line, file_level):
43+
# Special case #2: Builtin include header name usage
44+
if line.strip() == "#include BUILTIN_INC_HEADER_NAME":
45+
assert self._last_builtin is not None, 'No previous BUILTIN_INC_HEADER_NAME definition'
46+
self._log.debug('[%d] Detected usage of BUILTIN_INC_HEADER_NAME, including: %s',
47+
file_level, self._last_builtin)
48+
self.add_file(self._h_files[self._last_builtin])
49+
# return from the function as we have processed the included file
50+
return
51+
52+
# Special case #1: Builtin include header name definition
53+
if line.startswith('#define BUILTIN_INC_HEADER_NAME '):
54+
# the line is in this format: #define BUILTIN_INC_HEADER_NAME "<filename>"
55+
self._last_builtin = line.split('"', 2)[1]
56+
self._log.debug('[%d] Detected definition of BUILTIN_INC_HEADER_NAME: %s',
57+
file_level, self._last_builtin)
58+
59+
# the line is not anything special, just push it into the output
60+
self._output.append(line)
61+
62+
def add_file(self, filename, file_level=0):
63+
if os.path.basename(filename) in self._processed:
64+
self._log.warning('Tried to to process an already processed file: "%s"', filename)
65+
return
66+
67+
file_level += 1
68+
69+
# mark the start of the new file in the output
70+
self._output.append('#line 1 "%s"\n' % (filename))
71+
72+
line_idx = 0
73+
with open(filename, 'r') as input_file:
74+
in_copyright = False
75+
for line in input_file:
76+
line_idx += 1
77+
78+
if not in_copyright and line.startswith('/* Copyright '):
79+
in_copyright = True
80+
if not self._copyright['loaded']:
81+
self._copyright['lines'].append(line)
82+
continue
83+
84+
if in_copyright:
85+
if not self._copyright['loaded']:
86+
self._copyright['lines'].append(line)
87+
88+
if line.strip().endswith('*/'):
89+
in_copyright = False
90+
self._copyright['loaded'] = True
91+
92+
continue
93+
94+
# check if the line is an '#include' line
95+
match = SourceMerger._RE_INCLUDE.match(line)
96+
if not match:
97+
# the line is not a header
98+
self._process_non_include(line, file_level)
99+
continue
100+
101+
if match.group(1) == '<':
102+
# found a "global" include
103+
self._output.append(line)
104+
continue
105+
106+
name = match.group(2)
107+
108+
if name in self._remove_includes:
109+
self._log.debug('[%d] Removing include line (%s:%d): %s',
110+
file_level, filename, line_idx, line.strip())
111+
continue
112+
113+
if name not in self._h_files:
114+
self._log.warning('[%d] Include not found: "%s" in "%s:%d"',
115+
file_level, name, filename, line_idx)
116+
self._output.append(line)
117+
continue
118+
119+
if name in self._processed:
120+
self._log.debug('[%d] Already included: "%s"',
121+
file_level, name)
122+
continue
123+
124+
self._log.debug('[%d] Including: "%s"',
125+
file_level, self._h_files[name])
126+
self.add_file(self._h_files[name])
127+
128+
# mark the continuation of the current file in the output
129+
self._output.append('#line %d "%s"\n' % (line_idx + 1, filename))
130+
131+
if not name.endswith('.inc.h'):
132+
# if the included file is not a "*.inc.h" file mark it as processed
133+
self._processed.append(name)
134+
135+
file_level -= 1
136+
if not filename.endswith('.inc.h'):
137+
self._processed.append(os.path.basename(filename))
138+
139+
def write_output(self, out_fp):
140+
for line in self._copyright['lines']:
141+
out_fp.write(line)
142+
143+
for include in self._extra_includes:
144+
out_fp.write('#include "%s"\n' % include)
145+
146+
for line in self._output:
147+
out_fp.write(line)
148+
149+
150+
def match_files(base_dir, pattern):
151+
"""
152+
Return the files matching the given pattern.
153+
154+
:param base_dir: directory to search in
155+
:param pattern: file pattern to use
156+
:returns generator: the generator which iterates the matching file names
157+
"""
158+
for path, _, files in os.walk(base_dir):
159+
for name in files:
160+
if fnmatch.fnmatch(name, pattern):
161+
yield os.path.join(path, name)
162+
163+
164+
def collect_files(base_dir, pattern):
165+
"""
166+
Collect files in the provided base directory given a file pattern.
167+
Will collect all files in the base dir recursively.
168+
169+
:param base_dir: directory to search in
170+
:param pattern: file pattern to use
171+
:returns dictionary: a dictionary file base name -> file path mapping
172+
"""
173+
name_mapping = {}
174+
for fname in match_files(base_dir, pattern):
175+
name = os.path.basename(fname)
176+
177+
if name in name_mapping:
178+
print('Duplicate name detected: "%s" and "%s"' % (name, name_mapping[name]))
179+
continue
180+
181+
name_mapping[name] = fname
182+
183+
return name_mapping
184+
185+
186+
def run_merger(args):
187+
h_files = collect_files(args.base_dir, '*.h')
188+
c_files = collect_files(args.base_dir, '*.c')
189+
190+
for name in args.remove_include:
191+
c_files.pop(name, '')
192+
h_files.pop(name, '')
193+
194+
merger = SourceMerger(h_files, args.push_include, args.remove_include)
195+
if args.input_file:
196+
merger.add_file(args.input_file)
197+
198+
if args.append_c_files:
199+
# if the input file is in the C files list it should be removed to avoid
200+
# double inclusion of the file
201+
if args.input_file:
202+
input_name = os.path.basename(args.input_file)
203+
c_files.pop(input_name, '')
204+
205+
# Add the C files in reverse the order to make sure that builtins are
206+
# not at the beginning.
207+
for fname in sorted(c_files.values(), reverse=True):
208+
merger.add_file(fname)
209+
210+
with open(args.output_file, 'w') as output:
211+
merger.write_output(output)
212+
213+
214+
def main():
215+
parser = argparse.ArgumentParser(description='Merge source/header files.')
216+
parser.add_argument('--base-dir', metavar='DIR', type=str, dest='base_dir',
217+
help='', default=os.path.curdir)
218+
parser.add_argument('--input', metavar='FILE', type=str, dest='input_file',
219+
help='Main input source/header file')
220+
parser.add_argument('--output', metavar='FILE', type=str, dest='output_file',
221+
help='Output source/header file')
222+
parser.add_argument('--append-c-files', dest='append_c_files', default=False,
223+
action='store_true', help='das')
224+
parser.add_argument('--remove-include', action='append', default=[])
225+
parser.add_argument('--push-include', action='append', default=[])
226+
parser.add_argument('--verbose', '-v', action='store_true', default=False)
227+
228+
args = parser.parse_args()
229+
230+
log_level = logging.WARNING
231+
if args.verbose:
232+
log_level = logging.DEBUG
233+
234+
logging.basicConfig(level=log_level)
235+
logging.debug('Starting merge with args: %s', str(sys.argv))
236+
237+
run_merger(args)
238+
239+
240+
if __name__ == "__main__":
241+
main()

0 commit comments

Comments
 (0)