diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6e899766..c640ce10e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,8 @@ git submodule init git submodule update ``` +项目首次初始化和构建可能要花费较长时间。 + - 要构建项目, 请运行 `gradlew assemble` - 要运行测试, 请运行 `gradlew test` - 要构建项目并运行测试, 请运行 `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 都处于 - 协议支持: [添加新协议支持](#添加协议支持) @@ -46,10 +48,16 @@ git submodule update 请查看 [PacketFactory.kt](mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt) 了解网络层架构. 参考现有的 `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')) - 保证二进制兼容性: 在提交前执行 `gradlew build`, 若有不兼容变更会得到错误. 在提交时将 `binary-compatibility-validator.api` 一并提交 (如果有修改). (使用 [Kotlin/binary-compatibility-validator](https://github.com/Kotlin/binary-compatibility-validator)) +- 通过 GitHub 的 Pull Request 提交代码,很快就会有相关模块负责人员来审核 + + +如果你不太保证自己能达到上述要求也没关系,mirai 感谢你的每一行代码,维护者会审核代码并尽可能帮助你。 diff --git a/build.gradle.kts b/build.gradle.kts index 52b18fcab..5563c6e67 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -69,6 +69,8 @@ tasks.register("publishMiraiCoreArtifactsToMavenLocal") { ) } +analyzes.CompiledCodeVerify.run { registerAllVerifyTasks() } + allprojects { group = "net.mamoe" version = Versions.project diff --git a/buildSrc/src/main/kotlin/androidutil/AndroidApiLevelCheck.kt b/buildSrc/src/main/kotlin/analyzes/AndroidApiLevelCheck.kt similarity index 97% rename from buildSrc/src/main/kotlin/androidutil/AndroidApiLevelCheck.kt rename to buildSrc/src/main/kotlin/analyzes/AndroidApiLevelCheck.kt index b43c823ac..7ab6a717e 100644 --- a/buildSrc/src/main/kotlin/androidutil/AndroidApiLevelCheck.kt +++ b/buildSrc/src/main/kotlin/analyzes/AndroidApiLevelCheck.kt @@ -9,7 +9,7 @@ @file:Suppress("MemberVisibilityCanBePrivate") -package androidutil +package analyzes import groovy.util.Node 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.MethodInsnNode import java.io.File -import java.net.URL object AndroidApiLevelCheck { data class ClassInfo( @@ -260,11 +259,7 @@ object AndroidApiLevelCheck { .filter { it.isFile && it.extension == "class" } .map { file -> kotlin.runCatching { - val cnode = ClassNode() - file.inputStream().use { - ClassReader(it).accept(cnode, 0) - } - cnode + AsmUtil.run { file.readClass() } }.getOrNull() to file } .filter { it.first != null } diff --git a/buildSrc/src/main/kotlin/analyzes/AsmUtil.kt b/buildSrc/src/main/kotlin/analyzes/AsmUtil.kt new file mode 100644 index 000000000..80e7883cb --- /dev/null +++ b/buildSrc/src/main/kotlin/analyzes/AsmUtil.kt @@ -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 + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/analyzes/CompiledCodeVerify.kt b/buildSrc/src/main/kotlin/analyzes/CompiledCodeVerify.kt new file mode 100644 index 000000000..ed9eb770a --- /dev/null +++ b/buildSrc/src/main/kotlin/analyzes/CompiledCodeVerify.kt @@ -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() } + } + } + } + +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/analyzes/NoSuchMethodAnalyzer.kt b/buildSrc/src/main/kotlin/analyzes/NoSuchMethodAnalyzer.kt new file mode 100644 index 000000000..e7f5db256 --- /dev/null +++ b/buildSrc/src/main/kotlin/analyzes/NoSuchMethodAnalyzer.kt @@ -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") + } + } +} \ No newline at end of file diff --git a/mirai-core-api/build.gradle.kts b/mirai-core-api/build.gradle.kts index 3efab5f9d..5959feda1 100644 --- a/mirai-core-api/build.gradle.kts +++ b/mirai-core-api/build.gradle.kts @@ -103,7 +103,7 @@ kotlin { tasks.register("checkAndroidApiLevel") { doFirst { - androidutil.AndroidApiLevelCheck.check( + analyzes.AndroidApiLevelCheck.check( buildDir.resolve("classes/kotlin/android/main"), project.property("mirai.android.target.api.level")!!.toString().toInt(), project diff --git a/mirai-core-utils/build.gradle.kts b/mirai-core-utils/build.gradle.kts index 8d15b5c87..647f696c8 100644 --- a/mirai-core-utils/build.gradle.kts +++ b/mirai-core-utils/build.gradle.kts @@ -91,7 +91,7 @@ kotlin { tasks.register("checkAndroidApiLevel") { doFirst { - androidutil.AndroidApiLevelCheck.check( + analyzes.AndroidApiLevelCheck.check( buildDir.resolve("classes/kotlin/android/main"), project.property("mirai.android.target.api.level")!!.toString().toInt(), project diff --git a/mirai-core/build.gradle.kts b/mirai-core/build.gradle.kts index 178912fea..bc9aec17e 100644 --- a/mirai-core/build.gradle.kts +++ b/mirai-core/build.gradle.kts @@ -116,7 +116,7 @@ kotlin { tasks.register("checkAndroidApiLevel") { doFirst { - androidutil.AndroidApiLevelCheck.check( + analyzes.AndroidApiLevelCheck.check( buildDir.resolve("classes/kotlin/android/main"), project.property("mirai.android.target.api.level")!!.toString().toInt(), project