mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-28 16:50:09 +08:00
Merge remote-tracking branch 'mirai/dev' into dev
This commit is contained in:
commit
5f02c35e66
@ -14,6 +14,8 @@ git submodule init
|
|||||||
git submodule update
|
git submodule update
|
||||||
```
|
```
|
||||||
|
|
||||||
|
项目首次初始化和构建可能要花费较长时间。
|
||||||
|
|
||||||
- 要构建项目, 请运行 `gradlew assemble`
|
- 要构建项目, 请运行 `gradlew assemble`
|
||||||
- 要运行测试, 请运行 `gradlew test`
|
- 要运行测试, 请运行 `gradlew test`
|
||||||
- 要构建项目并运行测试, 请运行 `gradlew build`
|
- 要构建项目并运行测试, 请运行 `gradlew build`
|
||||||
@ -29,9 +31,9 @@ git submodule update
|
|||||||
|
|
||||||
### 能做什么?
|
### 能做什么?
|
||||||
|
|
||||||
- 维护社区: 可以为 [mirai-console](https://github.com/mamoe/mirai-console) 编写插件, 并发布到 discussions
|
- 维护社区: 可以为 [mirai-console](https://github.com/mamoe/mirai-console) 编写插件, 并发布到论坛
|
||||||
|
|
||||||
- 代码优化: 优化任何功能设计或实现, 或是引入一个新的设计(请先通过 issues 或 discussions 与维护者达成共识)
|
- 代码优化: 优化任何功能设计或实现, 或是引入一个新的设计
|
||||||
- 解决问题: 在 [issues](https://github.com/mamoe/mirai/issues) 查看 mirai 正遇到的所有问题, 或在 [里程碑](https://github.com/mamoe/mirai/milestones) 查看版本计划. 所有没有 assignee 的 issue 都处于
|
- 解决问题: 在 [issues](https://github.com/mamoe/mirai/issues) 查看 mirai 正遇到的所有问题, 或在 [里程碑](https://github.com/mamoe/mirai/milestones) 查看版本计划. 所有没有 assignee 的 issue 都处于
|
||||||
- 协议支持: [添加新协议支持](#添加协议支持)
|
- 协议支持: [添加新协议支持](#添加协议支持)
|
||||||
|
|
||||||
@ -46,10 +48,16 @@ git submodule update
|
|||||||
请查看 [PacketFactory.kt](mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt) 了解网络层架构.
|
请查看 [PacketFactory.kt](mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt) 了解网络层架构.
|
||||||
参考现有的 `PacketFactory` 实现和一些有关协议的 PR (带有 `protocol` 标签) 了解如何添加新的 `PacketFactory`.
|
参考现有的 `PacketFactory` 实现和一些有关协议的 PR (带有 `protocol` 标签) 了解如何添加新的 `PacketFactory`.
|
||||||
|
|
||||||
> 如果你不熟悉 Kotlin 或不熟练 Kotlin 也没关系, 你的 PR 会首先被维护者审阅并会收到修改建议. mirai 感谢你的每一行代码并会尽可能帮助你.
|
|
||||||
|
|
||||||
|
### 开发 mirai-core
|
||||||
|
|
||||||
### 注意事项
|
- 使用 IntelliJ IDEA 或 Android Studio
|
||||||
|
- 安装 IDE 插件 [kotlin-jvm-blocking-bridge](https://github.com/Him188/kotlin-jvm-blocking-bridge/blob/master/README-chs.md#%E5%AE%89%E8%A3%85-intellij-idea-%E6%88%96-android-studio-%E6%8F%92%E4%BB%B6)
|
||||||
|
- 在 mirai-core 和 mirai-core-api 使用纯 Kotlin 实现
|
||||||
- 尽量不要引用新的库
|
- 尽量不要引用新的库
|
||||||
- 遵守 Kotlin 官方代码规范(提交前使用 IDE 格式化代码 (commit 时勾选 'Reformat code'))
|
- 遵守 Kotlin 官方代码规范(提交前使用 IDE 格式化代码 (commit 时勾选 'Reformat code'))
|
||||||
- 保证二进制兼容性: 在提交前执行 `gradlew build`, 若有不兼容变更会得到错误. 在提交时将 `binary-compatibility-validator.api` 一并提交 (如果有修改). (使用 [Kotlin/binary-compatibility-validator](https://github.com/Kotlin/binary-compatibility-validator))
|
- 保证二进制兼容性: 在提交前执行 `gradlew build`, 若有不兼容变更会得到错误. 在提交时将 `binary-compatibility-validator.api` 一并提交 (如果有修改). (使用 [Kotlin/binary-compatibility-validator](https://github.com/Kotlin/binary-compatibility-validator))
|
||||||
|
- 通过 GitHub 的 Pull Request 提交代码,很快就会有相关模块负责人员来审核
|
||||||
|
|
||||||
|
|
||||||
|
如果你不太保证自己能达到上述要求也没关系,mirai 感谢你的每一行代码,维护者会审核代码并尽可能帮助你。
|
||||||
|
@ -69,6 +69,8 @@ tasks.register("publishMiraiCoreArtifactsToMavenLocal") {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
analyzes.CompiledCodeVerify.run { registerAllVerifyTasks() }
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "net.mamoe"
|
group = "net.mamoe"
|
||||||
version = Versions.project
|
version = Versions.project
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
package androidutil
|
package analyzes
|
||||||
|
|
||||||
import groovy.util.Node
|
import groovy.util.Node
|
||||||
import groovy.util.XmlParser
|
import groovy.util.XmlParser
|
||||||
@ -20,7 +20,6 @@ import org.objectweb.asm.tree.ClassNode
|
|||||||
import org.objectweb.asm.tree.FieldInsnNode
|
import org.objectweb.asm.tree.FieldInsnNode
|
||||||
import org.objectweb.asm.tree.MethodInsnNode
|
import org.objectweb.asm.tree.MethodInsnNode
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
object AndroidApiLevelCheck {
|
object AndroidApiLevelCheck {
|
||||||
data class ClassInfo(
|
data class ClassInfo(
|
||||||
@ -260,11 +259,7 @@ object AndroidApiLevelCheck {
|
|||||||
.filter { it.isFile && it.extension == "class" }
|
.filter { it.isFile && it.extension == "class" }
|
||||||
.map { file ->
|
.map { file ->
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val cnode = ClassNode()
|
AsmUtil.run { file.readClass() }
|
||||||
file.inputStream().use {
|
|
||||||
ClassReader(it).accept(cnode, 0)
|
|
||||||
}
|
|
||||||
cnode
|
|
||||||
}.getOrNull() to file
|
}.getOrNull() to file
|
||||||
}
|
}
|
||||||
.filter { it.first != null }
|
.filter { it.first != null }
|
105
buildSrc/src/main/kotlin/analyzes/AsmUtil.kt
Normal file
105
buildSrc/src/main/kotlin/analyzes/AsmUtil.kt
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||||
|
*
|
||||||
|
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||||
|
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||||
|
*
|
||||||
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package analyzes
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader
|
||||||
|
import org.objectweb.asm.Opcodes
|
||||||
|
import org.objectweb.asm.tree.ClassNode
|
||||||
|
import org.objectweb.asm.tree.FieldNode
|
||||||
|
import org.objectweb.asm.tree.MethodNode
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
typealias AsmClasses = Map<String, ClassNode>
|
||||||
|
typealias AsmClassesM = MutableMap<String, ClassNode>
|
||||||
|
|
||||||
|
object AsmUtil {
|
||||||
|
fun ClassNode.getMethod(name: String, desc: String, isStatic: Boolean): MethodNode? {
|
||||||
|
return methods?.firstOrNull {
|
||||||
|
it.name == name && it.desc == desc && ((it.access and Opcodes.ACC_STATIC) != 0) == isStatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ClassNode.getField(name: String, desc: String, isStatic: Boolean): FieldNode? {
|
||||||
|
return fields?.firstOrNull {
|
||||||
|
it.name == name && it.desc == desc && ((it.access and Opcodes.ACC_STATIC) != 0) == isStatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun File.readClass(): ClassNode = inputStream().use { it.readClass() }
|
||||||
|
|
||||||
|
fun InputStream.readClass(): ClassNode {
|
||||||
|
val cnode = ClassNode()
|
||||||
|
ClassReader(this).accept(cnode, 0)
|
||||||
|
return cnode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AsmClassesM.patchJvmClass(owner: String) {
|
||||||
|
if (owner.startsWith("java/") || owner.startsWith("javax/")) {
|
||||||
|
if (!this.containsKey(owner)) {
|
||||||
|
ClassLoader.getSystemClassLoader().getResourceAsStream("$owner.class")?.use {
|
||||||
|
val c = it.readClass()
|
||||||
|
this[c.name] = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AsmClassesM.hasField(
|
||||||
|
owner: String,
|
||||||
|
name: String,
|
||||||
|
desc: String,
|
||||||
|
opcode: Int
|
||||||
|
): Boolean {
|
||||||
|
patchJvmClass(owner)
|
||||||
|
val c = this[owner] ?: return false
|
||||||
|
val isStatic = opcode == Opcodes.GETSTATIC || opcode == Opcodes.PUTSTATIC
|
||||||
|
if (c.getField(name, desc, isStatic) != null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (isStatic) return false
|
||||||
|
return hasField(c.superName ?: "", name, desc, opcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AsmClassesM.hasMethod(
|
||||||
|
owner: String,
|
||||||
|
name: String,
|
||||||
|
desc: String,
|
||||||
|
opcode: Int
|
||||||
|
): Boolean {
|
||||||
|
patchJvmClass(owner)
|
||||||
|
when (opcode) {
|
||||||
|
Opcodes.INVOKESTATIC -> {
|
||||||
|
val c = this[owner] ?: return false
|
||||||
|
return c.getMethod(name, desc, true) != null
|
||||||
|
}
|
||||||
|
Opcodes.INVOKEINTERFACE,
|
||||||
|
Opcodes.INVOKESPECIAL,
|
||||||
|
Opcodes.INVOKEVIRTUAL -> {
|
||||||
|
fun loopFind(current: String): Boolean {
|
||||||
|
patchJvmClass(current)
|
||||||
|
val c = this[current] ?: return false
|
||||||
|
if (c.getMethod(name, desc, false) != null) return true
|
||||||
|
c.superName?.let {
|
||||||
|
if (loopFind(it)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.interfaces?.forEach {
|
||||||
|
if (loopFind(it)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return loopFind(owner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
90
buildSrc/src/main/kotlin/analyzes/CompiledCodeVerify.kt
Normal file
90
buildSrc/src/main/kotlin/analyzes/CompiledCodeVerify.kt
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||||
|
*
|
||||||
|
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||||
|
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||||
|
*
|
||||||
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package analyzes
|
||||||
|
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
typealias VerifyAction = (classes: Sequence<File>, libraries: Sequence<File>) -> Unit
|
||||||
|
|
||||||
|
data class ProjectInfo(val isMpp: Boolean, val name: String)
|
||||||
|
|
||||||
|
fun JvmProjectInfo(name: String) = ProjectInfo(false, name)
|
||||||
|
fun MppProjectInfo(name: String) = ProjectInfo(true, name)
|
||||||
|
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
object CompiledCodeVerify {
|
||||||
|
|
||||||
|
private const val RUN_ALL_VERITY_TASK_NAME = "runAllVerify"
|
||||||
|
private const val VERIFICATION_GROUP_NAME = "verification"
|
||||||
|
|
||||||
|
private val projectInfos = listOf(
|
||||||
|
MppProjectInfo("mirai-core-api"), MppProjectInfo("mirai-core-utils"),
|
||||||
|
JvmProjectInfo("mirai-console"), JvmProjectInfo("mirai-console-terminal")
|
||||||
|
).associateBy { it.name }
|
||||||
|
|
||||||
|
private val ProjectInfo.compileTasks: Array<String>
|
||||||
|
get() = if (isMpp) {
|
||||||
|
arrayOf(":$name:jvmMainClasses", ":$name:androidMainClasses")
|
||||||
|
} else arrayOf(":$name:classes")
|
||||||
|
|
||||||
|
private fun getCompiledClassesPath(project: Project, info: ProjectInfo): Sequence<Sequence<File>> =
|
||||||
|
if (info.isMpp) {
|
||||||
|
sequenceOf("kotlin/jvm/main", "kotlin/android/main")
|
||||||
|
} else {
|
||||||
|
sequenceOf("kotlin/main")
|
||||||
|
}.map { sequenceOf(project.buildDir.resolve("classes").resolve(it)) }
|
||||||
|
|
||||||
|
private fun getLibraries(project: Project, info: ProjectInfo): Sequence<Sequence<File>> =
|
||||||
|
if (info.isMpp) {
|
||||||
|
sequenceOf("jvmCompileClasspath", "androidCompileClasspath")
|
||||||
|
} else {
|
||||||
|
sequenceOf("compileClasspath")
|
||||||
|
}.map { project.configurations.getByName(it).files.asSequence() }
|
||||||
|
|
||||||
|
fun Project.registerVerifyTask(taskName: String, action: VerifyAction) {
|
||||||
|
|
||||||
|
val projectInfo = projectInfos[this.name] ?: error("Project info of $name not found")
|
||||||
|
|
||||||
|
tasks.register(taskName) {
|
||||||
|
group = VERIFICATION_GROUP_NAME
|
||||||
|
mustRunAfter(*projectInfo.compileTasks)
|
||||||
|
|
||||||
|
doFirst {
|
||||||
|
getCompiledClassesPath(project, projectInfo).zip(getLibraries(project, projectInfo))
|
||||||
|
.forEach { (compiledClasses, libraries) ->
|
||||||
|
action(compiledClasses, libraries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("check").configure { dependsOn(taskName) }
|
||||||
|
rootProject.tasks.getByName(RUN_ALL_VERITY_TASK_NAME).dependsOn(":$name:$taskName")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Project.registerVerifyTasks() { // for feature extends
|
||||||
|
// https://github.com/mamoe/mirai/pull/1080#issuecomment-801197312
|
||||||
|
if (name != "mirai-console") {
|
||||||
|
registerVerifyTask("verify_NoNoSuchMethodError", NoSuchMethodAnalyzer::check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Project/*RootProject*/.registerAllVerifyTasks() {
|
||||||
|
tasks.register(RUN_ALL_VERITY_TASK_NAME) {
|
||||||
|
group = VERIFICATION_GROUP_NAME
|
||||||
|
}
|
||||||
|
projectInfos.keys.forEach { projectName ->
|
||||||
|
findProject(projectName)?.let { subProject ->
|
||||||
|
subProject.afterEvaluate { subProject.registerVerifyTasks() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
90
buildSrc/src/main/kotlin/analyzes/NoSuchMethodAnalyzer.kt
Normal file
90
buildSrc/src/main/kotlin/analyzes/NoSuchMethodAnalyzer.kt
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||||
|
*
|
||||||
|
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||||
|
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||||
|
*
|
||||||
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package analyzes
|
||||||
|
|
||||||
|
import analyzes.AsmUtil.hasField
|
||||||
|
import analyzes.AsmUtil.hasMethod
|
||||||
|
import org.objectweb.asm.tree.FieldInsnNode
|
||||||
|
import org.objectweb.asm.tree.MethodInsnNode
|
||||||
|
import org.objectweb.asm.tree.MethodNode
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
object NoSuchMethodAnalyzer {
|
||||||
|
private fun analyzeMethod(
|
||||||
|
analyzer: AndroidApiLevelCheck.Analyzer,
|
||||||
|
method: MethodNode,
|
||||||
|
asmClasses: AsmClassesM
|
||||||
|
) {
|
||||||
|
analyzer.withContext("Analyze ${method.name}${method.desc}") {
|
||||||
|
method.instructions?.forEach { insn ->
|
||||||
|
when (insn) {
|
||||||
|
is MethodInsnNode -> {
|
||||||
|
if (insn.owner.startsWith("net/mamoe/mirai/")) {
|
||||||
|
if (!asmClasses.hasMethod(insn.owner, insn.name, insn.desc, insn.opcode)) {
|
||||||
|
report(
|
||||||
|
"No such method",
|
||||||
|
"${insn.owner}.${insn.name}${insn.desc}, opcode=${insn.opcode}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FieldInsnNode -> {
|
||||||
|
if (insn.owner.startsWith("net/mamoe/mirai/")) {
|
||||||
|
if (!asmClasses.hasField(insn.owner, insn.name, insn.desc, insn.opcode)) {
|
||||||
|
report(
|
||||||
|
"No such field",
|
||||||
|
"${insn.owner}.${insn.name}: ${insn.desc}, opcode=${insn.opcode}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun check(classes: Sequence<File>, libs: Sequence<File>) = AsmUtil.run {
|
||||||
|
val analyzer = AndroidApiLevelCheck.Analyzer(emptyMap())
|
||||||
|
val asmClasses: AsmClassesM = mutableMapOf()
|
||||||
|
libs.forEach { lib ->
|
||||||
|
if (lib.name.endsWith(".jar")) {
|
||||||
|
ZipFile(lib).use { zip ->
|
||||||
|
zip.entries().iterator().forEach l@{ entry ->
|
||||||
|
if (entry.isDirectory) return@l
|
||||||
|
if (!entry.name.endsWith(".class")) return@l
|
||||||
|
zip.getInputStream(entry).use { it.readClass() }.let {
|
||||||
|
asmClasses[it.name] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (lib.isDirectory) {
|
||||||
|
lib.walk().filter { it.isFile && it.extension == "class" }.forEach { f ->
|
||||||
|
f.readClass().let { asmClasses[it.name] = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
classes.map { it.walk() }.flatten().filter { it.isFile }
|
||||||
|
.filter { it.extension == "class" }
|
||||||
|
.map { it.readClass() to it }
|
||||||
|
.onEach { (c, _) ->
|
||||||
|
asmClasses[c.name] = c
|
||||||
|
}.toList().forEach { (classNode, file) ->
|
||||||
|
analyzer.file = file
|
||||||
|
classNode.methods?.forEach { method ->
|
||||||
|
analyzeMethod(analyzer, method, asmClasses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (analyzer.reported) {
|
||||||
|
error("Verify failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -103,7 +103,7 @@ kotlin {
|
|||||||
|
|
||||||
tasks.register("checkAndroidApiLevel") {
|
tasks.register("checkAndroidApiLevel") {
|
||||||
doFirst {
|
doFirst {
|
||||||
androidutil.AndroidApiLevelCheck.check(
|
analyzes.AndroidApiLevelCheck.check(
|
||||||
buildDir.resolve("classes/kotlin/android/main"),
|
buildDir.resolve("classes/kotlin/android/main"),
|
||||||
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
||||||
project
|
project
|
||||||
|
@ -91,7 +91,7 @@ kotlin {
|
|||||||
|
|
||||||
tasks.register("checkAndroidApiLevel") {
|
tasks.register("checkAndroidApiLevel") {
|
||||||
doFirst {
|
doFirst {
|
||||||
androidutil.AndroidApiLevelCheck.check(
|
analyzes.AndroidApiLevelCheck.check(
|
||||||
buildDir.resolve("classes/kotlin/android/main"),
|
buildDir.resolve("classes/kotlin/android/main"),
|
||||||
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
||||||
project
|
project
|
||||||
|
@ -116,7 +116,7 @@ kotlin {
|
|||||||
|
|
||||||
tasks.register("checkAndroidApiLevel") {
|
tasks.register("checkAndroidApiLevel") {
|
||||||
doFirst {
|
doFirst {
|
||||||
androidutil.AndroidApiLevelCheck.check(
|
analyzes.AndroidApiLevelCheck.check(
|
||||||
buildDir.resolve("classes/kotlin/android/main"),
|
buildDir.resolve("classes/kotlin/android/main"),
|
||||||
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
||||||
project
|
project
|
||||||
|
Loading…
Reference in New Issue
Block a user