Skip to content

Commit f246d46

Browse files
j-piaseckimeta-codesync[bot]
authored andcommitted
Integrate codegen into the api snapshot and allow configuration (#55642)
Summary: Pull Request resolved: #55642 Changelog: [Internal] Adds a configuration file for the snapshot generation, allowing to create multiple views of the API and different flavors. It also adds option to include codegen in the snapshot generation. Codegen needs to be executed first, and the path to its output needs to be passed when running the generation. Reviewed By: cipolleschi Differential Revision: D92958731
1 parent b5338d5 commit f246d46

File tree

6 files changed

+727
-35
lines changed

6 files changed

+727
-35
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,4 @@ fix_*.patch
180180
# Doxygen XML output used for C++ API tracking
181181
/packages/react-native/**/api/xml
182182
/packages/react-native/**/.doxygen.config.generated
183+
/scripts/cxx-api/codegen

packages/react-native/.doxygen.config.template

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ WARN_LOGFILE =
991991
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
992992
# Note: If this tag is empty the current directory is searched.
993993

994-
INPUT = ReactCommon ${ADDITIONAL_INPUTS}
994+
INPUT = ${INPUTS}
995995

996996
# This tag can be used to specify the character encoding of the source files
997997
# that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses
@@ -1063,10 +1063,7 @@ EXCLUDE_SYMLINKS = NO
10631063
# exclude all test directories for example use the pattern */test/*
10641064

10651065
EXCLUDE_PATTERNS = */tests/* \
1066-
*/samples/* \
1067-
*/platform/windows/* \
1068-
*/platform/macos/* \
1069-
*/platform/cxx/* ${ADDITIONAL_EXCLUDE_PATTERNS}
1066+
*/samples/* \ ${EXCLUDE_PATTERNS}
10701067

10711068
# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
10721069
# (namespaces, classes, functions, etc.) that should be excluded from the

scripts/cxx-api/config.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
ReactCommon:
2+
include_codegen: false
3+
inputs:
4+
- packages/react-native/ReactCommon
5+
exclude_patterns:
6+
- "*/test_utils/*"
7+
- "*/jni/*"
8+
- "*/platform/cxx/*"
9+
- "*/platform/windows/*"
10+
- "*/platform/macos/*"
11+
- "*/platform/ios/*"
12+
- "*/platform/android/*"
13+
definitions:
14+
variants:
15+
debug:
16+
definitions:
17+
DEBUG: 1
18+
REACT_NATIVE_DEBUG: 1
19+
release:
20+
definitions:
21+
NDEBUG: 1
22+
REACT_NATIVE_PRODUCTION: 1
23+
24+
ReactAndroid:
25+
include_codegen: true
26+
inputs:
27+
- packages/react-native/ReactCommon
28+
- packages/react-native/ReactAndroid
29+
exclude_patterns:
30+
- "*/test_utils/*"
31+
- "*/FBReactNativeSpec*/*"
32+
- "*/platform/cxx/*"
33+
- "*/platform/windows/*"
34+
- "*/platform/macos/*"
35+
- "*/platform/ios/*"
36+
definitions:
37+
RN_SERIALIZABLE_STATE: 1
38+
ANDROID: 1
39+
variants:
40+
debug:
41+
definitions:
42+
DEBUG: 1
43+
REACT_NATIVE_DEBUG: 1
44+
release:
45+
definitions:
46+
NDEBUG: 1
47+
REACT_NATIVE_PRODUCTION: 1
48+
49+
# ReactIOS?
50+
ReactApple:
51+
include_codegen: true
52+
inputs:
53+
- packages/react-native/ReactCommon
54+
- packages/react-native/React
55+
- packages/react-native/ReactApple
56+
exclude_patterns:
57+
- "*/test_utils/*"
58+
- "*/jni/*"
59+
- "*/platform/cxx/*"
60+
- "*/platform/windows/*"
61+
- "*/platform/macos/*"
62+
- "*/platform/android/*"
63+
definitions:
64+
variants:
65+
debug:
66+
definitions:
67+
DEBUG: 1
68+
REACT_NATIVE_DEBUG: 1
69+
release:
70+
definitions:
71+
NDEBUG: 1
72+
REACT_NATIVE_PRODUCTION: 1

scripts/cxx-api/parser/__main__.py

Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@
55

66
"""
77
Entry point for running the parser package as a script.
8+
9+
Usage:
10+
# With codegen modules path:
11+
python ... --codegen-path /path/to/codegen
12+
13+
# With output directory:
14+
python ... --output-dir /path/to/output
815
"""
916

17+
import argparse
1018
import os
1119
import subprocess
1220

21+
from .config import parse_config_file
1322
from .main import build_snapshot
1423
from .path_utils import get_react_native_dir
1524

@@ -49,8 +58,8 @@ def build_doxygen_config(
4958

5059
# replace the placeholder with the actual path
5160
config = (
52-
template.replace("${ADDITIONAL_INPUTS}", include_directories_str)
53-
.replace("${ADDITIONAL_EXCLUDE_PATTERNS}", exclude_patterns_str)
61+
template.replace("${INPUTS}", include_directories_str)
62+
.replace("${EXCLUDE_PATTERNS}", exclude_patterns_str)
5463
.replace("${PREDEFINED}", definitions_str)
5564
)
5665

@@ -59,34 +68,32 @@ def build_doxygen_config(
5968
f.write(config)
6069

6170

62-
def main():
63-
# Define the path to the react-native directory
64-
react_native_dir = (
65-
f"{get_react_native_dir()}/packages/react-native"
66-
if RUN_ON_REACT_NATIVE
67-
else f"{get_react_native_dir()}/scripts/cxx-api/manual_test"
71+
def build_snapshot_for_view(
72+
api_view: str,
73+
react_native_dir: str,
74+
include_directories: list[str],
75+
exclude_patterns: list[str],
76+
definitions: dict[str, str | int],
77+
output_dir: str,
78+
) -> None:
79+
print(f"Generating API view: {api_view}")
80+
# Generate the Doxygen config file
81+
print("Generating Doxygen config file")
82+
83+
build_doxygen_config(
84+
react_native_dir,
85+
include_directories=include_directories,
86+
exclude_patterns=exclude_patterns,
87+
definitions=definitions,
6888
)
69-
print(f"Running in directory: {react_native_dir}")
7089

71-
# If there is already an output directory, delete it
90+
print("Running Doxygen")
91+
92+
# If there is already a doxygen output directory, delete it
7293
if os.path.exists(os.path.join(react_native_dir, "api")):
7394
print("Deleting existing output directory")
7495
subprocess.run(["rm", "-rf", os.path.join(react_native_dir, "api")])
7596

76-
# Generate the Doxygen config file
77-
print("Generating Doxygen config file")
78-
if RUN_ON_REACT_NATIVE:
79-
build_doxygen_config(
80-
react_native_dir,
81-
include_directories=["ReactAndroid"],
82-
exclude_patterns=["*/platform/ios/*"],
83-
definitions={"RN_SERIALIZABLE_STATE": 1},
84-
)
85-
else:
86-
build_doxygen_config(react_native_dir)
87-
88-
print("Running Doxygen")
89-
9097
# Run doxygen with the config file
9198
result = subprocess.run(
9299
["doxygen", DOXYGEN_CONFIG_FILE],
@@ -105,14 +112,81 @@ def main():
105112
else:
106113
print("Doxygen finished successfully")
107114

115+
# build snapshot, convert to string, and save to file
116+
snapshot = build_snapshot(os.path.join(react_native_dir, "api", "xml"))
117+
snapshot_string = snapshot.to_string()
118+
119+
output_file = os.path.join(output_dir, f"{api_view}Cxx.api")
120+
os.makedirs(output_dir, exist_ok=True)
121+
122+
with open(output_file, "w") as f:
123+
f.write("// @generated by scripts/cxx-api\n\n")
124+
f.write(snapshot_string)
125+
126+
print(f"Snapshot written to: {os.path.abspath(output_file)}")
127+
128+
if not RUN_ON_REACT_NATIVE:
129+
print(snapshot_string)
130+
131+
132+
def main():
133+
parser = argparse.ArgumentParser(
134+
description="Generate API snapshots from C++ headers"
135+
)
136+
parser.add_argument(
137+
"--output-dir",
138+
type=str,
139+
help="Output directory for the snapshot",
140+
)
141+
parser.add_argument(
142+
"--codegen-path",
143+
type=str,
144+
help="Path to codegen generated code",
145+
)
146+
args = parser.parse_args()
147+
148+
# Define the path to the react-native directory
149+
react_native_dir = (
150+
os.path.join(get_react_native_dir(), "packages", "react-native")
151+
if RUN_ON_REACT_NATIVE
152+
else os.path.join(get_react_native_dir(), "scripts", "cxx-api", "manual_test")
153+
)
154+
print(f"Running in directory: {react_native_dir}")
155+
156+
if args.codegen_path:
157+
print(f"Codegen output path: {os.path.abspath(args.codegen_path)}")
158+
159+
output_dir = args.output_dir if args.output_dir else react_native_dir
160+
161+
# Parse config file
162+
config_path = os.path.join(
163+
get_react_native_dir(), "scripts", "cxx-api", "config.yml"
164+
)
165+
snapshot_configs = parse_config_file(
166+
config_path,
167+
get_react_native_dir(),
168+
codegen_path=args.codegen_path,
169+
)
170+
108171
if RUN_ON_REACT_NATIVE:
109-
# build snapshot, convert to string, and save to file
110-
snapshot = build_snapshot(os.path.join(react_native_dir, "api", "xml"))
111-
snapshot_string = snapshot.to_string()
112-
with open(os.path.join(react_native_dir, "api", "snapshot.api"), "w") as f:
113-
f.write(snapshot_string)
172+
for config in snapshot_configs:
173+
build_snapshot_for_view(
174+
api_view=config.snapshot_name,
175+
react_native_dir=react_native_dir,
176+
include_directories=config.inputs,
177+
exclude_patterns=config.exclude_patterns,
178+
definitions=config.definitions,
179+
output_dir=output_dir,
180+
)
114181
else:
115-
build_snapshot(os.path.join(react_native_dir, "api", "xml")).print()
182+
build_snapshot_for_view(
183+
api_view="Test",
184+
react_native_dir=react_native_dir,
185+
include_directories=[],
186+
exclude_patterns=[],
187+
definitions={},
188+
output_dir=output_dir,
189+
)
116190

117191

118192
if __name__ == "__main__":

scripts/cxx-api/parser/config.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
"""
7+
Config parsing module for C++ API snapshot generation.
8+
9+
This module handles parsing of the config.yml file and provides
10+
structured objects representing API views and their variants.
11+
"""
12+
13+
import os
14+
from dataclasses import dataclass, field
15+
16+
import yaml
17+
18+
19+
@dataclass
20+
class ApiViewVariant:
21+
"""Represents a build variant (e.g., debug, release) for an API view."""
22+
23+
name: str
24+
definitions: dict[str, str | int] = field(default_factory=dict)
25+
26+
27+
@dataclass
28+
class ApiViewSnapshotConfig:
29+
"""
30+
A fully resolved configuration for generating a single snapshot.
31+
32+
This combines the base view settings with variant-specific definitions.
33+
"""
34+
35+
snapshot_name: str
36+
inputs: list[str]
37+
exclude_patterns: list[str]
38+
definitions: dict[str, str | int]
39+
40+
41+
def parse_config(
42+
raw_config: dict,
43+
base_dir: str,
44+
codegen_path: str | None = None,
45+
) -> list[ApiViewSnapshotConfig]:
46+
"""
47+
Parse a raw config dictionary and return a flattened list of snapshot configs.
48+
49+
Args:
50+
raw_config: Dictionary containing the parsed YAML config
51+
base_dir: Base directory for resolving relative input paths
52+
codegen_path: Optional path to codegen generated code
53+
54+
Returns:
55+
Flattened list of ApiViewSnapshotConfig objects for all views and variants
56+
"""
57+
snapshot_configs = []
58+
59+
for view_name, view_config in raw_config.items():
60+
inputs = [
61+
path if path.startswith("/") else os.path.join(base_dir, path)
62+
for path in (view_config.get("inputs") or [])
63+
]
64+
65+
include_codegen = view_config.get("include_codegen", False)
66+
if include_codegen and codegen_path:
67+
inputs.append(os.path.abspath(codegen_path))
68+
69+
exclude_patterns = view_config.get("exclude_patterns") or []
70+
base_definitions = view_config.get("definitions") or {}
71+
72+
raw_variants = view_config.get("variants") or {}
73+
variants = [
74+
ApiViewVariant(
75+
name=variant_name,
76+
definitions=variant_config.get("definitions") or {},
77+
)
78+
for variant_name, variant_config in raw_variants.items()
79+
]
80+
81+
if not variants:
82+
snapshot_configs.append(
83+
ApiViewSnapshotConfig(
84+
snapshot_name=view_name,
85+
inputs=inputs,
86+
exclude_patterns=exclude_patterns,
87+
definitions=base_definitions,
88+
)
89+
)
90+
else:
91+
for variant in variants:
92+
merged_definitions = {**base_definitions, **variant.definitions}
93+
variant_suffix = variant.name.capitalize()
94+
snapshot_configs.append(
95+
ApiViewSnapshotConfig(
96+
snapshot_name=f"{view_name}{variant_suffix}",
97+
inputs=inputs,
98+
exclude_patterns=exclude_patterns,
99+
definitions=merged_definitions,
100+
)
101+
)
102+
103+
return snapshot_configs
104+
105+
106+
def parse_config_file(
107+
config_path: str,
108+
base_dir: str,
109+
codegen_path: str | None = None,
110+
) -> list[ApiViewSnapshotConfig]:
111+
"""
112+
Parse the config.yml file and return a flattened list of snapshot configs.
113+
114+
Args:
115+
config_path: Path to the config.yml file
116+
base_dir: Base directory for resolving relative input paths
117+
codegen_path: Optional path to codegen generated code
118+
119+
Returns:
120+
Flattened list of ApiViewSnapshotConfig objects for all views and variants
121+
"""
122+
with open(config_path, "r") as stream:
123+
raw_config = yaml.safe_load(stream)
124+
125+
return parse_config(raw_config, base_dir, codegen_path)

0 commit comments

Comments
 (0)