[build] Rewrite shadow relocation

This commit is contained in:
Him188 2022-11-02 19:28:04 +00:00
parent ca840f88be
commit c0ccdbe9d3
17 changed files with 699 additions and 344 deletions

View File

@ -9,7 +9,6 @@
@file:Suppress("UnstableApiUsage", "UNUSED_VARIABLE", "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.DokkaBaseConfiguration
import java.time.LocalDateTime
@ -86,13 +85,6 @@ allprojects {
}
configureJarManifest()
substituteDependenciesUsingExpectedVersion()
if (System.getenv("MIRAI_IS_SNAPSHOTS_PUBLISHING") != null) {
project.tasks.filterIsInstance<ShadowJar>().forEach { shadow ->
shadow.enabled = false // they are too big
}
logger.info("Disabled all shadow tasks.")
}
}
}

View File

@ -23,6 +23,7 @@ kotlin {
sourceSets.all {
languageSettings.optIn("kotlin.Experimental")
languageSettings.optIn("kotlin.RequiresOptIn")
languageSettings.optIn("kotlin.ExperimentalStdlibApi")
}
}

View File

@ -14,6 +14,7 @@ import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.getting
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.kpm.external.project
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
@ -29,6 +30,13 @@ val MIRAI_PLATFORM_ATTRIBUTE = Attribute.of(
"net.mamoe.mirai.platform", String::class.java
)
/**
* Flags a target as an HMPP intermediate target
*/
val MIRAI_PLATFORM_INTERMEDIATE = Attribute.of(
"net.mamoe.mirai.platform.intermediate", Boolean::class.javaObjectType
)
val IDEA_ACTIVE = System.getProperty("idea.active") == "true" && System.getProperty("publication.test") != "true"
val OS_NAME = System.getProperty("os.name").toLowerCase()
@ -150,6 +158,7 @@ fun Project.configureJvmTargetsHierarchical() {
}
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.common) // magic
attributes.attribute(MIRAI_PLATFORM_ATTRIBUTE, "jvmBase") // avoid resolution
attributes.attribute(MIRAI_PLATFORM_INTERMEDIATE, true)
}
}

View File

