# Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. """ Entry point for running the parser package as a script. Usage: # With output directory: python ... --output-dir /path/to/output """ import argparse import concurrent.futures import os import subprocess import sys import tempfile import traceback from .config import ApiViewSnapshotConfig, parse_config_file from .doxygen import get_doxygen_bin, run_doxygen from .main import build_snapshot from .path_utils import get_react_native_dir from .snapshot_diff import check_snapshots def run_command( cmd: list[str], label: str, verbose: bool = False, **kwargs, ) -> subprocess.CompletedProcess: """Run a subprocess command with consistent error handling.""" result = subprocess.run(cmd, **kwargs) if result.returncode != 0: if verbose: print(f"{label} finished with error: {result.stderr}") raise RuntimeError( f"{label} finished with error (exit code {result.returncode})" ) elif verbose: print(f"{label} finished successfully") return result def build_codegen( platform: str, verbose: bool = False, output_path: str = "./api/codegen", label: str = "", ) -> str: react_native_dir = os.path.join(get_react_native_dir(), "packages", "react-native") run_command( [ "node", "./scripts/generate-codegen-artifacts.js", "--path", "./", "--outputPath", output_path, "--targetPlatform", platform, "--forceOutputPath", ], label=f"[{label}] Codegen" if label else "Codegen", verbose=verbose, cwd=react_native_dir, capture_output=True, text=True, ) if os.path.isabs(output_path): return output_path return os.path.join(react_native_dir, output_path) def build_snapshot_for_view( api_view: str, react_native_dir: str, include_directories: list[str], exclude_patterns: list[str], definitions: dict[str, str | int], output_dir: str, codegen_platform: str | None = None, verbose: bool = True, input_filter: str = None, work_dir: str | None = None, ) -> str: if verbose: print(f"[{api_view}] Generating API view") include_directories = list(include_directories) if work_dir is None: work_dir = os.path.join(react_native_dir, "api") if codegen_platform is not None: codegen_output = os.path.join(work_dir, "codegen") codegen_dir = build_codegen( codegen_platform, verbose=verbose, output_path=codegen_output, label=api_view, ) include_directories.append(codegen_dir) elif verbose: print(f"[{api_view}] Skipping codegen") config_file = f".doxygen.config.{api_view}.generated" run_doxygen( working_dir=react_native_dir, include_directories=include_directories, exclude_patterns=exclude_patterns, definitions=definitions, input_filter=input_filter, verbose=verbose, output_dir=work_dir, config_file=config_file, label=api_view, ) if verbose: print(f"[{api_view}] Building snapshot") snapshot = build_snapshot(os.path.join(work_dir, "xml")) snapshot_string = snapshot.to_string() output_file = os.path.join(output_dir, f"{api_view}Cxx.api") os.makedirs(output_dir, exist_ok=True) with open(output_file, "w") as f: f.write("// @" + "generated by scripts/cxx-api\n\n") f.write(snapshot_string) return snapshot_string def build_snapshots( snapshot_configs: list[ApiViewSnapshotConfig], react_native_dir: str, output_dir: str, input_filter: str | None, verbose: bool, view_filter: str | None = None, is_test: bool = False, ) -> None: if not is_test: configs_to_build = [ config for config in snapshot_configs if not view_filter or config.snapshot_name == view_filter ] with tempfile.TemporaryDirectory(prefix="cxx-api-") as parent_tmp: with concurrent.futures.ThreadPoolExecutor() as executor: futures = {} for config in configs_to_build: work_dir = os.path.join(parent_tmp, config.snapshot_name) os.makedirs(work_dir, exist_ok=True) future = executor.submit( build_snapshot_for_view, api_view=config.snapshot_name, react_native_dir=react_native_dir, include_directories=config.inputs, exclude_patterns=config.exclude_patterns, definitions=config.definitions, output_dir=output_dir, codegen_platform=config.codegen_platform, verbose=verbose, input_filter=input_filter if config.input_filter else None, work_dir=work_dir, ) futures[future] = config.snapshot_name errors = [] for future in concurrent.futures.as_completed(futures): view_name = futures[future] try: future.result() except Exception as e: errors.append((view_name, e)) if verbose: print( f"[{view_name}] Error generating:\n" f"{traceback.format_exc()}" ) if errors: failed_views = ", ".join(name for name, _ in errors) raise RuntimeError(f"Failed to generate snapshots: {failed_views}") else: with tempfile.TemporaryDirectory(prefix="cxx-api-test-") as work_dir: snapshot = build_snapshot_for_view( api_view="Test", react_native_dir=react_native_dir, include_directories=[], exclude_patterns=[], definitions={}, output_dir=output_dir, codegen_platform=None, verbose=verbose, input_filter=input_filter, work_dir=work_dir, ) if verbose: print(snapshot) def get_default_snapshot_dir() -> str: return os.path.join(get_react_native_dir(), "scripts", "cxx-api", "api-snapshots") def main(): parser = argparse.ArgumentParser( description="Generate API snapshots from C++ headers" ) parser.add_argument( "--output-dir", type=str, help="Output directory for the snapshot", ) parser.add_argument( "--check", action="store_true", help="Generate snapshots to a temp directory and compare against committed ones", ) parser.add_argument( "--snapshot-dir", type=str, help="Directory containing committed snapshots for comparison (used with --check)", ) parser.add_argument( "--view", type=str, help="Name of the API view to generate", ) parser.add_argument( "--test", action="store_true", help="Run on the local test directory instead of the react-native directory", ) args = parser.parse_args() verbose = not args.check doxygen_bin = get_doxygen_bin() version_result = subprocess.run( [doxygen_bin, "--version"], capture_output=True, text=True, ) if verbose: print(f"Using Doxygen {version_result.stdout.strip()} ({doxygen_bin})") react_native_package_dir = ( os.path.join(get_react_native_dir(), "packages", "react-native") if not args.test else os.path.join(get_react_native_dir(), "scripts", "cxx-api", "manual_test") ) if verbose: print(f"Running in directory: {react_native_package_dir}") input_filter_path = os.path.join( get_react_native_dir(), "scripts", "cxx-api", "parser", "input_filters", "main.py", ) input_filter = None if os.path.exists(input_filter_path): input_filter = f"python3 {input_filter_path}" config_path = os.path.join( get_react_native_dir(), "scripts", "cxx-api", "config.yml" ) snapshot_configs = parse_config_file( config_path, get_react_native_dir(), ) with tempfile.TemporaryDirectory() as tmpdir: snapshot_output_dir = ( tmpdir if args.check else args.output_dir or get_default_snapshot_dir() ) build_snapshots( output_dir=snapshot_output_dir, verbose=not args.check, snapshot_configs=snapshot_configs, react_native_dir=react_native_package_dir, input_filter=input_filter, view_filter=args.view, is_test=args.test, ) if args.check: snapshot_dir = args.snapshot_dir or get_default_snapshot_dir() if not check_snapshots(snapshot_output_dir, snapshot_dir): sys.exit(1) print("All snapshot checks passed") if __name__ == "__main__": main()