@callstack/react-native-brownfield 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/gradle-plugin/brownfield/build.gradle.kts +4 -0
  3. package/gradle-plugin/brownfield/gradle/libs.versions.toml +2 -0
  4. package/gradle-plugin/brownfield/gradle.properties +1 -1
  5. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +24 -7
  6. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +4 -37
  7. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt +206 -104
  8. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ResourceTaskProcessor.kt +201 -4
  9. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantHelper.kt +7 -0
  10. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt +4 -0
  11. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/GradleExclude.kt +31 -0
  12. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +2 -0
  13. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjector.kt +28 -3
  14. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt +41 -4
  15. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +14 -1
  16. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/TaskOutputs.kt +40 -0
  17. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +14 -6
  18. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/HardExcludePredicateTest.kt +28 -0
  19. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ConfigureProcessJniTaskTest.kt +60 -0
  20. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/CopyExplodedResTaskTest.kt +368 -0
  21. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/JniLibsPlanTest.kt +252 -0
  22. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt +73 -0
  23. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MetroResDirWiringTest.kt +39 -0
  24. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ProcessAndCopyJniLibsTaskTest.kt +136 -0
  25. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/GradleExcludesTest.kt +81 -0
  26. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +56 -0
  27. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibraryNamespaceFallbackTest.kt +137 -0
  28. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +5 -0
  29. package/ios/Expo/ExpoHostRuntime.swift +3 -1
  30. package/ios/Expo/ExpoUpdatesStartup.swift +2 -1
  31. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  32. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  33. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  34. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +2 -2
  35. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  36. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  37. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  38. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  39. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  40. package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  41. package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -1
  42. package/lib/commonjs/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  43. package/lib/commonjs/index.js +1 -1
  44. package/lib/commonjs/index.js.map +1 -1
  45. package/lib/commonjs/scaffold/index.js +1 -1
  46. package/lib/commonjs/scaffold/index.js.map +1 -1
  47. package/lib/commonjs/scaffold/projectFiles.js +1 -1
  48. package/lib/commonjs/scaffold/projectFiles.js.map +1 -1
  49. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  50. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  51. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  52. package/lib/module/expo-config-plugin/ios/podfileHelpers.js +2 -2
  53. package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  54. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  55. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  56. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  57. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  58. package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  59. package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -1
  60. package/lib/module/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  61. package/lib/module/index.js +1 -1
  62. package/lib/module/index.js.map +1 -1
  63. package/lib/module/scaffold/index.js +1 -1
  64. package/lib/module/scaffold/index.js.map +1 -1
  65. package/lib/module/scaffold/projectFiles.js +1 -1
  66. package/lib/module/scaffold/projectFiles.js.map +1 -1
  67. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  68. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
  71. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
  73. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
  75. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +2 -2
  79. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -1
  80. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  81. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  82. package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  83. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
  84. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  85. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
  86. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  87. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
  88. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  89. package/lib/typescript/module/src/index.d.ts.map +1 -1
  90. package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
  91. package/lib/typescript/module/src/scaffold/projectFiles.d.ts +2 -2
  92. package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -1
  93. package/package.json +2 -2
  94. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  95. package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +42 -0
  96. package/src/expo-config-plugin/ios/podfileHelpers.ts +47 -13
  97. package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +11 -1
  98. package/src/expo-config-plugin/ios/xcodeHelpers.ts +67 -9
  99. package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  100. package/src/expo-config-plugin/template/android/build.gradle.kts +1 -1
  101. package/src/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  102. package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +7 -0
  103. package/src/index.ts +4 -2
  104. package/src/scaffold/index.ts +61 -4
  105. package/src/scaffold/projectFiles.ts +5 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @callstack/react-native-brownfield
2
2
 