@ -76,12 +76,13 @@ inline fun Project.configurePublishing(
addProjectComponents: Boolean = true,
setupGpg: Boolean = true,
skipPublicationSetup: Boolean = false,
addShadowJar: Boolean = true
) {
configureRemoteRepos()
if (skipPublicationSetup) return
val shadowJar = if (!addProjectComponents) null else tasks.register<ShadowJar>("shadowJar") {
val shadowJar = if (!addProjectComponents || !addShadowJar) null else tasks.register<ShadowJar>("shadowJar") {
archiveClassifier.set("all")
manifest.inheritFrom(tasks.getByName<Jar>("jar").manifest)
from(project.sourceSets["main"].output)

View File

@ -23,6 +23,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
@ -41,6 +42,20 @@ private fun Project.jvmVersion(): JavaVersion {
}
}
fun Project.optInForAllTargets(qualifiedClassname: String) {
tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinCompile::class) {
kotlinOptions.freeCompilerArgs += "-Xopt-in=$qualifiedClassname"
}
}
fun Project.enableLanguageFeatureForAllSourceSets(qualifiedClassname: String) {
kotlinSourceSets!!.all {
languageSettings {
this.enableLanguageFeature(qualifiedClassname)
}
}
}
fun Project.preConfigureJvmTarget() {
val defaultVer = jvmVersion()

View File

@ -7,13 +7,16 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
import org.gradle.api.Action
import org.gradle.api.DomainObjectCollection
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.kotlin.dsl.accessors.runtime.addDependencyTo
import org.gradle.kotlin.dsl.extra
import org.gradle.kotlin.dsl.invoke
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import java.io.File
/**
@ -34,7 +37,7 @@ import java.io.File
*
* Relocation 是模块范围的. 可以为 group id `io.ktor` 下的所有模块执行 relocation, 也可以仅为某一个精确的模块比如 `io.ktor:ktor-client-core` 执行.
*
* 要增加一条 relocation 规则, 使用 [relocateAllFromGroupId] 或者 [relocateExactArtifact]. 不要现在就过去用, 你必须先读完本文全部.
* 要增加一条 relocation 规则, 使用 [relocateAllFromGroupId] 或者 [addRelocationRuntime]. 不要现在就过去用, 你必须先读完本文全部.
*
* ## 间接依赖不会被处理
*
@ -81,61 +84,6 @@ import java.io.File
*/
object RelocationNotes
/**
* 配置 Ktor 依赖.
* @see RelocationNotes
* @see relocateKtorForCore
*/
fun NamedDomainObjectContainer<KotlinSourceSet>.configureMultiplatformKtorDependencies(addDep: KotlinDependencyHandler.(Any) -> Dependency?) {
getByName("commonMain").apply {
dependencies {
addDep(`ktor-io`)
addDep(`ktor-client-core`)
}
}
findByName("jvmBaseMain")?.apply {
dependencies {
addDep(`ktor-client-okhttp`)
}
}
configure(WIN_TARGETS.map { getByName(it + "Main") }) {
dependencies {
addDep(`ktor-client-curl`)
}
}
configure(LINUX_TARGETS.map { getByName(it + "Main") }) {
dependencies {
addDep(`ktor-client-cio`)
}
}
findByName("darwinMain")?.apply {
dependencies {
addDep(`ktor-client-darwin`)
}
}
}
inline fun <T> configure(list: Iterable<T>, function: T.() -> Unit) {
list.forEach(function)
}
/**
* 使用之前阅读 [RelocationNotes]
*/
fun Project.relocateKtorForCore(includeInRuntime: Boolean) {
// WARNING: You must also consider relocating transitive dependencies.
// Otherwise, user will get NoClassDefFound error when using mirai as a classpath dependency. See #2263.
relocateAllFromGroupId("io.ktor", includeInRuntime)
relocateAllFromGroupId("com.squareup.okhttp3", includeInRuntime)
relocateAllFromGroupId("com.squareup.okio", includeInRuntime)
}
/**
* relocate 一个 [groupId] 下的所有模块.
*
@ -148,23 +96,133 @@ fun Project.relocateKtorForCore(includeInRuntime: Boolean) {
*
* @param groupId 例如 `io.ktor`
* @param includeInRuntime relocate 后的依赖本体包含在运行时 classpath.
* @param packages relocate 的模块的全部顶层包. `com.squareup.okhttp3:okhttp` 的顶层包是 `okhttp3`
*/
fun Project.relocateAllFromGroupId(groupId: String, includeInRuntime: Boolean) {
relocationFilters.add(RelocationFilter(groupId, includeInRuntime = includeInRuntime))
fun Project.relocateAllFromGroupId(
groupId: String,
includeInRuntime: Boolean,
packages: List<String> = listOf(groupId),
) {
relocationFilters.add(
RelocationFilter(
groupId,
packages = packages,
includeInRuntime = includeInRuntime
)
)
}
fun Project.relocateAllFromGroupId(
groupId: String,
includeInRuntime: Boolean,
vararg packages: String,
) = relocateAllFromGroupId(groupId, includeInRuntime, packages.toList())
fun KotlinDependencyHandler.relocateCompileOnly(
relocatedDependency: RelocatedDependency,
action: ExternalModuleDependency.() -> Unit = {}
): ExternalModuleDependency {
val dependency = compileOnly(relocatedDependency.notation, action)
project.relocationFilters.add(
RelocationFilter(
dependency.group!!, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
)
)
// Don't add to runtime
return dependency
}
fun DependencyHandler.relocateCompileOnly(
project: Project,
relocatedDependency: RelocatedDependency,
action: Action<ExternalModuleDependency> = Action {}
): Dependency {
val dependency = addDependencyTo(this, "compileOnly", relocatedDependency.notation, action)
project.relocationFilters.add(
RelocationFilter(
dependency.group!!, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
)
)
// Don't add to runtime
return dependency
}
/**
* 精确地 relocate 一个依赖.
* 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
*
* 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用都会被 relocate [RELOCATION_ROOT_PACKAGE].
* 运行时 (runtime) 将会包含被 relocate 的依赖及其所有间接依赖.
*
* @see configureRelocationForTarget
*/
fun Project.relocateExactArtifact(groupId: String, artifactId: String, includeInRuntime: Boolean) {
relocationFilters.add(RelocationFilter(groupId, artifactId, includeInRuntime = includeInRuntime))
fun KotlinDependencyHandler.relocateImplementation(
relocatedDependency: RelocatedDependency,
action: ExternalModuleDependency.() -> Unit = {}
): ExternalModuleDependency {
val dependency = implementation(relocatedDependency.notation) {
}
project.relocationFilters.add(
RelocationFilter(
dependency.group!!, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
)
)
project.configurations.maybeCreate(SHADOW_RELOCATION_CONFIGURATION_NAME)
addDependencyTo(
project.dependencies,
SHADOW_RELOCATION_CONFIGURATION_NAME,
relocatedDependency.notation,
Action<ExternalModuleDependency> {
relocatedDependency.exclusionAction(this)
exclude(ExcludeProperties.`everything from kotlin`)
exclude(ExcludeProperties.`everything from kotlinx`)
action()
}
)
return dependency
}
fun DependencyHandler.relocateImplementation(
project: Project,
relocatedDependency: RelocatedDependency,
action: Action<ExternalModuleDependency> = Action {}
): ExternalModuleDependency {
val dependency =
addDependencyTo(this, "implementation", relocatedDependency.notation, Action<ExternalModuleDependency> {
relocatedDependency.exclusionAction(this)
exclude(ExcludeProperties.`everything from kotlin`)
exclude(ExcludeProperties.`everything from kotlinx`)
action.execute(this)
})
project.relocationFilters.add(
RelocationFilter(
dependency.group!!, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
)
)
project.configurations.maybeCreate(SHADOW_RELOCATION_CONFIGURATION_NAME)
addDependencyTo(
project.dependencies,
SHADOW_RELOCATION_CONFIGURATION_NAME,
relocatedDependency.notation,
Action<ExternalModuleDependency> {
relocatedDependency.exclusionAction(this)
exclude(ExcludeProperties.`everything from kotlin`)
exclude(ExcludeProperties.`everything from kotlinx`)
action(this)
}
)
return dependency
}
const val SHADOW_RELOCATION_CONFIGURATION_NAME = "shadowRelocation"
data class RelocationFilter(
val groupId: String,
val artifactId: String? = null,
val shadowFilter: String = groupId,
val packages: List<String> = listOf(groupId),
val filesFilter: String = groupId.replace(".", "/"),
/**
* Pack relocated dependency into the fat jar. If set to `false`, dependencies will be removed.

View File

@ -10,13 +10,13 @@
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.publish.tasks.GenerateModuleMetadata
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.creating
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.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
/**
@ -27,13 +27,12 @@ fun Project.configureMppShadow() {
configure(kotlin.targets.filter {
it.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.jvm
&& it.attributes.getAttribute(MIRAI_PLATFORM_ATTRIBUTE) == null
&& (it.attributes.getAttribute(MIRAI_PLATFORM_INTERMEDIATE) != true)
}) {
configureRelocationForTarget(project)
}
// regular shadow file, with suffix `-all`
configureRegularShadowJar(kotlin)
registerRegularShadowTask(this, mapTaskNameForMultipleTargets = true)
}
}
/**
@ -41,119 +40,47 @@ fun Project.configureMppShadow() {
* @see RelocationNotes
*/
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()}")
val configuration = project.configurations.findByName(SHADOW_RELOCATION_CONFIGURATION_NAME)
dependsOn(compilations["main"].compileKotlinTask) // compileKotlinJvm
// e.g. relocateJvmDependencies
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.toLowerCase()}") // e.g. "mirai-core-api-jvm"
// 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)
// }
dependsOn(compilations["main"].compileKotlinTask) // e.g. compileKotlinJvm
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 ->
val matchingFilter = relocationFilters.find { 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)
}
if (matchingFilter != null) {
fileFiltered = true
println("Including file: ${file.absolutePath}")
}
matchingFilter?.includeInRuntime == true
}
)
check(fileFiltered) { "[Shadow Relocation] Expected at least one file filtered for target $targetName. Filters: $relocationFilters" }
}
from(compilations["main"].output) // Add compilation result of mirai sourcecode, not including dependencies
configuration?.let {
from(it) // Include runtime dependencies
}
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" }
}
// Relocate packages
afterEvaluate {
val relocationFilters = project.relocationFilters
relocationFilters.forEach { relocation ->
relocation.packages.forEach { aPackage ->
relocate(aPackage, "$RELOCATION_ROOT_PACKAGE.$aPackage")
}
}
}
}
// Relocate before packing Jar. Projects that depends on this project actually (explicitly or implicitly) depends on the Jar.
tasks.getByName("${targetName}Jar").dependsOn(relocateDependencies)
// We will modify Kotlin metadata, so do generate metadata before relocation
val generateMetadataTask =
tasks.getByName("generateMetadataFileFor${targetName.capitalize()}Publication") as GenerateModuleMetadata
val patchMetadataTask = tasks.create("patchMetadataFileFor${targetName.capitalize()}RelocatedPublication") {
dependsOn(generateMetadataTask)
// remove dependencies in Kotlin module metadata
relocateDependencies.doLast {
doLast {
// mirai-core-jvm-2.13.0.module
val file = metadataTask.outputFile.asFile.get()
val file = generateMetadataTask.outputFile.asFile.get()
val metadata = Gson().fromJson(
file.readText(),
com.google.gson.JsonElement::class.java
@ -185,48 +112,109 @@ private fun KotlinTarget.configureRelocationForTarget(project: Project) = projec
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)
// 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) }
}
setRelocations()
from(project.configurations.findByName("jvmRuntimeClasspath"))
this.exclude { file ->
file.name.endsWith(".sf", ignoreCase = true)
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) }
}
}
/*
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()
)
}*/
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 const val relocationRootPackage = "net.mamoe.mirai.internal.deps"
private fun Sequence<Task>.dependsOn(
relocateDependencies: ShadowJar,
kotlinTarget: KotlinTarget,
): Int {
return onEach { relocateDependencies.dependsOn(it) }
.count().also {
check(it > 0) { "[Shadow Relocation] Expected at least one task task matched for target ${kotlinTarget.targetName}." }
}
}
private fun ShadowJar.setRelocations() {
project.relocationFilters.forEach { relocation ->
relocate(relocation.shadowFilter, "$relocationRootPackage.${relocation.groupId}")
private fun Sequence<Task>.mustRunAfter(
relocateDependencies: ShadowJar,
kotlinTarget: KotlinTarget,
): Int {
return onEach { relocateDependencies.mustRunAfter(it) }
.count().also {
check(it > 0) { "[Shadow Relocation] Expected at least one task task matched for target ${kotlinTarget.targetName}." }
}
}
private fun Project.registerRegularShadowTask(target: KotlinTarget, mapTaskNameForMultipleTargets: Boolean): ShadowJar {
return tasks.create(
if (mapTaskNameForMultipleTargets) "shadow${target.targetName}Jar" else "shadowJar",
ShadowJar::class
) {
group = "mirai"
archiveClassifier.set("all")
val compilation = target.compilations["main"]
dependsOn(compilation.compileKotlinTask)
from(compilation.output)
// components.findByName("java")?.let { from(it) }
project.sourceSets.findByName("main")?.output?.let { from(it) } // for JVM projects
configurations =
listOfNotNull(
project.configurations.findByName("runtimeClasspath"),
project.configurations.findByName("${target.targetName}RuntimeClasspath"),
project.configurations.findByName("runtime")
)
// 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)
}
}
}
}
fun Project.configureRelocatedShadowJarForJvmProject(kotlin: KotlinJvmProjectExtension): ShadowJar {
return registerRegularShadowTask(kotlin.target, mapTaskNameForMultipleTargets = false)
}
const val RELOCATION_ROOT_PACKAGE = "net.mamoe.mirai.internal.deps"

