Переглянути джерело

Configure shadow relocation and add checks for multiplatform publishing

Him188 3 роки тому
батько
коміт
fd67ba9204

+ 10 - 1
.github/workflows/build.yml

@@ -280,6 +280,9 @@ jobs:
       - name: "Assemble"
         run: ./gradlew assemble ${{ env.gradleArgs }}
 
+      - name: Publish Local Artifacts
+        run: ./gradlew :mirai-deps-test:publishMiraiLocalArtifacts ${{ env.gradleArgs }}
+
       - name: "Check"
         run: ./gradlew check ${{ env.gradleArgs }}
 
@@ -402,4 +405,10 @@ jobs:
         run: ./gradlew :mirai-core-api:${{ matrix.targetName }}Test ${{ env.gradleArgs }}
 
       - name: "Test mirai-core for ${{ matrix.os }}"
-        run: ./gradlew :mirai-core:${{ matrix.targetName }}Test ${{ env.gradleArgs }}
+        run: ./gradlew :mirai-core:${{ matrix.targetName }}Test ${{ env.gradleArgs }}
+
+      - name: Publish Local Artifacts
+        run: ./gradlew :mirai-deps-test:publishMiraiLocalArtifacts ${{ env.gradleArgs }}
+
+      - name: Check Publication
+        run: ./gradlew :mirai-deps-test:check ${{ env.gradleArgs }}

+ 5 - 1
.github/workflows/snapshots.yml

@@ -37,9 +37,13 @@ jobs:
       - name: Assemble
         run: ./gradlew assemble --scan
 
+      - name: Publish Local Artifacts
+        run: >
+          ./gradlew :mirai-deps-test:publishMiraiLocalArtifacts --scan
+
       - name: Check
         run: >
-          ./gradlew check --scan --no-parallel
+          ./gradlew check --scan
           -Dmirai.network.show.all.components=true
           -Dkotlinx.coroutines.debug=on
           -Dmirai.network.show.packet.details=true

+ 4 - 45
build.gradle.kts

@@ -12,8 +12,6 @@
 import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
 import org.jetbrains.dokka.base.DokkaBase
 import org.jetbrains.dokka.base.DokkaBaseConfiguration
-import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
-import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import java.time.LocalDateTime
 
 buildscript {
@@ -35,12 +33,13 @@ buildscript {
 }
 
 plugins {
-    kotlin("jvm") // version Versions.kotlinCompiler
-    kotlin("plugin.serialization") version Versions.kotlinCompiler
+    kotlin("jvm") apply false // version Versions.kotlinCompiler
+    kotlin("plugin.serialization") version Versions.kotlinCompiler apply false
     id("com.google.osdetector")
     id("org.jetbrains.dokka") version Versions.dokka
     id("me.him188.kotlin-jvm-blocking-bridge") version Versions.blockingBridge
-    id("me.him188.kotlin-dynamic-delegation") version Versions.dynamicDelegation
+    id("me.him188.kotlin-dynamic-delegation") version Versions.dynamicDelegation apply false
+    id("me.him188.maven-central-publish") version Versions.mavenCentralPublish apply false
     id("com.gradle.plugin-publish") version "1.0.0-rc-3" apply false
     id("org.jetbrains.kotlinx.binary-compatibility-validator") version Versions.binaryValidator apply false
 }
@@ -179,43 +178,3 @@ fun Project.configureDokka() {
         }
     }
 }
-
-fun Project.configureMppShadow() {
-    val kotlin =
-        runCatching {
-
-            (this as ExtensionAware).extensions.getByName("kotlin") as? KotlinMultiplatformExtension
-        }.getOrNull() ?: return
-
-    if (project.configurations.findByName("jvmRuntimeClasspath") != null) {
-        val shadowJvmJar by tasks.creating(ShadowJar::class) sd@{
-            group = "mirai"
-            archiveClassifier.set("-all")
-
-            val compilations =
-                kotlin.targets.filter { it.platformType == KotlinPlatformType.jvm }
-                    .map { it.compilations["main"] }
-
-            compilations.forEach {
-                dependsOn(it.compileKotlinTask)
-                from(it.output)
-            }
-
-            from(project.configurations.findByName("jvmRuntimeClasspath"))
-
-            this.exclude { file ->
-                file.name.endsWith(".sf", ignoreCase = true)
-            }
-
-            /*
-        this.manifest {
-            this.attributes(
-                "Manifest-Version" to 1,
-                "Implementation-Vendor" to "Mamoe Technologies",
-                "Implementation-Title" to this.name.toString(),
-                "Implementation-Version" to this.version.toString()
-            )
-        }*/
-        }
-    }
-}

+ 1 - 0
buildSrc/build.gradle.kts

@@ -57,6 +57,7 @@ dependencies {
     api(asm("tree"))
     api(asm("util"))
     api(asm("commons"))
+    api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
 
     api("gradle.plugin.com.google.gradle:osdetector-gradle-plugin:1.7.0")
 

+ 3 - 3
buildSrc/src/main/kotlin/HmppConfigure.kt

@@ -25,7 +25,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
 import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable
 import java.io.File
 
-private val miraiPlatform = Attribute.of(
+val MIRAI_PLATFORM_ATTRIBUTE = Attribute.of(
     "net.mamoe.mirai.platform", String::class.java
 )
 
@@ -140,7 +140,7 @@ fun Project.configureJvmTargetsHierarchical() {
                     this.compileKotlinTask.enabled = false // IDE complain
                 }
                 attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.common) // magic
-                attributes.attribute(miraiPlatform, "jvmBase") // avoid resolution
+                attributes.attribute(MIRAI_PLATFORM_ATTRIBUTE, "jvmBase") // avoid resolution
             }
         }
 
@@ -160,7 +160,7 @@ fun Project.configureJvmTargetsHierarchical() {
                 jvm("android") {
                     attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.androidJvm)
                     if (IDEA_ACTIVE) {
-                        attributes.attribute(miraiPlatform, "android") // avoid resolution
+                        attributes.attribute(MIRAI_PLATFORM_ATTRIBUTE, "android") // avoid resolution
                     }
                 }
                 val androidMain by sourceSets.getting

+ 13 - 13
buildSrc/src/main/kotlin/Mpp.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
  *
- *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ * 此源代码的使用受 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/master/LICENSE
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
 import org.gradle.api.NamedDomainObjectCollection
