@callstack/react-native-brownfield 6.0.1 → 6.1.1
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.
- package/CHANGELOG.md +30 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +12 -12
- package/android/src/main/java/com/callstack/reactnativebrownfield/RetryableOnce.kt +27 -0
- package/android/src/test/java/com/callstack/reactnativebrownfield/RetryableOnceTest.kt +37 -0
- package/gradle-plugin/brownfield/build.gradle.kts +4 -0
- package/gradle-plugin/brownfield/gradle/libs.versions.toml +2 -0
- package/gradle-plugin/brownfield/gradle.properties +1 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +24 -7
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +4 -37
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt +206 -104
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ResourceTaskProcessor.kt +201 -4
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantHelper.kt +7 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt +4 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/GradleExclude.kt +31 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +2 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjector.kt +28 -3
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +14 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/TaskOutputs.kt +40 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +14 -6
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/HardExcludePredicateTest.kt +28 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ConfigureProcessJniTaskTest.kt +60 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/CopyExplodedResTaskTest.kt +368 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/JniLibsPlanTest.kt +252 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt +73 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MetroResDirWiringTest.kt +39 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ProcessAndCopyJniLibsTaskTest.kt +136 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/GradleExcludesTest.kt +81 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +56 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +5 -0
- package/ios/ReactNativeBrownfield.swift +3 -1
- package/ios/ReactNativeBrownfieldModule.mm +35 -7
- package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
- package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -1
- package/lib/commonjs/index.js +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/scaffold/index.js +1 -1
- package/lib/commonjs/scaffold/index.js.map +1 -1
- package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
- package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/scaffold/index.js +1 -1
- package/lib/module/scaffold/index.js.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
- package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
- package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/expo-config-plugin/android/utils/constants.ts +1 -1
- package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +42 -0
- package/src/expo-config-plugin/template/android/build.gradle.kts +1 -1
- package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +7 -0
- package/src/index.ts +4 -2
- 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
|
+
}
|