Skip to content

Commit ebb2b9c

Browse files
okwasniewskifacebook-github-bot
authored andcommitted
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<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
1 parent a13d51f commit ebb2b9c

File tree

9 files changed

+235
-19
lines changed

9 files changed

+235
-19
lines changed

packages/react-native-codegen/src/generators/RNCodegen.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ type LibraryOptions = $ReadOnly<{
8383
type SchemasOptions = $ReadOnly<{
8484
schemas: {[string]: SchemaType},
8585
outputDirectory: string,
86+
supportedApplePlatforms?: {[string]: {[string]: boolean}},
8687
}>;
8788

8889
type LibraryGenerators =
@@ -289,7 +290,7 @@ module.exports = {
289290
return checkOrWriteFiles(generatedFiles, test);
290291
},
291292
generateFromSchemas(
292-
{schemas, outputDirectory}: SchemasOptions,
293+
{schemas, outputDirectory, supportedApplePlatforms}: SchemasOptions,
293294
{generators, test}: SchemasConfig,
294295
): boolean {
295296
Object.keys(schemas).forEach(libraryName =>
@@ -300,13 +301,15 @@ module.exports = {
300301

301302
for (const name of generators) {
302303
for (const generator of SCHEMAS_GENERATORS[name]) {
303-
generator(schemas).forEach((contents: string, fileName: string) => {
304-
generatedFiles.push({
305-
name: fileName,
306-
content: contents,
307-
outputDir: outputDirectory,
308-
});
309-
});
304+
generator(schemas, supportedApplePlatforms).forEach(
305+
(contents: string, fileName: string) => {
306+
generatedFiles.push({
307+
name: fileName,
308+
content: contents,
309+
outputDir: outputDirectory,
310+
});
311+
},
312+
);
310313
}
311314
}
312315
return checkOrWriteFiles(generatedFiles, test);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
const APPLE_PLATFORMS_MACRO_MAP = {
12+
ios: 'TARGET_OS_IOS',
13+
macos: 'TARGET_OS_OSX',
14+
tvos: 'TARGET_OS_TV',
15+
visionos: 'TARGET_OS_VISION',
16+
};
17+
18+
/**
19+
* Adds compiler macros to the file template to exclude unsupported platforms.
20+
*/
21+
function generateSupportedApplePlatformsMacro(
22+
fileTemplate: string,
23+
supportedPlatformsMap: ?{[string]: boolean},
24+
): string {
25+
if (!supportedPlatformsMap) {
26+
return fileTemplate;
27+
}
28+
29+
const compilerMacroString = Object.keys(supportedPlatformsMap)
30+
.reduce((acc: string[], platform) => {
31+
if (!supportedPlatformsMap[platform]) {
32+
return [...acc, `!${APPLE_PLATFORMS_MACRO_MAP[platform]}`];
33+
}
34+
return acc;
35+
}, [])
36+
.join(' && ');
37+
38+
if (!compilerMacroString) {
39+
return fileTemplate;
40+
}
41+
42+
return `#if ${compilerMacroString}
43+
${fileTemplate}
44+
#endif
45+
`;
46+
}
47+
48+
module.exports = {
49+
generateSupportedApplePlatformsMacro,
50+
};

packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderH.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

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

15+
const {
16+
generateSupportedApplePlatformsMacro,
17+
} = require('./ComponentsProviderUtils');
18+
1519
// File path -> contents
1620
type FilesOutput = Map<string, string>;
1721

@@ -63,13 +67,18 @@ Class<RCTComponentViewProtocol> ${className}Cls(void) __attribute__((used)); //
6367
`.trim();
6468

6569
module.exports = {
66-
generate(schemas: {[string]: SchemaType}): FilesOutput {
70+
generate(
71+
schemas: {[string]: SchemaType},
72+
supportedApplePlatforms?: {[string]: {[string]: boolean}},
73+
): FilesOutput {
6774
const fileName = 'RCTThirdPartyFabricComponentsProvider.h';
6875

6976
const lookupFuncs = Object.keys(schemas)
7077
.map(libraryName => {
7178
const schema = schemas[libraryName];
72-
return Object.keys(schema.modules)
79+
const librarySupportedApplePlatforms =
80+
supportedApplePlatforms?.[libraryName];
81+
const generatedLookup = Object.keys(schema.modules)
7382
.map(moduleName => {
7483
const module = schema.modules[moduleName];
7584
if (module.type !== 'Component') {
@@ -100,6 +109,11 @@ module.exports = {
100109
})
101110
.filter(Boolean)
102111
.join('\n');
112+
113+
return generateSupportedApplePlatformsMacro(
114+
generatedLookup,
115+
librarySupportedApplePlatforms,
116+
);
103117
})
104118
.join('\n');
105119

packages/react-native-codegen/src/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

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

15+
const {
16+
generateSupportedApplePlatformsMacro,
17+
} = require('./ComponentsProviderUtils');
18+
1519
// File path -> contents
1620
type FilesOutput = Map<string, string>;
1721

@@ -60,13 +64,19 @@ const LookupMapTemplate = ({
6064
{"${className}", ${className}Cls}, // ${libraryName}`;
6165

6266
module.exports = {
63-
generate(schemas: {[string]: SchemaType}): FilesOutput {
67+
generate(
68+
schemas: {[string]: SchemaType},
69+
supportedApplePlatforms?: {[string]: {[string]: boolean}},
70+
): FilesOutput {
6471
const fileName = 'RCTThirdPartyFabricComponentsProvider.mm';
6572

6673
const lookupMap = Object.keys(schemas)
6774
.map(libraryName => {
6875
const schema = schemas[libraryName];
69-
return Object.keys(schema.modules)
76+
const librarySupportedApplePlatforms =
77+
supportedApplePlatforms?.[libraryName];
78+
79+
const generatedLookup = Object.keys(schema.modules)
7080
.map(moduleName => {
7181
const module = schema.modules[moduleName];
7282
if (module.type !== 'Component') {
@@ -98,7 +108,13 @@ module.exports = {
98108

99109
return componentTemplates.length > 0 ? componentTemplates : null;
100110
})
101-
.filter(Boolean);
111+
.filter(Boolean)
112+
.join('\n');
113+
114+
return generateSupportedApplePlatformsMacro(
115+
generatedLookup,
116+
librarySupportedApplePlatforms,
117+
);
102118
})
103119
.join('\n');
104120

packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateThirdPartyFabricComponentsProviderObjCpp-test.js.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ Class<RCTComponentViewProtocol> RCTThirdPartyFabricComponentsProvider(const char
7171
{\\"MultiComponent1NativeComponent\\", MultiComponent1NativeComponentCls}, // TWO_COMPONENTS_SAME_FILE,
7272
{\\"MultiComponent2NativeComponent\\", MultiComponent2NativeComponentCls}, // TWO_COMPONENTS_SAME_FILE
7373
74-
{\\"MultiFile1NativeComponent\\", MultiFile1NativeComponentCls}, // TWO_COMPONENTS_DIFFERENT_FILES,
74+
{\\"MultiFile1NativeComponent\\", MultiFile1NativeComponentCls}, // TWO_COMPONENTS_DIFFERENT_FILES
75+
7576
{\\"MultiFile2NativeComponent\\", MultiFile2NativeComponentCls}, // TWO_COMPONENTS_DIFFERENT_FILES
7677
7778
{\\"CommandNativeComponent\\", CommandNativeComponentCls}, // COMMANDS
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
Pod::Spec.new do |s|
7+
s.name = "test-library-2"
8+
s.version = "0.0.0"
9+
s.ios.deployment_target = "9.0"
10+
s.osx.deployment_target = "13.0"
11+
s.tvos.deployment_target = "1.0"
12+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
Pod::Spec.new do |s|
7+
s.name = "test-library"
8+
s.version = "0.0.0"
9+
s.platforms = { :ios => "9.0", :osx => "13.0", visionos: "1.0" }
10+
end

packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,44 @@ describe('extractLibrariesFromJSON', () => {
8787
});
8888
});
8989

90+
describe('extractSupportedApplePlatforms', () => {
91+
it('extracts platforms when podspec specifies object of platforms', () => {
92+
const myDependency = 'test-library';
93+
const myDependencyPath = path.join(
94+
__dirname,
95+
`../__test_fixtures__/${myDependency}`,
96+
);
97+
let platforms = underTest._extractSupportedApplePlatforms(
98+
myDependency,
99+
myDependencyPath,
100+
);
101+
expect(platforms).toEqual({
102+
ios: true,
103+
macos: true,
104+
tvos: false,
105+
visionos: true,
106+
});
107+
});
108+
109+
it('extracts platforms when podspec specifies platforms separately', () => {
110+
const myDependency = 'test-library-2';
111+
const myDependencyPath = path.join(
112+
__dirname,
113+
`../__test_fixtures__/${myDependency}`,
114+
);
115+
let platforms = underTest._extractSupportedApplePlatforms(
116+
myDependency,
117+
myDependencyPath,
118+
);
119+
expect(platforms).toEqual({
120+
ios: true,
121+
macos: true,
122+
tvos: true,
123+
visionos: false,
124+
});
125+
});
126+
});
127+
90128
describe('delete empty files and folders', () => {
91129
beforeEach(() => {
92130
jest.resetModules();

packages/react-native/scripts/codegen/generate-artifacts-executor.js

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const utils = require('./codegen-utils');
2020
const generateSpecsCLIExecutor = require('./generate-specs-cli-executor');
2121
const {execSync} = require('child_process');
2222
const fs = require('fs');
23+
const glob = require('glob');
2324
const mkdirp = require('mkdirp');
2425
const os = require('os');
2526
const path = require('path');
@@ -144,6 +145,63 @@ function extractLibrariesFromJSON(configFile, dependencyPath) {
144145
}
145146
}
146147

148+
const APPLE_PLATFORMS = ['ios', 'macos', 'tvos', 'visionos'];
149+
150+
// Cocoapods specific platform keys
151+
function getCocoaPodsPlatformKey(platformName) {
152+
if (platformName === 'macos') {
153+
return 'osx';
154+
}
155+
return platformName;
156+
}
157+
158+
function extractSupportedApplePlatforms(dependency, dependencyPath) {
159+
console.log('[Codegen] Searching for podspec in the project dependencies.');
160+
const podspecs = glob.sync('*.podspec', {cwd: dependencyPath});
161+
162+
if (podspecs.length === 0) {
163+
return;
164+
}
165+
166+
// Take the first podspec found
167+
const podspec = fs.readFileSync(
168+
path.join(dependencyPath, podspecs[0]),
169+
'utf8',
170+
);
171+
172+
/**
173+
* Podspec can have platforms defined in two ways:
174+
* 1. `spec.platforms = { :ios => "11.0", :tvos => "11.0" }`
175+
* 2. `s.ios.deployment_target = "11.0"`
176+
* `s.tvos.deployment_target = "11.0"`
177+
*/
178+
const supportedPlatforms = podspec
179+
.split('\n')
180+
.filter(
181+
line => line.includes('platform') || line.includes('deployment_target'),
182+
)
183+
.join('');
184+
185+
// Generate a map of supported platforms { [platform]: true/false }
186+
const supportedPlatformsMap = APPLE_PLATFORMS.reduce(
187+
(acc, platform) => ({
188+
...acc,
189+
[platform]: supportedPlatforms.includes(
190+
getCocoaPodsPlatformKey(platform),
191+
),
192+
}),
193+
{},
194+
);
195+
196+
console.log(
197+
`[Codegen] Supported Apple platforms: ${Object.keys(supportedPlatformsMap)
198+
.filter(key => supportedPlatformsMap[key])
199+
.join(', ')} for ${dependency}`,
200+
);
201+
202+
return supportedPlatformsMap;
203+
}
204+
147205
function findExternalLibraries(pkgJson) {
148206
const dependencies = {
149207
...pkgJson.dependencies,
@@ -276,9 +334,16 @@ function generateSchemaInfo(library, platform) {
276334
library.config.jsSrcsDir,
277335
);
278336
console.log(`[Codegen] Processing ${library.config.name}`);
337+
338+
const supportedApplePlatforms = extractSupportedApplePlatforms(
339+
library.config.name,
340+
library.libraryPath,
341+
);
342+
279343
// Generate one schema for the entire library...
280344
return {
281345
library: library,
346+
supportedApplePlatforms,
282347
schema: utils
283348
.getCombineJSToSchema()
284349
.combineSchemasInFileList(
@@ -356,7 +421,7 @@ function mustGenerateNativeCode(includeLibraryPath, schemaInfo) {
356421
);
357422
}
358423

359-
function createComponentProvider(schemas) {
424+
function createComponentProvider(schemas, supportedApplePlatforms) {
360425
console.log('[Codegen] Creating component provider.');
361426
const outputDir = path.join(
362427
REACT_NATIVE_PACKAGE_ROOT_FOLDER,
@@ -368,6 +433,7 @@ function createComponentProvider(schemas) {
368433
{
369434
schemas: schemas,
370435
outputDirectory: outputDir,
436+
supportedApplePlatforms,
371437
},
372438
{
373439
generators: ['providerIOS'],
@@ -487,10 +553,15 @@ function execute(projectRoot, targetPlatform, baseOutputPath) {
487553
if (
488554
rootCodegenTargetNeedsThirdPartyComponentProvider(pkgJson, platform)
489555
) {
490-
const schemas = schemaInfos
491-
.filter(dependencyNeedsThirdPartyComponentProvider)
492-
.map(schemaInfo => schemaInfo.schema);
493-
createComponentProvider(schemas);
556+
const filteredSchemas = schemaInfos.filter(
557+
dependencyNeedsThirdPartyComponentProvider,
558+
);
559+
const schemas = filteredSchemas.map(schemaInfo => schemaInfo.schema);
560+
const supportedApplePlatforms = filteredSchemas.map(
561+
schemaInfo => schemaInfo.supportedApplePlatforms,
562+
);
563+
564+
createComponentProvider(schemas, supportedApplePlatforms);
494565
}
495566
cleanupEmptyFilesAndFolders(outputPath);
496567
}
@@ -508,4 +579,5 @@ module.exports = {
508579
// exported for testing purposes only:
509580
_extractLibrariesFromJSON: extractLibrariesFromJSON,
510581
_cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders,
582+
_extractSupportedApplePlatforms: extractSupportedApplePlatforms,
511583
};

0 commit comments

Comments
 (0)