build: build-logic 约定插件 (mcl.*)
This commit is contained in:
36
build-logic/convention/DESIGN_NOTES.md
Normal file
36
build-logic/convention/DESIGN_NOTES.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# DESIGN_NOTES — 约定插件
|
||||||
|
|
||||||
|
## 为什么存在
|
||||||
|
|
||||||
|
多模块项目里,`compileSdk`/`minSdk`/Java target、Compose、Hilt、Room、Lint、Spotless 等配置若在每个模块重复手写,必然漂移。约定插件把这些配置**集中编码**,模块只声明"我是什么类型"(`mcl.*` 插件),不关心"怎么配"。
|
||||||
|
|
||||||
|
这也是 harness-engineering 的"严谨性转移":把约束写进确定性规则,而不是依赖记忆。
|
||||||
|
|
||||||
|
## 插件映射
|
||||||
|
|
||||||
|
| 插件 id | 实现类 | 作用 |
|
||||||
|
|---------|--------|------|
|
||||||
|
| `mcl.kotlin.library` | `JvmLibraryConventionPlugin` | 纯 JVM(kotlin-jvm + jvmTarget 11 + spotless + junit) |
|
||||||
|
| `mcl.android.library` | `AndroidLibraryConventionPlugin` | Android 库(application/library 插件的库版 + lint + spotless + compileSdk/minSdk) |
|
||||||
|
| `mcl.android.library.compose` | `AndroidLibraryComposeConventionPlugin` | = library + Compose |
|
||||||
|
| `mcl.android.application.compose` | `AndroidApplicationComposeConventionPlugin` | = application + Compose |
|
||||||
|
| `mcl.android.feature` | `AndroidFeatureConventionPlugin` | = library.compose + hilt + 导航/lifecycle/共享模块依赖 |
|
||||||
|
| `mcl.android.hilt` | `AndroidHiltConventionPlugin` | KSP + Hilt(hilt-android + hilt-compiler) |
|
||||||
|
| `mcl.android.room` | `AndroidRoomConventionPlugin` | KSP + Room(runtime/ktx/compiler + schema 目录) |
|
||||||
|
| `mcl.android.lint` | `AndroidLintConventionPlugin` | lint 配置(checkDependencies、disable GradleDependency) |
|
||||||
|
| `mcl.root` | `RootPlugin` | 注册 `verifyArchitecture` |
|
||||||
|
|
||||||
|
## 关键实现要点
|
||||||
|
|
||||||
|
- **内置 Kotlin**:AGP 9 内置 Kotlin,因此**不**显式应用 `org.jetbrains.kotlin.android`;Kotlin 编译选项通过 `KotlinAndroidProjectExtension` 的 `compilerOptions` 配置(见 `KotlinAndroid.kt`)。
|
||||||
|
- **compose 插件自包含**:`*.compose` 插件先 `apply(mcl.android.library/application)` 再叠加 Compose,所以模块只需声明 compose 插件一个。
|
||||||
|
- **helper 函数**:`KotlinAndroid.kt`(SDK/Java/Kotlin)、`AndroidCompose.kt`(compose feature + BOM + tooling + material3)、`Spotless.kt`(ktlint,无 license header)拆成独立函数,插件只做编排。
|
||||||
|
- **`libs` 访问器**:`ProjectExtensions.kt` 里的 `val Project.libs` 让插件能引用版本目录(`libs.findLibrary/findVersion`)。
|
||||||
|
|
||||||
|
## 如何新增
|
||||||
|
|
||||||
|
1. **新模块**:建 `build.gradle.kts`(选对插件 + namespace)+ 空 `AndroidManifest.xml`(Android 模块),在 `settings.gradle.kts` include,在 `RootPlugin.kt` 的 allow-list 加一行。
|
||||||
|
2. **新插件**:在 `convention/build.gradle.kts` 的 `gradlePlugin { plugins { register(...) } }` 注册,并在 `gradle/libs.versions.toml` 的 `[plugins]` 里加一个 `mcl-*` 条目(无 version)。
|
||||||
|
3. **改配置**:优先改 helper 函数,让所有适用模块一并生效。
|
||||||
|
|
||||||
|
> 注意:`mcl-android-application` 与 `mcl-android-library` 因有 `-compose` 变体(叶子也是前缀),在 `convention/build.gradle.kts` 里引用它们的 plugin id 时要用 `.asProvider().get().pluginId`,其余用 `.get().pluginId`。
|
||||||
79
build-logic/convention/build.gradle.kts
Normal file
79
build-logic/convention/build.gradle.kts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.mingyuan.androidclient.buildlogic"
|
||||||
|
|
||||||
|
// build-logic targets the JDK used to run Gradle (17), unrelated to the device/runtime JDK.
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.android.gradlePlugin)
|
||||||
|
compileOnly(libs.kotlin.gradlePlugin)
|
||||||
|
compileOnly(libs.ksp.gradlePlugin)
|
||||||
|
compileOnly(libs.room.gradlePlugin)
|
||||||
|
compileOnly(libs.spotless.gradlePlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
validatePlugins {
|
||||||
|
enableStricterValidation = true
|
||||||
|
failOnWarning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gradlePlugin {
|
||||||
|
plugins {
|
||||||
|
register("androidApplication") {
|
||||||
|
id = libs.plugins.mcl.android.application.asProvider().get().pluginId
|
||||||
|
implementationClass = "AndroidApplicationConventionPlugin"
|
||||||
|
}
|
||||||
|
register("androidApplicationCompose") {
|
||||||
|
id = libs.plugins.mcl.android.application.compose.get().pluginId
|
||||||
|
implementationClass = "AndroidApplicationComposeConventionPlugin"
|
||||||
|
}
|
||||||
|
register("androidLibrary") {
|
||||||
|
id = libs.plugins.mcl.android.library.asProvider().get().pluginId
|
||||||
|
implementationClass = "AndroidLibraryConventionPlugin"
|
||||||
|
}
|
||||||
|
register("androidLibraryCompose") {
|
||||||
|
id = libs.plugins.mcl.android.library.compose.get().pluginId
|
||||||
|
implementationClass = "AndroidLibraryComposeConventionPlugin"
|
||||||
|
}
|
||||||
|
register("androidFeature") {
|
||||||
|
id = libs.plugins.mcl.android.feature.get().pluginId
|
||||||
|
implementationClass = "AndroidFeatureConventionPlugin"
|
||||||
|
}
|
||||||
|
register("androidHilt") {
|
||||||
|
id = libs.plugins.mcl.android.hilt.get().pluginId
|
||||||
|
implementationClass = "AndroidHiltConventionPlugin"
|
||||||
|
}
|
||||||
|
register("androidRoom") {
|
||||||
|
id = libs.plugins.mcl.android.room.get().pluginId
|
||||||
|
implementationClass = "AndroidRoomConventionPlugin"
|
||||||
|
}
|
||||||
|
register("androidLint") {
|
||||||
|
id = libs.plugins.mcl.android.lint.get().pluginId
|
||||||
|
implementationClass = "AndroidLintConventionPlugin"
|
||||||
|
}
|
||||||
|
register("kotlinLibrary") {
|
||||||
|
id = libs.plugins.mcl.kotlin.library.get().pluginId
|
||||||
|
implementationClass = "JvmLibraryConventionPlugin"
|
||||||
|
}
|
||||||
|
register("root") {
|
||||||
|
id = libs.plugins.mcl.root.get().pluginId
|
||||||
|
implementationClass = "RootPlugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
import com.mingyuan.androidclient.buildlogic.configureAndroidCompose
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.getByType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose for the application module. Applies [AndroidApplicationConventionPlugin] (compileSdk,
|
||||||
|
* lint, formatting) and then adds Compose. Apply this single plugin for the app module.
|
||||||
|
*/
|
||||||
|
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
apply(plugin = "mcl.android.application")
|
||||||
|
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||||
|
|
||||||
|
configureAndroidCompose(extensions.getByType<ApplicationExtension>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
import com.mingyuan.androidclient.buildlogic.configureKotlinAndroid
|
||||||
|
import com.mingyuan.androidclient.buildlogic.configureSpotless
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base convention for the application module. Applies the Android application plugin, Kotlin +
|
||||||
|
* Android SDK defaults, lint, and deterministic formatting. Compose is added by
|
||||||
|
* [AndroidApplicationComposeConventionPlugin].
|
||||||
|
*/
|
||||||
|
class AndroidApplicationConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
apply(plugin = "com.android.application")
|
||||||
|
apply(plugin = "mcl.android.lint")
|
||||||
|
|
||||||
|
extensions.configure<ApplicationExtension> {
|
||||||
|
configureKotlinAndroid(this)
|
||||||
|
defaultConfig.targetSdk = 37
|
||||||
|
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
testOptions.animationsDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
configureSpotless()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import com.mingyuan.androidclient.buildlogic.libs
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convention for a feature module: Compose + Hilt, plus the shared dependencies every feature
|
||||||
|
* needs (shared UI, design system, domain models, and Navigation). Feature modules should apply
|
||||||
|
* only this plugin (plus any feature-specific additions).
|
||||||
|
*/
|
||||||
|
class AndroidFeatureConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
apply(plugin = "mcl.android.library.compose")
|
||||||
|
apply(plugin = "mcl.android.hilt")
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
"implementation"(project(":core:ui"))
|
||||||
|
"implementation"(project(":core:designsystem"))
|
||||||
|
"implementation"(project(":core:model"))
|
||||||
|
"implementation"(project(":core:domain"))
|
||||||
|
"implementation"(libs.findLibrary("androidx-navigation-compose").get())
|
||||||
|
"implementation"(libs.findLibrary("androidx-lifecycle-runtime-compose").get())
|
||||||
|
"implementation"(libs.findLibrary("androidx-lifecycle-viewmodel-compose").get())
|
||||||
|
"implementation"(libs.findLibrary("androidx-hilt-navigation-compose").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import com.mingyuan.androidclient.buildlogic.libs
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies Hilt via KSP (not kapt) and wires the Hilt runtime + compiler.
|
||||||
|
*/
|
||||||
|
class AndroidHiltConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
with(pluginManager) {
|
||||||
|
apply("com.google.devtools.ksp")
|
||||||
|
apply("com.google.dagger.hilt.android")
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
"implementation"(libs.findLibrary("hilt-android").get())
|
||||||
|
"ksp"(libs.findLibrary("hilt-compiler").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import com.android.build.api.dsl.LibraryExtension
|
||||||
|
import com.mingyuan.androidclient.buildlogic.configureAndroidCompose
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.getByType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose for an Android library module. Applies [AndroidLibraryConventionPlugin] (compileSdk,
|
||||||
|
* lint, formatting) and then adds Compose. Apply this single plugin for Compose-enabled libraries.
|
||||||
|
*/
|
||||||
|
class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
apply(plugin = "mcl.android.library")
|
||||||
|
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||||
|
|
||||||
|
configureAndroidCompose(extensions.getByType<LibraryExtension>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import com.android.build.api.dsl.LibraryExtension
|
||||||
|
import com.mingyuan.androidclient.buildlogic.configureKotlinAndroid
|
||||||
|
import com.mingyuan.androidclient.buildlogic.configureSpotless
|
||||||
|
import com.mingyuan.androidclient.buildlogic.libs
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base convention for Android library modules. Applies the Android library plugin, Kotlin +
|
||||||
|
* Android SDK defaults, lint, formatting, and shared test dependencies. Compose is added by
|
||||||
|
* [AndroidLibraryComposeConventionPlugin].
|
||||||
|
*/
|
||||||
|
class AndroidLibraryConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
apply(plugin = "com.android.library")
|
||||||
|
apply(plugin = "mcl.android.lint")
|
||||||
|
|
||||||
|
extensions.configure<LibraryExtension> {
|
||||||
|
configureKotlinAndroid(this)
|
||||||
|
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
configureSpotless()
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
"testImplementation"(libs.findLibrary("junit").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
import com.android.build.api.dsl.LibraryExtension
|
||||||
|
import com.android.build.api.dsl.Lint
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project-wide Android Lint configuration, applied to application and library modules.
|
||||||
|
*/
|
||||||
|
class AndroidLintConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
when {
|
||||||
|
pluginManager.hasPlugin("com.android.application") ->
|
||||||
|
configure<ApplicationExtension> { lint(Lint::configure) }
|
||||||
|
pluginManager.hasPlugin("com.android.library") ->
|
||||||
|
configure<LibraryExtension> { lint(Lint::configure) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Lint.configure() {
|
||||||
|
checkDependencies = true
|
||||||
|
disable += "GradleDependency"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import androidx.room.gradle.RoomExtension
|
||||||
|
import com.google.devtools.ksp.gradle.KspExtension
|
||||||
|
import com.mingyuan.androidclient.buildlogic.libs
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies Room (via KSP) and wires runtime/ktx/compiler, generating Kotlin code and storing
|
||||||
|
* database schemas for future auto-migrations.
|
||||||
|
*/
|
||||||
|
class AndroidRoomConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
apply(plugin = "androidx.room")
|
||||||
|
apply(plugin = "com.google.devtools.ksp")
|
||||||
|
|
||||||
|
extensions.configure<KspExtension> {
|
||||||
|
arg("room.generateKotlin", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions.configure<RoomExtension> {
|
||||||
|
schemaDirectory("$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
"implementation"(libs.findLibrary("androidx-room-runtime").get())
|
||||||
|
"implementation"(libs.findLibrary("androidx-room-ktx").get())
|
||||||
|
"ksp"(libs.findLibrary("androidx-room-compiler").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import com.mingyuan.androidclient.buildlogic.configureKotlinJvm
|
||||||
|
import com.mingyuan.androidclient.buildlogic.configureSpotless
|
||||||
|
import com.mingyuan.androidclient.buildlogic.libs
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convention for pure Kotlin/JVM modules (no Android dependency) such as :core:model and
|
||||||
|
* :core:domain. Keeping these modules Android-free keeps them fast to test and forces domain
|
||||||
|
* logic to stay framework-agnostic.
|
||||||
|
*/
|
||||||
|
class JvmLibraryConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||||
|
configureKotlinJvm()
|
||||||
|
configureSpotless()
|
||||||
|
dependencies {
|
||||||
|
"testImplementation"(libs.findLibrary("junit").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
build-logic/convention/src/main/kotlin/RootPlugin.kt
Normal file
89
build-logic/convention/src/main/kotlin/RootPlugin.kt
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import org.gradle.api.GradleException
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.register
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root convention plugin. Registers the `verifyArchitecture` task, which enforces the module
|
||||||
|
* dependency direction declared in `docs/architecture.md`.
|
||||||
|
*
|
||||||
|
* The allow-list lives in ONE place here; adding a module means adding a line to [allowList]
|
||||||
|
* (and mirroring it in the architecture doc). The check reads each module's `build.gradle.kts`
|
||||||
|
* as a file (config-cache safe) and rejects any `project(":...")` reference that is not allowed.
|
||||||
|
*/
|
||||||
|
class RootPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
val featureAllowed = setOf(
|
||||||
|
":core:ui",
|
||||||
|
":core:designsystem",
|
||||||
|
":core:domain",
|
||||||
|
":core:model",
|
||||||
|
)
|
||||||
|
|
||||||
|
val allowList = mapOf(
|
||||||
|
":app" to setOf(
|
||||||
|
":core:common", ":core:model", ":core:domain", ":core:designsystem",
|
||||||
|
":core:ui", ":core:data", ":core:network", ":core:database", ":core:datastore",
|
||||||
|
":feature:auth", ":feature:checkin", ":feature:plan", ":feature:course",
|
||||||
|
":feature:survey", ":feature:feedback", ":feature:profile",
|
||||||
|
),
|
||||||
|
":core:common" to emptySet(),
|
||||||
|
":core:model" to emptySet(),
|
||||||
|
":core:domain" to setOf(":core:common", ":core:model"),
|
||||||
|
":core:designsystem" to setOf(":core:common"),
|
||||||
|
":core:ui" to setOf(":core:designsystem", ":core:model"),
|
||||||
|
":core:network" to setOf(":core:common", ":core:model"),
|
||||||
|
":core:database" to setOf(":core:model"),
|
||||||
|
":core:datastore" to setOf(":core:model"),
|
||||||
|
":core:data" to setOf(
|
||||||
|
":core:domain", ":core:model", ":core:network", ":core:database", ":core:datastore",
|
||||||
|
),
|
||||||
|
":feature:auth" to featureAllowed,
|
||||||
|
":feature:checkin" to featureAllowed,
|
||||||
|
":feature:plan" to featureAllowed,
|
||||||
|
":feature:course" to featureAllowed,
|
||||||
|
":feature:survey" to featureAllowed,
|
||||||
|
":feature:feedback" to featureAllowed,
|
||||||
|
":feature:profile" to featureAllowed,
|
||||||
|
)
|
||||||
|
|
||||||
|
val rootDir = target.rootDir
|
||||||
|
|
||||||
|
target.tasks.register("verifyArchitecture") {
|
||||||
|
group = "verification"
|
||||||
|
description = "Enforce module dependency direction (see docs/architecture.md)"
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val projectDependency = Regex("""project\("(:[^"]+)"\)""")
|
||||||
|
val violations = mutableListOf<String>()
|
||||||
|
|
||||||
|
for ((modulePath, allowed) in allowList) {
|
||||||
|
val buildFile = File(
|
||||||
|
rootDir,
|
||||||
|
"${modulePath.removePrefix(":").replace(":", "/")}/build.gradle.kts",
|
||||||
|
)
|
||||||
|
if (!buildFile.exists()) {
|
||||||
|
violations += "$modulePath: missing build.gradle.kts"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val declared = projectDependency
|
||||||
|
.findAll(buildFile.readText())
|
||||||
|
.map { it.groupValues[1] }
|
||||||
|
.toSet()
|
||||||
|
for (dep in declared) {
|
||||||
|
if (dep !in allowed) violations += "$modulePath -> $dep"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.isNotEmpty()) {
|
||||||
|
throw GradleException(
|
||||||
|
"Architecture violations (see docs/architecture.md):\n" +
|
||||||
|
violations.joinToString("\n") { " - $it" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
logger.lifecycle("Architecture check passed: no forbidden module dependencies.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.mingyuan.androidclient.buildlogic
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.CommonExtension
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Compose-specific options for an Android module: enable the Compose build feature,
|
||||||
|
* add the Compose BOM so all Compose artifacts share one version, and wire the preview/tooling
|
||||||
|
* dependencies used by Android Studio.
|
||||||
|
*/
|
||||||
|
internal fun Project.configureAndroidCompose(commonExtension: CommonExtension) {
|
||||||
|
commonExtension.apply {
|
||||||
|
buildFeatures.apply {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
val bom = libs.findLibrary("androidx-compose-bom").get()
|
||||||
|
"implementation"(platform(bom))
|
||||||
|
"androidTestImplementation"(platform(bom))
|
||||||
|
"implementation"(libs.findLibrary("androidx-compose-ui").get())
|
||||||
|
"implementation"(libs.findLibrary("androidx-compose-ui-graphics").get())
|
||||||
|
"implementation"(libs.findLibrary("androidx-compose-material3").get())
|
||||||
|
"implementation"(libs.findLibrary("androidx-compose-ui-tooling-preview").get())
|
||||||
|
"debugImplementation"(libs.findLibrary("androidx-compose-ui-tooling").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.mingyuan.androidclient.buildlogic
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.CommonExtension
|
||||||
|
import org.gradle.api.JavaVersion
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.plugins.JavaPluginExtension
|
||||||
|
import org.gradle.kotlin.dsl.assign
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure base Kotlin + Android options shared by every Android module.
|
||||||
|
*
|
||||||
|
* compileSdk / minSdk are the project-wide contract (see docs/architecture.md); Java/Kotlin
|
||||||
|
* target stays at 11 so library modules and the app agree on bytecode level.
|
||||||
|
*/
|
||||||
|
internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension) {
|
||||||
|
commonExtension.apply {
|
||||||
|
compileSdk = 37
|
||||||
|
|
||||||
|
defaultConfig.apply {
|
||||||
|
minSdk = 28
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions.apply {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configureKotlin<KotlinAndroidProjectExtension>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure base Kotlin options for pure JVM (non-Android) modules such as :core:model.
|
||||||
|
*/
|
||||||
|
internal fun Project.configureKotlinJvm() {
|
||||||
|
extensions.configure<JavaPluginExtension> {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
configureKotlin<KotlinJvmProjectExtension>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() = configure<T> {
|
||||||
|
// Treat Kotlin warnings as errors only when the `warningsAsErrors` Gradle property is set.
|
||||||
|
val warningsAsErrors = providers.gradleProperty("warningsAsErrors").map {
|
||||||
|
it.toBoolean()
|
||||||
|
}.orElse(false)
|
||||||
|
when (this) {
|
||||||
|
is KotlinAndroidProjectExtension -> compilerOptions
|
||||||
|
is KotlinJvmProjectExtension -> compilerOptions
|
||||||
|
else -> TODO("Unsupported project extension $this ${T::class}")
|
||||||
|
}.apply {
|
||||||
|
jvmTarget = JvmTarget.JVM_11
|
||||||
|
allWarningsAsErrors = warningsAsErrors
|
||||||
|
freeCompilerArgs.add(
|
||||||
|
// Enable experimental coroutines APIs, including Flow.
|
||||||
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.mingyuan.androidclient.buildlogic
|
||||||
|
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.VersionCatalog
|
||||||
|
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||||
|
import org.gradle.kotlin.dsl.getByType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access to the shared `libs` version catalog from within convention plugins.
|
||||||
|
*
|
||||||
|
* The catalog is declared in `gradle/libs.versions.toml` and wired into the included
|
||||||
|
* build via `build-logic/settings.gradle.kts`, so build-logic and the main build share
|
||||||
|
* one source of truth for versions.
|
||||||
|
*/
|
||||||
|
val Project.libs
|
||||||
|
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.mingyuan.androidclient.buildlogic
|
||||||
|
|
||||||
|
import com.diffplug.gradle.spotless.SpotlessExtension
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforce deterministic Kotlin formatting with ktlint (rules driven by `.editorconfig`).
|
||||||
|
*
|
||||||
|
* No license header is applied — this is a private project. Formatting only; run
|
||||||
|
* `./gradlew spotlessCheck` (verify) or `./gradlew spotlessApply` (fix).
|
||||||
|
*/
|
||||||
|
internal fun Project.configureSpotless() {
|
||||||
|
apply(plugin = "com.diffplug.spotless")
|
||||||
|
extensions.configure<SpotlessExtension> {
|
||||||
|
kotlin {
|
||||||
|
target("src/**/*.kt")
|
||||||
|
ktlint(libs.findVersion("ktlint").get().requiredVersion).editorConfigOverride(
|
||||||
|
mapOf(
|
||||||
|
"android" to "true",
|
||||||
|
// 包名 com.mingyuan.android_client 含下划线,刻意保留(见 docs/decisions/0004)。
|
||||||
|
"ktlint_standard_package-name" to "disabled",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
build-logic/gradle.properties
Normal file
5
build-logic/gradle.properties
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
org.gradle.configuration-cache.parallel=true
|
||||||
27
build-logic/settings.gradle.kts
Normal file
27
build-logic/settings.gradle.kts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
versionCatalogs {
|
||||||
|
create("libs") {
|
||||||
|
from(files("../gradle/libs.versions.toml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "build-logic"
|
||||||
|
include(":convention")
|
||||||
Reference in New Issue
Block a user