diff --git a/build.gradle.kts b/build.gradle.kts index 991eb3957..1bd773e45 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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().forEach { shadow -> - shadow.enabled = false // they are too big - } - logger.info("Disabled all shadow tasks.") - } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 28a399781..c9eaeeba4 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -23,6 +23,7 @@ kotlin { sourceSets.all { languageSettings.optIn("kotlin.Experimental") languageSettings.optIn("kotlin.RequiresOptIn") + languageSettings.optIn("kotlin.ExperimentalStdlibApi") } } diff --git a/buildSrc/src/main/kotlin/HmppConfigure.kt b/buildSrc/src/main/kotlin/HmppConfigure.kt index 201b2b218..1f9dccb4c 100644 --- a/buildSrc/src/main/kotlin/HmppConfigure.kt +++ b/buildSrc/src/main/kotlin/HmppConfigure.kt @@ -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) } } diff --git a/buildSrc/src/main/kotlin/JvmPublishing.kt b/buildSrc/src/main/kotlin/JvmPublishing.kt index 3e6be24f2..b8f6f1a34 100644 --- a/buildSrc/src/main/kotlin/JvmPublishing.kt +++ b/buildSrc/src/main/kotlin/JvmPublishing.kt @@ -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") { + val shadowJar = if (!addProjectComponents || !addShadowJar) null else tasks.register("shadowJar") { archiveClassifier.set("all") manifest.inheritFrom(tasks.getByName("jar").manifest) from(project.sourceSets["main"].output) diff --git a/buildSrc/src/main/kotlin/ProjectConfigure.kt b/buildSrc/src/main/kotlin/ProjectConfigure.kt index 8717afa00..409eb33d3 100644 --- a/buildSrc/src/main/kotlin/ProjectConfigure.kt +++ b/buildSrc/src/main/kotlin/ProjectConfigure.kt @@ -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() diff --git a/buildSrc/src/main/kotlin/Relocation.kt b/buildSrc/src/main/kotlin/Relocation.kt index b923fc758..812d3453f 100644 --- a/buildSrc/src/main/kotlin/Relocation.kt +++ b/buildSrc/src/main/kotlin/Relocation.kt @@ -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.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 configure(list: Iterable, 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 = 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 = 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 { + 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 = Action {} +): ExternalModuleDependency { + val dependency = + addDependencyTo(this, "implementation", relocatedDependency.notation, Action { + 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 { + 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 = listOf(groupId), val filesFilter: String = groupId.replace(".", "/"), /** * Pack relocated dependency into the fat jar. If set to `false`, dependencies will be removed. diff --git a/buildSrc/src/main/kotlin/Shadow.kt b/buildSrc/src/main/kotlin/Shadow.kt index c33e29505..d25ba3bc3 100644 --- a/buildSrc/src/main/kotlin/Shadow.kt +++ b/buildSrc/src/main/kotlin/Shadow.kt @@ -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.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.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) + } } -} \ No newline at end of file +} + +fun Project.configureRelocatedShadowJarForJvmProject(kotlin: KotlinJvmProjectExtension): ShadowJar { + return registerRegularShadowTask(kotlin.target, mapTaskNameForMultipleTargets = false) +} + +const val RELOCATION_ROOT_PACKAGE = "net.mamoe.mirai.internal.deps" \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 3db5b30ac..22fb9354c 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -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> { + 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 = 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 = 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}" diff --git a/mirai-core-all/build.gradle.kts b/mirai-core-all/build.gradle.kts index c19d24550..010d7b34a 100644 --- a/mirai-core-all/build.gradle.kts +++ b/mirai-core-all/build.gradle.kts @@ -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) \ No newline at end of file +// +//// 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")) \ No newline at end of file diff --git a/mirai-core-api/build.gradle.kts b/mirai-core-api/build.gradle.kts index b94f21ed4..8e39533aa 100644 --- a/mirai-core-api/build.gradle.kts +++ b/mirai-core-api/build.gradle.kts @@ -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" diff --git a/mirai-core-utils/build.gradle.kts b/mirai-core-utils/build.gradle.kts index f67d3edd2..ff8c6c99d 100644 --- a/mirai-core-utils/build.gradle.kts +++ b/mirai-core-utils/build.gradle.kts @@ -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" diff --git a/mirai-core/build.gradle.kts b/mirai-core/build.gradle.kts index 695db5b63..cb68df163 100644 --- a/mirai-core/build.gradle.kts +++ b/mirai-core/build.gradle.kts @@ -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" diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 3ff804c8d..317cd05ef 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -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() diff --git a/mirai-deps-test/build.gradle.kts b/mirai-deps-test/build.gradle.kts index 8b2f1addb..e154b7ae1 100644 --- a/mirai-deps-test/build.gradle.kts +++ b/mirai-deps-test/build.gradle.kts @@ -95,6 +95,7 @@ tasks.register("publishMiraiLocalArtifacts", Exec::class) { "./gradlew", publishMiraiArtifactsToMavenLocal.name, "--no-daemon", + "--stacktrace", "-Pkotlin.compiler.execution.strategy=in-process" ) standardOutput = System.out diff --git a/mirai-deps-test/test/AbstractTest.kt b/mirai-deps-test/test/AbstractTest.kt index ccacd0af5..5f1e4d9f7 100644 --- a/mirai-deps-test/test/AbstractTest.kt +++ b/mirai-deps-test/test/AbstractTest.kt @@ -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)" + } } \ No newline at end of file diff --git a/mirai-deps-test/test/CoreDependencyResolutionTest.kt b/mirai-deps-test/test/CoreDependencyResolutionTest.kt index 54be16c2f..6fdddf886 100644 --- a/mirai-deps-test/test/CoreDependencyResolutionTest.kt +++ b/mirai-deps-test/test/CoreDependencyResolutionTest.kt @@ -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() ) diff --git a/mirai-deps-test/test/CoreShadowRelocationTest.kt b/mirai-deps-test/test/CoreShadowRelocationTest.kt index 99a6636bc..2e31e0a6f 100644 --- a/mirai-deps-test/test/CoreShadowRelocationTest.kt +++ b/mirai-deps-test/test/CoreShadowRelocationTest.kt @@ -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 { - Class.forName("io.ktor.client.engine.okhttp.OkHttp") - } - } - @Test - fun `test transitive dependency`() { - assertThrows { - 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 { - 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 { 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) + } } \ No newline at end of file