mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-25 15:00:09 +08:00
Android api level check
This commit is contained in:
parent
dd7aed885d
commit
7a7c88b783
@ -41,6 +41,9 @@ fun version(name: String): String {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val asmVersion = version("asm")
|
||||
fun asm(module: String) = "org.ow2.asm:asm-$module:$asmVersion"
|
||||
|
||||
fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
|
||||
fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
|
||||
|
||||
@ -51,6 +54,9 @@ dependencies {
|
||||
api("org.jetbrains.kotlin", "kotlin-gradle-plugin", version("kotlinCompiler"))
|
||||
api("org.jetbrains.kotlin", "kotlin-compiler-embeddable", version("kotlinCompiler"))
|
||||
api("com.android.tools.build", "gradle", version("androidGradlePlugin"))
|
||||
api(asm("tree"))
|
||||
api(asm("util"))
|
||||
api(asm("commons"))
|
||||
|
||||
api(gradleApi())
|
||||
}
|
@ -42,6 +42,7 @@ object Versions {
|
||||
|
||||
const val slf4j = "1.7.30"
|
||||
const val log4j = "2.13.3"
|
||||
const val asm = "9.1"
|
||||
|
||||
|
||||
// If you the versions below, you need to sync changes to mirai-console/buildSrc/src/main/kotlin/Versions.kt
|
||||
|
294
buildSrc/src/main/kotlin/androidutil/AndroidApiLevelCheck.kt
Normal file
294
buildSrc/src/main/kotlin/androidutil/AndroidApiLevelCheck.kt
Normal file
@ -0,0 +1,294 @@
|
||||
/*
|
||||
* 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 androidutil
|
||||
|
||||
import groovy.util.Node
|
||||
import groovy.util.XmlParser
|
||||
import org.gradle.api.Project
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.Type
|
||||
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(
|
||||
val name: String,
|
||||
val since: Int,
|
||||
val superTypes: List<SuperInfo>,
|
||||
val fieldInfos: Map<String, MemberInfo>,
|
||||
val methodInfos: Map<String, MemberInfo>
|
||||
) {
|
||||
data class SuperInfo(
|
||||
val name: String,
|
||||
val since: Int?,
|
||||
val removed: Int?
|
||||
)
|
||||
|
||||
data class MemberInfo(
|
||||
val name: String,
|
||||
val since: Int?
|
||||
)
|
||||
}
|
||||
|
||||
class Analyzer(
|
||||
val classesInfos: Map<String, ClassInfo>
|
||||
) {
|
||||
var path: String? = null
|
||||
var context: String? = null
|
||||
var file: File? = null
|
||||
var apilevel = 0
|
||||
var reported = false
|
||||
inline fun withPath(path: String, block: Analyzer.() -> Unit) {
|
||||
this.path = path
|
||||
block(this)
|
||||
this.path = null
|
||||
}
|
||||
|
||||
inline fun withContext(context: String, block: Analyzer.() -> Unit) {
|
||||
this.context = context
|
||||
block(this)
|
||||
this.context = null
|
||||
}
|
||||
|
||||
fun report(prefix: String, message: String) {
|
||||
reported = true
|
||||
file?.let { file ->
|
||||
println("> $file")
|
||||
this.file = null
|
||||
}
|
||||
context?.let { context ->
|
||||
println(" > $context")
|
||||
this.context = null
|
||||
}
|
||||
path?.let { path ->
|
||||
println(" > $path")
|
||||
this.path = null
|
||||
}
|
||||
if (prefix.isBlank()) {
|
||||
message
|
||||
} else {
|
||||
"$prefix: $message"
|
||||
}.split('\n').forEach { println(" $it") }
|
||||
}
|
||||
|
||||
fun needCheck(type: String): Boolean {
|
||||
if (type.startsWith("android/")) return true
|
||||
if (type.startsWith("androidx/")) return true
|
||||
if (type.startsWith("java/")) return true
|
||||
if (type.startsWith("javax/")) return true
|
||||
return classesInfos.containsKey(type)
|
||||
}
|
||||
|
||||
fun checkClass(prefix: String, name: String) {
|
||||
if (!needCheck(name)) return
|
||||
val info = classesInfos[name]
|
||||
if (info == null) {
|
||||
report(prefix, "$name not found in api-version.xml")
|
||||
return
|
||||
}
|
||||
if (info.since > apilevel) {
|
||||
report(prefix, "$name since api level ${info.since}")
|
||||
}
|
||||
}
|
||||
|
||||
fun checkFieldAccess(prefix: String, owner: String, name: String) {
|
||||
if (!needCheck(owner)) return
|
||||
|
||||
val info = classesInfos[owner] ?: return
|
||||
val field = info.fieldInfos[name]
|
||||
if (field == null) {
|
||||
report(prefix, "No field $owner.$name")
|
||||
return
|
||||
}
|
||||
if ((field.since ?: 0) > apilevel) {
|
||||
report(prefix, "$owner.$name since api level ${field.since}")
|
||||
}
|
||||
}
|
||||
|
||||
fun checkMethodAccess(prefix: String, owner: String, name: String) {
|
||||
if (!needCheck(owner)) return
|
||||
|
||||
fun findMethod(type: String): ClassInfo.MemberInfo? {
|
||||
val cinfo = classesInfos[type] ?: return null
|
||||
return cinfo.methodInfos[name] ?: kotlin.run {
|
||||
cinfo.superTypes.forEach { stype ->
|
||||
if (stype.removed != null) {
|
||||
if (apilevel >= stype.removed) return@forEach
|
||||
}
|
||||
if (stype.since != null) {
|
||||
if (apilevel < stype.since) return@forEach
|
||||
}
|
||||
findMethod(stype.name)?.let { return it }
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val method = findMethod(owner)
|
||||
if (method == null) {
|
||||
report(prefix, "No method $owner.$name")
|
||||
return
|
||||
}
|
||||
if ((method.since ?: 0) > apilevel) {
|
||||
report(prefix, "$owner.$name since api level ${method.since}")
|
||||
}
|
||||
}
|
||||
|
||||
private val Type.top: Type
|
||||
get() = when (sort) {
|
||||
Type.ARRAY -> elementType
|
||||
else -> this
|
||||
}
|
||||
|
||||
|
||||
fun analyze(classNode: ClassNode, file: File) {
|
||||
this.file = file
|
||||
withContext("Check class") {
|
||||
withPath("class checking") {
|
||||
checkClass("Couldn't extend ${classNode.superName}", classNode.superName)
|
||||
classNode.interfaces?.forEach { checkClass("Couldn't implements $it", it) }
|
||||
}
|
||||
}
|
||||
classNode.fields?.forEach { field ->
|
||||
withContext("Field ${field.name}: ${field.desc}") {
|
||||
val type = Type.getType(field.desc).top.internalName
|
||||
checkClass("Couldn't access $type", type)
|
||||
}
|
||||
}
|
||||
classNode.methods?.forEach { method ->
|
||||
withContext("Method ${method.name}${method.desc}") {
|
||||
withPath("Checking method desc") {
|
||||
val returnType = Type.getReturnType(method.desc).top.internalName
|
||||
checkClass("Couldn't access $returnType", returnType)
|
||||
Type.getArgumentTypes(method.desc).map { it.top.internalName }.forEach {
|
||||
checkClass("Couldn't access $it", it)
|
||||
}
|
||||
}
|
||||
method.instructions?.forEach { insn ->
|
||||
when (insn) {
|
||||
is FieldInsnNode -> {
|
||||
withPath("Access field ${insn.owner}.${insn.name}: ${insn.desc}") {
|
||||
val type = Type.getType(insn.desc)
|
||||
val prefix = "Couldn't access ${insn.owner}.${insn.name}: ${insn.desc}"
|
||||
checkClass(prefix, type.internalName)
|
||||
checkFieldAccess(prefix, insn.owner, insn.name)
|
||||
}
|
||||
}
|
||||
is MethodInsnNode -> {
|
||||
withPath("Invoke method ${insn.owner}.${insn.name}${insn.desc}") {
|
||||
checkClass("Couldn't access ${insn.owner}", insn.owner)
|
||||
val returnType = Type.getReturnType(insn.desc).top.internalName
|
||||
checkClass("Couldn't access $returnType", returnType)
|
||||
Type.getArgumentTypes(insn.desc).map { it.top.internalName }.forEach {
|
||||
checkClass("Couldn't access $it", it)
|
||||
}
|
||||
checkMethodAccess(
|
||||
"Couldn't access ${insn.owner}.${insn.name}${insn.desc}",
|
||||
insn.owner,
|
||||
insn.name + insn.desc
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun check(classes: File, level: Int, project: Project) {
|
||||
val apiVersionsFile = project.rootProject.buildDir.resolve("android-api-versions.xml")
|
||||
if (!apiVersionsFile.isFile) {
|
||||
apiVersionsFile.parentFile.mkdirs()
|
||||
println("Downloading AndroidSDK/api-versions.xml")
|
||||
val apiVersionsFileTmp = project.rootProject.buildDir.resolve("android-api-versions.xml.tmp")
|
||||
URL("https://github.com/aosp-mirror/platform_development/raw/master/sdk/api-versions.xml")
|
||||
.openStream().use { upstream ->
|
||||
apiVersionsFileTmp.outputStream().use { upstream.copyTo(it) }
|
||||
}
|
||||
if (!apiVersionsFileTmp.renameTo(apiVersionsFile)) {
|
||||
apiVersionsFileTmp.copyTo(apiVersionsFile, overwrite = true)
|
||||
apiVersionsFileTmp.delete()
|
||||
}
|
||||
}
|
||||
val classesInfos = mutableMapOf<String, ClassInfo>()
|
||||
XmlParser().parse(apiVersionsFile).children().forEach { classNode ->
|
||||
classNode as Node
|
||||
if (classNode.name() == "class") {
|
||||
val fieldInfos = mutableMapOf<String, ClassInfo.MemberInfo>()
|
||||
val methodInfos = mutableMapOf<String, ClassInfo.MemberInfo>()
|
||||
val cinfo = ClassInfo(
|
||||
classNode.attribute("name").toString(),
|
||||
classNode.attribute("since").toString().toInt(),
|
||||
(classNode.children() as List<Node>).filter {
|
||||
it.name() == "implements" || it.name() == "extends"
|
||||
}.map {
|
||||
ClassInfo.SuperInfo(
|
||||
it.attribute("name").toString(),
|
||||
it.attribute("since")?.toString()?.toInt(),
|
||||
it.attribute("removed")?.toString()?.toInt()
|
||||
)
|
||||
},
|
||||
fieldInfos, methodInfos
|
||||
)
|
||||
classesInfos[cinfo.name] = cinfo
|
||||
classNode.children().forEach { memberNode ->
|
||||
memberNode as Node
|
||||
when (memberNode.name()) {
|
||||
"method" -> {
|
||||
val method = ClassInfo.MemberInfo(
|
||||
memberNode.attribute("name").toString(),
|
||||
memberNode.attribute("since")?.toString()?.toInt()
|
||||
)
|
||||
methodInfos[method.name] = method
|
||||
}
|
||||
"field" -> {
|
||||
val field = ClassInfo.MemberInfo(
|
||||
memberNode.attribute("name").toString(),
|
||||
memberNode.attribute("since")?.toString()?.toInt()
|
||||
)
|
||||
fieldInfos[field.name] = field
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val analyzer = Analyzer(classesInfos)
|
||||
analyzer.apilevel = level
|
||||
|
||||
classes.walk()
|
||||
.filter { it.isFile && it.extension == "class" }
|
||||
.map { file ->
|
||||
kotlin.runCatching {
|
||||
val cnode = ClassNode()
|
||||
file.inputStream().use {
|
||||
ClassReader(it).accept(cnode, 0)
|
||||
}
|
||||
cnode
|
||||
}.getOrNull() to file
|
||||
}
|
||||
.filter { it.first != null }
|
||||
.map {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as Pair<ClassNode, File>
|
||||
}
|
||||
.forEach { (classNode, file) ->
|
||||
analyzer.analyze(classNode, file)
|
||||
}
|
||||
|
||||
if (analyzer.reported) {
|
||||
error("Verity failed")
|
||||
}
|
||||
}
|
||||
}
|
@ -20,3 +20,4 @@ kotlin.native.enableDependencyPropagation=false
|
||||
#kotlin.mpp.enableGranularSourceSetsMetadata=true
|
||||
systemProp.org.gradle.internal.publish.checksums.insecure=true
|
||||
gnsp.disableApplyOnlyOnRootProjectEnforcement=true
|
||||
mirai.android.target.api.level=15
|
||||
|
@ -101,6 +101,18 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("checkAndroidApiLevel") {
|
||||
doFirst {
|
||||
androidutil.AndroidApiLevelCheck.check(
|
||||
buildDir.resolve("classes/kotlin/android/main"),
|
||||
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
||||
project
|
||||
)
|
||||
}
|
||||
group = "verification"
|
||||
this.mustRunAfter("androidMainClasses")
|
||||
}
|
||||
tasks.getByName("androidTest").dependsOn("checkAndroidApiLevel")
|
||||
|
||||
fun org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler.implementation1(dependencyNotation: String) =
|
||||
implementation(dependencyNotation) {
|
||||
|
@ -91,7 +91,8 @@ private fun String.forEachMiraiCode(block: (origin: String, name: String?, args:
|
||||
}
|
||||
}
|
||||
|
||||
private object MiraiCodeParsers : Map<String, MiraiCodeParser> by mapOf(
|
||||
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
|
||||
private object MiraiCodeParsers: AbstractMap<String, MiraiCodeParser>(), Map<String, MiraiCodeParser> by mapOf(
|
||||
"at" to MiraiCodeParser(Regex("""(\d*)""")) { (target) ->
|
||||
At(target.toLong())
|
||||
},
|
||||
|
@ -89,6 +89,19 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("checkAndroidApiLevel") {
|
||||
doFirst {
|
||||
androidutil.AndroidApiLevelCheck.check(
|
||||
buildDir.resolve("classes/kotlin/android/main"),
|
||||
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
||||
project
|
||||
)
|
||||
}
|
||||
group = "verification"
|
||||
this.mustRunAfter("androidMainClasses")
|
||||
}
|
||||
tasks.getByName("androidTest").dependsOn("checkAndroidApiLevel")
|
||||
|
||||
fun org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler.implementation1(dependencyNotation: String) =
|
||||
implementation(dependencyNotation) {
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib")
|
||||
|
@ -114,6 +114,19 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("checkAndroidApiLevel") {
|
||||
doFirst {
|
||||
androidutil.AndroidApiLevelCheck.check(
|
||||
buildDir.resolve("classes/kotlin/android/main"),
|
||||
project.property("mirai.android.target.api.level")!!.toString().toInt(),
|
||||
project
|
||||
)
|
||||
}
|
||||
group = "verification"
|
||||
this.mustRunAfter("androidMainClasses")
|
||||
}
|
||||
tasks.getByName("androidTest").dependsOn("checkAndroidApiLevel")
|
||||
|
||||
fun org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler.implementation1(dependencyNotation: String) =
|
||||
implementation(dependencyNotation) {
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib")
|
||||
|
Loading…
Reference in New Issue
Block a user