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