| #!/usr/bin/env python3 |
| # Copyright 2023 The PDFium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Tool for converting GN runtime_deps to CAS archive paths.""" |
| |
| import argparse |
| from collections import deque |
| import filecmp |
| import json |
| import logging |
| from pathlib import Path |
| import os |
| |
| EXCLUDE_DIRS = { |
| '.git', |
| '__pycache__', |
| } |
| |
| |
| def parse_runtime_deps(runtime_deps): |
| """Parses GN's `runtime_deps` format.""" |
| with runtime_deps: |
| return [line.rstrip() for line in runtime_deps] |
| |
| |
| def resolve_paths(root, initial_paths): |
| """Converts paths to CAS archive paths format.""" |
| absolute_root = os.path.abspath(root) |
| |
| resolved_paths = [] |
| unvisited_paths = deque(map(Path, initial_paths)) |
| while unvisited_paths: |
| path = unvisited_paths.popleft() |
| |
| if not path.exists(): |
| logging.warning('"%(path)s" does not exist', {'path': path}) |
| continue |
| |
| if path.is_dir(): |
| # Expand specific children if any are excluded. |
| child_paths = expand_dir(path) |
| if child_paths: |
| unvisited_paths.extendleft(child_paths) |
| continue |
| |
| resolved_paths.append(os.path.relpath(path, start=absolute_root)) |
| |
| resolved_paths.sort() |
| return [[absolute_root, path] for path in resolved_paths] |
| |
| |
| def expand_dir(path): |
| """Explicitly expands directory if any children are excluded.""" |
| expand = False |
| expanded_paths = [] |
| |
| for child_path in path.iterdir(): |
| if child_path.name in EXCLUDE_DIRS and path.is_dir(): |
| expand = True |
| continue |
| expanded_paths.append(child_path) |
| |
| return expanded_paths if expand else [] |
| |
| |
| def replace_output(resolved, output_path): |
| """Atomically replaces the output with the resolved JSON if changed.""" |
| new_output_path = output_path + '.new' |
| try: |
| with open(new_output_path, 'w', encoding='ascii') as new_output: |
| json.dump(resolved, new_output) |
| |
| if (os.path.exists(output_path) and |
| filecmp.cmp(new_output_path, output_path, shallow=False)): |
| return |
| |
| os.replace(new_output_path, output_path) |
| new_output_path = None |
| finally: |
| if new_output_path: |
| os.remove(new_output_path) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument('--root') |
| parser.add_argument( |
| 'runtime_deps', |
| help='runtime_deps written by GN', |
| type=argparse.FileType('r', encoding='utf_8'), |
| metavar='input.runtime_deps') |
| parser.add_argument( |
| 'output_json', |
| help='CAS archive paths in JSON format', |
| metavar='output.json') |
| args = parser.parse_args() |
| |
| runtime_deps = parse_runtime_deps(args.runtime_deps) |
| resolved_paths = resolve_paths(args.root, runtime_deps) |
| replace_output(resolved_paths, args.output_json) |
| |
| |
| if __name__ == '__main__': |
| main() |