1#!/usr/bin/env python3 2# Copyright 2023 The PDFium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""Tool for converting GN runtime_deps to CAS archive paths.""" 6 7import argparse 8from collections import deque 9import filecmp 10import json 11import logging 12from pathlib import Path 13import os 14 15EXCLUDE_DIRS = { 16 '.git', 17 '__pycache__', 18} 19 20 21def parse_runtime_deps(runtime_deps): 22 """Parses GN's `runtime_deps` format.""" 23 with runtime_deps: 24 return [line.rstrip() for line in runtime_deps] 25 26 27def resolve_paths(root, initial_paths): 28 """Converts paths to CAS archive paths format.""" 29 absolute_root = os.path.abspath(root) 30 31 resolved_paths = [] 32 unvisited_paths = deque(map(Path, initial_paths)) 33 while unvisited_paths: 34 path = unvisited_paths.popleft() 35 36 if not path.exists(): 37 logging.warning('"%(path)s" does not exist', {'path': path}) 38 continue 39 40 if path.is_dir(): 41 # Expand specific children if any are excluded. 42 child_paths = expand_dir(path) 43 if child_paths: 44 unvisited_paths.extendleft(child_paths) 45 continue 46 47 resolved_paths.append(os.path.relpath(path, start=absolute_root)) 48 49 resolved_paths.sort() 50 return [[absolute_root, path] for path in resolved_paths] 51 52 53def expand_dir(path): 54 """Explicitly expands directory if any children are excluded.""" 55 expand = False 56 expanded_paths = [] 57 58 for child_path in path.iterdir(): 59 if child_path.name in EXCLUDE_DIRS and path.is_dir(): 60 expand = True 61 continue 62 expanded_paths.append(child_path) 63 64 return expanded_paths if expand else [] 65 66 67def replace_output(resolved, output_path): 68 """Atomically replaces the output with the resolved JSON if changed.""" 69 new_output_path = output_path + '.new' 70 try: 71 with open(new_output_path, 'w', encoding='ascii') as new_output: 72 json.dump(resolved, new_output) 73 74 if (os.path.exists(output_path) and 75 filecmp.cmp(new_output_path, output_path, shallow=False)): 76 return 77 78 os.replace(new_output_path, output_path) 79 new_output_path = None 80 finally: 81 if new_output_path: 82 os.remove(new_output_path) 83 84 85def main(): 86 parser = argparse.ArgumentParser(description=__doc__) 87 parser.add_argument('--root') 88 parser.add_argument( 89 'runtime_deps', 90 help='runtime_deps written by GN', 91 type=argparse.FileType('r', encoding='utf_8'), 92 metavar='input.runtime_deps') 93 parser.add_argument( 94 'output_json', 95 help='CAS archive paths in JSON format', 96 metavar='output.json') 97 args = parser.parse_args() 98 99 runtime_deps = parse_runtime_deps(args.runtime_deps) 100 resolved_paths = resolve_paths(args.root, runtime_deps) 101 replace_output(resolved_paths, args.output_json) 102 103 104if __name__ == '__main__': 105 main() 106