|
| 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