From 3195472ab2e59c9eddb134c2c3f6a51b5b9dbfb3 Mon Sep 17 00:00:00 2001 From: Radoslaw Nowacki Date: Thu, 22 Oct 2026 09:10:51 +0200 Subject: [PATCH 1/2] fix(bgp): package local jars of embedded libraries into the AAR #417 dropped the merge of classes from each library's libs/*.jar (it only ran for minified builds before), so host apps crashed with NoClassDefFoundError. Merge them in every variant, leaving out the jar's root module-info.class. Refs #489 --- .changeset/aar-local-jars.md | 5 ++ .../processors/VariantTaskProvider.kt | 4 + .../brownfield/shared/MergeClassesTask.kt | 2 + .../processors/MergeClassesLocalJarsTest.kt | 73 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 .changeset/aar-local-jars.md create mode 100644 gradle-plugins/react/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt diff --git a/.changeset/aar-local-jars.md b/.changeset/aar-local-jars.md new file mode 100644 index 000000000..b72141aa6 --- /dev/null +++ b/.changeset/aar-local-jars.md @@ -0,0 +1,5 @@ +--- +'@callstack/react-native-brownfield': patch +--- + +fix: package classes from libraries' local jars (`libs/*.jar`) into the Android AAR; previously host apps crashed with NoClassDefFoundError. If your app also has those classes, because it added the jar itself as a workaround or depends on a library that the jar bundles, the build now fails with duplicate classes: remove the app's copy diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt index f17204e1d..24e3b6d49 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt @@ -66,6 +66,10 @@ class VariantTaskProvider(val project: Project) { it.dependsOn(VariantHelper.getJavaCompileTask(project, capitalizedVariantName)) it.variantName.set(variantName) it.inputClassesJars.from(aarLibraries.map { aarLibrary -> aarLibrary.getClassesJarFile() }) + // libs/*.jar of each library (#489) + it.inputClassesJars.from( + aarLibraries.map { aarLibrary -> project.fileTree(aarLibrary.getLibsDir()) { tree -> tree.include("*.jar") } }, + ) it.outputDir.set(DirectoryManager.getMergeClassDirectory(variantName)) } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt index ac76c8ce7..d4db61716 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt @@ -54,6 +54,8 @@ abstract class MergeClassesTask : DefaultTask() { copyTask.from(mergedOutputDir) copyTask.into(javacDir) copyTask.exclude("META-INF/") + // Root module descriptor of a library's local jar, not a class of the AAR. + copyTask.exclude("module-info.class") } project.copy { copyTask -> diff --git a/gradle-plugins/react/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt b/gradle-plugins/react/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt new file mode 100644 index 000000000..ca2be715f --- /dev/null +++ b/gradle-plugins/react/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt @@ -0,0 +1,73 @@ +package com.callstack.react.brownfield.processors + +import com.callstack.react.brownfield.shared.ExplodeAarTask +import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo +import com.callstack.react.brownfield.utils.AndroidArchiveLibrary +import com.callstack.react.brownfield.utils.DirectoryManager +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import kotlin.test.assertTrue + +/** + * A library that ships `implementation files("libs/some.jar")` has that jar in its exploded AAR's + * `libs/`. Its classes must be merged with the library's own `classes.jar` (#489). + */ +class MergeClassesLocalJarsTest { + @TempDir + lateinit var tempDir: File + + @Test + fun `merges classes from a library's local jars`() { + val project = ProjectBuilder.builder().withProjectDir(tempDir).build() + DirectoryManager.project = project + + val variantName = "release" + val library = + AndroidArchiveLibrary( + project, + UnresolvedArtifactInfo("RNApp", "react-native-aar-repro", "unspecified", null, false), + variantName, + ) + val explodeTask = project.tasks.register("explodeTest", ExplodeAarTask::class.java) + val mergeTask = VariantTaskProvider(project).mergeClasses(listOf(library), explodeTask, variantName).get() + + // Exploded after the task is configured, as on a clean build. + library.getExplodedAarRootDir().mkdirs() + writeJar(library.getClassesJarFile(), "com/aarrepro/AarReproPackage.class") + writeJar( + File(library.getLibsDir(), "aar-repro.jar"), + "com/aarrepro/jar/AarReproJarClass.class", + "module-info.class", + ) + + mergeTask.run() + + val merged = mergeTask.outputDir.get().asFile + assertTrue(File(merged, "com/aarrepro/AarReproPackage.class").exists(), "classes.jar was not merged") + assertTrue( + File(merged, "com/aarrepro/jar/AarReproJarClass.class").exists(), + "classes from libs/aar-repro.jar were not merged", + ) + val javacDir = File(tempDir, "build/intermediates/javac/release/compileReleaseJavaWithJavac/classes") + assertTrue(File(javacDir, "com/aarrepro/jar/AarReproJarClass.class").exists(), "jar classes are not compiled into the AAR") + assertTrue(!File(javacDir, "module-info.class").exists(), "a jar's module-info.class ended up in the AAR") + } + + private fun writeJar( + jar: File, + vararg entries: String, + ) { + jar.parentFile.mkdirs() + ZipOutputStream(jar.outputStream()).use { zip -> + entries.forEach { entry -> + zip.putNextEntry(ZipEntry(entry)) + zip.write(byteArrayOf(0xCA.toByte(), 0xFE.toByte(), 0xBA.toByte(), 0xBE.toByte())) + zip.closeEntry() + } + } + } +} From 7388c07e0d0043973c2f62306c71124ee7ddd829 Mon Sep 17 00:00:00 2001 From: Radoslaw Nowacki Date: Fri, 23 Oct 2026 10:44:59 +0200 Subject: [PATCH 2/2] chore(bgp): bump plugin version to 2.0.0-alpha12 --- apps/RNApp/android/build.gradle | 2 +- apps/scripts/prepare-android-build-gradle-for-ci.ts | 2 +- gradle-plugins/react/brownfield/gradle.properties | 2 +- .../expo-config-plugin/android/__tests__/gradleHelpers.test.ts | 2 +- .../src/expo-config-plugin/android/utils/constants.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/RNApp/android/build.gradle b/apps/RNApp/android/build.gradle index f26a88522..9c7b9a396 100644 --- a/apps/RNApp/android/build.gradle +++ b/apps/RNApp/android/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") - classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha11-SNAPSHOT") + classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha12-SNAPSHOT") } } diff --git a/apps/scripts/prepare-android-build-gradle-for-ci.ts b/apps/scripts/prepare-android-build-gradle-for-ci.ts index 17323bf60..b3b367e92 100644 --- a/apps/scripts/prepare-android-build-gradle-for-ci.ts +++ b/apps/scripts/prepare-android-build-gradle-for-ci.ts @@ -9,7 +9,7 @@ if (!projectDirName) { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const SNAPSHOT_VERSION = '2.0.0-alpha11-SNAPSHOT'; +const SNAPSHOT_VERSION = '2.0.0-alpha12-SNAPSHOT'; // Accepts a repo-relative app dir (e.g. "ExpoApp58") or an absolute path (e.g. // the scaffold-smoke app created outside the repo). const projectDir = path.isAbsolute(projectDirName) diff --git a/gradle-plugins/react/brownfield/gradle.properties b/gradle-plugins/react/brownfield/gradle.properties index 60afacf51..f1be75846 100644 --- a/gradle-plugins/react/brownfield/gradle.properties +++ b/gradle-plugins/react/brownfield/gradle.properties @@ -1,6 +1,6 @@ PROJECT_ID=com.callstack.react.brownfield ARTIFACT_ID=brownfield-gradle-plugin -VERSION=2.0.0-alpha11 +VERSION=2.0.0-alpha12 GROUP=com.callstack.react IMPLEMENTATION_CLASS=com.callstack.react.brownfield.plugin.RNBrownfieldPlugin diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/gradleHelpers.test.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/gradleHelpers.test.ts index 03ba1ee0c..850e330e9 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/gradleHelpers.test.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/gradleHelpers.test.ts @@ -16,7 +16,7 @@ describe('gradleHelpers', () => { }`; expect(modifyRootBuildGradle(contents)).toContain( - 'classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha11")' + 'classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha12")' ); }); diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/utils/constants.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/constants.ts index 0c67be0cb..97b952dbe 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/utils/constants.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/constants.ts @@ -1,2 +1,2 @@ -export const BROWNFIELD_PLUGIN_VERSION = '2.0.0-alpha11'; +export const BROWNFIELD_PLUGIN_VERSION = '2.0.0-alpha12'; export const brownfieldGradlePluginDependency = `classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`;