ehmaldonado | 4fb9746 | 2017-01-30 05:27:22 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. |
| 4 | # |
| 5 | # Use of this source code is governed by a BSD-style license |
| 6 | # that can be found in the LICENSE file in the root of the source |
| 7 | # tree. An additional intellectual property rights grant can be found |
| 8 | # in the file PATENTS. All contributing project authors may |
| 9 | # be found in the AUTHORS file in the root of the source tree. |
| 10 | |
| 11 | import argparse |
| 12 | import logging |
| 13 | import os |
| 14 | import re |
| 15 | import sys |
| 16 | |
| 17 | |
| 18 | DISPLAY_LEVEL = 1 |
| 19 | IGNORE_LEVEL = 0 |
| 20 | |
| 21 | # TARGET_RE matches a GN target, and extracts the target name and the contents. |
| 22 | TARGET_RE = re.compile(r'\d+\$(?P<indent>\s*)\w+\("(?P<target_name>\w+)"\) {' |
| 23 | r'(?P<target_contents>.*?)' |
| 24 | r'\d+\$(?P=indent)}', |
| 25 | re.MULTILINE | re.DOTALL) |
| 26 | |
| 27 | # SOURCES_RE matches a block of sources inside a GN target. |
| 28 | SOURCES_RE = re.compile(r'sources \+?= \[(?P<sources>.*?)\]', |
| 29 | re.MULTILINE | re.DOTALL) |
| 30 | |
| 31 | LOG_FORMAT = '%(message)s' |
| 32 | ERROR_MESSAGE = ("{}:{} in target '{}':\n" |
| 33 | " Source file '{}'\n" |
| 34 | " crosses boundary of package '{}'.\n") |
| 35 | |
| 36 | |
| 37 | class Logger(object): |
| 38 | def __init__(self, messages_left=None): |
| 39 | self.log_level = DISPLAY_LEVEL |
| 40 | self.messages_left = messages_left |
| 41 | |
kjellander | 94f4d9e | 2017-03-09 06:09:33 -0800 | [diff] [blame^] | 42 | def log(self, build_file_path, line_number, target_name, source_file, |
ehmaldonado | 4fb9746 | 2017-01-30 05:27:22 -0800 | [diff] [blame] | 43 | subpackage): |
| 44 | if self.messages_left is not None: |
| 45 | if not self.messages_left: |
| 46 | self.log_level = IGNORE_LEVEL |
| 47 | else: |
| 48 | self.messages_left -= 1 |
| 49 | message = ERROR_MESSAGE.format(build_file_path, line_number, target_name, |
| 50 | source_file, subpackage) |
| 51 | logging.log(self.log_level, message) |
| 52 | |
| 53 | |
| 54 | def _BuildSubpackagesPattern(packages, query): |
| 55 | """Returns a regular expression that matches source files inside subpackages |
| 56 | of the given query.""" |
ehmaldonado | f6ddbe7 | 2017-02-13 05:18:02 -0800 | [diff] [blame] | 57 | query += os.path.sep |
ehmaldonado | 4fb9746 | 2017-01-30 05:27:22 -0800 | [diff] [blame] | 58 | length = len(query) |
| 59 | pattern = r'(?P<line_number>\d+)\$\s*"(?P<source_file>(?P<subpackage>' |
ehmaldonado | f6ddbe7 | 2017-02-13 05:18:02 -0800 | [diff] [blame] | 60 | pattern += '|'.join(package[length:].replace(os.path.sep, '/') |
| 61 | for package in packages if package.startswith(query)) |
ehmaldonado | 4fb9746 | 2017-01-30 05:27:22 -0800 | [diff] [blame] | 62 | pattern += r')/[\w\./]*)"' |
| 63 | return re.compile(pattern) |
| 64 | |
| 65 | |
| 66 | def _ReadFileAndPrependLines(file_path): |
| 67 | """Reads the contents of a file and prepends the line number to every line.""" |
| 68 | with open(file_path) as f: |
| 69 | return "".join("{}${}".format(line_number, line) |
| 70 | for line_number, line in enumerate(f, 1)) |
| 71 | |
| 72 | |
| 73 | def _CheckBuildFile(build_file_path, packages, logger): |
| 74 | """Iterates oven all the targets of the given BUILD.gn file, and verifies that |
| 75 | the source files referenced by it don't belong to any of it's subpackages. |
| 76 | Returns True if a package boundary violation was found. |
| 77 | """ |
| 78 | found_violations = False |
| 79 | package = os.path.dirname(build_file_path) |
| 80 | subpackages_re = _BuildSubpackagesPattern(packages, package) |
| 81 | |
| 82 | build_file_contents = _ReadFileAndPrependLines(build_file_path) |
| 83 | for target_match in TARGET_RE.finditer(build_file_contents): |
| 84 | target_name = target_match.group('target_name') |
| 85 | target_contents = target_match.group('target_contents') |
| 86 | for sources_match in SOURCES_RE.finditer(target_contents): |
| 87 | sources = sources_match.group('sources') |
| 88 | for subpackages_match in subpackages_re.finditer(sources): |
| 89 | subpackage = subpackages_match.group('subpackage') |
| 90 | source_file = subpackages_match.group('source_file') |
| 91 | line_number = subpackages_match.group('line_number') |
| 92 | if subpackage: |
| 93 | found_violations = True |
kjellander | 94f4d9e | 2017-03-09 06:09:33 -0800 | [diff] [blame^] | 94 | logger.log(build_file_path, line_number, target_name, source_file, |
ehmaldonado | 4fb9746 | 2017-01-30 05:27:22 -0800 | [diff] [blame] | 95 | subpackage) |
| 96 | |
| 97 | return found_violations |
| 98 | |
| 99 | |
| 100 | def CheckPackageBoundaries(root_dir, logger, build_files=None): |
| 101 | packages = [root for root, _, files in os.walk(root_dir) |
| 102 | if 'BUILD.gn' in files] |
| 103 | default_build_files = [os.path.join(package, 'BUILD.gn') |
| 104 | for package in packages] |
| 105 | |
| 106 | build_files = build_files or default_build_files |
| 107 | return any([_CheckBuildFile(build_file_path, packages, logger) |
| 108 | for build_file_path in build_files]) |
| 109 | |
| 110 | |
| 111 | def main(): |
| 112 | parser = argparse.ArgumentParser( |
| 113 | description='Script that checks package boundary violations in GN ' |
| 114 | 'build files.') |
| 115 | |
| 116 | parser.add_argument('root_dir', metavar='ROOT_DIR', |
| 117 | help='The root directory that contains all BUILD.gn ' |
| 118 | 'files to be processed.') |
| 119 | parser.add_argument('build_files', metavar='BUILD_FILE', nargs='*', |
| 120 | help='A list of BUILD.gn files to be processed. If no ' |
| 121 | 'files are given, all BUILD.gn files under ROOT_DIR ' |
| 122 | 'will be processed.') |
| 123 | parser.add_argument('--max_messages', type=int, default=None, |
| 124 | help='If set, the maximum number of violations to be ' |
| 125 | 'displayed.') |
| 126 | |
| 127 | args = parser.parse_args() |
| 128 | |
| 129 | logging.basicConfig(format=LOG_FORMAT) |
| 130 | logging.getLogger().setLevel(DISPLAY_LEVEL) |
| 131 | logger = Logger(args.max_messages) |
| 132 | |
| 133 | return CheckPackageBoundaries(args.root_dir, logger, args.build_files) |
| 134 | |
| 135 | |
| 136 | if __name__ == '__main__': |
| 137 | sys.exit(main()) |