@callstack/react-native-brownfield 6.0.1 → 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 (61) hide show
  1. package/CHANGELOG.md +20 -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/Extension.kt +14 -1
  15. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/TaskOutputs.kt +40 -0
  16. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +14 -6
  17. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/HardExcludePredicateTest.kt +28 -0
  18. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ConfigureProcessJniTaskTest.kt +60 -0
  19. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/CopyExplodedResTaskTest.kt +368 -0
  20. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/JniLibsPlanTest.kt +252 -0
  21. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt +73 -0
  22. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MetroResDirWiringTest.kt +39 -0
  23. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ProcessAndCopyJniLibsTaskTest.kt +136 -0
  24. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/GradleExcludesTest.kt +81 -0
  25. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +56 -0
  26. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +5 -0
  27. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  28. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  29. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  30. package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -1
  31. package/lib/commonjs/index.js +1 -1
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/commonjs/scaffold/index.js +1 -1
  34. package/lib/commonjs/scaffold/index.js.map +1 -1
  35. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  36. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  37. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  38. package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -1
  39. package/lib/module/index.js +1 -1
  40. package/lib/module/index.js.map +1 -1
  41. package/lib/module/scaffold/index.js +1 -1
  42. package/lib/module/scaffold/index.js.map +1 -1
  43. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  44. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
  46. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
  49. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  50. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  51. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
  52. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  53. package/lib/typescript/module/src/index.d.ts.map +1 -1
  54. package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
  55. package/package.json +2 -2
  56. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  57. package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +42 -0
  58. package/src/expo-config-plugin/template/android/build.gradle.kts +1 -1
  59. package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +7 -0
  60. package/src/index.ts +4 -2
  61. package/src/scaffold/index.ts +1 -0
