Skip to content

Commit c789880

Browse files
satya164meta-codesync[bot]
authored andcommitted
Handle autolinking for pure C++ turbo modules without includesGeneratedCode (#56938)
Summary: Currently, pure C++ modules require `includesGeneratedCode: true`, and the library to be shipping codegen files to work. This is inconsistent with regular turbo modules that work with both setups. The issue is in 2 places: 1. CLI returns hardcoded default for C++ modules 2. Autolinking logic doesn't run codegen for C++ modules PR removing hardcoded default from CLI: react-native-community/cli#2799 This change updates the autolinking logic to detect pure C++ libraries without `includesGeneratedCode: true`, run codegen for them and use correct path for CMakeLists based on the codegen output. ## Changelog: [ANDROID] [FIXED] - Fix pure C++ turbo modules not working without `includesGeneratedCode: true` Pull Request resolved: #56938 Test Plan: - create new cpp library: `npx create-react-native-library@latest awesome-library --yes --description "my library" --type turbo-module --languages cpp` - run `yarn example android` and notice that the build works - apply the following patch to remove `includesGeneratedCode: true`: ```patch diff --git a/package.json b/package.json index e3c893a..f41100f 100644 --- a/package.json +++ b/package.json @@ -112,22 +112,16 @@ { "project": "tsconfig.build.json" } - ], - "codegen" + ] ] }, "codegenConfig": { "name": "AwesomeLibrarySpec", "type": "modules", "jsSrcsDir": "src", - "outputDir": { - "ios": "ios/generated", - "android": "android/generated" - }, "android": { "javaPackageName": "com.awesomelibrary" - }, - "includesGeneratedCode": true + } }, "prettier": { "quoteProps": "consistent", diff --git a/react-native.config.js b/react-native.config.js index fef2c32..46f9600 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -5,7 +5,6 @@ module.exports = { dependency: { platforms: { android: { - cmakeListsPath: 'generated/jni/CMakeLists.txt', cxxModuleCMakeListsModuleName: 'react-native-awesome-library', cxxModuleCMakeListsPath: 'CMakeLists.txt', cxxModuleHeaderName: 'AwesomeLibraryImpl', ``` - run `yarn example android` and notice that the build fails - apply the patch from this PR and from react-native-community/cli#2799 and notice that the module works correctly screenshot from the test library: <img width="360" height="780" alt="Screenshot_1779448311" src="https://github.com/user-attachments/assets/a3f43a3c-bb46-404c-80ff-173ea71a9887" /> Reviewed By: cortinico Differential Revision: D106094034 Pulled By: fabriziocucci fbshipit-source-id: 1d0058907e7a274a8b7de2ec69539dda70bb8f67
1 parent f363f6b commit c789880

4 files changed

Lines changed: 650 additions & 78 deletions

File tree

packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt

Lines changed: 201 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension
1111
import com.android.build.api.variant.LibraryAndroidComponentsExtension
1212
import com.android.build.gradle.internal.tasks.factory.dependsOn
1313
import com.facebook.react.internal.PrivateReactExtension
14+
import com.facebook.react.model.ModelAutolinkingDependenciesJson
1415
import com.facebook.react.tasks.GenerateAutolinkingNewArchitecturesFileTask
1516
import com.facebook.react.tasks.GenerateCodegenArtifactsTask
1617
import com.facebook.react.tasks.GenerateCodegenSchemaTask
@@ -38,6 +39,7 @@ import org.gradle.api.Project
3839
import org.gradle.api.Task
3940
import org.gradle.api.file.Directory
4041
import org.gradle.api.provider.Provider
42+
import org.gradle.api.tasks.TaskProvider
4143
import org.gradle.internal.jvm.Jvm
4244

4345
class ReactPlugin : Plugin<Project> {
@@ -107,7 +109,7 @@ class ReactPlugin : Plugin<Project> {
107109
project.configureReactTasks(variant = variant, config = extension)
108110
}
109111
}
110-
configureAutolinking(project, extension)
112+
configureAutolinking(project, extension, rootExtension)
111113
configureCodegen(project, extension, rootExtension, isLibrary = false)
112114
configureResources(project, extension)
113115
configureBuildTypesForApp(project)
@@ -173,78 +175,48 @@ class ReactPlugin : Plugin<Project> {
173175
localExtension.jsRootDir.convention(localExtension.root)
174176
}
175177

176-
// We create the task to produce schema from JS files.
177-
val generateCodegenSchemaTask =
178-
project.tasks.register(
179-
"generateCodegenSchemaFromJavaScript",
180-
GenerateCodegenSchemaTask::class.java,
181-
) { it ->
182-
it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
183-
it.codegenDir.set(rootExtension.codegenDir)
184-
it.generatedSrcDir.set(generatedSrcDir)
185-
it.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
186-
187-
// We're reading the package.json at configuration time to properly feed
188-
// the `jsRootDir` @Input property of this task & the onlyIf. Therefore, the
189-
// parsePackageJson should be invoked inside this lambda.
190-
val packageJson = findPackageJsonFile(project, rootExtension.root)
191-
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
192-
193-
val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir
194-
val includesGeneratedCode =
195-
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
196-
if (jsSrcsDirInPackageJson != null) {
197-
it.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson))
198-
} else {
199-
it.jsRootDir.set(localExtension.jsRootDir)
200-
}
201-
it.jsInputFiles.set(
202-
project.fileTree(it.jsRootDir) { tree ->
203-
tree.include("**/*.js")
204-
tree.include("**/*.jsx")
205-
tree.include("**/*.ts")
206-
tree.include("**/*.tsx")
207-
208-
tree.exclude("node_modules/**/*")
209-
tree.exclude("**/*.d.ts")
210-
// We want to exclude the build directory, to don't pick them up for execution
211-
// avoidance.
212-
tree.exclude("**/build/**/*")
213-
}
214-
)
215-
216-
val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root)
217-
it.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
218-
}
219-
220-
// We create the task to generate Java code from schema.
178+
// We create the tasks to produce schema from JS files and generate artifacts from schema.
221179
val generateCodegenArtifactsTask =
222-
project.tasks.register(
223-
"generateCodegenArtifactsFromSchema",
224-
GenerateCodegenArtifactsTask::class.java,
225-
) { task ->
226-
task.dependsOn(generateCodegenSchemaTask)
227-
task.reactNativeDir.set(rootExtension.reactNativeDir)
228-
task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
229-
task.generatedSrcDir.set(generatedSrcDir)
230-
task.packageJsonFile.set(findPackageJsonFile(project, rootExtension.root))
231-
task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName)
232-
task.libraryName.set(localExtension.libraryName)
233-
task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
234-
235-
// Please note that appNeedsCodegen is triggering a read of the package.json at
236-
// configuration time as we need to feed the onlyIf condition of this task.
237-
// Therefore, the appNeedsCodegen needs to be invoked inside this lambda.
238-
val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root)
239-
val packageJson = findPackageJsonFile(project, rootExtension.root)
240-
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
241-
val includesGeneratedCode =
242-
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
243-
task.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
244-
}
180+
registerCodegenTasks(
181+
project = project,
182+
rootExtension = rootExtension,
183+
generatedSrcDir = generatedSrcDir,
184+
packageJsonFile = { findPackageJsonFile(project, rootExtension.root) },
185+
schemaTaskName = "generateCodegenSchemaFromJavaScript",
186+
artifactsTaskName = "generateCodegenArtifactsFromSchema",
187+
configureJsRoot = { task, packageJson ->
188+
// We're reading the package.json at configuration time to properly feed
189+
// the `jsRootDir` @Input property of this task & the onlyIf. Therefore, the
190+
// parsePackageJson should be invoked inside this lambda.
191+
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
192+
val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir
193+
194+
if (packageJson != null && jsSrcsDirInPackageJson != null) {
195+
task.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson))
196+
} else {
197+
task.jsRootDir.set(localExtension.jsRootDir)
198+
}
199+
},
200+
configureCodegenArtifacts = { task, _ ->
201+
task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName)
202+
task.libraryName.set(localExtension.libraryName)
203+
},
204+
onlyIf = { packageJson ->
205+
// Please note that needsCodegenFromPackageJson is triggering a read of the
206+
// package.json at configuration time as we need to feed the onlyIf condition of this
207+
// task. Therefore, needsCodegenFromPackageJson needs to be invoked inside this
208+
// lambda.
209+
val needsCodegenFromPackageJson =
210+
project.needsCodegenFromPackageJson(rootExtension.root)
211+
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
212+
val includesGeneratedCode =
213+
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
214+
(isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode
215+
},
216+
)
245217

