@callstack/react-native-brownfield 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -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/AndroidArchiveLibrary.kt +41 -4
- 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/AndroidArchiveLibraryNamespaceFallbackTest.kt +137 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +5 -0
- package/ios/Expo/ExpoHostRuntime.swift +3 -1
- package/ios/Expo/ExpoUpdatesStartup.swift +2 -1
- 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/ios/podfileHelpers.js +2 -2
- package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
- package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -1
- package/lib/commonjs/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- 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/commonjs/scaffold/projectFiles.js +1 -1
- package/lib/commonjs/scaffold/projectFiles.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/ios/podfileHelpers.js +2 -2
- package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
- package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
- package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
- package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -1
- package/lib/module/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- 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/module/scaffold/projectFiles.js +1 -1
- package/lib/module/scaffold/projectFiles.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/ios/podfileHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.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/commonjs/src/scaffold/projectFiles.d.ts +2 -2
- package/lib/typescript/commonjs/src/scaffold/projectFiles.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/ios/podfileHelpers.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
- package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
- package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.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/lib/typescript/module/src/scaffold/projectFiles.d.ts +2 -2
- package/lib/typescript/module/src/scaffold/projectFiles.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/ios/podfileHelpers.ts +47 -13
- package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +11 -1
- package/src/expo-config-plugin/ios/xcodeHelpers.ts +67 -9
- package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/src/expo-config-plugin/template/android/build.gradle.kts +1 -1
- package/src/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +7 -0
- package/src/index.ts +4 -2
- package/src/scaffold/index.ts +61 -4
- package/src/scaffold/projectFiles.ts +5 -3
|
@@ -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
|
}
|
|
@@ -66,6 +66,10 @@ class VariantTaskProvider(val project: Project) {
|
|
|
66
66
|
it.dependsOn(VariantHelper.getJavaCompileTask(project, capitalizedVariantName))
|
|
67
67
|
it.variantName.set(variantName)
|
|
68
68
|
it.inputClassesJars.from(aarLibraries.map { aarLibrary -> aarLibrary.getClassesJarFile() })
|
|
69
|
+
// libs/*.jar of each library (#489)
|
|
70
|
+
it.inputClassesJars.from(
|
|
71
|
+
aarLibraries.map { aarLibrary -> project.fileTree(aarLibrary.getLibsDir()) { tree -> tree.include("*.jar") } },
|
|
72
|
+
)
|
|
69
73
|
it.outputDir.set(DirectoryManager.getMergeClassDirectory(variantName))
|
|
70
74
|
}
|
|
71
75
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
2
|
+
|
|
3
|
+
import org.gradle.api.Project
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The variant-independent configurations that feed what the host app gets. A variant's own
|
|
7
|
+
* (`debugRuntimeClasspath`) would leak into every variant, since the published list is shared, and a
|
|
8
|
+
* tool's (lint, detekt) may exclude things the app still needs.
|
|
9
|
+
*/
|
|
10
|
+
private val PUBLISHED_CONFIG_NAMES = TRANSITIVE_DEPENDENCY_CONFIG_NAMES + "embed"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The configuration-level excludes that keep a dependency out of the brownfield module's build.
|
|
14
|
+
* The plugin reads what embedded libraries declare, not what Gradle resolves, so without these an
|
|
15
|
+
* excluded dependency would still be published and the host app would resolve it. `:app`'s rules
|
|
16
|
+
* are not read: they describe the standalone RN app, which may drop things the host needs.
|
|
17
|
+
*/
|
|
18
|
+
internal fun gradleExcludes(project: Project): List<GradleExclude> =
|
|
19
|
+
project.configurations
|
|
20
|
+
.filter { it.name in PUBLISHED_CONFIG_NAMES }
|
|
21
|
+
.flatMap { it.excludeRules }
|
|
22
|
+
.map { GradleExclude(it.group, it.module) }
|
|
23
|
+
.distinct()
|
|
24
|
+
|
|
25
|
+
/** One exclude rule. Gradle types both parts as non-null, but a rule without one leaves it null. */
|
|
26
|
+
internal data class GradleExclude(val group: String?, val module: String?) {
|
|
27
|
+
fun matches(
|
|
28
|
+
groupId: String,
|
|
29
|
+
artifactId: String,
|
|
30
|
+
) = (group == null || group == groupId) && (module == null || module == artifactId)
|
|
31
|
+
}
|
|
@@ -54,6 +54,8 @@ abstract class MergeClassesTask : DefaultTask() {
|
|
|
54
54
|
copyTask.from(mergedOutputDir)
|
|
55
55
|
copyTask.into(javacDir)
|
|
56
56
|
copyTask.exclude("META-INF/")
|
|
57
|
+
// Root module descriptor of a library's local jar, not a class of the AAR.
|
|
58
|
+
copyTask.exclude("module-info.class")
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
project.copy { copyTask ->
|
|
@@ -19,6 +19,7 @@ import org.gradle.api.publish.tasks.GenerateModuleMetadata
|
|
|
19
19
|
internal fun mutatePomDependenciesNode(
|
|
20
20
|
dependenciesNode: Node,
|
|
21
21
|
dependencies: VersionMediatingDependencySet,
|
|
22
|
+
excludes: List<GradleExclude> = emptyList(),
|
|
22
23
|
shouldExclude: (groupId: String, artifactId: String) -> Boolean,
|
|
23
24
|
) {
|
|
24
25
|
dependenciesNode.children()
|
|
@@ -49,6 +50,21 @@ internal fun mutatePomDependenciesNode(
|
|
|
49
50
|
childTags.forEach { (tagName, tagValue) ->
|
|
50
51
|
newDepNode.appendNode(tagName, tagValue)
|
|
51
52
|
}
|
|
53
|
+
appendExclusions(newDepNode, excludes)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun appendExclusions(
|
|
59
|
+
dependencyNode: Node,
|
|
60
|
+
excludes: List<GradleExclude>,
|
|
61
|
+
) {
|
|
62
|
+
if (excludes.isEmpty()) return
|
|
63
|
+
val exclusionsNode = dependencyNode.appendNode("exclusions")
|
|
64
|
+
excludes.forEach { exclude ->
|
|
65
|
+
exclusionsNode.appendNode("exclusion").apply {
|
|
66
|
+
appendNode("groupId", exclude.group ?: "*")
|
|
67
|
+
appendNode("artifactId", exclude.module ?: "*")
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
}
|
|
@@ -69,6 +85,7 @@ internal fun resolveOrCreateDependenciesNode(root: Node): Node =
|
|
|
69
85
|
internal fun mutateModuleJsonVariants(
|
|
70
86
|
json: Map<*, *>,
|
|
71
87
|
dependencies: VersionMediatingDependencySet,
|
|
88
|
+
excludes: List<GradleExclude> = emptyList(),
|
|
72
89
|
shouldExclude: (groupId: String, artifactId: String) -> Boolean,
|
|
73
90
|
) {
|
|
74
91
|
@Suppress("UNCHECKED_CAST")
|
|
@@ -98,6 +115,9 @@ internal fun mutateModuleJsonVariants(
|
|
|
98
115
|
dependencyToAdd.version?.let { version ->
|
|
99
116
|
put("version", mapOf("requires" to version))
|
|
100
117
|
}
|
|
118
|
+
if (excludes.isNotEmpty()) {
|
|
119
|
+
put("excludes", excludes.map { mapOf("group" to (it.group ?: "*"), "module" to (it.module ?: "*")) })
|
|
120
|
+
}
|
|
101
121
|
},
|
|
102
122
|
)
|
|
103
123
|
}
|
|
@@ -116,6 +136,7 @@ internal fun mutateModuleJsonVariants(
|
|
|
116
136
|
class PublishingMetadataInjector(private val project: Project) {
|
|
117
137
|
private var dependencies = VersionMediatingDependencySet()
|
|
118
138
|
private var shouldExclude: (groupId: String, artifactId: String) -> Boolean = { _, _ -> false }
|
|
139
|
+
private var excludes: List<GradleExclude> = emptyList()
|
|
119
140
|
private var enabled = false
|
|
120
141
|
|
|
121
142
|
init {
|
|
@@ -124,12 +145,16 @@ class PublishingMetadataInjector(private val project: Project) {
|
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
/** Supplies the dependencies to inject and enables injection on the hooks registered in [init]. */
|
|
127
|
-
fun configure(
|
|
148
|
+
internal fun configure(
|
|
128
149
|
dependencies: VersionMediatingDependencySet,
|
|
129
150
|
shouldExclude: (groupId: String, artifactId: String) -> Boolean,
|
|
151
|
+
excludes: List<GradleExclude>,
|
|
130
152
|
) {
|
|
131
153
|
this.dependencies = dependencies
|
|
132
154
|
this.shouldExclude = shouldExclude
|
|
155
|
+
// Gradle writes the module's excludes onto the dependencies it declares; these are injected
|
|
156
|
+
// later, so they need them too, or an excluded library could still arrive through one.
|
|
157
|
+
this.excludes = excludes
|
|
133
158
|
this.enabled = true
|
|
134
159
|
}
|
|
135
160
|
|
|
@@ -148,7 +173,7 @@ class PublishingMetadataInjector(private val project: Project) {
|
|
|
148
173
|
if (!moduleFile.exists()) return@forEach
|
|
149
174
|
|
|
150
175
|
val json = moduleFile.inputStream().use { JsonSlurper().parse(it) as Map<*, *> }
|
|
151
|
-
mutateModuleJsonVariants(json, dependencies, shouldExclude)
|
|
176
|
+
mutateModuleJsonVariants(json, dependencies, excludes, shouldExclude)
|
|
152
177
|
moduleFile.writer().use {
|
|
153
178
|
it.write(JsonOutput.prettyPrint(JsonOutput.toJson(json)))
|
|
154
179
|
}
|
|
@@ -172,7 +197,7 @@ class PublishingMetadataInjector(private val project: Project) {
|
|
|
172
197
|
|
|
173
198
|
val root = it.asNode()
|
|
174
199
|
val dependenciesNode = resolveOrCreateDependenciesNode(root)
|
|
175
|
-
mutatePomDependenciesNode(dependenciesNode, dependencies, shouldExclude)
|
|
200
|
+
mutatePomDependenciesNode(dependenciesNode, dependencies, excludes, shouldExclude)
|
|
176
201
|
}
|
|
177
202
|
}
|
|
178
203
|
}
|
|
@@ -3,6 +3,7 @@ package com.callstack.react.brownfield.utils
|
|
|
3
3
|
import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
|
|
4
4
|
import org.gradle.api.Project
|
|
5
5
|
import java.io.File
|
|
6
|
+
import javax.xml.parsers.DocumentBuilderFactory
|
|
6
7
|
|
|
7
8
|
class AndroidArchiveLibrary(
|
|
8
9
|
private val project: Project,
|
|
@@ -46,11 +47,47 @@ class AndroidArchiveLibrary(
|
|
|
46
47
|
val regex = Regex("""namespace\s*=?\s*["']([^"']+)["']""")
|
|
47
48
|
val match = regex.find(text)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
return match?.groupValues?.get(1)
|
|
51
|
+
?: getNameSpaceFromAndroidManifest(subProj)
|
|
52
|
+
?: error(
|
|
53
|
+
"No namespace found in ${buildFile.path}, and no package attribute found in " +
|
|
54
|
+
"AndroidManifest.xml for ${artifact.moduleName}",
|
|
55
|
+
)
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Falls back to the `package` attribute of the dependency's `AndroidManifest.xml` when the
|
|
60
|
+
* module declares no `namespace` in its `build.gradle`. The Android Gradle plugin itself
|
|
61
|
+
* derives the namespace this way for such libraries, so they build fine as a regular
|
|
62
|
+
* dependency but trip up [getNameSpaceFromBuildGradle]'s `build.gradle`-only regex.
|
|
63
|
+
*
|
|
64
|
+
* The manifest is parsed as XML and only the root `<manifest>` element's `package`
|
|
65
|
+
* attribute is read, so comments or child attributes such as `android:targetPackage`
|
|
66
|
+
* cannot be mistaken for the package name.
|
|
67
|
+
*/
|
|
68
|
+
private fun getNameSpaceFromAndroidManifest(subProj: Project): String? {
|
|
69
|
+
val candidateManifests =
|
|
70
|
+
listOf(
|
|
71
|
+
File(subProj.projectDir, "src/main/AndroidManifest.xml"),
|
|
72
|
+
File(subProj.projectDir, "AndroidManifest.xml"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return candidateManifests
|
|
76
|
+
.filter { it.exists() }
|
|
77
|
+
.firstNotNullOfOrNull { manifestFile ->
|
|
78
|
+
runCatching {
|
|
79
|
+
val document =
|
|
80
|
+
DocumentBuilderFactory.newInstance().apply {
|
|
81
|
+
isNamespaceAware = true
|
|
82
|
+
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
|
|
83
|
+
setFeature("http://xml.org/sax/features/external-general-entities", false)
|
|
84
|
+
setFeature("http://xml.org/sax/features/external-parameter-entities", false)
|
|
85
|
+
}.newDocumentBuilder().parse(manifestFile)
|
|
86
|
+
document.documentElement
|
|
87
|
+
?.getAttribute("package")
|
|
88
|
+
?.takeIf { it.isNotBlank() }
|
|
89
|
+
}.getOrNull()
|
|
90
|
+
}
|
|
54
91
|
}
|
|
55
92
|
|
|
56
93
|
fun getManifestFile() = File(getExplodedAarRootDir(), "AndroidManifest.xml")
|
package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt
CHANGED
|
@@ -14,7 +14,9 @@ open class Extension {
|
|
|
14
14
|
var appProjectName = "app"
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Whether to
|
|
17
|
+
* Whether to take the .so files from the app module's stripped or unstripped native output. It
|
|
18
|
+
* does not change which .so files are embedded. AGP still strips the AAR's native libs when it
|
|
19
|
+
* packages the library.
|
|
18
20
|
*
|
|
19
21
|
* Default is `true`.
|
|
20
22
|
*/
|
|
@@ -54,6 +56,17 @@ open class Extension {
|
|
|
54
56
|
*/
|
|
55
57
|
var ignoreEmbeddedLibs = listOf<String>()
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Exact .so file names, taken from the app module's native output, to embed in the AAR on top
|
|
61
|
+
* of the app's own libs and the embedded libraries' libs. Use it for a .so the host app cannot
|
|
62
|
+
* get any other way: one the app module builds or ships itself, or one from a dependency the
|
|
63
|
+
* published AAR does not declare.
|
|
64
|
+
*
|
|
65
|
+
* Provide in this format:
|
|
66
|
+
* listOf("libmmkv.so")
|
|
67
|
+
*/
|
|
68
|
+
var dynamicLibs = listOf<String>()
|
|
69
|
+
|
|
57
70
|
/**
|
|
58
71
|
* Whether to publish embedded native modules' third-party dependencies into the generated POM
|
|
59
72
|
* and Gradle Module Metadata, so consuming apps resolve them instead of declaring them by hand.
|
package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/TaskOutputs.kt
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.utils
|
|
2
|
+
|
|
3
|
+
import org.gradle.api.GradleException
|
|
4
|
+
import org.gradle.api.Project
|
|
5
|
+
import org.gradle.api.file.DirectoryProperty
|
|
6
|
+
import java.lang.reflect.Method
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Finds `taskName`'s `getterName()` that returns a `DirectoryProperty`. Resolving the task by name
|
|
10
|
+
* (rather than importing its class) keeps this plugin from depending on RNGP's/expo-updates'
|
|
11
|
+
* internal task types. If the task, or the property, isn't there, this fails the build with a
|
|
12
|
+
* clear message instead of silently producing an empty or stale directory.
|
|
13
|
+
*/
|
|
14
|
+
internal fun directoryGetter(
|
|
15
|
+
appProject: Project,
|
|
16
|
+
taskName: String,
|
|
17
|
+
getterName: String,
|
|
18
|
+
): Method {
|
|
19
|
+
val task =
|
|
20
|
+
appProject.tasks.findByName(taskName)
|
|
21
|
+
?: throw GradleException(
|
|
22
|
+
"brownfield: expected task '$taskName' was not found in project " +
|
|
23
|
+
"'${appProject.path}'. Check that the plugin producing it is applied there.",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
val getter =
|
|
27
|
+
runCatching { task.javaClass.getMethod(getterName) }.getOrElse {
|
|
28
|
+
throw GradleException(
|
|
29
|
+
"brownfield: task '$taskName' (${task.javaClass.name}) does not expose " +
|
|
30
|
+
"'$getterName()'. The plugin that registers it most likely changed its " +
|
|
31
|
+
"API; the brownfield Gradle plugin needs updating to match.",
|
|
32
|
+
it,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
check(DirectoryProperty::class.java.isAssignableFrom(getter.returnType)) {
|
|
37
|
+
"brownfield: '$taskName.$getterName()' no longer returns a DirectoryProperty."
|
|
38
|
+
}
|
|
39
|
+
return getter
|
|
40
|
+
}
|
|
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.io.TempDir
|
|
|
7
7
|
import org.junit.jupiter.params.ParameterizedTest
|
|
8
8
|
import org.junit.jupiter.params.provider.MethodSource
|
|
9
9
|
import java.io.File
|
|
10
|
+
import kotlin.test.assertFalse
|
|
10
11
|
import kotlin.test.assertTrue
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -39,12 +40,19 @@ class AgpCompatibilityMatrixTest {
|
|
|
39
40
|
// `--dry-run` still runs configuration in full — variant callbacks, source set wiring, task
|
|
40
41
|
// graph construction — which is where every AGP API the plugin touches is exercised. It just
|
|
41
42
|
// skips the task actions, which would need a real RN app to produce anything meaningful.
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
val result =
|
|
44
|
+
GradleRunner.create()
|
|
45
|
+
.withProjectDir(projectDir)
|
|
46
|
+
.withPluginClasspath(pluginClasspathFor(agpVersion))
|
|
47
|
+
.withArguments("assembleDebug", "--dry-run", "--stacktrace")
|
|
48
|
+
.forwardOutput()
|
|
49
|
+
.build()
|
|
50
|
+
|
|
51
|
+
// AGP 9 turns resValues off by default and warns on every read of variant.resValues.
|
|
52
|
+
assertFalse(
|
|
53
|
+
result.output.contains("Accessing value resValues"),
|
|
54
|
+
"the plugin reads variant.resValues although the feature is disabled",
|
|
55
|
+
)
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
@Test
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.plugin
|
|
2
|
+
|
|
3
|
+
import com.callstack.react.brownfield.shared.gradleExcludes
|
|
4
|
+
import org.gradle.testfixtures.ProjectBuilder
|
|
5
|
+
import org.junit.jupiter.api.Test
|
|
6
|
+
import org.junit.jupiter.api.io.TempDir
|
|
7
|
+
import java.io.File
|
|
8
|
+
import kotlin.test.assertFalse
|
|
9
|
+
import kotlin.test.assertTrue
|
|
10
|
+
|
|
11
|
+
class HardExcludePredicateTest {
|
|
12
|
+
@TempDir
|
|
13
|
+
lateinit var tempDir: File
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun `never publishes what the brownfield module excludes, or its own subprojects`() {
|
|
17
|
+
val root = ProjectBuilder.builder().withName("MyApp").withProjectDir(tempDir).build()
|
|
18
|
+
val lib = ProjectBuilder.builder().withName("brownfieldlib").withParent(root).build()
|
|
19
|
+
lib.configurations.create("implementation")
|
|
20
|
+
lib.configurations.all { it.exclude(mapOf("group" to "com.caverock", "module" to "androidsvg")) }
|
|
21
|
+
|
|
22
|
+
val excluded = hardExcludePredicate(lib, null, emptyList(), emptyList(), gradleExcludes(lib))
|
|
23
|
+
|
|
24
|
+
assertTrue(excluded("com.caverock", "androidsvg"))
|
|
25
|
+
assertTrue(excluded("MyApp", "react-native-screens"))
|
|
26
|
+
assertFalse(excluded("androidx.core", "core-ktx"))
|
|
27
|
+
}
|
|
28
|
+
}
|