2
0
Эх сурвалжийг харах

[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 жил өмнө
parent
commit
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">
 <component name="ProjectRunConfigurationManager">
   <configuration default="false" name="Publish local artifacts" type="GradleRunConfiguration" factoryName="Gradle" folderName="Build">
   <configuration default="false" name="Publish local artifacts" type="GradleRunConfiguration" factoryName="Gradle" folderName="Build">
     <ExternalSystemSettings>
     <ExternalSystemSettings>
@@ -9,7 +18,7 @@
       <option name="executionName" />
       <option name="executionName" />
       <option name="externalProjectPath" value="$PROJECT_DIR$" />
       <option name="externalProjectPath" value="$PROJECT_DIR$" />
       <option name="externalSystemIdString" value="GRADLE" />
       <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="" />
+      <option name="scriptParameters" value="--stacktrace"/>
       <option name="taskDescriptions">
       <option name="taskDescriptions">
         <list />
         <list />
       </option>
       </option>
@@ -23,6 +32,7 @@
     <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
     <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
     <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
     <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
     <DebugAllEnabled>false</DebugAllEnabled>
     <DebugAllEnabled>false</DebugAllEnabled>
+      <ForceTestExec>false</ForceTestExec>
     <method v="2" />
     <method v="2" />
   </configuration>
   </configuration>
 </component>
 </component>

+ 1 - 3
build.gradle.kts

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

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

@@ -7,8 +7,6 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
  */
 
 
-@file:Suppress("UNUSED_VARIABLE")
-
 import com.google.gradle.osdetector.OsDetector
 import com.google.gradle.osdetector.OsDetector
 import org.gradle.api.Project
 import org.gradle.api.Project
 import org.gradle.api.attributes.Attribute
 import org.gradle.api.attributes.Attribute
@@ -28,14 +26,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
 import java.io.File
 import java.io.File
 import java.util.*
 import java.util.*
 
 
-val MIRAI_PLATFORM_ATTRIBUTE = Attribute.of(
+val MIRAI_PLATFORM_ATTRIBUTE: Attribute<String> = Attribute.of(
     "net.mamoe.mirai.platform", String::class.java
     "net.mamoe.mirai.platform", String::class.java
 )
 )
 
 
 /**
 /**
  * Flags a target as an HMPP intermediate target
  * 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
     "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.jvm.tasks.Jar
 import org.gradle.kotlin.dsl.get
 import org.gradle.kotlin.dsl.get
 import org.gradle.kotlin.dsl.register
 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() {
 fun Project.configureMppPublishing() {
@@ -45,7 +47,9 @@ fun Project.configureMppPublishing() {
             logPublishing { "Publications: ${publications.joinToString { it.name }}" }
             logPublishing { "Publications: ${publications.joinToString { it.name }}" }
 
 
             val (nonJvmPublications, jvmPublications) = publications.filterIsInstance<MavenPublication>()
             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) {
             for (publication in nonJvmPublications) {
                 configureMultiplatformPublication(publication, stubJavadoc, publication.name)
                 configureMultiplatformPublication(publication, stubJavadoc, publication.name)
@@ -76,7 +80,7 @@ fun Project.configureMppPublishing() {
                 configureMultiplatformPublication(publication, stubJavadoc, publication.name)
                 configureMultiplatformPublication(publication, stubJavadoc, publication.name)
                 publication.apply {
                 publication.apply {
                     artifacts.filter { it.classifier.isNullOrEmpty() && it.extension == "jar" }.forEach {
                     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"
             publication.artifactId = "${project.name}-metadata"
         }
         }
 
 
+        "jvm" -> {
+            publication.artifactId = "${project.name}-$moduleName"
+
+            useRelocatedPublication(publication, moduleName)
+        }
+
         else -> {
         else -> {
             // "jvm", "native", "js", "common"
             // "jvm", "native", "js", "common"
             publication.artifactId = "${project.name}-$moduleName"
             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 ->
 val publishPlatformArtifactsInRootModule: Project.(MavenPublication) -> Unit = { platformPublication ->
     lateinit var platformPomBuilder: XmlProvider
     lateinit var platformPomBuilder: XmlProvider
     platformPublication.pom.withXml { platformPomBuilder = this }
     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`
      * Kotlin packages. e.g. `io.ktor`
      */
      */
     vararg val packages: String,
     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`.
      * 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(
 class MultiplatformDependency private constructor(
     private val groupId: String,
     private val groupId: String,
     private val baseArtifactId: String,
     private val baseArtifactId: String,
     vararg val targets: 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 {
     companion object {
@@ -161,7 +203,7 @@ class MultiplatformDependency private constructor(
 
 
 fun ModuleDependency.exclude(multiplatformDependency: MultiplatformDependency) {
 fun ModuleDependency.exclude(multiplatformDependency: MultiplatformDependency) {
     multiplatformDependency.notations().forEach {
     multiplatformDependency.notations().forEach {
-        exclude(it)
+        exclude(it.toMap())
     }
     }
 }
 }
 
 
@@ -191,7 +233,10 @@ object ExcludeProperties {
 }
 }
 
 
 val `ktor-io` = ktor("io", Versions.ktor)
 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.`everything from slf4j`)
     exclude(ExcludeProperties.`slf4j-api`)
     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-websocket-serialization` = ktor("websocket-serialization", Versions.ktor)
 
 
 val `ktor-client-core` = ktor("client-core", 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.`ktor-io`)
     exclude(ExcludeProperties.`everything from slf4j`)
     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-darwin` = ktor("client-darwin", Versions.ktor)
 val `ktor-client-okhttp` = ktor("client-okhttp", Versions.ktor)
 val `ktor-client-okhttp` = ktor("client-okhttp", Versions.ktor)
 val `ktor-client-okhttp_relocated` =
 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.`ktor-io`)
         exclude(ExcludeProperties.`everything from slf4j`)
         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
  * 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.Action
 import org.gradle.api.DomainObjectCollection
 import org.gradle.api.DomainObjectCollection
 import org.gradle.api.Project
 import org.gradle.api.Project
@@ -66,7 +71,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
  *
  *
  * 如果你都使用 [relocateImplementation], 就会导致在 Android 平台发生 'Duplicated Class' 问题. 如果你都使用 [relocateCompileOnly] 则会在 clinit 阶段遇到 [NoClassDefFoundError]
  * 如果你都使用 [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-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.
  * 在 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].
  * 所以你需要为所有依赖了 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.
  * 被 relocate 的类会被直接当做是当前模块的类打包进 JAR.
@@ -89,7 +101,7 @@ object RelocationNotes
 /**
 /**
  * 添加一个通常的 [compileOnly][KotlinDependencyHandler.compileOnly] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  * 添加一个通常的 [compileOnly][KotlinDependencyHandler.compileOnly] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  *
  *
- * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
+ * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RelocationConfig.RELOCATION_ROOT_PACKAGE].
  * 运行时 (runtime) **不会**包含被 relocate 的依赖及其所有间接依赖.
  * 运行时 (runtime) **不会**包含被 relocate 的依赖及其所有间接依赖.
  *
  *
  * @see RelocationNotes
  * @see RelocationNotes
@@ -102,7 +114,9 @@ fun KotlinDependencyHandler.relocateCompileOnly(
     }
     }
     project.relocationFilters.add(
     project.relocationFilters.add(
         RelocationFilter(
         RelocationFilter(
-            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
+            relocatedDependency.notationsToExcludeInPom,
+            relocatedDependency.packages.toList(),
+            includeInRuntime = false,
         )
         )
     )
     )
     // Don't add to runtime
     // Don't add to runtime
@@ -112,7 +126,7 @@ fun KotlinDependencyHandler.relocateCompileOnly(
 /**
 /**
  * 添加一个通常的 [compileOnly][KotlinDependencyHandler.compileOnly] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  * 添加一个通常的 [compileOnly][KotlinDependencyHandler.compileOnly] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  *
  *
- * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
+ * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RelocationConfig.RELOCATION_ROOT_PACKAGE].
  * 运行时 (runtime) **不会**包含被 relocate 的依赖及其所有间接依赖.
  * 运行时 (runtime) **不会**包含被 relocate 的依赖及其所有间接依赖.
  *
  *
  * @see RelocationNotes
  * @see RelocationNotes
@@ -127,7 +141,9 @@ fun DependencyHandler.relocateCompileOnly(
         })
         })
     project.relocationFilters.add(
     project.relocationFilters.add(
         RelocationFilter(
         RelocationFilter(
-            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
+            relocatedDependency.notationsToExcludeInPom,
+            relocatedDependency.packages.toList(),
+            includeInRuntime = false,
         )
         )
     )
     )
     // Don't add to runtime
     // Don't add to runtime
@@ -137,7 +153,7 @@ fun DependencyHandler.relocateCompileOnly(
 /**
 /**
  * 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  * 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  *
  *
- * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
+ * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都会**被 relocate 到 [RelocationConfig.RELOCATION_ROOT_PACKAGE].
  * 运行时 (runtime) 将**会**包含被 relocate 的依赖及其所有间接依赖.
  * 运行时 (runtime) 将**会**包含被 relocate 的依赖及其所有间接依赖.
  *
  *
  * @see RelocationNotes
  * @see RelocationNotes
@@ -151,13 +167,14 @@ fun KotlinDependencyHandler.relocateImplementation(
     }
     }
     project.relocationFilters.add(
     project.relocationFilters.add(
         RelocationFilter(
         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(
     addDependencyTo(
         project.dependencies,
         project.dependencies,
-        SHADOW_RELOCATION_CONFIGURATION_NAME,
+        configurationName,
         relocatedDependency.notation,
         relocatedDependency.notation,
         Action<ExternalModuleDependency> {
         Action<ExternalModuleDependency> {
             relocatedDependency.exclusionAction(this)
             relocatedDependency.exclusionAction(this)
@@ -171,7 +188,7 @@ fun KotlinDependencyHandler.relocateImplementation(
 /**
 /**
  * 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  * 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  *
  *
- * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用都会被 relocate 到 [RELOCATION_ROOT_PACKAGE].
+ * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用都会被 relocate 到 [RelocationConfig.RELOCATION_ROOT_PACKAGE].
  * 运行时 (runtime) 将会包含被 relocate 的依赖及其所有间接依赖.
  * 运行时 (runtime) 将会包含被 relocate 的依赖及其所有间接依赖.
  *
  *
  * @see RelocationNotes
  * @see RelocationNotes
@@ -187,13 +204,14 @@ fun DependencyHandler.relocateImplementation(
         })
         })
     project.relocationFilters.add(
     project.relocationFilters.add(
         RelocationFilter(
         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(
     addDependencyTo(
         project.dependencies,
         project.dependencies,
-        SHADOW_RELOCATION_CONFIGURATION_NAME,
+        configurationName,
         relocatedDependency.notation,
         relocatedDependency.notation,
         Action<ExternalModuleDependency> {
         Action<ExternalModuleDependency> {
             relocatedDependency.exclusionAction(this)
             relocatedDependency.exclusionAction(this)
@@ -204,8 +222,7 @@ fun DependencyHandler.relocateImplementation(
     return dependency
     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() {
 private fun ExternalModuleDependency.intrinsicExclusions() {
     exclude(ExcludeProperties.`everything from kotlin`)
     exclude(ExcludeProperties.`everything from kotlin`)
@@ -213,14 +230,10 @@ private fun ExternalModuleDependency.intrinsicExclusions() {
 }
 }
 
 
 
 
-const val SHADOW_RELOCATION_CONFIGURATION_NAME = "shadowRelocation"
-
-
 data class RelocationFilter(
 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.
      * Pack relocated dependency into the fat jar. If set to `false`, dependencies will be removed.
      * This is to avoid duplicated classes. See #2291.
      * This is to avoid duplicated classes. See #2291.
@@ -229,10 +242,9 @@ data class RelocationFilter(
 ) {
 ) {
 
 
     fun matchesDependency(groupId: String?, artifactId: String?): Boolean {
     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
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
  */
 
 
+import shadow.registerRegularShadowTaskForJvmProject
+import shadow.shadowImplementation
+
 plugins {
 plugins {
     kotlin("jvm")
     kotlin("jvm")
     kotlin("plugin.serialization")
     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 许可证的约束, 可以在以下链接找到该许可证.
  * 此源代码的使用受 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.
  * 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")
 @file:Suppress("UnusedImport")
 
 
+import shadow.configureRelocatedShadowJarForJvmProject
+import shadow.relocateImplementation
+
 plugins {
 plugins {
     kotlin("jvm")
     kotlin("jvm")
     kotlin("plugin.serialization")
     kotlin("plugin.serialization")
@@ -34,7 +37,7 @@ dependencies {
 val shadow = configureRelocatedShadowJarForJvmProject(kotlin)
 val shadow = configureRelocatedShadowJarForJvmProject(kotlin)
 
 
 if (System.getenv("MIRAI_IS_SNAPSHOTS_PUBLISHING")?.toBoolean() != true) {
 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)
     configurePublishing("mirai-core-all", addShadowJar = false)
 
 

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

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

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

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

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

@@ -12,6 +12,8 @@
 import BinaryCompatibilityConfigurator.configureBinaryValidators
 import BinaryCompatibilityConfigurator.configureBinaryValidators
 import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractNativeLibrary
 import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractNativeLibrary
 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
+import shadow.relocateCompileOnly
+import shadow.relocateImplementation
 
 
 plugins {
 plugins {
     id("com.android.library")
     id("com.android.library")
@@ -49,6 +51,9 @@ kotlin {
                 implementation(`kotlinx-serialization-protobuf`)
                 implementation(`kotlinx-serialization-protobuf`)
                 implementation(`kotlinx-atomicfu`)
                 implementation(`kotlinx-atomicfu`)
 
 
+                // runtime from mirai-core-utils
+                relocateCompileOnly(`ktor-io_relocated`)
+
 //                relocateImplementation(`ktor-http_relocated`)
 //                relocateImplementation(`ktor-http_relocated`)
 //                relocateImplementation(`ktor-serialization_relocated`)
 //                relocateImplementation(`ktor-serialization_relocated`)
 //                relocateImplementation(`ktor-websocket-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 许可证的约束, 可以在以下链接找到该许可证.
  * 此源代码的使用受 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.
  * 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
         // Always print this very important message
         logger.warn("[publishMiraiArtifactsToMavenLocal] Project version is '${project.version}'.")
         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 许可证的约束, 可以在以下链接找到该许可证.
  * 此源代码的使用受 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.
  * 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
         @JvmStatic
         fun isMiraiLocalAvailable(): Boolean {
         fun isMiraiLocalAvailable(): Boolean {
+            val commandLine =
+                """./gradlew publishMiraiArtifactsToMavenLocal -Dmirai.build.project.version=$miraiLocalVersion"""
+
             return if (mavenLocalDir.resolve("net/mamoe/mirai-core/$miraiLocalVersion").exists()) {
             return if (mavenLocalDir.resolve("net/mamoe/mirai-core/$miraiLocalVersion").exists()) {
                 println(
                 println(
                     """
                     """
@@ -41,7 +44,8 @@ abstract class AbstractTest {
                 - added/removed a dependency for mirai-core series modules
                 - added/removed a dependency for mirai-core series modules
                 - changed version of any of the dependencies 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()
             """.trimIndent()
                 )
                 )
                 true
                 true
@@ -58,7 +62,9 @@ abstract class AbstractTest {
                 Note that you can ignore this test if you did not change project (dependency) structure.
                 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.
                 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)
                 Then you can run this test again. (By your original way or ./gradlew :mirai-deps-test:test)
                 """.trimIndent()
                 """.trimIndent()
                 System.err.println(
                 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 许可证的约束, 可以在以下链接找到该许可证.
  * 此源代码的使用受 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.
  * 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 {
             dependencies {
                 implementation("net.mamoe:mirai-core:$miraiLocalVersion") {
                 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()
         """.trimIndent()
@@ -130,6 +130,7 @@ class CoreShadowRelocationTest : AbstractTest() {
     @EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
     @EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
     fun `test mirai-core-api without transitive mirai-core-utils`() {
     fun `test mirai-core-api without transitive mirai-core-utils`() {
         val fragment = buildTestCases {
         val fragment = buildTestCases {
+            -`mirai-core-utils`
             -both(`ktor-io`)
             -both(`ktor-io`)
             -both(`ktor-client-core`)
             -both(`ktor-client-core`)
             -both(`ktor-client-okhttp`)
             -both(`ktor-client-okhttp`)
@@ -143,7 +144,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             """
             """
             dependencies {
             dependencies {
                 implementation("net.mamoe:mirai-core-api:$miraiLocalVersion") {
                 implementation("net.mamoe:mirai-core-api:$miraiLocalVersion") {
-                    exclude("net.mamoe", "mirai-core-utils")
+                    exclude("net.mamoe", "mirai-core-utils-jvm")
                 }
                 }
             }
             }
         """.trimIndent()
         """.trimIndent()
@@ -233,6 +234,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             val relocated: (FunctionTestCase.() -> FunctionTestCase)? = null,
             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-io` = ClassTestCase("ktor-io ByteBufferChannel", ByteBufferChannel)
         val `ktor-client-core` = ClassTestCase("ktor-client-core HttpClient", HttpClient)
         val `ktor-client-core` = ClassTestCase("ktor-client-core HttpClient", HttpClient)
         val `ktor-client-okhttp` = ClassTestCase("ktor-client-core OkHttp", KtorOkHttp)
         val `ktor-client-okhttp` = ClassTestCase("ktor-client-core OkHttp", KtorOkHttp)
@@ -302,7 +304,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             result.append(
             result.append(
                 """
                 """
                       @Test
                       @Test
-                      fun `no relocated ${name}`() {
+                      fun `no ${name}`() {
                         assertThrows<ClassNotFoundException> { Class.forName("$qualifiedClassName") }
                         assertThrows<ClassNotFoundException> { Class.forName("$qualifiedClassName") }
                       }
                       }
                     """.trimIndent()
                     """.trimIndent()