blob: ec1521b56a9825aa3ff740e80cf15e2672999f06 [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2017 The WebRTC 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 in the root of the source
# tree. An additional intellectual property rights grant can be found
# in the file PATENTS. All contributing project authors may
# be found in the AUTHORS file in the root of the source tree.
import argparse
import logging
import os
import re
import sys
DISPLAY_LEVEL = 1
IGNORE_LEVEL = 0
# TARGET_RE matches a GN target, and extracts the target name and the contents.
TARGET_RE = re.compile(r'\d+\$(?P<indent>\s*)\w+\("(?P<target_name>\w+)"\) {'
r'(?P<target_contents>.*?)'
r'\d+\$(?P=indent)}',
re.MULTILINE | re.DOTALL)
# SOURCES_RE matches a block of sources inside a GN target.
SOURCES_RE = re.compile(r'sources \+?= \[(?P<sources>.*?)\]',
re.MULTILINE | re.DOTALL)
LOG_FORMAT = '%(message)s'
ERROR_MESSAGE = ("{}:{} in target '{}':\n"
" Source file '{}'\n"
" crosses boundary of package '{}'.\n")
class Logger(object):
def __init__(self, messages_left=None):
self.log_level = DISPLAY_LEVEL
self.messages_left = messages_left
def Log(self, build_file_path, line_number, target_name, source_file,
subpackage):
if self.messages_left is not None:
if not self.messages_left:
self.log_level = IGNORE_LEVEL
else:
self.messages_left -= 1
message = ERROR_MESSAGE.format(build_file_path, line_number, target_name,
source_file, subpackage)
logging.log(self.log_level, message)
def _BuildSubpackagesPattern(packages, query):
"""Returns a regular expression that matches source files inside subpackages
of the given query."""
query += os.path.sep
length = len(query)
pattern = r'(?P<line_number>\d+)\$\s*"(?P<source_file>(?P<subpackage>'
pattern += '|'.join(package[length:].replace(os.path.sep, '/')
for package in packages if package.startswith(query))
pattern += r')/[\w\./]*)"'
return re.compile(pattern)
def _ReadFileAndPrependLines(file_path):
"""Reads the contents of a file and prepends the line number to every line."""
with open(file_path) as f:
return "".join("{}${}".format(line_number, line)
for line_number, line in enumerate(f, 1))
def _CheckBuildFile(build_file_path, packages, logger):
"""Iterates oven all the targets of the given BUILD.gn file, and verifies that
the source files referenced by it don't belong to any of it's subpackages.
Returns True if a package boundary violation was found.
"""
found_violations = False
package = os.path.dirname(build_file_path)
subpackages_re = _BuildSubpackagesPattern(packages, package)
build_file_contents = _ReadFileAndPrependLines(build_file_path)
for target_match in TARGET_RE.finditer(build_file_contents):
target_name = target_match.group('target_name')
target_contents = target_match.group('target_contents')
for sources_match in SOURCES_RE.finditer(target_contents):
sources = sources_match.group('sources')
for subpackages_match in subpackages_re.finditer(sources):
subpackage = subpackages_match.group('subpackage')
source_file = subpackages_match.group('source_file')
line_number = subpackages_match.group('line_number')
if subpackage:
found_violations = True
logger.Log(build_file_path, line_number, target_name, source_file,
subpackage)
return found_violations
def CheckPackageBoundaries(root_dir, logger, build_files=None):
packages = [root for root, _, files in os.walk(root_dir)
if 'BUILD.gn' in files]
default_build_files = [os.path.join(package, 'BUILD.gn')
for package in packages]
build_files = build_files or default_build_files
return any([_CheckBuildFile(build_file_path, packages, logger)
for build_file_path in build_files])
def main():
parser = argparse.ArgumentParser(
description='Script that checks package boundary violations in GN '
'build files.')
parser.add_argument('root_dir', metavar='ROOT_DIR',
help='The root directory that contains all BUILD.gn '
'files to be processed.')
parser.add_argument('build_files', metavar='BUILD_FILE', nargs='*',
help='A list of BUILD.gn files to be processed. If no '
'files are given, all BUILD.gn files under ROOT_DIR '
'will be processed.')
parser.add_argument('--max_messages', type=int, default=None,
help='If set, the maximum number of violations to be '
'displayed.')
args = parser.parse_args()
logging.basicConfig(format=LOG_FORMAT)
logging.getLogger().setLevel(DISPLAY_LEVEL)
logger = Logger(args.max_messages)
return CheckPackageBoundaries(args.root_dir, logger, args.build_files)
if __name__ == '__main__':
sys.exit(main())