build.gradle.kts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import org.gradle.api.DefaultTask
  2. import org.gradle.api.file.DirectoryProperty
  3. import org.gradle.api.provider.MapProperty
  4. import org.gradle.api.tasks.Input
  5. import org.gradle.api.tasks.InputDirectory
  6. import org.gradle.api.tasks.PathSensitive
  7. import org.gradle.api.tasks.PathSensitivity
  8. import org.gradle.api.tasks.TaskAction
  9. import org.gradle.work.DisableCachingByDefault
  10. @DisableCachingByDefault(because = "架构验证任务没有输出文件")
  11. abstract class VerifyConfigArchitectureTask : DefaultTask() {
  12. @get:InputDirectory
  13. @get:PathSensitive(PathSensitivity.RELATIVE)
  14. abstract val sourceRoot: DirectoryProperty
  15. @get:Input
  16. abstract val legacyPreferenceCallBaseline: MapProperty<String, Int>
  17. @get:Input
  18. abstract val legacyDaoInjectionBaseline: MapProperty<String, Int>
  19. @get:Input
  20. abstract val legacyUiDaoAccessBaseline: MapProperty<String, Int>
  21. @TaskAction
  22. fun verify() {
  23. val sourceRootDir = sourceRoot.get().asFile
  24. val kotlinFiles = sourceRootDir.walkTopDown()
  25. .filter { it.isFile && it.extension == "kt" }
  26. .toList()
  27. val preferenceBaseline = legacyPreferenceCallBaseline.get()
  28. val daoInjectionBaseline = legacyDaoInjectionBaseline.get()
  29. val uiDaoAccessBaseline = legacyUiDaoAccessBaseline.get()
  30. val violations = mutableListOf<String>()
  31. val forbiddenConfigImport = Regex(
  32. """^import io\.legado\.app\.(?:help\.config\.AppConfig|ui\.config\..*Config)$""",
  33. RegexOption.MULTILINE,
  34. )
  35. val preferenceCall = Regex("""\b(?:getPref|putPref)[A-Za-z0-9_]*\s*\(""")
  36. val daoImport = Regex(
  37. """^import io\.legado\.app\.data\.dao\.[A-Za-z0-9_*]+$""",
  38. RegexOption.MULTILINE,
  39. )
  40. val appDbDaoAccess = Regex(
  41. """(?:\bappDb|io\.legado\.app\.data\.appDb)\.[A-Za-z0-9_]*Dao\b"""
  42. )
  43. val readBookConfigWrite = Regex(
  44. """\bReadBookConfig\.[a-z_][A-Za-z0-9_]*(?:\.[a-z_][A-Za-z0-9_]*)?\s*="""
  45. )
  46. val readBookConfigMutationCall = Regex(
  47. """\bReadBookConfig\.durConfig\.set[A-Za-z0-9_]*\s*\("""
  48. )
  49. // R4.7:Config 的值字段已是 val,字段写入由编译器拦;剩下的唯一写入口是
  50. // ReadStyleConfigStore 的列表操作。它是 Koin 单例,谁 inject 谁就能绕过 gateway
  51. // 改配置且不触发 save/publishState——所以限定只有下面这几个文件能提到这个类型。
  52. val configStoreOwners = setOf(
  53. "io/legado/app/data/repository/ReadStyleConfigStore.kt",
  54. "io/legado/app/data/repository/ReadBookStyleConfigRepository.kt",
  55. "io/legado/app/help/config/ReadBookConfig.kt",
  56. "io/legado/app/di/appModule.kt",
  57. )
  58. val settingsUpdateDeclaration = Regex(
  59. """\b(?:class|interface|object|typealias)\s+[A-Za-z0-9_]*SettingsUpdate\b"""
  60. )
  61. val updateAllDeclaration = Regex("""\bfun\s+(?:<[^>\n]+>\s*)?updateAll\s*\(""")
  62. val injectedConfigFiles = setOf(
  63. "io/legado/app/help/config/AppConfig.kt",
  64. "io/legado/app/help/config/ReadBookConfig.kt",
  65. "io/legado/app/help/config/ThemePackageManager.kt",
  66. )
  67. kotlinFiles.forEach { file ->
  68. val text = file.readText()
  69. val relativePath = file.relativeTo(sourceRootDir).invariantSeparatorsPath
  70. val displayPath = "app/src/main/java/$relativePath"
  71. if ("prefDelegate" in text || "prefStateDelegate" in text ||
  72. "Snapshot.withMutableSnapshot" in text
  73. ) {
  74. violations += "$displayPath: 禁止 Snapshot 配置桥"
  75. }
  76. if ((relativePath.startsWith("io/legado/app/data/") ||
  77. relativePath.startsWith("io/legado/app/domain/")) &&
  78. forbiddenConfigImport.containsMatchIn(text)
  79. ) {
  80. violations += "$displayPath: data/domain 禁止导入全局 Config"
  81. }
  82. if (("@Composable" in text || "import androidx.compose" in text) &&
  83. forbiddenConfigImport.containsMatchIn(text)
  84. ) {
  85. violations += "$displayPath: Composable 禁止读取兼容 Config"
  86. }
  87. if (file.name.endsWith("Config.kt") &&
  88. ("mutableStateOf(" in text || "Snapshot.withMutableSnapshot" in text ||
  89. "import androidx.compose.runtime.State" in text ||
  90. "import androidx.compose.runtime.MutableState" in text)
  91. ) {
  92. violations += "$displayPath: 配置门面禁止持有 Compose State"
  93. }
  94. if (relativePath !=
  95. "io/legado/app/data/repository/ReadBookStyleConfigRepository.kt" &&
  96. (readBookConfigWrite.containsMatchIn(text) ||
  97. readBookConfigMutationCall.containsMatchIn(text))
  98. ) {
  99. violations += "$displayPath: ReadBookConfig 写入必须经过 ReadStyleGateway"
  100. }
  101. if (relativePath !in configStoreOwners && "ReadStyleConfigStore" in text) {
  102. violations += "$displayPath: 排版配置的写入口只对 ReadStyleGateway 的实现开放," +
  103. "不要注入 ReadStyleConfigStore"
  104. }
  105. if (relativePath in injectedConfigFiles && "GlobalContext" in text) {
  106. violations += "$displayPath: 配置所有者必须显式注入依赖,禁止 GlobalContext"
  107. }
  108. if (settingsUpdateDeclaration.containsMatchIn(text)) {
  109. violations += "$displayPath: 设置网关禁止重新引入 *SettingsUpdate 分发类型"
  110. }
  111. if (relativePath.startsWith("io/legado/app/domain/gateway/") &&
  112. file.name.endsWith("SettingsGateway.kt") &&
  113. updateAllDeclaration.containsMatchIn(text)
  114. ) {
  115. violations += "$displayPath: 设置网关批量修改必须使用单次 update { copy(...) }"
  116. }
  117. val preferenceCalls = preferenceCall.findAll(text).count()
  118. val allowedCalls = preferenceBaseline[relativePath] ?: 0
  119. if (preferenceCalls > allowedCalls) {
  120. violations += "$displayPath: 新增了 ${preferenceCalls - allowedCalls} 个旧偏好调用"
  121. }
  122. if (relativePath.startsWith("io/legado/app/ui/") &&
  123. file.name.contains("ViewModel")
  124. ) {
  125. val daoDependencies = daoImport.findAll(text).count() +
  126. appDbDaoAccess.findAll(text).count()
  127. val allowedDaoDependencies = daoInjectionBaseline[relativePath] ?: 0
  128. if (daoDependencies > allowedDaoDependencies) {
  129. violations += "$displayPath: ViewModel 新增了 ${daoDependencies - allowedDaoDependencies} 个 DAO 直连"
  130. } else if (daoDependencies < allowedDaoDependencies) {
  131. violations += "$displayPath: 已减少 DAO 直连,请将基线从 $allowedDaoDependencies 下调到 $daoDependencies"
  132. }
  133. }
  134. if (relativePath.startsWith("io/legado/app/ui/") &&
  135. !file.name.contains("ViewModel")
  136. ) {
  137. val daoDependencies = daoImport.findAll(text).count() +
  138. appDbDaoAccess.findAll(text).count()
  139. val allowedDaoDependencies = uiDaoAccessBaseline[relativePath] ?: 0
  140. if (daoDependencies > allowedDaoDependencies) {
  141. violations += "$displayPath: UI 层新增了 ${daoDependencies - allowedDaoDependencies} 个 DAO 直连"
  142. } else if (daoDependencies < allowedDaoDependencies) {
  143. violations += "$displayPath: 已减少 DAO 直连,请将基线从 $allowedDaoDependencies 下调到 $daoDependencies"
  144. }
  145. }
  146. }
  147. val sourcePaths = kotlinFiles.mapTo(hashSetOf()) {
  148. it.relativeTo(sourceRootDir).invariantSeparatorsPath
  149. }
  150. (daoInjectionBaseline.keys - sourcePaths).forEach { relativePath ->
  151. violations += "app/src/main/java/$relativePath: 文件已移除,请删除 DAO 直连基线"
  152. }
  153. (uiDaoAccessBaseline.keys - sourcePaths).forEach { relativePath ->
  154. violations += "app/src/main/java/$relativePath: 文件已移除,请删除 UI DAO 直连基线"
  155. }
  156. check(violations.isEmpty()) {
  157. violations.joinToString(prefix = "配置架构护栏失败:\n", separator = "\n")
  158. }
  159. }
  160. }
  161. buildscript {
  162. extra.apply {
  163. set("compile_sdk_version", 36)
  164. set("build_tool_version", "34.0.0")
  165. }
  166. }
  167. plugins {
  168. alias(libs.plugins.compose.compiler) apply false
  169. alias(libs.plugins.android.application) apply false
  170. alias(libs.plugins.android.library) apply false
  171. alias(libs.plugins.kotlin.parcelize) apply false
  172. alias(libs.plugins.kotlin.serialization) apply false
  173. alias(libs.plugins.ksp) apply false
  174. alias(libs.plugins.google.services) apply false
  175. alias(libs.plugins.room) apply false
  176. alias(libs.plugins.download) apply false
  177. alias(libs.plugins.android.test) apply false
  178. alias(libs.plugins.baselineprofile) apply false
  179. }
  180. tasks.register<Delete>("clean") {
  181. delete(rootProject.layout.buildDirectory)
  182. }
  183. val verifyConfigArchitecture = tasks.register<VerifyConfigArchitectureTask>(
  184. "verifyConfigArchitecture"
  185. ) {
  186. group = "verification"
  187. description = "禁止配置架构回退、UI 层(ViewModel 及其它)新增 DAO 直连和新增旧偏好调用"
  188. sourceRoot.set(layout.projectDirectory.dir("app/src/main/java"))
  189. legacyPreferenceCallBaseline.set(
  190. mapOf(
  191. "io/legado/app/App.kt" to 3,
  192. "io/legado/app/base/BaseActivity.kt" to 2,
  193. "io/legado/app/base/BaseService.kt" to 1,
  194. "io/legado/app/data/repository/CoverAlbumRepository.kt" to 4,
  195. "io/legado/app/data/repository/HighlightRuleRepository.kt" to 9,
  196. "io/legado/app/data/repository/HomeDashboardRepository.kt" to 3,
  197. "io/legado/app/data/repository/ReadRecordRepository.kt" to 1,
  198. "io/legado/app/data/repository/SettingsRepository.kt" to 7,
  199. "io/legado/app/help/config/LocalConfig.kt" to 3,
  200. "io/legado/app/help/config/ThemeConfigStore.kt" to 8,
  201. "io/legado/app/help/storage/Restore.kt" to 2,
  202. "io/legado/app/receiver/MediaButtonReceiver.kt" to 2,
  203. "io/legado/app/service/WebService.kt" to 2,
  204. "io/legado/app/ui/association/ImportReplaceRuleDialog.kt" to 1,
  205. "io/legado/app/ui/book/explore/ExploreShowViewModel.kt" to 2,
  206. "io/legado/app/ui/book/read/ReadBookViewModel.kt" to 2,
  207. "io/legado/app/ui/book/readRecord/ReadRecordViewModel.kt" to 1,
  208. "io/legado/app/ui/book/search/SearchViewModel.kt" to 3,
  209. "io/legado/app/ui/config/CheckSourceConfig.kt" to 1,
  210. "io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt" to 1,
  211. "io/legado/app/ui/replace/ReplaceRuleViewModel.kt" to 2,
  212. "io/legado/app/utils/ContextExtensions.kt" to 12,
  213. "io/legado/app/web/socket/BookSearchWebSocket.kt" to 2,
  214. )
  215. )
  216. legacyDaoInjectionBaseline.set(
  217. mapOf(
  218. // R2.1 已清零:ReadBookViewModel 的书籍/目录读写全部经 BookRepository。
  219. // 保留 0 值条目让棘轮继续盯着这个文件——新增一处直连就报红。
  220. "io/legado/app/ui/book/read/ReadBookViewModel.kt" to 0,
  221. // 护栏缺席期间(MAD-3 未合并窗口)main 新增的直连,随合并冻结,清理归 Track A/F2
  222. "io/legado/app/ui/book/readaloud/cloudtts/CloudTtsViewModel.kt" to 13,
  223. )
  224. )
  225. // 非 ViewModel 的 UI 层文件直连 DAO 的历史债,只冻结不修复;
  226. // 清理时逐条下调/删除。护栏会自动要求"减少了就下调基线",防止回退。
  227. legacyUiDaoAccessBaseline.set(
  228. mapOf(
  229. "io/legado/app/ui/association/AddToBookshelfDialog.kt" to 5,
  230. "io/legado/app/ui/association/ImportBookSourceDialog.kt" to 1,
  231. "io/legado/app/ui/association/ImportReplaceRuleDialog.kt" to 1,
  232. "io/legado/app/ui/association/ImportRssSourceDialog.kt" to 1,
  233. "io/legado/app/ui/book/audio/AudioPlayActivity.kt" to 1,
  234. "io/legado/app/ui/book/bookmark/BookmarkDialog.kt" to 2,
  235. "io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt" to 1,
  236. "io/legado/app/ui/book/group/GroupManageDialog.kt" to 2,
  237. "io/legado/app/ui/book/group/GroupSelectDialog.kt" to 1,
  238. "io/legado/app/ui/book/manga/ReadMangaActivity.kt" to 1,
  239. "io/legado/app/ui/book/read/ReadBookController.kt" to 3,
  240. "io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt" to 1,
  241. // 护栏缺席期间 main 新增(整书页码估算),随合并冻结
  242. "io/legado/app/ui/book/read/pageestimate/ExactChapterPageCountStore.kt" to 3,
  243. "io/legado/app/ui/book/search/SearchScope.kt" to 4,
  244. "io/legado/app/ui/book/search/SearchScopeDialog.kt" to 3,
  245. "io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt" to 1,
  246. "io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt" to 1,
  247. "io/legado/app/ui/book/source/manage/BookSourceActivity.kt" to 10,
  248. "io/legado/app/ui/book/source/manage/GroupManageDialog.kt" to 1,
  249. "io/legado/app/ui/config/bookshelfConfig/BookshelfManageScreenConfig.kt" to 1,
  250. "io/legado/app/ui/main/MainNavGraph.kt" to 2,
  251. "io/legado/app/ui/rss/article/RssArticlesCompose.kt" to 1,
  252. "io/legado/app/ui/rss/read/RssJsExtensions.kt" to 8,
  253. "io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt" to 1,
  254. "io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt" to 1,
  255. "io/legado/app/ui/widget/keyboard/KeyboardAssistsConfig.kt" to 7,
  256. "io/legado/app/ui/widget/keyboard/KeyboardToolPop.kt" to 1,
  257. )
  258. )
  259. }
  260. subprojects {
  261. tasks.configureEach {
  262. if (name.startsWith("assemble") || name.startsWith("compile")) {
  263. dependsOn(verifyConfigArchitecture)
  264. }
  265. }
  266. }