blob: a7ad67ca06c27a14af0313d50a35a244b6d0405e [file] [log] [blame]
#!/usr/bin/env python3
#
# 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.
"""Generate owners (.owners file) by looking at commit author for
libfuzzer test.
Invoked by GN from fuzzer_test.gni.
"""
import argparse
import os
import re
import subprocess
import sys
AUTHOR_REGEX = re.compile('author-mail <(.+)>')
CHROMIUM_SRC_DIR = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OWNERS_FILENAME = 'OWNERS'
THIRD_PARTY = 'third_party'
THIRD_PARTY_SEARCH_STRING = THIRD_PARTY + os.path.sep
def GetAuthorFromGitBlame(blame_output):
"""Return author from git blame output."""
for line in blame_output.decode('utf-8').splitlines():
m = AUTHOR_REGEX.match(line)
if m:
return m.group(1)
return None
def GetGitCommand():
"""Returns a git command that does not need to be executed using shell=True.
On non-Windows platforms: 'git'. On Windows: 'git.bat'.
"""
return 'git.bat' if sys.platform == 'win32' else 'git'
def GetOwnersIfThirdParty(source):
"""Return owners using the closest OWNERS file if in third_party."""
match_index = source.find(THIRD_PARTY_SEARCH_STRING)
if match_index == -1:
# Not in third_party, skip.
return None
path_prefix = source[:match_index + len(THIRD_PARTY_SEARCH_STRING)]
path_after_third_party = source[len(path_prefix):].split(os.path.sep)
# Test all the paths after third_party/<libname>, making sure that we don't
# test third_party/OWNERS itself, otherwise we'd default to CCing them for
# all fuzzer issues without OWNERS, which wouldn't be nice.
while path_after_third_party:
owners_file_path = path_prefix + \
os.path.join(*(path_after_third_party + [OWNERS_FILENAME]))
if os.path.exists(owners_file_path):
return open(owners_file_path).read()
path_after_third_party.pop()
return None
# pylint: disable=inconsistent-return-statements
def GetOwnersForFuzzer(sources):
"""Return owners given a list of sources as input."""
if not sources:
return
for source in sources:
full_source_path = os.path.join(CHROMIUM_SRC_DIR, source)
if not os.path.exists(full_source_path):
continue
with open(full_source_path, 'r') as source_file_handle:
source_content = source_file_handle.read()
if SubStringExistsIn(
['FuzzOneInput', 'LLVMFuzzerTestOneInput', 'PROTO_FUZZER'],
source_content):
# Found the fuzzer source (and not dependency of fuzzer).
git_dir = os.path.join(CHROMIUM_SRC_DIR, '.git')
git_command = GetGitCommand()
is_git_file = bool(subprocess.check_output(
[git_command, '--git-dir', git_dir, 'ls-files', source],
cwd=CHROMIUM_SRC_DIR))
if not is_git_file:
# File is not in working tree. Return owners for third_party.
return GetOwnersIfThirdParty(full_source_path)
# git log --follow and --reverse don't work together and using just
# --follow is too slow. Make a best estimate with an assumption that
# the original author has authored line 1 which is usually the
# copyright line and does not change even with file rename / move.
blame_output = subprocess.check_output(
[git_command, '--git-dir', git_dir, 'blame', '--porcelain', '-L1,1',
source], cwd=CHROMIUM_SRC_DIR)
return GetAuthorFromGitBlame(blame_output)
return None
# pylint: enable=inconsistent-return-statements
def FindGroupsAndDepsInDeps(deps_list, build_dir):
"""Return list of groups, as well as their deps, from a list of deps."""
groups = []
deps_for_groups = {}
for deps in deps_list:
output = subprocess.check_output(
[GNPath(), 'desc', '--fail-on-unused-args', build_dir, deps]).decode(
'utf8')
needle = 'Type: '
for line in output.splitlines():
if needle and not line.startswith(needle):
continue
if needle == 'Type: ':
if line != 'Type: group':
break
groups.append(deps)
assert deps not in deps_for_groups
deps_for_groups[deps] = []
needle = 'Direct dependencies'
elif needle == 'Direct dependencies':
needle = ''
else:
assert needle == ''
if needle == line:
break
deps_for_groups[deps].append(line.strip())
return groups, deps_for_groups
def TraverseGroups(deps_list, build_dir):
"""Filter out groups from a deps list. Add groups' direct dependencies."""
full_deps_set = set(deps_list)
deps_to_check = full_deps_set.copy()
# Keep track of groups to break circular dependendies, if any.
seen_groups = set()
while deps_to_check:
# Look for groups from the deps set.
groups, deps_for_groups = FindGroupsAndDepsInDeps(deps_to_check, build_dir)
groups = set(groups).difference(seen_groups)
if not groups:
break
# Update sets. Filter out groups from the full deps set.
full_deps_set.difference_update(groups)
deps_to_check.clear()
seen_groups.update(groups)
# Get the direct dependencies, and filter out known groups there too.
for group in groups:
deps_to_check.update(deps_for_groups[group])
deps_to_check.difference_update(seen_groups)
full_deps_set.update(deps_to_check)
return list(full_deps_set)
def GetSourcesFromDeps(deps_list, build_dir):
"""Return list of sources from parsing deps."""
if not deps_list:
return None
full_deps_list = TraverseGroups(deps_list, build_dir)
all_sources = []
for deps in full_deps_list:
output = subprocess.check_output(
[GNPath(), 'desc', '--fail-on-unused-args', build_dir, deps, 'sources'])
for source in bytes(output).decode('utf8').splitlines():
if source.startswith('//'):
source = source[2:]
all_sources.append(source)
return all_sources
def GNPath():
if sys.platform.startswith('linux'):
subdir, exe = 'linux64', 'gn'
elif sys.platform == 'darwin':
subdir, exe = 'mac', 'gn'
else:
subdir, exe = 'win', 'gn.exe'
return os.path.join(CHROMIUM_SRC_DIR, 'buildtools', subdir, exe)
def SubStringExistsIn(substring_list, string):
"""Return true if one of the substring in the list is found in |string|."""
return any(substring in string for substring in substring_list)
def main():
parser = argparse.ArgumentParser(description='Generate fuzzer owners file.')
parser.add_argument('--owners', required=True)
parser.add_argument('--build-dir')
parser.add_argument('--deps', nargs='+')
parser.add_argument('--sources', nargs='+')
args = parser.parse_args()
# Generate owners file.
with open(args.owners, 'w') as owners_file:
# If we found an owner, then write it to file.
# Otherwise, leave empty file to keep ninja happy.
owners = GetOwnersForFuzzer(args.sources)
if owners:
owners_file.write(owners)
return
# Could not determine owners from |args.sources|.
# So, try parsing sources from |args.deps|.
deps_sources = GetSourcesFromDeps(args.deps, args.build_dir)
owners = GetOwnersForFuzzer(deps_sources)
if owners:
owners_file.write(owners)
if __name__ == '__main__':
main()