From eada3f96b25f261b0f605db31493ae75991dc1ec Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:25:12 +0100 Subject: [PATCH 01/10] Bump Gradle to 9.4 (AGP to 9.1, Kotlin to 2.3) ## Summary: Follow-up to + #55453 + react-native-community/template#205 - bump Gradle wrapper from 9.3.1 to 9.4.0 (root, RNGP, helloworld) - bump RNGP: AGP from 8.12.0 to 9.1.0, Kotlin from 2.1.20 to 2.3.0 - bump RNGP Kotlin compiler API ver from KOTLIN_1_8 to KOTLIN_2_3 - AGP 9.1 DSL syntax updates - ReactPlugin.kt - fix: 'Argument type mismatch: actual type is File, but String was expected' - AGP 9.1 changed java.srcDir() to use directories.add(), expects String paths (not File objects) - old .asFile returns File, directories.add() needs a String - use .asFile.absolutePath to convert File to String path - AgpConfiguratorUtils.kt - fix: unresolved reference 'namespace' - AGP 9.1 removed direct namespace prop from LibraryAndroidComponentsExtension - use components.finalizeDsl { dsl -> dsl.namespace = ... } - fix: NoSuchMethodError for buildFeatures, 'LibraryBuildFeatures LibraryExtension.getBuildFeatures()' - AGP 9.1 removed direct property accessors and changed buildFeatures API to use lambda syntax - change `buildFeatures` and `defaultConfig` from direct property assignment to lambda syntax (`ext.buildFeatures { ... }` instead of `ext.buildFeatures`) - change `defaultConfig.buildConfigField()` and `defaultConfig.resValue()` to use lambda syntax (`ext.defaultConfig { ... }`) - NdkConfiguratorUtils.kt - remove deprecated `ext.buildFeatures.prefab = true` (AGP 9.1+ libs declare prefab config directly in their build.gradle) - replace direct `cmake.arguments` manipulation with `ext.defaultConfig { }` lambda syntax - keep manual CMake argument additions (PROJECT_BUILD_DIR, PROJECT_ROOT_DIR, REACT_ANDROID_DIR, REACT_COMMON_DIR, ANDROID_STL, ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES) - NB: AGP 9.1 changed DSL access - `defaultConfig` is now a lambda method, not a direct property - ReactAndroid/build.gradle.kts - remove `java.exclude()` calls for processing and module processing packages causing AGP 9.1 compilation errors - rn-tester/build.gradle.kts - change from `afterEvaluate` block to `tasks.withType<>().configureEach` - update task deps to use `configureEach` instead of `getByName()` - NB: more reliable task config w AGP 9.1 - JsonUtilsTest.kt - fix :gradle-plugin:shared:compileTestKotlin warnings, unnecessary non-null assertion (!!) on a non-null receiver of type '...' - causing :packages:rn-tester:android:app:benchmark:stripHermesBenchmarkDebugSymbols to fail then configureCMakeDebug Then follow-up react-native-community/template gradlew and kotlinVersion updates ## Changelog: [ANDROID] [CHANGED] - Gradle to 9.4.0, Kotlin 2.3.0 and AGP 9.1.0 ## Test Plan: - leotm/react-native-template-new-architecture#1933 - .github/actions/build-android/action.yml locally with prebuilt hermes-android mvnrepository.com/artifact/com.facebook.hermes/hermes-android/0.16.0 - JDK 26 sec incompat, JDK 17 ok, prebuilt stable com.facebook.hermes:hermes-android:0.16.0 artifact - useHermesStable=true, useHermesNightly=false, hermesV1Enabled=false, .hermesversion, version.properties, skip buildCodegenCLI - --dry-run -PreactNativeArchitectures=arm64-v8a -PenableWarningsAsErrors=true - .github\actions\build-fantom-runner\action.yml locally requires private:react-native-fantom and compiling hermes from source w debug flags (SLOW) --- gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle-plugin/gradle/libs.versions.toml | 4 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../build.gradle.kts | 2 +- .../kotlin/com/facebook/react/ReactPlugin.kt | 4 +- .../react/utils/AgpConfiguratorUtils.kt | 77 ++++++++++--------- .../react/utils/NdkConfiguratorUtils.kt | 47 ++++++----- .../settings-plugin/build.gradle.kts | 2 +- .../shared-testutil/build.gradle.kts | 2 +- .../gradle-plugin/shared/build.gradle.kts | 2 +- .../com/facebook/react/utils/JsonUtilsTest.kt | 6 +- .../ReactAndroid/build.gradle.kts | 2 - .../rn-tester/android/app/build.gradle.kts | 33 +++----- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 14 files changed, 93 insertions(+), 94 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f78a6af837..dbc3ce4a040f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/packages/gradle-plugin/gradle/libs.versions.toml b/packages/gradle-plugin/gradle/libs.versions.toml index c7502fcefd98..9922e5575473 100644 --- a/packages/gradle-plugin/gradle/libs.versions.toml +++ b/packages/gradle-plugin/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -agp = "8.12.0" +agp = "9.1.0" gson = "2.8.9" guava = "31.0.1-jre" javapoet = "1.13.0" junit = "4.13.2" -kotlin = "2.1.20" +kotlin = "2.3.0" assertj = "3.25.1" ktfmt = "0.22.0" diff --git a/packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties index 37f78a6af837..dbc3ce4a040f 100644 --- a/packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties +++ b/packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts b/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts index 85dadeba0c42..8f3b931b8e43 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts @@ -64,7 +64,7 @@ kotlin { jvmToolchain(17) } tasks.withType().configureEach { compilerOptions { - apiVersion.set(KotlinVersion.KOTLIN_1_8) + apiVersion.set(KotlinVersion.KOTLIN_2_3) // See comment above on JDK 11 support jvmTarget.set(JvmTarget.JVM_11) allWarningsAsErrors.set( 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 e7859ff6ad0b..9440eee3f3fe 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 @@ -234,12 +234,12 @@ class ReactPlugin : Plugin { if (isLibrary) { project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).finalizeDsl { ext -> - ext.sourceSets.getByName("main").java.srcDir(generatedSrcDir.get().dir("java").asFile) + ext.sourceSets.getByName("main").java.directories.add(generatedSrcDir.get().dir("java").asFile.absolutePath) } } else { project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl { ext -> - ext.sourceSets.getByName("main").java.srcDir(generatedSrcDir.get().dir("java").asFile) + ext.sourceSets.getByName("main").java.directories.add(generatedSrcDir.get().dir("java").asFile.absolutePath) } } diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt index c4a7b359d542..4aab7b9f7579 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt @@ -9,7 +9,6 @@ package com.facebook.react.utils import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension -import com.android.build.gradle.LibraryExtension import com.facebook.react.ReactExtension import com.facebook.react.utils.ProjectUtils.isEdgeToEdgeEnabled import com.facebook.react.utils.ProjectUtils.isHermesEnabled @@ -59,18 +58,21 @@ internal object AgpConfiguratorUtils { project.extensions .getByType(ApplicationAndroidComponentsExtension::class.java) .finalizeDsl { ext -> - ext.buildFeatures.buildConfig = true - ext.defaultConfig.buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true") - ext.defaultConfig.buildConfigField( - "boolean", - "IS_HERMES_ENABLED", - project.isHermesEnabled.toString(), - ) - ext.defaultConfig.buildConfigField( - "boolean", - "IS_EDGE_TO_EDGE_ENABLED", - project.isEdgeToEdgeEnabled.toString(), - ) + ext.buildFeatures { buildConfig = true } + // In AGP 9.1, buildConfigField is accessed via defaultConfig + ext.defaultConfig { + buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true") + buildConfigField( + "boolean", + "IS_HERMES_ENABLED", + project.isHermesEnabled.toString(), + ) + buildConfigField( + "boolean", + "IS_EDGE_TO_EDGE_ENABLED", + project.isEdgeToEdgeEnabled.toString(), + ) + } } } project.pluginManager.withPlugin("com.android.application", action) @@ -82,7 +84,7 @@ internal object AgpConfiguratorUtils { subproject.pluginManager.withPlugin("com.android.library") { subproject.extensions .getByType(LibraryAndroidComponentsExtension::class.java) - .finalizeDsl { ext -> ext.buildFeatures.buildConfig = true } + .finalizeDsl { ext -> ext.buildFeatures { buildConfig = true } } } } } @@ -97,13 +99,16 @@ internal object AgpConfiguratorUtils { project.extensions .getByType(ApplicationAndroidComponentsExtension::class.java) .finalizeDsl { ext -> - ext.buildFeatures.resValues = true - ext.defaultConfig.resValue( - "string", - "react_native_dev_server_ip", - devServerIp, - ) - ext.defaultConfig.resValue("integer", "react_native_dev_server_port", devServerPort) + ext.buildFeatures { resValues = true } + // In AGP 9.1, resValue is accessed via defaultConfig + ext.defaultConfig { + resValue( + "string", + "react_native_dev_server_ip", + devServerIp, + ) + resValue("integer", "react_native_dev_server_port", devServerPort) + } } } @@ -114,22 +119,22 @@ internal object AgpConfiguratorUtils { fun configureNamespaceForLibraries(appProject: Project) { appProject.rootProject.allprojects { subproject -> subproject.pluginManager.withPlugin("com.android.library") { - subproject.extensions + val components = subproject.extensions .getByType(LibraryAndroidComponentsExtension::class.java) - .finalizeDsl { ext -> - if (ext.namespace == null) { - val android = subproject.extensions.getByType(LibraryExtension::class.java) - val manifestFile = android.sourceSets.getByName("main").manifest.srcFile - - manifestFile - .takeIf { it.exists() } - ?.let { file -> - getPackageNameFromManifest(file)?.let { packageName -> - ext.namespace = packageName - } - } - } - } + + components.finalizeDsl { dsl -> + if (dsl.namespace.isNullOrEmpty()) { + val manifestFile = subproject.file("src/main/AndroidManifest.xml") + + manifestFile + .takeIf { it.exists() } + ?.let { file -> + getPackageNameFromManifest(file)?.let { packageName -> + dsl.namespace = packageName + } + } + } + } } } } diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/NdkConfiguratorUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/NdkConfiguratorUtils.kt index 119c80de03c6..54da853e3fb9 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/NdkConfiguratorUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/NdkConfiguratorUtils.kt @@ -20,10 +20,6 @@ internal object NdkConfiguratorUtils { project.pluginManager.withPlugin("com.android.application") { project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl { ext -> - // We enable prefab so users can consume .so/headers from ReactAndroid and hermes-engine - // .aar - ext.buildFeatures.prefab = true - // If the user has not provided a CmakeLists.txt path, let's provide // the default one from the framework if (ext.externalNativeBuild.cmake.path == null) { @@ -36,23 +32,32 @@ internal object NdkConfiguratorUtils { // Parameters should be provided in an additive manner (do not override what // the user provided, but allow for sensible defaults). - val cmakeArgs = ext.defaultConfig.externalNativeBuild.cmake.arguments - if (cmakeArgs.none { it.startsWith("-DPROJECT_BUILD_DIR") }) { - cmakeArgs.add("-DPROJECT_BUILD_DIR=${project.layout.buildDirectory.get().asFile}") - } - if (cmakeArgs.none { it.startsWith("-DPROJECT_ROOT_DIR") }) { - cmakeArgs.add("-DPROJECT_ROOT_DIR=${project.rootProject.layout.projectDirectory.asFile}") - } - if (cmakeArgs.none { it.startsWith("-DREACT_ANDROID_DIR") }) { - cmakeArgs.add( - "-DREACT_ANDROID_DIR=${extension.reactNativeDir.file("ReactAndroid").get().asFile}" - ) - } - if (cmakeArgs.none { it.startsWith("-DANDROID_STL") }) { - cmakeArgs.add("-DANDROID_STL=c++_shared") - } - if (cmakeArgs.none { it.startsWith("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES") }) { - cmakeArgs.add("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON") + // In AGP 9.1, defaultConfig is accessed as a lambda + ext.defaultConfig { + // Access cmake arguments through externalNativeBuild + val cmakeArgs = externalNativeBuild.cmake.arguments + if (cmakeArgs.none { it.startsWith("-DPROJECT_BUILD_DIR") }) { + cmakeArgs.add("-DPROJECT_BUILD_DIR=${project.layout.buildDirectory.get().asFile}") + } + if (cmakeArgs.none { it.startsWith("-DPROJECT_ROOT_DIR") }) { + cmakeArgs.add("-DPROJECT_ROOT_DIR=${project.rootProject.layout.projectDirectory.asFile}") + } + if (cmakeArgs.none { it.startsWith("-DREACT_ANDROID_DIR") }) { + cmakeArgs.add( + "-DREACT_ANDROID_DIR=${extension.reactNativeDir.file("ReactAndroid").get().asFile}" + ) + } + if (cmakeArgs.none { it.startsWith("-DREACT_COMMON_DIR") }) { + cmakeArgs.add( + "-DREACT_COMMON_DIR=${extension.reactNativeDir.file("ReactCommon").get().asFile}" + ) + } + if (cmakeArgs.none { it.startsWith("-DANDROID_STL") }) { + cmakeArgs.add("-DANDROID_STL=c++_shared") + } + if (cmakeArgs.none { it.startsWith("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES") }) { + cmakeArgs.add("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON") + } } val architectures = project.getReactNativeArchitectures() diff --git a/packages/gradle-plugin/settings-plugin/build.gradle.kts b/packages/gradle-plugin/settings-plugin/build.gradle.kts index 2fbeafd502c9..165bf38a8665 100644 --- a/packages/gradle-plugin/settings-plugin/build.gradle.kts +++ b/packages/gradle-plugin/settings-plugin/build.gradle.kts @@ -54,7 +54,7 @@ kotlin { jvmToolchain(17) } tasks.withType().configureEach { compilerOptions { - apiVersion.set(KotlinVersion.KOTLIN_1_8) + apiVersion.set(KotlinVersion.KOTLIN_2_3) // See comment above on JDK 11 support jvmTarget.set(JvmTarget.JVM_11) allWarningsAsErrors.set( diff --git a/packages/gradle-plugin/shared-testutil/build.gradle.kts b/packages/gradle-plugin/shared-testutil/build.gradle.kts index 26173fb55578..19dd3a1333df 100644 --- a/packages/gradle-plugin/shared-testutil/build.gradle.kts +++ b/packages/gradle-plugin/shared-testutil/build.gradle.kts @@ -27,7 +27,7 @@ kotlin { jvmToolchain(17) } tasks.withType().configureEach { compilerOptions { - apiVersion.set(KotlinVersion.KOTLIN_1_8) + apiVersion.set(KotlinVersion.KOTLIN_2_3) // See comment above on JDK 11 support jvmTarget.set(JvmTarget.JVM_11) allWarningsAsErrors.set( diff --git a/packages/gradle-plugin/shared/build.gradle.kts b/packages/gradle-plugin/shared/build.gradle.kts index 315984875ad8..87fb331634ed 100644 --- a/packages/gradle-plugin/shared/build.gradle.kts +++ b/packages/gradle-plugin/shared/build.gradle.kts @@ -33,7 +33,7 @@ kotlin { jvmToolchain(17) } tasks.withType().configureEach { compilerOptions { - apiVersion.set(KotlinVersion.KOTLIN_1_8) + apiVersion.set(KotlinVersion.KOTLIN_2_3) // See comment above on JDK 11 support jvmTarget.set(JvmTarget.JVM_11) allWarningsAsErrors.set( diff --git a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt index 0544a8775470..cbc6601ce6a8 100644 --- a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt +++ b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt @@ -86,9 +86,9 @@ class JsonUtilsTest { val parsed = JsonUtils.fromPackageJson(validJson)!! - assertThat("an awesome library").isEqualTo(parsed.codegenConfig!!.name) - assertThat("../js/").isEqualTo(parsed.codegenConfig!!.jsSrcsDir) - assertThat("com.awesome.library").isEqualTo(parsed.codegenConfig!!.android!!.javaPackageName) + assertThat("an awesome library").isEqualTo(parsed.codegenConfig.name) + assertThat("../js/").isEqualTo(parsed.codegenConfig.jsSrcsDir) + assertThat("com.awesome.library").isEqualTo(parsed.codegenConfig.android.javaPackageName) } @Test diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index c8435862e17c..e09e0effb4eb 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -627,8 +627,6 @@ android { "src/main/res/views/view", ) ) - java.exclude("com/facebook/react/processing") - java.exclude("com/facebook/react/module/processing") } lint { diff --git a/packages/rn-tester/android/app/build.gradle.kts b/packages/rn-tester/android/app/build.gradle.kts index fc1770cbd068..14497b77a2e7 100644 --- a/packages/rn-tester/android/app/build.gradle.kts +++ b/packages/rn-tester/android/app/build.gradle.kts @@ -184,26 +184,17 @@ tasks.withType().configureEach { } } -afterEvaluate { - if ( - (project.findProperty("react.internal.useHermesNightly") == null || - project.findProperty("react.internal.useHermesNightly").toString() == "false") && - (project.findProperty("react.internal.useHermesStable") == null || - project.findProperty("react.internal.useHermesStable").toString() == "false") - ) { - // As we're consuming Hermes from source, we want to make sure - // `hermesc` is built before we actually invoke the `emit*HermesResource` task - tasks - .getByName("createBundleReleaseJsAndAssets") - .dependsOn(":packages:react-native:ReactAndroid:hermes-engine:buildHermesC") - } +// As we're consuming Hermes from source, we want to make sure +// `hermesc` is built before we actually invoke the `emit*HermesResource` task +tasks.withType().configureEach { + dependsOn(":packages:react-native:ReactAndroid:hermes-engine:buildHermesC") +} - // As RN-Tester consumes the codegen from source, we need to make sure the codegen exists before - // we can actually invoke it. It's built by the ReactAndroid:buildCodegenCLI task. - tasks - .getByName("generateCodegenSchemaFromJavaScript") - .dependsOn(":packages:react-native:ReactAndroid:buildCodegenCLI") - tasks - .getByName("createBundleReleaseJsAndAssets") - .dependsOn(":packages:react-native:ReactAndroid:buildCodegenCLI") +// As RN-Tester consumes the codegen from source, we need to make sure the codegen exists before +// we can actually invoke it. It's built by the ReactAndroid:buildCodegenCLI task. +tasks.withType().configureEach { + dependsOn(":packages:react-native:ReactAndroid:buildCodegenCLI") +} +tasks.withType().configureEach { + dependsOn(":packages:react-native:ReactAndroid:buildCodegenCLI") } diff --git a/private/helloworld/android/gradle/wrapper/gradle-wrapper.properties b/private/helloworld/android/gradle/wrapper/gradle-wrapper.properties index 37f78a6af837..dbc3ce4a040f 100644 --- a/private/helloworld/android/gradle/wrapper/gradle-wrapper.properties +++ b/private/helloworld/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From a278ee50a3c9c1b24bf98a7f75da6fe61b91cd86 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:30:30 +0100 Subject: [PATCH 02/10] Fix Kotlin 2.3 warnings and CMake config gen - fix compileTestKotlin warnings by removing redundant null assertions and storing dependencies in local variables - add generateReactAndroidConfig task to gen ReactAndroidConfig.cmake for find_package() support - fix CMakeTask configuration to use tasks.configureEach instead of tasks.withType for internal AGP classes --- .../com/facebook/react/utils/JsonUtilsTest.kt | 77 ++++++++++--------- .../ReactAndroid/build.gradle.kts | 41 ++++++++++ .../cmake-utils/ReactNative-application.cmake | 6 ++ 3 files changed, 89 insertions(+), 35 deletions(-) diff --git a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt index cbc6601ce6a8..ebcddcc7f086 100644 --- a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt +++ b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt @@ -85,10 +85,12 @@ class JsonUtilsTest { ) val parsed = JsonUtils.fromPackageJson(validJson)!! + val codegenConfig = parsed.codegenConfig!! + val android = codegenConfig.android!! - assertThat("an awesome library").isEqualTo(parsed.codegenConfig.name) - assertThat("../js/").isEqualTo(parsed.codegenConfig.jsSrcsDir) - assertThat("com.awesome.library").isEqualTo(parsed.codegenConfig.android.javaPackageName) + assertThat("an awesome library").isEqualTo(codegenConfig.name) + assertThat("../js/").isEqualTo(codegenConfig.jsSrcsDir) + assertThat("com.awesome.library").isEqualTo(android.javaPackageName) } @Test @@ -180,15 +182,17 @@ class JsonUtilsTest { .trimIndent() ) val parsed = JsonUtils.fromAutolinkingConfigJson(validJson)!! - - assertThat("./packages/rn-tester").isEqualTo(parsed.project!!.android!!.sourceDir) - assertThat("RN-Tester").isEqualTo(parsed.project!!.android!!.appName) - assertThat("com.facebook.react.uiapp").isEqualTo(parsed.project!!.android!!.packageName) - assertThat("com.facebook.react.uiapp").isEqualTo(parsed.project!!.android!!.applicationId) - assertThat(".RNTesterActivity").isEqualTo(parsed.project!!.android!!.mainActivity) + val project = parsed.project!! + val android = project.android!! + + assertThat("./packages/rn-tester").isEqualTo(android.sourceDir) + assertThat("RN-Tester").isEqualTo(android.appName) + assertThat("com.facebook.react.uiapp").isEqualTo(android.packageName) + assertThat("com.facebook.react.uiapp").isEqualTo(android.applicationId) + assertThat(".RNTesterActivity").isEqualTo(android.mainActivity) assertThat("--mode HermesDebug") - .isEqualTo(parsed.project!!.android!!.watchModeCommandParams!![0]) - assertThat("implementation").isEqualTo(parsed.project!!.android!!.dependencyConfiguration) + .isEqualTo(android.watchModeCommandParams!![0]) + assertThat("implementation").isEqualTo(android.dependencyConfiguration) } @Test @@ -224,20 +228,22 @@ class JsonUtilsTest { "dependencyConfiguration": "implementation" } } - } + } """ .trimIndent() ) val parsed = JsonUtils.fromAutolinkingConfigJson(validJson)!! - - assertThat("./packages/rn-tester").isEqualTo(parsed.project!!.android!!.sourceDir) - assertThat("RN-Tester").isEqualTo(parsed.project!!.android!!.appName) - assertThat("com.facebook.react.uiapp").isEqualTo(parsed.project!!.android!!.packageName) - assertThat("com.facebook.react.uiapp").isEqualTo(parsed.project!!.android!!.applicationId) - assertThat(".RNTesterActivity").isEqualTo(parsed.project!!.android!!.mainActivity) + val project = parsed.project!! + val android = project.android!! + + assertThat("./packages/rn-tester").isEqualTo(android.sourceDir) + assertThat("RN-Tester").isEqualTo(android.appName) + assertThat("com.facebook.react.uiapp").isEqualTo(android.packageName) + assertThat("com.facebook.react.uiapp").isEqualTo(android.applicationId) + assertThat(".RNTesterActivity").isEqualTo(android.mainActivity) assertThat("--mode HermesDebug") - .isEqualTo(parsed.project!!.android!!.watchModeCommandParams!![0]) - assertThat("implementation").isEqualTo(parsed.project!!.android!!.dependencyConfiguration) + .isEqualTo(android.watchModeCommandParams!![0]) + assertThat("implementation").isEqualTo(android.dependencyConfiguration) } @Test @@ -282,51 +288,52 @@ class JsonUtilsTest { .trimIndent() ) val parsed = JsonUtils.fromAutolinkingConfigJson(validJson)!! + val dependencies = parsed.dependencies!! assertThat("./node_modules/@react-native/oss-library-example") - .isEqualTo(parsed.dependencies!!["@react-native/oss-library-example"]!!.root) + .isEqualTo(dependencies["@react-native/oss-library-example"]!!.root) assertThat("@react-native/oss-library-example") - .isEqualTo(parsed.dependencies!!["@react-native/oss-library-example"]!!.name) + .isEqualTo(dependencies["@react-native/oss-library-example"]!!.name) assertThat("react-native_oss-library-example") - .isEqualTo(parsed.dependencies!!["@react-native/oss-library-example"]!!.nameCleansed) + .isEqualTo(dependencies["@react-native/oss-library-example"]!!.nameCleansed) assertThat("./node_modules/@react-native/oss-library-example/android") .isEqualTo( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .sourceDir ) assertThat("import com.facebook.react.osslibraryexample.OSSLibraryExamplePackage;") .isEqualTo( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .packageImportPath ) assertThat("new OSSLibraryExamplePackage()") .isEqualTo( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .packageInstance ) assertThat(listOf("staging", "debug", "release")) .isEqualTo( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .buildTypes ) assertThat("OSSLibraryExampleSpec") .isEqualTo( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .libraryName ) assertThat(listOf("SampleNativeComponentComponentDescriptor")) .isEqualTo( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .componentDescriptors @@ -335,27 +342,27 @@ class JsonUtilsTest { "./node_modules/@react-native/oss-library-example/android/build/generated/source/codegen/jni/CMakeLists.txt" ) .isEqualTo( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .cmakeListsPath ) assertThat( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .cxxModuleHeaderName ) .isNull() assertThat( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .cxxModuleCMakeListsPath ) .isNull() assertThat( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .cxxModuleCMakeListsModuleName @@ -363,13 +370,13 @@ class JsonUtilsTest { .isNull() assertThat("implementation") .isEqualTo( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .dependencyConfiguration ) assertThat( - parsed.dependencies!!["@react-native/oss-library-example"]!! + dependencies["@react-native/oss-library-example"]!! .platforms!! .android!! .isPureCxxDependency!! diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index e09e0effb4eb..0d45450e2b6f 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -677,6 +677,47 @@ android { } } } + + // Generate CMake config file for find_package support + // This is needed for ReactNative-application.cmake to find ReactAndroid via find_package() + val generateReactAndroidConfig by tasks.registering { + outputs.dir(buildDir.resolve("cmake/ReactAndroid")) + doLast { + val configDir = outputs.files.first() + configDir.mkdirs() + val configContent = """ + # 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. + + # ReactAndroid CMake configuration file + + # The reactnative library + add_library(reactnative SHARED IMPORTED) + set_target_properties(reactnative PROPERTIES + IMPORTED_LOCATION "${buildDir}/prefab/debug/android/armeabi-v7a/libreactnative.so" + INTERFACE_INCLUDE_DIRECTORIES "${buildDir}/prefab-headers/reactnative" + ) + + # The jsi library + add_library(jsi SHARED IMPORTED) + set_target_properties(jsi PROPERTIES + IMPORTED_LOCATION "${buildDir}/prefab/debug/android/armeabi-v7a/libjsi.so" + INTERFACE_INCLUDE_DIRECTORIES "${buildDir}/prefab-headers/jsi" + ) + + """.trimIndent() + configDir.resolve("ReactAndroidConfig.cmake").writeText(configContent) + } + } + + // Ensure the config file is generated before external native build + tasks.configureEach { + if (this::class.java.name.contains("CMakeTask")) { + dependsOn(generateReactAndroidConfig) + } + } } tasks.withType().configureEach { diff --git a/packages/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake b/packages/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake index baa0fefa95ec..a6f1ccecd46d 100644 --- a/packages/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake +++ b/packages/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake @@ -47,6 +47,12 @@ if (PROJECT_ROOT_DIR) # variable is defined if user need to access it. endif () +# Set ReactAndroid_DIR to help find_package() locate ReactAndroidConfig.cmake +# The config file is generated by Gradle during the build and placed in the build directory +if (EXISTS "${REACT_ANDROID_DIR}/build/cmake/ReactAndroid/ReactAndroidConfig.cmake") + set(ReactAndroid_DIR "${REACT_ANDROID_DIR}/build/cmake/ReactAndroid" CACHE PATH "ReactAndroid CMake config directory") +endif () + file(GLOB override_cpp_SRC CONFIGURE_DEPENDS *.cpp) # We check if the user is providing a custom OnLoad.cpp file. If so, we pick that # for compilation. Otherwise we fallback to using the `default-app-setup/OnLoad.cpp` From c6370e89fcd5d29639029a56f4924b2d7117af05 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:43:10 +0100 Subject: [PATCH 03/10] Fix CMake config gen for AGP 9.1+ - use tasks.withType().configureEach instead of tasks.configureEach to properly find lazily created CMake tasks in AGP 9.1+ - use ${CMAKE_ANDROID_ARCH_ABI} variable instead of hardcoded armeabi-v7a for architecture-agnostic paths --- .../react-native/ReactAndroid/build.gradle.kts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 0d45450e2b6f..61b86937b201 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -692,18 +692,20 @@ android { # LICENSE file in the root directory of this source tree. # ReactAndroid CMake configuration file + # This file is generated during the Gradle build and used by CMake's find_package() - # The reactnative library + # The reactnative library - use the first available architecture + # The actual architecture will be determined by the CMAKE_ANDROID_ARCH_ABI variable add_library(reactnative SHARED IMPORTED) set_target_properties(reactnative PROPERTIES - IMPORTED_LOCATION "${buildDir}/prefab/debug/android/armeabi-v7a/libreactnative.so" + IMPORTED_LOCATION "${buildDir}/prefab/debug/android/\${CMAKE_ANDROID_ARCH_ABI}/libreactnative.so" INTERFACE_INCLUDE_DIRECTORIES "${buildDir}/prefab-headers/reactnative" ) # The jsi library add_library(jsi SHARED IMPORTED) set_target_properties(jsi PROPERTIES - IMPORTED_LOCATION "${buildDir}/prefab/debug/android/armeabi-v7a/libjsi.so" + IMPORTED_LOCATION "${buildDir}/prefab/debug/android/\${CMAKE_ANDROID_ARCH_ABI}/libjsi.so" INTERFACE_INCLUDE_DIRECTORIES "${buildDir}/prefab-headers/jsi" ) @@ -713,8 +715,12 @@ android { } // Ensure the config file is generated before external native build - tasks.configureEach { - if (this::class.java.name.contains("CMakeTask")) { + // The CMake tasks are created lazily by AGP with names like "configureCMake" + // We use withType to find and configure the tasks by name pattern + // Note: tasks.matching is not reliable for lazily created tasks in AGP 9.1+ + // Using withType().configureEach instead + tasks.withType().configureEach { + if (name.startsWith("configureCMake")) { dependsOn(generateReactAndroidConfig) } } From bf012fd12424ba0cedeb26e3a4a2e0d77db5b159 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:44:49 +0100 Subject: [PATCH 04/10] Fix CMake config gen for AGP 9.1+ - use correct prefab lib path: build/intermediates/prefab_package/debug/prefab/modules/ - convert Windows paths to forward slashes for CMake compat - use ${'$'}{CMAKE_ANDROID_ARCH_ABI} variable for architecture-agnostic paths --- packages/react-native/ReactAndroid/build.gradle.kts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 61b86937b201..0cec4a0ad7d4 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -685,6 +685,10 @@ android { doLast { val configDir = outputs.files.first() configDir.mkdirs() + // Convert paths to CMake-style forward slashes to avoid Windows escape sequence issues + val buildDirCmake = buildDir.path.replace("\\", "/") + // The prefab libraries are in build/intermediates/prefab_package/debug/prefab/modules/*/libs/android.* + val prefabLibsDir = buildDir.resolve("intermediates/prefab_package/debug/prefab/modules").path.replace("\\", "/") val configContent = """ # Copyright (c) Meta Platforms, Inc. and affiliates. # @@ -698,15 +702,15 @@ android { # The actual architecture will be determined by the CMAKE_ANDROID_ARCH_ABI variable add_library(reactnative SHARED IMPORTED) set_target_properties(reactnative PROPERTIES - IMPORTED_LOCATION "${buildDir}/prefab/debug/android/\${CMAKE_ANDROID_ARCH_ABI}/libreactnative.so" - INTERFACE_INCLUDE_DIRECTORIES "${buildDir}/prefab-headers/reactnative" + IMPORTED_LOCATION "${prefabLibsDir}/reactnative/libs/android.${'$'}{CMAKE_ANDROID_ARCH_ABI}/libreactnative.so" + INTERFACE_INCLUDE_DIRECTORIES "${buildDirCmake}/prefab-headers/reactnative" ) # The jsi library add_library(jsi SHARED IMPORTED) set_target_properties(jsi PROPERTIES - IMPORTED_LOCATION "${buildDir}/prefab/debug/android/\${CMAKE_ANDROID_ARCH_ABI}/libjsi.so" - INTERFACE_INCLUDE_DIRECTORIES "${buildDir}/prefab-headers/jsi" + IMPORTED_LOCATION "${prefabLibsDir}/jsi/libs/android.${'$'}{CMAKE_ANDROID_ARCH_ABI}/libjsi.so" + INTERFACE_INCLUDE_DIRECTORIES "${buildDirCmake}/prefab-headers/jsi" ) """.trimIndent() From 0d36f5e9f7b50ff13583a40c6786c497d9b3c169 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:25:07 +0100 Subject: [PATCH 05/10] Fix fbjni CMake config for AGP 9.1+ - download and extract fbjni AAR headers for prefab - add fbjni to ReactAndroid's prefab block - update ReactAndroidConfig.cmake to expose fbjni --- .../ReactAndroid/build.gradle.kts | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 0cec4a0ad7d4..7c853202eeca 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -84,6 +84,7 @@ val preparePrefab by prepareFmt, prepareFolly, prepareGlog, + prepareFbjni, ) dependsOn("generateCodegenArtifactsFromSchema") // To export to a ReactNativePrefabProcessingEntities.kt once all @@ -435,6 +436,50 @@ val prepareGlog by outputDir.set(File(thirdPartyNdkDir, "glog")) } +val downloadFbjniAarDest = File(downloadsDir, "fbjni-0.7.0.aar") +val downloadFbjniAar by + tasks.registering(Download::class) { + dependsOn(createNativeDepsDirectories) + src("https://repo1.maven.org/maven2/com/facebook/fbjni/fbjni/0.7.0/fbjni-0.7.0.aar") + onlyIfModified(true) + overwrite(false) + retries(5) + quiet(true) + dest(downloadFbjniAarDest) + } + +// Extract fbjni headers from AAR for prefab +val prepareFbjni by + tasks.registering { + dependsOn(downloadFbjniAar) + val inputAar = downloadFbjniAarDest + val outputDir = File(prefabHeadersDir, "fbjni") + outputs.dir(outputDir) + doLast { + outputDir.mkdirs() + // Extract only the fbjni headers from the AAR + val zip = java.util.zip.ZipFile(inputAar) + try { + zip.entries().asSequence().filter { it.name.startsWith("prefab/modules/fbjni/include/fbjni/") } + .forEach { entry -> + val destFile = File(outputDir, entry.name.removePrefix("prefab/modules/fbjni/include/")) + destFile.parentFile.mkdirs() + if (entry.isDirectory) { + destFile.mkdirs() + } else { + destFile.outputStream().use { output -> + zip.getInputStream(entry).use { input -> + input.copyTo(output) + } + } + } + } + } finally { + zip.close() + } + } + } + // Tasks used by Fantom to download the Native 3p dependencies used. val prepareNative3pDependencies by tasks.registering { dependsOn( @@ -444,6 +489,7 @@ val prepareNative3pDependencies by tasks.registering { prepareFmt, prepareFolly, prepareGlog, + prepareFbjni, ) } @@ -655,6 +701,9 @@ android { create("jsi") { headers = File(prefabHeadersDir, "jsi").absolutePath } create("reactnative") { headers = File(prefabHeadersDir, "reactnative").absolutePath } create("hermestooling") { headers = File(prefabHeadersDir, "hermestooling").absolutePath } + create("fbjni") { + headers = File(prefabHeadersDir, "fbjni").absolutePath + } } publishing { @@ -681,9 +730,9 @@ android { // Generate CMake config file for find_package support // This is needed for ReactNative-application.cmake to find ReactAndroid via find_package() val generateReactAndroidConfig by tasks.registering { - outputs.dir(buildDir.resolve("cmake/ReactAndroid")) + val configDir = buildDir.resolve("cmake/ReactAndroid") + outputs.dir(configDir) doLast { - val configDir = outputs.files.first() configDir.mkdirs() // Convert paths to CMake-style forward slashes to avoid Windows escape sequence issues val buildDirCmake = buildDir.path.replace("\\", "/") @@ -713,6 +762,13 @@ android { INTERFACE_INCLUDE_DIRECTORIES "${buildDirCmake}/prefab-headers/jsi" ) + # The fbjni library - from Gradle dependency's prefab module + add_library(fbjni SHARED IMPORTED) + set_target_properties(fbjni PROPERTIES + IMPORTED_LOCATION "${prefabLibsDir}/fbjni/libs/android.${'$'}{CMAKE_ANDROID_ARCH_ABI}/libfbjni.so" + INTERFACE_INCLUDE_DIRECTORIES "${buildDirCmake}/prefab-headers/fbjni" + ) + """.trimIndent() configDir.resolve("ReactAndroidConfig.cmake").writeText(configContent) } @@ -720,12 +776,11 @@ android { // Ensure the config file is generated before external native build // The CMake tasks are created lazily by AGP with names like "configureCMake" - // We use withType to find and configure the tasks by name pattern - // Note: tasks.matching is not reliable for lazily created tasks in AGP 9.1+ - // Using withType().configureEach instead - tasks.withType().configureEach { - if (name.startsWith("configureCMake")) { - dependsOn(generateReactAndroidConfig) + // We use task graph callback to find and configure CMake tasks + // whenTaskAdded registers callbacks for tasks as they are created (handles lazy tasks) + tasks.whenTaskAdded { task -> + if (task.name.startsWith("configureCMake")) { + task.dependsOn(generateReactAndroidConfig) } } } From d39e32a1bc0f9f8d534e00d8092f5d505447a495 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:26:51 +0100 Subject: [PATCH 06/10] Use version catalog for fbjni version --- packages/react-native/ReactAndroid/build.gradle.kts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 7c853202eeca..3c91e515ae46 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -12,6 +12,7 @@ import com.facebook.react.tasks.internal.utils.* import de.undercouch.gradle.tasks.download.Download import java.nio.file.Paths import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.gradle.kotlin.dsl.* plugins { id("maven-publish") @@ -436,11 +437,12 @@ val prepareGlog by outputDir.set(File(thirdPartyNdkDir, "glog")) } -val downloadFbjniAarDest = File(downloadsDir, "fbjni-0.7.0.aar") +val fbjniVersion = libs.versions.fbjni.get() +val downloadFbjniAarDest = File(downloadsDir, "fbjni-${fbjniVersion}.aar") val downloadFbjniAar by tasks.registering(Download::class) { dependsOn(createNativeDepsDirectories) - src("https://repo1.maven.org/maven2/com/facebook/fbjni/fbjni/0.7.0/fbjni-0.7.0.aar") + src("https://repo1.maven.org/maven2/com/facebook/fbjni/fbjni/${fbjniVersion}/fbjni-${fbjniVersion}.aar") onlyIfModified(true) overwrite(false) retries(5) From 58f6bfc6f8636c4277e78fc363a523be900ef496 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:28:08 +0100 Subject: [PATCH 07/10] Copy fbjni .so files to prefab_package directory - extract .so files from fbjni AAR to build/intermediates/prefab_package/ - ensure find_package(fbjni REQUIRED CONFIG) works correctly --- .../ReactAndroid/build.gradle.kts | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 3c91e515ae46..a9e201f9dea1 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -455,20 +455,23 @@ val prepareFbjni by tasks.registering { dependsOn(downloadFbjniAar) val inputAar = downloadFbjniAarDest - val outputDir = File(prefabHeadersDir, "fbjni") - outputs.dir(outputDir) + val headersOutputDir = File(prefabHeadersDir, "fbjni") + val libsOutputDir = buildDir.resolve("intermediates/prefab_package/debug/prefab/modules/fbjni/libs") + outputs.dir(headersOutputDir) + outputs.dir(libsOutputDir) doLast { - outputDir.mkdirs() + headersOutputDir.mkdirs() + libsOutputDir.mkdirs() // Extract only the fbjni headers from the AAR val zip = java.util.zip.ZipFile(inputAar) try { - zip.entries().asSequence().filter { it.name.startsWith("prefab/modules/fbjni/include/fbjni/") } - .forEach { entry -> - val destFile = File(outputDir, entry.name.removePrefix("prefab/modules/fbjni/include/")) + zip.entries().asSequence().forEach { entry -> + when { + entry.name.startsWith("prefab/modules/fbjni/include/fbjni/") -> { + // Extract headers + val destFile = File(headersOutputDir, entry.name.removePrefix("prefab/modules/fbjni/include/")) destFile.parentFile.mkdirs() - if (entry.isDirectory) { - destFile.mkdirs() - } else { + if (!entry.isDirectory) { destFile.outputStream().use { output -> zip.getInputStream(entry).use { input -> input.copyTo(output) @@ -476,6 +479,20 @@ val prepareFbjni by } } } + entry.name.startsWith("prefab/modules/fbjni/libs/android.") -> { + // Extract .so files to prefab_package directory + val destFile = File(libsOutputDir, entry.name.substringAfter("android.")) + destFile.parentFile.mkdirs() + if (!entry.isDirectory) { + destFile.outputStream().use { output -> + zip.getInputStream(entry).use { input -> + input.copyTo(output) + } + } + } + } + } + } } finally { zip.close() } From 76f4e3a8c46e08b6ebb3886fcd4c01616d8a00b4 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:37:19 +0100 Subject: [PATCH 08/10] Add dependency on ReactAndroid:generateReactAndroidConfig for rn-tester - ensure ReactAndroidConfig.cmake is generated before rn-tester's CMake runs - fix 'ReactAndroid::jsi' and 'ReactAndroid::reactnative' targets not found --- packages/rn-tester/android/app/build.gradle.kts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/rn-tester/android/app/build.gradle.kts b/packages/rn-tester/android/app/build.gradle.kts index 14497b77a2e7..1e28afccf8f7 100644 --- a/packages/rn-tester/android/app/build.gradle.kts +++ b/packages/rn-tester/android/app/build.gradle.kts @@ -198,3 +198,11 @@ tasks.withType().configureEa tasks.withType().configureEach { dependsOn(":packages:react-native:ReactAndroid:buildCodegenCLI") } + +// As RN-Tester uses CMake, we need to make sure the ReactAndroidConfig.cmake file is +// generated before CMake runs. This file is generated by ReactAndroid:generateReactAndroidConfig +tasks.whenTaskAdded { task -> + if (task.name.startsWith("configureCMake")) { + task.dependsOn(":packages:react-native:ReactAndroid:generateReactAndroidConfig") + } +} From 209b7eea829d43ef530410d3605a039a880cbd82 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:40:00 +0100 Subject: [PATCH 09/10] Use tasks.withType().configureEach for CMake task dependencies - more reliable for AGP 9.1+ lazy task creation - ensures generateReactAndroidConfig runs before CMake tasks --- packages/react-native/ReactAndroid/build.gradle.kts | 9 ++++----- packages/rn-tester/android/app/build.gradle.kts | 7 ++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index a9e201f9dea1..3ed6e7c623e7 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -795,11 +795,10 @@ android { // Ensure the config file is generated before external native build // The CMake tasks are created lazily by AGP with names like "configureCMake" - // We use task graph callback to find and configure CMake tasks - // whenTaskAdded registers callbacks for tasks as they are created (handles lazy tasks) - tasks.whenTaskAdded { task -> - if (task.name.startsWith("configureCMake")) { - task.dependsOn(generateReactAndroidConfig) + // We use configureEach to handle lazily created tasks in AGP 9.1+ + tasks.withType().configureEach { + if (name.startsWith("configureCMake")) { + dependsOn(generateReactAndroidConfig) } } } diff --git a/packages/rn-tester/android/app/build.gradle.kts b/packages/rn-tester/android/app/build.gradle.kts index 1e28afccf8f7..ec4dff6204bf 100644 --- a/packages/rn-tester/android/app/build.gradle.kts +++ b/packages/rn-tester/android/app/build.gradle.kts @@ -201,8 +201,9 @@ tasks.withType().configureEach { // As RN-Tester uses CMake, we need to make sure the ReactAndroidConfig.cmake file is // generated before CMake runs. This file is generated by ReactAndroid:generateReactAndroidConfig -tasks.whenTaskAdded { task -> - if (task.name.startsWith("configureCMake")) { - task.dependsOn(":packages:react-native:ReactAndroid:generateReactAndroidConfig") +// We use configureEach to handle lazily created CMake tasks in AGP 9.1+ +tasks.withType().configureEach { + if (name.startsWith("configureCMake")) { + dependsOn(":packages:react-native:ReactAndroid:generateReactAndroidConfig") } } From 573ff0295afaa94e00f8d7eaa5b105314b5b8bc7 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:00:16 +0100 Subject: [PATCH 10/10] Fix Kotlin compilation errors in build.gradle.kts - add missing imports for java.util.zip.ZipEntry and ZipFile - add explicit type annotation ZipEntry to the lambda parameter - replace entry.isDirectory with !entry.name.endsWith("/") since ZipEntry has no isDirectory property --- packages/react-native/ReactAndroid/build.gradle.kts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 3ed6e7c623e7..21acc2683c82 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -10,9 +10,12 @@ import com.facebook.react.internal.PrivateReactExtension import com.facebook.react.tasks.internal.* import com.facebook.react.tasks.internal.utils.* import de.undercouch.gradle.tasks.download.Download +import java.io.File import java.nio.file.Paths import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.gradle.kotlin.dsl.* +import java.util.zip.ZipEntry +import java.util.zip.ZipFile plugins { id("maven-publish") @@ -463,15 +466,15 @@ val prepareFbjni by headersOutputDir.mkdirs() libsOutputDir.mkdirs() // Extract only the fbjni headers from the AAR - val zip = java.util.zip.ZipFile(inputAar) + val zip = ZipFile(inputAar) try { - zip.entries().asSequence().forEach { entry -> + zip.entries().asSequence().forEach { entry: ZipEntry -> when { entry.name.startsWith("prefab/modules/fbjni/include/fbjni/") -> { // Extract headers val destFile = File(headersOutputDir, entry.name.removePrefix("prefab/modules/fbjni/include/")) destFile.parentFile.mkdirs() - if (!entry.isDirectory) { + if (!entry.name.endsWith("/")) { destFile.outputStream().use { output -> zip.getInputStream(entry).use { input -> input.copyTo(output) @@ -483,7 +486,7 @@ val prepareFbjni by // Extract .so files to prefab_package directory val destFile = File(libsOutputDir, entry.name.substringAfter("android.")) destFile.parentFile.mkdirs() - if (!entry.isDirectory) { + if (!entry.name.endsWith("/")) { destFile.outputStream().use { output -> zip.getInputStream(entry).use { input -> input.copyTo(output)