diff --git a/.changeset/cute-turtles-reply.md b/.changeset/cute-turtles-reply.md new file mode 100644 index 00000000..20b0758d --- /dev/null +++ b/.changeset/cute-turtles-reply.md @@ -0,0 +1,6 @@ +--- +'@callstack/react-native-brownfield': minor +'@callstack/brownfield-cli': minor +--- + +refactor: remove deprecated APIs diff --git a/apps/ExpoApp56/app.json b/apps/ExpoApp56/app.json index ce594fb3..0d2238ec 100644 --- a/apps/ExpoApp56/app.json +++ b/apps/ExpoApp56/app.json @@ -37,12 +37,7 @@ } } ], - [ - "@callstack/react-native-brownfield", - { - "debug": true - } - ], + "@callstack/react-native-brownfield", "expo-image", "expo-font", "expo-web-browser" diff --git a/apps/ExpoApp56/package.json b/apps/ExpoApp56/package.json index 834b77ba..01a85523 100644 --- a/apps/ExpoApp56/package.json +++ b/apps/ExpoApp56/package.json @@ -16,6 +16,17 @@ "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release --use-prebuilt-rn-core --use-prebuilt-expo --verbose", "eas:stg": "EXPO_TOKEN=$EAS_TOKEN eas update --channel production --message 'testing 1st stg channel update' --platform ios --environment staging" }, + "brownfield": { + "debug": true, + "android": { + "expo": { + "missingDimensionStrategies": [ + "type", + "alpha" + ] + } + } + }, "dependencies": { "@callstack/brownfield-navigation": "workspace:^", "@callstack/brownie": "workspace:^", 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/build.gradle b/apps/RNApp/android/build.gradle index 5fb6bd9f..77650a07 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-alpha06-SNAPSHOT") + classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha07-SNAPSHOT") } } diff --git a/apps/scripts/prepare-android-build-gradle-for-ci.ts b/apps/scripts/prepare-android-build-gradle-for-ci.ts index efeb0076..cda5bdf1 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-alpha06-SNAPSHOT'; +const SNAPSHOT_VERSION = '2.0.0-alpha07-SNAPSHOT'; const targetPath = path.resolve( __dirname, '..', diff --git a/docs/docs/docs/api-reference/configuration.mdx b/docs/docs/docs/api-reference/configuration.mdx index 0c16c862..84fe3bfd 100644 --- a/docs/docs/docs/api-reference/configuration.mdx +++ b/docs/docs/docs/api-reference/configuration.mdx @@ -186,8 +186,21 @@ All file-based platform options mirror CLI flags, but they use camelCase propert | `android.expo.groupId` | `string` | Maven group ID used when publishing the AAR. | | `android.expo.artifactId` | `string` | Maven artifact ID used when publishing the AAR. | | `android.expo.version` | `string` | Maven version used when publishing the AAR. | +| `android.expo.missingDimensionStrategies` | `string[]` | Gradle `missingDimensionStrategy` arguments: dimension name followed by one or more flavors. | | `android.expo.useLocalGradlePlugin` | `boolean` | Load the Brownfield Gradle plugin from `node_modules` via `includeBuild` instead of the Maven classpath dependency. Disabled by default. | +Example: + +```json +{ + "android": { + "expo": { + "missingDimensionStrategies": ["type", "alpha"] + } + } +} +``` + ### iOS keys | Key | Type | Description | diff --git a/docs/docs/docs/guides/troubleshooting.mdx b/docs/docs/docs/guides/troubleshooting.mdx index 5305eb21..46ce0bd2 100644 --- a/docs/docs/docs/guides/troubleshooting.mdx +++ b/docs/docs/docs/guides/troubleshooting.mdx @@ -37,3 +37,44 @@ implementation("com.google.android.material:material:1.13.0") ## [iOS] `No script URL provided` in Release configuration If you encounter this error, most likely you have missed a setup step and are missing `ReactNativeBrownfield.shared.bundle = ReactNativeBundle` before your call to `startReactNative`. + +## [Android] Defining Missing Dimension Strategies + +This maybe required if any third party library has defined product flavors and dimensions. If so, then the consuming modules, in brownfield case, `:app` and `:brownfieldlib` need +to define the `missingDimensionStrategy`. + +:::info +This should be only required for bare react native. For Expo, this is already handled by `@callstack/react-native-brownfield` expo config plugin. +You only need to define `android.expo.missingDimensionStrategies` using the brownfield config. +::: + +#### Step 1: +```groovy +// app/build.gradle + +android { + defaultConfig { + ... + versionName "1.0" + missingDimensionStrategy("type", "alpha") + } +} +``` + +#### Step 2: +```kts +// brownfieldlib/build.gradle + +reactBrownfield { + missingDimensionStrategies = listOf("type", "alpha") +} + +android { + defaultConfig { + ... + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + missingDimensionStrategy("type", "alpha") + } +} +``` \ No newline at end of file diff --git a/gradle-plugins/react/brownfield/config/detekt/detekt.yml b/gradle-plugins/react/brownfield/config/detekt/detekt.yml index 50c5af28..43ba1660 100644 --- a/gradle-plugins/react/brownfield/config/detekt/detekt.yml +++ b/gradle-plugins/react/brownfield/config/detekt/detekt.yml @@ -844,7 +844,7 @@ style: ignoredCharacters: [] ThrowsCount: active: true - max: 2 + max: 3 excludeGuardClauses: false TrailingWhitespace: active: false diff --git a/gradle-plugins/react/brownfield/gradle.properties b/gradle-plugins/react/brownfield/gradle.properties index bc623fe1..e2fe5e97 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-alpha06 +VERSION=2.0.0-alpha07 GROUP=com.callstack.react IMPLEMENTATION_CLASS=com.callstack.react.brownfield.plugin.RNBrownfieldPlugin 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..86f48480 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.dsl.LibraryExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension -import com.android.build.gradle.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 36217fa9..6f85dd47 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 @@ -22,6 +22,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 @@ -66,11 +67,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) } } @@ -125,16 +129,14 @@ class RNBrownfieldPlugin : Plugin { variantTaskProvider: VariantTaskProvider, ) { val variantName = variant.name - val capitalizedVariantName = variantName.replaceFirstChar(Char::titlecase) + val capitalizedVariantName = variantName.capitalized() /** ======= EXPLODE AAR =========*/ val explodeTask = ExplodeTaskProvider.getTask(variant, project, artifacts) /** ======= PreBuild =========*/ variantTaskProvider.preBuildTaskByVariant( - variantName, - variant.buildType.name, - variant.buildType.isDebuggable, + variant, explodeTask, ) @@ -142,7 +144,7 @@ class RNBrownfieldPlugin : Plugin { /** ======= MERGE CLASSES =========*/ val mergeClassesTask = variantTaskProvider.mergeClasses(aarLibraries, explodeTask, variantName) - project.tasks.named(VariantHelper.getAsmTransformTaskName(capitalizedVariantName)).configure { + VariantHelper.getAsmTransformTask(project, capitalizedVariantName).configureEach { it.dependsOn(mergeClassesTask) } @@ -152,27 +154,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) /** ===== 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 bf0bda48..9d6c38ac 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.dsl.LibraryExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension -import com.android.build.gradle.LibraryExtension import com.callstack.react.brownfield.exceptions.NameSpaceNotFound import com.callstack.react.brownfield.processors.VariantHelper import com.callstack.react.brownfield.utils.Extension @@ -43,10 +43,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 = @@ -58,37 +58,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 = VariantHelper.getExpoUpdatesResourcesTaskName(variant.name) + 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") + } - val appBuildDir = getAppBuildDir() - sourceSet.assets.srcDirs(bundlePathSegments.map { "$appBuildDir/generated/assets/$it" }) - sourceSet.res.srcDirs(bundlePathSegments.map { "$appBuildDir/generated/res/$it" }) - if (extension.useStrippedSoFiles) { - val capitalizedVariantName = variantName.capitalized() - val libsDir = project.layout.projectDirectory.dir("libs$capitalizedVariantName") - val copyTaskName = "copy${capitalizedVariantName}LibSources" - sourceSet.jniLibs.srcDir( - project.files(libsDir).builtBy(copyTaskName), - ) - } - 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), - ) - } + if (Utils.hasExpoUpdates(appProject, variantName)) { + val updateResourcesPathSegment = VariantHelper.getExpoUpdatesResourcesTaskName(variant.name) + 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 d7d95ecd..c4d39545 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,9 +1,10 @@ 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 +import com.callstack.react.brownfield.utils.Extension import com.callstack.react.brownfield.utils.capitalized import org.gradle.api.Project import org.gradle.api.Task @@ -20,6 +21,7 @@ object ExplodeTaskProvider { val variantTaskProvider = VariantTaskProvider(project) val bundleProvider = BundleTaskProvider(variantTaskProvider) val capitalizedVariantName = variant.name.capitalized() + val projectExt = project.extensions.getByType(Extension::class.java) return project.tasks.register( "explode${capitalizedVariantName}Aar", @@ -31,13 +33,11 @@ object ExplodeTaskProvider { 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 bundleTaskProvider = bundleProvider.getBundleTask(dependencyProject, variant, projectExt.missingDimensionStrategies) + 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 7ec7d3c2..fa53fc59 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,142 +1,134 @@ 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.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.DefaultTask import org.gradle.api.Project -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.TaskProvider +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction 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 - copySoLibsTask(variantName) - - mergeJniLibsTask.configure { - 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 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 IGNORE_EMBEDDED_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, + outDir: File, existingJNILibs: MutableMap>, ) { - val (fromDir, intoDir) = getStrippedLibsPath(variantName) + val fromDir = strippedLibsDir.get().asFile + if (!fromDir.exists()) return - existingJNILibs.forEach { (arch, libNames) -> - copyLibsForArchitecture(fromDir, intoDir, arch, libNames) + existingJNILibs.keys.forEach { arch -> + copyArchStrippedSoLibs(fromDir, outDir, arch) } } - private fun copyLibsForArchitecture( + private fun copyArchStrippedSoLibs( fromDir: File, - intoDir: File, + outDir: 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) + val destArchDir = File(outDir, arch).apply { mkdirs() } + sourceArchDir.listFiles()?.forEach { file -> + copyIfAllowed(file, destArchDir) } } - private fun copyLibFile( - sourceArchDir: File, + private fun copyIfAllowed( + file: 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}") - } + val name = file.name + if (!matchesAllowedLib(name)) return + + try { + file.copyTo(File(destArchDir, name), overwrite = true) + } catch (e: java.io.IOException) { + Logging.error("Failed to copy $name: ${e.message}", e.cause) } } + private fun matchesAllowedLib(name: String): Boolean { + val isCodegenLib = name.startsWith("libreact_codegen_") && name.endsWith(".so") + val isIgnoredLib = IGNORE_EMBEDDED_LIBS.any { lib -> name == lib || name.contains(lib) } + return name == "libappmodules.so" || isCodegenLib || !isIgnoredLib + } + private fun processNestedLibs( files: Array?, existingJNILibs: MutableMap>, @@ -147,17 +139,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, + ) { + 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) { + 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..77755acc 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/VariantHelper.kt b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantHelper.kt index 03d2070c..b01436a9 100644 --- a/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantHelper.kt +++ b/gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantHelper.kt @@ -56,4 +56,11 @@ object VariantHelper { ): TaskCollection { return project.tasks.matching { it.name == "compile${capitalizedVariantName}JavaWithJavac" } } + + fun getAsmTransformTask( + project: Project, + capitalizedVariantName: String, + ): TaskCollection { + return project.tasks.matching { it.name == getAsmTransformTaskName(capitalizedVariantName) } + } } 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 ff82ca59..f17204e1 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.shared.MergeClassesTask import com.callstack.react.brownfield.utils.AndroidArchiveLibrary @@ -13,7 +12,6 @@ import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.UnknownTaskException import org.gradle.api.tasks.TaskProvider -import java.io.File class VariantTaskProvider(val project: Project) { fun bundleTaskProvider( @@ -29,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 = VariantHelper.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") - if (Utils.isExpoProject(project)) { + val jsBundleTaskName = "${appProject.path}:createBundle${capitalizedBundledAssetsVariantName}JsAndAssets" + + variant.lifecycleTasks.registerPreBuild(explodeAarTask, jsBundleTaskName) + if (Utils.isExpoProject(project) && Utils.hasExpoUpdates(appProject, variantName)) { val updatesResourcesTaskName = VariantHelper.getExpoUpdatesResourcesTaskName(variantName) - if (Utils.hasExpoUpdates(appProject, variantName)) { - preBuildTask.dependsOn("${appProject.path}:$updatesResourcesTaskName") - } + variant.lifecycleTasks.registerPreBuild("${appProject.path}:$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..15e964d4 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,10 @@ -@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.dsl.LibraryProductFlavor +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 +13,79 @@ 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 - } + missingDimensionStrategies: List, + ): TaskProvider { + val taskName = resolveTaskName(project, variant, missingDimensionStrategies) + return variantTaskProvider.bundleTaskProvider(project, taskName) + } + + private fun resolveTaskName( + project: Project, + variant: LibraryVariant, + missingDimensionStrategies: List, + ): String { + val androidComponent = project.extensions.findByType(LibraryExtension::class.java) ?: return variant.name - // 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 + for (buildType in androidComponent.buildTypes) { + if (buildType.name != variant.name && buildType.name != variant.buildType) { + continue + } - strategies.any { (dimension, strategy) -> - val fallbacks = listOf(strategy.requested) + strategy.fallbacks - val libFlavor = libraryVariant.productFlavors.firstOrNull() ?: libraryVariant.mergedFlavor + if (androidComponent.productFlavors.isEmpty()) { + return buildType.name.capitalized() + } - dimension == libFlavor.dimension && - fallbacks.contains(libFlavor.name) && - variant.buildType.name == libraryVariant.buildType.name + val flavorNames = androidComponent.productFlavors.map { fl -> fl.name } + for (productFlavor in androidComponent.productFlavors) { + handleMissingDimensionStrategies(missingDimensionStrategies, project, flavorNames, productFlavor) + val flavorName = missingDimensionStrategies[1] + if (productFlavor.name != flavorName) { + continue } + + return "${productFlavor.name.capitalized()}${buildType.name.capitalized()}" } + } - return matchedVariant?.let { variantTaskProvider.bundleTaskProvider(project, it.name) } + return variant.name + } + + private fun handleMissingDimensionStrategies( + missingDimensionStrategies: List, + project: Project, + flavorNames: List, + productFlavor: LibraryProductFlavor, + ) { + if (missingDimensionStrategies.isEmpty()) { + throw MissingDimensionStrategiesException(getErrorMessage(project.name)) + } + + val dimension = missingDimensionStrategies[0] + if (productFlavor.dimension != dimension) { + throw DimensionMismatchException( + "Ensure the provided dimension matches the ${project.name}'s dimension. Required dimension: $dimension", + ) + } + + val flavorName = missingDimensionStrategies[1] + if (!flavorNames.contains(flavorName)) { + throw FlavorMismatchException( + "Ensure the provided flavor matches the ${project.name}'s flavor. Required flavor(s): $flavorNames", + ) + } + } + + private fun getErrorMessage(projectName: String): String { + return """ + Provide `missingDimensionStrategies` for $projectName: + If using Expo, provide the `android.expo.missingDimensionStrategies` property from Brownfield Config. + If using Bare React Native, provide the `reactBrownfield.missingDimensionStrategies` property. + """.trimIndent() } } + +class MissingDimensionStrategiesException(message: String) : IllegalStateException(message) + +class DimensionMismatchException(message: String) : IllegalArgumentException(message) + +class FlavorMismatchException(message: String) : IllegalArgumentException(message) 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 acbeba14..c8ab99a8 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 @@ -4,10 +4,12 @@ import com.callstack.react.brownfield.utils.AndroidArchiveLibrary 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..e613e04b 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. * @@ -41,4 +32,12 @@ open class Extension { set(value) { useStrippedSoFiles = value } + + /** + * List of missing dimension strategies. + * + * Provide in this format: + * listOf("type", "alpha") + */ + var missingDimensionStrategies = listOf() } diff --git a/packages/cli/schema.json b/packages/cli/schema.json index 9ab1ecb5..f8da9c24 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -65,6 +65,13 @@ "description": "Minimum SDK version for the Android library.", "type": "number" }, + "missingDimensionStrategies": { + "description": "Missing dimension strategies as dimension + flavor(s), e.g. \"type, alpha\".", + "items": { + "type": "string" + }, + "type": "array" + }, "packageName": { "description": "The package name for the Android library module.", "type": "string" diff --git a/packages/cli/src/__tests__/expoPluginConfig.test.ts b/packages/cli/src/__tests__/expoPluginConfig.test.ts index 6745c71e..c402d643 100644 --- a/packages/cli/src/__tests__/expoPluginConfig.test.ts +++ b/packages/cli/src/__tests__/expoPluginConfig.test.ts @@ -206,6 +206,7 @@ describe('resolveBrownfieldPluginConfig', () => { artifactId: 'brownfieldlib', version: '0.0.1-SNAPSHOT', useLocalGradlePlugin: false, + missingDimensionStrategies: [], }, }); }); @@ -270,6 +271,7 @@ describe('resolveBrownfieldPluginConfig', () => { expo: { minSdkVersion: 26, version: '2.0.0', + missingDimensionStrategies: ['type', 'alpha'], }, }, ios: { @@ -288,6 +290,7 @@ describe('resolveBrownfieldPluginConfig', () => { expo: { minSdkVersion: 26, version: '2.0.0', + missingDimensionStrategies: ['type', 'alpha'], }, }, ios: { @@ -309,6 +312,7 @@ describe('resolveBrownfieldPluginConfig', () => { moduleName: 'mylib', minSdkVersion: 26, version: '2.0.0', + missingDimensionStrategies: ['type', 'alpha'], }); expect(resolved.ios).toMatchObject({ frameworkName: 'MyLib', @@ -348,4 +352,22 @@ describe('resolveBrownfieldPluginConfig', () => { expect(resolved.android?.useLocalGradlePlugin).toBe(true); }); + + it('maps android.missingDimensionStrategies from legacy app.json plugin props', () => { + const resolved = resolveBrownfieldPluginConfig( + { + android: { + moduleName: 'mylib', + missingDimensionStrategies: ['type', 'alpha'], + }, + }, + null, + baseExpoConfig + ); + + expect(resolved.android?.missingDimensionStrategies).toEqual([ + 'type', + 'alpha', + ]); + }); }); diff --git a/packages/cli/src/expoPluginConfig.ts b/packages/cli/src/expoPluginConfig.ts index 9b455636..7ba77cf3 100644 --- a/packages/cli/src/expoPluginConfig.ts +++ b/packages/cli/src/expoPluginConfig.ts @@ -24,6 +24,7 @@ export type BrownfieldPluginProps = { artifactId?: string; version?: string; useLocalGradlePlugin?: boolean; + missingDimensionStrategies?: string[]; }; }; @@ -37,6 +38,7 @@ export type ResolvedBrownfieldPluginAndroidConfig = { artifactId: string; version: string; useLocalGradlePlugin: boolean; + missingDimensionStrategies: string[]; }; export type ResolvedBrownfieldPluginIosConfig = { @@ -189,6 +191,8 @@ export function resolveBrownfieldPluginConfig( version: effectiveProps.android?.version ?? '0.0.1-SNAPSHOT', useLocalGradlePlugin: effectiveProps.android?.useLocalGradlePlugin ?? false, + missingDimensionStrategies: + effectiveProps.android?.missingDimensionStrategies ?? [], } : null, }; diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 3c213b48..3ade87b4 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -93,6 +93,11 @@ export type BrownfieldExpoAndroidConfig = { * Disabled by default. */ useLocalGradlePlugin?: boolean; + + /** + * Missing dimension strategies as dimension + flavor(s), e.g. "type, alpha". + */ + missingDimensionStrategies?: string[]; }; /** 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 a9cf8a30..da74e44b 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-alpha06")' + 'classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha07")' ); }); diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts index 1e958099..1856cf48 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts @@ -193,6 +193,40 @@ describe('createAndroidModule', () => { ); }); + it('omits missingDimensionStrategy block when no strategies are defined', () => { + const androidDir = createAndroidDir(); + + createAndroidModule({ + androidDir, + config: createConfig(), + rnVersion: '0.85.3', + }); + + const buildGradle = readLibraryBuildGradle(androidDir); + expect(buildGradle).toContain('missingDimensionStrategies = listOf()'); + expect(buildGradle).not.toContain('missingDimensionStrategy('); + }); + + it('renders missingDimensionStrategies into the generated build.gradle', () => { + const androidDir = createAndroidDir(); + + createAndroidModule({ + androidDir, + config: createConfig({ + android: { + missingDimensionStrategies: ['type', 'alpha'], + }, + }), + rnVersion: '0.85.3', + }); + + const buildGradle = readLibraryBuildGradle(androidDir); + expect(buildGradle).toContain( + 'missingDimensionStrategies = listOf("type", "alpha")' + ); + expect(buildGradle).toContain('missingDimensionStrategy("type", "alpha")'); + }); + it('uses api dependencies and the installed React Native Hermes artifact for newer Expo projects', () => { const androidDir = createAndroidDir(); const projectRoot = path.dirname(androidDir); @@ -274,6 +308,7 @@ HERMES_V1_VERSION_NAME=250829098.0.10 artifactId: 'brownfieldlib', version: '1.0.0', useLocalGradlePlugin: false, + missingDimensionStrategies: [], ...(overrides.android ?? {}), }, }; 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 df6ada47..62d6d2a4 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-alpha06'; +export const BROWNFIELD_PLUGIN_VERSION = '2.0.0-alpha07'; export const brownfieldGradlePluginDependency = `classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`; diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/utils/formatHelpers.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/formatHelpers.ts new file mode 100644 index 00000000..fa275616 --- /dev/null +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/formatHelpers.ts @@ -0,0 +1,7 @@ +export function formatMissingDimensionStrategies( + missingDimensionStrategies: string[] +): string { + return missingDimensionStrategies + .map((value) => JSON.stringify(value)) + .join(', '); +} diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/utils/gradleHelpers.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/gradleHelpers.ts index 990a0f65..4c891155 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/utils/gradleHelpers.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/gradleHelpers.ts @@ -1,5 +1,6 @@ import { brownfieldGradlePluginDependency } from './constants'; import { Logger } from '../../logging'; +import { formatMissingDimensionStrategies } from './formatHelpers'; const LOCAL_GRADLE_PLUGIN_INCLUDE_BUILD = 'includeBuild("../node_modules/@callstack/react-native-brownfield/gradle-plugin/brownfield")'; @@ -56,6 +57,46 @@ export function modifyRootBuildGradle( return modifiedContents; } +/** + * Modifies the app build.gradle to add missingDimensionStrategy arguments + * @param contents The original app build.gradle content + * @param missingDimensionStrategies The arguments for missingDimensionStrategy + * @returns The modified build.gradle content + */ +export function modifyAppBuildGradleMissingDimensionStrategy( + contents: string, + missingDimensionStrategies: string[] +): string { + if (missingDimensionStrategies.length < 2) { + return contents; + } + + Logger.logDebug('Modifying app build.gradle to add missingDimensionStrategy'); + + const defaultConfigRegex = /^([ \t]*defaultConfig\s*\{)/m; + const match = contents.match(defaultConfigRegex); + + if (!match) { + throw new Error('Could not locate defaultConfig block in app build.gradle'); + } + + const baseIndent = match[1].match(/^[ \t]*/)?.[0] ?? ''; + const indentUnit = baseIndent.includes('\t') ? '\t' : ' '; + const args = formatMissingDimensionStrategies(missingDimensionStrategies); + const missingStrategyLine = `missingDimensionStrategy(${args})`; + + if (contents.includes(missingStrategyLine)) { + Logger.logDebug( + 'missingDimensionStrategy already present with same arguments, skipping' + ); + return contents; + } + + const insertion = `\n${baseIndent}${indentUnit}${missingStrategyLine}`; + + return contents.replace(defaultConfigRegex, `$1${insertion}`); +} + function addLocalGradlePluginIncludeBuild(contents: string): string { if (contents.includes('gradle-plugin/brownfield')) { Logger.logDebug( diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/withAndroidModuleFiles.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/withAndroidModuleFiles.ts index 663ee4c0..a3b2a80a 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/withAndroidModuleFiles.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/withAndroidModuleFiles.ts @@ -20,6 +20,18 @@ import { readExpoUpdatesStringResources, } from './utils/expo-updates'; import { getHermesArtifact } from './utils/hermes'; +import { formatMissingDimensionStrategies } from './utils/formatHelpers'; + +function renderMissingDimensionStrategyBlock( + missingDimensionStrategies: string[] +): string { + if (missingDimensionStrategies.length < 2) { + return ''; + } + + const args = formatMissingDimensionStrategies(missingDimensionStrategies); + return ` missingDimensionStrategy(${args})`; +} export function resolveCompileSdkVersionExpression( config: ResolvedBrownfieldPluginConfigWithAndroid @@ -81,6 +93,12 @@ export function createAndroidModule({ Logger.logDebug( `Resolved Hermes artifact: ${hermesArtifact.groupId}:${hermesArtifact.artifactId}:${hermesArtifact.version}` ); + const missingDimensionStrategiesList = formatMissingDimensionStrategies( + android.missingDimensionStrategies + ); + const missingDimensionStrategyBlock = renderMissingDimensionStrategyBlock( + android.missingDimensionStrategies + ); // generate module files const files: RenderedTemplateFile[] = [ @@ -96,6 +114,8 @@ export function createAndroidModule({ '{{ARTIFACT_VERSION}}': android.version, '{{RN_VERSION}}': rnVersion, '{{HERMES_ARTIFACT}}': `${hermesArtifact.groupId}:${hermesArtifact.artifactId}:${hermesArtifact.version}`, + '{{MISSING_DIMENSION_STRATEGIES}}': missingDimensionStrategiesList, + '{{MISSING_DIMENSION_STRATEGY_BLOCK}}': missingDimensionStrategyBlock, }), }, { diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts index 68aa8a6e..00cba4d1 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts @@ -1,4 +1,5 @@ import { + withAppBuildGradle, withProjectBuildGradle, withSettingsGradle, withFinalizedMod, @@ -6,6 +7,7 @@ import { } from '@expo/config-plugins'; import { + modifyAppBuildGradleMissingDimensionStrategy, modifyRootBuildGradle, modifySettingsGradle, } from './utils/gradleHelpers'; @@ -39,7 +41,18 @@ export const withBrownfieldAndroid: ConfigPlugin< return gradleConfig; }); - // Step 2: modify settings.gradle to include the new module + // Step 2: modify app build.gradle to add missingDimensionStrategy + config = withAppBuildGradle(config, (gradleConfig) => { + gradleConfig.modResults.contents = + modifyAppBuildGradleMissingDimensionStrategy( + gradleConfig.modResults.contents, + androidConfig.missingDimensionStrategies + ); + + return gradleConfig; + }); + + // Step 3: modify settings.gradle to include the new module config = withSettingsGradle(config, (settingsConfig) => { settingsConfig.modResults.contents = modifySettingsGradle( settingsConfig.modResults.contents, @@ -50,10 +63,10 @@ export const withBrownfieldAndroid: ConfigPlugin< return settingsConfig; }); - // Step 3: create the Android module files using dangerous mod + // Step 4: create the Android module files using dangerous mod config = withAndroidModuleFiles(config, props); - // Step 4: sync module metadata after Expo writes all Android files + // Step 5: sync module metadata after Expo writes all Android files config = withFinalizedMod(config, [ 'android', async (finalizedConfig) => { 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..f4ec0a2f 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 @@ -43,6 +43,10 @@ publishing { } } +reactBrownfield { + missingDimensionStrategies = listOf({{MISSING_DIMENSION_STRATEGIES}}) +} + react { autolinkLibrariesWithApp() } @@ -69,6 +73,8 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") + +{{MISSING_DIMENSION_STRATEGY_BLOCK}} } buildFeatures { diff --git a/packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts b/packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts index a1f550dc..dd81df70 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts @@ -56,6 +56,12 @@ export interface BrownfieldPluginAndroidConfig { * @default false */ useLocalGradlePlugin?: boolean; + + /** + * Missing dimension strategies as dimension + flavor(s). + * @default [] + */ + missingDimensionStrategies?: string[]; } /** diff --git a/scripts/__tests__/generate-brownfield-gradle-plugin-release-notes.test.ts b/scripts/__tests__/generate-brownfield-gradle-plugin-release-notes.test.ts index 921d51ad..e539a12e 100644 --- a/scripts/__tests__/generate-brownfield-gradle-plugin-release-notes.test.ts +++ b/scripts/__tests__/generate-brownfield-gradle-plugin-release-notes.test.ts @@ -25,14 +25,14 @@ test('supports prerelease versions when finding the previous plugin tag', () => const previousTag = findPreviousPluginTag( [ 'brownfield-gradle-plugin/v2.0.0-alpha01', - 'brownfield-gradle-plugin/v2.0.0-alpha06', + 'brownfield-gradle-plugin/v2.0.0-alpha07', 'brownfield-gradle-plugin/v2.0.0-beta01', 'brownfield-gradle-plugin/v2.0.0', ], '2.0.0-beta01' ); - assert.equal(previousTag, 'brownfield-gradle-plugin/v2.0.0-alpha06'); + assert.equal(previousTag, 'brownfield-gradle-plugin/v2.0.0-alpha07'); }); test('treats a stable release as newer than its prereleases', () => {