OLD | NEW |
| (Empty) |
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 | |
42 def Log(self, build_file_path, line_number, target_name, source_file, | |
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.""" | |
57 query += os.path.sep | |
58 length = len(query) | |
59 pattern = r'(?P<line_number>\d+)\$\s*"(?P<source_file>(?P<subpackage>' | |
60 pattern += '|'.join(package[length:].replace(os.path.sep, '/') | |
61 for package in packages if package.startswith(query)) | |
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 | |
94 logger.Log(build_file_path, line_number, target_name, source_file, | |
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()) | |
OLD | NEW |