@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
|
@@ -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
|
-
@
|
|
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
|
|
178
|
+
abstract val appNativeLibsDir: DirectoryProperty
|
|
50
179
|
|
|
51
180
|
@get:Input
|
|
52
|
-
abstract val
|
|
181
|
+
abstract val extraIgnoredLibs: ListProperty<String>
|
|
53
182
|
|
|
54
183
|
@get:Input
|
|
55
|
-
abstract val
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
169
|
-
it
|
|
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(
|
|
@@ -1,20 +1,39 @@
|
|
|
1
1
|
package com.callstack.react.brownfield.processors
|
|
2
2
|
|
|
3
|
+
import com.android.build.api.dsl.LibraryExtension
|
|
4
|
+
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
|
3
5
|
import com.android.build.api.variant.LibraryVariant
|
|
6
|
+
import com.android.ide.common.rendering.api.ResourceNamespace
|
|
7
|
+
import com.android.ide.common.resources.MergeConsumer
|
|
8
|
+
import com.android.ide.common.resources.ResourceMerger
|
|
9
|
+
import com.android.ide.common.resources.ResourceMergerItem
|
|
10
|
+
import com.android.ide.common.resources.ResourceSet
|
|
4
11
|
import com.callstack.react.brownfield.shared.ExplodeAarTask
|
|
12
|
+
import com.callstack.react.brownfield.shared.GradleILogger
|
|
5
13
|
import com.callstack.react.brownfield.utils.AndroidArchiveLibrary
|
|
14
|
+
import com.callstack.react.brownfield.utils.Extension
|
|
6
15
|
import com.callstack.react.brownfield.utils.capitalized
|
|
16
|
+
import com.callstack.react.brownfield.utils.directoryGetter
|
|
7
17
|
import org.gradle.api.DefaultTask
|
|
8
18
|
import org.gradle.api.Project
|
|
9
19
|
import org.gradle.api.file.ConfigurableFileCollection
|
|
10
20
|
import org.gradle.api.file.DirectoryProperty
|
|
21
|
+
import org.gradle.api.provider.ListProperty
|
|
11
22
|
import org.gradle.api.tasks.CacheableTask
|
|
23
|
+
import org.gradle.api.tasks.Input
|
|
12
24
|
import org.gradle.api.tasks.InputFiles
|
|
13
25
|
import org.gradle.api.tasks.OutputDirectory
|
|
14
26
|
import org.gradle.api.tasks.PathSensitive
|
|
15
27
|
import org.gradle.api.tasks.PathSensitivity
|
|
16
28
|
import org.gradle.api.tasks.TaskAction
|
|
17
29
|
import org.gradle.api.tasks.TaskProvider
|
|
30
|
+
import org.w3c.dom.Element
|
|
31
|
+
import org.w3c.dom.Node
|
|
32
|
+
import java.io.File
|
|
33
|
+
import javax.xml.parsers.DocumentBuilderFactory
|
|
34
|
+
import javax.xml.transform.TransformerFactory
|
|
35
|
+
import javax.xml.transform.dom.DOMSource
|
|
36
|
+
import javax.xml.transform.stream.StreamResult
|
|
18
37
|
|
|
19
38
|
@CacheableTask
|
|
20
39
|
abstract class CopyExplodedResTask : DefaultTask() {
|
|
@@ -22,23 +41,183 @@ abstract class CopyExplodedResTask : DefaultTask() {
|
|
|
22
41
|
@get:PathSensitive(PathSensitivity.RELATIVE)
|
|
23
42
|
abstract val inputResDirs: ConfigurableFileCollection
|
|
24
43
|
|
|
44
|
+
/** The module's own resources, which win over every library's, as an app's do over its dependencies. */
|
|
45
|
+
@get:InputFiles
|
|
46
|
+
@get:PathSensitive(PathSensitivity.RELATIVE)
|
|
47
|
+
abstract val moduleResDirs: ConfigurableFileCollection
|
|
48
|
+
|
|
49
|
+
/** `type/name` of the module's `resValue`s, which also win over every library's. */
|
|
50
|
+
@get:Input
|
|
51
|
+
abstract val moduleResValues: ListProperty<String>
|
|
52
|
+
|
|
53
|
+
/** The first library wins on a duplicate, so the order is part of the input. */
|
|
54
|
+
@get:Input
|
|
55
|
+
abstract val libraryOrder: ListProperty<String>
|
|
56
|
+
|
|
25
57
|
@get:OutputDirectory
|
|
26
58
|
abstract val outputDirectory: DirectoryProperty
|
|
27
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Every exploded AAR keeps its values in `res/values/values.xml`, so libraries can't simply be
|
|
62
|
+
* copied over each other (#490), and keeping a resource twice in one dir fails the build (#125).
|
|
63
|
+
* AGP's own merger picks the winners, with the precedence it uses in an app: the module's own
|
|
64
|
+
* resources, then the first library.
|
|
65
|
+
*/
|
|
28
66
|
@TaskAction
|
|
29
67
|
fun run() {
|
|
30
68
|
val out = outputDirectory.get().asFile
|
|
31
69
|
out.deleteRecursively()
|
|
32
70
|
out.mkdirs()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
val iLogger = GradleILogger(logger)
|
|
72
|
+
val moduleDirs = moduleResDirs.filter { it.exists() }.toList()
|
|
73
|
+
// 0 keeps every versioned resource. AGP applies the module's minSdk when it merges this dir.
|
|
74
|
+
val merger = ResourceMerger(0)
|
|
75
|
+
// Lowest priority first.
|
|
76
|
+
inputResDirs.filter { it.exists() }.reversed().forEachIndexed { index, resDir ->
|
|
77
|
+
merger.addDataSet(resourceSet("library$index", resDir, isLibrary = true, iLogger))
|
|
78
|
+
}
|
|
79
|
+
// One set per dir: two module dirs may override each other, like main and a flavor.
|
|
80
|
+
moduleDirs.forEachIndexed { index, resDir ->
|
|
81
|
+
merger.addDataSet(resourceSet("module$index", resDir, isLibrary = false, iLogger))
|
|
82
|
+
}
|
|
83
|
+
val writer = LibraryResWriter(moduleDirs, moduleResValues.get().toSet())
|
|
84
|
+
merger.mergeData(writer, false)
|
|
85
|
+
writer.messages.forEach { logger.warn("Brownfield: $it") }
|
|
86
|
+
writer.writeTo(out)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun resourceSet(
|
|
90
|
+
name: String,
|
|
91
|
+
resDir: File,
|
|
92
|
+
isLibrary: Boolean,
|
|
93
|
+
iLogger: GradleILogger,
|
|
94
|
+
) = ResourceSet(name, ResourceNamespace.RES_AUTO, if (isLibrary) name else null, false, null).apply {
|
|
95
|
+
addSource(resDir)
|
|
96
|
+
loadFromFiles(iLogger)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Writes the merged library resources that the module doesn't define itself. AGP ranks this generated
|
|
102
|
+
* dir above the module's own res, so a library copy of a module resource would win there.
|
|
103
|
+
*/
|
|
104
|
+
private class LibraryResWriter(
|
|
105
|
+
private val moduleDirs: List<File>,
|
|
106
|
+
private val moduleResValues: Set<String>,
|
|
107
|
+
) : MergeConsumer<ResourceMergerItem> {
|
|
108
|
+
private val files = mutableListOf<Pair<String, File>>()
|
|
109
|
+
private val values = sortedMapOf<String, MutableList<Node>>()
|
|
110
|
+
private val moduleAttrs = mutableMapOf<String, MutableSet<String>>()
|
|
111
|
+
private val styleableAttrWinners = mutableMapOf<String, MutableList<Node>>()
|
|
112
|
+
val messages = mutableListOf<String>()
|
|
113
|
+
|
|
114
|
+
override fun start(factory: DocumentBuilderFactory) = Unit
|
|
115
|
+
|
|
116
|
+
override fun end() = Unit
|
|
117
|
+
|
|
118
|
+
override fun removeItem(
|
|
119
|
+
removedItem: ResourceMergerItem,
|
|
120
|
+
replacedBy: ResourceMergerItem?,
|
|
121
|
+
) = Unit
|
|
122
|
+
|
|
123
|
+
override fun ignoreItemInMerge(item: ResourceMergerItem) = false
|
|
124
|
+
|
|
125
|
+
override fun addItem(item: ResourceMergerItem) {
|
|
126
|
+
val folder = valuesFolder(item.qualifiers)
|
|
127
|
+
val source = item.sourceFile?.file
|
|
128
|
+
if (source != null && moduleDirs.any { source.startsWith(it) }) {
|
|
129
|
+
if (item.type.getName() == "attr") moduleAttrs.getOrPut(folder) { mutableSetOf() }.add(item.name)
|
|
130
|
+
return
|
|
37
131
|
}
|
|
132
|
+
if (item.qualifiers.isEmpty() && "${item.type.getName()}/${item.name}" in moduleResValues) return
|
|
133
|
+
// An attr declared inside a styleable. The merger picked this one, so it becomes the definition.
|
|
134
|
+
if (item.ignoredFromDiskMerge) {
|
|
135
|
+
styleableAttrWinners.getOrPut(folder) { mutableListOf() }.add(item.value)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (item.isFileBased && source != null) {
|
|
139
|
+
files.add("${source.parentFile.name}/${source.name}" to source)
|
|
140
|
+
} else {
|
|
141
|
+
values.getOrPut(folder) { mutableListOf() }.add(item.value)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fun writeTo(out: File) {
|
|
146
|
+
files.forEach { (path, file) -> file.copyTo(File(out, path)) }
|
|
147
|
+
val transformer = TransformerFactory.newInstance().newTransformer()
|
|
148
|
+
(values.keys + styleableAttrWinners.keys).toSortedSet().forEach { folder ->
|
|
149
|
+
val nodes = values[folder].orEmpty()
|
|
150
|
+
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
|
|
151
|
+
val root = document.createElement("resources")
|
|
152
|
+
document.appendChild(root)
|
|
153
|
+
nodes.forEach { root.appendChild(document.importNode(it, true)) }
|
|
154
|
+
styleableAttrWinners[folder].orEmpty().forEach { root.appendChild(document.importNode(it, true)) }
|
|
155
|
+
keepOneAttrDefinition(root, folder, moduleAttrs[folder].orEmpty())
|
|
156
|
+
val target = File(out, "$folder/$folder.xml")
|
|
157
|
+
target.parentFile.mkdirs()
|
|
158
|
+
transformer.transform(DOMSource(document), StreamResult(target))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* AGP also registers an attr with a `format` (or enum/flag values) inside a styleable as its own
|
|
164
|
+
* `attr/<name>`, so one dir may define it only once. The merger's winner is written as a top-level
|
|
165
|
+
* attr, and every styleable copy, or one of an attr the module defines, becomes a plain reference.
|
|
166
|
+
*/
|
|
167
|
+
private fun keepOneAttrDefinition(
|
|
168
|
+
root: Element,
|
|
169
|
+
folder: String,
|
|
170
|
+
moduleAttrs: Set<String>,
|
|
171
|
+
) {
|
|
172
|
+
val defined = root.childElements().filter { it.tagName == "attr" }.associateBy { it.getAttribute("name") }
|
|
173
|
+
root.childElements()
|
|
174
|
+
.filter { it.tagName == "declare-styleable" }
|
|
175
|
+
.flatMap { it.childElements() }
|
|
176
|
+
.filter { it.tagName == "attr" && !it.getAttribute("name").startsWith("android:") }
|
|
177
|
+
.filter { definition(it).isNotEmpty() }
|
|
178
|
+
.forEach { attr ->
|
|
179
|
+
val name = attr.getAttribute("name")
|
|
180
|
+
val winner = defined[name]
|
|
181
|
+
if (winner == null && name !in moduleAttrs) return@forEach
|
|
182
|
+
if (winner != null && definition(winner) != definition(attr)) {
|
|
183
|
+
messages.add("$folder: attr/$name is defined differently by two libraries, keeping the first")
|
|
184
|
+
}
|
|
185
|
+
attr.removeAttribute("format")
|
|
186
|
+
attr.childElements().forEach { attr.removeChild(it) }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Format and enum/flag values of an attr; empty for a plain reference. */
|
|
191
|
+
private fun definition(attr: Element): String =
|
|
192
|
+
attr.getAttribute("format") +
|
|
193
|
+
attr.childElements().joinToString { "${it.tagName}:${it.getAttribute("name")}=${it.getAttribute("value")}" }
|
|
194
|
+
|
|
195
|
+
private fun valuesFolder(qualifiers: String) = if (qualifiers.isEmpty()) "values" else "values-$qualifiers"
|
|
196
|
+
|
|
197
|
+
private fun Element.childElements(): List<Element> {
|
|
198
|
+
val children = childNodes
|
|
199
|
+
return (0 until children.length).map { children.item(it) }.filterIsInstance<Element>()
|
|
38
200
|
}
|
|
39
201
|
}
|
|
40
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Some libraries commit the drawables Metro generates for their JS assets to their own `res`, and
|
|
205
|
+
* the app's bundle writes the same files to `generated/res/react`. Both end up as generated res of
|
|
206
|
+
* this module, where AGP fails on the duplicates, so Metro's copy is treated as the module's own and
|
|
207
|
+
* wins. Reading the bundle task's output also orders the copy after it, and keys the cache on it.
|
|
208
|
+
*/
|
|
209
|
+
internal fun wireMetroResDir(
|
|
210
|
+
task: CopyExplodedResTask,
|
|
211
|
+
appProject: Project,
|
|
212
|
+
bundleTaskName: String,
|
|
213
|
+
) {
|
|
214
|
+
val getter = directoryGetter(appProject, bundleTaskName, "getResourcesDir")
|
|
215
|
+
task.moduleResDirs.from(appProject.tasks.named(bundleTaskName).flatMap { getter.invoke(it) as DirectoryProperty })
|
|
216
|
+
}
|
|
217
|
+
|
|
41
218
|
object ResourceTaskProcessor {
|
|
219
|
+
private const val RES_VALUES_OFF_BY_DEFAULT_SINCE_AGP = 9
|
|
220
|
+
|
|
42
221
|
fun process(
|
|
43
222
|
project: Project,
|
|
44
223
|
variant: LibraryVariant,
|
|
@@ -47,12 +226,22 @@ object ResourceTaskProcessor {
|
|
|
47
226
|
) {
|
|
48
227
|
val capitalized = variant.name.capitalized()
|
|
49
228
|
val resDirs = aarLibraries.map { it.getResDir() }
|
|
229
|
+
val buildDir = project.layout.buildDirectory.get().asFile
|
|
230
|
+
val appProject = project.rootProject.project(project.extensions.getByType(Extension::class.java).appProjectName)
|
|
50
231
|
val copyResTask =
|
|
51
232
|
project.tasks.register(
|
|
52
233
|
"copyExplodedResFor$capitalized",
|
|
53
234
|
CopyExplodedResTask::class.java,
|
|
54
235
|
) { task ->
|
|
55
236
|
task.inputResDirs.setFrom(resDirs)
|
|
237
|
+
if (resValuesEnabled(project)) {
|
|
238
|
+
task.moduleResValues.set(variant.resValues.map { values -> values.keys.map { "${it.type}/${it.name}" } })
|
|
239
|
+
} else {
|
|
240
|
+
task.moduleResValues.set(emptyList())
|
|
241
|
+
}
|
|
242
|
+
variant.sources.res?.static?.let { static -> task.moduleResDirs.from(static.map { layers -> layers.flatten() }) }
|
|
243
|
+
task.libraryOrder.set(aarLibraries.map { it.getExplodedAarRootDir().relativeTo(buildDir).invariantSeparatorsPath })
|
|
244
|
+
wireMetroResDir(task, appProject, VariantHelper.getMetroBundleTaskName(variant))
|
|
56
245
|
task.dependsOn(explodeTask)
|
|
57
246
|
}
|
|
58
247
|
|
|
@@ -61,4 +250,12 @@ object ResourceTaskProcessor {
|
|
|
61
250
|
CopyExplodedResTask::outputDirectory,
|
|
62
251
|
)
|
|
63
252
|
}
|
|
253
|
+
|
|
254
|
+
/** AGP warns on every read of `variant.resValues` while the feature is off, the default from AGP 9 on. */
|
|
255
|
+
private fun resValuesEnabled(project: Project): Boolean {
|
|
256
|
+
val explicit = project.extensions.getByType(LibraryExtension::class.java).buildFeatures.resValues
|
|
257
|
+
val default = project.providers.gradleProperty("android.defaults.buildfeatures.resvalues").orNull?.toBooleanStrictOrNull()
|
|
258
|
+
val agpMajor = project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).pluginVersion.major
|
|
259
|
+
return explicit ?: default ?: (agpMajor < RES_VALUES_OFF_BY_DEFAULT_SINCE_AGP)
|
|
260
|
+
}
|
|
64
261
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.callstack.react.brownfield.processors
|
|
2
2
|
|
|
3
|
+
import com.android.build.api.variant.LibraryVariant
|
|
3
4
|
import com.callstack.react.brownfield.utils.capitalized
|
|
4
5
|
import org.gradle.api.Project
|
|
5
6
|
import org.gradle.api.Task
|
|
@@ -39,6 +40,12 @@ object VariantHelper {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** RNGP's `createBundle<Variant>JsAndAssets` task in the app project for [variant]. */
|
|
44
|
+
fun getMetroBundleTaskName(variant: LibraryVariant): String {
|
|
45
|
+
val bundledAssetsVariantName = getBundledAssetsVariantName(variant.name, variant.buildType, variant.debuggable)
|
|
46
|
+
return "createBundle${bundledAssetsVariantName.capitalized()}JsAndAssets"
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
fun getExpoUpdatesResourcesTaskName(variantName: String): String {
|
|
43
50
|
return "create${variantName.capitalized()}UpdatesResources"
|
|
44
51
|
}
|