Skip to content

Instantly share code, notes, and snippets.

@martinfinke
Created December 4, 2018 07:12
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save martinfinke/c474ac992155f29f0f0d871ddd8cd99d to your computer and use it in GitHub Desktop.
Save martinfinke/c474ac992155f29f0f0d871ddd8cd99d to your computer and use it in GitHub Desktop.
Convert a C++ library to a single-header file
#!/usr/bin/env python2
import argparse
from itertools import ifilter
import os
import re
import sys
# STL and C headers as of 04.12.2018, taken from cppreference.com
stl_headers = ['algorithm', 'any', 'array', 'assert.h', 'atomic', 'bit', 'bitset', 'cassert', 'ccomplex', 'cctype', 'cerrno', 'cfenv', 'cfloat', 'charconv', 'chrono', 'cinttypes', 'ciso646', 'climits', 'clocale', 'cmath', 'codecvt', 'compare', 'complex', 'complex.h', 'concepts', 'condition_variable', 'contract', 'csetjmp', 'csignal', 'cstdalign', 'cstdarg', 'cstdbool', 'cstddef', 'cstdint', 'cstdio', 'cstdlib', 'cstring', 'ctgmath', 'ctime', 'ctype.h', 'cuchar', 'cwchar', 'cwctype', 'deque', 'errno.h', 'exception', 'execution', 'experimental/algorithm', 'experimental/any', 'experimental/array', 'experimental/atomic', 'experimental/barrier', 'experimental/chrono', 'experimental/deque', 'experimental/exception_list', 'experimental/execution_policy', 'experimental/filesystem', 'experimental/forward_list', 'experimental/functional', 'experimental/future', 'experimental/iterator', 'experimental/latch', 'experimental/list', 'experimental/map', 'experimental/memory', 'experimental/memory_resource', 'experimental/numeric', 'experimental/optional', 'experimental/propagate_const', 'experimental/random', 'experimental/ranges/algorithm', 'experimental/ranges/concepts', 'experimental/ranges/functional', 'experimental/ranges/iterator', 'experimental/ranges/random', 'experimental/ranges/range', 'experimental/ranges/tuple', 'experimental/ranges/type_traits', 'experimental/ranges/utility', 'experimental/ratio', 'experimental/regex', 'experimental/set', 'experimental/source_location', 'experimental/string', 'experimental/string_view', 'experimental/system_error', 'experimental/tuple', 'experimental/type_traits', 'experimental/unordered_map', 'experimental/unordered_set', 'experimental/utility', 'experimental/vector', 'fenv.h', 'filesystem', 'float.h', 'forward_list', 'fstream', 'functional', 'future', 'initializer_list', 'inttypes.h', 'iomanip', 'ios', 'iosfwd', 'iostream', 'iso646.h', 'istream', 'iterator', 'limits', 'limits.h', 'list', 'locale', 'locale.h', 'map', 'math.h', 'memory', 'memory_resource', 'mutex', 'new', 'numeric', 'optional', 'ostream', 'queue', 'random', 'ratio', 'regex', 'scoped_allocator', 'set', 'setjmp.h', 'shared_mutex', 'signal.h', 'span', 'sstream', 'stack', 'stdalign.h', 'stdarg.h', 'stdatomic.h', 'stdbool.h', 'stddef.h', 'stdexcept', 'stdint.h', 'stdio.h', 'stdlib.h', 'stdnoreturn.h', 'streambuf', 'string', 'string.h', 'string_view', 'strstream', 'syncstream', 'system_error', 'tgmath.h', 'thread', 'threads.h', 'time.h', 'tuple', 'type_traits', 'typeindex', 'typeinfo', 'uchar.h', 'unordered_map', 'unordered_set', 'utility', 'valarray', 'variant', 'vector', 'version', 'wchar.h', 'wctype.h']
class IncludeInliner:
def __init__(self, header_search_paths, ignored_headers):
self.header_search_paths = header_search_paths
self.ignored_headers = ignored_headers
self.included_stl_headers = []
self.included_headers = []
self.include_regex = re.compile('^\s*#(?:include|import)\s+["<]([^">]+)[">]')
self.pragma_once_regex = re.compile('^\s*#pragma\s+once')
self.has_pragma_once = False
self.output_lines = []
def run(self, full_header_path):
if not full_header_path:
raise Exception("Header not found: " + header_file_path)
with open(full_header_path,'r') as header:
for lineIndex, line in enumerate(header):
lineInfo = full_header_path + ':' + str(lineIndex + 1) + ': '
# Check if line is a #pragma once
result = self.pragma_once_regex.match(line)
if result:
if self.has_pragma_once:
print(lineInfo + 'Removing unneeded #pragma once')
continue
self.output_lines.append(line)
self.has_pragma_once = True
continue
# Check if line is an #include
result = self.include_regex.match(line)
if not result:
self.output_lines.append(line)
continue
path = result.group(1)
# Remove ignored headers
if path in self.ignored_headers:
print(lineInfo + 'Removing ignored #include: ' + path)
continue
# Don't touch STL headers
if path in stl_headers:
if not path in self.included_stl_headers:
print(lineInfo + 'Keeping STL #include: ' + path)
self.output_lines.append(line)
self.included_stl_headers.append(path)
continue
full_include_path = self.get_full_header_path(IncludeInliner._get_parent_folder(full_header_path), path)
if not full_include_path in self.included_headers:
print(lineInfo + 'Inlining #include: ' + path)
self.run(full_include_path)
self.included_headers.append(full_include_path)
def get_output(self):
return ''.join(self.output_lines)
def get_full_header_path(self, cwd, header_file_path):
def is_valid(header_search_path):
full_path = os.path.join(header_search_path, header_file_path)
if os.path.isfile(full_path):
return full_path
relative_to_cwd = is_valid(cwd)
if relative_to_cwd:
return relative_to_cwd
return next(ifilter(lambda x: x, map(is_valid, self.header_search_paths)), None)
@staticmethod
def _get_parent_folder(path):
return os.path.abspath(os.path.join(path, os.pardir))
def main():
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('main_header', help='The main header file', type=argparse.FileType('r'))
parser.add_argument('--ignored_headers', help='A list of header files whose #include should be removed (without inlining them)', nargs='*')
parser.add_argument('--header_search_paths', help='A list of header search paths', nargs='*')
parser.add_argument('-o', '--output_file', help='The single-header output file', required=True, type=argparse.FileType('w'))
args = parser.parse_args()
inliner = IncludeInliner(args.header_search_paths, args.ignored_headers)
inliner.run(inliner.get_full_header_path(os.getcwd(), args.main_header.name))
args.output_file.write(inliner.get_output())
if __name__ == "__main__":
main()
@martinfinke
Copy link
Author

martinfinke commented Dec 4, 2018

Features:

  • Given a C++ file that #includes other files, inlines the #includes to create a single header file that can be used without other dependencies.
  • Doesn't touch STL and C standard library #includes
  • Includes every header only once (even STL and C standard library ones)
  • Doesn't add more than one #pragma once to the output single header file

Advantage: The user doesn't have to set up any header search paths in their project, so that your #includes can be resolved. The user just has to #include the one single-header file.

Usage:

create_single_header.py path/to/main_header.h --header_search_paths some/header_search/path another_header/search_path --ignored_headers ignore_this.hpp also/ignore_this.h -o single_header_output.h

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment