amalgam 4.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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
#!/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')
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):
    def __init__(self, name):
        self.name = name
        self.buf = StringIO()
        emit_file(self.buf, self.name)

    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(',')]

def should_ignore(name):
    return (name in already_included
            or not os.path.exists(resolve(name))
            or name in ignore_files)

def resolve(path):
    if os.path.isabs(path) or os.path.exists(path):
        p = path
    else:
        p = os.path.join(args.include_path, path)
    if os.path.exists(p):
        p = os.path.realpath(p).replace('%s/' % os.getcwd(), '')
#    print >>sys.stderr, '%s -> %s' % (path, p)
    return p

def emit_line_directive(out, name):
    print >>out, '''#ifdef %(prefix)s_MODULE_LINES
#line 1 "%(name)s"
#endif''' % dict(
    prefix = args.prefix,
    name = resolve(name),
)

def emit_body(out, name):
    path = resolve(name)
    if not os.path.exists(path):
        print >>out, '#include "%s"' % (name,)
        return

    with open(resolve(name)) as f:
        for l in f:
            match = re.match('(#include "(.*)")', l)
            if match:
                all, path = match.groups()
                if args.autoinc:
                    if not should_ignore(path):
                        already_included.add(path)
                        includes.append(File(path))
                print >>out, '/* Amalgamated: %s */' % (all,)
            else:
                print >>out, l,


def emit_file(out, name):
    emit_line_directive(out, name)
    emit_body(out, name)

for i in args.sources:
    sources.append(File(i))

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