@@ -13,17 +13,10 @@ import org.gradle.api.Project
 import org.gradle.api.artifacts.DependencySubstitutions
 import org.gradle.api.artifacts.ResolutionStrategy
 import org.gradle.api.artifacts.component.ComponentSelector
+import org.gradle.api.plugins.ExtensionAware
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
 import java.util.*
 
-/*
- * Copyright 2020 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/master/LICENSE
- */
-
 private object ProjectAndroidSdkAvailability {
     val map: MutableMap<String, Boolean> = mutableMapOf()
 
@@ -117,3 +110,10 @@ fun ResolutionStrategy.substituteDependencies(action: ResolutionStrategyDsl.() -
         action(ResolutionStrategyDsl(this))
     }
 }
+
+
+val Project.kotlinMpp
+    get() = runCatching {
+        (this as ExtensionAware).extensions.getByName("kotlin") as? KotlinMultiplatformExtension
+    }.getOrNull()
+

+ 62 - 23
buildSrc/src/main/kotlin/MppPublishing.kt

@@ -11,6 +11,7 @@ import org.gradle.api.Project
 import org.gradle.api.XmlProvider
 import org.gradle.api.publish.maven.MavenArtifact
 import org.gradle.api.publish.maven.MavenPublication
+import org.gradle.api.tasks.TaskProvider
 import org.gradle.jvm.tasks.Jar
 import org.gradle.kotlin.dsl.get
 import org.gradle.kotlin.dsl.register