@@ -0,0 +1,368 @@
1
+ package com.callstack.react.brownfield.processors
2
+
3
+ import com.android.ide.common.rendering.api.ResourceNamespace
4
+ import com.android.ide.common.resources.ResourceSet
5
+ import com.android.utils.StdLogger
6
+ import org.gradle.api.Project
7
+ import org.gradle.testfixtures.ProjectBuilder
8
+ import org.junit.jupiter.api.Test
9
+ import org.junit.jupiter.api.io.TempDir
10
+ import org.w3c.dom.Element
11
+ import java.io.File
12
+ import javax.xml.parsers.DocumentBuilderFactory
13
+ import kotlin.test.assertEquals
14
+ import kotlin.test.assertTrue
15
+
16
+ /**
17
+ * Every exploded AAR keeps its values in `res/values/values.xml`, so libraries share that path.
18
+ * See #490 (values lost) and #125 (duplicate resources failing the build).
19
+ */
20
+ class CopyExplodedResTaskTest {
21
+ @TempDir
22
+ lateinit var tempDir: File
23
+
24
+ @Test
25
+ fun `keeps the values of every library`() {
26
+ val bootsplash =
27
+ libraryRes(
28
+ "react-native-bootsplash",
29
+ """<attr name="bootSplashBackground" format="color" />""",
30
+ )
31
+ val brownfield =
32
+ libraryRes(
33
+ "callstack_react-native-brownfield",
34
+ """<string name="app_name">ReactNativeBrownfield</string>""",
35
+ )
36
+
37
+ val out = runCopy(bootsplash, brownfield)
38
+
39
+ val names = valuesElements(out).map { it.key() }
40
+ assertTrue("attr/bootSplashBackground" in names, "bootsplash values were dropped: $names")
41
+ assertTrue("string/app_name" in names, "brownfield values were dropped: $names")
42
+ }
43
+
44
+ @Test
45
+ fun `a name defined by two libraries ends up once`() {
46
+ val video = libraryRes("react-native-video", """<color name="red">#FF0000</color>""")
47
+ val messaging =
48
+ libraryRes("react-native-firebase_messaging", """<color name="red">#E53935</color>""")
49
+
50
+ val out = runCopy(video, messaging)
51
+
52
+ val reds = valuesElements(out).filter { it.key() == "color/red" }
53
+ assertEquals(1, reds.size, "duplicate resources would fail the build: ${reds.map { it.textContent }}")
54
+ assertEquals("#FF0000", reds.single().textContent, "the first library should win, like in an app")
55
+ }
56
+
57
+ @Test
58
+ fun `merges the attrs of a styleable defined by two libraries`() {
59
+ val first =
60
+ libraryRes(
61
+ "lib-a",
62
+ """<declare-styleable name="Shared"><attr name="aColor" format="color" /></declare-styleable>""",
63
+ )
64
+ val second =
65
+ libraryRes(
66
+ "lib-b",
67
+ """<declare-styleable name="Shared"><attr name="bSize" format="dimension" /></declare-styleable>""",
68
+ )
69
+
70
+ val out = runCopy(first, second)
71
+
72
+ val styleables = valuesElements(out).filter { it.key() == "declare-styleable/Shared" }
73
+ assertEquals(1, styleables.size)
74
+ // aapt2 sorts a styleable's attrs itself, so their order doesn't matter.
75
+ val attrs = styleables.single().childElements().map { it.getAttribute("name") }
76
+ assertEquals(setOf("aColor", "bSize"), attrs.toSet())
77
+ }
78
+
79
+ @Test
80
+ fun `defines an attr once when another library declares it inside a styleable`() {
81
+ val first = libraryRes("lib-a", """<attr name="tint" format="color" />""")
82
+ // One library can't define it twice itself, its own build would fail.
83
+ val second = libraryRes("lib-b", """<declare-styleable name="Badge"><attr name="tint" format="color" /></declare-styleable>""")
84
+ val third = libraryRes("lib-c", """<declare-styleable name="Chip"><attr name="tint" format="color" /></declare-styleable>""")
85
+
86
+ val out = runCopy(first, second, third)
87
+
88
+ val elements = valuesElements(out)
89
+ val definitions =
90
+ elements.filter { it.key() == "attr/tint" } +
91
+ elements.filter { it.tagName == "declare-styleable" }.flatMap { it.childElements() }
92
+ .filter { it.getAttribute("name") == "tint" && it.hasAttribute("format") }
93
+ assertEquals(1, definitions.size, "attr/tint defined more than once would fail the build")
94
+ val styleables = elements.filter { it.tagName == "declare-styleable" }.map { it.getAttribute("name") }
95
+ assertEquals(listOf("Badge", "Chip"), styleables.sorted())
96
+ }
97
+
98
+ @Test
99
+ fun `keeps an attr definition that only the second of two same-name styleables has`() {
100
+ val first = libraryRes("lib-a", """<declare-styleable name="S"><attr name="x" /></declare-styleable>""")
101
+ val second =
102
+ libraryRes("lib-b", """<declare-styleable name="S"><attr name="x" format="color" /></declare-styleable>""")
103
+
104
+ val out = runCopy(first, second)
105
+
106
+ val elements = valuesElements(out)
107
+ val definitions =
108
+ (elements + elements.filter { it.tagName == "declare-styleable" }.flatMap { it.childElements() })
109
+ .filter { it.tagName == "attr" && it.getAttribute("name") == "x" && it.hasAttribute("format") }
110
+ assertEquals(listOf("color"), definitions.map { it.getAttribute("format") })
111
+ }
112
+
113
+ @Test
114
+ fun `keeps one file when two libraries ship the same resource with different extensions`() {
115
+ val first = libraryRes("lib-a", "")
116
+ val second = libraryRes("lib-b", "")
117
+ File(first, "drawable").mkdirs()
118
+ File(first, "drawable/ic_close.png").writeBytes(byteArrayOf(1))
119
+ File(second, "drawable").mkdirs()
120
+ File(second, "drawable/ic_close.xml").writeText("<vector/>")
121
+
122
+ val out = runCopy(first, second)
123
+
124
+ assertEquals(listOf("ic_close.png"), File(out, "drawable").list().orEmpty().toList())
125
+ }
126
+
127
+ @Test
128
+ fun `keeps namespace declarations of merged styleable attrs`() {
129
+ val first = libraryRes("lib-a", """<declare-styleable name="Shared"><attr name="a" format="color" /></declare-styleable>""")
130
+ val second = File(tempDir, "exploded-aar/lib-b/res")
131
+ File(second, "values").mkdirs()
132
+ File(second, "values/values.xml").writeText(
133
+ """
134
+ <?xml version="1.0" encoding="utf-8"?>
135
+ <resources xmlns:tools="http://schemas.android.com/tools">
136
+ <declare-styleable name="Shared"><attr name="b" format="color" tools:ignore="X" /></declare-styleable>
137
+ </resources>
138
+ """.trimIndent(),
139
+ )
140
+
141
+ val out = runCopy(first, second)
142
+
143
+ val factory = DocumentBuilderFactory.newInstance().apply { isNamespaceAware = true }
144
+ File(out, "values").listFiles().orEmpty().forEach { factory.newDocumentBuilder().parse(it) }
145
+ }
146
+
147
+ @Test
148
+ fun `lets the module's own resources win over a library's`() {
149
+ val module = libraryRes("module", """<string name="app_name">MyBrownfieldModule</string>""")
150
+ File(module, "drawable").mkdirs()
151
+ File(module, "drawable/logo.png").writeBytes(byteArrayOf(1))
152
+ val brownfield =
153
+ libraryRes(
154
+ "callstack_react-native-brownfield",
155
+ """<string name="app_name">ReactNativeBrownfield</string><string name="other">x</string>""",
156
+ )
157
+ File(brownfield, "drawable").mkdirs()
158
+ File(brownfield, "drawable/logo.xml").writeText("<vector/>")
159
+
160
+ val out = runCopy(brownfield, moduleResDirs = listOf(module))
161
+
162
+ assertEquals(listOf("string/other"), valuesElements(out).map { it.key() })
163
+ assertTrue(!File(out, "drawable/logo.xml").exists(), "the library's drawable overrides the module's")
164
+ }
165
+
166
+ @Test
167
+ fun `drops a library's copy of a Metro asset`() {
168
+ val metro = File(tempDir, "app/generated/res/react/release")
169
+ File(metro, "drawable-mdpi").mkdirs()
170
+ File(metro, "drawable-mdpi/node_modules_sdk_assets_icon.png").writeBytes(byteArrayOf(1))
171
+ val lib = libraryRes("flagship-sdk", "")
172
+ File(lib, "drawable-mdpi-v4").mkdirs()
173
+ File(lib, "drawable-mdpi-v4/node_modules_sdk_assets_icon.png").writeBytes(byteArrayOf(2))
174
+ File(lib, "drawable-mdpi-v4/sdk_only.png").writeBytes(byteArrayOf(3))
175
+
176
+ val out = runCopy(lib, moduleResDirs = listOf(metro))
177
+
178
+ assertTrue(!File(out, "drawable-mdpi-v4/node_modules_sdk_assets_icon.png").exists())
179
+ assertTrue(File(out, "drawable-mdpi-v4/sdk_only.png").exists())
180
+ }
181
+
182
+ @Test
183
+ fun `matches the module's qualifiers with the normalized ones of an AAR`() {
184
+ val module = File(tempDir, "module/res")
185
+ File(module, "values-night").mkdirs()
186
+ File(module, "values-night/colors.xml").writeText(resources("""<color name="bg">#000000</color>"""))
187
+ val lib = libraryRes("lib-a", "")
188
+ File(lib, "values-night-v8").mkdirs()
189
+ File(lib, "values-night-v8/values-night-v8.xml").writeText(resources("""<color name="bg">#FFFFFF</color>"""))
190
+
191
+ val out = runCopy(lib, moduleResDirs = listOf(module))
192
+
193
+ assertEquals(emptyList(), valuesElements(out, "values-night-v8").map { it.key() })
194
+ }
195
+
196
+ @Test
197
+ fun `reads module values that declare entities in a DOCTYPE`() {
198
+ val module = File(tempDir, "module/res")
199
+ File(module, "values").mkdirs()
200
+ File(module, "values/strings.xml").writeText(
201
+ """
202
+ <?xml version="1.0" encoding="utf-8"?>
203
+ <!DOCTYPE resources [<!ENTITY product "Brownfield">]>
204
+ <resources>
205
+ <string name="app_name">&product;</string>
206
+ </resources>
207
+ """.trimIndent(),
208
+ )
209
+ val brownfield = libraryRes("brownfield", """<string name="app_name">ReactNativeBrownfield</string>""")
210
+
211
+ val out = runCopy(brownfield, moduleResDirs = listOf(module))
212
+
213
+ assertEquals(emptyList(), valuesElements(out).map { it.key() })
214
+ }
215
+
216
+ @Test
217
+ fun `lets the module's resValues win over a library's`() {
218
+ val brownfield = libraryRes("brownfield", """<string name="app_name">ReactNativeBrownfield</string>""")
219
+
220
+ val out = runCopy(brownfield, moduleResValues = listOf("string/app_name"))
221
+
222
+ assertEquals(emptyList(), valuesElements(out).map { it.key() })
223
+ }
224
+
225
+ @Test
226
+ fun `keeps namespaced values from an AGP-written AAR`() {
227
+ val lib = File(tempDir, "exploded-aar/lib-a/res")
228
+ File(lib, "values").mkdirs()
229
+ File(lib, "values/values.xml").writeText(
230
+ """
231
+ <?xml version="1.0" encoding="utf-8"?>
232
+ <resources xmlns:ns1="urn:oasis:names:tc:xliff:document:1.2">
233
+ <string name="greeting">Hi <ns1:g id="name">%s</ns1:g></string>
234
+ </resources>
235
+ """.trimIndent(),
236
+ )
237
+
238
+ val out = runCopy(lib)
239
+
240
+ val factory = DocumentBuilderFactory.newInstance().apply { isNamespaceAware = true }
241
+ val file = File(out, "values").listFiles().orEmpty().single()
242
+ val g = factory.newDocumentBuilder().parse(file).getElementsByTagNameNS("urn:oasis:names:tc:xliff:document:1.2", "g")
243
+ assertEquals(1, g.length)
244
+ }
245
+
246
+ @Test
247
+ fun `keeps the module's definition of an attr a library declares inside a styleable`() {
248
+ val module = File(tempDir, "module/res")
249
+ File(module, "values").mkdirs()
250
+ File(module, "values/attrs.xml").writeText(resources("""<attr name="tint" format="color" />"""))
251
+ val lib =
252
+ libraryRes("lib-a", """<declare-styleable name="Badge"><attr name="tint" format="dimension" /></declare-styleable>""")
253
+
254
+ val out = runCopy(lib, moduleResDirs = listOf(module))
255
+
256
+ val attrs = valuesElements(out).filter { it.tagName == "declare-styleable" }.flatMap { it.childElements() }
257
+ assertEquals(listOf(""), attrs.map { it.getAttribute("format") }, "the library would override the module's attr format")
258
+ }
259
+
260
+ @Test
261
+ fun `keeps the first library's format of an attr two libraries declare in different styleables`() {
262
+ listOf("A" to "B", "B" to "A", "Badge" to "Chip", "Chip" to "Badge").forEach { (firstStyleable, secondStyleable) ->
263
+ tempDir.listFiles()?.forEach { it.deleteRecursively() }
264
+ val first =
265
+ libraryRes("lib-a", """<declare-styleable name="$firstStyleable"><attr name="x" format="color" /></declare-styleable>""")
266
+ val second =
267
+ libraryRes(
268
+ "lib-b",
269
+ """<declare-styleable name="$secondStyleable"><attr name="x" format="dimension" /></declare-styleable>""",
270
+ )
271
+
272
+ val out = runCopy(first, second)
273
+
274
+ val elements = valuesElements(out)
275
+ val formats =
276
+ (elements + elements.filter { it.tagName == "declare-styleable" }.flatMap { it.childElements() })
277
+ .filter { it.tagName == "attr" && it.getAttribute("name") == "x" && it.hasAttribute("format") }
278
+ .map { it.getAttribute("format") }
279
+ assertEquals(listOf("color"), formats, "styleables $firstStyleable and $secondStyleable")
280
+ }
281
+ }
282
+
283
+ @Test
284
+ fun `keeps the same name in different qualifiers`() {
285
+ val lib = libraryRes("lib-a", """<color name="bg">#000000</color>""")
286
+ File(lib, "values-night").mkdirs()
287
+ File(lib, "values-night/values-night.xml").writeText(resources("""<color name="bg">#FFFFFF</color>"""))
288
+
289
+ val out = runCopy(lib)
290
+
291
+ assertEquals(listOf("#000000"), valuesElements(out, "values").map { it.textContent })
292
+ // Written with the implied version qualifier, as in an AAR.
293
+ assertEquals(listOf("#FFFFFF"), valuesElements(out, "values-night-v8").map { it.textContent })
294
+ }
295
+
296
+ @Test
297
+ fun `keeps the first library's file for other resource types`() {
298
+ val first = libraryRes("lib-a", "")
299
+ val second = libraryRes("lib-b", "")
300
+ listOf(first to "a", second to "b").forEach { (resDir, marker) ->
301
+ File(resDir, "drawable").mkdirs()
302
+ File(resDir, "drawable/icon.xml").writeText("<shape xmlns:android=\"x\" android:tag=\"$marker\"/>")
303
+ }
304
+
305
+ val out = runCopy(first, second)
306
+
307
+ assertTrue(File(out, "drawable/icon.xml").readText().contains("\"a\""))
308
+ }
309
+
310
+ private fun libraryRes(
311
+ libraryName: String,
312
+ valuesBody: String,
313
+ ): File {
314
+ val resDir = File(tempDir, "exploded-aar/$libraryName/res")
315
+ File(resDir, "values").mkdirs()
316
+ File(resDir, "values/values.xml").writeText(resources(valuesBody))
317
+ return resDir
318
+ }
319
+
320
+ private fun resources(body: String) =
321
+ """
322
+ <?xml version="1.0" encoding="utf-8"?>
323
+ <resources>
324
+ $body
325
+ </resources>
326
+ """.trimIndent()
327
+
328
+ private fun runCopy(
329
+ vararg resDirs: File,
330
+ moduleResDirs: List<File> = emptyList(),
331
+ moduleResValues: List<String> = emptyList(),
332
+ ): File {
333
+ val project: Project = ProjectBuilder.builder().withProjectDir(File(tempDir, "project")).build()
334
+ val out = File(tempDir, "out")
335
+ val task = project.tasks.register("copyExplodedResForTest", CopyExplodedResTask::class.java).get()
336
+ task.inputResDirs.setFrom(resDirs.toList())
337
+ task.moduleResDirs.setFrom(moduleResDirs)
338
+ task.moduleResValues.set(moduleResValues)
339
+ task.outputDirectory.set(out)
340
+
341
+ task.run()
342
+
343
+ // AGP loads the generated dir as one resource set, which fails on any duplicate (#125).
344
+ ResourceSet("generated", ResourceNamespace.RES_AUTO, null, true, null).apply {
345
+ addSource(out)
346
+ loadFromFiles(StdLogger(StdLogger.Level.WARNING))
347
+ }
348
+ return out
349
+ }
350
+
351
+ /** Top-level resources of every file in one qualifier directory, one entry per definition. */
352
+ private fun valuesElements(
353
+ resDir: File,
354
+ qualifierDir: String = "values",
355
+ ): List<Element> {
356
+ val files = File(resDir, qualifierDir).listFiles { f -> f.extension == "xml" }.orEmpty()
357
+ return files.flatMap { file ->
358
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file).documentElement.childElements()
359
+ }
360
+ }
361
+
362
+ private fun Element.key() = "$tagName/${getAttribute("name")}"
363
+
364
+ private fun Element.childElements(): List<Element> {
365
+ val children = childNodes
366
+ return (0 until children.length).map { children.item(it) }.filterIsInstance<Element>()
367
+ }
368
+ }
@@ -0,0 +1,252 @@
1
+ package com.callstack.react.brownfield.processors
2
+
3
+ import org.junit.jupiter.api.Test
4
+ import org.junit.jupiter.api.io.TempDir
5
+ import java.io.File
6
+ import kotlin.test.assertEquals
7
+ import kotlin.test.assertFalse
8
+ import kotlin.test.assertNull
9
+ import kotlin.test.assertTrue
10
+
11
+ class JniLibsPlanTest {
12
+ @TempDir
13
+ lateinit var tempDir: File
14
+
15
+ private val appDir by lazy { File(tempDir, "app") }
16
+
17
+ private fun aarDir(name: String) = File(tempDir, "aar-$name")
18
+
19
+ private fun so(
20
+ root: File,
21
+ abi: String,
22
+ name: String,
23
+ content: String = name,
24
+ ): File =
25
+ File(root, "$abi/$name").apply {
26
+ parentFile.mkdirs()
27
+ writeText(content)
28
+ }
29
+
30
+ private fun plan(
31
+ aars: List<File> = emptyList(),
32
+ app: File? = appDir,
33
+ ignored: List<String> = IGNORE_EMBEDDED_LIBS,
34
+ dynamic: List<String> = emptyList(),
35
+ ) = planJniLibs(aars, app, ignored, dynamic)
36
+
37
+ @Test
38
+ fun `copies app-owned libs from the app dir`() {
39
+ val appModules = so(appDir, "arm64-v8a", "libappmodules.so")
40
+ val codegen = so(appDir, "arm64-v8a", "libreact_codegen_rnscreens.so")
41
+
42
+ val plan = plan()
43
+
44
+ assertEquals(appModules, plan.copies["arm64-v8a/libappmodules.so"])
45
+ assertEquals(codegen, plan.copies["arm64-v8a/libreact_codegen_rnscreens.so"])
46
+ }
47
+
48
+ @Test
49
+ fun `skips app dir libs that no embedded AAR ships`() {
50
+ for (abi in listOf("arm64-v8a", "x86_64")) {
51
+ so(appDir, abi, "libQmBlur.so")
52
+ so(appDir, abi, "libandroidx.graphics.path.so")
53
+ }
54
+
55
+ val plan = plan()
56
+
57
+ assertTrue(plan.copies.isEmpty(), "Maven-only libs must stay out of the AAR: ${plan.copies.keys}")
58
+ }
59
+
60
+ @Test
61
+ fun `falls back to the embedded AAR copy when the app dir lacks it`() {
62
+ val aar = aarDir("a")
63
+ val fromAar = so(aar, "arm64-v8a", "libfoo.so")
64
+ so(appDir, "arm64-v8a", "libappmodules.so")
65
+
66
+ val plan = plan(aars = listOf(aar))
67
+
68
+ assertEquals(fromAar, plan.copies["arm64-v8a/libfoo.so"])
69
+ }
70
+
71
+ @Test
72
+ fun `drops ignored libs`() {
73
+ val aar = aarDir("a")
74
+ so(aar, "arm64-v8a", "libreactnative.so")
75
+ so(aar, "arm64-v8a", "libc++_shared.so")
76
+ so(aar, "arm64-v8a", "libdatadog-ndk.so")
77
+ so(appDir, "arm64-v8a", "libmmkv.so")
78
+
79
+ val plan =
80
+ plan(
81
+ aars = listOf(aar),
82
+ ignored = IGNORE_EMBEDDED_LIBS + "libdatadog" + "mmkv",
83
+ dynamic = listOf("libmmkv.so"),
84
+ )
85
+
86
+ assertTrue(plan.copies.isEmpty(), "ignored libs were embedded: ${plan.copies.keys}")
87
+ }
88
+
89
+ @Test
90
+ fun `keeps app-owned libs even if an ignore entry matches`() {
91
+ so(appDir, "arm64-v8a", "libappmodules.so")
92
+
93
+ val plan = plan(ignored = listOf("libappmodules"))
94
+
95
+ assertTrue("arm64-v8a/libappmodules.so" in plan.copies)
96
+ }
97
+
98
+ @Test
99
+ fun `includes dynamicLibs from the app dir`() {
100
+ val mmkv = so(appDir, "arm64-v8a", "libmmkv.so")
101
+
102
+ val plan = plan(dynamic = listOf("libmmkv.so"))
103
+
104
+ assertEquals(mmkv, plan.copies["arm64-v8a/libmmkv.so"])
105
+ assertTrue(plan.unmatchedDynamicLibs.isEmpty())
106
+ }
107
+
108
+ @Test
109
+ fun `prefers the app dir copy of a duplicate name`() {
110
+ val a = aarDir("a")
111
+ val b = aarDir("b")
112
+ so(a, "arm64-v8a", "libworklets.so", "a")
113
+ so(b, "arm64-v8a", "libworklets.so", "b")
114
+ val fromApp = so(appDir, "arm64-v8a", "libworklets.so", "app")
115
+
116
+ val plan = plan(aars = listOf(a, b))
117
+
118
+ assertEquals(listOf("arm64-v8a/libworklets.so"), plan.copies.keys.toList())
119
+ assertEquals(fromApp, plan.copies["arm64-v8a/libworklets.so"])
120
+ }
121
+
122
+ @Test
123
+ fun `keeps the first AAR copy of a duplicate name`() {
124
+ val a = aarDir("a")
125
+ val b = aarDir("b")
126
+ val first = so(a, "arm64-v8a", "libdup.so", "a")
127
+ so(b, "arm64-v8a", "libdup.so", "b")
128
+
129
+ val plan = plan(aars = listOf(a, b), app = null)
130
+
131
+ assertEquals(first, plan.copies["arm64-v8a/libdup.so"])
132
+ }
133
+
134
+ @Test
135
+ fun `records why each lib was skipped`() {
136
+ val aar = aarDir("a")
137
+ so(appDir, "arm64-v8a", "libQmBlur.so")
138
+ so(aar, "arm64-v8a", "libreactnative.so")
139
+
140
+ val plan = plan(aars = listOf(aar))
141
+
142
+ assertEquals(
143
+ mapOf("libQmBlur.so" to SkipReason.NOT_EMBEDDED, "libreactnative.so" to SkipReason.IGNORED),
144
+ plan.skipped,
145
+ )
146
+ }
147
+
148
+ @Test
149
+ fun `summary lists only libs no embedded library ships`() {
150
+ val aar = aarDir("a")
151
+ so(appDir, "arm64-v8a", "libQmBlur.so")
152
+ so(appDir, "arm64-v8a", "libandroidx.graphics.path.so")
153
+ so(aar, "arm64-v8a", "libreactnative.so")
154
+
155
+ val summary = skippedSummary(plan(aars = listOf(aar)), File("/r/skipped-so.txt"))!!
156
+
157
+ assertTrue(summary.startsWith("Brownfield: left 2 .so file(s) out of the AAR"), summary)
158
+ assertTrue("libQmBlur.so, libandroidx.graphics.path.so" in summary, summary)
159
+ assertTrue("your app module builds or ships it" in summary, summary)
160
+ assertTrue("reactBrownfield.dynamicLibs" in summary, summary)
161
+ assertTrue("Details: /r/skipped-so.txt" in summary, summary)
162
+ assertFalse("libreactnative.so" in summary, summary)
163
+ }
164
+
165
+ @Test
166
+ fun `no summary when nothing is skipped as not embedded`() {
167
+ val aar = aarDir("a")
168
+ so(appDir, "arm64-v8a", "libappmodules.so")
169
+ so(aar, "arm64-v8a", "libreactnative.so")
170
+
171
+ assertNull(skippedSummary(plan(aars = listOf(aar)), null))
172
+ }
173
+
174
+ @Test
175
+ fun `reports dynamicLibs entries that match nothing`() {
176
+ val plan = plan(dynamic = listOf("libnope.so", "libmmkv*.so"))
177
+
178
+ assertEquals(setOf("libnope.so", "libmmkv*.so"), plan.unmatchedDynamicLibs)
179
+ }
180
+
181
+ @Test
182
+ fun `only uses the ABIs the app module built`() {
183
+ val aar = aarDir("a")
184
+ so(appDir, "arm64-v8a", "libappmodules.so")
185
+ so(aar, "arm64-v8a", "libfoo.so")
186
+ so(aar, "x86_64", "libfoo.so")
187
+
188
+ val plan = plan(aars = listOf(aar))
189
+
190
+ assertEquals(setOf("arm64-v8a/libappmodules.so", "arm64-v8a/libfoo.so"), plan.copies.keys)
191
+ assertEquals(setOf("x86_64"), plan.excludedAbis)
192
+ }
193
+
194
+ @Test
195
+ fun `uses every ABI the AARs ship when the app module built none`() {
196
+ val aar = aarDir("a")
197
+ so(aar, "x86_64", "libfoo.so")
198
+
199
+ val plan = plan(aars = listOf(aar), app = null)
200
+
201
+ assertEquals(setOf("x86_64/libfoo.so"), plan.copies.keys)
202
+ assertTrue(plan.excludedAbis.isEmpty())
203
+ }
204
+
205
+ @Test
206
+ fun `warns about dynamicLibs entries that an ignore entry drops`() {
207
+ so(appDir, "arm64-v8a", "libmmkv.so")
208
+
209
+ val plan = plan(ignored = listOf("mmkv"), dynamic = listOf("libmmkv.so"))
210
+
211
+ assertEquals(setOf("libmmkv.so"), plan.ignoredDynamicLibs)
212
+ assertTrue(plan.unmatchedDynamicLibs.isEmpty())
213
+ val warning = planWarnings(plan).single()
214
+ assertTrue("libmmkv.so" in warning && "ignoreEmbeddedLibs" in warning, warning)
215
+ }
216
+
217
+ @Test
218
+ fun `warns about dynamicLibs entries that match nothing`() {
219
+ val warning = planWarnings(plan(dynamic = listOf("libnope.so"))).single()
220
+
221
+ assertTrue("'libnope.so' matches no .so file" in warning, warning)
222
+ }
223
+
224
+ @Test
225
+ fun `no warnings for a clean plan`() {
226
+ so(appDir, "arm64-v8a", "libappmodules.so")
227
+
228
+ assertTrue(planWarnings(plan()).isEmpty())
229
+ }
230
+
231
+ @Test
232
+ fun `stripped mode reads the app's stripped output`() {
233
+ assertEquals(
234
+ AppNativeLibsSource(
235
+ "intermediates/stripped_native_libs/devRelease/stripDevReleaseDebugSymbols/out/lib",
236
+ "stripDevReleaseDebugSymbols",
237
+ ),
238
+ appNativeLibsSource("devRelease", true),
239
+ )
240
+ }
241
+
242
+ @Test
243
+ fun `unstripped mode reads the app's merged output`() {
244
+ assertEquals(
245
+ AppNativeLibsSource(
246
+ "intermediates/merged_native_libs/devRelease/mergeDevReleaseNativeLibs/out/lib",
247
+ "mergeDevReleaseNativeLibs",
248
+ ),
249
+ appNativeLibsSource("devRelease", false),
250
+ )
251
+ }
252
+ }