From ebb2b9c3fce227135f437bd86db71125eebcf21f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Tue, 23 Jan 2024 06:53:57 -0800 Subject: [PATCH] feat: make codegen take OOT Apple platforms into account (#42047) Summary: ### The problem 1. We have a library that's supported on iOS but doesn't have support for visionOS. 2. We run pod install 3. Codegen runs and generates Code for this library and tries to reference library class in `RCTThirdPartyFabricComponentsProvider` 4. Example: ```objc Class RNCSafeAreaProviderCls(void) __attribute__((used)); // 0 ``` This is an issue because the library files are not linked for visionOS platform (because code is linked only for iOS due to pod supporting only iOS). ### Solution Make codegen take Apple OOT platforms into account by adding compiler macros if the given platform doesn't explicitly support this platform in the native package's podspec file. Example generated output for library supporting only `ios` and `visionos` in podspec: ![CleanShot 2023-12-22 at 15 48 22@2x](https://github.com/facebook/react-native/assets/52801365/0cdfe7f5-441d-4466-8713-5f65feef26e7) I used compiler conditionals because not every platform works the same, and if in the future let's say react-native-visionos were merged upstream compiler conditionals would still work. Also tvOS uses Xcode targets to differentiate which platform it builds so conditionally adding things to the generated file wouldn't work. ## Changelog: [IOS] [ADDED] - make codegen take OOT Apple platforms into account Pull Request resolved: https://github.com/facebook/react-native/pull/42047 Test Plan: 1. Generate a sample app with a template 5. Add third-party library (In my case it was https://github.com/callstack/react-native-slider) 6. Check if generated codegen code includes compiler macros Reviewed By: cipolleschi Differential Revision: D52656076 Pulled By: dmytrorykun fbshipit-source-id: c827f358997c70a3c49f80c55915c28bdab9b97f --- .../src/generators/RNCodegen.js | 19 +++-- .../components/ComponentsProviderUtils.js | 50 +++++++++++ ...rateThirdPartyFabricComponentsProviderH.js | 18 +++- ...hirdPartyFabricComponentsProviderObjCpp.js | 22 ++++- ...abricComponentsProviderObjCpp-test.js.snap | 3 +- .../test-library-2/test-library-2.podspec | 12 +++ .../test-library/test-library.podspec | 10 +++ .../generate-artifacts-executor-test.js | 38 +++++++++ .../codegen/generate-artifacts-executor.js | 82 +++++++++++++++++-- 9 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 packages/react-native-codegen/src/generators/components/ComponentsProviderUtils.js create mode 100644 packages/react-native/scripts/codegen/__test_fixtures__/test-library-2/test-library-2.podspec create mode 100644 packages/react-native/scripts/codegen/__test_fixtures__/test-library/test-library.podspec diff --git a/packages/react-native-codegen/src/generators/RNCodegen.js b/packages/react-native-codegen/src/generators/RNCodegen.js index ea9f7398d94b4e..5b7297f2ac0382 100644 --- a/packages/react-native-codegen/src/generators/RNCodegen.js +++ b/packages/react-native-codegen/src/generators/RNCodegen.js @@ -83,6 +83,7 @@ type LibraryOptions = $ReadOnly<{ type SchemasOptions = $ReadOnly<{ schemas: {[string]: SchemaType}, outputDirectory: string, + supportedApplePlatforms?: {[string]: {[string]: boolean}}, }>; type LibraryGenerators = @@ -289,7 +290,7 @@ module.exports = { return checkOrWriteFiles(generatedFiles, test); }, generateFromSchemas( - {schemas, outputDirectory}: SchemasOptions, + {schemas, outputDirectory, supportedApplePlatforms}: SchemasOptions, {generators, test}: SchemasConfig, ): boolean { Object.keys(schemas).forEach(libraryName => @@ -300,13 +301,15 @@ module.exports = { for (const name of generators) { for (const generator of SCHEMAS_GENERATORS[name]) { - generator(schemas).forEach((contents: string, fileName: string) => { - generatedFiles.push({ - name: fileName, - content: contents, - outputDir: outputDirectory, - }); - }); + generator(schemas, supportedApplePlatforms).forEach( + (contents: string, fileName: string) => { + generatedFiles.push({ + name: fileName, + content: contents, + outputDir: outputDirectory, + }); + }, + ); } } return checkOrWriteFiles(generatedFiles, test); diff --git a/packages/react-native-codegen/src/generators/components/ComponentsProviderUtils.js b/packages/react-native-codegen/src/generators/components/ComponentsProviderUtils.js new file mode 100644 index 00000000000000..bb050eec7c3f6e --- /dev/null +++ b/packages/react-native-codegen/src/generators/components/ComponentsProviderUtils.js @@ -0,0 +1,50 @@ +/** + * 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. + * + * @flow strict + * @format + */ + +const APPLE_PLATFORMS_MACRO_MAP = { + ios: 'TARGET_OS_IOS', + macos: 'TARGET_OS_OSX', + tvos: 'TARGET_OS_TV', + visionos: 'TARGET_OS_VISION', +}; + +/** + * Adds compiler macros to the file template to exclude unsupported platforms. + */ +function generateSupportedApplePlatformsMacro( + fileTemplate: string, + supportedPlatformsMap: ?{[string]: boolean}, +): string { + if (!supportedPlatformsMap) { + return fileTemplate; + } + + const compilerMacroString = Object.keys(supportedPlatformsMap) + .reduce((acc: string[], platform) => { + if (!supportedPlatformsMap[platform]) { + return [...acc, `!${APPLE_PLATFORMS_MACRO_MAP[platform]}`]; + } + return acc; + }, []) + .join(' && '); + + if (!compilerMacroString) { + return fileTemplate; + } + + return `#if ${compilerMacroString} +${fileTemplate} +#endif +`; +} + +module.exports = { + generateSupportedApplePlatformsMacro, +}; diff --git a/packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderH.js b/packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderH.js index 6598ba357f99b0..cc42edb5a04b1e 100644 --- a/packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderH.js +++ b/packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderH.js @@ -12,6 +12,10 @@ import type {SchemaType} from '../../CodegenSchema'; +const { + generateSupportedApplePlatformsMacro, +} = require('./ComponentsProviderUtils'); + // File path -> contents type FilesOutput = Map; @@ -63,13 +67,18 @@ Class ${className}Cls(void) __attribute__((used)); // `.trim(); module.exports = { - generate(schemas: {[string]: SchemaType}): FilesOutput { + generate( + schemas: {[string]: SchemaType}, + supportedApplePlatforms?: {[string]: {[string]: boolean}}, + ): FilesOutput { const fileName = 'RCTThirdPartyFabricComponentsProvider.h'; const lookupFuncs = Object.keys(schemas) .map(libraryName => { const schema = schemas[libraryName]; - return Object.keys(schema.modules) + const librarySupportedApplePlatforms = + supportedApplePlatforms?.[libraryName]; + const generatedLookup = Object.keys(schema.modules) .map(moduleName => { const module = schema.modules[moduleName]; if (module.type !== 'Component') { @@ -100,6 +109,11 @@ module.exports = { }) .filter(Boolean) .join('\n'); + + return generateSupportedApplePlatformsMacro( + generatedLookup, + librarySupportedApplePlatforms, + ); }) .join('\n'); diff --git a/packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js b/packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js index ced1c4e908f39f..ad99c964437eaa 100644 --- a/packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js +++ b/packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js @@ -12,6 +12,10 @@ import type {SchemaType} from '../../CodegenSchema'; +const { + generateSupportedApplePlatformsMacro, +} = require('./ComponentsProviderUtils'); + // File path -> contents type FilesOutput = Map; @@ -60,13 +64,19 @@ const LookupMapTemplate = ({ {"${className}", ${className}Cls}, // ${libraryName}`; module.exports = { - generate(schemas: {[string]: SchemaType}): FilesOutput { + generate( + schemas: {[string]: SchemaType}, + supportedApplePlatforms?: {[string]: {[string]: boolean}}, + ): FilesOutput { const fileName = 'RCTThirdPartyFabricComponentsProvider.mm'; const lookupMap = Object.keys(schemas) .map(libraryName => { const schema = schemas[libraryName]; - return Object.keys(schema.modules) + const librarySupportedApplePlatforms = + supportedApplePlatforms?.[libraryName]; + + const generatedLookup = Object.keys(schema.modules) .map(moduleName => { const module = schema.modules[moduleName]; if (module.type !== 'Component') { @@ -98,7 +108,13 @@ module.exports = { return componentTemplates.length > 0 ? componentTemplates : null; }) - .filter(Boolean); + .filter(Boolean) + .join('\n'); + + return generateSupportedApplePlatformsMacro( + generatedLookup, + librarySupportedApplePlatforms, + ); }) .join('\n'); diff --git a/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateThirdPartyFabricComponentsProviderObjCpp-test.js.snap b/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateThirdPartyFabricComponentsProviderObjCpp-test.js.snap index 89d82d1eedb55d..7543550f9946f4 100644 --- a/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateThirdPartyFabricComponentsProviderObjCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateThirdPartyFabricComponentsProviderObjCpp-test.js.snap @@ -71,7 +71,8 @@ Class RCTThirdPartyFabricComponentsProvider(const char {\\"MultiComponent1NativeComponent\\", MultiComponent1NativeComponentCls}, // TWO_COMPONENTS_SAME_FILE, {\\"MultiComponent2NativeComponent\\", MultiComponent2NativeComponentCls}, // TWO_COMPONENTS_SAME_FILE - {\\"MultiFile1NativeComponent\\", MultiFile1NativeComponentCls}, // TWO_COMPONENTS_DIFFERENT_FILES, + {\\"MultiFile1NativeComponent\\", MultiFile1NativeComponentCls}, // TWO_COMPONENTS_DIFFERENT_FILES + {\\"MultiFile2NativeComponent\\", MultiFile2NativeComponentCls}, // TWO_COMPONENTS_DIFFERENT_FILES {\\"CommandNativeComponent\\", CommandNativeComponentCls}, // COMMANDS diff --git a/packages/react-native/scripts/codegen/__test_fixtures__/test-library-2/test-library-2.podspec b/packages/react-native/scripts/codegen/__test_fixtures__/test-library-2/test-library-2.podspec new file mode 100644 index 00000000000000..0e292a8a55c465 --- /dev/null +++ b/packages/react-native/scripts/codegen/__test_fixtures__/test-library-2/test-library-2.podspec @@ -0,0 +1,12 @@ +# 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. + +Pod::Spec.new do |s| + s.name = "test-library-2" + s.version = "0.0.0" + s.ios.deployment_target = "9.0" + s.osx.deployment_target = "13.0" + s.tvos.deployment_target = "1.0" +end diff --git a/packages/react-native/scripts/codegen/__test_fixtures__/test-library/test-library.podspec b/packages/react-native/scripts/codegen/__test_fixtures__/test-library/test-library.podspec new file mode 100644 index 00000000000000..e1697ec5efca9a --- /dev/null +++ b/packages/react-native/scripts/codegen/__test_fixtures__/test-library/test-library.podspec @@ -0,0 +1,10 @@ +# 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. + +Pod::Spec.new do |s| + s.name = "test-library" + s.version = "0.0.0" + s.platforms = { :ios => "9.0", :osx => "13.0", visionos: "1.0" } +end diff --git a/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js b/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js index 79fa376c6ade0b..6c091f18b9868e 100644 --- a/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js +++ b/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js @@ -87,6 +87,44 @@ describe('extractLibrariesFromJSON', () => { }); }); +describe('extractSupportedApplePlatforms', () => { + it('extracts platforms when podspec specifies object of platforms', () => { + const myDependency = 'test-library'; + const myDependencyPath = path.join( + __dirname, + `../__test_fixtures__/${myDependency}`, + ); + let platforms = underTest._extractSupportedApplePlatforms( + myDependency, + myDependencyPath, + ); + expect(platforms).toEqual({ + ios: true, + macos: true, + tvos: false, + visionos: true, + }); + }); + + it('extracts platforms when podspec specifies platforms separately', () => { + const myDependency = 'test-library-2'; + const myDependencyPath = path.join( + __dirname, + `../__test_fixtures__/${myDependency}`, + ); + let platforms = underTest._extractSupportedApplePlatforms( + myDependency, + myDependencyPath, + ); + expect(platforms).toEqual({ + ios: true, + macos: true, + tvos: true, + visionos: false, + }); + }); +}); + describe('delete empty files and folders', () => { beforeEach(() => { jest.resetModules(); diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor.js b/packages/react-native/scripts/codegen/generate-artifacts-executor.js index 890384539743d9..cf0f594affc7a3 100644 --- a/packages/react-native/scripts/codegen/generate-artifacts-executor.js +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor.js @@ -20,6 +20,7 @@ const utils = require('./codegen-utils'); const generateSpecsCLIExecutor = require('./generate-specs-cli-executor'); const {execSync} = require('child_process'); const fs = require('fs'); +const glob = require('glob'); const mkdirp = require('mkdirp'); const os = require('os'); const path = require('path'); @@ -144,6 +145,63 @@ function extractLibrariesFromJSON(configFile, dependencyPath) { } } +const APPLE_PLATFORMS = ['ios', 'macos', 'tvos', 'visionos']; + +// Cocoapods specific platform keys +function getCocoaPodsPlatformKey(platformName) { + if (platformName === 'macos') { + return 'osx'; + } + return platformName; +} + +function extractSupportedApplePlatforms(dependency, dependencyPath) { + console.log('[Codegen] Searching for podspec in the project dependencies.'); + const podspecs = glob.sync('*.podspec', {cwd: dependencyPath}); + + if (podspecs.length === 0) { + return; + } + + // Take the first podspec found + const podspec = fs.readFileSync( + path.join(dependencyPath, podspecs[0]), + 'utf8', + ); + + /** + * Podspec can have platforms defined in two ways: + * 1. `spec.platforms = { :ios => "11.0", :tvos => "11.0" }` + * 2. `s.ios.deployment_target = "11.0"` + * `s.tvos.deployment_target = "11.0"` + */ + const supportedPlatforms = podspec + .split('\n') + .filter( + line => line.includes('platform') || line.includes('deployment_target'), + ) + .join(''); + + // Generate a map of supported platforms { [platform]: true/false } + const supportedPlatformsMap = APPLE_PLATFORMS.reduce( + (acc, platform) => ({ + ...acc, + [platform]: supportedPlatforms.includes( + getCocoaPodsPlatformKey(platform), + ), + }), + {}, + ); + + console.log( + `[Codegen] Supported Apple platforms: ${Object.keys(supportedPlatformsMap) + .filter(key => supportedPlatformsMap[key]) + .join(', ')} for ${dependency}`, + ); + + return supportedPlatformsMap; +} + function findExternalLibraries(pkgJson) { const dependencies = { ...pkgJson.dependencies, @@ -276,9 +334,16 @@ function generateSchemaInfo(library, platform) { library.config.jsSrcsDir, ); console.log(`[Codegen] Processing ${library.config.name}`); + + const supportedApplePlatforms = extractSupportedApplePlatforms( + library.config.name, + library.libraryPath, + ); + // Generate one schema for the entire library... return { library: library, + supportedApplePlatforms, schema: utils .getCombineJSToSchema() .combineSchemasInFileList( @@ -356,7 +421,7 @@ function mustGenerateNativeCode(includeLibraryPath, schemaInfo) { ); } -function createComponentProvider(schemas) { +function createComponentProvider(schemas, supportedApplePlatforms) { console.log('[Codegen] Creating component provider.'); const outputDir = path.join( REACT_NATIVE_PACKAGE_ROOT_FOLDER, @@ -368,6 +433,7 @@ function createComponentProvider(schemas) { { schemas: schemas, outputDirectory: outputDir, + supportedApplePlatforms, }, { generators: ['providerIOS'], @@ -487,10 +553,15 @@ function execute(projectRoot, targetPlatform, baseOutputPath) { if ( rootCodegenTargetNeedsThirdPartyComponentProvider(pkgJson, platform) ) { - const schemas = schemaInfos - .filter(dependencyNeedsThirdPartyComponentProvider) - .map(schemaInfo => schemaInfo.schema); - createComponentProvider(schemas); + const filteredSchemas = schemaInfos.filter( + dependencyNeedsThirdPartyComponentProvider, + ); + const schemas = filteredSchemas.map(schemaInfo => schemaInfo.schema); + const supportedApplePlatforms = filteredSchemas.map( + schemaInfo => schemaInfo.supportedApplePlatforms, + ); + + createComponentProvider(schemas, supportedApplePlatforms); } cleanupEmptyFilesAndFolders(outputPath); } @@ -508,4 +579,5 @@ module.exports = { // exported for testing purposes only: _extractLibrariesFromJSON: extractLibrariesFromJSON, _cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders, + _extractSupportedApplePlatforms: extractSupportedApplePlatforms, };