Relocation.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /*
  2. * Copyright 2019-2023 Mamoe Technologies and contributors.
  3. *
  4. * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  5. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
  6. *
  7. * https://github.com/mamoe/mirai/blob/dev/LICENSE
  8. */
  9. import org.gradle.api.Action
  10. import org.gradle.api.DomainObjectCollection
  11. import org.gradle.api.Project
  12. import org.gradle.api.artifacts.Configuration
  13. import org.gradle.api.artifacts.Dependency
  14. import org.gradle.api.artifacts.ExternalModuleDependency
  15. import org.gradle.api.artifacts.dsl.DependencyHandler
  16. import org.gradle.kotlin.dsl.accessors.runtime.addDependencyTo
  17. import org.gradle.kotlin.dsl.extra
  18. import org.gradle.kotlin.dsl.invoke
  19. import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
  20. /**
  21. * # 非常重要的提示 — 有关 relocation — 在看完全部本文前, 不要进行任何操作
  22. *
  23. * Mirai 会 relocate 一些内部使用的依赖库, 来允许调用方 (依赖 mirai 的模块).
  24. * 例如, mirai 使用 2.0, 但调用方可以使用 Ktor 1.0 而不会有 classpath 冲突.
  25. *
  26. * 这是通过 relocation 完成的. 在 mirai 项目构建中, Relocation 是指将一个模块的一些 class 的包名替换为另一个包名的过程.
  27. * 继续使用 Ktor 示例, `io.ktor.utils.io.core.ByteReadPacket` 将会被 relocate 为
  28. * `net.mamoe.mirai.internal.deps.io.ktor.utils.io.core.ByteReadPacket`, 即放到内部包内.
  29. *
  30. * ## 哪些模块被 relocate 了?
  31. *
  32. * 在 2.13.0 mirai 只 relocate `io.ktor`, `okhttp3`, `okio` 下的所有类.
  33. *
  34. * ## 如何配置 relocation?
  35. *
  36. * Relocation 的范围是通过 [Configuration] 指定的.
  37. *
  38. * 在通常的 `dependencies` 配置中, 使用 [relocateCompileOnly] 和 [relocateImplementation]
  39. * 可分别创建 `compileOnly` 或 `implementation` 的依赖, 并为其配置 relocation.
  40. *
  41. * ## 不能 relocate 参与 mirai 公开 API/ABI 的库
  42. *
  43. * 有些库的部分定义参与组成 mirai 的公开 API/ABI. 例如 kotlin-stdlib 提供 `String` 等类型, kotlinx-coroutines 提供 `CoroutineScope`等.
  44. * mirai 已经发布了使用这些类型的 API, 为了保证 ABI 兼容, 不能 relocate 它们 (你就需要在 [Configuration] `exclude`).
  45. * 要知道哪些 API 参与了 ABI, 执行 `./gradlew :mirai-core:dependencies` (把 `mirai-core` 换成你想了解的模块), 查看 `runtimeDependencies` 之类的.
  46. *
  47. * ## 考虑是否在运行时包含你的依赖 — 选择 [relocateCompileOnly] 和 [relocateImplementation]
  48. *
  49. * 根据 [Configuration] 配置不同, 被 relocate 的模块的间接依赖可能会或不会被处理.
  50. * 例如
  51. *
  52. * 所以 `io.ktor:ktor-client-okhttp` 依赖的 okhttp 不会被 relocate!
  53. * 因此你必须手动检查目标依赖的全部间接依赖并添加 relocation 配置. 不要轻易升级经过了 relocation 的依赖, 因为有可能他们的新版本会使用新的依赖!
  54. * 这个过程无法自动化, 因为你 relocate 的模块可能会依赖 kotlin-stlib 等你不会想要 relocation 的依赖. 为什么你不会想 relocate kotlin-stlib? 继续阅读
  55. *
  56. * relocate 依赖之后, 你的程序在运行时必须要有 relocate 之后的类, 比如 `net.mamoe.mirai.internal.deps.io.ktor.utils.io.core.ByteReadPacket`.
  57. *
  58. * [relocateCompileOnly] 会为你添加通常的 `compileOnly`, 然后配置 relocate, 但不会在打包 JAR 时包含被 relocate 的库. 而 [relocateImplementation] 则会包含.
  59. *
  60. * 在独立模块下很简单, 你只要一直使用 [relocateImplementation] 就行了.
  61. *
  62. * 但在有依赖关系的模块, 比如 mirai-core-api 依赖 mirai-core-utils, 而它们都需要使用 ktor-io, 就需要让_最早_依赖 ktor-io 的 mirai-core-utils 模块在运行时包含 ([relocateImplementation]), 而 mirai-core-api 不包含 ([relocateCompileOnly]).
  63. * 运行时 mirai-core-api 就会使用来自 mirai-core-utils 的 `net.mamoe.mirai.internal.deps.io.ktor.utils.io.core.ByteReadPacket`.
  64. *
  65. * 如果你都使用 [relocateImplementation], 就会导致在 Android 平台发生 'Duplicated Class' 问题. 如果你都使用 [relocateCompileOnly] 则会在 clinit 阶段遇到 [NoClassDefFoundError]
  66. *
  67. * ## relocation 发生的时机晚于编译
  68. *
  69. * mirai-core-utils relocate 了 ktor-io, 然后 mirai-core 在 `build.gradle.kts` 使用了 `implementation(project(":mirai-core-utils"))`.
  70. * 在 mirai-core 编译时, 编译器仍然会使用 relocate 之前的 `io.ktor`. 为了在 mirai-core 将对 `io.ktor` 的调用转为对 `net.mamoe.mirai.internal.deps.io.ktor` 的调用, 需要配置 relocation.
  71. * 所以此时就不能让 mirai-core 也打包 relocation 后的 ktor 而在运行时携带, 否则因为用户依赖 mirai-core 时 Maven 会同时下载 mirai-core-utils, 用户 classpath 就会有两份被 relocate 的 Ktor, 导致冲突.
  72. *
  73. * 所以你需要为所有依赖了 mirai-core-utils 的模块都分别配置 [relocateCompileOnly].
  74. *
  75. * ### "在运行时包含" 是如何实现的?
  76. *
  77. * 被 relocate 的类会被直接当做是当前模块的类打包进 JAR.
  78. * 比如 ktor-io 的所有代码就会被变换包名后全部打包进 "mirai-core-utils.jar",
  79. * 而不是在 maven POM 中定义而以后从 Maven Central 等远程仓库下载.
  80. *
  81. * ----
  82. *
  83. * 如果你已经懂了以上内容, 你就可以修改 relocation 相关了. 修改之后一定在 mirai-deps-test 模块中添加测试.
  84. */
  85. object RelocationNotes
  86. /**
  87. * 添加一个通常的 [compileOnly][KotlinDependencyHandler.compileOnly] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  88. *
  89. * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
  90. * 运行时 (runtime) **不会**包含被 relocate 的依赖及其所有间接依赖.
  91. *
  92. * @see RelocationNotes
  93. */
  94. fun KotlinDependencyHandler.relocateCompileOnly(
  95. relocatedDependency: RelocatedDependency,
  96. ): ExternalModuleDependency {
  97. val dependency = compileOnly(relocatedDependency.notation) {
  98. }
  99. project.relocationFilters.add(
  100. RelocationFilter(
  101. dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
  102. )
  103. )
  104. // Don't add to runtime
  105. return dependency
  106. }
  107. /**
  108. * 添加一个通常的 [compileOnly][KotlinDependencyHandler.compileOnly] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  109. *
  110. * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都不会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
  111. * 运行时 (runtime) **不会**包含被 relocate 的依赖及其所有间接依赖.
  112. *
  113. * @see RelocationNotes
  114. */
  115. fun DependencyHandler.relocateCompileOnly(
  116. project: Project,
  117. relocatedDependency: RelocatedDependency,
  118. ): Dependency {
  119. val dependency =
  120. addDependencyTo(this, "compileOnly", relocatedDependency.notation, Action<ExternalModuleDependency> {
  121. })
  122. project.relocationFilters.add(
  123. RelocationFilter(
  124. dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
  125. )
  126. )
  127. // Don't add to runtime
  128. return dependency
  129. }
  130. /**
  131. * 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  132. *
  133. * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用**都会**被 relocate 到 [RELOCATION_ROOT_PACKAGE].
  134. * 运行时 (runtime) 将**会**包含被 relocate 的依赖及其所有间接依赖.
  135. *
  136. * @see RelocationNotes
  137. */
  138. fun KotlinDependencyHandler.relocateImplementation(
  139. relocatedDependency: RelocatedDependency,
  140. action: ExternalModuleDependency.() -> Unit = {}
  141. ): ExternalModuleDependency {
  142. val dependency = implementation(relocatedDependency.notation) {
  143. }
  144. project.relocationFilters.add(
  145. RelocationFilter(
  146. dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
  147. )
  148. )
  149. project.configurations.maybeCreate(SHADOW_RELOCATION_CONFIGURATION_NAME)
  150. addDependencyTo(
  151. project.dependencies,
  152. SHADOW_RELOCATION_CONFIGURATION_NAME,
  153. relocatedDependency.notation,
  154. Action<ExternalModuleDependency> {
  155. relocatedDependency.exclusionAction(this)
  156. intrinsicExclusions()
  157. action()
  158. }
  159. )
  160. return dependency
  161. }
  162. /**
  163. * 添加一个通常的 [implementation][KotlinDependencyHandler.implementation] 依赖, 并按 [relocatedDependency] 定义的配置 relocate.
  164. *
  165. * 在发布版本时, 全部对 [RelocatedDependency.packages] 中的 API 的调用都会被 relocate 到 [RELOCATION_ROOT_PACKAGE].
  166. * 运行时 (runtime) 将会包含被 relocate 的依赖及其所有间接依赖.
  167. *
  168. * @see RelocationNotes
  169. */
  170. fun DependencyHandler.relocateImplementation(
  171. project: Project,
  172. relocatedDependency: RelocatedDependency,
  173. action: Action<ExternalModuleDependency> = Action {}
  174. ): ExternalModuleDependency {
  175. val dependency =
  176. addDependencyTo(this, "implementation", relocatedDependency.notation, Action<ExternalModuleDependency> {
  177. })
  178. project.relocationFilters.add(
  179. RelocationFilter(
  180. dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
  181. )
  182. )
  183. project.configurations.maybeCreate(SHADOW_RELOCATION_CONFIGURATION_NAME)
  184. addDependencyTo(
  185. project.dependencies,
  186. SHADOW_RELOCATION_CONFIGURATION_NAME,
  187. relocatedDependency.notation,
  188. Action<ExternalModuleDependency> {
  189. relocatedDependency.exclusionAction(this)
  190. intrinsicExclusions()
  191. action(this)
  192. }
  193. )
  194. return dependency
  195. }
  196. @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // compiler bug
  197. private val ExternalModuleDependency.groupNotNull get() = group!!
  198. private fun ExternalModuleDependency.intrinsicExclusions() {
  199. exclude(ExcludeProperties.`everything from kotlin`)
  200. exclude(ExcludeProperties.`everything from kotlinx`)
  201. }
  202. const val SHADOW_RELOCATION_CONFIGURATION_NAME = "shadowRelocation"
  203. data class RelocationFilter(
  204. val groupId: String,
  205. val artifactId: String? = null,
  206. val packages: List<String> = listOf(groupId),
  207. val filesFilter: String = groupId.replace(".", "/"),
  208. /**
  209. * Pack relocated dependency into the fat jar. If set to `false`, dependencies will be removed.
  210. * This is to avoid duplicated classes. See #2291.
  211. */ // #2291
  212. val includeInRuntime: Boolean,
  213. ) {
  214. fun matchesDependency(groupId: String?, artifactId: String?): Boolean {
  215. if (this.groupId == groupId) return true
  216. if (this.artifactId != null && this.artifactId == artifactId) return true
  217. return false
  218. }
  219. }
  220. val Project.relocationFilters: DomainObjectCollection<RelocationFilter>
  221. get() {
  222. if (project.extra.has("relocationFilters")) {
  223. @Suppress("UNCHECKED_CAST")
  224. return project.extra.get("relocationFilters") as DomainObjectCollection<RelocationFilter>
  225. } else {
  226. val container = project.objects.domainObjectSet(RelocationFilter::class.java)
  227. project.extra.set("relocationFilters", container)
  228. return container
  229. }
  230. }