Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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