246218
// We update the android configuration to include the generated sources.
247-
// This equivalent to this DSL:
219+
// This is equivalent to this DSL:
248220
//
249221
// android { sourceSets { main { java { srcDirs += "$generatedSrcDir/java" } } } }
250222
if (isLibrary) {
@@ -264,20 +236,101 @@ class ReactPlugin : Plugin<Project> {
264236
project.tasks.named("preBuild", Task::class.java).dependsOn(generateCodegenArtifactsTask)
265237
}
266238

239+
private fun registerCodegenTasks(
240+
project: Project,
241+
rootExtension: PrivateReactExtension,
242+
generatedSrcDir: Provider<Directory>,
243+
packageJsonFile: () -> File?,
244+
schemaTaskName: String,
245+
artifactsTaskName: String,
246+
configureJsRoot: (GenerateCodegenSchemaTask, File?) -> Unit,
247+
configureCodegenArtifacts: (GenerateCodegenArtifactsTask, File?) -> Unit,
248+
onlyIf: (File?) -> Boolean = { true },
249+
): TaskProvider<GenerateCodegenArtifactsTask> {
250+
// We create the task to produce schema from JS files.
251+
val generateCodegenSchemaTask =
252+
project.tasks.register(
253+
schemaTaskName,
254+
GenerateCodegenSchemaTask::class.java,
255+
) { task ->
256+
val packageJson = packageJsonFile()
257+
258+
task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
259+
task.codegenDir.set(rootExtension.codegenDir)
260+
task.generatedSrcDir.set(generatedSrcDir)
261+
task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
262+
263+
configureJsRoot(task, packageJson)
264+
265+
task.jsInputFiles.set(
266+
project.fileTree(task.jsRootDir) { tree ->
267+
tree.include("**/*.js")
268+
tree.include("**/*.jsx")
269+
tree.include("**/*.ts")
270+
tree.include("**/*.tsx")
271+
272+
tree.exclude("node_modules/**/*")
273+
tree.exclude("**/*.d.ts")
274+
// We want to exclude the build directory, to avoid picking them up for execution
275+
// avoidance.
276+
tree.exclude("**/build/**/*")
277+
}
278+
)
279+
val shouldRunTask = onlyIf(packageJson)
280+
task.onlyIf { shouldRunTask }
281+
}
282+
283+
// We create the task to generate Java code from schema.
284+
return project.tasks.register(
285+
artifactsTaskName,
286+
GenerateCodegenArtifactsTask::class.java,
287+
) { task ->
288+
val packageJson = packageJsonFile()
289+
290+
task.dependsOn(generateCodegenSchemaTask)
291+
task.reactNativeDir.set(rootExtension.reactNativeDir)
292+
task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
293+
task.generatedSrcDir.set(generatedSrcDir)
294+
task.packageJsonFile.set(packageJson)
295+
task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
296+
297+
configureCodegenArtifacts(task, packageJson)
298+
299+
// The caller decides whether codegen should run. For app/library projects this depends on
300+
// package.json and includesGeneratedCode. Pure C++ dependencies are filtered before task
301+
// registration, so their generated tasks can always run.
302+
val shouldRunTask = onlyIf(packageJson)
303+
task.onlyIf { shouldRunTask }
304+
}
305+
}
306+
267307
/** This function sets up Autolinking for App users */
268308
private fun configureAutolinking(
269309
project: Project,
270310
extension: ReactExtension,
311+
rootExtension: PrivateReactExtension,
271312
) {
272313
val generatedAutolinkingJavaDir: Provider<Directory> =
273314
project.layout.buildDirectory.dir("generated/autolinking/src/main/java")
274315
val generatedAutolinkingJniDir: Provider<Directory> =
275316
project.layout.buildDirectory.dir("generated/autolinking/src/main/jni")
317+
val generatedPureCxxSourceDir: Provider<Directory> =
318+
project.layout.buildDirectory.dir("generated/source/codegen/pureCxx")
276319

277320
// The autolinking.json file is available in the root build folder as it's generated
278321
// by ReactSettingsPlugin.kt
279322
val rootGeneratedAutolinkingFile =
280323
project.rootProject.layout.buildDirectory.file("generated/autolinking/autolinking.json")
324+
val pureCxxDependencies =
325+
getPureCxxCodegenDependencies(rootGeneratedAutolinkingFile.get().asFile)
326+
val pureCxxCodegenTasks =
327+
configurePureCxxDependenciesCodegen(
328+
project,
329+
extension,
330+
rootExtension,
331+
generatedPureCxxSourceDir,
332+
pureCxxDependencies,
333+
)
281334

282335
// We add a task called generateAutolinkingPackageList to do not clash with the existing task
283336
// called generatePackageList. This can to be renamed once we unlink the rn <-> cli
@@ -291,9 +344,7 @@ class ReactPlugin : Plugin<Project> {
291344
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
292345
}
293346

294-
// We add a task called generateAutolinkingPackageList to do not clash with the existing task
295-
// called generatePackageList. This can to be renamed once we unlink the rn <-> cli
296-
// dependency.
347+
// We add a task called generateReactNativeEntryPoint to generate the React Native entry point.
297348
val generateEntryPointTask =
298349
project.tasks.register(
299350
"generateReactNativeEntryPoint",
@@ -311,14 +362,19 @@ class ReactPlugin : Plugin<Project> {
311362
) { task ->
312363
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
313364
task.generatedOutputDirectory.set(generatedAutolinkingJniDir)
365+
366+
if (pureCxxDependencies.isNotEmpty()) {
367+
task.generatedPureCxxSourceDirectory.set(generatedPureCxxSourceDir)
368+
}
369+
370+
task.dependsOn(pureCxxCodegenTasks)
314371
}
315372
project.tasks
316373
.named("preBuild", Task::class.java)
317374
.dependsOn(generateAutolinkingNewArchitectureFilesTask)
318375

319-
// We let generateAutolinkingPackageList and generateEntryPoint depend on the preBuild task so
320-
// it's executed before
321-
// everything else.
376+
// We make preBuild depend on generateAutolinkingPackageList and generateEntryPoint so they run
377+
// before everything else.
322378
project.tasks
323379
.named("preBuild", Task::class.java)
324380
.dependsOn(generatePackageListTask, generateEntryPointTask)
@@ -333,4 +389,73 @@ class ReactPlugin : Plugin<Project> {
333389
}
334390
}
335391
}
392+
393+
private fun configurePureCxxDependenciesCodegen(
394+
project: Project,
395+
extension: ReactExtension,
396+
rootExtension: PrivateReactExtension,
397+
generatedPureCxxSourceDir: Provider<Directory>,
398+
dependencies: List<ModelAutolinkingDependenciesJson>,
399+
): List<TaskProvider<GenerateCodegenArtifactsTask>> {
400+
// Pure C++ dependencies are not included as Gradle subprojects, so configureCodegen won't run
401+
// for them. The app owns these generated codegen artifacts and links them from autolinking.
402+
return dependencies.mapNotNull { dependency ->
403+
val android = dependency.platforms?.android ?: return@mapNotNull null
404+
val libraryName = android.libraryName ?: return@mapNotNull null
405+
val dependencyRoot = File(dependency.root)
406+
val packageJson = File(dependencyRoot, "package.json")
407+
val parsedPackageJson = JsonUtils.fromPackageJson(packageJson)
408+
val jsSrcsDir = parsedPackageJson?.codegenConfig?.jsSrcsDir
409+
val generatedSrcDir = generatedPureCxxSourceDir.map { it.dir(libraryName) }
410+
val taskNameSuffix = taskNameSuffixForDependency(dependency)
411+
412+
registerCodegenTasks(
413+
project = project,
414+
rootExtension = rootExtension,
415+
generatedSrcDir = generatedSrcDir,
416+
packageJsonFile = { packageJson },
417+
schemaTaskName = "generate${taskNameSuffix}CodegenSchemaFromJavaScript",
418+
artifactsTaskName = "generate${taskNameSuffix}CodegenArtifactsFromSchema",
419+
configureJsRoot = { task, _ ->
420+
if (jsSrcsDir != null) {
421+
task.jsRootDir.set(File(packageJson.parentFile, jsSrcsDir))
422+
} else {
423+
task.jsRootDir.set(dependencyRoot)
424+
}
425+
},
426+
configureCodegenArtifacts = { task, _ ->
427+
val codegenJavaPackageName = parsedPackageJson?.codegenConfig?.android?.javaPackageName
428+
if (codegenJavaPackageName != null) {
429+
task.codegenJavaPackageName.set(codegenJavaPackageName)
430+
} else {
431+
task.codegenJavaPackageName.set(extension.codegenJavaPackageName)
432+
}
433+
task.libraryName.set(libraryName)
434+
},
435+
)
436+
}
437+
}
438+
439+
internal fun getPureCxxCodegenDependencies(
440+
autolinkingFile: File
441+
): List<ModelAutolinkingDependenciesJson> {
442+
val model = JsonUtils.fromAutolinkingConfigJson(autolinkingFile)
443+
return model?.dependencies?.values?.filter { dependency ->
444+
val android = dependency.platforms?.android
445+
446+
if (android?.isPureCxxDependency != true || android.libraryName == null) {
447+
return@filter false
448+
}
449+
450+
val packageJson = File(dependency.root, "package.json")
451+
val codegenConfig = JsonUtils.fromPackageJson(packageJson)?.codegenConfig
452+
codegenConfig != null && codegenConfig.includesGeneratedCode != true
453+
} ?: emptyList()
454+
}
455+
456+
internal fun taskNameSuffixForDependency(dependency: ModelAutolinkingDependenciesJson): String =
457+
dependency.name
458+
.map { char -> if (char.isLetterOrDigit()) char.toString() else "_${char.code}_" }
459+
.joinToString("")
460+
.replaceFirstChar { char -> char.titlecase() }
336461
}

0 commit comments

Comments
 (0)