Преглед на файлове

[build] Rewrite shadow relocation; fix dependency graph issues with Android, and improve build performance:

- Generate relocated JARs with classifier `relocated`, instead of replacing the output of `:jar` task.

- Create `JvmRelocated` publications to publish relocated artifacts.

- Patch Kotlin Metadata and Maven Pom for the added publication.

- Updated deps test to be more strict
Him188 преди 2 години
родител
ревизия
178ca6c1b5

+ 39 - 0
.run/Publish deps test artifacts.run.xml

@@ -0,0 +1,39 @@
+<!--
+  ~ Copyright 2019-2023 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
+  -->
+
+<component name="ProjectRunConfigurationManager">
+    <configuration default="false" name="Publish deps test artifacts" type="GradleRunConfiguration" factoryName="Gradle"
+                   folderName="Publishing Tests">
+        <ExternalSystemSettings>
+            <option name="env">
+                <map>
+                    <entry key="mirai.build.project.version" value="2.99.0-deps-test"/>
+                </map>
+            </option>
+            <option name="executionName"/>
+            <option name="externalProjectPath" value="$PROJECT_DIR$"/>
+            <option name="externalSystemIdString" value="GRADLE"/>
+            <option name="scriptParameters" value="--stacktrace"/>
+            <option name="taskDescriptions">
+                <list/>
+            </option>
+            <option name="taskNames">
+                <list>
+                    <option value=":mirai-deps-test:publishMiraiArtifactsToMavenLocal"/>
+                </list>
+            </option>
+            <option name="vmOptions"/>
+        </ExternalSystemSettings>
+        <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+        <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+        <DebugAllEnabled>false</DebugAllEnabled>
+        <ForceTestExec>false</ForceTestExec>
+        <method v="2"/>
+    </configuration>
+</component>

+ 11 - 1
.run/Publish local artifacts.run.xml

@@ -1,3 +1,12 @@
+<!--
+  ~ Copyright 2019-2023 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
+  -->
+
 <component name="ProjectRunConfigurationManager">
   <configuration default="false" name="Publish local artifacts" type="GradleRunConfiguration" factoryName="Gradle" folderName="Build">
     <ExternalSystemSettings>
@@ -9,7 +18,7 @@
       <option name="executionName" />
       <option name="externalProjectPath" value="$PROJECT_DIR$" />
       <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="" />
+      <option name="scriptParameters" value="--stacktrace"/>
       <option name="taskDescriptions">
         <list />
       </option>
@@ -23,6 +32,7 @@
     <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
     <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
     <DebugAllEnabled>false</DebugAllEnabled>
+      <ForceTestExec>false</ForceTestExec>
     <method v="2" />
   </configuration>
 </component>

+ 1 - 3
build.gradle.kts

@@ -11,6 +11,7 @@
 
 import org.jetbrains.dokka.base.DokkaBase
 import org.jetbrains.dokka.base.DokkaBaseConfiguration
+import shadow.configureMppShadow
 import java.time.LocalDateTime
 
 buildscript {
@@ -79,9 +80,6 @@ allprojects {
         substituteDependenciesUsingExpectedVersion()
     }
 }
