Skip to content

Commit

Permalink
feat: make codegen take OOT Apple platforms into account (#42047)
Browse files Browse the repository at this point in the history
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<RCTComponentViewProtocol> 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: #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
  • Loading branch information
okwasniewski authored and facebook-github-bot committed Jan 23, 2024
1 parent a13d51f commit ebb2b9c
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 19 deletions.
19 changes: 11 additions & 8 deletions packages/react-native-codegen/src/generators/RNCodegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type LibraryOptions = $ReadOnly<{
type SchemasOptions = $ReadOnly<{
schemas: {[string]: SchemaType},
outputDirectory: string,
supportedApplePlatforms?: {[string]: {[string]: boolean}},
}>;

type LibraryGenerators =
Expand Down Expand Up @@ -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 =>
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

import type {SchemaType} from '../../CodegenSchema';

const {
generateSupportedApplePlatformsMacro,
} = require('./ComponentsProviderUtils');

// File path -> contents
type FilesOutput = Map<string, string>;

Expand Down Expand Up @@ -63,13 +67,18 @@ Class<RCTComponentViewProtocol> ${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') {
Expand Down Expand Up @@ -100,6 +109,11 @@ module.exports = {
})
.filter(Boolean)
.join('\n');

return generateSupportedApplePlatformsMacro(
generatedLookup,
librarySupportedApplePlatforms,
);
})
.join('\n');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

import type {SchemaType} from '../../CodegenSchema';

const {
generateSupportedApplePlatformsMacro,
} = require('./ComponentsProviderUtils');

// File path -> contents
type FilesOutput = Map<string, string>;

Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -98,7 +108,13 @@ module.exports = {

return componentTemplates.length > 0 ? componentTemplates : null;
})
.filter(Boolean);
.filter(Boolean)
.join('\n');

return generateSupportedApplePlatformsMacro(
generatedLookup,
librarySupportedApplePlatforms,
);
})
.join('\n');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ Class<RCTComponentViewProtocol> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -368,6 +433,7 @@ function createComponentProvider(schemas) {
{
schemas: schemas,
outputDirectory: outputDir,
supportedApplePlatforms,
},
{
generators: ['providerIOS'],
Expand Down Expand Up @@ -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);
}
Expand All @@ -508,4 +579,5 @@ module.exports = {
// exported for testing purposes only:
_extractLibrariesFromJSON: extractLibrariesFromJSON,
_cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders,
_extractSupportedApplePlatforms: extractSupportedApplePlatforms,
};

0 comments on commit ebb2b9c

Please sign in to comment.