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
This commit is contained in:
Nicola Corti
2024-04-30 04:33:56 -07:00
committed by Facebook GitHub Bot
parent cc5bab83ab
commit 0d37d0a4b7
5 changed files with 525 additions and 10 deletions
@@ -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<Project> {
) {
val generatedAutolinkingDir: Provider<Directory> =
project.layout.buildDirectory.dir("generated/autolinking")
val generatedAutolinkingJavaDir: Provider<Directory> =
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)
}
}
}
}
@@ -15,8 +15,8 @@ data class ModelAutolinkingDependenciesPlatformAndroidJson(
val libraryName: String,
val componentDescriptors: List<String>,
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
)
@@ -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<ModelAutolinkingDependenciesJson>
) =
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<ModelAutolinkingDependenciesJson>
) =
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<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>asList(
new MainReactPackage(mConfig){{ packageClassInstances }}
));
}
}
"""
.trimIndent()
}
}
@@ -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<GeneratePackageListTask> {}
assertEquals("react", task.group)
}
@Test
fun generatePackageListTask_staticInputs_areSetCorrectly() {
val outputFolder = tempFolder.newFolder("build")
val inputFile = tempFolder.newFile("config.json")
val task =
createTestTask<GeneratePackageListTask> {
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<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val packages = emptyList<ModelAutolinkingDependenciesJson>()
val result = task.composePackageImports(packageName, packages)
assertEquals("", result)
}
@Test
fun composePackageImports_withPackages_returnsImportCorrectly() {
val task = createTestTask<GeneratePackageListTask>()
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<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val packages = emptyList<ModelAutolinkingDependenciesJson>()
val result = task.composePackageInstance(packageName, packages)
assertEquals("", result)
}
@Test
fun composePackageInstance_withPackages_returnsImportCorrectly() {
val task = createTestTask<GeneratePackageListTask>()
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<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val packages = emptyList<ModelAutolinkingDependenciesJson>()
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<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>asList(
new MainReactPackage(mConfig)
));
}
}
"""
.trimIndent(),
result)
}
@Test
fun composeFileContent_withPackages_returnsValidFile() {
val task = createTestTask<GeneratePackageListTask>()
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<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>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",
))))
}
@@ -24,7 +24,7 @@ class RunAutolinkingConfigTaskTest {
@Test
fun runAutolinkingConfigTask_groupIsSetCorrectly() {
val task = createTestTask<BundleHermesCTask> {}
val task = createTestTask<RunAutolinkingConfigTask> {}
assertEquals("react", task.group)
}