-afterEvaluate {
-    configureShadowDependenciesForPublishing()
-}
 
 subprojects {
     afterEvaluate {

+ 2 - 4
buildSrc/src/main/kotlin/HmppConfigure.kt

@@ -7,8 +7,6 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
-@file:Suppress("UNUSED_VARIABLE")
-
 import com.google.gradle.osdetector.OsDetector
 import org.gradle.api.Project
 import org.gradle.api.attributes.Attribute
@@ -28,14 +26,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
 import java.io.File
 import java.util.*
 
-val MIRAI_PLATFORM_ATTRIBUTE = Attribute.of(
+val MIRAI_PLATFORM_ATTRIBUTE: Attribute<String> = Attribute.of(
     "net.mamoe.mirai.platform", String::class.java
 )
 
 /**
  * Flags a target as an HMPP intermediate target
  */
-val MIRAI_PLATFORM_INTERMEDIATE = Attribute.of(
+val MIRAI_PLATFORM_INTERMEDIATE: Attribute<Boolean> = Attribute.of(
     "net.mamoe.mirai.platform.intermediate", Boolean::class.javaObjectType
 )
 

+ 169 - 0
buildSrc/src/main/kotlin/KotlinMetadataPatcher.kt

@@ -0,0 +1,169 @@
+/*
+ * Copyright 2019-2023 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 com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonElement
+import com.google.gson.JsonPrimitive
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.publish.maven.MavenPublication
+import org.gradle.api.publish.tasks.GenerateModuleMetadata
+import shadow.relocationFilters
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.security.MessageDigest
+
+fun Project.configurePatchKotlinModuleMetadataTask(
+    relocatedPublicationName: String,
+    relocateDependencies: Task,
+    originalPublicationName: String
+) {
+    // We will modify Kotlin metadata, so do generate metadata before relocation
+    val generateMetadataTask =
+        tasks.getByName("generateMetadataFileFor${originalPublicationName.titlecase()}Publication") as GenerateModuleMetadata
+
+    publications.getByName(relocatedPublicationName) {
+        this as MavenPublication
+        this.artifact(generateMetadataTask.outputFile) {
+            classifier = null
+            extension = "module"
+        }
+    }
+
+    generateMetadataTask.dependsOn(relocateDependencies)
+    val patchMetadataTask =
+        tasks.create("patchMetadataFileFor${relocatedPublicationName.capitalize()}RelocatedPublication") {
+            group = "mirai"
+            generateMetadataTask.finalizedBy(this)
+            dependsOn(generateMetadataTask)
+            dependsOn(relocateDependencies)
+
+            // remove dependencies in Kotlin module metadata
+            doLast {
+                // mirai-core-jvm-2.13.0.module
+                val file = generateMetadataTask.outputFile.asFile.get()
+                val metadata = Gson().fromJson(
+                    file.readText(),
+                    JsonElement::class.java
+                ).asJsonObject
+
+                val metadataVersion = metadata["formatVersion"]?.asString
+                check(metadataVersion == "1.1") {
+                    "Unsupported Kotlin metadata version. version=$metadataVersion, file=${file.absolutePath}"
+                }
+                for (variant in metadata["variants"]!!.asJsonArray) {
+                    patchKotlinMetadataVariant(variant, relocateDependencies.outputs.files.singleFile)
+                }
+
+
+                file.writeText(GsonBuilder().setPrettyPrinting().create().toJson(metadata))
+            }
+        }
+
+    // Set "publishKotlinMultiplatformPublicationTo*" and "publish${targetName.capitalize()}PublicationTo*" dependsOn patchMetadataTask
+    if (project.kotlinMpp != null) {
+        tasks.filter { it.name.startsWith("publishKotlinMultiplatformPublicationTo") }.let { publishTasks ->
+            if (publishTasks.isEmpty()) {
+                throw GradleException("[Shadow Relocation] Cannot find publishKotlinMultiplatformPublicationTo for project '${project.path}'.")
+            }
+            publishTasks.forEach { it.dependsOn(patchMetadataTask) }
+        }
+
+        tasks.filter { it.name.startsWith("publish${relocatedPublicationName.capitalize()}PublicationTo") }
+            .let { publishTasks ->
+                if (publishTasks.isEmpty()) {
+                    throw GradleException("[Shadow Relocation] Cannot find publish${relocatedPublicationName.capitalize()}PublicationTo for project '${project.path}'.")
+                }
+                publishTasks.forEach { it.dependsOn(patchMetadataTask) }
+            }
+    }
+}
+
+private fun Project.patchKotlinMetadataVariant(variant: JsonElement, relocatedJar: File) {
+    val dependencies = variant.asJsonObject["dependencies"]!!.asJsonArray
+    dependencies.removeAll { dependency ->
+        val dep = dependency.asJsonObject
+
+        val groupId = dep["group"]!!.asString
+        val artifactId = dep["module"]!!.asString
+        relocationFilters.any { filter ->
+            filter.matchesDependency(
+                groupId = groupId,
+                artifactId = artifactId
+            )
+        }.also {
+            println("[Shadow Relocation] Filtering out $groupId:$artifactId from Kotlin module")
+        }
+    }
+
+
+    /*
+    "files": [
+    {
+      "name": "mirai-core-jvm-2.99.0-local.jar",
+      "url": "mirai-core-jvm-2.99.0-local.jar",
+      "size": 14742378,
+      "sha512": "7ab4afc88384a58687467ba13c6aefeda20fa53fd7759dc2bc78b2d46a6285f94ba6ccae426d192e7745f773401b3cb42a853e5445dc23bdcb1b5295e78ff71c",
+      "sha256": "772f593bfb85a80794693d4d9dfe2f77c222cfe9ca7e0d571abaa320e7aa82d3",
+      "sha1": "cb7937269d29b574725d6f28668847fd672de7cf",
+      "md5": "3fca635ba5e55b7dd56c552e4ca01f7e"
+    }
+  ]
+     */
+
+    val files = variant.asJsonObject["files"].asJsonArray
+    val filesList = files.toList()
+    files.removeAll { true }
+    for (publishedFile0 in filesList) {
+        val publishedFile = publishedFile0.asJsonObject
+
+        val name = publishedFile["name"].asJsonPrimitive.asString
+        if (name.endsWith(".jar")) {
+            logPublishing { "Patching Kotlin Metadata: file $name" }
+            for (algorithm in ALGORITHMS) {
+                publishedFile.add(algorithm, JsonPrimitive(relocatedJar.digest(algorithm)))
+            }
+            publishedFile.add("size", JsonPrimitive(relocatedJar.length()))
+        } else {
+            error("Unexpected file '$name' while patching Kotlin metadata")
+        }
+
+        files.add(publishedFile)
+    }
+}
+
+private val ALGORITHMS = listOf("md5", "sha1", "sha256", "sha512")
+
+fun File.digest(algorithm: String): String {
+    val arr = inputStream().buffered().use { it.digest(algorithm) }
+    return arr.toUHexString("").lowercase()
+}
+
+fun InputStream.digest(algorithm: String): ByteArray {
+    val digest = MessageDigest.getInstance(algorithm)
+    digest.reset()
+    use { input ->
+        object : OutputStream() {
+            override fun write(b: Int) {
+                digest.update(b.toByte())
+            }
+
+            override fun write(b: ByteArray, off: Int, len: Int) {
+                digest.update(b, off, len)
+            }
+        }.use { output ->
+            input.copyTo(output)
+        }
+    }
+    return digest.digest()
+}
+

+ 141 - 4
buildSrc/src/main/kotlin/MppPublishing.kt

@@ -15,9 +15,11 @@ import org.gradle.api.tasks.TaskProvider
 import org.gradle.jvm.tasks.Jar
 import org.gradle.kotlin.dsl.get
 import org.gradle.kotlin.dsl.register
+import shadow.RelocationConfig
+import shadow.relocationFilters
 
-inline fun logPublishing(@Suppress("UNUSED_PARAMETER") message: () -> String) {
-//    println("[Publishing] Configuring $message")
+inline fun Project.logPublishing(message: () -> String) {
+    logger.debug("[Publishing] Configuring {}", message())
 }
 
 fun Project.configureMppPublishing() {
@@ -45,7 +47,9 @@ fun Project.configureMppPublishing() {
             logPublishing { "Publications: ${publications.joinToString { it.name }}" }
 
             val (nonJvmPublications, jvmPublications) = publications.filterIsInstance<MavenPublication>()
-                .partition { publication -> tasks.findByName("relocate${publication.name.titlecase()}Dependencies") == null }
+                .partition { publication ->
+                    tasks.findByName(RelocationConfig.taskNameForRelocateDependencies(publication.name)) == null
+                }
 
             for (publication in nonJvmPublications) {
                 configureMultiplatformPublication(publication, stubJavadoc, publication.name)
@@ -76,7 +80,7 @@ fun Project.configureMppPublishing() {
                 configureMultiplatformPublication(publication, stubJavadoc, publication.name)
                 publication.apply {
                     artifacts.filter { it.classifier.isNullOrEmpty() && it.extension == "jar" }.forEach {
-                        it.builtBy(tasks.findByName("relocate${publication.name.titlecase()}Dependencies"))
+                        it.builtBy(tasks.findByName(RelocationConfig.taskNameForRelocateDependencies(publication.name)))
                     }
                 }
             }
@@ -108,6 +112,12 @@ private fun Project.configureMultiplatformPublication(
             publication.artifactId = "${project.name}-metadata"
         }
 
+        "jvm" -> {
+            publication.artifactId = "${project.name}-$moduleName"
+
+            useRelocatedPublication(publication, moduleName)
+        }
+
         else -> {
             // "jvm", "native", "js", "common"
             publication.artifactId = "${project.name}-$moduleName"
@@ -115,6 +125,133 @@ private fun Project.configureMultiplatformPublication(
     }
 }
 
+/**
+ * Creates a new publication and disables [publication].
+ */
+private fun Project.useRelocatedPublication(
+    publication: MavenPublication,
+    moduleName: String
+) {
+    val relocatedPublicationName = RelocationConfig.relocatedPublicationName(publication.name)
+    registerRelocatedPublication(relocatedPublicationName, publication, moduleName)
+
+    logPublishing { "Registered relocated publication `$relocatedPublicationName` for module $moduleName, for project ${project.path}" }
+
+    // Add task dependencies
+    addTaskDependenciesForRelocatedPublication(moduleName, relocatedPublicationName)
+
+    val relocateDependencies = tasks.getByName(RelocationConfig.taskNameForRelocateDependencies(moduleName))
+
+    configurePatchKotlinModuleMetadataTask(relocatedPublicationName, relocateDependencies, publication.name)
+}
+
+private fun Project.registerRelocatedPublication(
+    relocatedPublicationName: String,
+    publication: MavenPublication,
+    moduleName: String
+) {
+    // copy POM XML, since POM contains transitive dependencies
+
+    var patched = false
+
+    lateinit var oldXmlProvider: XmlProvider
+    publication.pom.withXml { oldXmlProvider = this }
+
+    publications.register(relocatedPublicationName, MavenPublication::class.java) {
+        this.artifactId = publication.artifactId
+        this.groupId = publication.groupId
+        this.version = publication.version
+        this.artifacts.addAll(publication.artifacts.filterNot { it.classifier == null && it.extension == "jar" })
+
+        project.tasks.findByName(RelocationConfig.taskNameForRelocateDependencies(moduleName))
+            ?.let { relocateDependencies ->
+                this.artifact(relocateDependencies) {
+                    this.classifier = null
+                    this.extension = "jar"
+                }
+            }
+
+        pom.withXml {
+            val newXml = this
+            for (newChild in newXml.asNode().childrenNodes()) {
+                newXml.asNode().remove(newChild)
+            }
+            // Note: `withXml` is lazy, it is evaluated only when `generatePomFileFor...`
+            for (oldChild in oldXmlProvider.asNode().childrenNodes()) {
+                newXml.asNode().append(oldChild)
+            }
+            removeDependenciesInMavenPom(this)
+            patched = true
+        }
+    }
+
+    tasks.matching { it.name.startsWith("publish${relocatedPublicationName.titlecase()}PublicationTo") }.all {
+        dependsOn("generatePomFileFor${relocatedPublicationName.titlecase()}Publication")
+    }
+
+
+    tasks.matching { it.name == "generatePomFileFor${relocatedPublicationName.titlecase()}Publication" }.all {
+        dependsOn(tasks.getByName("generatePomFileFor${publication.name.titlecase()}Publication"))
+        doLast {
+            check(patched) { "POM is not patched" }
+        }
+    }
+}
+
+private fun Project.addTaskDependenciesForRelocatedPublication(moduleName: String, relocatedPublicationName: String) {
+    val originalTaskNamePrefix = "publish${moduleName.titlecase()}PublicationTo"
+    val relocatedTaskName = "publish${relocatedPublicationName.titlecase()}PublicationTo"
+    tasks.configureEach {
+        if (!name.startsWith(originalTaskNamePrefix)) return@configureEach
+        val originalTask = this
+
+        this.enabled = false
+        this.description = "${this.description} ([mirai] disabled in favor of $relocatedTaskName)"
+
+        val relocatedTasks = project.tasks.filter { it.name.startsWith(relocatedTaskName) }.toTypedArray()
+        check(relocatedTasks.isNotEmpty()) { "relocatedTasks is empty" }
+        relocatedTasks.forEach { publishRelocatedPublication ->
+            publishRelocatedPublication.dependsOn(*this.dependsOn.toTypedArray())
+            logger.info(
+                "[Publishing] $publishRelocatedPublication now dependsOn tasks: " +
+                        this.dependsOn.joinToString()
+            )
+        }
+
+        project.tasks.filter { it.dependsOn.contains(originalTask) }
+            .forEach { it.dependsOn(*relocatedTasks) }
+    }
+}
+
+// Remove relocated dependencies in Maven pom
+private fun Project.removeDependenciesInMavenPom(xmlProvider: XmlProvider) {
+    xmlProvider.run {
+        val node = asNode().getSingleChild("dependencies")
+        val dependencies = node.childrenNodes()
+        logger.info("[Shadow Relocation] deps: {}", dependencies)
+        logger.info(
+            "[Shadow Relocation] All filter notations: {}",
+            relocationFilters.flatMap { it.notations.notations() }.joinToString("\n")
+        )
+
+        dependencies.forEach { dep ->
+            val groupId = dep.getSingleChild("groupId").value().toString().removeSurrounding("[", "]")
+            val artifactId = dep.getSingleChild("artifactId").value().toString().removeSurrounding("[", "]")
+            logger.info("[Shadow Relocation] Checking $groupId:$artifactId")
+
+            if (
+                relocationFilters.any { filter ->
+                    filter.matchesDependency(groupId = groupId, artifactId = artifactId)
+                }
+            ) {
+                logger.info("[Shadow Relocation] Filtering out '$groupId:$artifactId' from pom for project '${project.path}'")
+                check(node.remove(dep)) { "Failed to remove dependency node" }
+            }
+        }
+
+    }
+}
+
 val publishPlatformArtifactsInRootModule: Project.(MavenPublication) -> Unit = { platformPublication ->
     lateinit var platformPomBuilder: XmlProvider
     platformPublication.pom.withXml { platformPomBuilder = this }

+ 0 - 292
buildSrc/src/main/kotlin/Shadow.kt

@@ -1,292 +0,0 @@
-/*
- * Copyright 2019-2023 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 com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
-import org.gradle.api.GradleException
-import org.gradle.api.Project
-import org.gradle.api.Task
-import org.gradle.api.artifacts.Configuration
-import org.gradle.api.execution.TaskExecutionGraph
-import org.gradle.api.publish.tasks.GenerateModuleMetadata
-import org.gradle.api.tasks.bundling.Jar
-import org.gradle.kotlin.dsl.DependencyHandlerScope
-import org.gradle.kotlin.dsl.create
-import org.gradle.kotlin.dsl.get
-import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
-import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
-
-/**
- * @see RelocationNotes
- */
-fun Project.configureMppShadow() {
-    val kotlin = kotlinMpp ?: return
-
-    configure(kotlin.targets.filter {
-        it.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.jvm
-                && (it.attributes.getAttribute(MIRAI_PLATFORM_INTERMEDIATE) != true)
-    }) {
-        configureRelocationForMppTarget(project)
-
-        registerRegularShadowTask(this, mapTaskNameForMultipleTargets = true)
-    }
-}
-
-/**
- * 配置 `publish` 和 `shadow` 相关依赖. 对于在本次构建的请求的任务及其直接或间接依赖, 以以下顺序执行:
- *
- * 1. 执行全部 `jar` 任务
- * 2. 执行全部 `relocate` 任务
- * 3. 执行全部 `publish` 任务
- *
- * 这是必要的因为 relocate 任务会覆盖 jar 任务的输出, 而在多模块并行编译时, Kotlin 编译器会依赖 jar 任务的输出. 如果在编译同时修改 JAR 文件, 就会导致 `ZipException`.
- *
- * 这也会让 publish 集中执行, Maven Central 不容易出问题.
- */
-fun Project.configureShadowDependenciesForPublishing() {
-    check(this.rootProject === this) {
-        "configureShadowDependenciesForPublishing can only be used on root project."
-    }
-
-    val jarTaskNames = arrayOf("jvmJar", "jvmBaseJar")
-    gradle.projectsEvaluated {
-        // Tasks requested to run in this build
-        val allTasks = rootProject.allprojects.asSequence().flatMap { it.tasks }
-
-        val publishTasks = allTasks.filter { it.name.contains("publish", ignoreCase = true) }
-        val relocateTasks = allTasks.filter { it.name.contains("relocate", ignoreCase = true) }
-        val jarTasks = allTasks.filter {
-            it.name in jarTaskNames
-        }
-        val compileKotlinTasks = allTasks.filter { it.name.contains("compileKotlin", ignoreCase = true) }
-        val compileTestKotlinTasks = allTasks.filter { it.name.contains("compileTestKotlin", ignoreCase = true) }
-
-        relocateTasks.dependsOn(compileKotlinTasks.toList())
-        relocateTasks.dependsOn(compileTestKotlinTasks.toList())
-        relocateTasks.dependsOn(jarTasks.toList())
-        publishTasks.dependsOn(relocateTasks.toList())
-    }
-}
-
-val TaskExecutionGraph.hierarchicalTasks: Sequence<Task>
-    get() = sequence {
-        suspend fun SequenceScope<Task>.addTask(task: Task) {
-            yield(task)
-            for (dependency in getDependencies(task)) {
-                addTask(dependency)
-            }
-        }
-
-        for (task in allTasks) {
-            addTask(task)
-        }
-    }
-
-/**
- * Relocate some dependencies for `.jar`
- * @see RelocationNotes
- */
-private fun KotlinTarget.configureRelocationForMppTarget(project: Project) = project.run {
-    val configuration = project.configurations.findByName(SHADOW_RELOCATION_CONFIGURATION_NAME)
-
-    // e.g. relocateJvmDependencies
-    // do not change task name. see `configureShadowDependenciesForPublishing`
-    val relocateDependencies = tasks.create("relocate${targetName.titlecase()}Dependencies", ShadowJar::class) {
-        group = "mirai"
-        description = "Relocate dependencies to internal package"
-        destinationDirectory.set(buildDir.resolve("libs")) // build/libs
-        archiveBaseName.set("${project.name}-${targetName.lowercase()}") // e.g. "mirai-core-api-jvm"
-
-        dependsOn(compilations["main"].compileTaskProvider) // e.g. compileKotlinJvm
-
-        from(compilations["main"].output) // Add compilation result of mirai sourcecode, not including dependencies
-        configuration?.let {
-            from(it) // Include runtime dependencies
-        }
-
-        // Relocate packages
-        afterEvaluate {
-            val relocationFilters = project.relocationFilters
-            relocationFilters.forEach { relocation ->
-                relocation.packages.forEach { aPackage ->
-                    relocate(aPackage, "$RELOCATION_ROOT_PACKAGE.$aPackage")
-                }
-            }
-        }
-    }
-
-    // We will modify Kotlin metadata, so do generate metadata before relocation
-    val generateMetadataTask =
-        tasks.getByName("generateMetadataFileFor${targetName.capitalize()}Publication") as GenerateModuleMetadata
-    generateMetadataTask.dependsOn(relocateDependencies)
-
-    val patchMetadataTask = tasks.create("patchMetadataFileFor${targetName.capitalize()}RelocatedPublication") {
-        dependsOn(generateMetadataTask)
-        dependsOn(relocateDependencies)
-
-        // remove dependencies in Kotlin module metadata
-        doLast {
-            // mirai-core-jvm-2.13.0.module
-            val file = generateMetadataTask.outputFile.asFile.get()
-            val metadata = Gson().fromJson(
-                file.readText(),
-                com.google.gson.JsonElement::class.java
-            ).asJsonObject
-
-            val metadataVersion = metadata["formatVersion"]?.asString
-            check(metadataVersion == "1.1") {
-                "Unsupported Kotlin metadata version. version=$metadataVersion, file=${file.absolutePath}"
-            }
-            for (variant in metadata["variants"]!!.asJsonArray) {
-                val dependencies = variant.asJsonObject["dependencies"]!!.asJsonArray
-                dependencies.removeAll { dependency ->
-                    val dep = dependency.asJsonObject
-
-                    val groupId = dep["group"]!!.asString
-                    val artifactId = dep["module"]!!.asString
-                    relocationFilters.any { filter ->
-                        filter.matchesDependency(
-                            groupId = groupId,
-                            artifactId = artifactId
-                        )
-                    }.also {
-                        println("[Shadow Relocation] Filtering out $groupId:$artifactId from Kotlin module")
-                    }
-                }
-            }
-
-
-            file.writeText(GsonBuilder().setPrettyPrinting().create().toJson(metadata))
-        }
-    }
-
-    // Set "publishKotlinMultiplatformPublicationTo*" and "publish${targetName.capitalize()}PublicationTo*" dependsOn patchMetadataTask
-    if (project.kotlinMpp != null) {
-        tasks.filter { it.name.startsWith("publishKotlinMultiplatformPublicationTo") }.let { publishTasks ->
-            if (publishTasks.isEmpty()) {
-                throw GradleException("[Shadow Relocation] Cannot find publishKotlinMultiplatformPublicationTo for project '${project.path}'.")
-            }
-            publishTasks.forEach { it.dependsOn(patchMetadataTask) }
-        }
-
-        tasks.filter { it.name.startsWith("publish${targetName.capitalize()}PublicationTo") }.let { publishTasks ->
-            if (publishTasks.isEmpty()) {
-                throw GradleException("[Shadow Relocation] Cannot find publish${targetName.capitalize()}PublicationTo for project '${project.path}'.")
-            }
-            publishTasks.forEach { it.dependsOn(patchMetadataTask) }
-        }
-    }
-
-    afterEvaluate {
-        // Remove relocated dependencies in Maven pom
-        mavenPublication {
-            pom.withXml {
-                val node = this.asNode().getSingleChild("dependencies")
-                val dependencies = node.childrenNodes()
-                logger.trace("[Shadow Relocation] deps: $dependencies")
-                dependencies.forEach { dep ->
-                    val groupId = dep.getSingleChild("groupId").value().toString()
-                    val artifactId = dep.getSingleChild("artifactId").value().toString()
-                    logger.trace("[Shadow Relocation] Checking $groupId:$artifactId")
-
-                    if (
-                        relocationFilters.any { filter ->
-                            filter.matchesDependency(groupId = groupId, artifactId = artifactId)
-                        }
-                    ) {
-                        logger.info("[Shadow Relocation] Filtering out '$groupId:$artifactId' from pom for project '${project.path}'")
-                        check(node.remove(dep)) { "Failed to remove dependency node" }
-                    }
-                }
-            }
-        }
-    }
-}
-
-private fun Sequence<Task>.dependsOn(
-    task: Task,
-) {
-    return forEach { it.dependsOn(task) }
-}
-
-private fun Sequence<Task>.dependsOn(
-    tasks: Iterable<Task>,
-) {
-    return forEach { it.dependsOn(tasks) }
-}
-
-/**
- * 添加 `implementation` 和 `shadow`
- */
-fun DependencyHandlerScope.shadowImplementation(dependencyNotation: Any) {
-    "implementation"(dependencyNotation)
-    "shadow"(dependencyNotation)
-}
-
-fun Project.registerRegularShadowTaskForJvmProject(
-    configurations: List<Configuration> = listOfNotNull(
-        project.configurations.findByName("runtimeClasspath"),
-        project.configurations.findByName("${kotlinJvm!!.target.name}RuntimeClasspath"),
-        project.configurations.findByName("runtime")
-    )
-): ShadowJar {
-    return project.registerRegularShadowTask(kotlinJvm!!.target, mapTaskNameForMultipleTargets = false, configurations)
-}
-
-fun Project.registerRegularShadowTask(
-    target: KotlinTarget,
-    mapTaskNameForMultipleTargets: Boolean,
-    configurations: List<Configuration> = listOfNotNull(
-        project.configurations.findByName("runtimeClasspath"),
-        project.configurations.findByName("${target.targetName}RuntimeClasspath"),
-        project.configurations.findByName("runtime")
-    ),
-): ShadowJar {
-    return tasks.create(
-        if (mapTaskNameForMultipleTargets) "shadow${target.targetName.capitalize()}Jar" else "shadowJar",
-        ShadowJar::class
-    ) {
-        group = "mirai"
-        archiveClassifier.set("all")
-
-        (tasks.findByName("jar") as? Jar)?.let {
-            manifest.inheritFrom(it.manifest)
-        }
-
-        val compilation = target.compilations["main"]
-        dependsOn(compilation.compileTaskProvider)
-        from(compilation.output)
-
-//        components.findByName("java")?.let { from(it) }
-        project.sourceSets.findByName("main")?.output?.let { from(it) } // for JVM projects
-        this.configurations = configurations
-
-        // Relocate packages
-        afterEvaluate {
-            val relocationFilters = project.relocationFilters
-            relocationFilters.forEach { relocation ->
-                relocation.packages.forEach { aPackage ->
-                    relocate(aPackage, "$RELOCATION_ROOT_PACKAGE.$aPackage")
-                }
-            }
-        }
-
-        exclude { file ->
-            file.name.endsWith(".sf", ignoreCase = true)
-        }
-        exclude("META-INF/INDEX.LIST", "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class")
-    }
-}
-
-fun Project.configureRelocatedShadowJarForJvmProject(kotlin: KotlinJvmProjectExtension): ShadowJar {
-    return registerRegularShadowTask(kotlin.target, mapTaskNameForMultipleTargets = false)
-}
-
-const val RELOCATION_ROOT_PACKAGE = "net.mamoe.mirai.internal.deps"

+ 76 - 8
buildSrc/src/main/kotlin/Versions.kt

@@ -120,6 +120,13 @@ class RelocatedDependency(
      * Kotlin packages. e.g. `io.ktor`
      */
     vararg val packages: String,
+    /**
+     * Exclude them, so no transitive dependencies exposed to Maven and Kotlin JVM consumers
+     */
+    val notationsToExcludeInPom: RelocatableDependency = MultiplatformDependency.jvm(
+        notation.substringBefore(":"),
+        notation.substringAfter(":").substringBeforeLast(":")
+    ),
     /**
      * Additional exclusions apart from everything from `org.jetbrains.kotlin` and `org.jetbrains.kotlinx`.
      */
@@ -142,14 +149,49 @@ fun KotlinDependencyHandler.implementationKotlinxIo(module: String) {
     }
 }
 
+
+class DependencyNotation(
+    val groupId: String,
+    val artifactId: String,
+) {
+    fun toMap(): Map<String, String> {
+        return mapOf("group" to groupId, "module" to artifactId)
+    }
+
+    override fun toString(): String {
+        return "$groupId:$artifactId"
+    }
+}
+
+sealed interface RelocatableDependency {
+    fun notations(): Sequence<DependencyNotation>
+}
+
+class SinglePlatformDependency(
+    val groupId: String,
+    val artifactId: String
+) : RelocatableDependency {
+    override fun notations(): Sequence<DependencyNotation> {
+        return sequenceOf(DependencyNotation(groupId, artifactId))
+    }
+}
+
+class CompositeDependency(
+    private val dependencies: List<RelocatableDependency>
+) : RelocatableDependency {
+    constructor(vararg dependencies: RelocatableDependency) : this(dependencies.toList())
+
+    override fun notations(): Sequence<DependencyNotation> = dependencies.asSequence().flatMap { it.notations() }
+}
+
 class MultiplatformDependency private constructor(
     private val groupId: String,
     private val baseArtifactId: String,
     vararg val targets: String,
-) {
-    fun notations(): Sequence<Map<String, String>> {
-        return sequenceOf(mapOf("group" to groupId, "module" to baseArtifactId))
-            .plus(targets.asSequence().map { mapOf("group" to groupId, "module" to "$baseArtifactId.$it") })
+) : RelocatableDependency {
+    override fun notations(): Sequence<DependencyNotation> {
+        return sequenceOf(DependencyNotation(groupId, baseArtifactId))
+            .plus(targets.asSequence().map { DependencyNotation(groupId, "$baseArtifactId-$it") })
     }
 
     companion object {
@@ -161,7 +203,7 @@ class MultiplatformDependency private constructor(
 
 fun ModuleDependency.exclude(multiplatformDependency: MultiplatformDependency) {
     multiplatformDependency.notations().forEach {
-        exclude(it)
+        exclude(it.toMap())
     }
 }
 
@@ -191,7 +233,10 @@ object ExcludeProperties {
 }
 
 val `ktor-io` = ktor("io", Versions.ktor)
-val `ktor-io_relocated` = RelocatedDependency(`ktor-io`, "io.ktor.utils.io") {
+val `ktor-io_relocated` = RelocatedDependency(
+    `ktor-io`, "io.ktor.utils.io",
+    notationsToExcludeInPom = MultiplatformDependency.jvm("io.ktor", "ktor-io")
+) {
     exclude(ExcludeProperties.`everything from slf4j`)
     exclude(ExcludeProperties.`slf4j-api`)
 }
@@ -202,7 +247,16 @@ val `ktor-serialization` = ktor("serialization", Versions.ktor)
 val `ktor-websocket-serialization` = ktor("websocket-serialization", Versions.ktor)
 
 val `ktor-client-core` = ktor("client-core", Versions.ktor)
-val `ktor-client-core_relocated` = RelocatedDependency(`ktor-client-core`, "io.ktor") {
+val `ktor-client-core_relocated` = RelocatedDependency(
+    `ktor-client-core`, "io.ktor",
+    notationsToExcludeInPom = CompositeDependency(
+        MultiplatformDependency.jvm("io.ktor", "ktor-io"),
+        MultiplatformDependency.jvm("io.ktor", "ktor-client-core"),
+        MultiplatformDependency.jvm("io.ktor", "ktor-client-okhttp"),
+        MultiplatformDependency.jvm("io.ktor", "ktor-http"),
+        MultiplatformDependency.jvm("io.ktor", "ktor-utils"),
+    )
+) {
     exclude(ExcludeProperties.`ktor-io`)
     exclude(ExcludeProperties.`everything from slf4j`)
 }
@@ -213,7 +267,21 @@ val `ktor-client-curl` = ktor("client-curl", Versions.ktor)
 val `ktor-client-darwin` = ktor("client-darwin", Versions.ktor)
 val `ktor-client-okhttp` = ktor("client-okhttp", Versions.ktor)
 val `ktor-client-okhttp_relocated` =
-    RelocatedDependency(ktor("client-okhttp", Versions.ktor), "io.ktor", "okhttp", "okio") {
+    RelocatedDependency(
+        ktor("client-okhttp", Versions.ktor), "io.ktor", "okhttp", "okio",
+        notationsToExcludeInPom = CompositeDependency(
+            MultiplatformDependency.jvm("io.ktor", "ktor-io"),
+            MultiplatformDependency.jvm("io.ktor", "ktor-client-core"),
+            MultiplatformDependency.jvm("io.ktor", "ktor-client-okhttp"),
+            MultiplatformDependency.jvm("io.ktor", "ktor-http"),
+            MultiplatformDependency.jvm("io.ktor", "ktor-serialization"),
+            MultiplatformDependency.jvm("io.ktor", "ktor-utils"),
+            MultiplatformDependency.jvm("io.ktor", "ktor-websockets"),
+            MultiplatformDependency.jvm("io.ktor", "ktor-websockets-serialization"),
+            MultiplatformDependency.jvm("com.squareup.okhttp3", "okhttp3"),
+            MultiplatformDependency.jvm("com.squareup.okio", "okio"),
+        )
+    ) {
         exclude(ExcludeProperties.`ktor-io`)
         exclude(ExcludeProperties.`everything from slf4j`)
     }

+ 38 - 26
buildSrc/src/main/kotlin/Relocation.kt → buildSrc/src/main/kotlin/shadow/Relocation.kt

@@ -7,6 +7,11 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
+package shadow
+
+import ExcludeProperties
+import RelocatableDependency
+import RelocatedDependency
 import org.gradle.api.Action
 import org.gradle.api.DomainObjectCollection
 import org.gradle.api.Project
@@ -66,7 +71,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
  *
  * 如果你都使用 [relocateImplementation], 就会导致在 Android 平台发生 'Duplicated Class' 问题. 如果你都使用 [relocateCompileOnly] 则会在 clinit 阶段遇到 [NoClassDefFoundError]
  *
- * ## relocation 发生的时机晚于编译
+ * ## relocation 发生的时机晚于编译 (Jar)
  *
  * mirai-core-utils relocate 了 ktor-io, 然后 mirai-core 在 `build.gradle.kts` 使用了 `implementation(project(":mirai-core-utils"))`.
  * 在 mirai-core 编译时, 编译器仍然会使用 relocate 之前的 `io.ktor`. 为了在 mirai-core 将对 `io.ktor` 的调用转为对 `net.mamoe.mirai.internal.deps.io.ktor` 的调用, 需要配置 relocation.
@@ -74,6 +79,13 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
  *
  * 所以你需要为所有依赖了 mirai-core-utils 的模块都分别配置 [relocateCompileOnly].
  *
+ * ## relocation 仅在发布 (e.g. `publishToMavenLocal`) 时自动使用
+ *
+ * 其他任何时候, 比如在 mirai-console 编译时, mirai-console 依赖的是未 relocate 的 JAR. 使用 `jar` 任务打包的也是未 relocate 的.
+ *
+ * 若需要 relocated 的 JAR, 使用 `relocateJvmDependencies`. 其中 `Jvm` 可换为其他启动了 relocate 的 Kotlin target 名.
+ * 可在 IDEA Gradle 视图中找到 mirai 文件夹, 查看可用的 task 列表.
+ *
  * ### "在运行时包含" 是如何实现的?
  *
  * 被 relocate 的类会被直接当做是当前模块的类打包进 JAR.
@@ -89,7 +101,7 @@ object RelocationNotes
 /**
  * 添加一个通常的 [compileOnly][KotlinDependencyHandler.compileOnly] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  *
- * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
+ * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RelocationConfig.RELOCATION_ROOT_PACKAGE].
  * 运行时 (runtime) **不会**包含被 relocate 的依赖及其所有间接依赖.
  *
  * @see RelocationNotes
@@ -102,7 +114,9 @@ fun KotlinDependencyHandler.relocateCompileOnly(
     }
     project.relocationFilters.add(
         RelocationFilter(
-            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
+            relocatedDependency.notationsToExcludeInPom,
+            relocatedDependency.packages.toList(),
+            includeInRuntime = false,
         )
     )
     // Don't add to runtime
@@ -112,7 +126,7 @@ fun KotlinDependencyHandler.relocateCompileOnly(
 /**
  * 添加一个通常的 [compileOnly][KotlinDependencyHandler.compileOnly] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  *
- * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
+ * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RelocationConfig.RELOCATION_ROOT_PACKAGE].
  * 运行时 (runtime) **不会**包含被 relocate 的依赖及其所有间接依赖.
  *
  * @see RelocationNotes
@@ -127,7 +141,9 @@ fun DependencyHandler.relocateCompileOnly(
         })
     project.relocationFilters.add(
         RelocationFilter(
-            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
+            relocatedDependency.notationsToExcludeInPom,
+            relocatedDependency.packages.toList(),
+            includeInRuntime = false,
         )
     )
     // Don't add to runtime
@@ -137,7 +153,7 @@ fun DependencyHandler.relocateCompileOnly(
 /**
  * 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  *
- * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
+ * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都会**被 relocate 到 [RelocationConfig.RELOCATION_ROOT_PACKAGE].
  * 运行时 (runtime) 将**会**包含被 relocate 的依赖及其所有间接依赖.
  *
  * @see RelocationNotes
@@ -151,13 +167,14 @@ fun KotlinDependencyHandler.relocateImplementation(
     }
     project.relocationFilters.add(
         RelocationFilter(
-            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
+            relocatedDependency.notationsToExcludeInPom, relocatedDependency.packages.toList(), includeInRuntime = true,
         )
     )
-    project.configurations.maybeCreate(SHADOW_RELOCATION_CONFIGURATION_NAME)
+    val configurationName = RelocationConfig.SHADOW_RELOCATION_CONFIGURATION_NAME
+    project.configurations.maybeCreate(configurationName)
     addDependencyTo(
         project.dependencies,
-        SHADOW_RELOCATION_CONFIGURATION_NAME,
+        configurationName,
         relocatedDependency.notation,
         Action<ExternalModuleDependency> {
             relocatedDependency.exclusionAction(this)
@@ -171,7 +188,7 @@ fun KotlinDependencyHandler.relocateImplementation(
 /**
  * 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  *
- * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用都会被 relocate 到 [RELOCATION_ROOT_PACKAGE].
+ * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用都会被 relocate 到 [RelocationConfig.RELOCATION_ROOT_PACKAGE].
  * 运行时 (runtime) 将会包含被 relocate 的依赖及其所有间接依赖.
  *
  * @see RelocationNotes
@@ -187,13 +204,14 @@ fun DependencyHandler.relocateImplementation(
         })
     project.relocationFilters.add(
         RelocationFilter(
-            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
+            relocatedDependency.notationsToExcludeInPom, relocatedDependency.packages.toList(), includeInRuntime = true,
         )
     )
-    project.configurations.maybeCreate(SHADOW_RELOCATION_CONFIGURATION_NAME)
+    val configurationName = RelocationConfig.SHADOW_RELOCATION_CONFIGURATION_NAME
+    project.configurations.maybeCreate(configurationName)
     addDependencyTo(
         project.dependencies,
-        SHADOW_RELOCATION_CONFIGURATION_NAME,
+        configurationName,
         relocatedDependency.notation,
         Action<ExternalModuleDependency> {
             relocatedDependency.exclusionAction(this)
@@ -204,8 +222,7 @@ fun DependencyHandler.relocateImplementation(
     return dependency
 }
 
-@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // compiler bug
-private val ExternalModuleDependency.groupNotNull get() = group!!
+private val ExternalModuleDependency.groupNotNull: String get() = group.toString()
 
 private fun ExternalModuleDependency.intrinsicExclusions() {
     exclude(ExcludeProperties.`everything from kotlin`)
@@ -213,14 +230,10 @@ private fun ExternalModuleDependency.intrinsicExclusions() {
 }
 
 
-const val SHADOW_RELOCATION_CONFIGURATION_NAME = "shadowRelocation"
-
-
 data class RelocationFilter(
-    val groupId: String,
-    val artifactId: String? = null,
-    val packages: List<String> = listOf(groupId),
-    val filesFilter: String = groupId.replace(".", "/"),
+    val notations: RelocatableDependency,
+    val packages: List<String>,
+//    val filesFilter: String = groupId.replace(".", "/"),
     /**
      * Pack relocated dependency into the fat jar. If set to `false`, dependencies will be removed.
      * This is to avoid duplicated classes. See #2291.
@@ -229,10 +242,9 @@ data class RelocationFilter(
 ) {
 
     fun matchesDependency(groupId: String?, artifactId: String?): Boolean {
-        if (this.groupId == groupId) return true
-        if (this.artifactId != null && this.artifactId == artifactId) return true
-
-        return false
+        return notations.notations().any {
+            it.groupId == groupId && it.artifactId == artifactId
+        }
     }
 }
 

+ 24 - 0
buildSrc/src/main/kotlin/shadow/RelocationConfig.kt

@@ -0,0 +1,24 @@
+/*
+ * Copyright 2019-2023 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 shadow
+
+import titlecase
+
+object RelocationConfig {
+    const val RELOCATION_ROOT_PACKAGE = "net.mamoe.mirai.internal.deps"
+
+    const val SHADOW_RELOCATION_CONFIGURATION_NAME = "shadowRelocation"
+
+    fun taskNameForRelocateDependencies(
+        targetName: String
+    ) = "relocate${targetName.titlecase()}Dependencies"
+
+    fun relocatedPublicationName(originalPublicationName: String): String = originalPublicationName + "Relocated"
+}

+ 145 - 0
buildSrc/src/main/kotlin/shadow/Shadow.kt

@@ -0,0 +1,145 @@
+/*
+ * Copyright 2019-2023 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 shadow
+
+import MIRAI_PLATFORM_INTERMEDIATE
+import capitalize
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import kotlinJvm
+import kotlinMpp
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.kotlin.dsl.DependencyHandlerScope
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.get
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
+import sourceSets
+
+/**
+ * @see RelocationNotes
+ */
+fun Project.configureMppShadow() {
+    val kotlin = kotlinMpp ?: return
+
+    configure(kotlin.targets.filter {
+        it.platformType == KotlinPlatformType.jvm
+                && (it.attributes.getAttribute(MIRAI_PLATFORM_INTERMEDIATE) != true)
+    }) {
+        configureRelocationForMppTarget(project)
+
+        registerRegularShadowTask(this, mapTaskNameForMultipleTargets = true)
+    }
+}
+
+/**
+ * Relocate some dependencies for `.jar`
+ * @see RelocationNotes
+ */
+private fun KotlinTarget.configureRelocationForMppTarget(project: Project) = project.run {
+    val configuration = project.configurations.findByName(RelocationConfig.SHADOW_RELOCATION_CONFIGURATION_NAME)
+
+    // e.g. relocateJvmDependencies
+    // do not change task name. see `configureShadowDependenciesForPublishing`
+    val relocateDependenciesName = RelocationConfig.taskNameForRelocateDependencies(targetName)
+    tasks.create(relocateDependenciesName, ShadowJar::class) {
+        group = "mirai"
+        description = "Relocate dependencies to internal package"
+        destinationDirectory.set(buildDir.resolve("libs")) // build/libs
+        archiveBaseName.set("${project.name}-${targetName.lowercase()}-relocated") // e.g. "mirai-core-api-jvm"
+
+        dependsOn(compilations["main"].compileTaskProvider) // e.g. compileKotlinJvm
+
+        from(compilations["main"].output) // Add the compilation result of mirai sourcecode, not including dependencies
+        configuration?.let {
+            from(it) // Include runtime dependencies
+        }
+
+        // Relocate packages
+        afterEvaluate {
+            val relocationFilters = project.relocationFilters
+            relocationFilters.forEach { relocation ->
+                relocation.packages.forEach { aPackage ->
+                    relocate(aPackage, "${RelocationConfig.RELOCATION_ROOT_PACKAGE}.$aPackage")
+                }
+            }
+        }
+    }
+}
+
+/**
+ * 添加 `implementation` 和 `shadow`
+ */
+fun DependencyHandlerScope.shadowImplementation(dependencyNotation: Any) {
+    "implementation"(dependencyNotation)
+    "shadow"(dependencyNotation)
+}
+
+fun Project.registerRegularShadowTaskForJvmProject(
+    configurations: List<Configuration> = listOfNotNull(
+        project.configurations.findByName("runtimeClasspath"),
+        project.configurations.findByName("${kotlinJvm!!.target.name}RuntimeClasspath"),
+        project.configurations.findByName("runtime")
+    )
+): ShadowJar {
+    return project.registerRegularShadowTask(kotlinJvm!!.target, mapTaskNameForMultipleTargets = false, configurations)
+}
+
+fun Project.registerRegularShadowTask(
+    target: KotlinTarget,
+    mapTaskNameForMultipleTargets: Boolean,
+    configurations: List<Configuration> = listOfNotNull(
+        project.configurations.findByName("runtimeClasspath"),
+        project.configurations.findByName("${target.targetName}RuntimeClasspath"),
+        project.configurations.findByName("runtime")
+    ),
+): ShadowJar {
+    return tasks.create(
+        if (mapTaskNameForMultipleTargets) "shadow${target.targetName.capitalize()}Jar" else "shadowJar",
+        ShadowJar::class
+    ) {
+        group = "mirai"
+        archiveClassifier.set("all")
+
+        (tasks.findByName("jar") as? Jar)?.let {
+            manifest.inheritFrom(it.manifest)
+        }
+
+        val compilation = target.compilations["main"]
+        dependsOn(compilation.compileTaskProvider)
+        from(compilation.output)
+
+//        components.findByName("java")?.let { from(it) }
+        project.sourceSets.findByName("main")?.output?.let { from(it) } // for JVM projects
+        this.configurations = configurations
+
+        // Relocate packages
+        afterEvaluate {
+            val relocationFilters = project.relocationFilters
+            relocationFilters.forEach { relocation ->
+                relocation.packages.forEach { aPackage ->
+                    relocate(aPackage, "${RelocationConfig.RELOCATION_ROOT_PACKAGE}.$aPackage")
+                }
+            }
+        }
+
+        exclude { file ->
+            file.name.endsWith(".sf", ignoreCase = true)
+        }
+        exclude("META-INF/INDEX.LIST", "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class")
+    }
+}
+
+fun Project.configureRelocatedShadowJarForJvmProject(kotlin: KotlinJvmProjectExtension): ShadowJar {
+    return registerRegularShadowTask(kotlin.target, mapTaskNameForMultipleTargets = false)
+}
+

+ 3 - 0
mirai-console/frontend/mirai-console-terminal/build.gradle.kts

@@ -7,6 +7,9 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
+import shadow.registerRegularShadowTaskForJvmProject
+import shadow.shadowImplementation
+
 plugins {
     kotlin("jvm")
     kotlin("plugin.serialization")

+ 5 - 2
mirai-core-all/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -9,6 +9,9 @@
 
 @file:Suppress("UnusedImport")
 
+import shadow.configureRelocatedShadowJarForJvmProject
+import shadow.relocateImplementation
+
 plugins {
     kotlin("jvm")
     kotlin("plugin.serialization")
@@ -34,7 +37,7 @@ dependencies {
 val shadow = configureRelocatedShadowJarForJvmProject(kotlin)
 
 if (System.getenv("MIRAI_IS_SNAPSHOTS_PUBLISHING")?.toBoolean() != true) {
-    // Do not publish -all jars to snapshot server since they are too large.
+    // Do not publish `-all` jars to snapshot server since they are too large.
 
     configurePublishing("mirai-core-all", addShadowJar = false)
 

+ 1 - 0
mirai-core-api/build.gradle.kts

@@ -9,6 +9,7 @@
 @file:Suppress("UNUSED_VARIABLE")
 
 import BinaryCompatibilityConfigurator.configureBinaryValidators
+import shadow.relocateCompileOnly
 
 plugins {
     id("com.android.library")

+ 3 - 1
mirai-core-utils/build.gradle.kts

@@ -9,6 +9,8 @@
 
 @file:Suppress("UNUSED_VARIABLE")
 
+import shadow.relocateImplementation
+
 plugins {
     id("com.android.library")
     kotlin("multiplatform")
@@ -98,7 +100,7 @@ if (tasks.findByName("androidMainClasses") != null) {
     tasks.getByName("androidBaseTest").dependsOn("checkAndroidApiLevel")
 }
 
-//configureMppPublishing()
+configureMppPublishing()
 
 //mavenCentralPublish {
 //    artifactId = "mirai-core-utils"

+ 5 - 0
mirai-core/build.gradle.kts

@@ -12,6 +12,8 @@
 import BinaryCompatibilityConfigurator.configureBinaryValidators
 import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractNativeLibrary
 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
+import shadow.relocateCompileOnly
+import shadow.relocateImplementation
 
 plugins {
     id("com.android.library")
@@ -49,6 +51,9 @@ kotlin {
                 implementation(`kotlinx-serialization-protobuf`)
                 implementation(`kotlinx-atomicfu`)
 
+                // runtime from mirai-core-utils
+                relocateCompileOnly(`ktor-io_relocated`)
+
 //                relocateImplementation(`ktor-http_relocated`)
 //                relocateImplementation(`ktor-serialization_relocated`)
 //                relocateImplementation(`ktor-websocket-serialization_relocated`)

+ 4 - 10
mirai-deps-test/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -48,16 +48,10 @@ val publishMiraiArtifactsToMavenLocal by tasks.registering {
         // Always print this very important message
         logger.warn("[publishMiraiArtifactsToMavenLocal] Project version is '${project.version}'.")
     }
+}
 
-    doLast {
-        // delete shadowed Jars, since Kotlin can't compile modules that depend on them.
-        rootProject.subprojects
-            .asSequence()
-            .flatMap { proj -> proj.tasks.filter { task -> task.name.contains("relocate") } }
-            .flatMap { it.outputs.files }
-            .filter { it.isFile && it.name.endsWith(".jar") }
-            .forEach { it.delete() }
-    }
+tasks.getByName("test") {
+    mustRunAfter(publishMiraiArtifactsToMavenLocal)
 }
 
 

+ 9 - 3
mirai-deps-test/test/AbstractTest.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -33,6 +33,9 @@ abstract class AbstractTest {
 
         @JvmStatic
         fun isMiraiLocalAvailable(): Boolean {
+            val commandLine =
+                """./gradlew publishMiraiArtifactsToMavenLocal -Dmirai.build.project.version=$miraiLocalVersion"""
+
             return if (mavenLocalDir.resolve("net/mamoe/mirai-core/$miraiLocalVersion").exists()) {
                 println(
                     """
@@ -41,7 +44,8 @@ abstract class AbstractTest {
                 - added/removed a dependency for mirai-core series modules
                 - changed version of any of the dependencies for mirai-core series modules
                 
-                You can update by running `./gradlew publishMiraiLocalArtifacts`.
+                You can update by running the following command: 
+                $commandLine
             """.trimIndent()
                 )
                 true
@@ -58,7 +62,9 @@ abstract class AbstractTest {
                 Note that you can ignore this test if you did not change project (dependency) structure.
                 And you don't need to worry if you does not run this test — this test is always executed on the CI when you make a PR.
 
-                You can run `./gradlew publishMiraiLocalArtifacts` to publish local artifacts. 
+                You can run the following command to publish local artifacts:
+                $commandLine
+                
                 Then you can run this test again. (By your original way or ./gradlew :mirai-deps-test:test)
                 """.trimIndent()
                 System.err.println(

+ 7 - 5
mirai-deps-test/test/CoreShadowRelocationTest.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -117,8 +117,8 @@ class CoreShadowRelocationTest : AbstractTest() {
             """
             dependencies {
                 implementation("net.mamoe:mirai-core:$miraiLocalVersion") {
-                    exclude("net.mamoe", "mirai-core-api")
-                    exclude("net.mamoe", "mirai-core-utils")
+                    exclude("net.mamoe", "mirai-core-api-jvm")
+                    exclude("net.mamoe", "mirai-core-utils-jvm")
                 }
             }
         """.trimIndent()
@@ -130,6 +130,7 @@ class CoreShadowRelocationTest : AbstractTest() {
     @EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
     fun `test mirai-core-api without transitive mirai-core-utils`() {
         val fragment = buildTestCases {
+            -`mirai-core-utils`
             -both(`ktor-io`)
             -both(`ktor-client-core`)
             -both(`ktor-client-okhttp`)
@@ -143,7 +144,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             """
             dependencies {
                 implementation("net.mamoe:mirai-core-api:$miraiLocalVersion") {
-                    exclude("net.mamoe", "mirai-core-utils")
+                    exclude("net.mamoe", "mirai-core-utils-jvm")
                 }
             }
         """.trimIndent()
@@ -233,6 +234,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             val relocated: (FunctionTestCase.() -> FunctionTestCase)? = null,
         )
 
+        val `mirai-core-utils` = ClassTestCase("mirai-core-utils Symbol", "net.mamoe.mirai.utils.Symbol")
         val `ktor-io` = ClassTestCase("ktor-io ByteBufferChannel", ByteBufferChannel)
         val `ktor-client-core` = ClassTestCase("ktor-client-core HttpClient", HttpClient)
         val `ktor-client-okhttp` = ClassTestCase("ktor-client-core OkHttp", KtorOkHttp)
@@ -302,7 +304,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             result.append(
                 """
                       @Test
-                      fun `no relocated ${name}`() {
+                      fun `no ${name}`() {
                         assertThrows<ClassNotFoundException> { Class.forName("$qualifiedClassName") }
                       }
                     """.trimIndent()