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