build-support/iwyu.py (197 lines of code) (raw):

#!/usr/bin/env python # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. from __future__ import print_function from cStringIO import StringIO import glob import json import logging import optparse import os import re import subprocess import sys from kudu_util import get_upstream_commit, check_output, ROOT, Colors, init_logging import iwyu.fix_includes from iwyu.fix_includes import ParseAndMergeIWYUOutput _USAGE = """\ %prog [--fix] [--sort-only] [--all | --from-git | <path>...] %prog is a wrapper around include-what-you-use that passes the appropriate configuration and filters the output to ignore known issues. In addition, it can automatically pipe the output back into the IWYU-provided 'fix_includes.py' script in order to fix any reported issues. """ _MAPPINGS_DIR = os.path.join(ROOT, "build-support/iwyu/mappings/") _TOOLCHAIN_DIR = os.path.join(ROOT, "thirdparty/clang-toolchain/bin") _IWYU_TOOL = os.path.join(ROOT, "build-support/iwyu/iwyu_tool.py") # Matches source files that we should run on. _RE_SOURCE_FILE = re.compile(r'\.(c|cc|h)$') # Matches compilation errors in the output of IWYU _RE_CLANG_ERROR = re.compile(r'^.+?:\d+:\d+:\s*' r'(fatal )?error:', re.MULTILINE) # Files that we don't want to ever run IWYU on, because they aren't clean yet. _MUTED_FILES = set([ "src/kudu/cfile/cfile_reader.h", "src/kudu/cfile/cfile_writer.h", "src/kudu/client/client-internal.h", "src/kudu/client/client-test.cc", "src/kudu/common/encoded_key-test.cc", "src/kudu/common/schema.h", "src/kudu/experiments/rwlock-perf.cc", "src/kudu/rpc/reactor.cc", "src/kudu/rpc/reactor.h", "src/kudu/security/ca/cert_management.cc", "src/kudu/security/ca/cert_management.h", "src/kudu/security/cert-test.cc", "src/kudu/security/cert.cc", "src/kudu/security/cert.h", "src/kudu/security/openssl_util.cc", "src/kudu/security/openssl_util.h", "src/kudu/security/tls_context.cc", "src/kudu/security/tls_handshake.cc", "src/kudu/security/tls_socket.h", "src/kudu/security/x509_check_host.cc", "src/kudu/server/default-path-handlers.cc", "src/kudu/server/webserver.cc", "src/kudu/util/bit-util-test.cc", "src/kudu/util/group_varint-test.cc", "src/kudu/util/minidump.cc", "src/kudu/util/mt-metrics-test.cc", "src/kudu/util/process_memory.cc", "src/kudu/util/rle-test.cc" ]) # Flags to pass to iwyu/fix_includes.py for Kudu-specific style. _FIX_INCLUDES_STYLE_FLAGS = [ '--blank_lines', '--blank_line_between_c_and_cxx_includes', '--separate_project_includes=kudu/' ] # Directory containin the compilation database _BUILD_DIR = os.path.join(ROOT, 'build/latest') def _get_file_list_from_git(): upstream_commit = get_upstream_commit() out = check_output(["git", "diff", "--name-only", upstream_commit]).splitlines() return [l for l in out if _RE_SOURCE_FILE.search(l)] def _get_paths_from_compilation_db(): db_path = os.path.join(_BUILD_DIR, 'compile_commands.json') with open(db_path, 'r') as fileobj: compilation_db = json.load(fileobj) return [entry['file'] for entry in compilation_db] def _run_iwyu_tool(paths): iwyu_args = ['--max_line_length=256'] for m in glob.glob(os.path.join(_MAPPINGS_DIR, "*.imp")): iwyu_args.append("--mapping_file=%s" % os.path.abspath(m)) cmdline = [_IWYU_TOOL, '-p', _BUILD_DIR] cmdline.extend(paths) cmdline.append('--') cmdline.extend(iwyu_args) # iwyu_tool.py requires include-what-you-use on the path env = os.environ.copy() env['PATH'] = "%s:%s" % (_TOOLCHAIN_DIR, env['PATH']) def crash(output): sys.exit((Colors.RED + "Failed to run IWYU tool.\n\n" + Colors.RESET + Colors.YELLOW + "Command line:\n" + Colors.RESET + "%s\n\n" + Colors.YELLOW + "Output:\n" + Colors.RESET + "%s") % (" ".join(cmdline), output)) try: output = check_output(cmdline, env=env, stderr=subprocess.STDOUT) if '\nFATAL ERROR: ' in output or \ 'Assertion failed: ' in output or \ _RE_CLANG_ERROR.search(output): crash(output) return output except subprocess.CalledProcessError, e: crash(e.output) def _is_muted(path): assert os.path.isabs(path) rel = os.path.relpath(path, ROOT) return not rel.startswith('src/') or rel in _MUTED_FILES def _filter_paths(paths): return [p for p in paths if not _is_muted(p)] def _relativize_paths(paths): """ Make paths relative to the build directory. """ return [os.path.relpath(p, _BUILD_DIR) for p in paths] def _get_thirdparty_include_dirs(): return glob.glob(os.path.join(ROOT, "thirdparty", "installed", "*", "include")) def _get_fixer_flags(flags): args = ['--quiet', '--nosafe_headers', '--source_root=%s' % os.path.join(ROOT, 'src')] if flags.dry_run: args.append("--dry_run") for d in _get_thirdparty_include_dirs(): args.extend(['--thirdparty_include_dir', d]) args.extend(_FIX_INCLUDES_STYLE_FLAGS) fixer_flags, _ = iwyu.fix_includes.ParseArgs(args) return fixer_flags def _do_iwyu(flags, paths): iwyu_output = _run_iwyu_tool(paths) if flags.dump_iwyu_output: logging.info("Dumping iwyu output to %s", flags.dump_iwyu_output) with file(flags.dump_iwyu_output, "w") as f: print(iwyu_output, file=f) stream = StringIO(iwyu_output) fixer_flags = _get_fixer_flags(flags) # Passing None as 'fix_paths' tells the fixer script to process # all of the IWYU output, instead of just the output corresponding # to files in 'paths'. This means that if you run this script on a # .cc file, it will also report and fix errors in headers included # by that .cc file. fix_paths = None records = ParseAndMergeIWYUOutput(stream, fix_paths, fixer_flags) unfiltered_count = len(records) records = [r for r in records if not _is_muted(os.path.abspath(r.filename))] if len(records) < unfiltered_count: logging.info("Muted IWYU suggestions on %d file(s)", unfiltered_count - len(records)) return iwyu.fix_includes.FixManyFiles(records, fixer_flags) def _do_sort_only(flags, paths): fixer_flags = _get_fixer_flags(flags) iwyu.fix_includes.SortIncludesInFiles(paths, fixer_flags) def main(argv): parser = optparse.OptionParser(usage=_USAGE) for i, arg in enumerate(argv): if arg.startswith('-'): argv[i] = argv[i].replace('_', '-') parser.add_option('--all', action='store_true', help=('Process all files listed in the compilation database of the current ' 'build.')) parser.add_option('--from-git', action='store_true', help=('Determine the list of files to run IWYU automatically based on git. ' 'All files which are modified in the current working tree or in commits ' 'not yet committed upstream by gerrit are processed.')) parser.add_option('--fix', action='store_false', dest="dry_run", default=True, help=('If this is set, fixes IWYU issues in place.')) parser.add_option('-s', '--sort-only', action='store_true', help=('Just sort #includes of files listed on cmdline;' ' do not add or remove any #includes')) parser.add_option('--dump-iwyu-output', type='str', help=('A path to dump the raw IWYU output to. This can be useful for ' 'debugging this tool.')) (flags, paths) = parser.parse_args(argv[1:]) if bool(flags.from_git) + bool(flags.all) + (len(paths) > 0) != 1: sys.exit('Must specify exactly one of --all, --from-git, or a list of paths') do_filtering = True if flags.from_git: paths = _get_file_list_from_git() paths = [os.path.abspath(os.path.join(ROOT, p)) for p in paths] elif paths: paths = [os.path.abspath(p) for p in paths] # If paths are specified explicitly, don't filter them out. do_filtering = False elif flags.all: paths = _filter_paths(_get_paths_from_compilation_db()) else: assert False, "Should not reach here" if do_filtering: orig_count = len(paths) paths = _filter_paths(paths) if len(paths) != orig_count: logging.info("Filtered %d paths muted by configuration in iwyu.py", orig_count - len(paths)) else: muted_paths = [p for p in paths if _is_muted(p)] if muted_paths: logging.warning("%d selected path(s) are known to have IWYU issues:" % len(muted_paths)) for p in muted_paths: logging.warning(" %s" % p) # If we came up with an empty list (no relevant files changed in the commit) # then we should early-exit. Otherwise, we'd end up passing an empty list to # IWYU and it will run on every file. if flags.from_git and not paths: logging.info("No files selected for analysis.") sys.exit(0) # IWYU output will be relative to the compilation database which is in # the build directory. In order for the fixer script to properly find them, we need # to treat all paths relative to that directory and chdir into it first. paths = _relativize_paths(paths) os.chdir(_BUILD_DIR) # For correct results, IWYU depends on the generated header files. logging.info("Ensuring IWYU dependencies are built...") if os.path.exists('Makefile'): subprocess.check_call(['make', 'iwyu-generated-headers']) elif os.path.exists('build.ninja'): subprocess.check_call(['ninja', 'iwyu-generated-headers']) else: logging.error('No Makefile or build.ninja found in build directory %s', _BUILD_DIR) sys.exit(1) logging.info("Checking %d file(s)...", len(paths)) if flags.sort_only: return _do_sort_only(flags, paths) else: return _do_iwyu(flags, paths) if __name__ == "__main__": init_logging() sys.exit(main(sys.argv))