3
+ ## 6.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#520](https://github.com/callstack/react-native-brownfield/pull/520) [`b87cdff`](https://github.com/callstack/react-native-brownfield/commit/b87cdff0de1cb147289a7497973b6f191dc74134) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - fix: pick the right `.so` files for the Android AAR. With `useStrippedSoFiles = false`, the AAR now has `libappmodules.so` and `libreact_codegen_*.so`. Before this fix, those libs were missing and host apps crashed. The AAR also stops embedding the `.so` files of Maven dependencies. The host already gets those through the POM, so its build no longer fails with `2 files found with path 'lib/<abi>/<name>.so'`. If your app module builds or ships a `.so` of its own, list it in `reactBrownfield.dynamicLibs`, which is back. The build lists the `.so` files it left out in `<brownfield module>/build/reports/brownfield/<variant>/skipped-so.txt`. After upgrading, check that file: it lists the `.so` files that used to be in the AAR and now aren't.
8
+
9
+ - [#522](https://github.com/callstack/react-native-brownfield/pull/522) [`20776bb`](https://github.com/callstack/react-native-brownfield/commit/20776bb714c2de1e962515aa7c88235b60c7dabf) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - feat: the published POM and `module.json` now follow the brownfield module's Gradle exclude rules (`configurations.all { exclude(...) }`). Before, the plugin re-added an excluded dependency when it injected the embedded libraries' dependencies, so the host app still resolved it. The excludes are also written onto every dependency the plugin injects, so the excluded library can't arrive through one of them. On Expo, list them in `android.excludeDependencies` (as `group:module` or `group`), which the config plugin writes into the generated module.
10
+
11
+ ### Patch Changes
12
+
13
+ - [#512](https://github.com/callstack/react-native-brownfield/pull/512) [`520811c`](https://github.com/callstack/react-native-brownfield/commit/520811c47a122cacd1ec15f1b064ad08d699eebb) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - fix: package classes from libraries' local jars (`libs/*.jar`) into the Android AAR; previously host apps crashed with NoClassDefFoundError. If your app also has those classes, because it added the jar itself as a workaround or depends on a library that the jar bundles, the build now fails with duplicate classes: remove the app's copy
14
+
15
+ - [#513](https://github.com/callstack/react-native-brownfield/pull/513) [`f3dbbd1`](https://github.com/callstack/react-native-brownfield/commit/f3dbbd1c2579f905a358faac0ad1677b3740cebc) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - fix: keep the resources of every library embedded in the Android AAR; previously only one library's `res/values` survived and host apps failed at resource linking. As in a regular app, the module's own `res` and `resValue`s win over a library's, and when two libraries define the same resource the first one wins (previously the last one won for files such as drawables)
16
+
17
+ - [#521](https://github.com/callstack/react-native-brownfield/pull/521) [`58aedad`](https://github.com/callstack/react-native-brownfield/commit/58aedad94f38098424937533f2ce0b0c3df62067) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - fix: the AAR build no longer fails with duplicate resources when an embedded library ships the same drawables Metro generates for the app's bundle. Metro's copy wins.
18
+
19
+ - [#503](https://github.com/callstack/react-native-brownfield/pull/503) [`6a258da`](https://github.com/callstack/react-native-brownfield/commit/6a258da42f2bd2cbb112f56948108323aeba74d9) Thanks [@RaphaelFakhri](https://github.com/RaphaelFakhri)! - `ReactNativeBrownfield.onMessage` no longer calls the callback a second time, with the raw message text, when the callback throws. The error now propagates instead of being swallowed.
20
+ - Updated dependencies [[`20776bb`](https://github.com/callstack/react-native-brownfield/commit/20776bb714c2de1e962515aa7c88235b60c7dabf)]:
21
+ - @callstack/brownfield-cli@6.1.0
22
+
23
+ ## 6.0.1
24
+
25
+ ### Patch Changes
26
+
27
+ - [#487](https://github.com/callstack/react-native-brownfield/pull/487) [`79817d1`](https://github.com/callstack/react-native-brownfield/commit/79817d1a21b0cb28ae63d41e935c57af8e7db39f) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - Support Expo modules in React Native CLI apps. `brownfield package:*` no longer runs `expo prebuild` on projects with committed `android/` and `ios/` directories, which used to delete the brownfield library module. Prebuild now runs only when the native directory is missing or gitignored. If you commit the generated directories of an Expo app, run `npx expo prebuild` yourself after changing `app.json` or config plugins. `create-react-native-brownfield` now detects Expo and sets up the iOS framework to use the Expo runtime. On a re-run after adding Expo, it also raises an existing framework target to iOS 16.4.
28
+
29
+ `--use-prebuilt-expo` now also turns Expo's precompiled modules on or off for `pod install`, and turns on Swift explicit modules so packaging works in projects that still carry the old `SWIFT_ENABLE_EXPLICIT_MODULES = NO` workaround. It defaults to building Expo from source when the app's React Native version doesn't match the one Expo's precompiled modules were built for.
30
+
31
+ `package:ios` now copies the expo-image dependency XCFrameworks (SDWebImage and libavif) as real files instead of symlinks into `node_modules`, so the package works when moved to another machine.
32
+
33
+ Re-running `create-react-native-brownfield` after `pod install` no longer adds a second Sources build phase to the framework target, which made the next `pod install` fail.
34
+
35
+ The `package:ios` script that `create-react-native-brownfield` adds now passes `--add-spm-package`, so the iOS app adds one local Swift package instead of every XCFramework. Existing scripts aren't changed.
36
+
37
+ - [#508](https://github.com/callstack/react-native-brownfield/pull/508) [`7e63e20`](https://github.com/callstack/react-native-brownfield/commit/7e63e204c449ab36ce5b109ff3eb5db086260089) Thanks [@malopezr7](https://github.com/malopezr7)! - Fix an iOS crash with expo-updates 57 when updates are enabled. The Expo runtime no longer asks the Updates controller for its launch asset before the controller has started.
38
+
39
+ - [#495](https://github.com/callstack/react-native-brownfield/pull/495) [`3b38aeb`](https://github.com/callstack/react-native-brownfield/commit/3b38aebb63810a8d1cc686bd70cccdeb279bf119) Thanks [@hurali97](https://github.com/hurali97)! - fix: resolve the namespace from AndroidManifest.xml `package` for libraries without a `namespace` in build.gradle
40
+
41
+ - [#500](https://github.com/callstack/react-native-brownfield/pull/500) [`0f62490`](https://github.com/callstack/react-native-brownfield/commit/0f62490e7602d1aad56ee5d9eba2fd667dc0171a) Thanks [@hurali97](https://github.com/hurali97)! - fix(android): stop the scaffolded `ReactNativeHostManager` from loading React Native twice
42
+
43
+ The `ReactNativeHostManager.kt` generated by `npm create @callstack/react-native-brownfield` for React Native CLI projects called `loadReactNative(application)` and then `ReactNativeBrownfield.initialize(...)`. Both run `DefaultNewArchitectureEntryPoint.load()`, which overrides the React Native feature flags, so the app crashed on startup with `Feature flags cannot be overridden more than once`.
44
+
45
+ The generated host manager now:
46
+ - no longer calls `loadReactNative` (`ReactNativeBrownfield.initialize` already loads the native libraries and feature flags exactly once);
47
+ - passes the `ReactHost` as a factory to the non-deprecated `initialize` overload, so it is built after `SoLoader.init` instead of crashing with `SoLoader.init() not yet called`;
48
+ - keeps the one behavior `loadReactNative` provided on the side by flagging edge-to-edge when `IS_EDGE_TO_EDGE_ENABLED` is set.
49
+
50
+ Fixes [#492](https://github.com/callstack/react-native-brownfield/issues/492).
51
+
52
+ - Updated dependencies [[`79817d1`](https://github.com/callstack/react-native-brownfield/commit/79817d1a21b0cb28ae63d41e935c57af8e7db39f), [`69fe3e3`](https://github.com/callstack/react-native-brownfield/commit/69fe3e3f05a49036cae1f3564a47ef3eed186155)]:
53
+ - @callstack/brownfield-cli@6.0.1
54
+
3
55
  ## 6.0.0
4
56
 
5
57
  ### Major Changes
@@ -113,10 +113,14 @@ dependencies {
113
113
  // minimum Gradle version) for every app that applies this plugin.
114
114
  compileOnly(libs.agp)
115
115
  compileOnly(libs.common)
116
+ compileOnly(libs.sdk.common)
117
+ compileOnly(libs.layoutlib.api)
116
118
  implementation(libs.asm.commons)
117
119
  implementation(libs.versioncompare)
118
120
  testImplementation(libs.agp)
119
121
  testImplementation(libs.common)
122
+ testImplementation(libs.sdk.common)
123
+ testImplementation(libs.layoutlib.api)
120
124
  testImplementation(libs.junit.jupiter)
121
125
  testImplementation(gradleTestKit())
122
126
  testImplementation(kotlin("test"))
@@ -19,6 +19,8 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
19
19
  [libraries]
20
20
  agp = { module = "com.android.tools.build:gradle", name = "agp", version.ref = "agp" }
21
21
  common = { module = "com.android.tools:common", version.ref = "common" }
22
+ sdk-common = { module = "com.android.tools:sdk-common", version.ref = "common" }
23
+ layoutlib-api = { module = "com.android.tools.layoutlib:layoutlib-api", version.ref = "common" }
22
24
  asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm-commons" }
23
25
  versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "versioncompare" }
24
26
  junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
@@ -1,6 +1,6 @@
1
1
  PROJECT_ID=com.callstack.react.brownfield
2
2
  ARTIFACT_ID=brownfield-gradle-plugin
3
- VERSION=2.0.0-alpha10
3
+ VERSION=2.0.0-alpha14
4
4
  GROUP=com.callstack.react
5
5
  IMPLEMENTATION_CLASS=com.callstack.react.brownfield.plugin.RNBrownfieldPlugin
6
6
 
@@ -19,11 +19,13 @@ import com.callstack.react.brownfield.processors.VariantTaskProvider
19
19
  import com.callstack.react.brownfield.shared.BaseProject
20
20
  import com.callstack.react.brownfield.shared.Constants
21
21
  import com.callstack.react.brownfield.shared.Constants.PROJECT_ID
22
+ import com.callstack.react.brownfield.shared.GradleExclude
22
23
  import com.callstack.react.brownfield.shared.Logging
23
24
  import com.callstack.react.brownfield.shared.PublishingMetadataInjector
24
25
  import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
25
26
  import com.callstack.react.brownfield.shared.VersionMediatingDependencySet
26
27
  import com.callstack.react.brownfield.shared.dropHardExcludedDependencies
28
+ import com.callstack.react.brownfield.shared.gradleExcludes
27
29
  import com.callstack.react.brownfield.utils.AndroidArchiveLibrary
28
30
  import com.callstack.react.brownfield.utils.DirectoryManager
29
31
  import com.callstack.react.brownfield.utils.Extension
@@ -33,6 +35,24 @@ import org.gradle.api.Plugin
33
35
  import org.gradle.api.Project
34
36
  import org.gradle.api.ProjectConfigurationException
35
37
 
38
+ /**
39
+ * Coordinates that must never be published: embedded in the AAR (not resolvable from Maven), or
40
+ * excluded by the brownfield module's own Gradle rules.
41
+ */
42
+ internal fun hardExcludePredicate(
43
+ project: Project,
44
+ expoPublishingHelper: ExpoPublishingHelper?,
45
+ artifacts: List<UnresolvedArtifactInfo>,
46
+ expoProjects: List<ExpoGradleProjectProjection>,
47
+ excludes: List<GradleExclude>,
48
+ ): (String, String) -> Boolean =
49
+ { groupId, artifactId ->
50
+ (expoPublishingHelper?.shouldExcludeDependency(groupId, artifactId) ?: (groupId == project.rootProject.name)) ||
51
+ artifacts.any { it.moduleGroup == groupId && it.moduleName == artifactId } ||
52
+ expoProjects.any { it.publication?.groupId == groupId && it.publication?.artifactId == artifactId } ||
53
+ excludes.any { it.matches(groupId, artifactId) }
54
+ }
55
+
36
56
  class RNBrownfieldPlugin : Plugin<Project> {
37
57
  private lateinit var extension: Extension
38
58
  private lateinit var project: Project
@@ -140,11 +160,8 @@ class RNBrownfieldPlugin : Plugin<Project> {
140
160
  // Coordinates that must never be published: they're embedded in the AAR, not
141
161
  // resolvable from Maven. Distinct from "superseded" coordinates, which ARE re-injected
142
162
  // (replacing a stale entry) and so must not be dropped here.
143
- val hardExcludePredicate: (String, String) -> Boolean = { groupId, artifactId ->
144
- (expoPublishingHelper?.shouldExcludeDependency(groupId, artifactId) ?: (groupId == project.rootProject.name)) ||
145
- artifacts.any { it.moduleGroup == groupId && it.moduleName == artifactId } ||
146
- expoProjects.any { it.publication?.groupId == groupId && it.publication?.artifactId == artifactId }
147
- }
163
+ val excludes = gradleExcludes(project)
164
+ val hardExcludePredicate = hardExcludePredicate(project, expoPublishingHelper, artifacts, expoProjects, excludes)
148
165
  dropHardExcludedDependencies(transitiveDeps, hardExcludePredicate)
149
166
 
150
167
  Logging.log(
@@ -155,7 +172,7 @@ class RNBrownfieldPlugin : Plugin<Project> {
155
172
  hardExcludePredicate(groupId, artifactId) || rncSupersededCoordinates.contains(groupId to artifactId)
156
173
  }
157
174
 
158
- injector.configure(transitiveDeps, removalPredicate)
175
+ injector.configure(transitiveDeps, removalPredicate, excludes)
159
176
  Logging.log("PublishingMetadataInjector ran: injected merged transitive dependencies into POM and Gradle Module Metadata")
160
177
 
161
178
  // Informational only — throwing would recreate the upgrade break that the namespaced
@@ -277,7 +294,7 @@ class RNBrownfieldPlugin : Plugin<Project> {
277
294
 
278
295
  /** ===== jniLibsProcessor ===== */
279
296
  val jniLibsProcessor = JNILibsProcessor(project)
280
- jniLibsProcessor.processJniLibs(aarLibraries, variant)
297
+ jniLibsProcessor.processJniLibs(aarLibraries, variant, explodeTask)
281
298
 
282
299
  /** ===== proguardProcessor ===== */
283
300
  val proguardProcessor = ProguardProcessor(project)
@@ -8,8 +8,7 @@ import com.callstack.react.brownfield.exceptions.NameSpaceNotFound
8
8
  import com.callstack.react.brownfield.processors.VariantHelper
9
9
  import com.callstack.react.brownfield.utils.Extension
10
10
  import com.callstack.react.brownfield.utils.Utils
11
- import com.callstack.react.brownfield.utils.capitalized
12
- import org.gradle.api.GradleException
11
+ import com.callstack.react.brownfield.utils.directoryGetter
13
12
  import org.gradle.api.Project
14
13
  import org.gradle.api.Task
15
14
  import org.gradle.api.file.DirectoryProperty
@@ -57,16 +56,9 @@ object RNSourceSets {
57
56
  // 2. Use the onVariants block to wire RN bundle outputs via the Variant Sources API
58
57
  componentsExtension.onVariants { variant ->
59
58
  val variantName = variant.name
60
- val bundledAssetsVariantName =
61
- VariantHelper.getBundledAssetsVariantName(
62
- variantName = variantName,
63
- buildTypeName = variant.buildType,
64
- isDebuggable = variant.debuggable,
65
- )
66
- val capitalizedBundledAssetsVariantName = bundledAssetsVariantName.capitalized()
67
59
  val appProject = getAppProject()
68
60
 
69
- val jsBundleTaskName = "createBundle${capitalizedBundledAssetsVariantName}JsAndAssets"
61
+ val jsBundleTaskName = VariantHelper.getMetroBundleTaskName(variant)
70
62
 
71
63
  /**
72
64
  * Wire the bundle task's own `jsBundleDir`/`resourcesDir` outputs as *generated*
@@ -89,12 +81,7 @@ object RNSourceSets {
89
81
 
90
82
  /**
91
83
  * Wires `taskName`'s `getterName` output (expected to be a `DirectoryProperty`) as a
92
- * generated source directory for `sources`.
93
- *
94
- * Resolving the task by name (rather than importing its class) keeps this plugin from
95
- * depending on RNGP's/expo-updates' internal task types. If the task, or its expected output
96
- * property, isn't there, this fails the build immediately with a clear message instead of
97
- * silently producing an empty/stale source directory.
84
+ * generated source directory for `sources`. See [directoryGetter] for the checks.
98
85
  */
99
86
  private fun wireGeneratedSourceDirectory(
100
87
  appProject: Project,
@@ -104,27 +91,7 @@ object RNSourceSets {
104
91
  ) {
105
92
  if (sources == null) return
106
93
 
107
- val task =
108
- appProject.tasks.findByName(taskName)
109
- ?: throw GradleException(
110
- "brownfield: expected task '$taskName' was not found in project " +
111
- "'${appProject.path}'. Check that the plugin producing it is applied there.",
112
- )
113
-
114
- val getter =
115
- runCatching { task.javaClass.getMethod(getterName) }.getOrElse {
116
- throw GradleException(
117
- "brownfield: task '$taskName' (${task.javaClass.name}) does not expose " +
118
- "'$getterName()'. The plugin that registers it most likely changed its " +
119
- "API; the brownfield Gradle plugin needs updating to match.",
120
- it,
121
- )
122
- }
123
-
124
- check(DirectoryProperty::class.java.isAssignableFrom(getter.returnType)) {
125
- "brownfield: '$taskName.$getterName()' no longer returns a DirectoryProperty."
126
- }
127
-
94
+ val getter = directoryGetter(appProject, taskName, getterName)
128
95
  val taskProvider = appProject.tasks.named(taskName)
129
96
  sources.addGeneratedSourceDirectory(taskProvider) { t -> getter.invoke(t) as DirectoryProperty }
130
97
  }
@@ -1,28 +1,27 @@
1
1
  package com.callstack.react.brownfield.processors
2
2
 
3
3
  import com.android.build.api.variant.LibraryVariant
4
- import com.callstack.react.brownfield.shared.Logging
5
4
  import com.callstack.react.brownfield.utils.AndroidArchiveLibrary
6
5
  import com.callstack.react.brownfield.utils.Extension
7
6
  import com.callstack.react.brownfield.utils.capitalized
8
7
  import org.gradle.api.DefaultTask
9
8
  import org.gradle.api.Project
9
+ import org.gradle.api.Task
10
10
  import org.gradle.api.file.ConfigurableFileCollection
11
11
  import org.gradle.api.file.DirectoryProperty
12
+ import org.gradle.api.file.RegularFileProperty
12
13
  import org.gradle.api.provider.ListProperty
13
- import org.gradle.api.provider.Property
14
14
  import org.gradle.api.tasks.CacheableTask
15
15
  import org.gradle.api.tasks.Input
16
- import org.gradle.api.tasks.InputDirectory
17
16
  import org.gradle.api.tasks.InputFiles
18
17
  import org.gradle.api.tasks.Optional
19
18
  import org.gradle.api.tasks.OutputDirectory
19
+ import org.gradle.api.tasks.OutputFile
20
20
  import org.gradle.api.tasks.PathSensitive
21
21
  import org.gradle.api.tasks.PathSensitivity
22
22
  import org.gradle.api.tasks.TaskAction
23
+ import org.gradle.api.tasks.TaskProvider
23
24
  import java.io.File
24
- import kotlin.collections.component1
25
- import kotlin.collections.component2
26
25
 
27
26
  /**
28
27
  * Below contains a list of dynamic libs which we should not embed
@@ -37,126 +36,246 @@ val IGNORE_EMBEDDED_LIBS =
37
36
  "libreactnative.so",
38
37
  )
39
38
 
39
+ private val ABIS = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
40
+
41
+ private const val APP_MODULES_LIB = "libappmodules.so"
42
+
43
+ internal enum class SkipReason { IGNORED, NOT_EMBEDDED }
44
+
45
+ /**
46
+ * @property copies source file for each `<abi>/<name>` that goes into the AAR
47
+ * @property unmatchedDynamicLibs dynamicLibs entries that match no file in the app module's output
48
+ * @property skipped every lib name that no ABI copies, with why
49
+ * @property ignoredDynamicLibs dynamicLibs entries dropped because an ignore entry matches them
50
+ * @property excludedAbis ABIs the embedded AARs ship but the app module did not build
51
+ */
52
+ internal data class JniLibsPlan(
53
+ val copies: Map<String, File>,
54
+ val unmatchedDynamicLibs: Set<String>,
55
+ val skipped: Map<String, SkipReason>,
56
+ val ignoredDynamicLibs: Set<String> = emptySet(),
57
+ val excludedAbis: Set<String> = emptySet(),
58
+ )
59
+
60
+ private fun isAppOwned(name: String) = name == APP_MODULES_LIB || (name.startsWith("libreact_codegen_") && name.endsWith(".so"))
61
+
62
+ private fun isIgnored(
63
+ name: String,
64
+ ignoredLibs: List<String>,
65
+ ) = ignoredLibs.any { entry -> name == entry || name.contains(entry) }
66
+
67
+ private fun libNames(dir: File?): List<String> = dir?.listFiles()?.filter { it.isFile }?.map { it.name }.orEmpty()
68
+
69
+ /**
70
+ * Picks the .so files for the AAR: the app module's own libs, the libs the embedded libraries
71
+ * ship, and [dynamicLibs], minus [ignoredLibs]. Libs that only come from Maven dependencies stay
72
+ * out, because the host app gets them through the published POM.
73
+ */
74
+ internal fun planJniLibs(
75
+ aarJniDirs: List<File>,
76
+ appNativeLibsDir: File?,
77
+ ignoredLibs: List<String>,
78
+ dynamicLibs: List<String>,
79
+ ): JniLibsPlan {
80
+ val copies = linkedMapOf<String, File>()
81
+ val matchedDynamicLibs = mutableSetOf<String>()
82
+ val candidates = mutableMapOf<String, SkipReason>()
83
+ val ignoredDynamicLibs = mutableSetOf<String>()
84
+
85
+ // An ABI folder without libappmodules.so crashes on devices that pick it, so follow the app
86
+ // module's ABIs (it may use abiFilters) and only fall back to all of them when it built none.
87
+ val appAbis = ABIS.filter { abi -> appNativeLibsDir?.let { File(it, abi).isDirectory } == true }
88
+ val abis = appAbis.ifEmpty { ABIS }
89
+ val excludedAbis = ABIS.filter { abi -> abi !in abis && aarJniDirs.any { File(it, abi).isDirectory } }.toSet()
90
+
91
+ for (abi in abis) {
92
+ val appAbiDir = appNativeLibsDir?.let { File(it, abi) }
93
+ val appNames = libNames(appAbiDir).toSet()
94
+
95
+ val embedded = linkedMapOf<String, File>()
96
+ aarJniDirs.forEach { jniDir ->
97
+ val abiDir = File(jniDir, abi)
98
+ libNames(abiDir).forEach { name -> embedded.putIfAbsent(name, File(abiDir, name)) }
99
+ }
100
+ val extra = dynamicLibs.filter { it in appNames }
101
+ matchedDynamicLibs += extra
102
+ ignoredDynamicLibs += extra.filter { isIgnored(it, ignoredLibs) }
103
+
104
+ val wanted = appNames.filter(::isAppOwned) + (embedded.keys + extra).filterNot { isIgnored(it, ignoredLibs) }
105
+ wanted.forEach { name ->
106
+ copies["$abi/$name"] = appAbiDir?.let { File(it, name) }?.takeIf { it.isFile } ?: embedded.getValue(name)
107
+ }
108
+
109
+ (appNames + embedded.keys).filterNot { it in wanted }.forEach { name ->
110
+ val reason = if (isIgnored(name, ignoredLibs)) SkipReason.IGNORED else SkipReason.NOT_EMBEDDED
111
+ // IGNORED wins: once a name is ignored in one ABI it is ignored everywhere.
112
+ if (candidates[name] != SkipReason.IGNORED) candidates[name] = reason
113
+ }
114
+ }
115
+
116
+ val copiedNames = copies.keys.map { it.substringAfter('/') }.toSet()
117
+ return JniLibsPlan(
118
+ copies = copies,
119
+ unmatchedDynamicLibs = dynamicLibs.toSet() - matchedDynamicLibs,
120
+ skipped = candidates.filterKeys { it !in copiedNames }.toSortedMap(),
121
+ ignoredDynamicLibs = ignoredDynamicLibs,
122
+ excludedAbis = excludedAbis,
123
+ )
124
+ }
125
+
126
+ internal fun planWarnings(plan: JniLibsPlan): List<String> =
127
+ plan.unmatchedDynamicLibs.map { entry ->
128
+ "Brownfield: dynamicLibs entry '$entry' matches no .so file in the app module's native output."
129
+ } +
130
+ plan.ignoredDynamicLibs.map { name ->
131
+ "Brownfield: dynamicLibs entry '$name' is not in the AAR because an ignore entry matches it. " +
132
+ "Remove that entry from ignoreEmbeddedLibs to embed it."
133
+ }
134
+
135
+ /**
136
+ * One line naming the libs no embedded library ships, or null when there are none. Ignored libs
137
+ * are left out: they are skipped on purpose and would only add noise on every build.
138
+ */
139
+ internal fun skippedSummary(
140
+ plan: JniLibsPlan,
141
+ reportFile: File?,
142
+ ): String? {
143
+ val names = plan.skipped.filterValues { it == SkipReason.NOT_EMBEDDED }.keys.sorted()
144
+ if (names.isEmpty()) return null
145
+ val details = reportFile?.let { " Details: ${it.absolutePath}" }.orEmpty()
146
+ return "Brownfield: left ${names.size} .so file(s) out of the AAR because no embedded library ships them: " +
147
+ "${names.joinToString(", ")}. If one comes from a Maven dependency, the host app gets it through the POM. " +
148
+ "If not (for example, your app module builds or ships it), add it to reactBrownfield.dynamicLibs.$details"
149
+ }
150
+
151
+ internal data class AppNativeLibsSource(val relativeDir: String, val producerTaskName: String)
152
+
153
+ internal fun appNativeLibsSource(
154
+ variantName: String,
155
+ useStrippedSoFiles: Boolean,
156
+ ): AppNativeLibsSource {
157
+ val capitalized = variantName.capitalized()
158
+ return if (useStrippedSoFiles) {
159
+ val task = "strip${capitalized}DebugSymbols"
160
+ AppNativeLibsSource("intermediates/stripped_native_libs/$variantName/$task/out/lib", task)
161
+ } else {
162
+ val task = "merge${capitalized}NativeLibs"
163
+ AppNativeLibsSource("intermediates/merged_native_libs/$variantName/$task/out/lib", task)
164
+ }
165
+ }
166
+
40
167
  @CacheableTask
41
168
  abstract class ProcessAndCopyJniLibsTask : DefaultTask() {
42
169
  @get:InputFiles
43
170
  @get:PathSensitive(PathSensitivity.RELATIVE)
44
171
  abstract val aarJniDirs: ConfigurableFileCollection
45
172
 
46
- @get:InputDirectory
173
+ // @InputFiles rather than @InputDirectory: a variant without native code has no such dir,
174
+ // and @InputDirectory would fail validation before the task can run.
175
+ @get:InputFiles
47
176
  @get:Optional
48
177
  @get:PathSensitive(PathSensitivity.RELATIVE)
49
- abstract val strippedLibsDir: DirectoryProperty
178
+ abstract val appNativeLibsDir: DirectoryProperty
50
179
 
51
180
  @get:Input
52
- abstract val useStrippedSoFiles: Property<Boolean>
181
+ abstract val extraIgnoredLibs: ListProperty<String>
53
182
 
54
183
  @get:Input
55
- abstract val extraIgnoredLibs: ListProperty<String>
184
+ abstract val dynamicLibs: ListProperty<String>
56
185
 
57
186
  @get:OutputDirectory
58
187
  abstract val outputDirectory: DirectoryProperty
59
188
 
189
+ // A declared output, so it is restored on cache hits when nothing is logged.
190
+ @get:OutputFile
191
+ abstract val skippedReportFile: RegularFileProperty
192
+
60
193
  @TaskAction
61
194
  fun execute() {
62
195
  val outDir = outputDirectory.get().asFile
63
- // Clear out old data to ensure clean, reproducible builds
64
196
  outDir.deleteRecursively()
65
197
  outDir.mkdirs()
66
198
 
67
- val existingJNILibs =
68
- listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
69
- .associateWith { mutableListOf<String>() }
70
- .toMutableMap()
71
-
72
- // 1. Scan and process duplicate libs from AAR paths
73
- aarJniDirs.forEach { jniDir ->
74
- if (jniDir.exists()) {
75
- processNestedLibs(jniDir.listFiles(), existingJNILibs)
76
-
77
- // If not using stripped files, copy the unstripped ones directly to our output
78
- if (!useStrippedSoFiles.get()) {
79
- jniDir.copyRecursively(outDir, overwrite = true)
80
- }
81
- }
199
+ val appDir = appNativeLibsDir.orNull?.asFile
200
+ if (appDir != null && !appDir.exists()) {
201
+ logger.warn(
202
+ "Brownfield: $appDir does not exist, so libappmodules.so and libreact_codegen_*.so " +
203
+ "were not found and are not in the AAR.",
204
+ )
82
205
  }
83
206
 
84
- // 2. If using stripped files, copy targeted files from the stripped directory
85
- if (useStrippedSoFiles.get() && strippedLibsDir.isPresent) {
86
- copyStrippedSoLibs(outDir, existingJNILibs)
87
- }
88
- }
207
+ val plan =
208
+ planJniLibs(
209
+ aarJniDirs.files.toList(),
210
+ appDir,
211
+ IGNORE_EMBEDDED_LIBS + extraIgnoredLibs.getOrElse(emptyList()),
212
+ dynamicLibs.getOrElse(emptyList()),
213
+ )
89
214
 
90
- private fun copyStrippedSoLibs(
91
- outDir: File,
92
- existingJNILibs: MutableMap<String, MutableList<String>>,
93
- ) {
94
- val fromDir = strippedLibsDir.get().asFile
95
- if (!fromDir.exists()) return
215
+ plan.copies.forEach { (path, source) -> source.copyTo(File(outDir, path), overwrite = true) }
96
216
 
97
- existingJNILibs.keys.forEach { arch ->
98
- copyArchStrippedSoLibs(fromDir, outDir, arch)
217
+ writeReport(plan)
218
+ val reportFile = skippedReportFile.get().asFile
219
+ skippedSummary(plan, reportFile)?.let { logger.lifecycle(it) }
220
+ plan.skipped.forEach { (name, reason) -> logger.info("Brownfield: skipped $name ($reason)") }
221
+ planWarnings(plan).forEach { logger.warn(it) }
222
+ if (plan.excludedAbis.isNotEmpty()) {
223
+ logger.info("Brownfield: left out ABIs the app module did not build: ${plan.excludedAbis.sorted().joinToString(", ")}")
99
224
  }
100
225
  }
101
226
 
102
- private fun copyArchStrippedSoLibs(
103
- fromDir: File,
104
- outDir: File,
105
- arch: String,
106
- ) {
107
- val sourceArchDir = File(fromDir, arch)
108
- if (!sourceArchDir.exists()) return
109
-
110
- val destArchDir = File(outDir, arch).apply { mkdirs() }
111
- sourceArchDir.listFiles()?.forEach { file ->
112
- copyIfAllowed(file, destArchDir)
113
- }
227
+ private fun writeReport(plan: JniLibsPlan) {
228
+ val reportFile = skippedReportFile.get().asFile
229
+ reportFile.parentFile.mkdirs()
230
+ val lines =
231
+ if (plan.skipped.isEmpty()) {
232
+ listOf("No .so files were left out.")
233
+ } else {
234
+ listOfNotNull(skippedSummary(plan, null)) + plan.skipped.map { (name, reason) -> "$name: $reason" }
235
+ }
236
+ reportFile.writeText(lines.joinToString("\n", postfix = "\n"))
114
237
  }
238
+ }
115
239
 
116
- private fun copyIfAllowed(
117
- file: File,
118
- destArchDir: File,
119
- ) {
120
- val name = file.name
121
- if (!matchesAllowedLib(name)) return
240
+ internal fun wireAarJniDirs(
241
+ task: ProcessAndCopyJniLibsTask,
242
+ jniDirs: List<File>,
243
+ explodeTask: TaskProvider<out Task>,
244
+ ) {
245
+ // The dirs are plain paths, so Gradle can't infer who fills them; without this the task can
246
+ // run before the AARs are unpacked and drop every embedded lib.
247
+ task.aarJniDirs.setFrom(jniDirs)
248
+ task.aarJniDirs.builtBy(explodeTask)
249
+ }
122
250
 
123
- try {
124
- file.copyTo(File(destArchDir, name), overwrite = true)
125
- } catch (e: java.io.IOException) {
126
- Logging.error("Failed to copy $name: ${e.message}", e.cause)
127
- }
128
- }
251
+ internal fun configureProcessJniTask(
252
+ task: ProcessAndCopyJniLibsTask,
253
+ project: Project,
254
+ appProject: Project,
255
+ projectExt: Extension,
256
+ variantName: String,
257
+ jniDirs: List<File>,
258
+ explodeTask: TaskProvider<out Task>,
259
+ ) {
260
+ val source = appNativeLibsSource(variantName, projectExt.useStrippedSoFiles)
129
261
 
130
- private fun matchesAllowedLib(name: String): Boolean {
131
- val isCodegenLib = name.startsWith("libreact_codegen_") && name.endsWith(".so")
132
- val ignoredLibs = IGNORE_EMBEDDED_LIBS + extraIgnoredLibs.getOrElse(emptyList())
133
- val isIgnoredLib = ignoredLibs.any { lib -> name == lib || name.contains(lib) }
134
- return name == "libappmodules.so" || isCodegenLib || !isIgnoredLib
135
- }
262
+ task.extraIgnoredLibs.set(projectExt.ignoreEmbeddedLibs)
263
+ task.dynamicLibs.set(projectExt.dynamicLibs)
264
+ wireAarJniDirs(task, jniDirs, explodeTask)
265
+ task.appNativeLibsDir.set(appProject.layout.buildDirectory.dir(source.relativeDir))
266
+ task.skippedReportFile.set(project.layout.buildDirectory.file("reports/brownfield/$variantName/skipped-so.txt"))
136
267
 
137
- private fun processNestedLibs(
138
- files: Array<File>?,
139
- existingJNILibs: MutableMap<String, MutableList<String>>,
140
- ) {
141
- files?.forEach { folder ->
142
- val libFiles = folder.listFiles() ?: return@forEach
143
- val libList = existingJNILibs[folder.name] ?: return@forEach
144
-
145
- libFiles.forEach { file ->
146
- if (file.name in libList) {
147
- file.delete()
148
- } else {
149
- libList.add(file.name)
150
- }
151
- }
152
- }
153
- }
268
+ task.dependsOn(
269
+ "${appProject.path}:${source.producerTaskName}",
270
+ "${project.path}:generateCodegenSchemaFromJavaScript",
271
+ )
154
272
  }
155
273
 
156
274
  class JNILibsProcessor(val project: Project) {
157
275
  fun processJniLibs(
158
276
  aarLibraries: Collection<AndroidArchiveLibrary>,
159
277
  variant: LibraryVariant,
278
+ explodeTask: TaskProvider<out Task>,
160
279
  ) {
161
280
  val variantName = variant.name
162
281
  val capitalizedVariantName = variantName.capitalized()
@@ -165,25 +284,8 @@ class JNILibsProcessor(val project: Project) {
165
284
 
166
285
  val processJniTask =
167
286
  project.tasks.register("processCustomJniLibsFor$capitalizedVariantName", ProcessAndCopyJniLibsTask::class.java) {
168
- it.useStrippedSoFiles.set(projectExt.useStrippedSoFiles)
169
- it.extraIgnoredLibs.set(projectExt.ignoreEmbeddedLibs)
170
-
171
- val jniDirs =
172
- aarLibraries.map { aarLib ->
173
- aarLib.getJniDir()
174
- }
175
-
176
- it.aarJniDirs.setFrom(jniDirs)
177
-
178
- val fromDirProvider =
179
- appProject.layout.buildDirectory.dir(
180
- "intermediates/stripped_native_libs/$variantName/strip${capitalizedVariantName}DebugSymbols/out/lib",
181
- )
182
- it.strippedLibsDir.set(fromDirProvider)
183
-
184
- val stripTaskPath = ":${appProject.name}:strip${capitalizedVariantName}DebugSymbols"
185
- val codegenTaskPath = ":${project.name}:generateCodegenSchemaFromJavaScript"
186
- it.dependsOn(stripTaskPath, codegenTaskPath)
287
+ val jniDirs = aarLibraries.map { aarLib -> aarLib.getJniDir() }
288
+ configureProcessJniTask(it, project, appProject, projectExt, variantName, jniDirs, explodeTask)
187
289
  }
188
290
 
189
291
  variant.sources.jniLibs?.addGeneratedSourceDirectory(