gradle plugin

This commit is contained in:
Karlatemp 2022-01-20 22:01:25 +08:00
parent d32913aa9f
commit 5df5ecc444
No known key found for this signature in database
GPG Key ID: C6B606FF23D8FED7
6 changed files with 308 additions and 4 deletions

View File

@ -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 {

View File

@ -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())
}
}
}

View File

@ -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") }
}
}
}

View File

@ -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<String>()
val api = mutableSetOf<String>()
val linkedDependencies = mutableSetOf<String>()
val linkToApi = mutableSetOf<String>()
val shadowedFiles = mutableSetOf<File>()
val shadowedDependencies = mutableSetOf<String>()
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<String>) {
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<ResolvedArtifact>()
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<out Capability>,
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<GenMetadataTask>(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)
}
}
}

View File

@ -30,6 +30,10 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
public class MiraiConsoleGradlePlugin : Plugin<Project> {
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<Project> {
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<Project> {
}
}
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<Project> {
plugins.apply(ShadowPlugin::class.java)
plugins.apply(BintrayPlugin::class.java)
project.setupConfigurations()
afterEvaluate {
configureCompileTarget()
kotlinTargets.forEach { configureTarget(it) }

View File

@ -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())
}