@@ -43,30 +44,39 @@ fun Project.configureMppPublishing() {
         publishing {
             logPublishing("Publications: ${publications.joinToString { it.name }}")
 
-            publications.filterIsInstance<MavenPublication>().forEach { publication ->
-                // Maven Central always require javadoc.jar
-                publication.artifact(stubJavadoc)
+            val (nonJvmPublications, jvmPublications) = publications.filterIsInstance<MavenPublication>()
+                .partition { publication -> tasks.findByName("relocate${publication.name.titlecase()}Dependencies") == null }
 
-                publication.setupPom(project)
-
-                logPublishing(publication.name)
-                when (val type = publication.name) {
-                    "kotlinMultiplatform" -> {
-                        publication.artifactId = project.name
-
-                        // publishPlatformArtifactsInRootModule(publications.getByName("jvm") as MavenPublication)
-
-                        // TODO: 2021/1/30 现在添加 JVM 到 root module 会导致 Gradle 依赖无法解决
-                        // https://github.com/mamoe/mirai/issues/932
-                    }
-                    "metadata" -> { // TODO: 2021/1/21 seems no use. none `type` is "metadata"
-                        publication.artifactId = "${project.name}-metadata"
-                    }
-                    "common" -> {
-                    }
-                    else -> {
-                        // "jvm", "native", "js"
-                        publication.artifactId = "${project.name}-$type"
+            for (publication in nonJvmPublications) {
+                configureMultiplatformPublication(publication, stubJavadoc, publication.name)
+            }
+            for (publication in jvmPublications) {
+//                publications.remove(publication)
+//                val newPublication =
+//                    publications.register(publication.name + "Shadowed", MavenPublication::class.java) {
+//                        val target = kotlinTargets.orEmpty().single { it.targetName == publication.name }
+//                        from(target.components.single())
+//                        this.groupId = publication.groupId
+//                        this.artifactId = publication.artifactId
+//                        this.version = publication.version
+//                        artifacts {
+//                            publication.artifacts
+//                                .filter { !(it.classifier.isNullOrEmpty() && it.extension == "jar") } // not .jar
+//                                .forEach { artifact(it) } // copy Kotlin metadata artifacts
+//                        }
+//                        artifacts.removeAll { it.classifier.isNullOrEmpty() && it.extension == "jar" }
+//                        // add relocated jar
+//                        tasks.findByName("relocate${publication.name.titlecase()}Dependencies")?.let { relocation ->
+//                            artifact(relocation) {
+//                                classifier = ""
+//                                extension = "jar"
+//                            }
+//                        }
+//                    }
+                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"))
                     }
                 }
             }
@@ -75,6 +85,35 @@ fun Project.configureMppPublishing() {
     }
 }
 
+private fun Project.configureMultiplatformPublication(
+    publication: MavenPublication,
+    stubJavadoc: TaskProvider<Jar>,
+    moduleName: String,
+) {
+    // Maven Central always require javadoc.jar
+    publication.artifact(stubJavadoc)
+    publication.setupPom(project)
+
+    logPublishing(publication.name + ": moduleName = $moduleName")
+    when (moduleName) {
+        "kotlinMultiplatform" -> {
+            publication.artifactId = project.name
+
+            // publishPlatformArtifactsInRootModule(publications.getByName("jvm") as MavenPublication)
+
+            // TODO: 2021/1/30 现在添加 JVM 到 root module 会导致 Gradle 依赖无法解决
+            // https://github.com/mamoe/mirai/issues/932
+        }
+        "metadata" -> { // TODO: 2021/1/21 seems no use. none `type` is "metadata"
+            publication.artifactId = "${project.name}-metadata"
+        }
+        else -> {
+            // "jvm", "native", "js", "common"
+            publication.artifactId = "${project.name}-$moduleName"
+        }
+    }
+}
+
 val publishPlatformArtifactsInRootModule: Project.(MavenPublication) -> Unit = { platformPublication ->
     lateinit var platformPomBuilder: XmlProvider
     platformPublication.pom.withXml { platformPomBuilder = this }

+ 4 - 4
buildSrc/src/main/kotlin/ProjectConfigure.kt

@@ -111,8 +111,8 @@ fun Project.configureKotlinTestSettings() {
             dependencies {
                 "testImplementation"(kotlin("test-junit5"))?.because(b)
 
-                "testApi"("org.junit.jupiter:junit-jupiter-api:${Versions.junit}")?.because(b)
-                "testRuntimeOnly"("org.junit.jupiter:junit-jupiter-engine:${Versions.junit}")?.because(b)
+                "testApi"(`junit-jupiter-api`)?.because(b)
+                "testRuntimeOnly"(`junit-jupiter-engine`)?.because(b)
             }
         }
         isKotlinMpp -> {
@@ -121,8 +121,8 @@ fun Project.configureKotlinTestSettings() {
                     sourceSet.dependencies {
                         implementation(kotlin("test-junit5"))?.because(b)
 
-                        implementation("org.junit.jupiter:junit-jupiter-api:${Versions.junit}")?.because(b)
-                        runtimeOnly("org.junit.jupiter:junit-jupiter-engine:${Versions.junit}")?.because(b)
+                        implementation(`junit-jupiter-api`)?.because(b)
+                        runtimeOnly(`junit-jupiter-engine`)?.because(b)
                     }
                 }
 

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

@@ -0,0 +1,276 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import org.gradle.api.DomainObjectCollection
+import org.gradle.api.Project
+import org.gradle.api.publish.tasks.GenerateModuleMetadata
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.creating
+import org.gradle.kotlin.dsl.extra
+import org.gradle.kotlin.dsl.get
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
+import java.io.File
+
+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_ATTRIBUTE) == null
+    }) {
+        configureRelocationForTarget(project)
+    }
+
+    // regular shadow file, with suffix `-all`
+    configureRegularShadowJar(kotlin)
+}
+
+/**
+ * Relocate some dependencies for `.jar`
+ */
+private fun KotlinTarget.configureRelocationForTarget(project: Project) = project.run {
+    val relocateDependencies =
+        // e.g. relocateJvmDependencies
+        tasks.create("relocate${targetName.titlecase()}Dependencies", ShadowJar::class) {
+            group = "mirai"
+            description = "Relocate dependencies to internal package"
+            destinationDirectory.set(buildDir.resolve("libs"))
+//            archiveClassifier.set("")
+            archiveBaseName.set("${project.name}-${targetName.toLowerCase()}")
+
+            dependsOn(compilations["main"].compileKotlinTask) // compileKotlinJvm
+
+            // Run after all *Jar tasks from all projects, since Kotlin compiler may depend on the .jar file, concurrently modifying the jar will cause Kotlin compiler to fail.
+//            allprojects
+//                .asSequence()
+//                .flatMap { it.tasks }
+//                .filter { it.name.contains("compileKotlin") }
+//                .forEach { jar ->
+//                    mustRunAfter(jar)
+//                }
+
+            from(compilations["main"].output)
+
+//            // change name to
+//            doLast {
+//                outputs.files.singleFile.renameTo(
+//                    outputs.files.singleFile.parentFile.resolve(
+//                        "${project.name}-${targetName.toLowerCase()}-${project.version}.jar"
+//                    )
+//                )
+//            }
+            // Filter only those should be relocated
+
+            afterEvaluate {
+                setRelocations()
+
+                var fileFiltered = relocationFilters.isEmpty()
+                from(project.configurations.getByName("${targetName}RuntimeClasspath")
+                    .files
+                    .filter { file ->
+                        relocationFilters.any { filter ->
+                            // file.absolutePath example: /Users/xxx/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.7.0-RC/7f9f07fc65e534c15a820f61d846b9ffdba8f162/kotlin-stdlib-jdk8-1.7.0-RC.jar
+                            filter.matchesFile(file)
+                        }.also {
+                            fileFiltered = fileFiltered || it
+                            if (it) {
+                                println("Including file: ${file.absolutePath}")
+                            }
+                        }
+                    }
+                )
+                check(fileFiltered) { "[Shadow Relocation] Expected at least one file filtered for target $targetName. Filters: $relocationFilters" }
+            }
+        }
+
+    val allTasks = rootProject.allprojects.asSequence().flatMap { it.tasks }
+    allTasks
+        .filter {
+            it.name.startsWith("publish${targetName.titlecase()}PublicationTo")
+        }
+        .onEach { it.dependsOn(relocateDependencies) }
+        .count().let {
+            check(it > 0) { "[Shadow Relocation] Expected at least one publication matched for target $targetName." }
+        }
+
+    // Ensure all compilation has finished, otherwise Kotlin compiler will complain.
+    allTasks
+        .filter { it.name.endsWith("Jar") }
+        .onEach { relocateDependencies.dependsOn(it) }
+        .count().let {
+            check(it > 0) { "[Shadow Relocation] Expected at least one task matched for target $targetName." }
+        }
+
+    allTasks
+        .filter { it.name.startsWith("compileKotlin") }
+        .onEach { relocateDependencies.dependsOn(it) }
+        .count().let {
+            check(it > 0) { "[Shadow Relocation] Expected at least one task matched for target $targetName." }
+        }
+
+    val metadataTask =
+        tasks.getByName("generateMetadataFileFor${targetName.capitalize()}Publication") as GenerateModuleMetadata
+    relocateDependencies.dependsOn(metadataTask)
+
+    afterEvaluate {
+        // remove 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)
+                        }
+                    ) {
+                        println("[Shadow Relocation] Filtering out $groupId:$artifactId from pom")
+                        check(node.remove(dep)) { "Failed to remove dependency node" }
+                    }
+                }
+            }
+        }
+
+        // remove dependencies in Kotlin module metadata
+        relocateDependencies.doLast {
+            // mirai-core-jvm-2.13.0.module
+            val file = metadataTask.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))
+        }
+    }
+}
+
+private fun Project.configureRegularShadowJar(kotlin: KotlinMultiplatformExtension) {
+    if (project.configurations.findByName("jvmRuntimeClasspath") != null) {
+        val shadowJvmJar by tasks.creating(ShadowJar::class) sd@{
+            group = "mirai"
+            archiveClassifier.set("-all")
+
+            val compilations =
+                kotlin.targets.filter { it.platformType == KotlinPlatformType.jvm }
+                    .map { it.compilations["main"] }
+
+            compilations.forEach {
+                dependsOn(it.compileKotlinTask)
+                from(it.output)
+            }
+
+            setRelocations()
+
+            from(project.configurations.findByName("jvmRuntimeClasspath"))
+
+            this.exclude { file ->
+                file.name.endsWith(".sf", ignoreCase = true)
+            }
+
+            /*
+        this.manifest {
+            this.attributes(
+                "Manifest-Version" to 1,
+                "Implementation-Vendor" to "Mamoe Technologies",
+                "Implementation-Title" to this.name.toString(),
+                "Implementation-Version" to this.version.toString()
+            )
+        }*/
+        }
+    }
+}
+
+data class RelocationFilter(
+    val groupId: String,
+    val artifactId: String? = null,
+    val shadowFilter: String = groupId,
+    val filesFilter: String = groupId.replace(".", "/")
+) {
+
+    fun matchesFile(file: File): Boolean {
+        val path = file.absolutePath.replace("\\", "/")
+        return filesFilter in path
+                || groupId in path
+    }
+
+    fun matchesDependency(groupId: String?, artifactId: String?): Boolean {
+        if (this.groupId == groupId) return true
+        if (this.artifactId != null && this.artifactId == artifactId) return true
+
+        return false
+    }
+}
+
+val Project.relocationFilters: DomainObjectCollection<RelocationFilter>
+    get() {
+        if (project.extra.has("relocationFilters")) {
+            @Suppress("UNCHECKED_CAST")
+            return project.extra.get("relocationFilters") as DomainObjectCollection<RelocationFilter>
+
+        } else {
+            val container = project.objects.domainObjectSet(RelocationFilter::class.java)
+            project.extra.set("relocationFilters", container)
+            return container
+        }
+    }
+
+private const val relocationRootPackage = "net.mamoe.mirai.internal.deps"
+
+private fun ShadowJar.setRelocations() {
+    project.relocationFilters.forEach { relocation ->
+        relocate(relocation.shadowFilter, "$relocationRootPackage.${relocation.groupId}")
+    }
+}
+
+fun Project.configureRelocationForCore() {
+    relocateAllFromGroupId("io.ktor")
+}
+
+fun Project.relocateAllFromGroupId(groupId: String) {
+    relocationFilters.add(RelocationFilter(groupId))
+}
+
+// This does not include transitive dependencies
+fun Project.relocateExactArtifact(groupId: String, artifactId: String) {
+    relocationFilters.add(RelocationFilter(groupId, artifactId))
+}

