#!/usr/bin/env python """ A script for finding and [copying|printing] C++ headers that get recursively imported from one (or more) starting points. Supports absolute imports relative to some root folder (project root) and relative imports relative to the header that is doing the importing. Does not support conditional imports (resulting from #ifdef macros and such). All the #import statements found in a header (one #import per line) are traversed. Supports Python2 and Python3. """ __author__ = "Florijan Stamenkovic" __copyright__ = "Copyright 2017, Memgraph" import logging import sys import os import re import shutil from argparse import ArgumentParser # the prefix of an include directive PREFIX = "#include" log = logging.getLogger(__name__) def parse_args(): argp = ArgumentParser(description=__doc__) argp.add_argument("--logging", default="INFO", choices=["INFO", "DEBUG"], help="Logging level") argp.add_argument("--roots", required=True, nargs="+", help="One or more paths in which headers are sought") argp.add_argument("--start", required=True, nargs="+", help="One or more headers from which to start scanning") argp.add_argument("--stdout", action="store_true", help="If found paths should be printed out to stdout") argp.add_argument("--copy", default=None, help="Prefix of the path where the headers should be copied") return argp.parse_args() def main(): args = parse_args() logging.basicConfig(level=args.logging) log.info("Recursively detecting used C/C++ headers in roots '%s' with starting point(s) '%s'", args.roots, args.start) args.roots = [os.path.abspath(p) for p in args.roots] results = set() for start in args.start: find_recursive(start, args.roots, results) results = list(sorted(results)) log.debug("Found %d paths:", len(results)) for r in results: log.debug("\t%s", r) # print out the results if required if args.stdout: for result in results: print(result) # copy the results if required if args.copy is not None: for root, path in results: from_path = os.path.join(root, path) to_path = os.path.join(args.copy, path) log.debug("Copying '%s' to '%s'", from_path, to_path) # create a directory if necessary, Py2 and Py3 compatible to_dir = os.path.dirname(to_path) if not os.path.exists(to_dir): os.makedirs(to_dir) shutil.copy(from_path, to_path) def abs_to_relative(roots, path): """ Args: roots: list of str, a list of possible prefixes to the 'path'. path: str, a path to a file. Return: A tuple (relative_path, root) where 'root' is one of the given 'roots' and where os.path.join(root, relative_path) equals the given 'path' Raise: An exception if none of the 'roots' is a prefix of 'path' """ for root in roots: if path.startswith(root): return (root, path[len(root) + 1:]) raise Exception("Failed to find prefix of '%s'in '%r'" % ( path, roots)) def find_recursive(path, roots, results): """ Recursivelly looks for headers and adds them to results. Results are added as tuples of form (root, header_path) where 'root' is one of the given roots: the one in which the header was found, and 'header_path' is the found header's path relative to 'root'. Args: path: str of tuple. If str, it's considered a path that has one of the roots as prefix. If tuple it's considered a (prefix, suffix) that defines a path. In both forms the path is to a header. This header is added to results and scanned for #include statements of other headers. For each #include (relative to current `path` or to `project_root`) for which a file is found this same function is called. roots: list of str, List of folders in which headers are sought. Must be absolute paths. results: a collection into which the results are added. The collection contains tuples of form (root, path), see function description. """ log.debug("Processing path: %s", path) if isinstance(path, str): path = os.path.abspath(path) path = abs_to_relative(roots, path) # from this point onward 'path' is a tuple (root, suffix) if path in results: log.debug("Skipping already present path '%r'", path) return log.debug("Adding path '%r'", path) results.add(path) # go through files and look for include directives with open(os.path.join(*path)) as f: for line in filter(lambda l: l.startswith(PREFIX), map(lambda l: l.strip(), f)): include = line[len(PREFIX):].strip() include = re.sub("[\"\'\>\<]", "", include) log.debug("Processing include '%s'", include) # search for the include relatively to the current header include_rel = os.path.join( os.path.dirname(os.path.join(*path)), include) if os.path.exists(include_rel) and os.path.isfile(include_rel): find_recursive(include_rel, roots, results) continue # search for file in roots for root in roots: include_abs = os.path.join(root, include) if os.path.exists(include_abs) and os.path.isfile(include_abs): find_recursive((root, include), roots, results) continue if __name__ == '__main__': main()