226 lines
7.1 KiB
Python
Executable File
226 lines
7.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright 2018 The Chromium Authors
|
|
# 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()
|