diff --git a/buildSrc/src/main/kotlin/DependencyDumper.kt b/buildSrc/src/main/kotlin/DependencyDumper.kt new file mode 100644 index 00000000000..55075a05de1 --- /dev/null +++ b/buildSrc/src/main/kotlin/DependencyDumper.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.tasks.TaskProvider +import java.io.File +import java.util.zip.ZipFile + +object DependencyDumper { + fun registerDumpTask(project: Project, confName: String, out: File): TaskProvider { + return regDmpTask(project, confName) { deps -> + deps.forEach { println(" `- $it") } + out.writeText(deps.joinToString("\n", postfix = "\n")) + } + } + + fun registerDumpTaskKtSrc(project: Project, confName: String, out: File, className: String): TaskProvider { + val pkgName = className.substringBeforeLast(".") + val kname = className.substringAfterLast(".") + return regDmpTask(project, confName) { deps -> + out.printWriter().use { pr -> + pr.println("package $pkgName") + pr.println() + pr.println("internal object $kname {") + pr.println(" val dependencies: List = listOf(") + deps.forEach { dependency -> + pr.append(" \"").append(dependency).println("\",") + } + pr.println(" )") + pr.println("}") + } + } + } + + private fun regDmpTask(project: Project, confName: String, action: (List) -> Unit): TaskProvider { + val dependenciesDump = project.tasks.maybeCreate("dependenciesDump") + dependenciesDump.group = "mirai" + return project.tasks.register("dependenciesDump_${confName.capitalize()}") { + group = "mirai" + doLast { + val dependencies = HashSet() + fun emit(dep: ResolvedDependency) { + if (dependencies.add(dep.moduleGroup + ":" + dep.moduleName)) { + dep.children.forEach { emit(it) } + } + } + project.configurations.getByName(confName).resolvedConfiguration.firstLevelModuleDependencies.forEach { dependency -> + emit(dependency) + } + val stdep = dependencies.toMutableList() + stdep.sort() + action(stdep) + } + }.also { dependenciesDump.dependsOn(it) } + } + + fun registerDumpClassGraph(project: Project, confName: String, out: String): TaskProvider { + val dependenciesDump = project.tasks.maybeCreate("dependenciesDump") + dependenciesDump.group = "mirai" + return project.tasks.register("dependenciesDumpGraph_${confName.capitalize()}") { + group = "mirai" + val outFile = temporaryDir.resolve(out) + outputs.file(outFile) + val conf = project.configurations.getByName(confName) + dependsOn(conf) + + doLast { + outFile.parentFile.mkdirs() + + val classes = HashSet() + conf.resolvedConfiguration.files.forEach { file -> + if (file.isFile) { + ZipFile(file).use { zipFile -> + zipFile.entries().asSequence() + .filter { it.name.endsWith(".class") } + .filterNot { it.name.startsWith("META-INF") } + .map { it.name.substringBeforeLast('.').replace('/', '.') } + .map { it.removePrefix(".") } + .forEach { classes.add(it) } + } + } else if (file.isDirectory) { + file.walk() + .filter { it.isFile } + .filter { it.name.endsWith(".class") } + .map { it.relativeTo(file).path.substringBeforeLast('.') } + .map { it.replace('\\', '.').replace('/', '.') } + .map { it.removePrefix(".") } + .forEach { classes.add(it) } + } + } + outFile.bufferedWriter().use { writer -> + classes.sorted().forEach { writer.append(it).append('\n') } + } + } + }.also { dependenciesDump.dependsOn(it) } + } +} diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index b48ef00e9ad..ff318adf0d7 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -53,6 +53,8 @@ object Versions { const val difflib = "1.3.0" const val netty = "4.1.63.Final" const val bouncycastle = "1.64" + const val mavenArtifactResolver = "1.7.3" + const val mavenResolverProvider = "3.8.4" const val junit = "5.7.2" @@ -151,3 +153,9 @@ const val `caller-finder` = "io.github.karlatemp:caller:1.1.1" const val `android-runtime` = "com.google.android:android:${Versions.android}" const val `netty-all` = "io.netty:netty-all:${Versions.netty}" const val `bouncycastle` = "org.bouncycastle:bcprov-jdk15on:${Versions.bouncycastle}" + +const val `maven-resolver-api` = "org.apache.maven.resolver:maven-resolver-api:${Versions.mavenArtifactResolver}" +const val `maven-resolver-impl` = "org.apache.maven.resolver:maven-resolver-impl:${Versions.mavenArtifactResolver}" +const val `maven-resolver-connector-basic` = "org.apache.maven.resolver:maven-resolver-connector-basic:${Versions.mavenArtifactResolver}" +const val `maven-resolver-transport-http` = "org.apache.maven.resolver:maven-resolver-transport-http:${Versions.mavenArtifactResolver}" +const val `maven-resolver-provider` = "org.apache.maven:maven-resolver-provider:${Versions.mavenResolverProvider}" diff --git a/mirai-console/backend/integration-test/test/MiraiConsoleIntegrationTestBootstrap.kt b/mirai-console/backend/integration-test/test/MiraiConsoleIntegrationTestBootstrap.kt index 713513eecf5..035739fcb37 100644 --- a/mirai-console/backend/integration-test/test/MiraiConsoleIntegrationTestBootstrap.kt +++ b/mirai-console/backend/integration-test/test/MiraiConsoleIntegrationTestBootstrap.kt @@ -11,6 +11,7 @@ package net.mamoe.console.integrationtest import net.mamoe.console.integrationtest.testpoints.DoNothingPoint import net.mamoe.console.integrationtest.testpoints.MCITBSelfAssertions +import net.mamoe.console.integrationtest.testpoints.PluginSharedLibraries import net.mamoe.console.integrationtest.testpoints.plugin.PluginDataRenameToIdTest import net.mamoe.console.integrationtest.testpoints.terminal.TestTerminalLogging import org.junit.jupiter.api.Test @@ -36,6 +37,7 @@ class MiraiConsoleIntegrationTestBootstrap { MCITBSelfAssertions, PluginDataRenameToIdTest, TestTerminalLogging, + PluginSharedLibraries, ).asSequence().map { v -> when (v) { is Class<*> -> v diff --git a/mirai-console/backend/integration-test/test/testpoints/PluginSharedLibraries.kt b/mirai-console/backend/integration-test/test/testpoints/PluginSharedLibraries.kt new file mode 100644 index 00000000000..4eb4287ab93 --- /dev/null +++ b/mirai-console/backend/integration-test/test/testpoints/PluginSharedLibraries.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.console.integrationtest.testpoints + +import net.mamoe.console.integrationtest.AbstractTestPoint +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +internal object PluginSharedLibraries : AbstractTestPoint() { + override fun beforeConsoleStartup() { + if (System.getenv("CI").orEmpty().toBoolean()) { + println("CI env") + File("config/Console/PluginDependencies.yml").writeText( + "repoLoc: ['https://repo.maven.apache.org/maven2']" + ) + } + File("plugin-shared-libraries").mkdirs() + File("plugin-shared-libraries/libraries.txt").writeText( + """ + io.github.karlatemp:unsafe-accessor:1.6.2 + """.trimIndent() + ) + ZipOutputStream(File("plugin-shared-libraries/test.jar").outputStream().buffered()).use { zipOutput -> + zipOutput.putNextEntry(ZipEntry("net/mamoe/console/it/psl/PluginSharedLib.class")) + ClassWriter(0).also { writer -> + writer.visit( + Opcodes.V1_8, + 0, + "net/mamoe/console/it/psl/PluginSharedLib", + null, + "java/lang/Object", + null + ) + }.toByteArray().let { zipOutput.write(it) } + } + } +} \ No newline at end of file diff --git a/mirai-console/backend/integration-test/testers/MCITSelfTestPlugin/src/MCITSelfTestPlugin.kt b/mirai-console/backend/integration-test/testers/MCITSelfTestPlugin/src/MCITSelfTestPlugin.kt index 69361c41358..b863679d98d 100644 --- a/mirai-console/backend/integration-test/testers/MCITSelfTestPlugin/src/MCITSelfTestPlugin.kt +++ b/mirai-console/backend/integration-test/testers/MCITSelfTestPlugin/src/MCITSelfTestPlugin.kt @@ -30,4 +30,8 @@ public object MCITSelfTestPlugin : KotlinPlugin( assertTrue { true } } + + public fun someAction() { + logger.info { "Called!" } + } } diff --git a/mirai-console/backend/integration-test/testers/plugin-depend-on-other/build.gradle.kts b/mirai-console/backend/integration-test/testers/plugin-depend-on-other/build.gradle.kts new file mode 100644 index 00000000000..ab542c3038f --- /dev/null +++ b/mirai-console/backend/integration-test/testers/plugin-depend-on-other/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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 + */ + +@file:Suppress("UnusedImport") + +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + id("java") +} + +version = "0.0.0" + +kotlin { + explicitApiWarning() +} + +dependencies { + api(project(":mirai-console.integration-test")) + api(parent!!.project("MCITSelfTestPlugin")) +} diff --git a/mirai-console/backend/integration-test/testers/plugin-depend-on-other/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin b/mirai-console/backend/integration-test/testers/plugin-depend-on-other/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin new file mode 100644 index 00000000000..e4e3fcf5316 --- /dev/null +++ b/mirai-console/backend/integration-test/testers/plugin-depend-on-other/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin @@ -0,0 +1,10 @@ +# +# Copyright 2019-2022 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/dev/LICENSE +# + +net.mamoe.console.integrationtest.ep.dependonother.PluginDependOnOther \ No newline at end of file diff --git a/mirai-console/backend/integration-test/testers/plugin-depend-on-other/src/PluginDependOnOther.kt b/mirai-console/backend/integration-test/testers/plugin-depend-on-other/src/PluginDependOnOther.kt new file mode 100644 index 00000000000..dac898e0962 --- /dev/null +++ b/mirai-console/backend/integration-test/testers/plugin-depend-on-other/src/PluginDependOnOther.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.console.integrationtest.ep.dependonother + +import net.mamoe.console.integrationtest.ep.mcitselftest.MCITSelfTestPlugin +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin +import net.mamoe.mirai.utils.info +import kotlin.test.assertFails +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals +import kotlin.test.assertSame + +/* +PluginDependOnOther: 测试插件依赖其他插件的情况 + */ +public object PluginDependOnOther : KotlinPlugin( + JvmPluginDescription( + id = "net.mamoe.tester.plugin-depend-on-other", + version = "1.0.0", + name = "Plugin Depend On Other", + ) { + dependsOn("net.mamoe.tester.mirai-console-self-test") + dependsOn("net.mamoe.tester.plugin-dynamic-dependencies-download") + } +) { + override fun onEnable() { + logger.info { "Do dependency call: " + MCITSelfTestPlugin::class.java } + logger.info { "No Depends on: " + Class.forName("samepkg.P") } + logger.info(Throwable("Stack trace")) + MCITSelfTestPlugin.someAction() + logger.info { "Shared library: " + Class.forName("net.mamoe.console.it.psl.PluginSharedLib") } + assertNotEquals(javaClass.classLoader, Class.forName("net.mamoe.console.it.psl.PluginSharedLib").classLoader) + + // dependencies-shared + kotlin.run { + val pluginDepDynDownload = Class.forName("net.mamoe.console.integrationtest.ep.pddd.P") + val gsonC = Class.forName("com.google.gson.Gson") + logger.info { "Gson located $gsonC <${gsonC.classLoader}>" } + assertSame(gsonC, Class.forName(gsonC.name, false, pluginDepDynDownload.classLoader)) + assertFailsWith { + Class.forName("com.zaxxer.sparsebits.SparseBitSet") // private in dynamic-dep-download + } + assertFailsWith { + Class.forName("net.mamoe.assertion.something.not.existing") + } + } + } +} diff --git a/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/mirai-console-plugin/dependencies-private.txt b/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/mirai-console-plugin/dependencies-private.txt new file mode 100644 index 00000000000..c87bdd6fe47 --- /dev/null +++ b/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/mirai-console-plugin/dependencies-private.txt @@ -0,0 +1,4 @@ +com.zaxxer:SparseBitSet:1.2 + +## Test console non-hard-link override +org.bouncycastle:bcprov-jdk15on:1.63 diff --git a/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/mirai-console-plugin/dependencies-shared.txt b/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/mirai-console-plugin/dependencies-shared.txt new file mode 100644 index 00000000000..bd91aca64bf --- /dev/null +++ b/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/mirai-console-plugin/dependencies-shared.txt @@ -0,0 +1 @@ +com.google.code.gson:gson:2.8.9 diff --git a/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin b/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin new file mode 100644 index 00000000000..e33788fe1f7 --- /dev/null +++ b/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin @@ -0,0 +1 @@ +net.mamoe.console.integrationtest.ep.pddd.P \ No newline at end of file diff --git a/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/src/P.kt b/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/src/P.kt new file mode 100644 index 00000000000..f82eef076b2 --- /dev/null +++ b/mirai-console/backend/integration-test/testers/plugin-dynamic-dependencies-download/src/P.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.console.integrationtest.ep.pddd + +import net.mamoe.mirai.console.extension.PluginComponentStorage +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin +import kotlin.test.assertEquals + +/* +PluginDynamicDependenciesDownload: 测试动态运行时下载 + */ +internal object P : KotlinPlugin( + JvmPluginDescription( + id = "net.mamoe.tester.plugin-dynamic-dependencies-download", + version = "1.0.0", + name = "Plugin Dynamic Dependencies Download", + ) +) { + override fun PluginComponentStorage.onLoad() { + Class.forName("com.google.gson.Gson") // shared + Class.forName("com.zaxxer.sparsebits.SparseBitSet") // private + + // console-non-hard-link dependency + // mirai-core used 1.64 current + assertEquals( + "1.63.0", + Class.forName("org.bouncycastle.LICENSE").`package`.implementationVersion + ) + } +} diff --git a/mirai-console/backend/integration-test/testers/same-pkg-1/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin b/mirai-console/backend/integration-test/testers/same-pkg-1/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin new file mode 100644 index 00000000000..702495240ea --- /dev/null +++ b/mirai-console/backend/integration-test/testers/same-pkg-1/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin @@ -0,0 +1,10 @@ +# +# Copyright 2019-2022 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/dev/LICENSE +# + +samepkg.P \ No newline at end of file diff --git a/mirai-console/backend/integration-test/testers/same-pkg-1/src/P.kt b/mirai-console/backend/integration-test/testers/same-pkg-1/src/P.kt new file mode 100644 index 00000000000..6fa03a0ab2e --- /dev/null +++ b/mirai-console/backend/integration-test/testers/same-pkg-1/src/P.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package samepkg + +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin + +/* +same-pkg-1: 测试包名一样时插件可以正常加载 + */ +internal object P : KotlinPlugin( + JvmPluginDescription( + id = "net.mamoe.tester.samepkg-1", + version = "1.0.0", + name = "SamePkg 1", + ) +) {} diff --git a/mirai-console/backend/integration-test/testers/same-pkg-2/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin b/mirai-console/backend/integration-test/testers/same-pkg-2/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin new file mode 100644 index 00000000000..702495240ea --- /dev/null +++ b/mirai-console/backend/integration-test/testers/same-pkg-2/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin @@ -0,0 +1,10 @@ +# +# Copyright 2019-2022 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/dev/LICENSE +# + +samepkg.P \ No newline at end of file diff --git a/mirai-console/backend/integration-test/testers/same-pkg-2/src/P.kt b/mirai-console/backend/integration-test/testers/same-pkg-2/src/P.kt new file mode 100644 index 00000000000..392d12c5974 --- /dev/null +++ b/mirai-console/backend/integration-test/testers/same-pkg-2/src/P.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package samepkg + +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin + +/* +same-pkg-2: 测试包名一样时插件可以正常加载 + */ +internal object P : KotlinPlugin( + JvmPluginDescription( + id = "net.mamoe.tester.samepkg-2", + version = "1.0.0", + name = "SamePkg 2", + ) +) {} diff --git a/mirai-console/backend/mirai-console/.gitignore b/mirai-console/backend/mirai-console/.gitignore index dcd5d087a9b..02ee6605c21 100644 --- a/mirai-console/backend/mirai-console/.gitignore +++ b/mirai-console/backend/mirai-console/.gitignore @@ -1 +1,2 @@ -src/internal/MiraiConsoleBuildConstants.kt \ No newline at end of file +src/internal/MiraiConsoleBuildConstants.kt +src/internal/MiraiConsoleBuildDependencies.kt \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/build.gradle.kts b/mirai-console/backend/mirai-console/build.gradle.kts index b5d08abc17a..29d900b6935 100644 --- a/mirai-console/backend/mirai-console/build.gradle.kts +++ b/mirai-console/backend/mirai-console/build.gradle.kts @@ -30,6 +30,14 @@ kotlin { explicitApiWarning() } + +// 搜索 mirai-console (包括 core) 直接使用并对外公开的类 (api) +configurations.create("consoleRuntimeClasspath").attributes { + attribute(Usage.USAGE_ATTRIBUTE, + project.objects.named(Usage::class.java, Usage.JAVA_API) + ) +} + dependencies { compileAndTestRuntime(project(":mirai-core-api")) compileAndTestRuntime(project(":mirai-core-utils")) @@ -46,10 +54,20 @@ dependencies { smartImplementation(`yamlkt-jvm`) smartImplementation(`jetbrains-annotations`) smartImplementation(`caller-finder`) + smartImplementation(`maven-resolver-api`) + smartImplementation(`maven-resolver-provider`) + smartImplementation(`maven-resolver-impl`) + smartImplementation(`maven-resolver-connector-basic`) + smartImplementation(`maven-resolver-transport-http`) smartApi(`kotlinx-coroutines-jdk8`) testApi(project(":mirai-core")) testApi(`kotlin-stdlib-jdk8`) + + "consoleRuntimeClasspath"(project) + "consoleRuntimeClasspath"(project(":mirai-core-utils")) + "consoleRuntimeClasspath"(project(":mirai-core-api")) + "consoleRuntimeClasspath"(project(":mirai-core")) } tasks { @@ -71,5 +89,21 @@ tasks { } } +tasks.getByName("compileKotlin").dependsOn( + DependencyDumper.registerDumpTaskKtSrc( + project, + "consoleRuntimeClasspath", + project.file("src/internal/MiraiConsoleBuildDependencies.kt"), + "net.mamoe.mirai.console.internal.MiraiConsoleBuildDependencies" + ) +) + +val graphDump = DependencyDumper.registerDumpClassGraph(project, "consoleRuntimeClasspath", "allclasses.txt") +tasks.named("processResources").configure { + from(graphDump) { + into("META-INF/mirai-console") + } +} + configurePublishing("mirai-console") configureBinaryValidator(null) \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api index 60136b41ceb..e3ac99d3f4d 100644 --- a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api +++ b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api @@ -1974,7 +1974,11 @@ public abstract interface class net/mamoe/mirai/console/plugin/PluginManager { public fun enablePlugin (Lnet/mamoe/mirai/console/plugin/Plugin;)V public abstract fun getBuiltInLoaders ()Ljava/util/List; public abstract fun getPluginDescription (Lnet/mamoe/mirai/console/plugin/Plugin;)Lnet/mamoe/mirai/console/plugin/description/PluginDescription; + public abstract fun getPluginLibrariesFolder ()Ljava/io/File; + public abstract fun getPluginLibrariesPath ()Ljava/nio/file/Path; public abstract fun getPluginLoaders ()Ljava/util/List; + public abstract fun getPluginSharedLibrariesFolder ()Ljava/io/File; + public abstract fun getPluginSharedLibrariesPath ()Ljava/nio/file/Path; public abstract fun getPlugins ()Ljava/util/List; public abstract fun getPluginsConfigFolder ()Ljava/io/File; public abstract fun getPluginsConfigPath ()Ljava/nio/file/Path; @@ -1993,7 +1997,11 @@ public final class net/mamoe/mirai/console/plugin/PluginManager$INSTANCE : net/m public fun getBuiltInLoaders ()Ljava/util/List; public final synthetic fun getDescription (Lnet/mamoe/mirai/console/plugin/Plugin;)Lnet/mamoe/mirai/console/plugin/description/PluginDescription; public fun getPluginDescription (Lnet/mamoe/mirai/console/plugin/Plugin;)Lnet/mamoe/mirai/console/plugin/description/PluginDescription; + public fun getPluginLibrariesFolder ()Ljava/io/File; + public fun getPluginLibrariesPath ()Ljava/nio/file/Path; public fun getPluginLoaders ()Ljava/util/List; + public fun getPluginSharedLibrariesFolder ()Ljava/io/File; + public fun getPluginSharedLibrariesPath ()Ljava/nio/file/Path; public fun getPlugins ()Ljava/util/List; public fun getPluginsConfigFolder ()Ljava/io/File; public fun getPluginsConfigPath ()Ljava/nio/file/Path; diff --git a/mirai-console/backend/mirai-console/src/internal/data/builtins/ConsoleDataScopeImpl.kt b/mirai-console/backend/mirai-console/src/internal/data/builtins/ConsoleDataScopeImpl.kt index fed628c3901..79f3a6bd34d 100644 --- a/mirai-console/backend/mirai-console/src/internal/data/builtins/ConsoleDataScopeImpl.kt +++ b/mirai-console/backend/mirai-console/src/internal/data/builtins/ConsoleDataScopeImpl.kt @@ -29,7 +29,10 @@ internal class ConsoleDataScopeImpl( override val configHolder: AutoSavePluginDataHolder = ConsoleBuiltInPluginConfigHolder(this.coroutineContext) private val data: List = mutableListOf() - private val configs: MutableList = mutableListOf(AutoLoginConfig) + private val configs: MutableList = mutableListOf( + AutoLoginConfig, + PluginDependenciesConfig, + ) override fun addAndReloadConfig(config: PluginConfig) { configs.add(config) diff --git a/mirai-console/backend/mirai-console/src/internal/data/builtins/PluginDependenciesConfig.kt b/mirai-console/backend/mirai-console/src/internal/data/builtins/PluginDependenciesConfig.kt new file mode 100644 index 00000000000..102958ee94f --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/data/builtins/PluginDependenciesConfig.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.console.internal.data.builtins + +import net.mamoe.mirai.console.data.ReadOnlyPluginConfig +import net.mamoe.mirai.console.data.ValueDescription +import net.mamoe.mirai.console.data.value + +@Suppress("RemoveExplicitTypeArguments") +internal object PluginDependenciesConfig : ReadOnlyPluginConfig("PluginDependencies") { + @ValueDescription("远程仓库, 如无必要无需修改") + val repoLoc by value(listOf("https://maven.aliyun.com/repository/public")) +} \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/AllDependenciesClassesHolder.kt b/mirai-console/backend/mirai-console/src/internal/plugin/AllDependenciesClassesHolder.kt new file mode 100644 index 00000000000..4851b4899fb --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/plugin/AllDependenciesClassesHolder.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.console.internal.plugin + +// Same as LazyLoad +internal object AllDependenciesClassesHolder { + @JvmField + internal val allclasses = AllDependenciesClassesHolder::class.java + .getResourceAsStream("/META-INF/mirai-console/allclasses.txt")!! + .bufferedReader().use { reader -> + reader.useLines { lines -> + lines.filterNot { it.isBlank() } + .toHashSet() + } + } + + @JvmField + internal val appClassLoader: ClassLoader = AllDependenciesClassesHolder::class.java.classLoader +} diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/BuiltInJvmPluginLoaderImpl.kt b/mirai-console/backend/mirai-console/src/internal/plugin/BuiltInJvmPluginLoaderImpl.kt index dd2ebc75594..072efb33be5 100644 --- a/mirai-console/backend/mirai-console/src/internal/plugin/BuiltInJvmPluginLoaderImpl.kt +++ b/mirai-console/backend/mirai-console/src/internal/plugin/BuiltInJvmPluginLoaderImpl.kt @@ -18,15 +18,14 @@ import net.mamoe.mirai.console.data.PluginDataStorage import net.mamoe.mirai.console.internal.util.PluginServiceHelper.findServices import net.mamoe.mirai.console.internal.util.PluginServiceHelper.loadAllServices import net.mamoe.mirai.console.plugin.PluginManager +import net.mamoe.mirai.console.plugin.id import net.mamoe.mirai.console.plugin.jvm.* import net.mamoe.mirai.console.plugin.loader.AbstractFilePluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoadException import net.mamoe.mirai.console.plugin.name -import net.mamoe.mirai.utils.MiraiLogger -import net.mamoe.mirai.utils.castOrNull -import net.mamoe.mirai.utils.childScope -import net.mamoe.mirai.utils.verbose +import net.mamoe.mirai.utils.* import java.io.File +import java.io.InputStream import java.nio.file.Path import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.CoroutineContext @@ -51,7 +50,70 @@ internal class BuiltInJvmPluginLoaderImpl( override val dataStorage: PluginDataStorage get() = MiraiConsoleImplementation.getInstance().dataStorageForJvmPluginLoader - override val classLoaders: MutableList = mutableListOf() + + internal val jvmPluginLoadingCtx: JvmPluginsLoadingCtx by lazy { + val classLoader = DynLibClassLoader(BuiltInJvmPluginLoaderImpl::class.java.classLoader) + val ctx = JvmPluginsLoadingCtx( + classLoader, + mutableListOf(), + JvmPluginDependencyDownloader(logger), + ) + logger.verbose { "Plugin shared libraries: " + PluginManager.pluginSharedLibrariesFolder } + PluginManager.pluginSharedLibrariesFolder.listFiles()?.asSequence().orEmpty() + .onEach { logger.debug { "Peek $it in shared libraries" } } + .filter { file -> + if (file.isDirectory) { + return@filter true + } + if (!file.exists()) { + logger.debug { "Skipped $file because file not exists" } + return@filter false + } + if (file.isFile) { + if (file.extension == "jar") { + return@filter true + } + logger.debug { "Skipped $file because extension <${file.extension}> != jar" } + return@filter false + } + logger.debug { "Skipped $file because unknown error" } + return@filter false + } + .filter { it.isDirectory || (it.isFile && it.extension == "jar") } + .forEach { pt -> + classLoader.addLib(pt) + logger.debug { "Linked static shared library: $pt" } + } + val libraries = PluginManager.pluginSharedLibrariesFolder.resolve("libraries.txt") + if (libraries.isFile) { + logger.verbose { "Linking static shared libraries...." } + val libs = libraries.useLines { lines -> + lines.filter { it.isNotBlank() } + .filterNot { it.startsWith("#") } + .onEach { logger.verbose { "static lib queued: $it" } } + .toMutableList() + } + val staticLibs = ctx.downloader.resolveDependencies(libs) + staticLibs.artifactResults.forEach { artifactResult -> + if (artifactResult.isResolved) { + ctx.sharedLibrariesLoader.addLib(artifactResult.artifact.file) + ctx.sharedLibrariesDependencies.add(artifactResult.artifact.depId()) + logger.debug { "Linked static shared library: ${artifactResult.artifact}" } + logger.verbose { "Linked static shared library: ${artifactResult.artifact.file}" } + } + } + } else { + libraries.createNewFile() + } + ctx + } + + override val classLoaders: MutableList get() = jvmPluginLoadingCtx.pluginClassLoaders + + override fun findLoadedClass(name: String): Class<*>? { + return classLoaders.firstNotNullOfOrNull { it.loadedClass(name) } + } + @Suppress("EXTENSION_SHADOWED_BY_MEMBER") // doesn't matter override fun getPluginDescription(plugin: JvmPlugin): JvmPluginDescription = plugin.description @@ -61,7 +123,7 @@ internal class BuiltInJvmPluginLoaderImpl( override fun Sequence.extractPlugins(): List { ensureActive() - fun Sequence>.findAllInstances(): Sequence> { + fun Sequence>.findAllInstances(): Sequence> { return onEach { (_, pluginClassLoader) -> val exportManagers = pluginClassLoader.findServices( ExportManager::class @@ -81,7 +143,9 @@ internal class BuiltInJvmPluginLoaderImpl( JvmPlugin::class, KotlinPlugin::class, JavaPlugin::class - ).loadAllServices() + ).loadAllServices().also { plugins -> + plugins.firstOrNull()?.logger?.let { pluginClassLoader.linkedLogger = it } + } }.flatMap { (f, list) -> list.associateBy { f }.asSequence() @@ -91,7 +155,7 @@ internal class BuiltInJvmPluginLoaderImpl( val filePlugins = this.filterNot { pluginFileToInstanceMap.containsKey(it) }.associateWith { - JvmPluginClassLoader(it, MiraiConsole::class.java.classLoader, classLoaders) + JvmPluginClassLoaderN.newLoader(it, jvmPluginLoadingCtx) }.onEach { (_, classLoader) -> classLoaders.add(classLoader) }.asSequence().findAllInstances().onEach { @@ -149,6 +213,36 @@ internal class BuiltInJvmPluginLoaderImpl( PluginManager.pluginsDataPath.moveNameFolder(plugin) PluginManager.pluginsConfigPath.moveNameFolder(plugin) check(plugin is JvmPluginInternal) { "A JvmPlugin must extend AbstractJvmPlugin to be loaded by JvmPluginLoader.BuiltIn" } + // region Link dependencies + plugin.javaClass.classLoader.safeCast()?.let { jvmPluginClassLoaderN -> + // Link plugin dependencies + plugin.description.dependencies.asSequence().mapNotNull { dependency -> + plugin.logger.verbose { "Linking dependency: ${dependency.id}" } + PluginManager.plugins.firstOrNull { it.id == dependency.id } + }.mapNotNull { it.javaClass.classLoader.safeCast() }.forEach { dependency -> + plugin.logger.debug { "Linked dependency: $dependency" } + jvmPluginClassLoaderN.dependencies.add(dependency) + } + // Link jar dependencies + fun InputStream?.readDependencies(): Collection { + if (this == null) return emptyList() + return bufferedReader().useLines { lines -> + lines.filterNot { it.isBlank() } + .filterNot { it.startsWith('#') } + .map { it.trim() } + .toMutableList() + } + } + jvmPluginClassLoaderN.linkPluginSharedLibraries( + plugin.logger, + jvmPluginClassLoaderN.getResourceAsStream("META-INF/mirai-console-plugin/dependencies-shared.txt").readDependencies() + ) + jvmPluginClassLoaderN.linkPluginPrivateLibraries( + plugin.logger, + jvmPluginClassLoaderN.getResourceAsStream("META-INF/mirai-console-plugin/dependencies-private.txt").readDependencies() + ) + } + // endregion plugin.internalOnLoad() }.getOrElse { throw PluginLoadException("Exception while loading ${plugin.description.smartToString()}", it) diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt index 67b57a6d10d..7a6d7a022ba 100644 --- a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt +++ b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt @@ -10,105 +10,267 @@ package net.mamoe.mirai.console.internal.plugin +import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.plugin.jvm.ExportManager +import net.mamoe.mirai.utils.* +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.graph.DependencyFilter import java.io.File import java.net.URL import java.net.URLClassLoader import java.util.* -import java.util.concurrent.ConcurrentHashMap +import java.util.zip.ZipFile -internal class JvmPluginClassLoader( - val file: File, +/* +Class resolving: + +| +`- Resolve standard classes: hard linked by console (@see AllDependenciesClassesHolder) +`- Resolve classes in shared libraries (Shared in all plugins) +| +|-===== SANDBOX ===== +| +`- Resolve classes in plugin dependency shared libraries (Shared by depend-ed plugins) +`- Resolve classes in independent libraries (Can only be loaded by current plugin) +`- Resolve classes in current jar. +`- Resolve classes from other plugin jar +`- Resolve by AppClassLoader + + */ + +internal class JvmPluginsLoadingCtx( + val sharedLibrariesLoader: DynLibClassLoader, + val pluginClassLoaders: MutableList, + val downloader: JvmPluginDependencyDownloader, +) { + val sharedLibrariesDependencies = HashSet() + val sharedLibrariesFilter: DependencyFilter = DependencyFilter { node, _ -> + return@DependencyFilter node.artifact.depId() !in sharedLibrariesDependencies + } +} + +internal class DynLibClassLoader( parent: ClassLoader?, - val classLoaders: Collection, -) : URLClassLoader(arrayOf(file.toURI().toURL()), parent) { - //// 只允许插件 getResource 时获取插件自身资源, #205 - override fun getResources(name: String?): Enumeration = findResources(name) - override fun getResource(name: String?): URL? = findResource(name) - // getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源 - // 因此无需 override getResourceAsStream +) : URLClassLoader(arrayOf(), parent) { + companion object { + init { + ClassLoader.registerAsParallelCapable() + } + } + + internal fun loadClassInThisClassLoader(name: String): Class<*>? { + synchronized(getClassLoadingLock(name)) { + findLoadedClass(name)?.let { return it } + try { + return findClass(name) + } catch (ignored: ClassNotFoundException) { + } + } + return null + } + + internal fun addLib(url: URL) { + addURL(url) + } + + internal fun addLib(file: File) { + addURL(file.toURI().toURL()) + } override fun toString(): String { - return "JvmPluginClassLoader{source=$file}" + return "DynLibClassLoader@" + hashCode() + } +} + +@Suppress("JoinDeclarationAndAssignment") +internal class JvmPluginClassLoaderN : URLClassLoader { + val file: File + val ctx: JvmPluginsLoadingCtx + val sharedLibrariesLogger: DynLibClassLoader + + val dependencies: MutableCollection = hashSetOf() + + lateinit var pluginSharedCL: DynLibClassLoader + lateinit var pluginIndependentCL: DynLibClassLoader + + @Suppress("PrivatePropertyName") + private val file_: File + get() = file + + var linkedLogger by lateinitMutableProperty { MiraiConsole.createLogger("JvmPlugin[" + file_.name + "]") } + val undefinedDependencies = mutableSetOf() + + private constructor(file: File, ctx: JvmPluginsLoadingCtx, unused: Unit) : super( + arrayOf(), ctx.sharedLibrariesLoader + ) { + this.sharedLibrariesLogger = ctx.sharedLibrariesLoader + this.file = file + this.ctx = ctx + init0() } - private val cache = ConcurrentHashMap>() + private constructor(file: File, ctx: JvmPluginsLoadingCtx) : super( + file.name, + arrayOf(), ctx.sharedLibrariesLoader + ) { + this.sharedLibrariesLogger = ctx.sharedLibrariesLoader + this.file = file + this.ctx = ctx + init0() + } + + private fun init0() { + ZipFile(file).use { zipFile -> + zipFile.entries().asSequence() + .filter { it.name.endsWith(".class") } + .map { it.name.substringBeforeLast('.') } + .map { it.removePrefix("/").replace('/', '.') } + .map { it.substringBeforeLast('.') } + .forEach { pkg -> + pluginMainPackages.add(pkg) + } + } + pluginSharedCL = DynLibClassLoader(ctx.sharedLibrariesLoader) + pluginIndependentCL = DynLibClassLoader(pluginSharedCL) + addURL(file.toURI().toURL()) + } + + private val pluginMainPackages: MutableSet = HashSet() internal var declaredFilter: ExportManager? = null + val sharedClLoadedDependencies = mutableSetOf() + internal fun containsSharedDependency( + dependency: String + ): Boolean { + if (dependency in sharedClLoadedDependencies) return true + return dependencies.any { it.containsSharedDependency(dependency) } + } + + internal fun linkPluginSharedLibraries(logger: MiraiLogger, dependencies: Collection) { + linkLibraries(logger, dependencies, true) + } + + internal fun linkPluginPrivateLibraries(logger: MiraiLogger, dependencies: Collection) { + linkLibraries(logger, dependencies, false) + } + + private fun linkLibraries(logger: MiraiLogger, dependencies: Collection, shared: Boolean) { + if (dependencies.isEmpty()) return + val results = ctx.downloader.resolveDependencies( + dependencies, ctx.sharedLibrariesFilter, + DependencyFilter { node, _ -> + return@DependencyFilter !containsSharedDependency(node.artifact.depId()) + }) + val files = results.artifactResults.mapNotNull { result -> + result.artifact?.let { it to it.file } + } + val linkType = if (shared) "(shared)" else "(private)" + files.forEach { (artifact, lib) -> + logger.verbose { "Linking $lib $linkType" } + if (shared) { + pluginSharedCL.addLib(lib) + sharedClLoadedDependencies.add(artifact.depId()) + } else { + pluginIndependentCL.addLib(lib) + } + logger.debug { "Linked $artifact $linkType" } + } + } + companion object { - val loadingLock = ConcurrentHashMap() + private val java9: Boolean init { ClassLoader.registerAsParallelCapable() + java9 = kotlin.runCatching { Class.forName("java.lang.Module") }.isSuccess + } + + fun newLoader(file: File, ctx: JvmPluginsLoadingCtx): JvmPluginClassLoaderN { + return when { + java9 -> JvmPluginClassLoaderN(file, ctx) + else -> JvmPluginClassLoaderN(file, ctx, Unit) + } } } - override fun findClass(name: String): Class<*> { - synchronized(kotlin.run { - val lock = Any() - loadingLock.putIfAbsent(name, lock) ?: lock - }) { - return findClass(name, false) ?: throw ClassNotFoundException(name) + internal fun resolvePluginSharedLibAndPluginClass(name: String): Class<*>? { + return try { + pluginSharedCL.loadClass(name) + } catch (e: ClassNotFoundException) { + resolvePluginPublicClass(name) } } - internal fun findClass(name: String, disableGlobal: Boolean): Class<*>? { - // First. Try direct load in cache. - val cachedClass = cache[name] - if (cachedClass != null) { - if (disableGlobal) { - val filter = declaredFilter - if (filter != null && !filter.isExported(name)) { - throw LoadingDeniedException(name) - } + internal fun resolvePluginPublicClass(name: String): Class<*>? { + if (pluginMainPackages.contains(name.pkgName())) { + if (declaredFilter?.isExported(name) == false) return null + synchronized(getClassLoadingLock(name)) { + findLoadedClass(name)?.let { return it } + return super.findClass(name) } - return cachedClass } - if (disableGlobal) { - // ==== Process Loading Request From JvmPluginClassLoader ==== - // - // If load from other classloader, - // means no other loaders are cached. - // direct load - return kotlin.runCatching { - super.findClass(name).also { cache[name] = it } - }.getOrElse { - if (it is ClassNotFoundException) null - else throw it - }?.also { - // This request is from other classloader, - // so we need to check the class is exported or not. - val filter = declaredFilter - if (filter != null && !filter.isExported(name)) { - throw LoadingDeniedException(name) - } + return null + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> = loadClass(name) + + override fun loadClass(name: String): Class<*> { + if (name.startsWith("io.netty") || name in AllDependenciesClassesHolder.allclasses) { + return AllDependenciesClassesHolder.appClassLoader.loadClass(name) + } + if (name.startsWith("net.mamoe.mirai.")) { // Avoid plugin classing cheating + try { + return AllDependenciesClassesHolder.appClassLoader.loadClass(name) + } catch (ignored: ClassNotFoundException) { } } + sharedLibrariesLogger.loadClassInThisClassLoader(name)?.let { return it } - // ==== Process Loading Request From JDK ClassLoading System ==== + // Search dependencies first + dependencies.forEach { dependency -> + dependency.resolvePluginSharedLibAndPluginClass(name)?.let { return it } + } + // Search in independent class loader + // @context: pluginIndependentCL.parent = pluinSharedCL + try { + return pluginIndependentCL.loadClass(name) + } catch (ignored: ClassNotFoundException) { + } - // First. scan other classLoaders's caches - classLoaders.forEach { otherClassloader -> - if (otherClassloader === this) return@forEach - val filter = otherClassloader.declaredFilter - if (otherClassloader.cache.containsKey(name)) { - return if (filter == null || filter.isExported(name)) { - otherClassloader.cache[name] - } else throw LoadingDeniedException("$name was not exported by $otherClassloader") + try { + synchronized(getClassLoadingLock(name)) { + findLoadedClass(name)?.let { return it } + return super.findClass(name) } + } catch (error: ClassNotFoundException) { + // Finally, try search from other plugins and console system + ctx.pluginClassLoaders.forEach { other -> + if (other !== this && other !in dependencies) { + other.resolvePluginPublicClass(name)?.let { + if (undefinedDependencies.add(other.file.name)) { + linkedLogger.warning { "Linked class $name in ${other.file.name} but plugin not depend on it." } + linkedLogger.warning { "Class loading logic may change in feature." } + } + return it + } + } + } + return AllDependenciesClassesHolder.appClassLoader.loadClass(name) } - classLoaders.forEach { otherClassloader -> - val other = kotlin.runCatching { - if (otherClassloader === this) super.findClass(name).also { cache[name] = it } - else otherClassloader.findClass(name, true) - }.onFailure { err -> - if (err is LoadingDeniedException || err !is ClassNotFoundException) - throw err - }.getOrNull() - if (other != null) return other - } - throw ClassNotFoundException(name) + } + + internal fun loadedClass(name: String): Class<*>? = super.findLoadedClass(name) + + //// 只允许插件 getResource 时获取插件自身资源, https://github.com/mamoe/mirai-console/issues/205 + override fun getResources(name: String?): Enumeration = findResources(name) + override fun getResource(name: String?): URL? = findResource(name) + // getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源 + // 因此无需 override getResourceAsStream + + override fun toString(): String { + return "JvmPluginClassLoader{${file.name}}" } } -internal class LoadingDeniedException(name: String) : ClassNotFoundException(name) +private fun String.pkgName(): String = substringBeforeLast('.', "") +internal fun Artifact.depId(): String = "$groupId:$artifactId" diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt new file mode 100644 index 00000000000..74286f9aa57 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.console.internal.plugin + +import net.mamoe.mirai.console.internal.MiraiConsoleBuildDependencies +import net.mamoe.mirai.console.internal.data.builtins.PluginDependenciesConfig +import net.mamoe.mirai.console.plugin.PluginManager +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.debug +import net.mamoe.mirai.utils.verbose +import org.apache.maven.repository.internal.MavenRepositorySystemUtils +import org.codehaus.plexus.util.ReaderFactory +import org.codehaus.plexus.util.xml.pull.MXParser +import org.codehaus.plexus.util.xml.pull.XmlPullParser +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.collection.CollectRequest +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory +import org.eclipse.aether.graph.Dependency +import org.eclipse.aether.graph.DependencyFilter +import org.eclipse.aether.repository.LocalRepository +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.repository.WorkspaceReader +import org.eclipse.aether.repository.WorkspaceRepository +import org.eclipse.aether.resolution.DependencyRequest +import org.eclipse.aether.resolution.DependencyResult +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory +import org.eclipse.aether.spi.connector.transport.TransporterFactory +import org.eclipse.aether.spi.locator.ServiceLocator +import org.eclipse.aether.transfer.AbstractTransferListener +import org.eclipse.aether.transfer.TransferEvent +import org.eclipse.aether.transport.http.HttpTransporterFactory +import java.io.File + + +@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate") +internal class JvmPluginDependencyDownloader( + val logger: MiraiLogger, +) { + val repositories: MutableList + val session: RepositorySystemSession + val locator: ServiceLocator + val repository: RepositorySystem + val dependencyFilter: DependencyFilter = DependencyFilter { node, parents -> + if (node == null || node.artifact == null) return@DependencyFilter true + + val artGroup = node.artifact.groupId + val artId = node.artifact.artifactId + + // mirai used netty-all + if (artGroup == "io.netty") return@DependencyFilter false + + if (artGroup == "net.mamoe") { + if (artId in listOf( + "mirai-core", + "mirai-core-jvm", + "mirai-core-android", + "mirai-core-api", + "mirai-core-api-jvm", + "mirai-core-api-android", + "mirai-core-utils", + "mirai-core-utils-jvm", + "mirai-core-utils-android", + "mirai-console", + "mirai-console-terminal", + ) + ) return@DependencyFilter false + } + + // Loaded by console system + if ("$artGroup:$artId" in MiraiConsoleBuildDependencies.dependencies) + return@DependencyFilter false + + // println(" `- filter: $node") + true + } + + init { + locator = MavenRepositorySystemUtils.newServiceLocator() + locator.addService(RepositoryConnectorFactory::class.java, BasicRepositoryConnectorFactory::class.java) + locator.addService(TransporterFactory::class.java, HttpTransporterFactory::class.java) + repository = locator.getService(RepositorySystem::class.java) + session = MavenRepositorySystemUtils.newSession() + session.checksumPolicy = "fail" + session.localRepositoryManager = repository.newLocalRepositoryManager( + session, LocalRepository(PluginManager.pluginLibrariesFolder) + ) + session.transferListener = object : AbstractTransferListener() { + override fun transferStarted(event: TransferEvent) { + logger.verbose { + "Downloading ${event.resource?.repositoryUrl}${event.resource?.resourceName}" + } + } + + override fun transferFailed(event: TransferEvent) { + logger.warning(event.exception) + } + } + val userHome = System.getProperty("user.home") + fun findMavenLocal(): File { + val mavenHome = File(userHome, ".m2") + fun findFromSettingsXml(): File? { + val settings = File(mavenHome, "settings.xml") + if (!settings.isFile) return null + ReaderFactory.newXmlReader(settings).use { reader -> + val parser = MXParser() + parser.setInput(reader) + + var eventType = parser.eventType + var joinedSettings = false + while (eventType != XmlPullParser.END_DOCUMENT) { + when (eventType) { + XmlPullParser.START_TAG -> { + if (!joinedSettings) { + if (parser.name != "settings") { + return null + } + joinedSettings = true + } else { + if (parser.name == "localRepository") { + val loc = File(parser.nextText()) + if (loc.isDirectory) return loc + return null + } else { + parser.skipSubTree() + } + } + } + // else -> parser.skipSubTree() + } + eventType = parser.next() + } + } + return null + } + return kotlin.runCatching { + findFromSettingsXml() + }.onFailure { error -> + logger.warning(error) + }.getOrNull() ?: File(mavenHome, "repository") + } + + fun findGradleDepCache(): File { + return File(userHome, ".gradle/caches/modules-2/files-2.1") + } + + val mavenLocRepo = findMavenLocal() + val gradleLocRepo = findGradleDepCache() + logger.debug { "Maven local: $mavenLocRepo" } + logger.debug { "Gradle cache local: $gradleLocRepo" } + session.workspaceReader = object : WorkspaceReader { + private val repository: WorkspaceRepository = WorkspaceRepository("default") + override fun getRepository(): WorkspaceRepository = repository + + override fun findArtifact(artifact: Artifact): File? { + // logger.debug { "Try resolve $artifact" } + val path = session.localRepositoryManager.getPathForLocalArtifact(artifact) + File(mavenLocRepo, path).takeIf { it.isFile }?.let { return it } + val gradleDep = gradleLocRepo + .resolve(artifact.groupId) + .resolve(artifact.artifactId) + .resolve(artifact.baseVersion) + if (gradleDep.isDirectory) { + val fileName = buildString { + append(artifact.artifactId) + append('-') + append(artifact.baseVersion) + artifact.classifier?.takeIf { it.isNotEmpty() }?.let { c -> + append('-').append(c) + } + append('.').append(artifact.extension) + } + gradleDep.walk().maxDepth(2) + .filter { it.isFile } + .firstOrNull { it.name == fileName } + ?.let { return it } + } + return null + } + + override fun findVersions(artifact: Artifact?): MutableList { + return mutableListOf() + } + + } + session.setReadOnly() + repositories = repository.newResolutionRepositories( + session, + PluginDependenciesConfig.repoLoc.map { url -> + RemoteRepository.Builder(null, "default", url).build() + } + ) + logger.debug { "Remote server: " + PluginDependenciesConfig.repoLoc } + } + + public fun resolveDependencies(deps: Collection, vararg filters: DependencyFilter): DependencyResult { + + val dependencies: MutableList = ArrayList() + for (library in deps) { + val defaultArtifact = DefaultArtifact(library) + val dependency = Dependency(defaultArtifact, null) + dependencies.add(dependency) + } + return repository.resolveDependencies( + session, DependencyRequest( + CollectRequest( + null as Dependency?, dependencies, + repositories + ), + when { + filters.isEmpty() -> dependencyFilter + else -> DependencyFilter { node, parents -> + if (node == null || node.artifact == null) return@DependencyFilter true + if (!dependencyFilter.accept(node, parents)) return@DependencyFilter false + filters.forEach { filter -> + if (!filter.accept(node, parents)) return@DependencyFilter false + } + return@DependencyFilter true + } + } + ) + ) + } +} diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/PluginManagerImpl.kt b/mirai-console/backend/mirai-console/src/internal/plugin/PluginManagerImpl.kt index 56a3dd0ff00..c2d744f30f3 100644 --- a/mirai-console/backend/mirai-console/src/internal/plugin/PluginManagerImpl.kt +++ b/mirai-console/backend/mirai-console/src/internal/plugin/PluginManagerImpl.kt @@ -48,6 +48,12 @@ internal class PluginManagerImpl( override val pluginsConfigPath: Path = MiraiConsole.rootPath.resolve("config").apply { mkdir() } override val pluginsConfigFolder: File = pluginsConfigPath.toFile() + override val pluginLibrariesPath: Path = MiraiConsole.rootPath.resolve("plugin-libraries").apply { mkdir() } + override val pluginLibrariesFolder: File = pluginLibrariesPath.toFile() + + override val pluginSharedLibrariesPath: Path = MiraiConsole.rootPath.resolve("plugin-shared-libraries").apply { mkdir() } + override val pluginSharedLibrariesFolder: File = pluginSharedLibrariesPath.toFile() + @Suppress("ObjectPropertyName") private val _pluginLoaders: MutableList> by lazy { builtInLoaders.toMutableList() diff --git a/mirai-console/backend/mirai-console/src/internal/util/CommonUtils.kt b/mirai-console/backend/mirai-console/src/internal/util/CommonUtils.kt index 4fbe0e58571..fdf60d03fd0 100644 --- a/mirai-console/backend/mirai-console/src/internal/util/CommonUtils.kt +++ b/mirai-console/backend/mirai-console/src/internal/util/CommonUtils.kt @@ -38,7 +38,7 @@ internal inline fun runIgnoreException(block: () -> Unit internal fun StackFrame.findLoader(): ClassLoader? { classInstance?.let { return it.classLoader } return runCatching { - JvmPluginLoader.implOrNull?.classLoaders?.firstOrNull { it.findClass(className, true) != null } + JvmPluginLoader.implOrNull?.findLoadedClass(className)?.classLoader }.getOrNull() } diff --git a/mirai-console/backend/mirai-console/src/plugin/PluginManager.kt b/mirai-console/backend/mirai-console/src/plugin/PluginManager.kt index 02947891930..1207fffa970 100644 --- a/mirai-console/backend/mirai-console/src/plugin/PluginManager.kt +++ b/mirai-console/backend/mirai-console/src/plugin/PluginManager.kt @@ -82,6 +82,44 @@ public interface PluginManager { */ public val pluginsConfigFolder: File + /** + * 插件运行时依赖存放路径 [Path], 插件自动下载的依赖都会存放于此目录 + * + * **实现细节**: 在 terminal 前端实现为 `$rootPath/plugin-libraries`, + * 依赖 jar 文件由插件共享, 但是运行时插件加载的类是互相隔离的 + * + * @since 2.11 + */ + public val pluginLibrariesPath: Path + + /** + * 插件运行时依赖存放路径 [File], 插件自动下载的依赖都会存放于此目录 + * + * **实现细节**: 在 terminal 前端实现为 `$rootPath/plugin-libraries`, + * 依赖 jar 文件由插件共享, 但是运行时插件加载的类是互相隔离的 + * + * @since 2.11 + */ + public val pluginLibrariesFolder: File + + /** + * 插件运行时依赖存放路径 [Path], 该路径下的依赖由全部插件共享 + * + * **实现细节**: 在 terminal 前端实现为 `$rootPath/plugin-shared-libraries` + * + * @since 2.11 + */ + public val pluginSharedLibrariesPath: Path + + /** + * 插件运行时依赖存放路径 [File], 该路径下的依赖由全部插件共享 + * + * **实现细节**: 在 terminal 前端实现为 `$rootPath/plugin-shared-libraries` + * + * @since 2.11 + */ + public val pluginSharedLibrariesFolder: File + // endregion diff --git a/mirai-console/backend/mirai-console/src/plugin/jvm/JvmPluginLoader.kt b/mirai-console/backend/mirai-console/src/plugin/jvm/JvmPluginLoader.kt index f68a0b02adc..4882ed6a02a 100644 --- a/mirai-console/backend/mirai-console/src/plugin/jvm/JvmPluginLoader.kt +++ b/mirai-console/backend/mirai-console/src/plugin/jvm/JvmPluginLoader.kt @@ -44,6 +44,9 @@ public interface JvmPluginLoader : CoroutineScope, FilePluginLoader + @MiraiInternalApi + public fun findLoadedClass(name: String): Class<*>? + public companion object BuiltIn : JvmPluginLoader by (dynamicDelegation { MiraiConsoleImplementation.getInstance().jvmPluginLoader }) { diff --git a/mirai-console/tools/gradle-plugin/README.md b/mirai-console/tools/gradle-plugin/README.md index c8169f6b3be..e0a0ff6ad74 100644 --- a/mirai-console/tools/gradle-plugin/README.md +++ b/mirai-console/tools/gradle-plugin/README.md @@ -40,6 +40,23 @@ mirai { // this: MiraiConsoleExtension DSL 详见 [MiraiConsoleExtension](src/MiraiConsoleExtension.kt)。 +### 打包依赖 + +Mirai Console Gradle 在打包 JAR(`buildPlugin`) 时不会携带任何外部依赖, +而是会保存一份依赖列表,在加载插件时下载, +如果您使用了不可在 `Maven Central` 搜索到的依赖, 请使用以下配置告知 mirai-console-gradle + +```groovy +dependencies { + implementation "org.example:test:1.0.0" + + // 无需版本号 + shadowLink "org.example:test" + // build.gradle.kts + "shadowLink"("org.example:test") +} +``` + ### `publishPlugin` 配置好 Bintray 参数,使用 `./gradlew publishPlugin` 可自动发布并上传插件到 Bintray。 @@ -57,9 +74,9 @@ mirai { *2021/3/21 更新:* 由于 Bintray JCenter 即将关闭,随着论坛的发展,mirai 正在策划插件中心服务。待插件中心完成后将会提供更好的插件分发平台。 -#### 排除依赖 +#### 排除依赖 (过时) -如果要在打包 JAR(`buildPlugin`)时排除一些依赖,请使用如下配置: +如果要在打包 JAR(`buildPluginLegacy`)时排除一些依赖,请使用如下配置: ```kotlin mirai { diff --git a/mirai-console/tools/gradle-plugin/src/integTest/kotlin/AbstractTest.kt b/mirai-console/tools/gradle-plugin/src/integTest/kotlin/AbstractTest.kt index 29e0328e060..0e6c651a6a8 100644 --- a/mirai-console/tools/gradle-plugin/src/integTest/kotlin/AbstractTest.kt +++ b/mirai-console/tools/gradle-plugin/src/integTest/kotlin/AbstractTest.kt @@ -12,6 +12,8 @@ package net.mamoe.mirai.console.gradle import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.internal.PluginUnderTestMetadataReading import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.api.io.TempDir import java.io.File @@ -91,4 +93,16 @@ abstract class AbstractTest { // """ } + + @JvmField + @RegisterExtension + internal val after: AfterEachCallback = AfterEachCallback { context -> + if (context.executionException.isPresent) { + val inst = context.requiredTestInstance as AbstractTest + println("====================== build.gradle ===========================") + println(inst.tempDir.resolve("build.gradle").readText()) + println("==================== settings.gradle ==========================") + println(inst.tempDir.resolve("settings.gradle").readText()) + } + } } \ No newline at end of file diff --git a/mirai-console/tools/gradle-plugin/src/integTest/kotlin/TestBuildPlugin.kt b/mirai-console/tools/gradle-plugin/src/integTest/kotlin/TestBuildPlugin.kt index 7d0b392b2d4..ffb6bb2ccf6 100644 --- a/mirai-console/tools/gradle-plugin/src/integTest/kotlin/TestBuildPlugin.kt +++ b/mirai-console/tools/gradle-plugin/src/integTest/kotlin/TestBuildPlugin.kt @@ -10,14 +10,49 @@ package net.mamoe.mirai.console.gradle import org.junit.jupiter.api.Test +import java.util.zip.ZipFile +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class TestBuildPlugin : AbstractTest() { @Test fun `can build plugin`() { + tempDir.resolve("build.gradle").appendText( + """ + dependencies { + api "com.zaxxer:SparseBitSet:1.2" + implementation "com.google.code.gson:gson:2.8.9" + api "org.slf4j:slf4j-simple:1.7.32" + shadowLink "org.slf4j:slf4j-simple" + } + """.trimIndent() + ) gradleRunner() - .withArguments("buildPlugin", "--stacktrace") + .withArguments("buildPlugin", "dependencies", "--stacktrace", "--info") .build() + val jar = tempDir.resolve("build/libs").listFiles()!!.first { it.name.endsWith(".mirai.jar") } + ZipFile(jar).use { zipFile -> + + assertNotNull(zipFile.getEntry("org/slf4j/impl/SimpleLogger.class")) + + val dpPrivate = zipFile.getInputStream( + zipFile.getEntry("META-INF/mirai-console-plugin/dependencies-private.txt") + ).use { it.readBytes().decodeToString() } + val dpShared = zipFile.getInputStream( + zipFile.getEntry("META-INF/mirai-console-plugin/dependencies-shared.txt") + ).use { it.readBytes().decodeToString() } + + assertTrue { dpShared.contains("com.zaxxer:SparseBitSet:1.2") } + assertFalse { dpShared.contains("com.google.code.gson:gson") } + assertFalse { dpShared.contains("org.slf4j:slf4j-simple") } + + assertTrue { dpPrivate.contains("com.zaxxer:SparseBitSet:1.2") } + assertTrue { dpPrivate.contains("com.google.code.gson:gson:2.8.9") } + assertFalse { dpPrivate.contains("org.slf4j:slf4j-simple") } + } + } } \ No newline at end of file diff --git a/mirai-console/tools/gradle-plugin/src/main/kotlin/BuildMiraiPluginV2.kt b/mirai-console/tools/gradle-plugin/src/main/kotlin/BuildMiraiPluginV2.kt new file mode 100644 index 00000000000..77a95da3d98 --- /dev/null +++ b/mirai-console/tools/gradle-plugin/src/main/kotlin/BuildMiraiPluginV2.kt @@ -0,0 +1,205 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.console.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.ExternalModuleDependency +import org.gradle.api.artifacts.ResolvedArtifact +import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.attributes.AttributeContainer +import org.gradle.api.capabilities.Capability +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.internal.artifacts.ivyservice.DefaultLenientConfiguration +import org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ArtifactVisitor +import org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ResolvableArtifact +import org.gradle.api.internal.file.FileCollectionInternal +import org.gradle.api.internal.file.FileCollectionStructureVisitor +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskContainer +import org.gradle.internal.DisplayName +import org.gradle.internal.component.external.model.ModuleComponentArtifactIdentifier +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.get +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import java.io.File +import javax.inject.Inject + +@Suppress("RedundantLambdaArrow", "RemoveExplicitTypeArguments") +public open class BuildMiraiPluginV2 : Jar() { + + // @get:Internal + private lateinit var metadataTask: GenMetadataTask + + internal open class GenMetadataTask + @Inject internal constructor( + @JvmField internal val orgTask: BuildMiraiPluginV2, + ) : DefaultTask() { + companion object { + val miraiDependencies = mutableSetOf( + "net.mamoe:mirai-core-api", + "net.mamoe:mirai-core-api-jvm", + "net.mamoe:mirai-core-api-android", + "net.mamoe:mirai-core", + "net.mamoe:mirai-core-jvm", + "net.mamoe:mirai-core-android", + "net.mamoe:mirai-core-utils", + "net.mamoe:mirai-core-utils-jvm", + "net.mamoe:mirai-core-utils-android", + "net.mamoe:mirai-console", + "net.mamoe:mirai-console-terminal", + ) + } + @TaskAction + internal fun run() { + val runtime = mutableSetOf() + val api = mutableSetOf() + val linkedDependencies = mutableSetOf() + val linkToApi = mutableSetOf() + val shadowedFiles = mutableSetOf() + val shadowedDependencies = mutableSetOf() + + project.configurations.findByName(MiraiConsoleGradlePlugin.MIRAI_SHADOW_CONF_NAME)?.allDependencies?.forEach { dep -> + if (dep is ExternalModuleDependency) { + val artId = "${dep.group}:${dep.name}" + shadowedDependencies.add(artId) + } + } + project.configurations.findByName("apiElements")?.allDependencies?.forEach { dep -> + if (dep is ExternalModuleDependency) { + val artId = "${dep.group}:${dep.name}" + linkedDependencies.add(artId) + linkToApi.add(artId) + } + } + project.configurations.findByName("implementation")?.allDependencies?.forEach { dep -> + if (dep is ExternalModuleDependency) { + linkedDependencies.add("${dep.group}:${dep.name}") + } + } + linkedDependencies.removeAll(shadowedDependencies) + linkToApi.removeAll(shadowedDependencies) + linkedDependencies.addAll(miraiDependencies) + + fun ResolvedDependency.depId(): String = "$moduleGroup:$moduleName" + + val runtimeClasspath = project.configurations["runtimeClasspath"].resolvedConfiguration + fun markAsResolved(resolvedDependency: ResolvedDependency) { + val depId = resolvedDependency.depId() + linkedDependencies.add(depId) + resolvedDependency.children.forEach { markAsResolved(it) } + } + + fun linkDependencyTo(resolvedDependency: ResolvedDependency, dependencies: MutableCollection) { + dependencies.add(resolvedDependency.module.toString()) + resolvedDependency.children.forEach { linkDependencyTo(it, dependencies) } + } + + fun resolveDependency(resolvedDependency: ResolvedDependency) { + val depId = resolvedDependency.depId() + if (depId in linkedDependencies) { + markAsResolved(resolvedDependency) + linkDependencyTo(resolvedDependency, runtime) + if (depId in linkToApi) { + linkDependencyTo(resolvedDependency, api) + } + return + } + } + runtimeClasspath.firstLevelModuleDependencies.forEach { resolveDependency(it) } + + logger.info { "linkedDependencies: $linkedDependencies" } + logger.info { "linkToAPi : $linkToApi" } + logger.info { "api : $api" } + logger.info { "runtime : $runtime" } + + val lenientConfiguration = runtimeClasspath.lenientConfiguration + if (lenientConfiguration is DefaultLenientConfiguration) { + val resolvedArtifacts = mutableSetOf() + lenientConfiguration.select().visitArtifacts(object : ArtifactVisitor { + override fun prepareForVisit(source: FileCollectionInternal.Source): FileCollectionStructureVisitor.VisitType { + return FileCollectionStructureVisitor.VisitType.Visit + } + + override fun visitArtifact( + variantName: DisplayName, + variantAttributes: AttributeContainer, + capabilities: MutableList, + artifact: ResolvableArtifact + ) { + resolvedArtifacts.add(artifact.toPublicView()) + } + + override fun requireArtifactFiles(): Boolean = false + override fun visitFailure(failure: Throwable) {} + }, false) + resolvedArtifacts + } else { + runtimeClasspath.resolvedArtifacts + }.forEach { artifact -> + val artId = artifact.id + if (artId is ModuleComponentArtifactIdentifier) { + val cid = artId.componentIdentifier + if ("${cid.group}:${cid.module}" in linkedDependencies) { + return@forEach + } + } + logger.info { " `- $artId - ${artId.javaClass}" } + shadowedFiles.add(artifact.file) + } + + shadowedFiles.forEach { file -> + if (file.isDirectory) { + orgTask.from(file) + } else if (file.extension == "jar") { + orgTask.from(project.zipTree(file)) + } else { + orgTask.from(file) + } + } + + temporaryDir.also { + it.mkdirs() + }.let { tmpDir -> + tmpDir.resolve("api.txt").writeText(api.sorted().joinToString("\n")) + tmpDir.resolve("runtime.txt").writeText(runtime.sorted().joinToString("\n")) + orgTask.from(tmpDir.resolve("api.txt")) { copy -> + copy.into("META-INF/mirai-console-plugin") + copy.rename { "dependencies-shared.txt" } + } + orgTask.from(tmpDir.resolve("runtime.txt")) { copy -> + copy.into("META-INF/mirai-console-plugin") + copy.rename { "dependencies-private.txt" } + } + } + } + } + + internal fun registerMetadataTask(tasks: TaskContainer, metadataTaskName: String) { + metadataTask = tasks.create(metadataTaskName, this) + } + + internal fun init(target: KotlinTarget) { + dependsOn(metadataTask) + archiveExtension.set("mirai.jar") + duplicatesStrategy = DuplicatesStrategy.WARN + + val compilations = target.compilations.filter { it.name == KotlinCompilation.MAIN_COMPILATION_NAME } + compilations.forEach { + dependsOn(it.compileKotlinTask) + from(it.output.allOutputs) + metadataTask.dependsOn(it.compileKotlinTask) + } + exclude { elm -> + elm.path.startsWith("META-INF/") && elm.name.endsWith(".sf", ignoreCase = true) + } + } +} \ No newline at end of file diff --git a/mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt b/mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt index 5690f99f985..5bb2773d44a 100644 --- a/mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt +++ b/mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt @@ -30,6 +30,10 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.KotlinTarget public class MiraiConsoleGradlePlugin : Plugin { + internal companion object { + const val MIRAI_SHADOW_CONF_NAME: String = "shadowLink" + } + private fun KotlinSourceSet.configureSourceSet(project: Project, target: KotlinTarget) { try { @Suppress("DEPRECATION") // user may use 1.4 @@ -113,12 +117,19 @@ public class MiraiConsoleGradlePlugin : Plugin { fun registerBuildPluginTask(target: KotlinTarget, isSingleTarget: Boolean) { tasks.create( "buildPlugin".wrapNameWithPlatform(target, isSingleTarget), + BuildMiraiPluginV2::class.java + ).also { buildPluginV2 -> + buildPluginV2.registerMetadataTask(tasks, "miraiPrepareMetadata".wrapNameWithPlatform(target, isSingleTarget)) + buildPluginV2.init(target) + } + tasks.create( + "buildPluginLegacy".wrapNameWithPlatform(target, isSingleTarget), BuildMiraiPluginTask::class.java, target ).apply shadow@{ group = "mirai" - archiveExtension.set("mirai.jar") + archiveExtension.set("legacy.mirai.jar") val compilations = target.compilations.filter { it.name == MAIN_COMPILATION_NAME } @@ -153,6 +164,10 @@ public class MiraiConsoleGradlePlugin : Plugin { } } + private fun Project.setupConfigurations() { + configurations.create(MIRAI_SHADOW_CONF_NAME).isCanBeResolved = false + } + override fun apply(target: Project): Unit = with(target) { extensions.create("mirai", MiraiConsoleExtension::class.java) @@ -162,6 +177,8 @@ public class MiraiConsoleGradlePlugin : Plugin { plugins.apply(ShadowPlugin::class.java) plugins.apply(BintrayPlugin::class.java) + project.setupConfigurations() + afterEvaluate { configureCompileTarget() kotlinTargets.forEach { configureTarget(it) } diff --git a/mirai-console/tools/gradle-plugin/src/main/kotlin/dsl.kt b/mirai-console/tools/gradle-plugin/src/main/kotlin/dsl.kt new file mode 100644 index 00000000000..4ea40a4515c --- /dev/null +++ b/mirai-console/tools/gradle-plugin/src/main/kotlin/dsl.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.console.gradle + +import org.gradle.api.logging.Logger + +internal inline fun Logger.info(msg: () -> String) { + if (isInfoEnabled) info(msg()) +} \ No newline at end of file