1#!/usr/bin/env python2.7 2# 3# Copyright 2018 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6"""Generate owners (.owners file) by looking at commit author for 7libfuzzer test. 8 9Invoked by GN from fuzzer_test.gni. 10""" 11 12import argparse 13import os 14import re 15import subprocess 16import sys 17 18AUTHOR_REGEX = re.compile('author-mail <(.+)>') 19CHROMIUM_SRC_DIR = os.path.dirname( 20 os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 21OWNERS_FILENAME = 'OWNERS' 22THIRD_PARTY_SEARCH_STRING = 'third_party' + os.sep 23 24 25def GetAuthorFromGitBlame(blame_output): 26 """Return author from git blame output.""" 27 for line in blame_output.splitlines(): 28 m = AUTHOR_REGEX.match(line) 29 if m: 30 return m.group(1) 31 32 return None 33 34 35def GetGitCommand(): 36 """Returns a git command that does not need to be executed using shell=True. 37 On non-Windows platforms: 'git'. On Windows: 'git.bat'. 38 """ 39 return 'git.bat' if sys.platform == 'win32' else 'git' 40 41 42def GetOwnersIfThirdParty(source): 43 """Return owners using OWNERS file if in third_party.""" 44 match_index = source.find(THIRD_PARTY_SEARCH_STRING) 45 if match_index == -1: 46 # Not in third_party, skip. 47 return None 48 49 match_index_with_library = source.find( 50 os.sep, match_index + len(THIRD_PARTY_SEARCH_STRING)) 51 if match_index_with_library == -1: 52 # Unable to determine library name, skip. 53 return None 54 55 owners_file_path = os.path.join(source[:match_index_with_library], 56 OWNERS_FILENAME) 57 if not os.path.exists(owners_file_path): 58 return None 59 60 return open(owners_file_path).read() 61 62 63def GetOwnersForFuzzer(sources): 64 """Return owners given a list of sources as input.""" 65 if not sources: 66 return 67 68 for source in sources: 69 full_source_path = os.path.join(CHROMIUM_SRC_DIR, source) 70 if not os.path.exists(full_source_path): 71 continue 72 73 with open(full_source_path, 'r') as source_file_handle: 74 source_content = source_file_handle.read() 75 76 if SubStringExistsIn( 77 ['FuzzOneInput', 'LLVMFuzzerTestOneInput', 'PROTO_FUZZER'], 78 source_content): 79 # Found the fuzzer source (and not dependency of fuzzer). 80 81 git_dir = os.path.join(CHROMIUM_SRC_DIR, '.git') 82 git_command = GetGitCommand() 83 is_git_file = bool(subprocess.check_output( 84 [git_command, '--git-dir', git_dir, 'ls-files', source], 85 cwd=CHROMIUM_SRC_DIR)) 86 if not is_git_file: 87 # File is not in working tree. Return owners for third_party. 88 return GetOwnersIfThirdParty(full_source_path) 89 90 # git log --follow and --reverse don't work together and using just 91 # --follow is too slow. Make a best estimate with an assumption that 92 # the original author has authored line 1 which is usually the 93 # copyright line and does not change even with file rename / move. 94 blame_output = subprocess.check_output( 95 [git_command, '--git-dir', git_dir, 'blame', '--porcelain', '-L1,1', 96 source], cwd=CHROMIUM_SRC_DIR) 97 return GetAuthorFromGitBlame(blame_output) 98 99 return None 100 101 102def FindGroupsAndDepsInDeps(deps_list, build_dir): 103 """Return list of groups, as well as their deps, from a list of deps.""" 104 groups = [] 105 deps_for_groups = {} 106 for deps in deps_list: 107 output = subprocess.check_output( 108 [GNPath(), 'desc', '--fail-on-unused-args', build_dir, deps]) 109 needle = 'Type: ' 110 for line in output.splitlines(): 111 if needle and not line.startswith(needle): 112 continue 113 if needle == 'Type: ': 114 if line != 'Type: group': 115 break 116 groups.append(deps) 117 assert deps not in deps_for_groups 118 deps_for_groups[deps] = [] 119 needle = 'Direct dependencies' 120 elif needle == 'Direct dependencies': 121 needle = '' 122 else: 123 assert needle == '' 124 if needle == line: 125 break 126 deps_for_groups[deps].append(line.strip()) 127 128 return groups, deps_for_groups 129 130 131def TraverseGroups(deps_list, build_dir): 132 """Filter out groups from a deps list. Add groups' direct dependencies.""" 133 full_deps_set = set(deps_list) 134 deps_to_check = full_deps_set.copy() 135 136 # Keep track of groups to break circular dependendies, if any. 137 seen_groups = set() 138 139 while deps_to_check: 140 # Look for groups from the deps set. 141 groups, deps_for_groups = FindGroupsAndDepsInDeps(deps_to_check, build_dir) 142 groups = set(groups).difference(seen_groups) 143 if not groups: 144 break 145 146 # Update sets. Filter out groups from the full deps set. 147 full_deps_set.difference_update(groups) 148 deps_to_check.clear() 149 seen_groups.update(groups) 150 151 # Get the direct dependencies, and filter out known groups there too. 152 for group in groups: 153 deps_to_check.update(deps_for_groups[group]) 154 deps_to_check.difference_update(seen_groups) 155 full_deps_set.update(deps_to_check) 156 return list(full_deps_set) 157 158 159def GetSourcesFromDeps(deps_list, build_dir): 160 """Return list of sources from parsing deps.""" 161 if not deps_list: 162 return None 163 164 full_deps_list = TraverseGroups(deps_list, build_dir) 165 all_sources = [] 166 for deps in full_deps_list: 167 output = subprocess.check_output( 168 [GNPath(), 'desc', '--fail-on-unused-args', build_dir, deps, 'sources']) 169 for source in output.splitlines(): 170 if source.startswith('//'): 171 source = source[2:] 172 all_sources.append(source) 173 174 return all_sources 175 176 177def GNPath(): 178 if sys.platform.startswith('linux'): 179 subdir, exe = 'linux64', 'gn' 180 elif sys.platform == 'darwin': 181 subdir, exe = 'mac', 'gn' 182 else: 183 subdir, exe = 'win', 'gn.exe' 184 185 return os.path.join(CHROMIUM_SRC_DIR, 'buildtools', subdir, exe) 186 187 188def SubStringExistsIn(substring_list, string): 189 """Return true if one of the substring in the list is found in |string|.""" 190 return any([substring in string for substring in substring_list]) 191 192 193def main(): 194 parser = argparse.ArgumentParser(description='Generate fuzzer owners file.') 195 parser.add_argument('--owners', required=True) 196 parser.add_argument('--build-dir') 197 parser.add_argument('--deps', nargs='+') 198 parser.add_argument('--sources', nargs='+') 199 args = parser.parse_args() 200 201 # Generate owners file. 202 with open(args.owners, 'w') as owners_file: 203 # If we found an owner, then write it to file. 204 # Otherwise, leave empty file to keep ninja happy. 205 owners = GetOwnersForFuzzer(args.sources) 206 if owners: 207 owners_file.write(owners) 208 return 209 210 # Could not determine owners from |args.sources|. 211 # So, try parsing sources from |args.deps|. 212 deps_sources = GetSourcesFromDeps(args.deps, args.build_dir) 213 owners = GetOwnersForFuzzer(deps_sources) 214 if owners: 215 owners_file.write(owners) 216 217 218if __name__ == '__main__': 219 main() 220