diff --git a/mirai-console/tools/gradle-plugin/README.md b/mirai-console/tools/gradle-plugin/README.md index c8169f6b3..e0a0ff6ad 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 29e0328e0..0e6c651a6 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 7d0b392b2..ffb6bb2cc 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 000000000..77a95da3d --- /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 5690f99f9..5bb2773d4 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 000000000..4ea40a451 --- /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