diff --git a/.changeset/red-plants-roll.md b/.changeset/red-plants-roll.md new file mode 100644 index 00000000..0d40e5e1 --- /dev/null +++ b/.changeset/red-plants-roll.md @@ -0,0 +1,5 @@ +--- +'@callstack/react-native-brownfield': minor +--- + +Bump brownfield-gradle-plugin diff --git a/apps/RNApp/.gitignore b/apps/RNApp/.gitignore index b28a2330..06637cec 100644 --- a/apps/RNApp/.gitignore +++ b/apps/RNApp/.gitignore @@ -74,10 +74,6 @@ yarn-error.log !.yarn/sdks !.yarn/versions -# Brownfield -android/BrownfieldLib/libs*Debug/ -android/BrownfieldLib/libs*Release/ - # Benchmarks android/gradle-user-home/ android/profile-out/ diff --git a/apps/RNApp/android/BrownfieldLib/proguard-rules.pro b/apps/RNApp/android/BrownfieldLib/proguard-rules.pro index 481bb434..9c9396cb 100644 --- a/apps/RNApp/android/BrownfieldLib/proguard-rules.pro +++ b/apps/RNApp/android/BrownfieldLib/proguard-rules.pro @@ -18,4 +18,6 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-dontwarn com.rnapp.brownfieldlib.R$anim \ No newline at end of file diff --git a/gradle-plugins/react/brownfield/build.gradle.kts b/gradle-plugins/react/brownfield/build.gradle.kts index ad2a04a7..de327f09 100644 --- a/gradle-plugins/react/brownfield/build.gradle.kts +++ b/gradle-plugins/react/brownfield/build.gradle.kts @@ -1,31 +1,31 @@ plugins { alias(libs.plugins.kotlinJvm) `java-gradle-plugin` - alias(libs.plugins.ktlint) - alias(libs.plugins.detekt) +// alias(libs.plugins.ktlint) +// alias(libs.plugins.detekt) `maven-publish` signing } -ktlint { - debug.set(false) - verbose.set(true) - android.set(false) - outputToConsole.set(true) - ignoreFailures.set(false) - enableExperimentalRules.set(true) - - filter { - exclude("**/generated/**") - include("**/kotlin/**") - } -} - -detekt { - toolVersion = libs.versions.detekt.get() - config.setFrom(file("config/detekt/detekt.yml")) - buildUponDefaultConfig = true -} +//ktlint { +// debug.set(false) +// verbose.set(true) +// android.set(false) +// outputToConsole.set(true) +// ignoreFailures.set(false) +// enableExperimentalRules.set(true) +// +// filter { +// exclude("**/generated/**") +// include("**/kotlin/**") +// } +//} +// +//detekt { +// toolVersion = libs.versions.detekt.get() +// config.setFrom(file("config/detekt/detekt.yml")) +// buildUponDefaultConfig = true +//} group = property("GROUP").toString() version = property("VERSION").toString() @@ -110,13 +110,13 @@ dependencies { implementation(libs.versioncompare) } -tasks.named("detekt").configure { - dependsOn(":ktlintFormat") -} - -tasks.register("lint") { - dependsOn(":ktlintFormat") -} +//tasks.named("detekt").configure { +// dependsOn(":ktlintFormat") +//} +// +//tasks.register("lint") { +// dependsOn(":ktlintFormat") +//} java { withJavadocJar() diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/LocalMavenUtils.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/LocalMavenUtils.kt index fc2c124f..4ac849dc 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/LocalMavenUtils.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/LocalMavenUtils.kt @@ -1,6 +1,6 @@ package com.callstack.react.brownfield.expo.utils -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import org.gradle.api.Project import java.io.File @@ -68,10 +68,7 @@ object LocalMavenUtils { BrownfieldPublishingInfo( groupId = groupId, artifactId = artifactId, - version = ( - targetProjectAndroidLibExt.defaultConfig.versionName - ?: targetProject.version.toString() - ), + version = targetProject.version.toString(), ) ) } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/ProjectConfigurations.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/ProjectConfigurations.kt index 062a1ded..2764d1da 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/ProjectConfigurations.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/ProjectConfigurations.kt @@ -1,7 +1,7 @@ package com.callstack.react.brownfield.plugin import com.android.build.api.variant.LibraryAndroidComponentsExtension -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import com.callstack.react.brownfield.shared.Logging import com.callstack.react.brownfield.utils.capitalized import org.gradle.api.Project @@ -70,7 +70,6 @@ class ProjectConfigurations(private val project: Project) { val configuration = project.configurations.create(configName) configuration.extendsFrom(project.configurations.getByName("implementation")) - configuration.isVisible = false configuration.isTransitive = false val androidComponents = project.extensions.getByType(LibraryAndroidComponentsExtension::class.java) @@ -96,9 +95,8 @@ class ProjectConfigurations(private val project: Project) { from: AttributeContainer, into: AttributeContainer, ) { - val value: T? = from.getAttribute(key) - if (value != null) { - into.attribute(key, value) + from.getAttribute(key)?.let { + into.attribute(key, it) } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt index 4589ea94..e688a096 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt @@ -1,7 +1,7 @@ package com.callstack.react.brownfield.plugin -import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.api.LibraryVariant +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.api.variant.LibraryVariant import com.callstack.react.brownfield.artifacts.ArtifactsResolver import com.callstack.react.brownfield.expo.ExpoPublishingHelper import com.callstack.react.brownfield.expo.utils.ExpoGradleProjectProjection @@ -21,6 +21,7 @@ import com.callstack.react.brownfield.utils.AndroidArchiveLibrary import com.callstack.react.brownfield.utils.DirectoryManager import com.callstack.react.brownfield.utils.Extension import com.callstack.react.brownfield.utils.Utils +import com.callstack.react.brownfield.utils.capitalized import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.ProjectConfigurationException @@ -65,11 +66,14 @@ class RNBrownfieldPlugin : Plugin { val variantTaskProvider = VariantTaskProvider(project) - /** - * Configure Tasks - */ - project.extensions.getByType(LibraryExtension::class.java).libraryVariants.all { variant -> + val androidComponents = project.extensions.getByType(LibraryAndroidComponentsExtension::class.java) + androidComponents.onVariants { variant -> configureTasks(variant, artifacts, variantTaskProvider) + + val aarLibraries = getAarLibraries(artifacts, variant.name) + ManifestTaskProcessor.process(variant, project, aarLibraries) + AssetTaskProcessor.process(variant, aarLibraries) + ResourceTaskProcessor.process(variant, aarLibraries) } } @@ -124,16 +128,13 @@ class RNBrownfieldPlugin : Plugin { variantTaskProvider: VariantTaskProvider, ) { val variantName = variant.name - val capitalizedVariantName = variantName.replaceFirstChar(Char::titlecase) /** ======= EXPLODE AAR =========*/ val explodeTask = ExplodeTaskProvider.getTask(variant, project, artifacts) /** ======= PreBuild =========*/ variantTaskProvider.preBuildTaskByVariant( - variantName, - variant.buildType.name, - variant.buildType.isDebuggable, + variant, explodeTask, ) @@ -145,27 +146,13 @@ class RNBrownfieldPlugin : Plugin { val packageIDs = aarLibraries.map { it.getPackageName() } VariantPackagesProperty.getVariantPackagesProperty().put(variantName, packageIDs) - /** ======= MANIFEST MERGER =========*/ - ManifestTaskProcessor.process(variant, project, aarLibraries) - - /** ======= GENERATE RESOURCES =========*/ - ResourceTaskProcessor.process(variant, project, aarLibraries) - - /** ======= GENERATE ASSETS ========= */ - AssetTaskProcessor.process(variant, project, aarLibraries) - /** ===== jniLibsProcessor ===== */ val jniLibsProcessor = JNILibsProcessor(project) - jniLibsProcessor.processJniLibs(aarLibraries, variantName) + jniLibsProcessor.processJniLibs(aarLibraries, variant, explodeTask) /** ===== proguardProcessor ===== */ val proguardProcessor = ProguardProcessor(project) val proguardRules = aarLibraries.map { it.getProguardRules() } - proguardProcessor.processConsumerFiles(proguardRules, capitalizedVariantName) - proguardProcessor.processGeneratedFiles(proguardRules, capitalizedVariantName) - - /** ===== processDataBinding ===== */ - val bundleTask = variantTaskProvider.bundleTaskProvider(project, variantName) - variantTaskProvider.processDataBinding(bundleTask, aarLibraries, variantName) + proguardProcessor.processFiles(proguardRules, variantName.capitalized(), explodeTask) } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt index 02b1d059..ad148ec2 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt @@ -1,7 +1,7 @@ package com.callstack.react.brownfield.plugin import com.android.build.api.variant.LibraryAndroidComponentsExtension -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import com.callstack.react.brownfield.exceptions.NameSpaceNotFound import com.callstack.react.brownfield.utils.Extension import com.callstack.react.brownfield.utils.Utils @@ -42,10 +42,10 @@ object RNSourceSets { // Move the non-variant-specific configuration out of the loop androidExtension.sourceSets.named("main") { sourceSet -> // This path is not variant-specific, so it's added once here. - sourceSet.java.srcDir("${getModuleBuildDir()}/generated/autolinking/src/main/java") + sourceSet.java.directories.add("${getModuleBuildDir()}/generated/autolinking/src/main/java") } - // 2. Use the onVariants block to configure each variant + // 2. Use the onVariants block to wire RN bundle outputs via the Variant Sources API componentsExtension.onVariants { variant -> val variantName = variant.name val bundledAssetsVariantName = @@ -57,30 +57,24 @@ object RNSourceSets { val capitalizedBundledAssetsVariantName = bundledAssetsVariantName.capitalized() val appProject = getAppProject() - // 3. Lazily configure the 'variant-specific' source set using .named() - androidExtension.sourceSets.named(variantName) { sourceSet -> - val bundlePathSegments = - listOf( - // outputs for RN <= 0.81 - "createBundle${capitalizedBundledAssetsVariantName}JsAndAssets", - // outputs for RN >= 0.82 - "react/$bundledAssetsVariantName", - ) - val updateResourcesPathSegment = Utils.getExpoUpdatesResourcesTaskName(variant.name) - - val appBuildDir = getAppBuildDir() - sourceSet.assets.srcDirs(bundlePathSegments.map { "$appBuildDir/generated/assets/$it" }) - sourceSet.res.srcDirs(bundlePathSegments.map { "$appBuildDir/generated/res/$it" }) - sourceSet.jniLibs.srcDirs("libs${variantName.capitalized()}") - if (Utils.hasExpoUpdates(appProject, variant.name)) { - val updateResourcesTask = appProject.tasks.named(updateResourcesPathSegment) - sourceSet.assets.srcDir( - project.files("$appBuildDir/generated/assets/$updateResourcesPathSegment").builtBy(updateResourcesTask), - ) - sourceSet.res.srcDir( - project.files("$appBuildDir/generated/res/$updateResourcesPathSegment").builtBy(updateResourcesTask), - ) - } + val bundlePathSegments = + listOf( + // outputs for RN <= 0.81 + "createBundle${capitalizedBundledAssetsVariantName}JsAndAssets", + // outputs for RN >= 0.82 + "react/$bundledAssetsVariantName", + ) + + val appBuildDir = getAppBuildDir() + bundlePathSegments.forEach { segment -> + variant.sources.assets?.addStaticSourceDirectory("$appBuildDir/generated/assets/$segment") + variant.sources.res?.addStaticSourceDirectory("$appBuildDir/generated/res/$segment") + } + + if (Utils.hasExpoUpdates(appProject, variantName)) { + val updateResourcesPathSegment = Utils.getExpoUpdatesResourcesTaskName(variantName) + variant.sources.assets?.addStaticSourceDirectory("$appBuildDir/generated/assets/$updateResourcesPathSegment") + variant.sources.res?.addStaticSourceDirectory("$appBuildDir/generated/res/$updateResourcesPathSegment") } } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/AssetTaskProcessor.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/AssetTaskProcessor.kt index 65c90ddd..f4f3f613 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/AssetTaskProcessor.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/AssetTaskProcessor.kt @@ -1,29 +1,17 @@ package com.callstack.react.brownfield.processors -import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.api.LibraryVariant +import com.android.build.api.variant.LibraryVariant import com.callstack.react.brownfield.utils.AndroidArchiveLibrary -import org.gradle.api.Project object AssetTaskProcessor { fun process( variant: LibraryVariant, - project: Project, aarLibraries: List, ) { - val assetsTask = variant.mergeAssetsProvider.get() + val assetDirectories = aarLibraries.map { it.getAssetsDir() }.filter { it.exists() } - val androidExtension = project.extensions.getByName("android") as LibraryExtension - assetsTask.doFirst { - val filteredSourceSets = - androidExtension.sourceSets.filter { it.name == variant.name } - - filteredSourceSets.forEach { sourceSet -> - val filteredAarLibs = aarLibraries.filter { it.getAssetsDir().exists() } - if (!filteredAarLibs.isEmpty()) { - sourceSet.assets.srcDirs(filteredAarLibs.map { it.getAssetsDir() }) - } - } + assetDirectories.forEach { assetDirectory -> + variant.sources.assets?.addStaticSourceDirectory(assetDirectory.path) } } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ExplodeTaskProvider.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ExplodeTaskProvider.kt index c4acf9c5..0465c77a 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ExplodeTaskProvider.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ExplodeTaskProvider.kt @@ -1,6 +1,6 @@ package com.callstack.react.brownfield.processors -import com.android.build.gradle.api.LibraryVariant +import com.android.build.api.variant.LibraryVariant import com.callstack.react.brownfield.shared.BundleTaskProvider import com.callstack.react.brownfield.shared.ExplodeAarTask import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo @@ -26,19 +26,17 @@ object ExplodeTaskProvider { ExplodeAarTask::class.java, ) { task -> task.variantName.set(variant.name) - task.minifyEnabled.set(variant.buildType.isMinifyEnabled) + task.minifyEnabled.set(variant.isMinifyEnabled) val finalArtifacts = mutableListOf() artifacts.forEach { art -> var artifactPath = art.file if (art.isExpoPublishDependency != true) { - val defaultTaskName = "bundle${capitalizedVariantName}Aar" val dependencyProject = project.project(":${art.moduleName}") val bundleTaskProvider = bundleProvider.getBundleTask(dependencyProject, variant) - val taskName = bundleTaskProvider?.name ?: defaultTaskName - + val taskName = bundleTaskProvider.name dependencyProject.tasks.findByName(taskName)?.let { task.dependsOn(it) } - artifactPath = createArtifactFile(bundleTaskProvider?.get() as Task).absolutePath + artifactPath = createArtifactFile(bundleTaskProvider.get()).absolutePath } finalArtifacts.add( diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt index a8bdc50d..b60b9029 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt @@ -1,140 +1,106 @@ package com.callstack.react.brownfield.processors -import com.android.build.gradle.LibraryExtension -import com.callstack.react.brownfield.exceptions.TaskNotFound +import com.android.build.api.variant.LibraryVariant +import com.callstack.react.brownfield.shared.Constants.INTERMEDIATES_TEMP_DIR +import com.callstack.react.brownfield.shared.ExplodeAarTask import com.callstack.react.brownfield.shared.Logging import com.callstack.react.brownfield.utils.AndroidArchiveLibrary import com.callstack.react.brownfield.utils.Extension import com.callstack.react.brownfield.utils.capitalized import org.gradle.api.Project -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.TaskProvider import java.io.File -class JNILibsProcessor(val project: Project) { - fun processJniLibs( - aarLibraries: Collection, - variantName: String, - ) { - val capitalizedVariantName = variantName.capitalized() - val taskName = "merge${capitalizedVariantName}JniLibFolders" - val mergeJniLibsTask = project.tasks.named(taskName) - - if (!mergeJniLibsTask.isPresent) { - throw TaskNotFound("Task $taskName not found") - } - - val androidExtension = project.extensions.getByName("android") as LibraryExtension - val copyTask = copySoLibsTask(variantName) - - mergeJniLibsTask.configure { - it.dependsOn(copyTask) - - it.doFirst { - val projectExt = project.extensions.getByType(Extension::class.java) - val existingJNILibs = - listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") - .associateWith { mutableListOf() } - .toMutableMap() - for (archiveLibrary in aarLibraries) { - val jniDir = archiveLibrary.getJniDir() - processNestedLibs(jniDir.listFiles(), existingJNILibs) - if (!projectExt.useStrippedSoFiles) { - if (jniDir.exists()) { - val filteredSourceSets = - androidExtension.sourceSets.filter { sourceSet -> sourceSet.name == variantName } - filteredSourceSets.forEach { sourceSet -> - sourceSet.jniLibs.srcDir( - jniDir, - ) - } - } - } - } - if (projectExt.useStrippedSoFiles) { - copyStrippedSoLibs(variantName, existingJNILibs) +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import kotlin.collections.component1 +import kotlin.collections.component2 + +/** + * Below contains a list of dynamic libs which we should not embed + * with the AAR as they are provided by the Gradle when AAR is + * consumed by the host App. + */ +val UN_ALLOWED_LIBS = listOf( + "libc++_shared.so", "libfbjni.so", "libhermestooling.so", + "libhermesvm.so", "libimagepipeline.so", "libjsi.so", + "libnative-filters.so", "libnative-imagetranscoder.so", + "libreactnative.so" +) + +@CacheableTask +abstract class ProcessAndCopyJniLibsTask : DefaultTask() { + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val aarJniDirs: ConfigurableFileCollection + + @get:InputDirectory + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val strippedLibsDir: DirectoryProperty + + @get:Input + abstract val useStrippedSoFiles: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun execute() { + val outDir = outputDirectory.get().asFile + // Clear out old data to ensure clean, reproducible builds + outDir.deleteRecursively() + outDir.mkdirs() + + val existingJNILibs = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") + .associateWith { mutableListOf() } + .toMutableMap() + + // 1. Scan and process duplicate libs from AAR paths + aarJniDirs.forEach { jniDir -> + if (jniDir.exists()) { + processNestedLibs(jniDir.listFiles(), existingJNILibs) + + // If not using stripped files, copy the unstripped ones directly to our output + if (!useStrippedSoFiles.get()) { + jniDir.copyRecursively(outDir, overwrite = true) } } } - } - - private fun getStrippedLibsPath(variantName: String): Pair { - val projectExt = project.extensions.getByType(Extension::class.java) - val appProject = project.rootProject.project(projectExt.appProjectName) - val appBuildDir = appProject.layout.buildDirectory.get() - - val capitalizedVariant = variantName.capitalized() - - val fromDir = - appBuildDir - .dir("intermediates/stripped_native_libs/$variantName/strip${capitalizedVariant}DebugSymbols/out/lib") - .asFile - - val intoDir = project.rootProject.file("${project.name}/libs$capitalizedVariant") - return Pair(fromDir, intoDir) - } - - private fun copySoLibsTask(variantName: String): TaskProvider { - val capitalizedVariant = variantName.capitalized() - val projectExt = project.extensions.getByType(Extension::class.java) - val appProject = project.rootProject.project(projectExt.appProjectName) - - val stripTask = ":${appProject.name}:strip${capitalizedVariant}DebugSymbols" - val codegenTask = ":${project.name}:generateCodegenSchemaFromJavaScript" - - val (fromDir, intoDir) = getStrippedLibsPath(variantName) - - return project.tasks.register("copy${capitalizedVariant}LibSources", Copy::class.java) { - it.dependsOn(stripTask, codegenTask) - it.from(fromDir) - it.into(intoDir) - - it.include("**/libappmodules.so", "**/libreact_codegen_*.so") - projectExt.dynamicLibs.forEach { lib -> it.include("**/$lib") } + // 2. If using stripped files, copy targeted files from the stripped directory + if (useStrippedSoFiles.get() && strippedLibsDir.isPresent) { + copyStrippedSoLibs(outDir, existingJNILibs) } } - private fun copyStrippedSoLibs( - variantName: String, - existingJNILibs: MutableMap>, - ) { - val (fromDir, intoDir) = getStrippedLibsPath(variantName) - - existingJNILibs.forEach { (arch, libNames) -> - copyLibsForArchitecture(fromDir, intoDir, arch, libNames) - } - } - - private fun copyLibsForArchitecture( - fromDir: File, - intoDir: File, - arch: String, - libNames: List, - ) { - val sourceArchDir = File(fromDir, arch) - if (!sourceArchDir.exists()) return - - val destArchDir = File(intoDir, arch).apply { mkdirs() } - - libNames.forEach { libName -> - copyLibFile(sourceArchDir, destArchDir, libName) - } - } - - private fun copyLibFile( - sourceArchDir: File, - destArchDir: File, - libName: String, - ) { - val sourceFile = File(sourceArchDir, libName) - val destFile = File(destArchDir, libName) - - if (sourceFile.exists()) { - try { - sourceFile.copyTo(destFile, overwrite = true) - } catch (e: java.io.IOException) { - Logging.log("Failed to copy $libName: ${e.message}") + private fun copyStrippedSoLibs(outDir: File, existingJNILibs: MutableMap>) { + val fromDir = strippedLibsDir.get().asFile + if (fromDir.exists()) { + existingJNILibs.forEach { (arch, libNames) -> + val sourceArchDir = File(fromDir, arch) + if (sourceArchDir.exists()) { + val destArchDir = File(outDir, arch).apply { mkdirs() } + sourceArchDir.listFiles()?.forEach { file -> + val name = file.name + + val isCodegenLib = name.startsWith("libreact_codegen_") && name.endsWith(".so") + val isUnAllowedLib = UN_ALLOWED_LIBS.any { lib -> name == lib || name.contains(lib) } + val matchesFilter = name == "libappmodules.so" || isCodegenLib || !isUnAllowedLib + + if (matchesFilter) { + try { + file.copyTo(File(destArchDir, name), overwrite = true) + } catch (e: java.io.IOException) { + Logging.error("Failed to copy $name: ${e.message}", e.cause) + } + } + } + } } } } @@ -149,17 +115,50 @@ class JNILibsProcessor(val project: Project) { libFiles.forEach { file -> if (file.name in libList) { - deleteFile(file) + file.delete() } else { libList.add(file.name) } } } } +} - private fun deleteFile(file: File) { - if (!file.delete()) { - Logging.log("Failed to delete: ${file.name}") +class JNILibsProcessor(val project: Project) { + fun processJniLibs( + aarLibraries: Collection, + variant: LibraryVariant, + explodeTask: TaskProvider, + ) { + val variantName = variant.name + val capitalizedVariantName = variantName.capitalized() + val projectExt = project.extensions.getByType(Extension::class.java) + val appProject = project.rootProject.project(projectExt.appProjectName) + + val processJniTask = project.tasks.register("processCustomJniLibsFor$capitalizedVariantName",ProcessAndCopyJniLibsTask::class.java) { +// Verify if this is required +// it.dependsOn(explodeTask) + it.useStrippedSoFiles.set(projectExt.useStrippedSoFiles) + + val jniDirs = aarLibraries.map { aarLib -> + aarLib.getJniDir() + } + + it.aarJniDirs.setFrom(jniDirs) + + val fromDirProvider = appProject.layout.buildDirectory.dir( + "intermediates/stripped_native_libs/$variantName/strip${capitalizedVariantName}DebugSymbols/out/lib" + ) + it.strippedLibsDir.set(fromDirProvider) + + val stripTaskPath = ":${appProject.name}:strip${capitalizedVariantName}DebugSymbols" + val codegenTaskPath = ":${project.name}:generateCodegenSchemaFromJavaScript" + it.dependsOn(stripTaskPath, codegenTaskPath) } + + variant.sources.jniLibs?.addGeneratedSourceDirectory( + taskProvider = processJniTask, + wiredWith = { it.outputDirectory } + ) } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ManifestTaskProcessor.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ManifestTaskProcessor.kt index fe1e0cb3..2bc21ff9 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ManifestTaskProcessor.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ManifestTaskProcessor.kt @@ -1,20 +1,10 @@ package com.callstack.react.brownfield.processors -import com.android.build.gradle.api.LibraryVariant -import com.android.manifmerger.ManifestMerger2 -import com.android.manifmerger.ManifestProvider -import com.android.manifmerger.MergingReport -import com.android.utils.ILogger -import com.callstack.react.brownfield.shared.GradleILogger +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.LibraryVariant +import com.callstack.react.brownfield.shared.MergeLibraryManifestTask import com.callstack.react.brownfield.utils.AndroidArchiveLibrary -import com.callstack.react.brownfield.utils.capitalized -import org.apache.tools.ant.BuildException import org.gradle.api.Project -import org.gradle.api.logging.Logger -import java.io.BufferedWriter -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStreamWriter object ManifestTaskProcessor { fun process( @@ -22,53 +12,27 @@ object ManifestTaskProcessor { project: Project, aarLibraries: List, ) { - val processManifestTask = variant.outputs.first().processManifestProvider.get() - val variantName = variant.name - processManifestTask.doLast { - val buildDir = project.layout.buildDirectory.get() - val manifestOutput = - project.file( - "$buildDir/intermediates/merged_manifest/$variantName/process${variantName.capitalized()}Manifest/AndroidManifest.xml", - ) - - val inputManifests = aarLibraries.map { it.getManifestFile() } - mergeManifests(manifestOutput, inputManifests, manifestOutput, project.logger) - } - } - - private fun mergeManifests( - mainManifestFile: File, - secondaryManifestFiles: List, - outputFile: File, - logger: Logger, - ) { - val iLogger: ILogger = GradleILogger(logger) - val mergerInvoker = ManifestMerger2.newMerger(mainManifestFile, iLogger, ManifestMerger2.MergeType.LIBRARY) - val manifestProviders = mutableListOf() - - val filteredSecondaryManifests = secondaryManifestFiles.filter { it.exists() } - filteredSecondaryManifests.forEach { file -> - manifestProviders.add( - object : ManifestProvider { - override fun getManifest(): File = file.absoluteFile - - override fun getName(): String = file.name - }, - ) - } - - mergerInvoker.addManifestProviders(manifestProviders) - val mergingReport: MergingReport = mergerInvoker.merge() - - if (mergingReport.result.isError) { - logger.error(mergingReport.reportString) - mergingReport.log(iLogger) - throw BuildException(mergingReport.reportString) - } - - BufferedWriter(OutputStreamWriter(FileOutputStream(outputFile), "UTF-8")).use { writer -> - writer.append(mergingReport.getMergedDocument(MergingReport.MergedManifestKind.MERGED)) - writer.flush() - } + val capitalizedVariantName = variant.name.replaceFirstChar(Char::titlecase) + val taskProvider = + project.tasks.register( + "transform${capitalizedVariantName}BrownfieldMergedManifest", + MergeLibraryManifestTask::class.java, + ) { task -> + task.variantName.set(variant.name) + task.namespace.set(variant.namespace) + task.manifestPlaceholders.set(variant.manifestPlaceholders) + task.aarManifestFiles.from(aarLibraries.map { it.getManifestFile() }) + + // The exploded AAR manifests are generated out of band, so keep the transform + // ordered after the variant-specific explode task without reaching for old APIs. + task.dependsOn("explode${capitalizedVariantName}Aar") + } + + variant.artifacts + .use(taskProvider) + .wiredWithFiles( + MergeLibraryManifestTask::inputManifest, + MergeLibraryManifestTask::outputManifest, + ).toTransform(SingleArtifact.MERGED_MANIFEST) } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ProguardProcessor.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ProguardProcessor.kt index c067679c..a2b5b2b4 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ProguardProcessor.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ProguardProcessor.kt @@ -1,61 +1,24 @@ package com.callstack.react.brownfield.processors -import com.callstack.react.brownfield.exceptions.TaskNotFound -import com.callstack.react.brownfield.shared.Logging +import com.callstack.react.brownfield.shared.ExplodeAarTask import com.callstack.react.brownfield.utils.Utils import org.gradle.api.Project -import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.TaskProvider import java.io.File class ProguardProcessor(val project: Project) { - fun processConsumerFiles( + fun processFiles( proguardRules: List, capitalizedVariantName: String, - ) { + explodeTask: TaskProvider, + ) { val mergeTaskName = "merge${capitalizedVariantName}ConsumerProguardFiles" - val mergeFileTask = project.tasks.named(mergeTaskName) - - if (!mergeFileTask.isPresent) { - throw TaskNotFound("Task $mergeTaskName not found") - } - - mergeFileTask.get().doLast { - val outputFile = it.outputs.files.singleFile - doLast(proguardRules, outputFile) - } - } - - fun processGeneratedFiles( - proguardRules: List, - capitalizedVariantName: String, - ) { - val mergeGenerateProguardTask: TaskProvider<*>? - val mergeName = "merge${capitalizedVariantName}GeneratedProguardFiles" - mergeGenerateProguardTask = project.tasks.named(mergeName) - - mergeGenerateProguardTask.get().doLast { - val outputFile = it.outputs.files.singleFile - doLast(proguardRules, outputFile) - } - } - - private fun doLast( - files: List, - outputFile: File, - ) { - try { - val outputFileToMerge = - @Suppress("USELESS_IS_CHECK") - if (outputFile is File) { - outputFile - } else { - (outputFile as? RegularFileProperty)?.get()?.asFile - ?: error("Invalid output file") - } - Utils.mergeFiles(files, outputFileToMerge) - } catch (e: NoSuchFileException) { - Logging.log(e) + project.tasks.matching { it.name == mergeTaskName }.configureEach { task -> + task.dependsOn(explodeTask) + task.doLast { + val outputFile = task.outputs.files.singleFile + Utils.mergeFiles(proguardRules, outputFile) + } } } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ResourceTaskProcessor.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ResourceTaskProcessor.kt index f58416bb..656a3225 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ResourceTaskProcessor.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ResourceTaskProcessor.kt @@ -1,26 +1,17 @@ package com.callstack.react.brownfield.processors -import com.android.build.gradle.api.LibraryVariant -import com.callstack.react.brownfield.exceptions.TaskNotFound +import com.android.build.api.variant.LibraryVariant import com.callstack.react.brownfield.utils.AndroidArchiveLibrary -import com.callstack.react.brownfield.utils.capitalized -import org.gradle.api.Project object ResourceTaskProcessor { fun process( variant: LibraryVariant, - project: Project, aarLibraries: List, ) { - val taskPath = "generate${variant.name.capitalized()}Resources" - val resourceGenTask = project.tasks.named(taskPath) + val resDirectories = aarLibraries.map { it.getResDir() }.filter { it.exists() } - if (!resourceGenTask.isPresent) { - throw TaskNotFound("Task $taskPath not found") + resDirectories.forEach { resDirectory -> + variant.sources.res?.addStaticSourceDirectory(resDirectory.path) } - - variant.registerGeneratedResFolders( - project.files(aarLibraries.map { it.getResDir() }), - ) } } 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 a4bf30e3..ff3e2fa0 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 @@ -1,7 +1,6 @@ package com.callstack.react.brownfield.processors -import com.android.build.gradle.internal.tasks.factory.dependsOn -import com.callstack.react.brownfield.exceptions.TaskNotFound +import com.android.build.api.variant.LibraryVariant import com.callstack.react.brownfield.shared.ExplodeAarTask import com.callstack.react.brownfield.utils.AndroidArchiveLibrary import com.callstack.react.brownfield.utils.DirectoryManager @@ -28,77 +27,28 @@ class VariantTaskProvider(val project: Project) { } } - fun processDataBinding( - bundleTask: TaskProvider, - aarLibraries: Collection, - variantName: String, - ) { - bundleTask.configure { task -> - task.doLast { - aarLibraries.forEach { - val dataBindingFolder = it.getDataBindingFolder() - if (dataBindingFolder.exists()) { - val filePath = getReBundleFilePath(dataBindingFolder.name, variantName) - File(filePath).mkdirs() - project.copy { copyTask -> - copyTask.from(dataBindingFolder) - copyTask.into(filePath) - } - } - - val dataBindingLogFolder = it.getDataBindingLogFolder() - if (dataBindingLogFolder.exists()) { - val filePath = getReBundleFilePath(dataBindingLogFolder.name, variantName) - File(filePath).mkdirs() - project.copy { copyTask -> - copyTask.from(dataBindingLogFolder) - copyTask.into(filePath) - } - } - } - } - } - } - - private fun getReBundleFilePath( - folderName: String, - variantName: String, - ) = "${DirectoryManager.getReBundleDirectory( - variantName, - ).path}/$folderName" - fun preBuildTaskByVariant( - variantName: String, - buildTypeName: String, - isVariantDebuggable: Boolean, + variant: LibraryVariant, explodeAarTask: TaskProvider, ) { - val preBuildTaskPath = "pre${variantName.capitalized()}Build" - val preBuildTask = project.tasks.named(preBuildTaskPath) - - if (!preBuildTask.isPresent) { - throw TaskNotFound("Can not find $preBuildTaskPath task") - } - - preBuildTask.dependsOn(explodeAarTask) - + val variantName = variant.name val bundledAssetsVariantName = Utils.getBundledAssetsVariantName( variantName = variantName, - buildTypeName = buildTypeName, - isDebuggable = isVariantDebuggable, + buildTypeName = variant.buildType, + isDebuggable = variant.debuggable, ) val capitalizedBundledAssetsVariantName = bundledAssetsVariantName.capitalized() val projectExt = project.extensions.getByType(Extension::class.java) val appProject = project.rootProject.project(projectExt.appProjectName) - preBuildTask.dependsOn("${appProject.path}:createBundle${capitalizedBundledAssetsVariantName}JsAndAssets") + + val jsBundleTaskName = "${appProject.path}:createBundle${capitalizedBundledAssetsVariantName}JsAndAssets" - if (Utils.isExpoProject(project)) { + variant.lifecycleTasks.registerPreBuild(explodeAarTask, jsBundleTaskName) + if (Utils.isExpoProject(project) && Utils.hasExpoUpdates(appProject, variantName)) { val updatesResourcesTaskName = Utils.getExpoUpdatesResourcesTaskName(variantName) - if (Utils.hasExpoUpdates(appProject, variantName)) { - preBuildTask.dependsOn("${appProject.path}:$updatesResourcesTaskName") - } + variant.lifecycleTasks.registerPreBuild(updatesResourcesTaskName) } } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/BundleTaskProvider.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/BundleTaskProvider.kt index 26a0452c..2e5630e4 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/BundleTaskProvider.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/BundleTaskProvider.kt @@ -1,10 +1,9 @@ -@file:Suppress("DEPRECATION") - package com.callstack.react.brownfield.shared -import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.api.LibraryVariant +import com.android.build.api.dsl.LibraryExtension +import com.android.build.api.variant.LibraryVariant import com.callstack.react.brownfield.processors.VariantTaskProvider +import com.callstack.react.brownfield.utils.capitalized import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.tasks.TaskProvider @@ -13,34 +12,25 @@ class BundleTaskProvider(private val variantTaskProvider: VariantTaskProvider) { fun getBundleTask( project: Project, variant: LibraryVariant, - ): TaskProvider? { - val androidExtension = project.extensions.getByType(LibraryExtension::class.java) - - // Find the first variant in the library that matches our criteria - val matchedVariant = - androidExtension.libraryVariants.find { libraryVariant -> - // 1. Try Simple Match - if (libraryVariant.name == variant.name || libraryVariant.name == variant.buildType.name) { - return@find true + ): TaskProvider { + val androidComponent = project.extensions.findByType(LibraryExtension::class.java) + // TODO: evaluate if default makes sense + var taskName = variant.name + + androidComponent?.buildTypes?.forEach { + if (it.name == variant.name || it.name == variant.buildType) { + if (androidComponent.productFlavors.isEmpty()) { + taskName = it.name.capitalized() + return@forEach } - // 2. Try Dimension Strategy Match - val flavor = variant.productFlavors.firstOrNull() ?: variant.mergedFlavor - val strategies = - runCatching { - androidExtension.productFlavors.getByName(flavor.name).missingDimensionStrategies - }.getOrNull() ?: return@find false - - strategies.any { (dimension, strategy) -> - val fallbacks = listOf(strategy.requested) + strategy.fallbacks - val libFlavor = libraryVariant.productFlavors.firstOrNull() ?: libraryVariant.mergedFlavor - - dimension == libFlavor.dimension && - fallbacks.contains(libFlavor.name) && - variant.buildType.name == libraryVariant.buildType.name + androidComponent.productFlavors.forEach { productFlavor -> + taskName = "${productFlavor.name.capitalized()}${it.name.capitalized()}" + return@forEach } } - return matchedVariant?.let { variantTaskProvider.bundleTaskProvider(project, it.name) } + } + return variantTaskProvider.bundleTaskProvider(project, taskName) } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt index 669e0497..d6a5537f 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt @@ -38,7 +38,6 @@ object Constants { const val PROJECT_ID = "com.callstack.react.brownfield" const val PLUGIN_NAME = "react-brownfield" - const val RE_BUNDLE_FOLDER = "aar_rebundle" const val INTERMEDIATES_TEMP_DIR = PLUGIN_NAME val BROWNFIELD_EXPO_TRANSITIVE_DEPS_WHITELISTED_MODULES_FOR_DISCOVERY = diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/ExplodeAarTask.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/ExplodeAarTask.kt index 79a90320..079ccaa4 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/ExplodeAarTask.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/ExplodeAarTask.kt @@ -6,10 +6,12 @@ import com.callstack.react.brownfield.utils.DirectoryManager import org.gradle.api.DefaultTask import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction +@CacheableTask abstract class ExplodeAarTask : DefaultTask() { @get:Internal abstract val inputArtifacts: ListProperty diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Logging.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Logging.kt index a5b54a4c..dc72a9ce 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Logging.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Logging.kt @@ -5,7 +5,7 @@ import com.callstack.react.brownfield.shared.Constants.PLUGIN_NAME object Logging : BaseProject() { fun error( message: Any, - error: Throwable, + error: Throwable?, ) { project.logger.error(getFormattedMessage(message), error) } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeLibraryManifestTask.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeLibraryManifestTask.kt new file mode 100644 index 00000000..5899ac71 --- /dev/null +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeLibraryManifestTask.kt @@ -0,0 +1,178 @@ +package com.callstack.react.brownfield.shared + +import com.android.manifmerger.ManifestMerger2 +import com.android.manifmerger.ManifestProvider +import com.android.manifmerger.ManifestSystemProperty +import com.android.manifmerger.MergingReport +import org.apache.tools.ant.BuildException +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +@CacheableTask +abstract class MergeLibraryManifestTask : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val inputManifest: RegularFileProperty + + @get:OutputFile + abstract val outputManifest: RegularFileProperty + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val aarManifestFiles: ConfigurableFileCollection + + @get:Input + abstract val variantName: Property + + @get:Input + abstract val namespace: Property + + @get:Input + abstract val manifestPlaceholders: MapProperty + + @TaskAction + fun run() { + val mainManifestFile = inputManifest.get().asFile + val outputFile = outputManifest.get().asFile + val dependencyManifests = aarManifestFiles.files.filter(File::exists) + + outputFile.parentFile.mkdirs() + + if (dependencyManifests.isEmpty()) { + Files.copy(mainManifestFile.toPath(), outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + return + } + + val tempDir = temporaryDir + val (sanitizedManifestFile, extractedSdkVersions) = + stripSdkVersionsFromManifest(mainManifestFile, tempDir) + + try { + val iLogger = GradleILogger(logger) + val mergerInvoker = + ManifestMerger2 + .newMerger(sanitizedManifestFile, iLogger, ManifestMerger2.MergeType.LIBRARY) + .setNamespace(namespace.get()) + .setPlaceHolderValues(manifestPlaceholders.get().mapValues { it.value as Any }) + + extractedSdkVersions.forEach { (property, value) -> + mergerInvoker.setOverride(property, value) + } + + mergerInvoker.addManifestProviders( + dependencyManifests.map { manifestFile -> + object : ManifestProvider { + override fun getManifest(): File = manifestFile.absoluteFile + + override fun getName(): String = manifestFile.name + } + }, + ) + + val mergingReport = mergerInvoker.merge() + + if (mergingReport.result.isError) { + logger.error("Manifest merge failed for variant ${variantName.get()}") + logger.error(mergingReport.reportString) + mergingReport.log(iLogger) + throw BuildException(mergingReport.reportString) + } + + val mergedDocument = + mergingReport.getMergedDocument(MergingReport.MergedManifestKind.MERGED) + ?: throw BuildException("Manifest merge produced no output for variant ${variantName.get()}") + outputFile.writeText(mergedDocument, Charsets.UTF_8) + } finally { + sanitizedManifestFile.delete() + } + } + + /** + * AGP 9.0+ ManifestMerger2 rejects a main manifest that contains with SDK-version + * attributes (minSdkVersion / targetSdkVersion / maxSdkVersion), because those values are + * already tracked by the build system. This helper parses the manifest with the DOM API, + * removes only those offending attributes, drops the element entirely when no + * meaningful attributes remain, and writes the result to a temporary file in [tempDir] + * (Gradle's task-private scratch area, outside the declared outputs). + * + * It returns both the sanitized temp file and a map of the stripped SDK version values so the + * caller can restore them via ManifestMerger2.Invoker.setOverride(), keeping merger behaviour + * identical to before while satisfying the new validation. + */ + private fun stripSdkVersionsFromManifest( + manifestFile: File, + tempDir: File, + ): Pair> { + val sdkVersionAttributes = + mapOf( + "minSdkVersion" to ManifestSystemProperty.UsesSdk.MIN_SDK_VERSION, + "targetSdkVersion" to ManifestSystemProperty.UsesSdk.TARGET_SDK_VERSION, + "maxSdkVersion" to ManifestSystemProperty.UsesSdk.MAX_SDK_VERSION, + ) + val androidNs = "http://schemas.android.com/apk/res/android" + + val document = + DocumentBuilderFactory.newInstance().apply { + isNamespaceAware = true + setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) + setFeature("http://xml.org/sax/features/external-general-entities", false) + setFeature("http://xml.org/sax/features/external-parameter-entities", false) + }.newDocumentBuilder().parse(manifestFile) + + val usesSdkNodes = document.getElementsByTagNameNS("*", "uses-sdk") + val nodesToRemove = mutableListOf() + val extractedSdkVersions = mutableMapOf() + + for (i in 0 until usesSdkNodes.length) { + val element = usesSdkNodes.item(i) as? org.w3c.dom.Element ?: continue + + sdkVersionAttributes.forEach { (attrName, property) -> + val value = element.getAttributeNS(androidNs, attrName) + if (value.isNotEmpty()) { + extractedSdkVersions[property] = value + element.removeAttributeNS(androidNs, attrName) + } + } + + val remainingAttrs = element.attributes + val hasMeaningfulRemainingAttrs = + (0 until remainingAttrs.length).any { j -> + val attr = remainingAttrs.item(j) + attr.prefix != "xmlns" && attr.nodeName != "xmlns" + } + + if (!hasMeaningfulRemainingAttrs) { + nodesToRemove.add(element) + } + } + + nodesToRemove.forEach { it.parentNode?.removeChild(it) } + + tempDir.mkdirs() + val tempFile = File(tempDir, "sanitized_${manifestFile.name}") + TransformerFactory.newInstance().newTransformer().transform( + DOMSource(document), + StreamResult(tempFile), + ) + + return Pair(tempFile, extractedSdkVersions) + } +} diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt index b7513ca9..f9dfcc74 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt @@ -70,8 +70,4 @@ class AndroidArchiveLibrary( fun getResDir(): File = File(getExplodedAarRootDir(), "res") fun getProguardRules(): File = File(getExplodedAarRootDir(), "proguard.txt") - - fun getDataBindingFolder(): File = File(getExplodedAarRootDir(), "data-binding") - - fun getDataBindingLogFolder(): File = File(getExplodedAarRootDir(), "data-binding-base-class-log") } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/DirectoryManager.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/DirectoryManager.kt index 1f9c1e5b..52c8e049 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/DirectoryManager.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/DirectoryManager.kt @@ -2,7 +2,6 @@ package com.callstack.react.brownfield.utils import com.callstack.react.brownfield.shared.BaseProject import com.callstack.react.brownfield.shared.Constants.INTERMEDIATES_TEMP_DIR -import com.callstack.react.brownfield.shared.Constants.RE_BUNDLE_FOLDER import java.io.File object DirectoryManager : BaseProject() { @@ -13,8 +12,4 @@ object DirectoryManager : BaseProject() { fun getKotlinMetaDirectory(variantName: String): File { return project.file("$buildDir/tmp/kotlin-classes/$variantName/META-INF") } - - fun getReBundleDirectory(variantName: String): File { - return project.file("$buildDir/outputs/$RE_BUNDLE_FOLDER/$variantName") - } } diff --git a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt index b31dd519..18ede494 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt @@ -13,15 +13,6 @@ open class Extension { */ var appProjectName = "app" - /** - * List of dynamic libs (.so) files that you wish to bundle with - * the aar. - * - * By default, only `libappmodules.so` and `libreact_codegen_*.so` are - * bundled. - */ - var dynamicLibs = listOf() - /** * Whether to use stripped .so files. * diff --git a/packages/react-native-brownfield/src/expo-config-plugin/template/android/build.gradle.kts b/packages/react-native-brownfield/src/expo-config-plugin/template/android/build.gradle.kts index 827fff57..7699fb90 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/template/android/build.gradle.kts +++ b/packages/react-native-brownfield/src/expo-config-plugin/template/android/build.gradle.kts @@ -69,6 +69,8 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") + + missingDimensionStrategy("abc", "dev") } buildFeatures {