+ 9 - 3
buildSrc/src/main/kotlin/Versions.kt

@@ -13,6 +13,9 @@ import org.gradle.api.attributes.Attribute
 import org.gradle.kotlin.dsl.exclude
 import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
 
+// DO NOT CHANGE FILENAME OR RELATIVE PATH TO ROOT PROJECT.
+// mirai-deps-test DEPENDS ON THE PATH.
+
 object Versions {
     val project = System.getenv("mirai.build.project.version")
         ?: /*PROJECT_VERSION_START*/"2.13.0"/*PROJECT_VERSION_END*/
@@ -22,11 +25,11 @@ object Versions {
     val consoleIntellij = "221-$project-162-1" // idea-mirai-kotlin-patch
     val consoleTerminal = project
 
-    const val kotlinCompiler = "1.7.0-RC"
+    const val kotlinCompiler = "1.7.0"
     const val kotlinStdlib = kotlinCompiler
     const val dokka = "1.6.21"
 
-    const val kotlinCompilerForIdeaPlugin = "1.7.0-RC"
+    const val kotlinCompilerForIdeaPlugin = "1.7.0"
 
     const val coroutines = "1.6.2"
     const val atomicFU = "0.17.2"
@@ -40,6 +43,7 @@ object Versions {
 
     const val blockingBridge = "2.1.0-170.1"
     const val dynamicDelegation = "0.3.0-170.1"
+    const val mavenCentralPublish = "1.0.0-dev-3"
 
     const val androidGradlePlugin = "4.1.1"
     const val android = "4.1.1.4"
@@ -138,7 +142,9 @@ const val `kotlin-stdlib-jdk8` = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Vers
 const val `kotlin-reflect` = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinStdlib}"
 const val `kotlin-test` = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlinStdlib}"
 const val `kotlin-test-junit5` = "org.jetbrains.kotlin:kotlin-test-junit5:${Versions.kotlinStdlib}"
-
+const val `junit-jupiter-api` = "org.junit.jupiter:junit-jupiter-api:${Versions.junit}"
+const val `junit-jupiter-params` = "org.junit.jupiter:junit-jupiter-params:${Versions.junit}"
+const val `junit-jupiter-engine` = "org.junit.jupiter:junit-jupiter-engine:${Versions.junit}"
 
 //const val `mirai-core-api` = "net.mamoe:mirai-core-api:${Versions.core}"
 //const val `mirai-core` = "net.mamoe:mirai-core:${Versions.core}"

+ 14 - 1
buildSrc/src/main/kotlin/utils.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -7,6 +7,8 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
+import groovy.util.Node
+import groovy.util.NodeList
 import java.io.InputStream
 import java.io.OutputStream
 import java.security.MessageDigest
@@ -60,3 +62,14 @@ fun InputStream.md5(): ByteArray {
     }
     return digest.digest()
 }
+
+
+
+
+fun Node.getSingleChild(name: String): Node {
+    return (this.get(name) as NodeList).single() as Node
+}
+
+fun Node.childrenNodes(): List<Node> {
+    return this.children().filterIsInstance<Node>()
+}

+ 5 - 6
mirai-console/tools/gradle-plugin/build.gradle.kts

