mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-11 13:56:16 +00:00
merge matching and tests module with patcher, make builder api context aware and refactor
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.vanniktech.mavenPublish)
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
kotlin {
|
||||
@OptIn(ExperimentalAbiValidation::class)
|
||||
abiValidation {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
jvm()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.multidexlib2)
|
||||
implementation(libs.smali)
|
||||
implementation(project(":patcher"))
|
||||
}
|
||||
jvmTest.dependencies {
|
||||
implementation(libs.mockk)
|
||||
implementation(libs.kotlin.test)
|
||||
implementation(project(":tests"))
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xexplicit-backing-fields",
|
||||
"-Xcontext-parameters"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
named<Test>("jvmTest") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "githubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials(PasswordCredentials::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signAllPublications()
|
||||
extensions.getByType<SigningExtension>().useGpgCmd()
|
||||
|
||||
coordinates(group.toString(), project.name, version.toString())
|
||||
|
||||
pom {
|
||||
name = "ReVanced Patcher Matching API"
|
||||
description = "Matching API used by ReVanced."
|
||||
inceptionYear = "2022"
|
||||
url = "https://revanced.app"
|
||||
licenses {
|
||||
license {
|
||||
name = "GNU General Public License v3.0"
|
||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = "ReVanced"
|
||||
name = "ReVanced"
|
||||
email = "contact@revanced.app"
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
|
||||
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
|
||||
url = "https://github.com/revanced/revanced-patcher"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,12 +50,14 @@ kotlin {
|
||||
jvmTest.dependencies {
|
||||
implementation(libs.mockk)
|
||||
implementation(libs.kotlin.test)
|
||||
implementation(project(":tests"))
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs = listOf("-Xcontext-parameters")
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xexplicit-backing-fields",
|
||||
"-Xcontext-parameters"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,14 @@ class Fingerprint internal constructor(
|
||||
) {
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
// Backing field needed for lazy initialization.
|
||||
private var _matchOrNull: Match? = null
|
||||
private var _matchOrNull: FingerprintMatch? = null
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
private val matchOrNull: Match?
|
||||
private val matchOrNull: FingerprintMatch?
|
||||
get() = matchOrNull()
|
||||
|
||||
context(context: BytecodePatchContext)
|
||||
internal fun matchOrNull(): Match? {
|
||||
internal fun matchOrNull(): FingerprintMatch? {
|
||||
if (_matchOrNull != null) return _matchOrNull
|
||||
|
||||
var match = strings?.mapNotNull {
|
||||
@@ -57,7 +57,7 @@ class Fingerprint internal constructor(
|
||||
context(_: BytecodePatchContext)
|
||||
fun matchOrNull(
|
||||
classDef: ClassDef,
|
||||
): Match? {
|
||||
): FingerprintMatch? {
|
||||
if (_matchOrNull != null) return _matchOrNull
|
||||
|
||||
for (method in classDef.methods) {
|
||||
@@ -77,7 +77,7 @@ class Fingerprint internal constructor(
|
||||
fun matchOrNull(
|
||||
method: Method,
|
||||
classDef: ClassDef,
|
||||
): Match? {
|
||||
): FingerprintMatch? {
|
||||
if (_matchOrNull != null) return _matchOrNull
|
||||
|
||||
if (returnType != null && !method.returnType.startsWith(returnType)) {
|
||||
@@ -109,7 +109,7 @@ class Fingerprint internal constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
val stringMatches: List<Match.StringMatch>? =
|
||||
val stringMatches: List<FingerprintMatch.StringMatch>? =
|
||||
if (strings != null) {
|
||||
buildList {
|
||||
val instructions = method.instructionsOrNull ?: return null
|
||||
@@ -128,7 +128,7 @@ class Fingerprint internal constructor(
|
||||
val index = stringsList.indexOfFirst(string::contains)
|
||||
if (index == -1) return@forEachIndexed
|
||||
|
||||
add(Match.StringMatch(string, instructionIndex))
|
||||
add(FingerprintMatch.StringMatch(string, instructionIndex))
|
||||
stringsList.removeAt(index)
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class Fingerprint internal constructor(
|
||||
val patternMatch = if (opcodes != null) {
|
||||
val instructions = method.instructionsOrNull ?: return null
|
||||
|
||||
fun patternScan(): Match.PatternMatch? {
|
||||
fun patternScan(): FingerprintMatch.PatternMatch? {
|
||||
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
|
||||
|
||||
val instructionLength = instructions.count()
|
||||
@@ -168,7 +168,7 @@ class Fingerprint internal constructor(
|
||||
}
|
||||
|
||||
// The entire pattern has been scanned.
|
||||
return Match.PatternMatch(
|
||||
return FingerprintMatch.PatternMatch(
|
||||
index,
|
||||
index + patternIndex,
|
||||
)
|
||||
@@ -183,7 +183,7 @@ class Fingerprint internal constructor(
|
||||
null
|
||||
}
|
||||
|
||||
_matchOrNull = Match(
|
||||
_matchOrNull = FingerprintMatch(
|
||||
context,
|
||||
classDef,
|
||||
method,
|
||||
@@ -266,7 +266,7 @@ class Fingerprint internal constructor(
|
||||
}
|
||||
|
||||
@Deprecated("Use the matcher API instead.")
|
||||
class Match internal constructor(
|
||||
class FingerprintMatch internal constructor(
|
||||
val context: BytecodePatchContext,
|
||||
val originalClassDef: ClassDef,
|
||||
val originalMethod: Method,
|
||||
|
||||
@@ -97,12 +97,12 @@ fun BytecodePatchContext.firstMethodOrNull(
|
||||
fun BytecodePatchContext.firstMethod(
|
||||
vararg strings: String,
|
||||
predicate: MethodPredicate = { true },
|
||||
) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate))
|
||||
) = requireNotNull(firstMethodOrNull(strings = strings, predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableOrNull(
|
||||
vararg strings: String,
|
||||
predicate: MethodPredicate = { true },
|
||||
) = firstMethodOrNull(*strings, predicate = predicate)?.let { method ->
|
||||
) = firstMethodOrNull(strings = strings, predicate)?.let { method ->
|
||||
firstClassDefMutable(method.definingClass).methods.first {
|
||||
MethodUtil.methodSignaturesMatch(method, it)
|
||||
}
|
||||
@@ -110,7 +110,7 @@ fun BytecodePatchContext.firstMethodMutableOrNull(
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutable(
|
||||
vararg strings: String, predicate: MethodPredicate = { true }
|
||||
) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate))
|
||||
) = requireNotNull(firstMethodMutableOrNull(strings = strings, predicate))
|
||||
|
||||
fun gettingFirstClassDefOrNull(
|
||||
type: String? = null, predicate: ClassDefPredicate = { true }
|
||||
@@ -131,22 +131,22 @@ fun gettingFirstClassDefMutable(
|
||||
fun gettingFirstMethodOrNull(
|
||||
vararg strings: String,
|
||||
predicate: MethodPredicate = { true },
|
||||
) = cachedReadOnlyProperty { firstMethodOrNull(*strings, predicate = predicate) }
|
||||
) = cachedReadOnlyProperty { firstMethodOrNull(strings = strings, predicate) }
|
||||
|
||||
fun gettingFirstMethod(
|
||||
vararg strings: String,
|
||||
predicate: MethodPredicate = { true },
|
||||
) = cachedReadOnlyProperty { firstMethod(*strings, predicate = predicate) }
|
||||
) = cachedReadOnlyProperty { firstMethod(strings = strings, predicate) }
|
||||
|
||||
fun gettingFirstMethodMutableOrNull(
|
||||
vararg strings: String,
|
||||
predicate: MethodPredicate = { true },
|
||||
) = cachedReadOnlyProperty { firstMethodMutableOrNull(*strings, predicate = predicate) }
|
||||
) = cachedReadOnlyProperty { firstMethodMutableOrNull(strings = strings, predicate) }
|
||||
|
||||
fun gettingFirstMethodMutable(
|
||||
vararg strings: String,
|
||||
predicate: MethodPredicate = { true },
|
||||
) = cachedReadOnlyProperty { firstMethodMutable(*strings, predicate = predicate) }
|
||||
) = cachedReadOnlyProperty { firstMethodMutable(strings = strings, predicate) }
|
||||
|
||||
class PredicateContext internal constructor() : MutableMap<Any, Any> by mutableMapOf()
|
||||
|
||||
@@ -373,22 +373,22 @@ fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
|
||||
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
|
||||
vararg strings: String,
|
||||
predicate: DeclarativeMethodPredicate = { }
|
||||
) = firstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
|
||||
) = firstMethodOrNull(strings = strings) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
|
||||
vararg strings: String,
|
||||
predicate: DeclarativeMethodPredicate = { }
|
||||
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate))
|
||||
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(strings = strings, predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
|
||||
vararg strings: String,
|
||||
predicate: DeclarativeMethodPredicate = { }
|
||||
) = firstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
|
||||
) = firstMethodMutableOrNull(strings = strings) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
|
||||
vararg strings: String,
|
||||
predicate: DeclarativeMethodPredicate = { }
|
||||
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate))
|
||||
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(strings = strings, predicate))
|
||||
|
||||
fun gettingFirstClassDefByDeclarativePredicateOrNull(
|
||||
type: String? = null,
|
||||
@@ -413,22 +413,22 @@ fun gettingFirstClassDefMutableByDeclarativePredicate(
|
||||
fun gettingFirstMethodByDeclarativePredicateOrNull(
|
||||
vararg strings: String,
|
||||
predicate: DeclarativeMethodPredicate = { }
|
||||
) = gettingFirstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
|
||||
) = gettingFirstMethodOrNull(strings = strings) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstMethodByDeclarativePredicate(
|
||||
vararg strings: String,
|
||||
predicate: DeclarativeMethodPredicate = { }
|
||||
) = cachedReadOnlyProperty { firstMethodByDeclarativePredicate(*strings, predicate = predicate) }
|
||||
) = cachedReadOnlyProperty { firstMethodByDeclarativePredicate(strings = strings, predicate) }
|
||||
|
||||
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
|
||||
vararg strings: String,
|
||||
predicate: DeclarativeMethodPredicate = { }
|
||||
) = gettingFirstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
|
||||
) = gettingFirstMethodMutableOrNull(strings = strings) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstMethodMutableByDeclarativePredicate(
|
||||
vararg strings: String,
|
||||
predicate: DeclarativeMethodPredicate = { }
|
||||
) = cachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(*strings, predicate = predicate) }
|
||||
) = cachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(strings = strings, predicate) }
|
||||
|
||||
context(list: MutableList<T.() -> Boolean>)
|
||||
fun <T> allOf(block: MutableList<T.() -> Boolean>.() -> Unit) {
|
||||
@@ -453,21 +453,9 @@ fun <T> all(target: T): Boolean = list.all { target.it() }
|
||||
context(list: MutableList<T.() -> Boolean>)
|
||||
fun <T> any(target: T): Boolean = list.any { target.it() }
|
||||
|
||||
fun firstMethodBuilder(
|
||||
vararg strings: String,
|
||||
builder:
|
||||
context(PredicateContext, MutableList<Method.() -> Boolean>, IndexedMatcher<Instruction>, MutableList<String>) () -> Unit
|
||||
) = with(mutableListOf<String>()) stringsList@{
|
||||
addAll(strings)
|
||||
|
||||
with(indexedMatcher<Instruction>()) {
|
||||
Match(indices = indices, strings = this@stringsList) { builder() }
|
||||
}
|
||||
}
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>)
|
||||
fun accessFlags(vararg flags: AccessFlags) =
|
||||
predicate { accessFlags(*flags) }
|
||||
predicate { accessFlags(flags = flags) }
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>)
|
||||
fun returnType(
|
||||
@@ -624,22 +612,39 @@ fun noneOf(
|
||||
predicates.none { predicate -> predicate(currentIndex, lastMatchedIndex) }
|
||||
}
|
||||
|
||||
class Match internal constructor(
|
||||
val indices: List<Int>,
|
||||
val strings: List<String>,
|
||||
private val predicate: DeclarativeMethodPredicate
|
||||
fun firstMethodBuilder(
|
||||
vararg strings: String,
|
||||
builder:
|
||||
context(PredicateContext, MutableList<Method.() -> Boolean>, IndexedMatcher<Instruction>, MutableList<String>)() -> Unit
|
||||
) = Match(strings = strings, builder)
|
||||
|
||||
class Match private constructor(
|
||||
private val strings: MutableList<String>,
|
||||
indexedMatcher: IndexedMatcher<Instruction> = indexedMatcher(),
|
||||
build: context(
|
||||
PredicateContext, MutableList<Method.() -> Boolean>,
|
||||
IndexedMatcher<Instruction>, MutableList<String>) () -> Unit
|
||||
) {
|
||||
private var _methodOrNull: MutableMethod? = null
|
||||
internal constructor(
|
||||
vararg strings: String,
|
||||
builder: context(
|
||||
PredicateContext, MutableList<Method.() -> Boolean>,
|
||||
IndexedMatcher<Instruction>, MutableList<String>) () -> Unit
|
||||
) : this(strings = mutableListOf(elements = strings), build = builder)
|
||||
|
||||
private val methodOrNullMap = HashMap<BytecodePatchContext, MutableMethod?>(1)
|
||||
|
||||
private val predicate: DeclarativeMethodPredicate = context(strings, indexedMatcher) { { build() } }
|
||||
|
||||
context(context: BytecodePatchContext)
|
||||
val methodOrNull: MutableMethod?
|
||||
get() = _methodOrNull ?: run {
|
||||
_methodOrNull = if (strings.isEmpty())
|
||||
context.firstMethodMutableByDeclarativePredicateOrNull(predicate = predicate)
|
||||
else
|
||||
context.firstMethodMutableByDeclarativePredicateOrNull(strings = strings.toTypedArray(), predicate)
|
||||
|
||||
_methodOrNull
|
||||
val methodOrNull: MutableMethod?
|
||||
get() = if (context in methodOrNullMap) methodOrNullMap[context]
|
||||
else methodOrNullMap.getOrPut(context) {
|
||||
context.firstMethodMutableByDeclarativePredicateOrNull(
|
||||
strings = strings.toTypedArray(),
|
||||
predicate
|
||||
)
|
||||
}
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
@@ -650,4 +655,6 @@ class Match internal constructor(
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val classDef get() = requireNotNull(classDefOrNull)
|
||||
|
||||
val indices = indexedMatcher.indices
|
||||
}
|
||||
@@ -236,7 +236,7 @@ by patchesByFile.values.flatten().toSet()
|
||||
@Suppress("MISSING_DEPENDENCY_IN_INFERRED_TYPE_ANNOTATION_WARNING")
|
||||
internal fun getPatches(classNames: List<String>, classLoader: ClassLoader): Set<Patch> {
|
||||
fun Member.isUsable() =
|
||||
Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && (this !is Method || parameterCount != 0)
|
||||
Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && (this !is Method || parameterCount == 0)
|
||||
|
||||
fun Class<*>.getPatchFields() = fields
|
||||
.filter { it.type.isPatch && it.isUsable() }
|
||||
|
||||
@@ -19,7 +19,7 @@ internal class FingerprintTest : PatcherTestBase() {
|
||||
"Fingerprints should match correctly."
|
||||
)
|
||||
assertNull(
|
||||
fingerprint { returns("doesnt exist") }.originalMethodOrNull,
|
||||
fingerprint { returns("does not exist") }.originalMethodOrNull,
|
||||
"Fingerprints should match correctly."
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ object MatchingTest : PatcherTestBase() {
|
||||
fun setup() = setupMock()
|
||||
|
||||
@Test
|
||||
fun `matches via builder api`() {
|
||||
fun `finds via builder api`() {
|
||||
fun firstMethodBuilder(fail: Boolean = false) = firstMethodBuilder {
|
||||
name("method")
|
||||
definingClass("class")
|
||||
@@ -45,7 +45,7 @@ object MatchingTest : PatcherTestBase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `matches via declarative api`() {
|
||||
fun `finds via declarative api`() {
|
||||
bytecodePatch {
|
||||
apply {
|
||||
val method = firstMethodByDeclarativePredicateOrNull {
|
||||
@@ -64,7 +64,7 @@ object MatchingTest : PatcherTestBase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `predicate matcher works correctly`() {
|
||||
fun `predicate api works correctly`() {
|
||||
bytecodePatch {
|
||||
apply {
|
||||
assertDoesNotThrow("Should find method") { firstMethod { name == "method" } }
|
||||
@@ -77,15 +77,4 @@ internal class PatcherTest : PatcherTestBase() {
|
||||
"afterDependents of a patch should be called " +
|
||||
"regardless of dependant patches failing."
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws if unmatched fingerprint match is used`() {
|
||||
with(bytecodePatchContext) {
|
||||
val fingerprint = fingerprint { strings("doesnt exist") }
|
||||
|
||||
assertThrows<PatchException>("Expected an exception because the fingerprint can't match.") {
|
||||
fingerprint.patternMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,9 @@ abstract class PatcherTestBase {
|
||||
every { this@bytecodePatchContext.getProperty("apkFile") } returns mockk<File>()
|
||||
|
||||
every { this@bytecodePatchContext.classDefs } returns ClassDefs().apply {
|
||||
invokePrivateMethod($$"initializeCache$patcher")
|
||||
javaClass.getDeclaredMethod($$"initializeCache$patcher").apply {
|
||||
isAccessible = true
|
||||
}.invoke(this)
|
||||
}
|
||||
|
||||
every { get() } returns emptySet()
|
||||
@@ -100,16 +102,4 @@ abstract class PatcherTestBase {
|
||||
}
|
||||
|
||||
protected operator fun Patch.invoke() = setOf(this)()
|
||||
|
||||
private fun Any.setPrivateField(field: String, value: Any) {
|
||||
this::class.java.getDeclaredField(field).apply {
|
||||
this.isAccessible = true
|
||||
set(this@setPrivateField, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any.invokePrivateMethod(method: String) =
|
||||
javaClass.getDeclaredMethod(method).apply {
|
||||
isAccessible = true
|
||||
}.invoke(this)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.patch.PatchBuilder.invoke
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
@@ -41,7 +41,7 @@ internal class SmaliTest {
|
||||
""",
|
||||
)
|
||||
|
||||
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(0).target.location.index
|
||||
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(1).target.location.index
|
||||
|
||||
assertEquals(0, targetLocationIndex, "Label should point to index 0")
|
||||
}
|
||||
|
||||
@@ -22,5 +22,3 @@ dependencyResolutionManagement {
|
||||
}
|
||||
|
||||
include(":patcher")
|
||||
include(":matching")
|
||||
include(":tests")
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import com.android.build.api.dsl.androidLibrary
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.multidexlib2)
|
||||
implementation(libs.smali)
|
||||
implementation(project(":patcher"))
|
||||
implementation(libs.mockk)
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user