diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index f3c0982c7e0..6ac71449e47 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -11,6 +11,7 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.gradle.internal.tasks.factory.dependsOn import com.facebook.react.internal.PrivateReactExtension +import com.facebook.react.model.ModelAutolinkingDependenciesJson import com.facebook.react.tasks.GenerateAutolinkingNewArchitecturesFileTask import com.facebook.react.tasks.GenerateCodegenArtifactsTask import com.facebook.react.tasks.GenerateCodegenSchemaTask @@ -38,6 +39,7 @@ import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.file.Directory import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider import org.gradle.internal.jvm.Jvm class ReactPlugin : Plugin { @@ -107,7 +109,7 @@ class ReactPlugin : Plugin { project.configureReactTasks(variant = variant, config = extension) } } - configureAutolinking(project, extension) + configureAutolinking(project, extension, rootExtension) configureCodegen(project, extension, rootExtension, isLibrary = false) configureResources(project, extension) configureBuildTypesForApp(project) @@ -173,78 +175,48 @@ class ReactPlugin : Plugin { localExtension.jsRootDir.convention(localExtension.root) } - // We create the task to produce schema from JS files. - val generateCodegenSchemaTask = - project.tasks.register( - "generateCodegenSchemaFromJavaScript", - GenerateCodegenSchemaTask::class.java, - ) { it -> - it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) - it.codegenDir.set(rootExtension.codegenDir) - it.generatedSrcDir.set(generatedSrcDir) - it.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath) - - // We're reading the package.json at configuration time to properly feed - // the `jsRootDir` @Input property of this task & the onlyIf. Therefore, the - // parsePackageJson should be invoked inside this lambda. - val packageJson = findPackageJsonFile(project, rootExtension.root) - val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) } - - val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir - val includesGeneratedCode = - parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false - if (jsSrcsDirInPackageJson != null) { - it.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson)) - } else { - it.jsRootDir.set(localExtension.jsRootDir) - } - it.jsInputFiles.set( - project.fileTree(it.jsRootDir) { tree -> - tree.include("**/*.js") - tree.include("**/*.jsx") - tree.include("**/*.ts") - tree.include("**/*.tsx") - - tree.exclude("node_modules/**/*") - tree.exclude("**/*.d.ts") - // We want to exclude the build directory, to don't pick them up for execution - // avoidance. - tree.exclude("**/build/**/*") - } - ) - - val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root) - it.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode } - } - - // We create the task to generate Java code from schema. + // We create the tasks to produce schema from JS files and generate artifacts from schema. val generateCodegenArtifactsTask = - project.tasks.register( - "generateCodegenArtifactsFromSchema", - GenerateCodegenArtifactsTask::class.java, - ) { task -> - task.dependsOn(generateCodegenSchemaTask) - task.reactNativeDir.set(rootExtension.reactNativeDir) - task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) - task.generatedSrcDir.set(generatedSrcDir) - task.packageJsonFile.set(findPackageJsonFile(project, rootExtension.root)) - task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName) - task.libraryName.set(localExtension.libraryName) - task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath) - - // Please note that appNeedsCodegen is triggering a read of the package.json at - // configuration time as we need to feed the onlyIf condition of this task. - // Therefore, the appNeedsCodegen needs to be invoked inside this lambda. - val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root) - val packageJson = findPackageJsonFile(project, rootExtension.root) - val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) } - val includesGeneratedCode = - parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false - task.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode } - } + registerCodegenTasks( + project = project, + rootExtension = rootExtension, + generatedSrcDir = generatedSrcDir, + packageJsonFile = { findPackageJsonFile(project, rootExtension.root) }, + schemaTaskName = "generateCodegenSchemaFromJavaScript", + artifactsTaskName = "generateCodegenArtifactsFromSchema", + configureJsRoot = { task, packageJson -> + // We're reading the package.json at configuration time to properly feed + // the `jsRootDir` @Input property of this task & the onlyIf. Therefore, the + // parsePackageJson should be invoked inside this lambda. + val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) } + val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir + + if (packageJson != null && jsSrcsDirInPackageJson != null) { + task.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson)) + } else { + task.jsRootDir.set(localExtension.jsRootDir) + } + }, + configureCodegenArtifacts = { task, _ -> + task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName) + task.libraryName.set(localExtension.libraryName) + }, + onlyIf = { packageJson -> + // Please note that needsCodegenFromPackageJson is triggering a read of the + // package.json at configuration time as we need to feed the onlyIf condition of this + // task. Therefore, needsCodegenFromPackageJson needs to be invoked inside this + // lambda. + val needsCodegenFromPackageJson = + project.needsCodegenFromPackageJson(rootExtension.root) + val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) } + val includesGeneratedCode = + parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false + (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode + }, + ) // We update the android configuration to include the generated sources. - // This equivalent to this DSL: + // This is equivalent to this DSL: // // android { sourceSets { main { java { srcDirs += "$generatedSrcDir/java" } } } } if (isLibrary) { @@ -264,24 +236,103 @@ class ReactPlugin : Plugin { project.tasks.named("preBuild", Task::class.java).dependsOn(generateCodegenArtifactsTask) } + private fun registerCodegenTasks( + project: Project, + rootExtension: PrivateReactExtension, + generatedSrcDir: Provider, + packageJsonFile: () -> File?, + schemaTaskName: String, + artifactsTaskName: String, + configureJsRoot: (GenerateCodegenSchemaTask, File?) -> Unit, + configureCodegenArtifacts: (GenerateCodegenArtifactsTask, File?) -> Unit, + onlyIf: (File?) -> Boolean = { true }, + ): TaskProvider { + // We create the task to produce schema from JS files. + val generateCodegenSchemaTask = + project.tasks.register( + schemaTaskName, + GenerateCodegenSchemaTask::class.java, + ) { task -> + val packageJson = packageJsonFile() + + task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) + task.codegenDir.set(rootExtension.codegenDir) + task.generatedSrcDir.set(generatedSrcDir) + task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath) + + configureJsRoot(task, packageJson) + + task.jsInputFiles.set( + project.fileTree(task.jsRootDir) { tree -> + tree.include("**/*.js") + tree.include("**/*.jsx") + tree.include("**/*.ts") + tree.include("**/*.tsx") + + tree.exclude("node_modules/**/*") + tree.exclude("**/*.d.ts") + // We want to exclude the build directory, to avoid picking them up for execution + // avoidance. + tree.exclude("**/build/**/*") + } + ) + val shouldRunTask = onlyIf(packageJson) + task.onlyIf { shouldRunTask } + } + + // We create the task to generate Java code from schema. + return project.tasks.register( + artifactsTaskName, + GenerateCodegenArtifactsTask::class.java, + ) { task -> + val packageJson = packageJsonFile() + + task.dependsOn(generateCodegenSchemaTask) + task.reactNativeDir.set(rootExtension.reactNativeDir) + task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) + task.generatedSrcDir.set(generatedSrcDir) + task.packageJsonFile.set(packageJson) + task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath) + + configureCodegenArtifacts(task, packageJson) + + // The caller decides whether codegen should run. For app/library projects this depends on + // package.json and includesGeneratedCode. Pure C++ dependencies are filtered before task + // registration, so their generated tasks can always run. + val shouldRunTask = onlyIf(packageJson) + task.onlyIf { shouldRunTask } + } + } + /** This function sets up Autolinking for App users */ private fun configureAutolinking( project: Project, extension: ReactExtension, + rootExtension: PrivateReactExtension, ) { val generatedAutolinkingJavaDir: Provider = project.layout.buildDirectory.dir("generated/autolinking/src/main/java") val generatedAutolinkingJniDir: Provider = project.layout.buildDirectory.dir("generated/autolinking/src/main/jni") + val generatedPureCxxSourceDir: Provider = + project.layout.buildDirectory.dir("generated/source/codegen/pureCxx") // The autolinking.json file is available in the root build folder as it's generated // by ReactSettingsPlugin.kt val rootGeneratedAutolinkingFile = project.rootProject.layout.buildDirectory.file("generated/autolinking/autolinking.json") + val pureCxxDependencies = + getPureCxxCodegenDependencies(rootGeneratedAutolinkingFile.get().asFile) + val pureCxxCodegenTasks = + configurePureCxxDependenciesCodegen( + project, + extension, + rootExtension, + generatedPureCxxSourceDir, + pureCxxDependencies, + ) - // We add a task called generateAutolinkingPackageList to do not clash with the existing task - // called generatePackageList. This can to be renamed once we unlink the rn <-> cli - // dependency. + // We add a task called generateReactNativeEntryPoint to generate the React Native entry point. val generatePackageListTask = project.tasks.register( "generateAutolinkingPackageList", @@ -311,14 +362,19 @@ class ReactPlugin : Plugin { ) { task -> task.autolinkInputFile.set(rootGeneratedAutolinkingFile) task.generatedOutputDirectory.set(generatedAutolinkingJniDir) + + if (pureCxxDependencies.isNotEmpty()) { + task.generatedPureCxxSourceDirectory.set(generatedPureCxxSourceDir) + } + + task.dependsOn(pureCxxCodegenTasks) } project.tasks .named("preBuild", Task::class.java) .dependsOn(generateAutolinkingNewArchitectureFilesTask) - // We let generateAutolinkingPackageList and generateEntryPoint depend on the preBuild task so - // it's executed before - // everything else. + // We make preBuild depend on generateAutolinkingPackageList and generateEntryPoint so they run + // before everything else. project.tasks .named("preBuild", Task::class.java) .dependsOn(generatePackageListTask, generateEntryPointTask) @@ -333,4 +389,76 @@ class ReactPlugin : Plugin { } } } + + private fun configurePureCxxDependenciesCodegen( + project: Project, + extension: ReactExtension, + rootExtension: PrivateReactExtension, + generatedPureCxxSourceDir: Provider, + dependencies: List, + ): List> { + // Pure C++ dependencies are not included as Gradle subprojects, so configureCodegen won't run + // for them. The app owns these generated codegen artifacts and links them from autolinking. + return dependencies.mapNotNull { dependency -> + val android = dependency.platforms?.android ?: return@mapNotNull null + val libraryName = android.libraryName ?: return@mapNotNull null + val dependencyRoot = File(dependency.root) + val packageJson = File(dependencyRoot, "package.json") + val parsedPackageJson = JsonUtils.fromPackageJson(packageJson) + val jsSrcsDir = parsedPackageJson?.codegenConfig?.jsSrcsDir + val generatedSrcDir = generatedPureCxxSourceDir.map { it.dir(libraryName) } + val taskNameSuffix = taskNameSuffixForDependency(dependency) + + registerCodegenTasks( + project = project, + rootExtension = rootExtension, + generatedSrcDir = generatedSrcDir, + packageJsonFile = { packageJson }, + schemaTaskName = "generate${taskNameSuffix}CodegenSchemaFromJavaScript", + artifactsTaskName = "generate${taskNameSuffix}CodegenArtifactsFromSchema", + configureJsRoot = { task, _ -> + if (jsSrcsDir != null) { + task.jsRootDir.set(File(packageJson.parentFile, jsSrcsDir)) + } else { + task.jsRootDir.set(dependencyRoot) + } + }, + configureCodegenArtifacts = { task, _ -> + val codegenJavaPackageName = parsedPackageJson?.codegenConfig?.android?.javaPackageName + if (codegenJavaPackageName != null) { + task.codegenJavaPackageName.set(codegenJavaPackageName) + } else { + task.codegenJavaPackageName.set(extension.codegenJavaPackageName) + } + task.libraryName.set(libraryName) + }, + ) + } + } + + internal fun getPureCxxCodegenDependencies( + autolinkingFile: File + ): List { + val model = JsonUtils.fromAutolinkingConfigJson(autolinkingFile) + return model + ?.dependencies + ?.values + ?.filter { dependency -> + val android = dependency.platforms?.android + + if (android?.isPureCxxDependency != true || android.libraryName == null) { + return@filter false + } + + val packageJson = File(dependency.root, "package.json") + val codegenConfig = JsonUtils.fromPackageJson(packageJson)?.codegenConfig + codegenConfig != null && codegenConfig.includesGeneratedCode != true + } ?: emptyList() + } + + private fun taskNameSuffixForDependency(dependency: ModelAutolinkingDependenciesJson): String = + dependency.name + .map { char -> if (char.isLetterOrDigit()) char.toString() else "_${char.code}_" } + .joinToString("") + .replaceFirstChar { char -> char.titlecase() } } diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt index 0a3e311dbd9..1b895422938 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt @@ -14,7 +14,9 @@ import java.io.File import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction @@ -28,6 +30,8 @@ abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() { @get:OutputDirectory abstract val generatedOutputDirectory: DirectoryProperty + @get:Optional @get:InputDirectory abstract val generatedPureCxxSourceDirectory: DirectoryProperty + @TaskAction fun taskAction() { val model = JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile) @@ -55,10 +59,11 @@ abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() { packages.joinToString("\n") { dep -> var addDirectoryString = "" val libraryName = dep.libraryName - val cmakeListsPath = dep.cmakeListsPath + val cmakeListsPath = cmakeListsPathForDependency(dep) val cxxModuleCMakeListsPath = dep.cxxModuleCMakeListsPath if (libraryName != null && cmakeListsPath != null) { - // If user provided a custom cmakeListsPath, let's honor it. + // If user provided a custom cmakeListsPath, let's honor it. Otherwise, pure C++ + // dependencies use the app-owned generated codegen directory. val nativeFolderPath = sanitizeCmakeListsPath(cmakeListsPath) addDirectoryString += """ @@ -96,6 +101,28 @@ abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() { return CMAKE_TEMPLATE.replace("{{ libraryIncludes }}", libraryIncludes) } + private fun cmakeListsPathForDependency( + dep: ModelAutolinkingDependenciesPlatformAndroidJson + ): String? { + if (dep.cmakeListsPath != null) { + return dep.cmakeListsPath + } + + if ( + dep.isPureCxxDependency != true || + dep.libraryName == null || + !generatedPureCxxSourceDirectory.isPresent + ) { + return null + } + + return generatedPureCxxSourceDirectory + .get() + .file("${dep.libraryName}/jni/CMakeLists.txt") + .asFile + .absolutePath + } + internal fun generateCppFileContent( packages: List ): String { diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt new file mode 100644 index 00000000000..0b8bca337da --- /dev/null +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt @@ -0,0 +1,158 @@ +/* + * 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. + */ + +package com.facebook.react + +import java.io.File +import org.assertj.core.api.Assertions.assertThat +import org.intellij.lang.annotations.Language +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class ReactPluginTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test + fun getPureCxxCodegenDependencies_filtersDependenciesCorrectly() { + val includesGeneratedCode = createPackage("includes-generated-code", true) + val withoutGeneratedCode = createPackage("without-generated-code", false) + val withoutIncludesGeneratedCode = createPackage("without-includes-generated-code", null) + val withoutCodegenConfig = createPackageWithoutCodegenConfig("without-codegen-config") + val missingNonPureCxxPackage = File(tempFolder.root, "missing-non-pure-cxx-package") + + val autolinkingFile = + createAutolinkingFile( + """ + { + "reactNativeVersion": "1000.0.0", + "dependencies": { + "includes-generated-code": { + "root": "${includesGeneratedCode.invariantSeparatorsPath}", + "name": "includes-generated-code", + "platforms": { + "android": { + "sourceDir": "${includesGeneratedCode.invariantSeparatorsPath}/android", + "packageImportPath": "import com.facebook.react.IncludesGeneratedCodePackage;", + "packageInstance": "new IncludesGeneratedCodePackage()", + "buildTypes": [], + "libraryName": "IncludesGeneratedCode", + "isPureCxxDependency": true + } + } + }, + "without-generated-code": { + "root": "${withoutGeneratedCode.invariantSeparatorsPath}", + "name": "without-generated-code", + "platforms": { + "android": { + "sourceDir": "${withoutGeneratedCode.invariantSeparatorsPath}/android", + "packageImportPath": "import com.facebook.react.WithoutGeneratedCodePackage;", + "packageInstance": "new WithoutGeneratedCodePackage()", + "buildTypes": [], + "libraryName": "WithoutGeneratedCode", + "isPureCxxDependency": true + } + } + }, + "without-includes-generated-code": { + "root": "${withoutIncludesGeneratedCode.invariantSeparatorsPath}", + "name": "without-includes-generated-code", + "platforms": { + "android": { + "sourceDir": "${withoutIncludesGeneratedCode.invariantSeparatorsPath}/android", + "packageImportPath": "import com.facebook.react.WithoutIncludesGeneratedCodePackage;", + "packageInstance": "new WithoutIncludesGeneratedCodePackage()", + "buildTypes": [], + "libraryName": "WithoutIncludesGeneratedCode", + "isPureCxxDependency": true + } + } + }, + "without-codegen-config": { + "root": "${withoutCodegenConfig.invariantSeparatorsPath}", + "name": "without-codegen-config", + "platforms": { + "android": { + "sourceDir": "${withoutCodegenConfig.invariantSeparatorsPath}/android", + "packageImportPath": "import com.facebook.react.WithoutCodegenConfigPackage;", + "packageInstance": "new WithoutCodegenConfigPackage()", + "buildTypes": [], + "libraryName": "WithoutCodegenConfig", + "isPureCxxDependency": true + } + } + }, + "missing-non-pure-cxx-package": { + "root": "${missingNonPureCxxPackage.invariantSeparatorsPath}", + "name": "missing-non-pure-cxx-package", + "platforms": { + "android": { + "sourceDir": "${missingNonPureCxxPackage.invariantSeparatorsPath}/android", + "packageImportPath": "import com.facebook.react.MissingNonPureCxxPackage;", + "packageInstance": "new MissingNonPureCxxPackage()", + "buildTypes": [], + "libraryName": "MissingNonPureCxxPackage", + "isPureCxxDependency": false + } + } + } + } + } + """ + .trimIndent() + ) + + val result = ReactPlugin().getPureCxxCodegenDependencies(autolinkingFile) + + assertThat(result.map { it.name }) + .containsExactly("without-generated-code", "without-includes-generated-code") + } + + private fun createPackage(name: String, includesGeneratedCode: Boolean? = null): File { + val folder = tempFolder.newFolder(name) + File(folder, "package.json").writeText(packageJson(includesGeneratedCode)) + return folder + } + + private fun createPackageWithoutCodegenConfig(name: String): File { + val folder = tempFolder.newFolder(name) + File(folder, "package.json").writeText(packageJson()) + return folder + } + + private fun createAutolinkingFile(@Language("JSON") input: String) = + tempFolder.newFile("autolinking.json").apply { writeText(input) } + + private fun packageJson(includesGeneratedCode: Boolean?): String { + val includesGeneratedCodeLine = + includesGeneratedCode?.let { ""","includesGeneratedCode": $it""" } ?: "" + // language=JSON + return """ + { + "version": "1.0.0", + "codegenConfig": { + "name": "TestSpec", + "type": "modules", + "jsSrcsDir": "src"$includesGeneratedCodeLine + } + } + """ + .trimIndent() + } + + private fun packageJson(): String { + // language=JSON + return """ + { + "version": "1.0.0" + } + """ + .trimIndent() + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt index 6db0c9ecd2c..36034f08106 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt @@ -13,6 +13,7 @@ import com.facebook.react.model.ModelAutolinkingDependenciesPlatformAndroidJson import com.facebook.react.model.ModelAutolinkingDependenciesPlatformJson import com.facebook.react.tasks.GenerateAutolinkingNewArchitecturesFileTask.Companion.sanitizeCmakeListsPath import com.facebook.react.tests.createTestTask +import java.io.File import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.junit.Test @@ -41,6 +42,7 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest { assertThat(task.generatedOutputDirectory.get().asFile).isEqualTo(outputFolder) assertThat(task.generatedOutputDirectory.get().asFile).isEqualTo(outputFolder) + assertThat(task.generatedPureCxxSourceDirectory.isPresent).isFalse() } @Test @@ -178,6 +180,132 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest { ) } + @Test + fun generateCmakeFileContent_withGeneratedPureCxxPath_usesItWhenCmakeListsPathIsMissing() { + val generatedPureCxxSourceDirectory = tempFolder.newFolder("pureCxx") + val generatedCmakeListsPath = + File(generatedPureCxxSourceDirectory, "aPackage/jni/CMakeLists.txt").absolutePath + val generatedNativeFolderPath = sanitizeCmakeListsPath(generatedCmakeListsPath) + + val task = + createTestTask { + it.generatedPureCxxSourceDirectory.set(generatedPureCxxSourceDirectory) + } + + val output = + task.generateCmakeFileContent( + listOf( + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./a/directory", + packageImportPath = "import com.facebook.react.aPackage;", + packageInstance = "new APackage()", + buildTypes = emptyList(), + libraryName = "aPackage", + isPureCxxDependency = true, + ) + ) + ) + + assertThat(output) + .contains( + """ + if(EXISTS "$generatedNativeFolderPath") + add_subdirectory("$generatedNativeFolderPath" aPackage_autolinked_build) + list(APPEND AUTOLINKED_LIBRARIES react_codegen_aPackage) + else() + message(WARNING "React Native: Skipping autolinked library 'react_codegen_aPackage' because the source directory does not exist: $generatedNativeFolderPath") + endif() + """ + .trimIndent() + ) + } + + @Test + fun generateCmakeFileContent_withGeneratedPureCxxPath_preservesCmakeListsPath() { + val generatedPureCxxSourceDirectory = tempFolder.newFolder("pureCxx") + + val task = + createTestTask { + it.generatedPureCxxSourceDirectory.set(generatedPureCxxSourceDirectory) + } + + val output = + task.generateCmakeFileContent( + listOf( + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./a/directory", + packageImportPath = "import com.facebook.react.aPackage;", + packageInstance = "new APackage()", + buildTypes = emptyList(), + libraryName = "aPackage", + cmakeListsPath = "./a/directory/CMakeLists.txt", + isPureCxxDependency = true, + ) + ) + ) + + assertThat(output) + .contains( + """ + if(EXISTS "./a/directory/") + add_subdirectory("./a/directory/" aPackage_autolinked_build) + list(APPEND AUTOLINKED_LIBRARIES react_codegen_aPackage) + else() + message(WARNING "React Native: Skipping autolinked library 'react_codegen_aPackage' because the source directory does not exist: ./a/directory/") + endif() + """ + .trimIndent() + ) + assertThat(output).doesNotContain(generatedPureCxxSourceDirectory.absolutePath) + } + + @Test + fun generateCmakeFileContent_withGeneratedPureCxxPath_ignoresNonPureCxxDependency() { + val generatedPureCxxSourceDirectory = tempFolder.newFolder("pureCxx") + + val task = + createTestTask { + it.generatedPureCxxSourceDirectory.set(generatedPureCxxSourceDirectory) + } + + val output = + task.generateCmakeFileContent( + listOf( + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./a/directory", + packageImportPath = "import com.facebook.react.aPackage;", + packageInstance = "new APackage()", + buildTypes = emptyList(), + libraryName = "aPackage", + isPureCxxDependency = false, + ) + ) + ) + + assertThat(output).doesNotContain("aPackage_autolinked_build") + assertThat(output).doesNotContain(generatedPureCxxSourceDirectory.absolutePath) + } + + @Test + fun generateCmakeFileContent_withPureCxxDependencyAndMissingGeneratedDirectory_skipsIt() { + val output = + createTestTask() + .generateCmakeFileContent( + listOf( + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./a/directory", + packageImportPath = "import com.facebook.react.aPackage;", + packageInstance = "new APackage()", + buildTypes = emptyList(), + libraryName = "aPackage", + isPureCxxDependency = true, + ) + ) + ) + + assertThat(output).doesNotContain("aPackage_autolinked_build") + } + @Test fun generateCppFileContent_withNoPackages_returnsEmpty() { val output =