| #!/usr/bin/env python2.7 |
| # |
| # 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_SEARCH_STRING = 'third_party' + os.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 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 |
| |
| match_index_with_library = source.find( |
| os.sep, match_index + len(THIRD_PARTY_SEARCH_STRING)) |
| if match_index_with_library == -1: |
| # Unable to determine library name, skip. |
| return None |
| |
| owners_file_path = os.path.join(source[:match_index_with_library], |
| OWNERS_FILENAME) |
| if not os.path.exists(owners_file_path): |
| return None |
| |
| return open(owners_file_path).read() |
| |
| |
| 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 |
| |
| |
| 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]) |
| 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 output.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() |