1*6777b538SAndroid Build Coastguard Worker#!/usr/bin/env python3 2*6777b538SAndroid Build Coastguard Worker# 3*6777b538SAndroid Build Coastguard Worker# Copyright 2018 The Chromium Authors 4*6777b538SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 5*6777b538SAndroid Build Coastguard Worker# found in the LICENSE file. 6*6777b538SAndroid Build Coastguard Worker"""Generates a `foo.owners` file for a `fuzzer_test("foo", ...)` GN target. 7*6777b538SAndroid Build Coastguard Worker 8*6777b538SAndroid Build Coastguard WorkerBy default, the closest `OWNERS` file is located and copied, except for 9*6777b538SAndroid Build Coastguard Worker`//OWNERS` and `//third_party/OWNERS` for fear of spamming top-level owners with 10*6777b538SAndroid Build Coastguard Workerfuzzer bugs they know nothing about. 11*6777b538SAndroid Build Coastguard Worker 12*6777b538SAndroid Build Coastguard WorkerIf no such file can be located, then we attempt to use `git blame` to identify 13*6777b538SAndroid Build Coastguard Workerthe author of the main fuzzer `.cc` file. Note that this does not work for code 14*6777b538SAndroid Build Coastguard Workerin git submodules (e.g. most code in `third_party/`), in which case we generate 15*6777b538SAndroid Build Coastguard Workeran empty file. 16*6777b538SAndroid Build Coastguard Worker 17*6777b538SAndroid Build Coastguard WorkerInvoked by GN from `fuzzer_test.gni`. 18*6777b538SAndroid Build Coastguard Worker""" 19*6777b538SAndroid Build Coastguard Worker 20*6777b538SAndroid Build Coastguard Workerimport argparse 21*6777b538SAndroid Build Coastguard Workerimport os 22*6777b538SAndroid Build Coastguard Workerimport re 23*6777b538SAndroid Build Coastguard Workerimport subprocess 24*6777b538SAndroid Build Coastguard Workerimport sys 25*6777b538SAndroid Build Coastguard Worker 26*6777b538SAndroid Build Coastguard Workerfrom typing import Optional 27*6777b538SAndroid Build Coastguard Worker 28*6777b538SAndroid Build Coastguard WorkerAUTHOR_REGEX = re.compile('author-mail <(.+)>') 29*6777b538SAndroid Build Coastguard WorkerCHROMIUM_SRC_DIR = os.path.dirname( 30*6777b538SAndroid Build Coastguard Worker os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 31*6777b538SAndroid Build Coastguard WorkerOWNERS_FILENAME = 'OWNERS' 32*6777b538SAndroid Build Coastguard WorkerTHIRD_PARTY = 'third_party' 33*6777b538SAndroid Build Coastguard WorkerTHIRD_PARTY_SEARCH_STRING = THIRD_PARTY + os.path.sep 34*6777b538SAndroid Build Coastguard Worker 35*6777b538SAndroid Build Coastguard Worker 36*6777b538SAndroid Build Coastguard Workerdef GetAuthorFromGitBlame(blame_output): 37*6777b538SAndroid Build Coastguard Worker """Return author from git blame output.""" 38*6777b538SAndroid Build Coastguard Worker for line in blame_output.decode('utf-8').splitlines(): 39*6777b538SAndroid Build Coastguard Worker m = AUTHOR_REGEX.match(line) 40*6777b538SAndroid Build Coastguard Worker if m: 41*6777b538SAndroid Build Coastguard Worker return m.group(1) 42*6777b538SAndroid Build Coastguard Worker 43*6777b538SAndroid Build Coastguard Worker return None 44*6777b538SAndroid Build Coastguard Worker 45*6777b538SAndroid Build Coastguard Worker 46*6777b538SAndroid Build Coastguard Workerdef GetGitCommand(): 47*6777b538SAndroid Build Coastguard Worker """Returns a git command that does not need to be executed using shell=True. 48*6777b538SAndroid Build Coastguard Worker On non-Windows platforms: 'git'. On Windows: 'git.bat'. 49*6777b538SAndroid Build Coastguard Worker """ 50*6777b538SAndroid Build Coastguard Worker return 'git.bat' if sys.platform == 'win32' else 'git' 51*6777b538SAndroid Build Coastguard Worker 52*6777b538SAndroid Build Coastguard Worker 53*6777b538SAndroid Build Coastguard Workerdef GetOwnersFromOwnersFile(source: str) -> Optional[str]: 54*6777b538SAndroid Build Coastguard Worker """Finds the owners of `source` from the closest OWNERS file. 55*6777b538SAndroid Build Coastguard Worker 56*6777b538SAndroid Build Coastguard Worker Both //OWNERS or */third_party/OWNERS are ignored so as not to spam top-level 57*6777b538SAndroid Build Coastguard Worker owners with unowned fuzzer bugs. 58*6777b538SAndroid Build Coastguard Worker 59*6777b538SAndroid Build Coastguard Worker Args: 60*6777b538SAndroid Build Coastguard Worker source: Relative path from the chromium src directory to the target source 61*6777b538SAndroid Build Coastguard Worker file. 62*6777b538SAndroid Build Coastguard Worker 63*6777b538SAndroid Build Coastguard Worker Returns: 64*6777b538SAndroid Build Coastguard Worker The entire contents of the closest OWNERS file. That is, the first OWNERS 65*6777b538SAndroid Build Coastguard Worker file encountered while walking up through the ancestor directories of the 66*6777b538SAndroid Build Coastguard Worker target source file. 67*6777b538SAndroid Build Coastguard Worker """ 68*6777b538SAndroid Build Coastguard Worker # TODO(https://crbug.com/1513729): Use `pathlib` instead of `os.path` for 69*6777b538SAndroid Build Coastguard Worker # better ergonomics and robustness. 70*6777b538SAndroid Build Coastguard Worker dirs = source.split(os.path.sep)[:-1] 71*6777b538SAndroid Build Coastguard Worker 72*6777b538SAndroid Build Coastguard Worker # Note: We never test for //OWNERS, i.e. when `dirs` is empty. 73*6777b538SAndroid Build Coastguard Worker while dirs: 74*6777b538SAndroid Build Coastguard Worker # Never return the contents of */third_party/OWNERS, and stop searching. 75*6777b538SAndroid Build Coastguard Worker if dirs[-1] == THIRD_PARTY: 76*6777b538SAndroid Build Coastguard Worker break 77*6777b538SAndroid Build Coastguard Worker 78*6777b538SAndroid Build Coastguard Worker owners_file_path = os.path.join(CHROMIUM_SRC_DIR, *dirs, OWNERS_FILENAME) 79*6777b538SAndroid Build Coastguard Worker if os.path.exists(owners_file_path): 80*6777b538SAndroid Build Coastguard Worker # TODO(https://crbug.com/1513729): OWNERS files can reference others, 81*6777b538SAndroid Build Coastguard Worker # have per-file directives, etc. We should be cleverer than this. 82*6777b538SAndroid Build Coastguard Worker return open(owners_file_path).read() 83*6777b538SAndroid Build Coastguard Worker 84*6777b538SAndroid Build Coastguard Worker dirs.pop() 85*6777b538SAndroid Build Coastguard Worker 86*6777b538SAndroid Build Coastguard Worker return None 87*6777b538SAndroid Build Coastguard Worker 88*6777b538SAndroid Build Coastguard Workerdef GetOwnersForFuzzer(sources): 89*6777b538SAndroid Build Coastguard Worker """Return owners given a list of sources as input.""" 90*6777b538SAndroid Build Coastguard Worker if not sources: 91*6777b538SAndroid Build Coastguard Worker return None 92*6777b538SAndroid Build Coastguard Worker 93*6777b538SAndroid Build Coastguard Worker for source in sources: 94*6777b538SAndroid Build Coastguard Worker full_source_path = os.path.join(CHROMIUM_SRC_DIR, source) 95*6777b538SAndroid Build Coastguard Worker if not os.path.exists(full_source_path): 96*6777b538SAndroid Build Coastguard Worker continue 97*6777b538SAndroid Build Coastguard Worker 98*6777b538SAndroid Build Coastguard Worker with open(full_source_path, 'r') as source_file_handle: 99*6777b538SAndroid Build Coastguard Worker source_content = source_file_handle.read() 100*6777b538SAndroid Build Coastguard Worker 101*6777b538SAndroid Build Coastguard Worker if SubStringExistsIn( 102*6777b538SAndroid Build Coastguard Worker ['FuzzOneInput', 'LLVMFuzzerTestOneInput', 'PROTO_FUZZER'], 103*6777b538SAndroid Build Coastguard Worker source_content): 104*6777b538SAndroid Build Coastguard Worker # Found the fuzzer source (and not dependency of fuzzer). 105*6777b538SAndroid Build Coastguard Worker 106*6777b538SAndroid Build Coastguard Worker # Try finding the closest OWNERS file first. 107*6777b538SAndroid Build Coastguard Worker owners = GetOwnersFromOwnersFile(source) 108*6777b538SAndroid Build Coastguard Worker if owners: 109*6777b538SAndroid Build Coastguard Worker return owners 110*6777b538SAndroid Build Coastguard Worker 111*6777b538SAndroid Build Coastguard Worker git_dir = os.path.join(CHROMIUM_SRC_DIR, '.git') 112*6777b538SAndroid Build Coastguard Worker git_command = GetGitCommand() 113*6777b538SAndroid Build Coastguard Worker is_git_file = bool(subprocess.check_output( 114*6777b538SAndroid Build Coastguard Worker [git_command, '--git-dir', git_dir, 'ls-files', source], 115*6777b538SAndroid Build Coastguard Worker cwd=CHROMIUM_SRC_DIR)) 116*6777b538SAndroid Build Coastguard Worker if not is_git_file: 117*6777b538SAndroid Build Coastguard Worker # File is not in working tree. If no OWNERS file was found, we cannot 118*6777b538SAndroid Build Coastguard Worker # tell who it belongs to. 119*6777b538SAndroid Build Coastguard Worker return None 120*6777b538SAndroid Build Coastguard Worker 121*6777b538SAndroid Build Coastguard Worker # `git log --follow` and `--reverse` don't work together and using just 122*6777b538SAndroid Build Coastguard Worker # `--follow` is too slow. Make a best estimate with an assumption that the 123*6777b538SAndroid Build Coastguard Worker # original author has authored the copyright block, which (generally) does 124*6777b538SAndroid Build Coastguard Worker # not change even with file rename/move. Look at the last line of the 125*6777b538SAndroid Build Coastguard Worker # block, as a copyright block update sweep in late 2022 made one person 126*6777b538SAndroid Build Coastguard Worker # responsible for changing the first line of every copyright block in the 127*6777b538SAndroid Build Coastguard Worker # repo, and it would be best to avoid assigning ownership of every fuzz 128*6777b538SAndroid Build Coastguard Worker # issue predating that year to that one person. 129*6777b538SAndroid Build Coastguard Worker blame_output = subprocess.check_output( 130*6777b538SAndroid Build Coastguard Worker [git_command, '--git-dir', git_dir, 'blame', '--porcelain', '-L3,3', 131*6777b538SAndroid Build Coastguard Worker source], cwd=CHROMIUM_SRC_DIR) 132*6777b538SAndroid Build Coastguard Worker return GetAuthorFromGitBlame(blame_output) 133*6777b538SAndroid Build Coastguard Worker 134*6777b538SAndroid Build Coastguard Worker return None 135*6777b538SAndroid Build Coastguard Worker 136*6777b538SAndroid Build Coastguard Workerdef FindGroupsAndDepsInDeps(deps_list, build_dir): 137*6777b538SAndroid Build Coastguard Worker """Return list of groups, as well as their deps, from a list of deps.""" 138*6777b538SAndroid Build Coastguard Worker groups = [] 139*6777b538SAndroid Build Coastguard Worker deps_for_groups = {} 140*6777b538SAndroid Build Coastguard Worker for deps in deps_list: 141*6777b538SAndroid Build Coastguard Worker output = subprocess.check_output( 142*6777b538SAndroid Build Coastguard Worker [GNPath(), 'desc', '--fail-on-unused-args', build_dir, deps]).decode( 143*6777b538SAndroid Build Coastguard Worker 'utf8') 144*6777b538SAndroid Build Coastguard Worker needle = 'Type: ' 145*6777b538SAndroid Build Coastguard Worker for line in output.splitlines(): 146*6777b538SAndroid Build Coastguard Worker if needle and not line.startswith(needle): 147*6777b538SAndroid Build Coastguard Worker continue 148*6777b538SAndroid Build Coastguard Worker if needle == 'Type: ': 149*6777b538SAndroid Build Coastguard Worker if line != 'Type: group': 150*6777b538SAndroid Build Coastguard Worker break 151*6777b538SAndroid Build Coastguard Worker groups.append(deps) 152*6777b538SAndroid Build Coastguard Worker assert deps not in deps_for_groups 153*6777b538SAndroid Build Coastguard Worker deps_for_groups[deps] = [] 154*6777b538SAndroid Build Coastguard Worker needle = 'Direct dependencies' 155*6777b538SAndroid Build Coastguard Worker elif needle == 'Direct dependencies': 156*6777b538SAndroid Build Coastguard Worker needle = '' 157*6777b538SAndroid Build Coastguard Worker else: 158*6777b538SAndroid Build Coastguard Worker assert needle == '' 159*6777b538SAndroid Build Coastguard Worker if needle == line: 160*6777b538SAndroid Build Coastguard Worker break 161*6777b538SAndroid Build Coastguard Worker deps_for_groups[deps].append(line.strip()) 162*6777b538SAndroid Build Coastguard Worker 163*6777b538SAndroid Build Coastguard Worker return groups, deps_for_groups 164*6777b538SAndroid Build Coastguard Worker 165*6777b538SAndroid Build Coastguard Worker 166*6777b538SAndroid Build Coastguard Workerdef TraverseGroups(deps_list, build_dir): 167*6777b538SAndroid Build Coastguard Worker """Filter out groups from a deps list. Add groups' direct dependencies.""" 168*6777b538SAndroid Build Coastguard Worker full_deps_set = set(deps_list) 169*6777b538SAndroid Build Coastguard Worker deps_to_check = full_deps_set.copy() 170*6777b538SAndroid Build Coastguard Worker 171*6777b538SAndroid Build Coastguard Worker # Keep track of groups to break circular dependendies, if any. 172*6777b538SAndroid Build Coastguard Worker seen_groups = set() 173*6777b538SAndroid Build Coastguard Worker 174*6777b538SAndroid Build Coastguard Worker while deps_to_check: 175*6777b538SAndroid Build Coastguard Worker # Look for groups from the deps set. 176*6777b538SAndroid Build Coastguard Worker groups, deps_for_groups = FindGroupsAndDepsInDeps(deps_to_check, build_dir) 177*6777b538SAndroid Build Coastguard Worker groups = set(groups).difference(seen_groups) 178*6777b538SAndroid Build Coastguard Worker if not groups: 179*6777b538SAndroid Build Coastguard Worker break 180*6777b538SAndroid Build Coastguard Worker 181*6777b538SAndroid Build Coastguard Worker # Update sets. Filter out groups from the full deps set. 182*6777b538SAndroid Build Coastguard Worker full_deps_set.difference_update(groups) 183*6777b538SAndroid Build Coastguard Worker deps_to_check.clear() 184*6777b538SAndroid Build Coastguard Worker seen_groups.update(groups) 185*6777b538SAndroid Build Coastguard Worker 186*6777b538SAndroid Build Coastguard Worker # Get the direct dependencies, and filter out known groups there too. 187*6777b538SAndroid Build Coastguard Worker for group in groups: 188*6777b538SAndroid Build Coastguard Worker deps_to_check.update(deps_for_groups[group]) 189*6777b538SAndroid Build Coastguard Worker deps_to_check.difference_update(seen_groups) 190*6777b538SAndroid Build Coastguard Worker full_deps_set.update(deps_to_check) 191*6777b538SAndroid Build Coastguard Worker return list(full_deps_set) 192*6777b538SAndroid Build Coastguard Worker 193*6777b538SAndroid Build Coastguard Worker 194*6777b538SAndroid Build Coastguard Workerdef GetSourcesFromDeps(deps_list, build_dir): 195*6777b538SAndroid Build Coastguard Worker """Return list of sources from parsing deps.""" 196*6777b538SAndroid Build Coastguard Worker if not deps_list: 197*6777b538SAndroid Build Coastguard Worker return None 198*6777b538SAndroid Build Coastguard Worker 199*6777b538SAndroid Build Coastguard Worker full_deps_list = TraverseGroups(deps_list, build_dir) 200*6777b538SAndroid Build Coastguard Worker all_sources = [] 201*6777b538SAndroid Build Coastguard Worker for deps in full_deps_list: 202*6777b538SAndroid Build Coastguard Worker output = subprocess.check_output( 203*6777b538SAndroid Build Coastguard Worker [GNPath(), 'desc', '--fail-on-unused-args', build_dir, deps, 'sources']) 204*6777b538SAndroid Build Coastguard Worker for source in bytes(output).decode('utf8').splitlines(): 205*6777b538SAndroid Build Coastguard Worker if source.startswith('//'): 206*6777b538SAndroid Build Coastguard Worker source = source[2:] 207*6777b538SAndroid Build Coastguard Worker all_sources.append(source) 208*6777b538SAndroid Build Coastguard Worker 209*6777b538SAndroid Build Coastguard Worker return all_sources 210*6777b538SAndroid Build Coastguard Worker 211*6777b538SAndroid Build Coastguard Worker 212*6777b538SAndroid Build Coastguard Workerdef GNPath(): 213*6777b538SAndroid Build Coastguard Worker if sys.platform.startswith('linux'): 214*6777b538SAndroid Build Coastguard Worker subdir, exe = 'linux64', 'gn' 215*6777b538SAndroid Build Coastguard Worker elif sys.platform == 'darwin': 216*6777b538SAndroid Build Coastguard Worker subdir, exe = 'mac', 'gn' 217*6777b538SAndroid Build Coastguard Worker else: 218*6777b538SAndroid Build Coastguard Worker subdir, exe = 'win', 'gn.exe' 219*6777b538SAndroid Build Coastguard Worker 220*6777b538SAndroid Build Coastguard Worker return os.path.join(CHROMIUM_SRC_DIR, 'buildtools', subdir, exe) 221*6777b538SAndroid Build Coastguard Worker 222*6777b538SAndroid Build Coastguard Worker 223*6777b538SAndroid Build Coastguard Workerdef SubStringExistsIn(substring_list, string): 224*6777b538SAndroid Build Coastguard Worker """Return true if one of the substring in the list is found in |string|.""" 225*6777b538SAndroid Build Coastguard Worker return any(substring in string for substring in substring_list) 226*6777b538SAndroid Build Coastguard Worker 227*6777b538SAndroid Build Coastguard Worker 228*6777b538SAndroid Build Coastguard Workerdef main(): 229*6777b538SAndroid Build Coastguard Worker parser = argparse.ArgumentParser(description='Generate fuzzer owners file.') 230*6777b538SAndroid Build Coastguard Worker parser.add_argument('--owners', required=True) 231*6777b538SAndroid Build Coastguard Worker parser.add_argument('--build-dir') 232*6777b538SAndroid Build Coastguard Worker parser.add_argument('--deps', nargs='+') 233*6777b538SAndroid Build Coastguard Worker parser.add_argument('--sources', nargs='+') 234*6777b538SAndroid Build Coastguard Worker args = parser.parse_args() 235*6777b538SAndroid Build Coastguard Worker 236*6777b538SAndroid Build Coastguard Worker # Generate owners file. 237*6777b538SAndroid Build Coastguard Worker with open(args.owners, 'w') as owners_file: 238*6777b538SAndroid Build Coastguard Worker # If we found an owner, then write it to file. 239*6777b538SAndroid Build Coastguard Worker # Otherwise, leave empty file to keep ninja happy. 240*6777b538SAndroid Build Coastguard Worker owners = GetOwnersForFuzzer(args.sources) 241*6777b538SAndroid Build Coastguard Worker if owners: 242*6777b538SAndroid Build Coastguard Worker owners_file.write(owners) 243*6777b538SAndroid Build Coastguard Worker return 244*6777b538SAndroid Build Coastguard Worker 245*6777b538SAndroid Build Coastguard Worker # Could not determine owners from |args.sources|. 246*6777b538SAndroid Build Coastguard Worker # So, try parsing sources from |args.deps|. 247*6777b538SAndroid Build Coastguard Worker deps_sources = GetSourcesFromDeps(args.deps, args.build_dir) 248*6777b538SAndroid Build Coastguard Worker owners = GetOwnersForFuzzer(deps_sources) 249*6777b538SAndroid Build Coastguard Worker if owners: 250*6777b538SAndroid Build Coastguard Worker owners_file.write(owners) 251*6777b538SAndroid Build Coastguard Worker 252*6777b538SAndroid Build Coastguard Worker 253*6777b538SAndroid Build Coastguard Workerif __name__ == '__main__': 254*6777b538SAndroid Build Coastguard Worker main() 255