diff --git a/build-logic/convention/DESIGN_NOTES.md b/build-logic/convention/DESIGN_NOTES.md new file mode 100644 index 0000000..4a3bdaf --- /dev/null +++ b/build-logic/convention/DESIGN_NOTES.md @@ -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`。 diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts new file mode 100644 index 0000000..92c699e --- /dev/null +++ b/build-logic/convention/build.gradle.kts @@ -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" + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt new file mode 100644 index 0000000..e2fcb00 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -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 { + override fun apply(target: Project) { + with(target) { + apply(plugin = "mcl.android.application") + apply(plugin = "org.jetbrains.kotlin.plugin.compose") + + configureAndroidCompose(extensions.getByType()) + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt new file mode 100644 index 0000000..ad40f84 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -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 { + override fun apply(target: Project) { + with(target) { + apply(plugin = "com.android.application") + apply(plugin = "mcl.android.lint") + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = 37 + defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testOptions.animationsDisabled = true + } + + configureSpotless() + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt new file mode 100644 index 0000000..95e4240 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -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 { + 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()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt new file mode 100644 index 0000000..f7a7500 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt @@ -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 { + 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()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt new file mode 100644 index 0000000..8bc0c31 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -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 { + override fun apply(target: Project) { + with(target) { + apply(plugin = "mcl.android.library") + apply(plugin = "org.jetbrains.kotlin.plugin.compose") + + configureAndroidCompose(extensions.getByType()) + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt new file mode 100644 index 0000000..f678bab --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -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 { + override fun apply(target: Project) { + with(target) { + apply(plugin = "com.android.library") + apply(plugin = "mcl.android.lint") + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + configureSpotless() + + dependencies { + "testImplementation"(libs.findLibrary("junit").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt new file mode 100644 index 0000000..7aad1f9 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt @@ -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 { + override fun apply(target: Project) { + with(target) { + when { + pluginManager.hasPlugin("com.android.application") -> + configure { lint(Lint::configure) } + pluginManager.hasPlugin("com.android.library") -> + configure { lint(Lint::configure) } + } + } + } +} + +private fun Lint.configure() { + checkDependencies = true + disable += "GradleDependency" +} diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt new file mode 100644 index 0000000..4540e14 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt @@ -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 { + override fun apply(target: Project) { + with(target) { + apply(plugin = "androidx.room") + apply(plugin = "com.google.devtools.ksp") + + extensions.configure { + arg("room.generateKotlin", "true") + } + + extensions.configure { + 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()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt new file mode 100644 index 0000000..a0da895 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt @@ -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 { + override fun apply(target: Project) { + with(target) { + apply(plugin = "org.jetbrains.kotlin.jvm") + configureKotlinJvm() + configureSpotless() + dependencies { + "testImplementation"(libs.findLibrary("junit").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/RootPlugin.kt b/build-logic/convention/src/main/kotlin/RootPlugin.kt new file mode 100644 index 0000000..8ecc872 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/RootPlugin.kt @@ -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 { + 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() + + 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.") + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/AndroidCompose.kt new file mode 100644 index 0000000..ebed3a0 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/AndroidCompose.kt @@ -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()) + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/KotlinAndroid.kt new file mode 100644 index 0000000..954a74a --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/KotlinAndroid.kt @@ -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() +} + +/** + * Configure base Kotlin options for pure JVM (non-Android) modules such as :core:model. + */ +internal fun Project.configureKotlinJvm() { + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + configureKotlin() +} + +private inline fun Project.configureKotlin() = configure { + // 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", + ) + } +} diff --git a/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/ProjectExtensions.kt new file mode 100644 index 0000000..c91a620 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/ProjectExtensions.kt @@ -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().named("libs") diff --git a/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/Spotless.kt b/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/Spotless.kt new file mode 100644 index 0000000..b3507c9 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/mingyuan/androidclient/buildlogic/Spotless.kt @@ -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 { + 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() + } + } +} diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties new file mode 100644 index 0000000..62a516a --- /dev/null +++ b/build-logic/gradle.properties @@ -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 diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..0f461ae --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -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")