1#!/usr/bin/env python3 2# Copyright 2023 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""Tests that no linker inputs are from private paths.""" 6 7import argparse 8import fnmatch 9import os 10import pathlib 11import sys 12 13_DIR_SRC_ROOT = pathlib.Path(__file__).resolve().parents[2] 14 15 16def _print_paths(paths, limit): 17 for path in paths[:limit]: 18 print(path) 19 if len(paths) > limit: 20 print(f'... and {len(paths) - limit} more.') 21 print() 22 23 24def _apply_allowlist(found, globs): 25 ignored_paths = [] 26 new_found = [] 27 for path in found: 28 for pattern in globs: 29 if fnmatch.fnmatch(path, pattern): 30 ignored_paths.append(path) 31 break 32 else: 33 new_found.append(path) 34 return new_found, ignored_paths 35 36 37def _find_private_paths(linker_inputs, private_paths, root_out_dir): 38 seen = set() 39 found = [] 40 for linker_input in linker_inputs: 41 dirname = os.path.dirname(linker_input) 42 if dirname in seen: 43 continue 44 45 to_check = dirname 46 # Strip ../ prefix. 47 if to_check.startswith('..'): 48 to_check = os.path.relpath(to_check, _DIR_SRC_ROOT) 49 else: 50 if root_out_dir: 51 # Strip secondary toolchain subdir 52 to_check = to_check[len(root_out_dir) + 1:] 53 # Strip top-level dir (e.g. "obj", "gen"). 54 parts = to_check.split(os.path.sep, 1) 55 if len(parts) == 1: 56 continue 57 to_check = parts[1] 58 59 if any(to_check.startswith(p) for p in private_paths): 60 found.append(linker_input) 61 else: 62 seen.add(dirname) 63 return found 64 65 66def _read_private_paths(path): 67 text = pathlib.Path(path).read_text() 68 69 # Check if .gclient_entries was not valid. https://crbug.com/1427829 70 if text.startswith('# ERROR: '): 71 sys.stderr.write(text) 72 sys.exit(1) 73 74 # Remove src/ prefix from paths. 75 # We care only about paths within src/ since GN cannot reference files 76 # outside of // (and what would the obj/ path for them look like?). 77 ret = [p[4:] for p in text.splitlines() if p.startswith('src/')] 78 if not ret: 79 sys.stderr.write(f'No src/ paths found in {args.private_paths_file}\n') 80 sys.stderr.write(f'This test should not be run on public bots.\n') 81 sys.stderr.write(f'File contents:\n') 82 sys.stderr.write(text) 83 sys.exit(1) 84 85 return ret 86 87 88def main(): 89 parser = argparse.ArgumentParser() 90 parser.add_argument('--linker-inputs', 91 required=True, 92 help='Path to file containing one linker input per line, ' 93 'relative to --root-out-dir') 94 parser.add_argument('--private-paths-file', 95 required=True, 96 help='Path to file containing list of paths that are ' 97 'considered private, relative gclient root.') 98 parser.add_argument('--root-out-dir', 99 required=True, 100 help='See --linker-inputs.') 101 parser.add_argument('--allow-violation', 102 action='append', 103 help='globs of private paths to allow.') 104 parser.add_argument('--expect-failure', 105 action='store_true', 106 help='Invert exit code.') 107 args = parser.parse_args() 108 109 private_paths = _read_private_paths(args.private_paths_file) 110 linker_inputs = pathlib.Path(args.linker_inputs).read_text().splitlines() 111 112 root_out_dir = args.root_out_dir 113 if root_out_dir == '.': 114 root_out_dir = '' 115 116 found = _find_private_paths(linker_inputs, private_paths, root_out_dir) 117 118 if args.allow_violation: 119 found, ignored_paths = _apply_allowlist(found, args.allow_violation) 120 if ignored_paths: 121 print('Ignoring {len(ignored_paths)} allowlisted private paths:') 122 _print_paths(sorted(ignored_paths), 10) 123 124 if found: 125 limit = 10 if args.expect_failure else 1000 126 print(f'Found {len(found)} private paths being linked into public code:') 127 _print_paths(found, limit) 128 elif args.expect_failure: 129 print('Expected to find a private path, but none were found.') 130 131 sys.exit(0 if bool(found) == args.expect_failure else 1) 132 133 134if __name__ == '__main__': 135 main() 136