@@ -46,14 +46,13 @@ dependencies {
     implementation(`log4j-core`)
 
     testApi(kotlin("test-junit5"))
-    testApi("org.junit.jupiter:junit-jupiter-api:${Versions.junit}")
-    testApi("org.junit.jupiter:junit-jupiter-params:${Versions.junit}")
+    testApi(`junit-jupiter-api`)
+    testApi(`junit-jupiter-params`)
 
     "integTestApi"(kotlin("test-junit5"))
-    "integTestApi"("org.junit.jupiter:junit-jupiter-api:${Versions.junit}")
-    "integTestApi"("org.junit.jupiter:junit-jupiter-params:${Versions.junit}")
-    "integTestImplementation"("org.junit.jupiter:junit-jupiter-engine:${Versions.junit}")
-//    "integTestImplementation"("org.spockframework:spock-core:1.3-groovy-2.5")
+    "integTestApi"(`junit-jupiter-api`)
+    "integTestApi"(`junit-jupiter-params`)
+    "integTestImplementation"(`junit-jupiter-engine`)
     "integTestImplementation"(gradleTestKit())
 
     kotlinVersionForIntegrationTest(kotlin("gradle-plugin", "1.5.21"))

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

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
  *
- *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ * 此源代码的使用受 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/master/LICENSE
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
 @file:Suppress("UnusedImport")
@@ -27,4 +27,6 @@ dependencies {
 
 if (System.getenv("MIRAI_IS_SNAPSHOTS_PUBLISHING")?.toBoolean() != true) {
     configurePublishing("mirai-core-all")
-}
+}
+
+configureRelocationForCore()

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

@@ -18,6 +18,7 @@ plugins {
     id("signing")
     id("me.him188.kotlin-jvm-blocking-bridge")
     id("me.him188.kotlin-dynamic-delegation")
+//    id("me.him188.maven-central-publish")
 
     `maven-publish`
 }
@@ -105,4 +106,13 @@ if (tasks.findByName("androidMainClasses") != null) {
 }
 
 configureMppPublishing()
-configureBinaryValidators(setOf("jvm", "android").filterTargets())
+configureBinaryValidators(setOf("jvm", "android").filterTargets())
+configureRelocationForCore()
+
+//mavenCentralPublish {
+//    artifactId = "mirai-core-api"
+//    githubProject("mamoe", "mirai")
+//    developer("Mamoe Technologies", email = "support@mamoe.net", url = "https://github.com/mamoe")
+//    licenseFromGitHubProject("AGPLv3", "dev")
+//    publishPlatformArtifactsInRootModule = "jvm"
+//}

+ 10 - 18
mirai-core-utils/build.gradle.kts

@@ -15,6 +15,7 @@ plugins {
 
     id("kotlinx-atomicfu")
     id("me.him188.kotlin-jvm-blocking-bridge")
+//    id("me.him188.maven-central-publish")
     `maven-publish`
 }
 
@@ -94,22 +95,13 @@ if (tasks.findByName("androidMainClasses") != null) {
     tasks.getByName("androidTest").dependsOn("checkAndroidApiLevel")
 }
 
-fun org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler.implementation1(dependencyNotation: String) =
-    implementation(dependencyNotation) {
-        exclude("org.jetbrains.kotlin", "kotlin-stdlib")
-        exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core")
-        exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-common")
-        exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-jvm")
-        exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-metadata")
-    }
-
-fun org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler.api1(dependencyNotation: String) =
-    api(dependencyNotation) {
-        exclude("org.jetbrains.kotlin", "kotlin-stdlib")
-        exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core")
-        exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-common")
-        exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-jvm")
-        exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-metadata")
-    }
-
 configureMppPublishing()
+configureRelocationForCore()
+
+//mavenCentralPublish {
+//    artifactId = "mirai-core-utils"
+//    githubProject("mamoe", "mirai")
+//    developer("Mamoe Technologies", email = "support@mamoe.net", url = "https://github.com/mamoe")
+//    licenseFromGitHubProject("AGPLv3", "dev")
+//    publishPlatformArtifactsInRootModule = "jvm"
+//}

+ 11 - 1
mirai-core/build.gradle.kts

@@ -19,6 +19,7 @@ plugins {
     kotlin("plugin.serialization")
     id("me.him188.kotlin-jvm-blocking-bridge")
     id("me.him188.kotlin-dynamic-delegation")
+//    id("me.him188.maven-central-publish")
     `maven-publish`
 }
 
@@ -202,4 +203,13 @@ if (tasks.findByName("androidMainClasses") != null) {
 }
 
 configureMppPublishing()
-configureBinaryValidators(setOf("jvm", "android").filterTargets())
+configureBinaryValidators(setOf("jvm", "android").filterTargets())
+configureRelocationForCore()
+
+//mavenCentralPublish {
+//    artifactId = "mirai-core"
+//    githubProject("mamoe", "mirai")
+//    developer("Mamoe Technologies", email = "support@mamoe.net", url = "https://github.com/mamoe")
+//    licenseFromGitHubProject("AGPLv3", "dev")
+//    publishPlatformArtifactsInRootModule = "jvm"
+//}

+ 1 - 0
mirai-deps-test/.gitignore

@@ -0,0 +1 @@
+src/BuildConfig.kt

+ 3 - 0
mirai-deps-test/README.md

@@ -0,0 +1,3 @@
+# native-deps-test
+
+测试 Native 

+ 100 - 0
mirai-deps-test/build.gradle.kts

@@ -0,0 +1,100 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+@file:Suppress("UnusedImport")
+
+plugins {
+    kotlin("jvm")
+    id("java-gradle-plugin")
+}
+
+dependencies {
+    implementation(gradleApi())
+    implementation(gradleKotlinDsl())
+    implementation(kotlin("gradle-plugin-api"))
+    implementation(kotlin("gradle-plugin"))
+    implementation(kotlin("stdlib"))
+
+    api("com.github.jengelman.gradle.plugins:shadow:6.0.0")
+    api(`jetbrains-annotations`)
+
+    testImplementation(kotlin("test-junit5"))
+    testImplementation(`junit-jupiter-api`)
+    testImplementation(`junit-jupiter-params`)
+    testRuntimeOnly(`junit-jupiter-engine`)
+}
+
+tasks.getByName("test", Test::class) {
+    environment("mirai.root.project.dir", rootProject.projectDir.absolutePath)
+}
+
+val publishMiraiArtifactsToMavenLocal by tasks.registering {
+    group = "mirai"
+    description = "Publish all mirai artifacts to MavenLocal"
+    val publishTasks = rootProject.allprojects.mapNotNull { proj ->
+        proj.tasks.findByName("publishToMavenLocal")
+    }
+    dependsOn(publishTasks)
+
+    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.register("generateBuildConfig") {
+    group = "mirai"
+
+    doLast {
+        val text = """
+            package net.mamoe.mirai.deps.test
+            
+            /**
+             * This file was generated by Gradle task `generateBuildConfig`.
+             */
+            object BuildConfig {
+                /**
+                 * Kotlin version used to compile mirai-core
+                 */
+                const val kotlinVersion = "${Versions.kotlinCompiler}"
+            }
+        """.trimIndent() + "\n"
+        val file = project.projectDir.resolve("src/BuildConfig.kt")
+        if (!file.exists() || file.readText() != text) {
+            file.writeText(text)
+        }
+    }
+    tasks.getByName("assemble").dependsOn(this) // if src is empty, compileKotlin will be skipped.
+    tasks.getByName("compileKotlin").dependsOn(this)
+    tasks.getByName("compileTestKotlin").dependsOn(this)
+}
+
+tasks.register("publishMiraiLocalArtifacts", Exec::class) {
+    group = "mirai"
+    description = "Starts a child process to publish v2.99.0-deps-test artifacts to MavenLocal"
+
+    workingDir(rootProject.projectDir)
+    environment("mirai.build.project.version", "2.99.0-deps-test")
+    commandLine(
+        "./gradlew",
+        publishMiraiArtifactsToMavenLocal.name,
+        "--no-daemon",
+        "-Pkotlin.compiler.execution.strategy=in-process"
+    )
+    standardOutput = System.out
+    errorOutput = System.err
+}
+
+
+version = Versions.core

+ 9 - 0
mirai-deps-test/gradle.properties

@@ -0,0 +1,9 @@
+#
+# Copyright 2019-2022 Mamoe Technologies and contributors.
+#
+# 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+# Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+#
+# https://github.com/mamoe/mirai/blob/dev/LICENSE
+#
+

+ 173 - 0
mirai-deps-test/test/AbstractTest.kt

@@ -0,0 +1,173 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.deps.test
+
+import org.gradle.api.internal.artifacts.mvnsettings.DefaultMavenFileLocations
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.extension.AfterEachCallback
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.junit.jupiter.api.io.TempDir
+import java.io.File
+
+// Copied from mirai-console-gradle
+abstract class AbstractTest {
+    companion object {
+        const val miraiLocalVersion = "2.99.0-deps-test" // do Search Everywhere before changing this
+        const val REASON_LOCAL_ARTIFACT_NOT_AVAILABLE = "local artifacts not available"
+
+        private val mavenLocalDir: File by lazy {
+            org.gradle.api.internal.artifacts.mvnsettings.DefaultLocalMavenRepositoryLocator(
+                org.gradle.api.internal.artifacts.mvnsettings.DefaultMavenSettingsProvider(DefaultMavenFileLocations())
+            ).localMavenRepository
+        }
+
+        @JvmStatic
+        fun isMiraiLocalAvailable(): Boolean {
+            return if (mavenLocalDir.resolve("net/mamoe/mirai-core/$miraiLocalVersion").exists()) {
+                println(
+                    """
+                [mirai-deps-test] Found local artifacts `$miraiLocalVersion`! 
+                Please note that you may need to manually update local artifacts if you have:
+                - 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`.
+            """.trimIndent()
+                )
+                true
+            } else {
+                System.err.println(
+                    """
+                [mirai-deps-test] ERROR: Test is not run, because there are no local artifacts available for dependencies testing. 
+                Please build and publish local artifacts with version `$miraiLocalVersion` before running this test(:mirai-deps-test:test).
+                This could have be automated but it will take a huge amount of time for your routine testing.
+                 
+                You can run this test manually if you have:
+                - added/removed a dependency for mirai-core series modules
+                - changed version of any of the dependencies for mirai-core series modules
+                
+                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. 
+                Then you can run this test again. (By your original way or ./gradlew :mirai-deps-test:test)
+                """.trimIndent()
+                )
+                false
+            }
+        }
+    }
+
+    @JvmField
+    @TempDir
+    var tempDirField: File? = null
+
+    val tempDir: File get() = tempDirField!!
+
+    val kotlinVersion = BuildConfig.kotlinVersion
+
+    lateinit var mainSrcDir: File
+    lateinit var commonMainSrcDir: File
+    lateinit var nativeMainSrcDir: File
+    lateinit var testDir: File
+    lateinit var buildFile: File
+    lateinit var settingsFile: File
+    lateinit var propertiesFile: File
+
+
+    @OptIn(ExperimentalStdlibApi::class)
+    fun runGradle(vararg arguments: String) {
+        System.gc()
+        GradleRunner.create()
+            .withProjectDir(tempDir)
+            .withPluginClasspath()
+            .withGradleVersion("7.2")
+            .forwardOutput()
+            .withEnvironment(System.getenv())
+            .withArguments(buildList {
+                addAll(arguments)
+                add("-Pkotlin.compiler.execution.strategy=in-process")
+                add("-Dorg.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8")
+                add("--stacktrace")
+            })
+            .build()
+    }
+
+    @BeforeEach
+    fun setup() {
+        println("Temp path is " + tempDir.absolutePath)
+
+        settingsFile = File(tempDir, "settings.gradle")
+        settingsFile.delete()
+        settingsFile.writeText(
+            """
+            pluginManagement {
+                repositories {
+                    gradlePluginPortal()
+                    mavenCentral()
+                    mavenLocal()
+                }
+            }
+        """
+        )
+
+        File(tempDir, "gradle.properties").apply {
+            delete()
+            writeText(
+                """
+                org.gradle.daemon=false
+                org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
+            """.trimIndent()
+            )
+        }
+        mainSrcDir = tempDir.resolve("src/main/kotlin").apply { mkdirs() }
+        commonMainSrcDir = tempDir.resolve("src/commonMain/kotlin").apply { mkdirs() }
+        nativeMainSrcDir = tempDir.resolve("src/nativeMain/kotlin").apply { mkdirs() }
+        testDir = tempDir.resolve("src/test/kotlin").apply { mkdirs() }
+
+        buildFile = tempDir.resolve("build.gradle.kts")
+        buildFile.writeText(
+            """
+            import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+            plugins {
+                kotlin("jvm") version "1.7.0"
+            }
+            group = "org.example"
+            version = "1.0-SNAPSHOT"
+            repositories {
+                mavenCentral()
+                mavenLocal()
+            }
+            dependencies {
+                testImplementation(kotlin("test"))
+            }
+            tasks.test {
+                useJUnitPlatform()
+            }
+            tasks.withType<KotlinCompile> {
+                kotlinOptions.jvmTarget = "1.8"
+            }
+        """.trimIndent() + "\n\n"
+        )
+    }
+
+    @JvmField
+    @RegisterExtension
+    internal val after: AfterEachCallback = AfterEachCallback { context ->
+        if (context.executionException.isPresent) {
+            val inst = context.requiredTestInstance as AbstractTest
+            println("====================== build.gradle ===========================")
+            println(inst.tempDir.resolve("build.gradle").readText())
+            println("==================== settings.gradle ==========================")
+            println(inst.tempDir.resolve("settings.gradle").readText())
+        }
+    }
+}

+ 167 - 0
mirai-deps-test/test/CoreDependencyResolutionTest.kt

@@ -0,0 +1,167 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.deps.test
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledIf
+
+class CoreDependencyResolutionTest : AbstractTest() {
+    @Test
+    @EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
+    fun `test resolve JVM root from Kotlin JVM`() {
+        mainSrcDir.resolve("main.kt").writeText(
+            """
+            package test
+            fun main () {
+                println(net.mamoe.mirai.BotFactory)
+            }
+        """.trimIndent()
+        )
+        buildFile.writeText(
+            """
+            plugins {
+                id("org.jetbrains.kotlin.jvm") version "$kotlinVersion"
+            }
+            repositories {
+                mavenCentral()
+                mavenLocal()
+            }
+            dependencies {
+                implementation("net.mamoe:mirai-core:$miraiLocalVersion")
+            }
+        """.trimIndent()
+        )
+        runGradle("build")
+    }
+
+    @Test
+    @EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
+    fun `test resolve JVM from Kotlin JVM`() {
+        mainSrcDir.resolve("main.kt").writeText(
+            """
+            package test
+            fun main () {
+                println(net.mamoe.mirai.BotFactory)
+            }
+        """.trimIndent()
+        )
+        buildFile.writeText(
+            """
+            plugins {
+                id("org.jetbrains.kotlin.jvm") version "$kotlinVersion"
+            }
+            repositories {
+                mavenCentral()
+                mavenLocal()
+            }
+            dependencies {
+                implementation("net.mamoe:mirai-core-jvm:$miraiLocalVersion")
+            }
+        """.trimIndent()
+        )
+        runGradle("build")
+    }
+
+    @Test
+    @EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
+    fun `test resolve JVM and Native from common`() {
+        commonMainSrcDir.resolve("main.kt").writeText(
+            """
+            package test
+            fun main () {
+                println(net.mamoe.mirai.BotFactory)
+            }
+        """.trimIndent()
+        )
+        buildFile.writeText(
+            """
+            |import org.apache.tools.ant.taskdefs.condition.Os
+            |import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
+            |
+            |plugins {
+            |    id("org.jetbrains.kotlin.multiplatform") version "$kotlinVersion"
+            |}
+            |repositories {
+            |    mavenCentral()
+            |    mavenLocal()
+            |}
+            |kotlin {
+            |    targets {
+            |        jvm()
+            |        val nativeMainSets = mutableListOf<KotlinSourceSet>()
+            |        val nativeTestSets = mutableListOf<KotlinSourceSet>()
+            |        when {
+            |            Os.isFamily(Os.FAMILY_MAC) -> if (Os.isArch("aarch64")) macosArm64("native") else macosX64("native")
+            |            Os.isFamily(Os.FAMILY_WINDOWS) -> mingwX64("native")
+            |            else -> linuxX64("native")
+            |        }
+            |    }
+            |    sourceSets {
+            |        val commonMain by getting {
+            |            dependencies {
+            |               api("net.mamoe:mirai-core:$miraiLocalVersion")
+            |            }
+            |        }
+            |    }
+            |}
+        """.trimMargin()
+        )
+
+        runGradle("build")
+    }
+
+    @Test
+    @EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
+    fun `test resolve Native from common`() {
+        nativeMainSrcDir.resolve("main.kt").writeText(
+            """
+            package test
+            fun main () {
+                println(net.mamoe.mirai.BotFactory)
+            }
+        """.trimIndent()
+        )
+        buildFile.writeText(
+            """
+            |import org.apache.tools.ant.taskdefs.condition.Os
+            |import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
+            |
+            |plugins {
+            |    id("org.jetbrains.kotlin.multiplatform") version "$kotlinVersion"
+            |}
+            |repositories {
+            |    mavenCentral()
+            |    mavenLocal()
+            |}
+            |kotlin {
+            |    targets {
+            |        jvm()
+            |        val nativeMainSets = mutableListOf<KotlinSourceSet>()
+            |        val nativeTestSets = mutableListOf<KotlinSourceSet>()
+            |        when {
+            |            Os.isFamily(Os.FAMILY_MAC) -> if (Os.isArch("aarch64")) macosArm64("native") else macosX64("native")
+            |            Os.isFamily(Os.FAMILY_WINDOWS) -> mingwX64("native")
+            |            else -> linuxX64("native")
+            |        }
+            |    }
+            |    sourceSets {
+            |        val nativeMain by getting {
+            |            dependencies {
+            |               api("net.mamoe:mirai-core:$miraiLocalVersion")
+            |            }
+            |        }
+            |    }
+            |}
+        """.trimMargin()
+        )
+
+        runGradle("build")
+    }
+}

+ 48 - 0
mirai-deps-test/test/CoreShadowRelocationTest.kt

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.deps.test
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledIf
+
+class CoreShadowRelocationTest : AbstractTest() {
+    @Test
+    @EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
+    fun `test OkHttp filtered out`() {
+        testDir.resolve("test.kt").writeText(
+            """
+            package test
+            import org.junit.jupiter.api.*
+            class MyTest {
+              @Test
+              fun `test base dependency`() {
+                assertThrows<ClassNotFoundException> {
+                  Class.forName("io.ktor.client.engine.okhttp.OkHttp")
+                }
+              }
+              @Test
+              fun `test transitive dependency`() {
+                assertThrows<ClassNotFoundException> {
+                  Class.forName("okhttp3.OkHttpClient")
+                }
+              }
+            }
+        """.trimIndent()
+        )
+        buildFile.appendText(
+            """
+            dependencies {
+                implementation("net.mamoe:mirai-core:$miraiLocalVersion")
+            }
+        """.trimIndent()
+        )
+        runGradle("check")
+    }
+}

+ 10 - 0
mirai-deps-test/test/package.kt

@@ -0,0 +1,10 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.deps.test

+ 76 - 60
settings.gradle.kts

@@ -1,15 +1,15 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
  *
- *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ * 此源代码的使用受 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/master/LICENSE
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
 pluginManagement {
     repositories {
-        if (System.getProperty("use.maven.local") == "true") {
+        if (System.getProperty("use.maven.local") == "true") { // you can enable by adding `systemProp.use.maven.local=true` in 'gradle.properties'.
             mavenLocal()
         }
         gradlePluginPortal()
@@ -18,24 +18,31 @@ pluginManagement {
     }
 }
 
-val allProjects = mutableListOf<ProjectDescriptor>()
 rootProject.name = "mirai"
 
+/**
+ * Projects included so far
+ */
+val allProjects = mutableListOf<ProjectDescriptor>()
+
 fun includeProject(projectPath: String, dir: String? = null) {
     include(projectPath)
     if (dir != null) project(projectPath).projectDir = file(dir)
     allProjects.add(project(projectPath))
 }
 
+fun includeConsoleProject(projectPath: String, dir: String? = null) =
+    includeProject(projectPath, "mirai-console/$dir")
+
+
 includeProject(":mirai-core-utils")
 includeProject(":mirai-core-api")
 includeProject(":mirai-core")
+
 includeProject(":mirai-core-all")
 includeProject(":mirai-bom")
 includeProject(":mirai-dokka")
-
-//includeProject(":binary-compatibility-validator")
-//includeProject(":binary-compatibility-validator-android", "binary-compatibility-validator/android")
+includeProject(":mirai-deps-test")
 
 includeProject(":mirai-logging-log4j2", "logging/mirai-logging-log4j2")
 includeProject(":mirai-logging-slf4j", "logging/mirai-logging-slf4j")
@@ -43,55 +50,50 @@ includeProject(":mirai-logging-slf4j-simple", "logging/mirai-logging-slf4j-simpl
 includeProject(":mirai-logging-slf4j-logback", "logging/mirai-logging-slf4j-logback")
 
 
-val disableOldFrontEnds = true
-
-fun includeConsoleProject(projectPath: String, dir: String? = null) =
-    includeProject(projectPath, "mirai-console/$dir")
-
 includeConsoleProject(":mirai-console-compiler-annotations", "tools/compiler-annotations")
 includeConsoleProject(":mirai-console", "backend/mirai-console")
 includeConsoleProject(":mirai-console.codegen", "backend/codegen")
 includeConsoleProject(":mirai-console-terminal", "frontend/mirai-console-terminal")
 
-// region mirai-console.integration-test
-includeConsoleProject(":mirai-console.integration-test", "backend/integration-test")
-
-val consoleIntegrationTestSubPluginBuildGradleKtsTemplate by lazy {
-    rootProject.projectDir
-        .resolve("mirai-console/backend/integration-test/testers")
-        .resolve("tester.template.gradle.kts")
-        .readText()
-}
-
-@Suppress("SimpleRedundantLet")
-fun includeConsoleITPlugin(prefix: String, path: File) {
-    path.resolve("build.gradle.kts").takeIf { !it.isFile }?.let { initScript ->
-        initScript.writeText(consoleIntegrationTestSubPluginBuildGradleKtsTemplate)
-    }
-
-    val projectPath = "$prefix${path.name}"
-    include(projectPath)
-    project(projectPath).projectDir = path
-    path.listFiles()?.asSequence().orEmpty()
-        .filter { it.isDirectory }
-        .filter { it.resolve(".nested-module.txt").exists() }
-        .forEach { includeConsoleITPlugin("${projectPath}:", it) }
-}
-rootProject.projectDir
-    .resolve("mirai-console/backend/integration-test/testers")
-    .listFiles()?.asSequence().orEmpty()
-    .filter { it.isDirectory }
-    .forEach { includeConsoleITPlugin(":mirai-console.integration-test:", it) }
-// endregion
+includeConsoleIntegrationTestProjects()
 
 includeConsoleProject(":mirai-console-compiler-common", "tools/compiler-common")
 includeConsoleProject(":mirai-console-intellij", "tools/intellij-plugin")
 includeConsoleProject(":mirai-console-gradle", "tools/gradle-plugin")
 
-@Suppress("ConstantConditionIf")
-if (!disableOldFrontEnds) {
-    includeConsoleProject(":mirai-console-terminal", "frontend/mirai-console-terminal")
+//includeConsoleFrontendGraphical()
+
+includeProject(":ci-release-helper")
+
+includeBinaryCompatibilityValidatorProjects()
+
+/**
+ * Configures a project `:validator:path-to-project:target-name` for binary compatibility validation.
+ *
+ * To enable validation for a project,
+ * create a subdirectory with name of the target under "compatibility-validation",
+ * then sync **twice**. See `:mirai-core-api` for an example.
+ *
+ * **Note**: This function depends on [allProjects], and should be used at the end.
+ */
+fun includeBinaryCompatibilityValidatorProjects() {
+    val result = mutableListOf<ProjectDescriptor>()
+    for (project in allProjects) {
+        val validationDir = project.projectDir.resolve("compatibility-validation")
+        if (!validationDir.exists()) continue
+        validationDir.listFiles().orEmpty<File>().forEach { dir ->
+            if (dir.resolve("build.gradle.kts").isFile) {
+                val path = ":validator" + project.path + ":${dir.name}"
+                include(path)
+                project(path).projectDir = dir
+//            project(path).name = "${project.name}-validator-${dir.name}"
+                result.add(project(path))
+            }
+        }
+    }
+}
 
+fun includeConsoleLegacyFrontendProjects() {
     println("JDK version: ${JavaVersion.current()}")
 
     if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
@@ -101,20 +103,34 @@ if (!disableOldFrontEnds) {
     }
 }
 
-includeProject(":ci-release-helper")
+fun includeConsoleIntegrationTestProjects() {
+    includeConsoleProject(":mirai-console.integration-test", "backend/integration-test")
 
+    val consoleIntegrationTestSubPluginBuildGradleKtsTemplate by lazy {
+        rootProject.projectDir
+            .resolve("mirai-console/backend/integration-test/testers")
+            .resolve("tester.template.gradle.kts")
+            .readText()
+    }
 
-val result = mutableListOf<ProjectDescriptor>()
-for (project in allProjects) {
-    val validationDir = project.projectDir.resolve("compatibility-validation")
-    if (!validationDir.exists()) continue
-    validationDir.listFiles().orEmpty<File>().forEach { dir ->
-        if (dir.resolve("build.gradle.kts").isFile) {
-            val path = ":validator" + project.path + ":${dir.name}"
-            include(path)
-            project(path).projectDir = dir
-//            project(path).name = "${project.name}-validator-${dir.name}"
-            result.add(project(path))
+    @Suppress("SimpleRedundantLet")
+    fun includeConsoleITPlugin(prefix: String, path: File) {
+        path.resolve("build.gradle.kts").takeIf { !it.isFile }?.let { initScript ->
+            initScript.writeText(consoleIntegrationTestSubPluginBuildGradleKtsTemplate)
         }
+
+        val projectPath = "$prefix${path.name}"
+        include(projectPath)
+        project(projectPath).projectDir = path
+        path.listFiles()?.asSequence().orEmpty()
+            .filter { it.isDirectory }
+            .filter { it.resolve(".nested-module.txt").exists() }
+            .forEach { includeConsoleITPlugin("${projectPath}:", it) }
     }
-}
+
+    rootProject.projectDir
+        .resolve("mirai-console/backend/integration-test/testers")
+        .listFiles()?.asSequence().orEmpty()
+        .filter { it.isDirectory }
+        .forEach { includeConsoleITPlugin(":mirai-console.integration-test:", it) }
+}