View File

@ -7,8 +7,12 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:Suppress("ObjectPropertyName", "ObjectPropertyName", "unused", "MemberVisibilityCanBePrivate")
@file:Suppress(
"ObjectPropertyName", "ObjectPropertyName", "unused", "MemberVisibilityCanBePrivate", "RemoveRedundantBackticks"
)
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.attributes.Attribute
import org.gradle.kotlin.dsl.exclude
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
@ -19,7 +23,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
object Versions {
val project = System.getenv("mirai.build.project.version")?.takeIf { it.isNotBlank() }
?: System.getProperty("mirai.build.project.version")?.takeIf { it.isNotBlank() }
?: /*PROJECT_VERSION_START*/"2.14.0"/*PROJECT_VERSION_END*/
?: /*PROJECT_VERSION_START*/"2.14.0-dev-shadow-1"/*PROJECT_VERSION_END*/
val core = project
val console = project
@ -40,6 +44,8 @@ object Versions {
* 注意, 不要轻易升级 ktor 版本. 阅读 [RelocationNotes], 尤其是间接依赖部分.
*/
const val ktor = "2.1.0"
const val okhttp = "4.9.3" // 需要跟 Ktor 依赖的相同, 用于 shadow 后携带到 runtime
const val okio = "3.2.0" // 需要跟 OkHttp 依赖的相同, 用于 shadow 后携带到 runtime
const val binaryValidator = "0.4.0"
@ -93,7 +99,17 @@ val `kotlinx-serialization-json` = kotlinx("serialization-json", Versions.serial
val `kotlinx-serialization-protobuf` = kotlinx("serialization-protobuf", Versions.serialization)
const val `kotlinx-atomicfu` = "org.jetbrains.kotlinx:atomicfu:${Versions.atomicFU}"
val `ktor-io` = ktor("io", Versions.ktor)
/**
* @see relocateImplementation
*/
class RelocatedDependency(
val notation: String,
vararg val packages: String,
/**
* Additional exclusions apart from everything from `org.jetbrains.kotlin` and `org.jetbrains.kotlinx`.
*/
val exclusionAction: ExternalModuleDependency.() -> Unit = {},
)
fun KotlinDependencyHandler.implementationKotlinxIo(module: String) {
implementation(module) {
@ -111,14 +127,83 @@ fun KotlinDependencyHandler.implementationKotlinxIo(module: String) {
}
}
class MultiplatformDependency private constructor(
private val groupId: String,
private val baseArtifactId: String,
vararg val targets: String,
) {
fun notations(): Sequence<Map<String, String>> {
return sequenceOf(mapOf("group" to groupId, "module" to baseArtifactId))
.plus(targets.asSequence().map { mapOf("group" to groupId, "module" to "$baseArtifactId.$it") })
}
companion object {
fun jvm(groupId: String, baseArtifactId: String): MultiplatformDependency {
return MultiplatformDependency(groupId, baseArtifactId, "common", "metadata", "jvm", "jdk8", "jdk7")
}
}
}
fun ModuleDependency.exclude(multiplatformDependency: MultiplatformDependency) {
multiplatformDependency.notations().forEach {
exclude(it)
}
}
object ExcludeProperties {
val `everything from kotlin` = exclude(groupId = "org.jetbrains.kotlin", null)
val `everything from kotlinx` = exclude(groupId = "org.jetbrains.kotlinx", null)
val `kotlin-stdlib` = multiplatformJvm(groupId = "org.jetbrains.kotlin", "kotlin-stdlib")
val `kotlinx-coroutines` = multiplatformJvm(groupId = "org.jetbrains.kotlinx", "kotlinx-coroutines")
val `ktor-io` = multiplatformJvm(groupId = "io.ktor", "ktor-io")
val `everything from slf4j` = exclude(groupId = "org.slf4j", null)
/**
* @see org.gradle.kotlin.dsl.exclude
*/
@OptIn(ExperimentalStdlibApi::class)
private fun exclude(
groupId: String?, artifactId: String?
): Map<String, String> = buildMap {
groupId?.let { put("group", groupId) }
artifactId?.let { put("module", artifactId) }
}
private fun multiplatformJvm(
groupId: String, baseArtifactId: String
): MultiplatformDependency = MultiplatformDependency.jvm(groupId, baseArtifactId)
}
val `ktor-io` = ktor("io", Versions.ktor)
val `ktor-io_relocated` = RelocatedDependency(`ktor-io`, "io.ktor.utils.io") {
exclude(ExcludeProperties.`everything from slf4j`)
}
val `ktor-http` = ktor("http", Versions.ktor)
val `ktor-events` = ktor("events", Versions.ktor)
val `ktor-serialization` = ktor("serialization", Versions.ktor)
val `ktor-websocket-serialization` = ktor("websocket-serialization", Versions.ktor)
val `ktor-client-core` = ktor("client-core", Versions.ktor)
val `ktor-client-core_relocated` = RelocatedDependency(`ktor-client-core`, "io.ktor") {
exclude(ExcludeProperties.`ktor-io`)
exclude(ExcludeProperties.`everything from slf4j`)
}
val `ktor-client-cio` = ktor("client-cio", Versions.ktor)
val `ktor-client-mock` = ktor("client-mock", Versions.ktor)
val `ktor-client-curl` = ktor("client-curl", Versions.ktor)
val `ktor-client-darwin` = ktor("client-darwin", Versions.ktor)
val `ktor-client-okhttp` = ktor("client-okhttp", Versions.ktor)
val `ktor-client-okhttp_relocated` =
RelocatedDependency(ktor("client-okhttp", Versions.ktor), "io.ktor", "okhttp", "okio") {
exclude(ExcludeProperties.`ktor-io`)
exclude(ExcludeProperties.`everything from slf4j`)
}
const val `okhttp3` = "com.squareup.okhttp3:okhttp:${Versions.okhttp}"
const val `okio` = "com.squareup.okio:okio-jvm:${Versions.okio}"
val `ktor-client-android` = ktor("client-android", Versions.ktor)
val `ktor-client-logging` = ktor("client-logging", Versions.ktor)
val `ktor-network` = ktor("network-jvm", Versions.ktor)
@ -144,6 +229,7 @@ val ATTRIBUTE_MIRAI_TARGET_PLATFORM: Attribute<String> = Attribute.of("mirai.tar
const val `kotlin-compiler` = "org.jetbrains.kotlin:kotlin-compiler:${Versions.kotlinCompiler}"
const val `kotlin-compiler_forIdea` = "org.jetbrains.kotlin:kotlin-compiler:${Versions.kotlinCompilerForIdeaPlugin}"
const val `kotlin-stdlib` = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlinStdlib}"
const val `kotlin-stdlib-jdk8` = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlinStdlib}"
const val `kotlin-reflect` = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinStdlib}"
const val `kotlin-test` = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlinStdlib}"

View File

@ -23,10 +23,34 @@ dependencies {
api(project(":mirai-core"))
api(project(":mirai-core-api"))
api(project(":mirai-core-utils"))
relocateImplementation(project, `ktor-client-core_relocated`)
relocateImplementation(project, `ktor-client-okhttp_relocated`)
relocateImplementation(project, `ktor-io_relocated`)
}
val shadow = configureRelocatedShadowJarForJvmProject(kotlin)
if (System.getenv("MIRAI_IS_SNAPSHOTS_PUBLISHING")?.toBoolean() != true) {
configurePublishing("mirai-core-all")
// Do not publish -all jars to snapshot server since they are too large.
configurePublishing("mirai-core-all", addShadowJar = false)
publications {
getByName("mavenJava", MavenPublication::class) {
artifact(shadow)
}
}
tasks.getByName("publishMavenJavaPublicationToMavenLocal").dependsOn(shadow)
tasks.findByName("publishMavenJavaPublicationToMavenCentralRepository")?.dependsOn(shadow)
}
relocateKtorForCore(true)
//
//// WARNING: You must also consider relocating transitive dependencies.
//// Otherwise, user will get NoClassDefFound error when using mirai as a classpath dependency. See #2263.
//
//val includeInRuntime = true
//relocateAllFromGroupId("io.ktor", includeInRuntime, "io.ktor")
//relocateAllFromGroupId("com.squareup.okhttp3", includeInRuntime, listOf("okhttp3"))
//relocateAllFromGroupId("com.squareup.okio", includeInRuntime, listOf("okio"))

View File

@ -43,7 +43,7 @@ kotlin {
implementation(project(":mirai-core-utils"))
implementation(project(":mirai-console-compiler-annotations"))
implementation(`kotlinx-serialization-protobuf`)
implementation(`ktor-io`)
relocateCompileOnly(`ktor-io_relocated`) // runtime from mirai-core-utils
}
}
@ -103,7 +103,6 @@ if (tasks.findByName("androidMainClasses") != null) {
configureMppPublishing()
configureBinaryValidators(setOf("jvm", "android").filterTargets())
relocateKtorForCore(false)
//mavenCentralPublish {
// artifactId = "mirai-core-api"

View File

@ -36,7 +36,10 @@ kotlin {
api(`kotlinx-coroutines-core`)
implementation(`kotlinx-serialization-protobuf`)
implementation(`ktor-io`)
relocateImplementation(`ktor-io_relocated`) {
exclude(ExcludeProperties.`kotlin-stdlib`)
exclude(ExcludeProperties.`kotlinx-coroutines`)
}
}
}
@ -54,7 +57,6 @@ kotlin {
}
findByName("androidMain")?.apply {
//
dependencies {
compileOnly(`android-runtime`)
// api1(`ktor-client-android`)
@ -95,7 +97,6 @@ if (tasks.findByName("androidMainClasses") != null) {
}
configureMppPublishing()
relocateKtorForCore(true)
//mavenCentralPublish {
// artifactId = "mirai-core-utils"

View File

@ -42,8 +42,10 @@ kotlin {
implementation(project(":mirai-core-utils"))
implementation(`kotlinx-serialization-protobuf`)
implementation(`ktor-io`)
implementation(`ktor-client-core`)
// relocateImplementation(`ktor-http_relocated`)
// relocateImplementation(`ktor-serialization_relocated`)
// relocateImplementation(`ktor-websocket-serialization_relocated`)
}
}
@ -59,7 +61,6 @@ kotlin {
dependencies {
implementation(`log4j-api`)
implementation(`netty-handler`)
implementation(`ktor-client-okhttp`)
api(`kotlinx-coroutines-jdk8`) // use -jvm modules for this magic target 'jvmBase'
}
}
@ -105,6 +106,38 @@ kotlin {
}
}
// Ktor
findByName("commonMain")?.apply {
dependencies {
relocateCompileOnly(`ktor-io_relocated`) // runtime from mirai-core-utils
relocateImplementation(`ktor-client-core_relocated`)
}
}
findByName("jvmBaseMain")?.apply {
dependencies {
relocateImplementation(`ktor-client-okhttp_relocated`)
}
}
configure(WIN_TARGETS.map { getByName(it + "Main") }) {
dependencies {
implementation(`ktor-client-curl`)
}
}
configure(LINUX_TARGETS.map { getByName(it + "Main") }) {
dependencies {
implementation(`ktor-client-cio`)
}
}
findByName("darwinMain")?.apply {
dependencies {
implementation(`ktor-client-darwin`)
}
}
// Linkage
NATIVE_TARGETS.forEach { targetName ->
val defFile = projectDir.resolve("src/nativeMain/cinterop/OpenSSL.def")
val target = targets.getByName(targetName) as KotlinNativeTarget
@ -136,24 +169,6 @@ kotlin {
}
}
configure(WIN_TARGETS.map { getByName(it + "Main") }) {
dependencies {
implementation(`ktor-client-curl`)
}
}
configure(LINUX_TARGETS.map { getByName(it + "Main") }) {
dependencies {
implementation(`ktor-client-cio`)
}
}
findByName("darwinMain")?.apply {
dependencies {
implementation(`ktor-client-darwin`)
}
}
disableCrossCompile()
// val unixMain by getting {
// dependencies {
@ -202,7 +217,6 @@ if (tasks.findByName("androidMainClasses") != null) {
configureMppPublishing()
configureBinaryValidators(setOf("jvm", "android").filterTargets())
relocateKtorForCore(false)
//mavenCentralPublish {
// artifactId = "mirai-core"

View File

@ -72,6 +72,13 @@ internal fun getMiraiImpl() = Mirai as MiraiImpl
internal expect fun createDefaultHttpClient(): HttpClient
// used by `net.mamoe.mirai.deps.test.CoreDependencyResolutionTest` in mirai-deps-test module. Do not change signature.
@Suppress("unused")
@TestOnly
internal fun testHttpClient() {
createDefaultHttpClient().close()
}
@Suppress("FunctionName")
internal expect fun _MiraiImpl_static_init()

View File

@ -95,6 +95,7 @@ tasks.register("publishMiraiLocalArtifacts", Exec::class) {
"./gradlew",
publishMiraiArtifactsToMavenLocal.name,
"--no-daemon",
"--stacktrace",
"-Pkotlin.compiler.execution.strategy=in-process"
)
standardOutput = System.out

View File

@ -23,7 +23,7 @@ abstract class AbstractTest {
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 {
val mavenLocalDir: File by lazy {
org.gradle.api.internal.artifacts.mvnsettings.DefaultLocalMavenRepositoryLocator(
org.gradle.api.internal.artifacts.mvnsettings.DefaultMavenSettingsProvider(DefaultMavenFileLocations())
).localMavenRepository
@ -165,9 +165,20 @@ abstract class AbstractTest {
if (context.executionException.isPresent) {
val inst = context.requiredTestInstance as AbstractTest
println("====================== build.gradle ===========================")
println(inst.tempDir.resolve("build.gradle").readText())
println(inst.tempDir.resolveFirstExisting("build.gradle", "build.gradle.kts").readTextIfFound())
println("==================== settings.gradle ==========================")
println(inst.tempDir.resolve("settings.gradle").readText())
println(inst.tempDir.resolveFirstExisting("settings.gradle", "settings.gradle.kts").readTextIfFound())
}
}
private fun File.resolveFirstExisting(vararg files: String): File? {
return files.asSequence().map { resolve(it) }.firstOrNull { it.exists() }
}
private fun File?.readTextIfFound(): String =
when {
this == null -> "(not found)"
exists() -> readText()
else -> "($name not found)"
}
}

View File

@ -13,17 +13,20 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIf
class CoreDependencyResolutionTest : AbstractTest() {
private val testCode = """
package test
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "EXPERIMENTAL_API_USAGE")
fun main () {
println(net.mamoe.mirai.BotFactory)
println(net.mamoe.mirai.Mirai)
println(net.mamoe.mirai.internal.testHttpClient())
}
""".trimIndent()
@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()
)
mainSrcDir.resolve("main.kt").writeText(testCode)
buildFile.writeText(
"""
plugins {
@ -36,6 +39,9 @@ class CoreDependencyResolutionTest : AbstractTest() {
dependencies {
implementation("net.mamoe:mirai-core:$miraiLocalVersion")
}
kotlin.sourceSets.all {
languageSettings.optIn("net.mamoe.mirai.utils.TestOnly")
}
""".trimIndent()
)
runGradle("build")
@ -44,14 +50,7 @@ class CoreDependencyResolutionTest : AbstractTest() {
@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()
)
mainSrcDir.resolve("main.kt").writeText(testCode)
buildFile.writeText(
"""
plugins {
@ -64,6 +63,9 @@ class CoreDependencyResolutionTest : AbstractTest() {
dependencies {
implementation("net.mamoe:mirai-core-jvm:$miraiLocalVersion")
}
kotlin.sourceSets.all {
languageSettings.optIn("net.mamoe.mirai.utils.TestOnly")
}
""".trimIndent()
)
runGradle("build")
@ -72,14 +74,7 @@ class CoreDependencyResolutionTest : AbstractTest() {
@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()
)
commonMainSrcDir.resolve("main.kt").writeText(testCode)
buildFile.writeText(
"""
|import org.apache.tools.ant.taskdefs.condition.Os
@ -111,6 +106,9 @@ class CoreDependencyResolutionTest : AbstractTest() {
| }
| }
|}
|kotlin.sourceSets.all {
| languageSettings.optIn("net.mamoe.mirai.utils.TestOnly")
|}
""".trimMargin()
)
@ -120,14 +118,7 @@ class CoreDependencyResolutionTest : AbstractTest() {
@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()
)
nativeMainSrcDir.resolve("main.kt").writeText(testCode)
buildFile.writeText(
"""
|import org.apache.tools.ant.taskdefs.condition.Os
@ -159,6 +150,9 @@ class CoreDependencyResolutionTest : AbstractTest() {
| }
| }
|}
|kotlin.sourceSets.all {
| languageSettings.optIn("net.mamoe.mirai.utils.TestOnly")
|}
""".trimMargin()
)

View File

@ -11,31 +11,77 @@ package net.mamoe.mirai.deps.test
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIf
import kotlin.test.assertTrue
/**
* 为每个模块测试 relocated 的依赖是否存在于运行时
*/
class CoreShadowRelocationTest : AbstractTest() {
companion object {
private const val ByteBufferChannel = "io.ktor.utils.io.ByteBufferChannel"
private const val HttpClient = "io.ktor.client.HttpClient"
private const val KtorOkHttp = "io.ktor.client.engine.okhttp.OkHttp"
private const val OkHttp = "okhttp3.OkHttp"
private const val OkIO = "okio.ByteString"
private fun relocated(string: String): String {
return "net.mamoe.mirai.internal.deps.$string"
}
}
@Test
@EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
fun `test OkHttp filtered out`() {
testDir.resolve("test.kt").writeText(
fun `test mirai-core-utils`() {
val fragment = buildTestCases {
+relocated(`ktor-io`)
-both(`ktor-client-core`)
-both(`ktor-client-okhttp`)
-both(`okhttp3-okhttp`)
-both(okio)
}
applyCodeFragment(fragment)
buildFile.appendText(
"""
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")
}
}
dependencies {
implementation("net.mamoe:mirai-core-utils:$miraiLocalVersion")
}
""".trimIndent()
)
runGradle("check")
}
@Test
@EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
fun `test mirai-core-api with transitive mirai-core-utils`() {
val fragment = buildTestCases {
+relocated(`ktor-io`)
-both(`ktor-client-core`)
-both(`ktor-client-okhttp`)
-both(`okhttp3-okhttp`)
-both(okio)
}
applyCodeFragment(fragment)
buildFile.appendText(
"""
dependencies {
implementation("net.mamoe:mirai-core-api:$miraiLocalVersion")
}
""".trimIndent()
)
runGradle("check")
}
@Test
@EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
fun `test mirai-core with transitive mirai-core-api and mirai-core-utils`() {
val fragment = buildTestCases {
+relocated(`ktor-io`)
+relocated(`ktor-client-core`)
+relocated(`ktor-client-okhttp`)
+relocated(`okhttp3-okhttp`)
+relocated(okio)
}
applyCodeFragment(fragment)
buildFile.appendText(
"""
dependencies {
@ -46,25 +92,20 @@ class CoreShadowRelocationTest : AbstractTest() {
runGradle("check")
}
// ktor-io is shadowed into runtime in mirai-core-utils. So without mirai-core-utils,
// we should expect no relocated ktor-io found, otherwise there will be duplicated classes on Android.
// https://github.com/mamoe/mirai/issues/2291
@Test
@EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
fun `no duplicated class when dependency shared across modules`() {
testDir.resolve("test.kt").writeText(
"""
package test
import org.junit.jupiter.api.*
class MyTest {
@Test
fun `test base dependency`() {
assertThrows<ClassNotFoundException> {
Class.forName("net.mamoe.mirai.internal.deps.io.ktor.utils.io.ByteBufferChannel") // should only present in mirai-core-utils
}
}
}
""".trimIndent()
)
fun `test mirai-core without transitive mirai-core-api and mirai-core-utils`() {
val fragment = buildTestCases {
-both(`ktor-io`)
+relocated(`ktor-client-core`)
+relocated(`ktor-client-okhttp`)
+relocated(`okhttp3-okhttp`)
+relocated(okio)
}
applyCodeFragment(fragment)
buildFile.appendText(
"""
dependencies {
@ -80,23 +121,21 @@ class CoreShadowRelocationTest : AbstractTest() {
@Test
@EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
fun `relocated ktor presents in mirai-core-utils`() {
testDir.resolve("test.kt").writeText(
"""
package test
import org.junit.jupiter.api.*
class MyTest {
@Test
fun `test base dependency`() {
Class.forName("net.mamoe.mirai.internal.deps.io.ktor.utils.io.ByteBufferChannel")
}
}
""".trimIndent()
)
fun `test mirai-core-api without transitive mirai-core-utils`() {
val fragment = buildTestCases {
-both(`ktor-io`)
-both(`ktor-client-core`)
-both(`ktor-client-okhttp`)
-both(`okhttp3-okhttp`)
-both(okio)
}
applyCodeFragment(fragment)
buildFile.appendText(
"""
dependencies {
implementation("net.mamoe:mirai-core-utils:$miraiLocalVersion")
implementation("net.mamoe:mirai-core-api:$miraiLocalVersion") {
exclude("net.mamoe", "mirai-core-utils")
}
}
""".trimIndent()
)
@ -105,26 +144,141 @@ class CoreShadowRelocationTest : AbstractTest() {
@Test
@EnabledIf("isMiraiLocalAvailable", disabledReason = REASON_LOCAL_ARTIFACT_NOT_AVAILABLE)
fun `relocated ktor presents transitively in mirai-core`() {
testDir.resolve("test.kt").writeText(
"""
package test
import org.junit.jupiter.api.*
class MyTest {
@Test
fun `test base dependency`() {
Class.forName("net.mamoe.mirai.internal.deps.io.ktor.utils.io.ByteBufferChannel")
}
}
""".trimIndent()
)
fun `test mirai-core-all`() {
val fragment = buildTestCases {
+relocated(`ktor-io`)
+relocated(`ktor-client-core`)
+relocated(`ktor-client-okhttp`)
+relocated(`okhttp3-okhttp`)
+relocated(okio)
}
applyCodeFragment(fragment)
// mirai-core-all-2.99.0-deps-test-all.jar
val miraiCoreAllJar =
mavenLocalDir.resolve("net/mamoe/mirai-core-all/$miraiLocalVersion/mirai-core-all-$miraiLocalVersion-all.jar")
assertTrue("'${miraiCoreAllJar.absolutePath}' does not exist") { miraiCoreAllJar.exists() }
buildFile.appendText(
"""
dependencies {
implementation("net.mamoe:mirai-core:$miraiLocalVersion")
implementation(fileTree("${miraiCoreAllJar.absolutePath}"))
}
""".trimIndent()
)
runGradle("check")
}
@Suppress("PropertyName")
private class TestBuilder {
private val result = StringBuilder(
"""
package test
import org.junit.jupiter.api.*
class MyTest {
""".trimIndent()
).append("\n").append("\n")
class TestCase(
val name: String,
val qualifiedClassName: String,
)
val `ktor-io` = TestCase("ktor-io ByteBufferChannel", ByteBufferChannel)
val `ktor-client-core` = TestCase("ktor-client-core HttpClient", HttpClient)
val `ktor-client-okhttp` = TestCase("ktor-client-core OkHttp", KtorOkHttp)
val `okhttp3-okhttp` = TestCase("okhttp3 OkHttp", OkHttp)
val okio = TestCase("okio ByteString", OkIO)
class Relocated(
val testCase: TestCase
)
class Both(
val testCase: TestCase
)
private fun appendHas(name: String, qualifiedClassName: String) {
result.append(
"""
@Test
fun `has ${name}`() {
Class.forName("$qualifiedClassName")
}
""".trimIndent()
).append("\n")
}
private fun appendNotFound(name: String, qualifiedClassName: String) {
result.append(
"""
@Test
fun `no relocated ${name}`() {
assertThrows<ClassNotFoundException> { Class.forName("$qualifiedClassName") }
}
""".trimIndent()
).append("\n")
}
/**
* Asserts a class exists. Also asserts its relocated class does not exist.
*/
operator fun TestCase.unaryPlus() {
appendHas(name, qualifiedClassName)
appendNotFound("relocated $name", Companion.relocated(qualifiedClassName))
}
/**
* Asserts a class does not exist.
*/
operator fun TestCase.unaryMinus() {
appendNotFound(name, qualifiedClassName)
}
/**
* Asserts a relocated class exists. Also asserts the original class does not exist.
*/
operator fun Relocated.unaryPlus() {
this.testCase.run {
appendHas("relocated $name", Companion.relocated(qualifiedClassName))
appendNotFound(name, qualifiedClassName)
}
}
/**
* Asserts a relocated class does not exist.
*/
operator fun Relocated.unaryMinus() {
this.testCase.run {
appendNotFound("relocated $name", Companion.relocated(qualifiedClassName))
}
}
/**
* Asserts both the class and its relocated one do not exist.
*/
operator fun Both.unaryMinus() {
-this.testCase
-relocated(this.testCase)
}
fun relocated(testCase: TestCase): Relocated = Relocated(testCase)
fun both(testCase: TestCase) = Both(testCase)
fun build(): String = result.append("\n}\n").toString()
}
private inline fun buildTestCases(action: TestBuilder.() -> Unit): String {
return TestBuilder().apply(action).build()
}
private fun applyCodeFragment(fragment: String) {
println("Applying code fragment: \n\n$fragment\n\n\n===========End of Fragment===========")
testDir.resolve("test.kt").writeText(fragment)
}
}