amalgam.py 5.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
#!/usr/bin/env python
#
# Generate a reversible amalgamation of several C source files
# along with their required internal headers.
#
# This script assumes that there are a bunch of C files, a bunch
# of private header files and one public header file.
#
# The script takes a list of C file names, parses `#include` directives
# found in them and recursively resolves dependencies in such a way
# that a header referenced from an included header will be emitted before the
# header that depends on it. All headers will always be emitted before the
# source files.
#
# The embedded include files usually contain private internals.
# However sometimes it's necessary for some other tools or for advanced users
# to have access to internal definitions. One such example is the generated
# C source containing frozen heap. The amalgamation script will allow users
# to include the amalgamated C file and cause extract only the internal headers:
#
#     #define NS_EXPORT_INTERNAL_HEADERS
#     #include "v7.c"
#
# Where `NS` can be overridden via the --prefix flag.
# This feature can be enabled with the --exportable-headers, and basically
# all it does is to wrap the C body in a preprocessor guard.
#
# TODO(mkm): make it work also for mongoose where we also generate
# the public header from a bunch of unamalgamated header files.
# Currently this script can handle mongoose amalgamation because it doesn't
# flip the --autoinc flag.
#

import argparse
import re
import sys
import os
from StringIO import StringIO

parser = argparse.ArgumentParser(description='Produce an amalgamated source')
parser.add_argument('--prefix', default="NS",
                    help='prefix for MODULE_LINES guard')
parser.add_argument('--srcdir', default=".", help='source dir')
parser.add_argument('--ignore', default="",
                    help='comma separated list of files to not amalgam')
# hack, teach amalgam to render the LICENSE file instead
parser.add_argument('--first', type=str, help='put this file in first position.'
                    ' Usually contains licensing info')
parser.add_argument('--public-header', dest="public",
                    help='name of the public header file that will be'
                    ' included at the beginning of the file')
parser.add_argument('--autoinc', action='store_true',
                    help='automatically embed include files')
54 55 56 57
parser.add_argument('--strict', action='store_true',
                    help='fail loudly if an include file cannot be resolved')
parser.add_argument('--norel', action='store_true',
                    help="do not try to compute a friendly relative path")
58 59 60 61 62 63
parser.add_argument('--exportable-headers', dest="export", action='store_true',
                    help='allow exporting internal headers')
parser.add_argument('-I', default=".", dest='include_path', help='include path')
parser.add_argument('sources', nargs='*', help='sources')

class File(object):
64
    def __init__(self, name, parent_name):
65
        self.name = name
66
        self.parent_name = parent_name
67
        self.buf = StringIO()
68
        emit_file(self.buf, self.name, self.parent_name)
69 70 71 72 73 74 75 76 77 78 79 80 81 82

    def emit(self):
        print self.buf.getvalue(),


args = parser.parse_args()

sources = []
includes = []

already_included = set()

ignore_files = [i.strip() for i in args.ignore.split(',')]

83
def should_ignore(name, parent_name):
84
    return (name in already_included
85
            or not (args.strict or os.path.exists(resolve(name, parent_name)))
86 87
            or name in ignore_files)

88 89 90 91 92 93 94
def resolve(path, parent_name):
    path_from_parent = None
    if parent_name != None and not os.path.isabs(path):
        # calculate the path relative to the "parent_name" file, i.e. to the file
        # which includes the current one.
        path_from_parent = os.path.join(os.path.dirname(parent_name), path)

95 96
    if os.path.isabs(path) or os.path.exists(path):
        p = path
97 98
    elif path_from_parent != None and os.path.exists(path_from_parent):
        p = path_from_parent
99 100
    else:
        p = os.path.join(args.include_path, path)
101
    if os.path.exists(p) and not args.norel:
102
        p = os.path.realpath(p).replace('%s/' % os.getcwd(), '')
103
    # print >>sys.stderr, '%s -> %s (cwd %s)' % (path, p, os.getcwd())
104 105
    return p

106
def emit_line_directive(out, name, parent_name):
107 108 109 110
    print >>out, '''#ifdef %(prefix)s_MODULE_LINES
#line 1 "%(name)s"
#endif''' % dict(
    prefix = args.prefix,
111
    name = resolve(name, parent_name),
112 113
)

114 115 116
def emit_body(out, name, parent_name):
    resolved_name = resolve(name, parent_name)
    if not args.strict and not os.path.exists(resolved_name):
117 118 119
        print >>out, '#include "%s"' % (name,)
        return

120
    with open(resolved_name) as f:
121
        for l in f:
122
            match = re.match('( *#include "(.*)")', l)
123
            if match:
124
                all, path_to_include = match.groups()
125
                if args.autoinc:
126 127 128
                    if not should_ignore(path_to_include, parent_name):
                        already_included.add(path_to_include)
                        includes.append(File(path_to_include, resolved_name))
129 130 131 132 133
                print >>out, '/* Amalgamated: %s */' % (all,)
            else:
                print >>out, l,


134 135 136
def emit_file(out, name, parent_name):
    emit_line_directive(out, name, parent_name)
    emit_body(out, name, parent_name)
137 138

for i in args.sources:
139
    sources.append(File(i, None))
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162

if args.first:
    for inc in reversed(args.first.split(',')):
        for i, f in enumerate(includes):
            if f.name == inc:
                del includes[i]
                includes.insert(0, f)
                break

# emitting

if args.public:
    print '#include "%s"' % (args.public)

for i in includes:
    i.emit()

if args.export:
    print '#ifndef %s_EXPORT_INTERNAL_HEADERS' % (args.prefix,)
for i in sources:
    i.emit()
if args.export:
    print '#endif /* %s_EXPORT_INTERNAL_HEADERS */' % (args.prefix,)