Pārlūkot izejas kodu

Fix gradle multi-projects packaging error; fix #1973

Karlatemp 3 gadi atpakaļ
vecāks
revīzija
7f3b67ad9e

+ 14 - 0
mirai-console/tools/gradle-plugin/README.md

@@ -70,6 +70,20 @@ dependencies {
 }
 ```
 
+特别的, 如果使用了子项目 (Gradle MultiProjects), Mirai Console Gradle 默认也会打包进 JAR.
+
+如果您希望 Mirai Console Gradle 像处理一般依赖一样处理 Gradle 子项目, 请使用以下配置告知
+
+```groovy
+dependencies {
+    implementation project(":nested")
+
+    asNormalDep project(":nested")
+    // build.gradle.kts
+    "asNormalDep"(project(":nested"))
+}
+```
+
 ### `publishPlugin`
 
 配置好 Bintray 参数,使用 `./gradlew publishPlugin` 可自动发布并上传插件到 Bintray。

+ 153 - 1
mirai-console/tools/gradle-plugin/src/integTest/kotlin/TestBuildPlugin.kt

@@ -7,9 +7,13 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
+@file:Suppress("DuplicatedCode")
+
 package net.mamoe.mirai.console.gradle
 
+import org.junit.jupiter.api.DisplayName
 import org.junit.jupiter.api.Test
+import java.io.File
 import java.util.zip.ZipFile
 import kotlin.test.assertFalse
 import kotlin.test.assertNotNull
@@ -17,6 +21,148 @@ import kotlin.test.assertTrue
 
 class TestBuildPlugin : AbstractTest() {
 
+    @Test
+    @DisplayName("project as normal dependency")
+    fun buildWithMultiProjectsAsNormalDependency() {
+        settingsFile.appendText(
+            """
+            include("nested")
+        """.trimIndent()
+        )
+        tempDir.resolve("nested").also { it.mkdirs() }.resolve("build.gradle").writeText(
+            """
+            plugins {
+                id("org.jetbrains.kotlin.jvm")
+                id("net.mamoe.mirai-console")
+            }
+            dependencies {
+                api "com.zaxxer:SparseBitSet:1.2"
+            }
+            repositories {
+                mavenCentral()
+            }
+        """.trimIndent()
+        )
+
+        tempDir.resolve("build.gradle").appendText(
+            """
+            dependencies {
+                implementation project(":nested") 
+                asNormalDep project(":nested") 
+            }
+        """.trimIndent()
+        )
+
+        gradleRunner()
+            .withArguments(":buildPlugin", "--stacktrace", "--info")
+            .build()
+
+
+        ZipFile(findJar()).use { zipFile ->
+
+            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() }
+
+            assertFalse { dpShared.contains("com.zaxxer:SparseBitSet:1.2") }
+            assertTrue { dpPrivate.contains("com.zaxxer:SparseBitSet:1.2") }
+            assertTrue { dpPrivate.contains(":nested") }
+        }
+    }
+
+    @Test
+    @DisplayName("no api extends if using implementation")
+    fun buildWithMultiProjectsWithoutApi() {
+        settingsFile.appendText(
+            """
+            include("nested")
+        """.trimIndent()
+        )
+        tempDir.resolve("nested").also { it.mkdirs() }.resolve("build.gradle").writeText(
+            """
+            plugins {
+                id("org.jetbrains.kotlin.jvm")
+                id("net.mamoe.mirai-console")
+            }
+            dependencies {
+                api "com.zaxxer:SparseBitSet:1.2"
+            }
+            repositories {
+                mavenCentral()
+            }
+        """.trimIndent()
+        )
+
+        tempDir.resolve("build.gradle").appendText(
+            """
+            dependencies {
+                implementation project(":nested") 
+            }
+        """.trimIndent()
+        )
+
+        gradleRunner()
+            .withArguments(":buildPlugin", "--stacktrace", "--info")
+            .build()
+
+
+        ZipFile(findJar()).use { zipFile ->
+
+            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() }
+
+            assertFalse { dpShared.contains("com.zaxxer:SparseBitSet:1.2") }
+            assertTrue { dpPrivate.contains("com.zaxxer:SparseBitSet:1.2") }
+        }
+    }
+
+    @Test
+    @DisplayName("build with multi projects")
+    fun buildWithMultiProjects() {
+        settingsFile.appendText(
+            """
+            include("nested")
+        """.trimIndent()
+        )
+        tempDir.resolve("nested").also { it.mkdirs() }.resolve("build.gradle").writeText(
+            """
+            plugins {
+                id("org.jetbrains.kotlin.jvm")
+                id("net.mamoe.mirai-console")
+            }
+            dependencies {
+                api "com.zaxxer:SparseBitSet:1.2"
+                implementation "com.google.code.gson:gson:2.8.9"
+                api "org.slf4j:slf4j-simple:1.7.32"
+            }
+            repositories {
+                mavenCentral()
+            }
+        """.trimIndent()
+        )
+
+        tempDir.resolve("build.gradle").appendText(
+            """
+            dependencies {
+                api project(":nested") 
+                shadowLink "org.slf4j:slf4j-simple"
+            }
+        """.trimIndent()
+        )
+
+        gradleRunner()
+            .withArguments(":buildPlugin", "dependencies", "--stacktrace", "--info")
+            .build()
+        checkOutput()
+    }
+
     @Test
     fun `can build plugin`() {
         tempDir.resolve("build.gradle").appendText(
@@ -32,7 +178,13 @@ class TestBuildPlugin : AbstractTest() {
         gradleRunner()
             .withArguments("buildPlugin", "dependencies", "--stacktrace", "--info")
             .build()
-        val jar = tempDir.resolve("build/libs").listFiles()!!.first { it.name.endsWith(".mirai.jar") }
+        checkOutput()
+    }
+
+    private fun findJar(): File = tempDir.resolve("build/libs").listFiles()!!.first { it.name.endsWith(".mirai.jar") }
+
+    private fun checkOutput() {
+        val jar = findJar()
         ZipFile(jar).use { zipFile ->
 
             assertNotNull(zipFile.getEntry("org/slf4j/impl/SimpleLogger.class"))

+ 61 - 11
mirai-console/tools/gradle-plugin/src/main/kotlin/BuildMiraiPluginV2.kt

@@ -10,9 +10,9 @@
 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.Project
+import org.gradle.api.artifacts.*
+import org.gradle.api.artifacts.component.ProjectComponentIdentifier
 import org.gradle.api.attributes.AttributeContainer
 import org.gradle.api.capabilities.Capability
 import org.gradle.api.file.DuplicatesStrategy
@@ -58,6 +58,8 @@ public open class BuildMiraiPluginV2 : Jar() {
                 "net.mamoe:mirai-console-terminal",
             )
         }
+
+        @Suppress("LocalVariableName")
         @TaskAction
         internal fun run() {
             val runtime = mutableSetOf<String>()
@@ -66,6 +68,8 @@ public open class BuildMiraiPluginV2 : Jar() {
             val linkToApi = mutableSetOf<String>()
             val shadowedFiles = mutableSetOf<File>()
             val shadowedDependencies = mutableSetOf<String>()
+            val subprojects = mutableSetOf<String>()
+            val subprojects_fullpath = mutableSetOf<String>()
 
             project.configurations.findByName(MiraiConsoleGradlePlugin.MIRAI_SHADOW_CONF_NAME)?.allDependencies?.forEach { dep ->
                 if (dep is ExternalModuleDependency) {
@@ -73,18 +77,52 @@ public open class BuildMiraiPluginV2 : Jar() {
                     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(MiraiConsoleGradlePlugin.MIRAI_AS_NORMAL_DEP_CONF_NAME)?.allDependencies?.forEach { dep ->
+                if (dep is ProjectDependency) {
+                    linkedDependencies.add("${dep.group}:${dep.name}")
                 }
             }
-            project.configurations.findByName("implementation")?.allDependencies?.forEach { dep ->
-                if (dep is ExternalModuleDependency) {
-                    linkedDependencies.add("${dep.group}:${dep.name}")
+
+            fun deepForeachDependencies(conf: Configuration?, action: (Dependency) -> Unit) {
+                (conf ?: return).allDependencies.forEach { dep ->
+                    action(dep)
+                    if (dep is ProjectDependency) {
+                        subprojects.add("${dep.group}:${dep.name}")
+                        deepForeachDependencies(dep.dependencyProject.configurations.findByName(conf.name), action)
+                    }
+                }
+
+            }
+
+            fun resolveProject(project: Project, doResolveApi: Boolean) {
+                deepForeachDependencies(project.configurations.findByName("apiElements")) { dep ->
+                    if (dep is ExternalModuleDependency) {
+                        val artId = "${dep.group}:${dep.name}"
+                        linkedDependencies.add(artId)
+                        if (doResolveApi) {
+                            linkToApi.add(artId)
+                        }
+                    }
+                    if (dep is ProjectDependency) {
+                        subprojects_fullpath.add(dep.dependencyProject.path)
+                        subprojects.add("${dep.group}:${dep.name}")
+                        resolveProject(dep.dependencyProject, doResolveApi)
+                    }
+                }
+
+                project.configurations.findByName("implementation")?.allDependencies?.forEach { dep ->
+                    if (dep is ExternalModuleDependency) {
+                        linkedDependencies.add("${dep.group}:${dep.name}")
+                    }
+                    if (dep is ProjectDependency) {
+                        subprojects_fullpath.add(dep.dependencyProject.path)
+                        subprojects.add("${dep.group}:${dep.name}")
+                        resolveProject(dep.dependencyProject, false)
+                    }
                 }
             }
+
+            resolveProject(project, true)
             linkedDependencies.removeAll(shadowedDependencies)
             linkToApi.removeAll(shadowedDependencies)
             linkedDependencies.addAll(miraiDependencies)
@@ -105,6 +143,7 @@ public open class BuildMiraiPluginV2 : Jar() {
 
             fun resolveDependency(resolvedDependency: ResolvedDependency) {
                 val depId = resolvedDependency.depId()
+                logger.info { "resolving         : $depId" }
                 if (depId in linkedDependencies) {
                     markAsResolved(resolvedDependency)
                     linkDependencyTo(resolvedDependency, runtime)
@@ -113,6 +152,10 @@ public open class BuildMiraiPluginV2 : Jar() {
                     }
                     return
                 }
+                if (depId in subprojects) {
+                    resolvedDependency.children.forEach { resolveDependency(it) }
+                    return
+                }
             }
             runtimeClasspath.firstLevelModuleDependencies.forEach { resolveDependency(it) }
 
@@ -120,6 +163,7 @@ public open class BuildMiraiPluginV2 : Jar() {
             logger.info { "linkToAPi         : $linkToApi" }
             logger.info { "api               : $api" }
             logger.info { "runtime           : $runtime" }
+            logger.info { "subprojects       : $subprojects" }
 
             val lenientConfiguration = runtimeClasspath.lenientConfiguration
             if (lenientConfiguration is DefaultLenientConfiguration) {
@@ -152,6 +196,12 @@ public open class BuildMiraiPluginV2 : Jar() {
                         return@forEach
                     }
                 }
+                val cid = artId.componentIdentifier
+                if (cid is ProjectComponentIdentifier) {
+                    if (cid.projectPath in subprojects_fullpath) {
+                        return@forEach
+                    }
+                }
                 logger.info { "  `- $artId - ${artId.javaClass}" }
                 shadowedFiles.add(artifact.file)
             }

+ 2 - 0
mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt

@@ -32,6 +32,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
 public class MiraiConsoleGradlePlugin : Plugin<Project> {
     internal companion object {
         const val MIRAI_SHADOW_CONF_NAME: String = "shadowLink"
+        const val MIRAI_AS_NORMAL_DEP_CONF_NAME: String = "asNormalDep"
     }
 
     private fun KotlinSourceSet.configureSourceSet(project: Project, target: KotlinTarget) {
@@ -167,6 +168,7 @@ public class MiraiConsoleGradlePlugin : Plugin<Project> {
 
     private fun Project.setupConfigurations() {
         configurations.create(MIRAI_SHADOW_CONF_NAME).isCanBeResolved = false
+        configurations.create(MIRAI_AS_NORMAL_DEP_CONF_NAME).isCanBeResolved = false
     }
 
     override fun apply(target: Project): Unit = with(target) {