generate-header-include-checks.py 4.76 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#!/usr/bin/env python
# vim:fenc=utf-8:shiftwidth=2

# Copyright 2018 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Check that each header can be included in isolation.

For each header we generate one .cc file which only includes this one header.
All these .cc files are then added to a sources.gni file which is included in
BUILD.gn. Just compile to check whether there are any violations to the rule
that each header must be includable in isolation.
"""

16 17 18
# for py2/py3 compatibility
from __future__ import print_function

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
import argparse
import os
import os.path
import re
import sys

# TODO(clemensh): Extend to tests.
DEFAULT_INPUT = ['base', 'src']
DEFAULT_GN_FILE = 'BUILD.gn'
MY_DIR = os.path.dirname(os.path.realpath(__file__))
V8_DIR = os.path.dirname(MY_DIR)
OUT_DIR = os.path.join(V8_DIR, 'check-header-includes')
AUTO_EXCLUDE = [
  # flag-definitions.h needs a mode set for being included.
  'src/flag-definitions.h',
]
AUTO_EXCLUDE_PATTERNS = [
  'src/base/atomicops_internals_.*',
] + [
  # platform-specific headers
  '\\b{}\\b'.format(p) for p in
40 41
    ('win', 'win32', 'ia32', 'x64', 'arm', 'arm64', 'mips', 'mips64', 's390',
     'ppc')]
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

args = None
def parse_args():
  global args
  parser = argparse.ArgumentParser()
  parser.add_argument('-i', '--input', type=str, action='append',
                      help='Headers or directories to check (directories '
                           'are scanned for headers recursively); default: ' +
                           ','.join(DEFAULT_INPUT))
  parser.add_argument('-x', '--exclude', type=str, action='append',
                      help='Add an exclude pattern (regex)')
  parser.add_argument('-v', '--verbose', action='store_true',
                      help='Be verbose')
  args = parser.parse_args()
  args.exclude = (args.exclude or []) + AUTO_EXCLUDE_PATTERNS
  args.exclude += ['^' + re.escape(x) + '$' for x in AUTO_EXCLUDE]
  if not args.input:
    args.input=DEFAULT_INPUT


def printv(line):
  if args.verbose:
64
    print(line)
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


def find_all_headers():
  printv('Searching for headers...')
  header_files = []
  exclude_patterns = [re.compile(x) for x in args.exclude]
  def add_recursively(filename):
    full_name = os.path.join(V8_DIR, filename)
    if not os.path.exists(full_name):
      sys.exit('File does not exist: {}'.format(full_name))
    if os.path.isdir(full_name):
      for subfile in os.listdir(full_name):
        full_name = os.path.join(filename, subfile)
        printv('Scanning {}'.format(full_name))
        add_recursively(full_name)
    elif filename.endswith('.h'):
      printv('--> Found header file {}'.format(filename))
      for p in exclude_patterns:
        if p.search(filename):
          printv('--> EXCLUDED (matches {})'.format(p.pattern))
          return
      header_files.append(filename)

  for filename in args.input:
    add_recursively(filename)

  return header_files


def get_cc_file_name(header):
  split = os.path.split(header)
  header_dir = os.path.relpath(split[0], V8_DIR)
  # Prefix with the directory name, to avoid collisions in the object files.
  prefix = header_dir.replace(os.path.sep, '-')
  cc_file_name = 'test-include-' + prefix + '-' + split[1][:-1] + 'cc'
  return os.path.join(OUT_DIR, cc_file_name)


def create_including_cc_files(header_files):
104
  comment = 'check including this header in isolation'
105 106
  for header in header_files:
    cc_file_name = get_cc_file_name(header)
107 108 109 110 111 112 113 114
    rel_cc_file_name = os.path.relpath(cc_file_name, V8_DIR)
    content = '#include "{}"  // {}\n'.format(header, comment)
    if os.path.exists(cc_file_name):
      with open(cc_file_name) as cc_file:
        if cc_file.read() == content:
          printv('File {} is up to date'.format(rel_cc_file_name))
          continue
    printv('Creating file {}'.format(rel_cc_file_name))
115
    with open(cc_file_name, 'w') as cc_file:
116
      cc_file.write(content)
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


def generate_gni(header_files):
  gni_file = os.path.join(OUT_DIR, 'sources.gni')
  printv('Generating file "{}"'.format(os.path.relpath(gni_file, V8_DIR)))
  with open(gni_file, 'w') as gn:
    gn.write("""\
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# This list is filled automatically by tools/check_header_includes.py.
check_header_includes_sources = [
""");
    for header in header_files:
      cc_file_name = get_cc_file_name(header)
      gn.write('    "{}",\n'.format(os.path.relpath(cc_file_name, V8_DIR)))
    gn.write(']\n')


def main():
  parse_args()
  header_files = find_all_headers()
  if not os.path.exists(OUT_DIR):
    os.mkdir(OUT_DIR)
  create_including_cc_files(header_files)
  generate_gni(header_files)

if __name__ == '__main__':
  main()