Skip to content
Open
247 changes: 102 additions & 145 deletions scripts/cxx-api/parser/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,67 +13,39 @@

import argparse
import os
import shutil
import subprocess
import sys
import tempfile

from .config import parse_config_file
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

DOXYGEN_CONFIG_FILE = ".doxygen.config.generated"


def build_doxygen_config(
directory: str,
include_directories: list[str] = None,
exclude_patterns: list[str] = None,
definitions: dict[str, str | int] = None,
input_filter: str = None,
) -> None:
if include_directories is None:
include_directories = []
if exclude_patterns is None:
exclude_patterns = []
if definitions is None:
definitions = {}

include_directories_str = " ".join(include_directories)
exclude_patterns_str = "\\\n".join(exclude_patterns)
if len(exclude_patterns) > 0:
exclude_patterns_str = f"\\\n{exclude_patterns_str}"

definitions_str = " ".join(
[
f'{key}="{value}"' if isinstance(value, str) else f"{key}={value}"
for key, value in definitions.items()
]
)

input_filter_str = input_filter if input_filter else ""

# read the template file
with open(os.path.join(directory, ".doxygen.config.template")) as f:
template = f.read()

# replace the placeholders with the actual values
config = (
template.replace("${INPUTS}", include_directories_str)
.replace("${EXCLUDE_PATTERNS}", exclude_patterns_str)
.replace("${PREDEFINED}", definitions_str)
.replace("${DOXYGEN_INPUT_FILTER}", input_filter_str)
)

# write the config file
with open(os.path.join(directory, DOXYGEN_CONFIG_FILE), "w") as f:
f.write(config)
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}")
sys.exit(1)
elif verbose:
print(f"{label} finished successfully")
return result


def build_codegen(platform: str, verbose: bool = False) -> str:
react_native_dir = os.path.join(get_react_native_dir(), "packages", "react-native")

result = subprocess.run(
run_command(
[
"node",
"./scripts/generate-codegen-artifacts.js",
Expand All @@ -85,17 +57,11 @@ def build_codegen(platform: str, verbose: bool = False) -> str:
platform,
"--forceOutputPath",
],
label="Codegen",
verbose=verbose,
cwd=react_native_dir,
)

if result.returncode != 0:
if verbose:
print(f"Codegen finished with error: {result.stderr}")
sys.exit(1)
else:
if verbose:
print("Codegen finished successfully")

return os.path.join(react_native_dir, "api", "codegen")


Expand All @@ -109,64 +75,35 @@ def build_snapshot_for_view(
codegen_platform: str | None = None,
verbose: bool = True,
input_filter: str = None,
) -> None:
# If there is already an output directory, delete it
if os.path.exists(os.path.join(react_native_dir, "api")):
if verbose:
print("Deleting existing output directory")
subprocess.run(["rm", "-rf", os.path.join(react_native_dir, "api")])

) -> str:
if verbose:
print(f"Generating API view: {api_view}")

api_dir = os.path.join(react_native_dir, "api")
if os.path.exists(api_dir):
if verbose:
print(" Deleting existing output directory")
shutil.rmtree(api_dir)

if codegen_platform is not None:
codegen_dir = build_codegen(codegen_platform, verbose=verbose)
include_directories.append(codegen_dir)
elif verbose:
print("Skipping codegen")

if verbose:
print("Generating Doxygen config file")
print(" Skipping codegen")

build_doxygen_config(
react_native_dir,
run_doxygen(
working_dir=react_native_dir,
include_directories=include_directories,
exclude_patterns=exclude_patterns,
definitions=definitions,
input_filter=input_filter,
verbose=verbose,
)

if verbose:
print("Running Doxygen")
if input_filter:
print(f" Using input filter: {input_filter}")

# Run doxygen with the config file
doxygen_bin = os.environ.get("DOXYGEN_BIN", "doxygen")

result = subprocess.run(
[doxygen_bin, DOXYGEN_CONFIG_FILE],
cwd=react_native_dir,
capture_output=True,
text=True,
)

# Check the result
if result.returncode != 0:
if verbose:
print(f"Doxygen finished with error: {result.stderr}")
sys.exit(1)
else:
if verbose:
print("Doxygen finished successfully")

# Delete the Doxygen config file
if verbose:
print("Deleting Doxygen config file")
subprocess.run(["rm", DOXYGEN_CONFIG_FILE], cwd=react_native_dir)
print(" Building snapshot")

# build snapshot, convert to string, and save to file
snapshot = build_snapshot(os.path.join(react_native_dir, "api", "xml"))
snapshot = build_snapshot(os.path.join(api_dir, "xml"))
snapshot_string = snapshot.to_string()

output_file = os.path.join(output_dir, f"{api_view}Cxx.api")
Expand All @@ -179,6 +116,52 @@ def build_snapshot_for_view(
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:
for config in snapshot_configs:
if view_filter and config.snapshot_name != view_filter:
continue

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,
)
else:
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,
)

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"
Expand All @@ -198,6 +181,11 @@ def main():
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",
Expand All @@ -207,7 +195,7 @@ def main():

verbose = not args.check

doxygen_bin = os.environ.get("DOXYGEN_BIN", "doxygen")
doxygen_bin = get_doxygen_bin()
version_result = subprocess.run(
[doxygen_bin, "--version"],
capture_output=True,
Expand All @@ -216,7 +204,6 @@ def main():
if verbose:
print(f"Using Doxygen {version_result.stdout.strip()} ({doxygen_bin})")

# Define the path to the react-native directory
react_native_package_dir = (
os.path.join(get_react_native_dir(), "packages", "react-native")
if not args.test
Expand All @@ -238,7 +225,6 @@ def main():
if os.path.exists(input_filter_path):
input_filter = f"python3 {input_filter_path}"

# Parse config file
config_path = os.path.join(
get_react_native_dir(), "scripts", "cxx-api", "config.yml"
)
Expand All @@ -247,57 +233,28 @@ def main():
get_react_native_dir(),
)

def build_snapshots(output_dir: str, verbose: bool) -> None:
if not args.test:
for config in snapshot_configs:
build_snapshot_for_view(
api_view=config.snapshot_name,
react_native_dir=react_native_package_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,
)
else:
snapshot = build_snapshot_for_view(
api_view="Test",
react_native_dir=react_native_package_dir,
include_directories=[],
exclude_patterns=[],
definitions={},
output_dir=output_dir,
codegen_platform=None,
verbose=verbose,
input_filter=input_filter,
)

if verbose:
print(snapshot)
with tempfile.TemporaryDirectory() as tmpdir:
snapshot_output_dir = (
tmpdir if args.check else args.output_dir or get_default_snapshot_dir()
)

if args.check:
with tempfile.TemporaryDirectory() as tmpdir:
build_snapshots(tmpdir, verbose=False)
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,
)

snapshot_dir = args.snapshot_dir or os.path.join(
get_react_native_dir(), "scripts", "cxx-api", "api-snapshots"
)
if args.check:
snapshot_dir = args.snapshot_dir or get_default_snapshot_dir()

if not check_snapshots(tmpdir, snapshot_dir):
if not check_snapshots(snapshot_output_dir, snapshot_dir):
sys.exit(1)

print("All snapshot checks passed")
else:
output_dir = (
args.output_dir
if args.output_dir
else os.path.join(
get_react_native_dir(), "scripts", "cxx-api", "api-snapshots"
)
)
build_snapshots(output_dir, verbose=True)


if __name__ == "__main__":
Expand Down
Loading
Loading