From 8672d36829e12e51133fdca4d8025b8d1ab1f19c Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Tue, 30 Apr 2024 04:33:56 -0700 Subject: [PATCH] RNGP - Move generateAutolinkingPackageList to core autolinking Summary: This diff is part of RFC0759 https://github.com/react-native-community/discussions-and-proposals/pull/759 Here I'm moving over the task to generate the Package List for Autolinking inside RNGP. The logic is the same as this one: https://github.com/react-native-community/cli/blob/73f880c3d87cdde81204364289f2f488a473c52b/packages/cli-platform-android/native_modules.gradle#L217 The class is generated as PackageList2 to avoid a duplicate class build failure with the current Autolinking from CLI. Changelog: [Internal] [Changed] - RNGP - Move generateAutolinkingPackageList to core autolinking Reviewed By: cipolleschi Differential Revision: D56637394 fbshipit-source-id: 929b42af3a0e1951cb7a0f4ace47bbbb84000780 --- .../kotlin/com/facebook/react/ReactPlugin.kt | 39 ++- ...olinkingDependenciesPlatformAndroidJson.kt | 8 +- .../react/tasks/GeneratePackageListTask.kt | 180 +++++++++++ .../tasks/GeneratePackageListTaskTest.kt | 306 ++++++++++++++++++ .../tasks/RunAutolinkingConfigTaskTest.kt | 2 +- 5 files changed, 525 insertions(+), 10 deletions(-) create mode 100644 packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt create mode 100644 packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index 4dffba5547c90f..af1193ceac2562 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -12,6 +12,7 @@ import com.android.build.gradle.internal.tasks.factory.dependsOn import com.facebook.react.internal.PrivateReactExtension import com.facebook.react.tasks.GenerateCodegenArtifactsTask import com.facebook.react.tasks.GenerateCodegenSchemaTask +import com.facebook.react.tasks.GeneratePackageListTask import com.facebook.react.tasks.RunAutolinkingConfigTask import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForApp import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForLibraries @@ -219,13 +220,41 @@ class ReactPlugin : Plugin { ) { val generatedAutolinkingDir: Provider = project.layout.buildDirectory.dir("generated/autolinking") + val generatedAutolinkingJavaDir: Provider = + project.layout.buildDirectory.dir("generated/autolinking/src/main/java") val configOutputFile = generatedAutolinkingDir.get().file("config-output.json") - project.tasks.register("runAutolinkingConfig", RunAutolinkingConfigTask::class.java) { task -> - task.autolinkConfigCommand.set(extension.autolinkConfigCommand) - task.autolinkConfigFile.set(extension.autolinkConfigFile) - task.autolinkOutputFile.set(configOutputFile) - task.autolinkLockFiles.set(extension.autolinkLockFiles) + val runAutolinkingConfigTask = + project.tasks.register("runAutolinkingConfig", RunAutolinkingConfigTask::class.java) { task + -> + task.autolinkConfigCommand.set(extension.autolinkConfigCommand) + task.autolinkConfigFile.set(extension.autolinkConfigFile) + task.autolinkOutputFile.set(configOutputFile) + task.autolinkLockFiles.set(extension.autolinkLockFiles) + } + + // 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. + val generatePackageListTask = + project.tasks.register( + "generateAutolinkingPackageList", GeneratePackageListTask::class.java) { task -> + task.dependsOn(runAutolinkingConfigTask) + task.autolinkInputFile.set(configOutputFile) + task.generatedOutputDirectory.set(generatedAutolinkingJavaDir) + } + + // We let generateAutolinkingPackageList depend on the preBuild task so it's executed before + // everything else. + project.tasks.named("preBuild", Task::class.java).dependsOn(generatePackageListTask) + + // We tell Android Gradle Plugin that inside /build/generated/autolinking/src/main/java there + // are sources to be compiled as well. + project.extensions.getByType(AndroidComponentsExtension::class.java).apply { + onVariants(selector().all()) { variant -> + variant.sources.java?.addStaticSourceDirectory( + generatedAutolinkingJavaDir.get().asFile.absolutePath) + } } } } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/model/ModelAutolinkingDependenciesPlatformAndroidJson.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/model/ModelAutolinkingDependenciesPlatformAndroidJson.kt index 4304dcf082f100..37fcec1d4fef1f 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/model/ModelAutolinkingDependenciesPlatformAndroidJson.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/model/ModelAutolinkingDependenciesPlatformAndroidJson.kt @@ -15,8 +15,8 @@ data class ModelAutolinkingDependenciesPlatformAndroidJson( val libraryName: String, val componentDescriptors: List, val cmakeListsPath: String, - val cxxModuleCMakeListsModuleName: String?, - val cxxModuleCMakeListsPath: String?, - val cxxModuleHeaderName: String?, - val dependencyConfiguration: String? + val cxxModuleCMakeListsModuleName: String? = null, + val cxxModuleCMakeListsPath: String? = null, + val cxxModuleHeaderName: String? = null, + val dependencyConfiguration: String? = null ) diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt new file mode 100644 index 00000000000000..6cf69d6bbf0fd8 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt @@ -0,0 +1,180 @@ +/* + * 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.tasks + +import com.facebook.react.model.ModelAutolinkingDependenciesJson +import com.facebook.react.utils.JsonUtils +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.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction + +abstract class GeneratePackageListTask : DefaultTask() { + + init { + group = "react" + } + + @get:InputFile abstract val autolinkInputFile: RegularFileProperty + + @get:OutputDirectory abstract val generatedOutputDirectory: DirectoryProperty + + @TaskAction + fun taskAction() { + val model = JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile) + + val packageName = + model?.project?.android?.packageName + ?: error( + "RNGP - Autolinking: Could not find project.android.packageName in react-native config output! Could not autolink packages without this field.") + val packages = model.dependencies?.values ?: emptyList() + + val packageImports = composePackageImports(packageName, packages) + val packageClassInstance = composePackageInstance(packageName, packages) + val generatedFileContents = composeFileContent(packageImports, packageClassInstance) + + val outputDir = generatedOutputDirectory.get().asFile + outputDir.mkdirs() + File(outputDir, GENERATED_FILENAME).apply { + parentFile.mkdirs() + writeText(generatedFileContents) + } + } + + internal fun composePackageImports( + packageName: String, + packages: Collection + ) = + packages.joinToString("\n") { entry -> + val packageImportPath = + requireNotNull(entry.platforms?.android?.packageImportPath) { + "RNGP - Autolinking: Missing `packageImportPath` in `config` for dependency ${entry.name}. This is required to generate the autolinking package list." + } + "// ${entry.name}\n${interpolateDynamicValues(packageImportPath, packageName)}" + } + + internal fun composePackageInstance( + packageName: String, + packages: Collection + ) = + if (packages.isEmpty()) { + "" + } else { + ",\n " + + packages.joinToString(",\n ") { entry -> + val packageInstance = + requireNotNull(entry.platforms?.android?.packageInstance) { + "RNGP - Autolinking: Missing `packageInstance` in `config` for dependency ${entry.name}. This is required to generate the autolinking package list." + } + interpolateDynamicValues(packageInstance, packageName) + } + } + + internal fun composeFileContent(packageImports: String, packageClassInstance: String): String = + generatedFileContentsTemplate + .replace("{{ packageImports }}", packageImports) + .replace("{{ packageClassInstances }}", packageClassInstance) + + companion object { + const val GENERATED_FILENAME = "com/facebook/react/PackageList2.java" + + /** + * Before adding the package replacement mechanism, BuildConfig and R classes were imported + * automatically into the scope of the file. We want to replace all non-FQDN references to those + * classes with the package name of the MainApplication. + * + * We want to match "R" or "BuildConfig": + * - new Package(R.string…), + * - Module.configure(BuildConfig); + * ^ hence including (BuildConfig|R) + * but we don't want to match "R": + * - new Package(getResources…), + * - new PackageR…, + * - new Royal…, + * ^ hence excluding \w before and after matches + * and "BuildConfig" that has FQDN reference: + * - Module.configure(com.acme.BuildConfig); + * ^ hence excluding . before the match. + */ + internal fun interpolateDynamicValues(input: String, packageName: String): String = + input.replace(Regex("([^.\\w])(BuildConfig|R)(\\W)")) { match -> + val (prefix, className, suffix) = match.destructured + "${prefix}${packageName}.${className}${suffix}" + } + + // language=java + val generatedFileContentsTemplate = + """ + package com.facebook.react; + + import android.app.Application; + import android.content.Context; + import android.content.res.Resources; + + import com.facebook.react.ReactPackage; + import com.facebook.react.shell.MainPackageConfig; + import com.facebook.react.shell.MainReactPackage; + import java.util.Arrays; + import java.util.ArrayList; + + {{ packageImports }} + + public class PackageList2 { + private Application application; + private ReactNativeHost reactNativeHost; + private MainPackageConfig mConfig; + + public PackageList(ReactNativeHost reactNativeHost) { + this(reactNativeHost, null); + } + + public PackageList(Application application) { + this(application, null); + } + + public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) { + this.reactNativeHost = reactNativeHost; + mConfig = config; + } + + public PackageList(Application application, MainPackageConfig config) { + this.reactNativeHost = null; + this.application = application; + mConfig = config; + } + + private ReactNativeHost getReactNativeHost() { + return this.reactNativeHost; + } + + private Resources getResources() { + return this.getApplication().getResources(); + } + + private Application getApplication() { + if (this.reactNativeHost == null) return this.application; + return this.reactNativeHost.getApplication(); + } + + private Context getApplicationContext() { + return this.getApplication().getApplicationContext(); + } + + public ArrayList getPackages() { + return new ArrayList<>(Arrays.asList( + new MainReactPackage(mConfig){{ packageClassInstances }} + )); + } + } + """ + .trimIndent() + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt new file mode 100644 index 00000000000000..b76bf3ce842c59 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt @@ -0,0 +1,306 @@ +/* + * 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.tasks + +import com.facebook.react.model.ModelAutolinkingDependenciesJson +import com.facebook.react.model.ModelAutolinkingDependenciesPlatformAndroidJson +import com.facebook.react.model.ModelAutolinkingDependenciesPlatformJson +import com.facebook.react.tests.createTestTask +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class GeneratePackageListTaskTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test + fun generatePackageListTask_groupIsSetCorrectly() { + val task = createTestTask {} + assertEquals("react", task.group) + } + + @Test + fun generatePackageListTask_staticInputs_areSetCorrectly() { + val outputFolder = tempFolder.newFolder("build") + val inputFile = tempFolder.newFile("config.json") + + val task = + createTestTask { + it.generatedOutputDirectory.set(outputFolder) + it.autolinkInputFile.set(inputFile) + } + + assertEquals(inputFile, task.inputs.files.singleFile) + assertEquals(outputFolder, task.outputs.files.singleFile) + } + + @Test + fun composePackageImports_withNoPackages_returnsEmpty() { + val task = createTestTask() + val packageName = "com.facebook.react" + val packages = emptyList() + val result = task.composePackageImports(packageName, packages) + assertEquals("", result) + } + + @Test + fun composePackageImports_withPackages_returnsImportCorrectly() { + val task = createTestTask() + val packageName = "com.facebook.react" + + val result = task.composePackageImports(packageName, testDependencies) + assertEquals( + """ + // @react-native/a-package + import com.facebook.react.aPackage; + // @react-native/another-package + import com.facebook.react.anotherPackage; + """ + .trimIndent(), + result) + } + + @Test + fun composePackageInstance_withNoPackages_returnsEmpty() { + val task = createTestTask() + val packageName = "com.facebook.react" + val packages = emptyList() + val result = task.composePackageInstance(packageName, packages) + assertEquals("", result) + } + + @Test + fun composePackageInstance_withPackages_returnsImportCorrectly() { + val task = createTestTask() + val packageName = "com.facebook.react" + + val result = task.composePackageInstance(packageName, testDependencies) + assertEquals( + """ + , + new APackage(), + new AnotherPackage() + """ + .trimIndent(), + result) + } + + @Test + fun interpolateDynamicValues_withNoBuildConfigOrROccurrencies_doesNothing() { + val packageName = "com.facebook.react" + val input = "com.facebook.react.aPackage" + val output = GeneratePackageListTask.interpolateDynamicValues(input, packageName) + assertEquals(input, output) + } + + @Test + fun interpolateDynamicValues_withR_doesQualifyThem() { + val packageName = "com.facebook.react" + val input = "new APackageWithR(R.string.value)" + val output = GeneratePackageListTask.interpolateDynamicValues(input, packageName) + assertEquals("new APackageWithR(com.facebook.react.R.string.value)", output) + } + + @Test + fun interpolateDynamicValues_withBuildConfig_doesQualifyThem() { + val packageName = "com.facebook.react" + val input = "new APackageWithBuildConfigInTheName(BuildConfig.VALUE)" + val output = GeneratePackageListTask.interpolateDynamicValues(input, packageName) + assertEquals( + "new APackageWithBuildConfigInTheName(com.facebook.react.BuildConfig.VALUE)", output) + } + + @Test + fun composeFileContent_withNoPackages_returnsValidFile() { + val task = createTestTask() + val packageName = "com.facebook.react" + val packages = emptyList() + val imports = task.composePackageImports(packageName, packages) + val instance = task.composePackageInstance(packageName, packages) + val result = task.composeFileContent(imports, instance) + // language=java + assertEquals( + """ + package com.facebook.react; + + import android.app.Application; + import android.content.Context; + import android.content.res.Resources; + + import com.facebook.react.ReactPackage; + import com.facebook.react.shell.MainPackageConfig; + import com.facebook.react.shell.MainReactPackage; + import java.util.Arrays; + import java.util.ArrayList; + + + + public class PackageList2 { + private Application application; + private ReactNativeHost reactNativeHost; + private MainPackageConfig mConfig; + + public PackageList(ReactNativeHost reactNativeHost) { + this(reactNativeHost, null); + } + + public PackageList(Application application) { + this(application, null); + } + + public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) { + this.reactNativeHost = reactNativeHost; + mConfig = config; + } + + public PackageList(Application application, MainPackageConfig config) { + this.reactNativeHost = null; + this.application = application; + mConfig = config; + } + + private ReactNativeHost getReactNativeHost() { + return this.reactNativeHost; + } + + private Resources getResources() { + return this.getApplication().getResources(); + } + + private Application getApplication() { + if (this.reactNativeHost == null) return this.application; + return this.reactNativeHost.getApplication(); + } + + private Context getApplicationContext() { + return this.getApplication().getApplicationContext(); + } + + public ArrayList getPackages() { + return new ArrayList<>(Arrays.asList( + new MainReactPackage(mConfig) + )); + } + } + """ + .trimIndent(), + result) + } + + @Test + fun composeFileContent_withPackages_returnsValidFile() { + val task = createTestTask() + val packageName = "com.facebook.react" + val imports = task.composePackageImports(packageName, testDependencies) + val instance = task.composePackageInstance(packageName, testDependencies) + val result = task.composeFileContent(imports, instance) + // language=java + assertEquals( + """ + package com.facebook.react; + + import android.app.Application; + import android.content.Context; + import android.content.res.Resources; + + import com.facebook.react.ReactPackage; + import com.facebook.react.shell.MainPackageConfig; + import com.facebook.react.shell.MainReactPackage; + import java.util.Arrays; + import java.util.ArrayList; + + // @react-native/a-package + import com.facebook.react.aPackage; + // @react-native/another-package + import com.facebook.react.anotherPackage; + + public class PackageList2 { + private Application application; + private ReactNativeHost reactNativeHost; + private MainPackageConfig mConfig; + + public PackageList(ReactNativeHost reactNativeHost) { + this(reactNativeHost, null); + } + + public PackageList(Application application) { + this(application, null); + } + + public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) { + this.reactNativeHost = reactNativeHost; + mConfig = config; + } + + public PackageList(Application application, MainPackageConfig config) { + this.reactNativeHost = null; + this.application = application; + mConfig = config; + } + + private ReactNativeHost getReactNativeHost() { + return this.reactNativeHost; + } + + private Resources getResources() { + return this.getApplication().getResources(); + } + + private Application getApplication() { + if (this.reactNativeHost == null) return this.application; + return this.reactNativeHost.getApplication(); + } + + private Context getApplicationContext() { + return this.getApplication().getApplicationContext(); + } + + public ArrayList getPackages() { + return new ArrayList<>(Arrays.asList( + new MainReactPackage(mConfig), + new APackage(), + new AnotherPackage() + )); + } + } + """ + .trimIndent(), + result) + } + + private val testDependencies = + listOf( + ModelAutolinkingDependenciesJson( + "root", + "@react-native/a-package", + ModelAutolinkingDependenciesPlatformJson( + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./a/directory", + packageImportPath = "import com.facebook.react.aPackage;", + packageInstance = "new APackage()", + buildTypes = emptyList(), + libraryName = "aPackage", + componentDescriptors = emptyList(), + cmakeListsPath = "./a/directory/CMakeLists.txt", + ))), + ModelAutolinkingDependenciesJson( + "root", + "@react-native/another-package", + ModelAutolinkingDependenciesPlatformJson( + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./another/directory", + packageImportPath = "import com.facebook.react.anotherPackage;", + packageInstance = "new AnotherPackage()", + buildTypes = emptyList(), + libraryName = "anotherPackage", + componentDescriptors = emptyList(), + cmakeListsPath = "./another/directory/CMakeLists.txt", + )))) +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTaskTest.kt index 32470eca12c09f..45d258321adbd2 100644 --- a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTaskTest.kt +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTaskTest.kt @@ -24,7 +24,7 @@ class RunAutolinkingConfigTaskTest { @Test fun runAutolinkingConfigTask_groupIsSetCorrectly() { - val task = createTestTask {} + val task = createTestTask {} assertEquals("react", task.group) }