diff --git a/.gitignore b/.gitignore index 009ef89eca1a..c6f76af066f3 100644 --- a/.gitignore +++ b/.gitignore @@ -179,5 +179,6 @@ fix_*.patch # Doxygen XML output used for C++ API tracking /packages/react-native/**/api/xml +/packages/react-native/**/api/codegen /packages/react-native/**/.doxygen.config.generated /scripts/cxx-api/codegen diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateNativeCode.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateNativeCode.js index d8db33f00e31..77d7c9505880 100644 --- a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateNativeCode.js +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateNativeCode.js @@ -22,9 +22,16 @@ function generateNativeCode( schemaInfos /*: $ReadOnlyArray<$FlowFixMe> */, includesGeneratedCode /*: boolean */, platform /*: string */, + forceOutputPath /*: boolean */ = false, ) /*: Array */ { return schemaInfos.map(schemaInfo => { - generateCode(outputPath, schemaInfo, includesGeneratedCode, platform); + generateCode( + outputPath, + schemaInfo, + includesGeneratedCode, + platform, + forceOutputPath, + ); }); } @@ -33,6 +40,7 @@ function generateCode( schemaInfo /*: $FlowFixMe */, includesGeneratedCode /*: boolean */, platform /*: string */, + forceOutputPath /*: boolean */ = false, ) { if (shouldSkipGenerationForFBReactNativeSpec(schemaInfo, platform)) { codegenLog( @@ -60,8 +68,9 @@ function generateCode( ); // Finally, copy artifacts to the final output directory. - const outputDir = - reactNativeCoreLibraryOutputPath(libraryName, platform) ?? outputPath; + const outputDir = forceOutputPath + ? outputPath + : (reactNativeCoreLibraryOutputPath(libraryName, platform) ?? outputPath); fs.mkdirSync(outputDir, {recursive: true}); cpSyncRecursiveIfChanged(tmpOutputDir, outputDir); codegenLog(`Generated artifacts: ${outputDir}`); diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js index 4d41c5c5e88d..780f58c03955 100644 --- a/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js @@ -66,6 +66,7 @@ function execute( optionalBaseOutputPath /*: ?string */, source /*: string */, runReactNativeCodegen /*: boolean */ = true, + forceOutputPath /*: boolean */ = false, ) { try { codegenLog(`Analyzing ${path.join(projectRoot, 'package.json')}`); @@ -146,6 +147,7 @@ function execute( ), pkgJsonIncludesGeneratedCode(pkgJson), platform, + forceOutputPath, ); } diff --git a/packages/react-native/scripts/generate-codegen-artifacts.js b/packages/react-native/scripts/generate-codegen-artifacts.js index 30ef4de65382..15996b88ea73 100644 --- a/packages/react-native/scripts/generate-codegen-artifacts.js +++ b/packages/react-native/scripts/generate-codegen-artifacts.js @@ -31,8 +31,26 @@ const argv = yargs description: 'Whether the script is invoked from an `app` or a `library`', default: 'app', }) + .option('f', { + alias: 'forceOutputPath', + description: + 'Whether to force React Native Core artifacts to output to the specified path', + type: 'boolean', + default: false, + }) .usage('Usage: $0 -p [path to app] -t [target platform] -o [output path]') .demandOption(['p', 't']).argv; -// $FlowFixMe[prop-missing] -executor.execute(argv.path, argv.targetPlatform, argv.outputPath, argv.source); +executor.execute( + // $FlowFixMe[prop-missing] + argv.path, + // $FlowFixMe[prop-missing] + argv.targetPlatform, + // $FlowFixMe[prop-missing] + argv.outputPath, + // $FlowFixMe[prop-missing] + argv.source, + true, // runReactNativeCodegen + // $FlowFixMe[prop-missing] + argv.forceOutputPath, +); diff --git a/scripts/cxx-api/config.yml b/scripts/cxx-api/config.yml index 78a1f74a9859..72a986834548 100644 --- a/scripts/cxx-api/config.yml +++ b/scripts/cxx-api/config.yml @@ -1,5 +1,5 @@ ReactCommon: - include_codegen: false + codegen: inputs: - packages/react-native/ReactCommon exclude_patterns: @@ -22,13 +22,13 @@ ReactCommon: REACT_NATIVE_PRODUCTION: 1 ReactAndroid: - include_codegen: true + codegen: + platform: android inputs: - packages/react-native/ReactCommon - packages/react-native/ReactAndroid exclude_patterns: - "*/test_utils/*" - - "*/FBReactNativeSpec*/*" - "*/platform/cxx/*" - "*/platform/windows/*" - "*/platform/macos/*" @@ -48,7 +48,8 @@ ReactAndroid: # ReactIOS? ReactApple: - include_codegen: true + codegen: + platform: ios inputs: - packages/react-native/ReactCommon - packages/react-native/React diff --git a/scripts/cxx-api/parser/__main__.py b/scripts/cxx-api/parser/__main__.py index fb3ac56dc8d6..0bd9c514b733 100644 --- a/scripts/cxx-api/parser/__main__.py +++ b/scripts/cxx-api/parser/__main__.py @@ -7,9 +7,6 @@ Entry point for running the parser package as a script. Usage: - # With codegen modules path: - python ... --codegen-path /path/to/codegen - # With output directory: python ... --output-dir /path/to/output """ @@ -73,6 +70,35 @@ def build_doxygen_config( f.write(config) +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( + [ + "node", + "./scripts/generate-codegen-artifacts.js", + "--path", + "./", + "--outputPath", + "./api/codegen", + "--targetPlatform", + platform, + "--forceOutputPath", + ], + 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") + + def build_snapshot_for_view( api_view: str, react_native_dir: str, @@ -80,11 +106,26 @@ def build_snapshot_for_view( exclude_patterns: list[str], definitions: dict[str, str | int], output_dir: str, + 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")]) + if verbose: print(f"Generating API view: {api_view}") + + 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") build_doxygen_config( @@ -95,12 +136,6 @@ def build_snapshot_for_view( input_filter=input_filter, ) - # If there is already a doxygen 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")]) - if verbose: print("Running Doxygen") if input_filter: @@ -120,6 +155,7 @@ def build_snapshot_for_view( if result.returncode != 0: if verbose: print(f"Doxygen finished with error: {result.stderr}") + sys.exit(1) else: if verbose: print("Doxygen finished successfully") @@ -152,11 +188,6 @@ def main(): type=str, help="Output directory for the snapshot", ) - parser.add_argument( - "--codegen-path", - type=str, - help="Path to codegen generated code", - ) parser.add_argument( "--check", action="store_true", @@ -194,9 +225,6 @@ def main(): if verbose: print(f"Running in directory: {react_native_package_dir}") - if verbose and args.codegen_path: - print(f"Codegen output path: {os.path.abspath(args.codegen_path)}") - input_filter_path = os.path.join( get_react_native_dir(), "scripts", @@ -216,7 +244,6 @@ def main(): snapshot_configs = parse_config_file( config_path, get_react_native_dir(), - codegen_path=args.codegen_path, ) def build_snapshots(output_dir: str, verbose: bool) -> None: @@ -229,6 +256,7 @@ def build_snapshots(output_dir: str, verbose: bool) -> None: exclude_patterns=config.exclude_patterns, definitions=config.definitions, output_dir=output_dir, + codegen_platform=config.codegen_platform, verbose=verbose, input_filter=input_filter, ) @@ -240,6 +268,7 @@ def build_snapshots(output_dir: str, verbose: bool) -> None: exclude_patterns=[], definitions={}, output_dir=output_dir, + codegen_platform=None, verbose=verbose, input_filter=input_filter, ) diff --git a/scripts/cxx-api/parser/config.py b/scripts/cxx-api/parser/config.py index 880d91ab86b1..0c04ae34dc71 100644 --- a/scripts/cxx-api/parser/config.py +++ b/scripts/cxx-api/parser/config.py @@ -36,12 +36,12 @@ class ApiViewSnapshotConfig: inputs: list[str] exclude_patterns: list[str] definitions: dict[str, str | int] + codegen_platform: str | None = None def parse_config( raw_config: dict, base_dir: str, - codegen_path: str | None = None, ) -> list[ApiViewSnapshotConfig]: """ Parse a raw config dictionary and return a flattened list of snapshot configs. @@ -62,10 +62,8 @@ def parse_config( for path in (view_config.get("inputs") or []) ] - include_codegen = view_config.get("include_codegen", False) - if include_codegen and codegen_path: - inputs.append(os.path.abspath(codegen_path)) - + codegen_config = view_config.get("codegen") or {} + codegen_platform = codegen_config.get("platform") exclude_patterns = view_config.get("exclude_patterns") or [] base_definitions = view_config.get("definitions") or {} @@ -85,6 +83,7 @@ def parse_config( inputs=inputs, exclude_patterns=exclude_patterns, definitions=base_definitions, + codegen_platform=codegen_platform, ) ) else: @@ -97,6 +96,7 @@ def parse_config( inputs=inputs, exclude_patterns=exclude_patterns, definitions=merged_definitions, + codegen_platform=codegen_platform, ) ) @@ -106,7 +106,6 @@ def parse_config( def parse_config_file( config_path: str, base_dir: str, - codegen_path: str | None = None, ) -> list[ApiViewSnapshotConfig]: """ Parse the config.yml file and return a flattened list of snapshot configs. @@ -122,4 +121,4 @@ def parse_config_file( with open(config_path, "r") as stream: raw_config = yaml.safe_load(stream) - return parse_config(raw_config, base_dir, codegen_path) + return parse_config(raw_config, base_dir) diff --git a/scripts/cxx-api/tests/test_config.py b/scripts/cxx-api/tests/test_config.py index d34b4dc0a5dd..17090b4eddfe 100644 --- a/scripts/cxx-api/tests/test_config.py +++ b/scripts/cxx-api/tests/test_config.py @@ -26,7 +26,7 @@ def test_single_view_no_variants(self): """Single view without variants""" config = { "ReactCommon": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": ["packages/react-native/ReactCommon"], "exclude_patterns": ["*/jni/*"], "definitions": {"FOO": 1}, @@ -46,7 +46,7 @@ def test_single_view_with_variants(self): """Single view with debug and release variants""" config = { "ReactCommon": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": ["packages/react-native/ReactCommon"], "exclude_patterns": [], "definitions": {}, @@ -78,7 +78,7 @@ def test_base_definitions_merged_with_variant(self): """Base definitions are merged with variant definitions""" config = { "ReactAndroid": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {"RN_SERIALIZABLE_STATE": 1, "ANDROID": 1}, @@ -108,7 +108,7 @@ def test_variant_definitions_override_base(self): """Variant definitions override base definitions with same key""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {"MODE": "base", "SHARED": 1}, @@ -129,7 +129,7 @@ def test_empty_variant_definitions(self): """Variant with empty definitions still inherits base""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {"BASE": 1}, @@ -147,7 +147,7 @@ def test_none_variant_definitions(self): """Variant with None definitions still inherits base""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {"BASE": 1}, @@ -169,7 +169,7 @@ def test_relative_paths_resolved(self): """Relative paths are joined with base_dir""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": ["packages/foo", "packages/bar"], "exclude_patterns": [], "definitions": {}, @@ -186,7 +186,7 @@ def test_absolute_paths_preserved(self): """Absolute paths are not modified""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": ["/absolute/path/foo", "relative/path"], "exclude_patterns": [], "definitions": {}, @@ -203,7 +203,7 @@ def test_empty_inputs(self): """Empty inputs list""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {}, @@ -217,7 +217,7 @@ def test_none_inputs(self): """None inputs treated as empty list""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": None, "exclude_patterns": [], "definitions": {}, @@ -231,49 +231,67 @@ def test_none_inputs(self): # Codegen path handling # ========================================================================= - def test_codegen_path_added_when_include_codegen_true(self): - """Codegen path is appended when include_codegen is true""" + def test_codegen_platform_set_when_generate_true(self): + """codegen_platform is set when codegen.generate is true""" config = { "TestView": { - "include_codegen": True, - "inputs": ["packages/foo"], + "codegen": {"generate": True, "platform": "android"}, + "inputs": [], "exclude_patterns": [], "definitions": {}, } } - result = parse_config(config, "/base/dir", codegen_path="/codegen/path") + result = parse_config(config, "/base/dir") - self.assertIn("/codegen/path", result[0].inputs) - self.assertEqual(len(result[0].inputs), 2) + self.assertEqual(result[0].codegen_platform, "android") - def test_codegen_path_not_added_when_include_codegen_false(self): - """Codegen path is not added when include_codegen is false""" + def test_codegen_platform_ios(self): + """codegen_platform correctly stores ios platform""" config = { "TestView": { - "include_codegen": False, - "inputs": ["packages/foo"], + "codegen": {"generate": True, "platform": "ios"}, + "inputs": [], "exclude_patterns": [], "definitions": {}, } } - result = parse_config(config, "/base/dir", codegen_path="/codegen/path") + result = parse_config(config, "/base/dir") - self.assertNotIn("/codegen/path", result[0].inputs) - self.assertEqual(len(result[0].inputs), 1) + self.assertEqual(result[0].codegen_platform, "ios") - def test_codegen_path_not_added_when_none(self): - """Codegen path not added when codegen_path is None""" + def test_codegen_platform_propagated_to_variants(self): + """codegen_platform is propagated to all variant configs""" config = { "TestView": { - "include_codegen": True, - "inputs": ["packages/foo"], + "codegen": {"generate": True, "platform": "android"}, + "inputs": [], "exclude_patterns": [], "definitions": {}, + "variants": { + "debug": {"definitions": {"DEBUG": 1}}, + "release": {"definitions": {"NDEBUG": 1}}, + }, } } - result = parse_config(config, "/base/dir", codegen_path=None) + result = parse_config(config, "/base/dir") + + self.assertEqual(len(result), 2) + for r in result: + self.assertEqual(r.codegen_platform, "android") + + def test_codegen_missing_defaults_to_no_codegen(self): + """Missing codegen config defaults to no codegen""" + config = { + "TestView": { + "inputs": [], + "exclude_patterns": [], + "definitions": {}, + } + } + result = parse_config(config, "/base/dir") - self.assertEqual(len(result[0].inputs), 1) + self.assertEqual(len(result[0].inputs), 0) + self.assertIsNone(result[0].codegen_platform) # ========================================================================= # Multiple views @@ -283,13 +301,13 @@ def test_multiple_views(self): """Multiple views are all parsed""" config = { "ViewA": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {"A": 1}, }, "ViewB": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {"B": 1}, @@ -305,7 +323,7 @@ def test_multiple_views_with_variants(self): """Multiple views each with variants""" config = { "ViewA": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {}, @@ -315,7 +333,7 @@ def test_multiple_views_with_variants(self): }, }, "ViewB": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {}, @@ -338,7 +356,7 @@ def test_exclude_patterns_preserved(self): """Exclude patterns are passed through unchanged""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": ["*/jni/*", "*/platform/ios/*"], "definitions": {}, @@ -355,7 +373,7 @@ def test_none_exclude_patterns(self): """None exclude_patterns treated as empty list""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": None, "definitions": {}, @@ -373,7 +391,7 @@ def test_variant_name_capitalized(self): """Variant names are capitalized in snapshot name""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": {}, @@ -408,7 +426,7 @@ def test_none_definitions(self): """None definitions treated as empty dict""" config = { "TestView": { - "include_codegen": False, + "codegen": {"generate": False}, "inputs": [], "exclude_patterns": [], "definitions": None,