Cleanup and rework build logic (#3113)

This commit is contained in:
AntsyLich
2026-03-22 01:34:45 +06:00
committed by GitHub
parent 2366fbb4b3
commit 4af8f1eab0
65 changed files with 680 additions and 590 deletions
+75
View File
@@ -0,0 +1,75 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.samWithReceiver)
alias(libs.plugins.spotless)
`java-gradle-plugin`
}
// Configuration should be synced with [/gradle/build-logic/src/main/kotlin/PluginSpotless.kt]
val ktlintVersion = libs.ktlint.bom.get().version
val editorConfigFile = rootProject.file("../../.editorconfig")
spotless {
kotlin {
target("src/**/*.kt")
ktlint(ktlintVersion).setEditorConfigPath(editorConfigFile)
trimTrailingWhitespace()
endWithNewline()
}
kotlinGradle {
target("*.kts")
ktlint(ktlintVersion).setEditorConfigPath(editorConfigFile)
trimTrailingWhitespace()
endWithNewline()
}
}
dependencies {
compileOnly(gradleKotlinDsl())
compileOnly(libs.android.gradle)
compileOnly(libs.kotlin.compose.compiler.gradle)
compileOnly(libs.kotlin.gradle)
implementation(libs.spotless.gradle)
implementation(libs.tapmoc.gradle)
// These allow us to reference the dependency catalog inside our compiled plugins
compileOnly(files(libs::class.java.superclass.protectionDomain.codeSource.location))
compileOnly(files(mihonx::class.java.superclass.protectionDomain.codeSource.location))
}
samWithReceiver {
annotation("org.gradle.api.HasImplicitReceiver")
}
gradlePlugin {
plugins {
register("android-application") {
id = mihonx.plugins.android.application.get().pluginId
implementationClass = "PluginAndroidApplication"
}
register("android-base") {
id = mihonx.plugins.android.base.get().pluginId
implementationClass = "PluginAndroidBase"
}
register("android-library") {
id = mihonx.plugins.android.library.get().pluginId
implementationClass = "PluginAndroidLibrary"
}
register("android-test") {
id = mihonx.plugins.android.test.get().pluginId
implementationClass = "PluginAndroidTest"
}
register("compose-android") {
id = mihonx.plugins.compose.get().pluginId
implementationClass = "PluginComposeAndroid"
}
register("kotlin-multiplatform") {
id = mihonx.plugins.kotlin.multiplatform.get().pluginId
implementationClass = "PluginKotlinMultiplatform"
}
register("spotless") {
id = mihonx.plugins.spotless.get().pluginId
implementationClass = "PluginSpotless"
}
}
}
+18
View File
@@ -0,0 +1,18 @@
dependencyResolutionManagement {
@Suppress("UnstableApiUsage")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
versionCatalogs {
create("libs") {
from(files("../libs.versions.toml"))
}
create("mihonx") {
from(files("../mihon.versions.toml"))
}
}
}
rootProject.name = "build-logic"
@@ -0,0 +1,16 @@
import mihon.gradle.extensions.alias
import mihon.gradle.extensions.libs
import mihon.gradle.extensions.mihonx
import mihon.gradle.extensions.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
@Suppress("UNUSED")
class PluginAndroidApplication : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins {
alias(libs.plugins.android.application)
alias(mihonx.plugins.android.base)
}
}
}
@@ -0,0 +1,21 @@
import mihon.gradle.configurations.configureAndroid
import mihon.gradle.configurations.configureKotlin
import mihon.gradle.extensions.alias
import mihon.gradle.extensions.configureTest
import mihon.gradle.extensions.libs
import mihon.gradle.extensions.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
@Suppress("UNUSED")
class PluginAndroidBase : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins {
alias(libs.plugins.kotlin.android)
}
configureKotlin()
configureTest()
configureAndroid()
}
}
@@ -0,0 +1,16 @@
import mihon.gradle.extensions.alias
import mihon.gradle.extensions.libs
import mihon.gradle.extensions.mihonx
import mihon.gradle.extensions.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
@Suppress("UNUSED")
class PluginAndroidLibrary : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins {
alias(libs.plugins.android.library)
alias(mihonx.plugins.android.base)
}
}
}
@@ -0,0 +1,16 @@
import mihon.gradle.extensions.alias
import mihon.gradle.extensions.libs
import mihon.gradle.extensions.mihonx
import mihon.gradle.extensions.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
@Suppress("UNUSED")
class PluginAndroidTest : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins {
alias(libs.plugins.android.test)
alias(mihonx.plugins.android.base)
}
}
}
@@ -0,0 +1,33 @@
import mihon.gradle.extensions.alias
import mihon.gradle.extensions.android
import mihon.gradle.extensions.api
import mihon.gradle.extensions.debugApi
import mihon.gradle.extensions.implementation
import mihon.gradle.extensions.libs
import mihon.gradle.extensions.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
@Suppress("UNUSED")
class PluginComposeAndroid : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins {
alias(libs.plugins.kotlin.compose.compiler)
}
android {
buildFeatures {
compose = true
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
// Compose @Preview tooling
api(libs.androidx.compose.uiToolingPreview)
debugApi(libs.androidx.compose.uiTooling)
}
}
}
@@ -0,0 +1,36 @@
import mihon.gradle.configurations.configureAndroid
import mihon.gradle.configurations.configureKotlin
import mihon.gradle.extensions.alias
import mihon.gradle.extensions.configureTest
import mihon.gradle.extensions.libs
import mihon.gradle.extensions.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
@Suppress("UNUSED")
class PluginKotlinMultiplatform : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.multiplatform)
}
configureKotlin()
configureTest()
configureAndroid()
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate()
androidTarget()
}
}
}
private fun Project.kotlin(block: KotlinMultiplatformExtension.() -> Unit) {
extensions.configure(block)
}
@@ -0,0 +1,44 @@
import com.diffplug.gradle.spotless.SpotlessExtension
import mihon.gradle.extensions.alias
import mihon.gradle.extensions.libs
import mihon.gradle.extensions.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
@Suppress("UNUSED")
class PluginSpotless : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins {
alias(libs.plugins.spotless)
}
// Configuration should be synced with [/gradle/build-logic/build.gradle.kts]
val ktlintVersion = libs.ktlint.bom.get().version
spotless {
kotlin {
target("src/**/*.kt")
ktlint(ktlintVersion)
trimTrailingWhitespace()
endWithNewline()
}
kotlinGradle {
target("*.kts")
ktlint(ktlintVersion)
trimTrailingWhitespace()
endWithNewline()
}
format("xml") {
target("src/**/*.xml")
trimTrailingWhitespace()
endWithNewline()
}
}
}
}
private fun Project.spotless(block: SpotlessExtension.() -> Unit) {
extensions.configure(block)
}
@@ -0,0 +1,17 @@
package mihon.gradle
import org.gradle.api.Project
interface BuildConfig {
val includeTelemetry: Boolean
val enableUpdater: Boolean
val enableCodeShrink: Boolean
val includeDependencyInfo: Boolean
}
val Project.Config: BuildConfig get() = object : BuildConfig {
override val includeTelemetry: Boolean = project.hasProperty("include-telemetry")
override val enableUpdater: Boolean = project.hasProperty("enable-updater")
override val enableCodeShrink: Boolean = !project.hasProperty("disable-code-shrink")
override val includeDependencyInfo: Boolean = project.hasProperty("include-dependency-info")
}
@@ -0,0 +1,45 @@
package mihon.gradle
import org.gradle.api.Project
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
// Git is needed in your system PATH for these commands to work.
// If it's not installed, you can return a random value as a workaround
fun Project.getLatestCommitCount(): String {
return exec("git rev-list --count HEAD")
// return "1"
}
fun Project.getLatestCommitSha(): String {
return exec("git rev-parse --short HEAD")
// return "1"
}
private val BUILD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
/**
* @param useLatestCommitTime If `true`, the build time is based on the timestamp of the last Git commit;
* otherwise, the current time is used. Both are in UTC.
* @return A formatted string representing the build time. The format used is defined by [BUILD_TIME_FORMATTER].
*/
fun Project.getBuildTime(useLatestCommitTime: Boolean): String {
return if (useLatestCommitTime) {
val epoch = exec("git log -1 --format=%ct").toLong()
Instant.ofEpochSecond(epoch).atOffset(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
} else {
LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
}
}
fun Project.exec(command: String): String {
return providers.exec {
commandLine = command.split(" ")
}
.standardOutput
.asText
.get()
.trim()
}
@@ -0,0 +1,32 @@
package mihon.gradle.configurations
import com.android.build.api.dsl.ApplicationDefaultConfig
import mihon.gradle.extensions.android
import mihon.gradle.extensions.coreLibraryDesugaring
import mihon.gradle.extensions.libs
import mihon.gradle.extensions.mihonx
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
fun Project.configureAndroid() {
android {
defaultConfig {
minSdk = mihonx.versions.android.sdk.min.get().toInt()
if (this is ApplicationDefaultConfig) {
targetSdk = mihonx.versions.android.sdk.target.get().toInt()
}
ndkVersion = mihonx.versions.android.ndk.get()
}
compileSdk = mihonx.versions.android.sdk.compile.get().toInt()
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring(libs.android.desugar)
}
}
@@ -0,0 +1,27 @@
package mihon.gradle.configurations
import mihon.gradle.extensions.mihonx
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.HasConfigurableKotlinCompilerOptions
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonCompilerOptions
import tapmoc.configureJavaCompatibility
fun Project.configureKotlin() {
configureJavaCompatibility(mihonx.versions.java.get().toInt())
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-parameters")
}
}
}
private fun Project.kotlin(block: KotlinBaseExtension.() -> Unit) {
extensions.configure(block)
}
private fun KotlinBaseExtension.compilerOptions(block: KotlinCommonCompilerOptions.() -> Unit) {
if (this is HasConfigurableKotlinCompilerOptions<*>) compilerOptions(block)
}
@@ -0,0 +1,32 @@
package mihon.gradle.extensions
import org.gradle.api.Project
import org.gradle.api.artifacts.ExternalModuleDependencyBundle
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.DependencyHandlerScope
fun DependencyHandlerScope.api(dependencyNotation: Provider<MinimalExternalModuleDependency>) {
add("api", dependencyNotation)
}
fun DependencyHandlerScope.coreLibraryDesugaring(dependencyNotation: Provider<MinimalExternalModuleDependency>) {
add("coreLibraryDesugaring", dependencyNotation)
}
fun DependencyHandlerScope.debugApi(dependencyNotation: Provider<MinimalExternalModuleDependency>) {
add("debugApi", dependencyNotation)
}
@JvmName("implementationBundle")
fun DependencyHandlerScope.implementation(dependencyNotation: Provider<ExternalModuleDependencyBundle>) {
add("implementation", dependencyNotation)
}
fun DependencyHandlerScope.implementation(dependencyNotation: Provider<MinimalExternalModuleDependency>) {
add("implementation", dependencyNotation)
}
fun DependencyHandlerScope.implementation(dependencyNotation: Project) {
add("implementation", dependencyNotation)
}
@@ -0,0 +1,9 @@
package mihon.gradle.extensions
import org.gradle.api.plugins.PluginManager
import org.gradle.api.provider.Provider
import org.gradle.plugin.use.PluginDependency
fun PluginManager.alias(notation: Provider<PluginDependency>) {
apply(notation.get().pluginId)
}
@@ -0,0 +1,35 @@
package mihon.gradle.extensions
import com.android.build.api.dsl.CommonExtension
import com.android.build.gradle.BaseExtension
import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.accessors.dm.LibrariesForMihonx
import org.gradle.api.Project
import org.gradle.api.plugins.PluginManager
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.the
import org.gradle.kotlin.dsl.withType
internal val Project.libs get() = the<LibrariesForLibs>()
internal val Project.mihonx get() = the<LibrariesForMihonx>()
internal fun Project.plugins(block: PluginManager.() -> Unit) {
pluginManager.apply(block)
}
internal fun Project.android(block: CommonExtension<*, *, *, *, *, *>.() -> Unit) {
extensions.configure<BaseExtension> {
if (this is CommonExtension<*, *, *, *, *, *>) apply(block)
}
}
fun Project.configureTest() {
tasks.withType<Test> {
useJUnitPlatform()
testLogging {
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
}
}
@@ -0,0 +1,46 @@
package mihon.gradle.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
abstract class GenerateLocalesConfigTask : DefaultTask() {
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun action() {
val locales = project.fileTree("src/commonMain/moko-resources") {
matching { include("**/strings.xml") }
}
.asSequence()
.filterNot { it.readText().contains(emptyResourcesElement) }
.map {
it.parentFile.name
.replace("base", "en")
.replace("-r", "-")
.replace("+", "-")
}
.distinct()
.sorted()
.joinToString("\n") { "| <locale android:name=\"$it\"/>" }
val content = """
|<?xml version="1.0" encoding="utf-8"?>
|<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
$locales
|</locale-config>
""".trimMargin()
outputDir.get().file("xml/locales_config.xml").asFile.apply {
parentFile.mkdirs()
writeText(content)
}
}
companion object {
private val emptyResourcesElement = "<resources>\\s*</resources>|<resources\\s*/>".toRegex()
}
}
@@ -0,0 +1,34 @@
package mihon.gradle.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
abstract class ReplaceShortcutsPlaceholderTask : DefaultTask() {
@get:Input
abstract val applicationId: Property<String>
@get:InputFile
abstract val shortcutsFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun action() {
val content = shortcutsFile.asFile.get()
.readText()
.replace($$"${applicationId}", applicationId.get())
outputDir.get().file("xml/shortcuts.xml").asFile.apply {
parentFile.mkdirs()
writeText(content)
}
}
}
+9 -2
View File
@@ -2,7 +2,6 @@
aboutLibraries = "13.2.1"
android-desugar = "2.1.5"
android-gradle = "8.13.2"
android-shortcut = "0.1.2"
androidx-activity-compose = "1.13.0"
androidx-annotation = "1.9.1"
androidx-appCompat = "1.7.1"
@@ -68,6 +67,7 @@ sqldelight-androidx-driver = "0.0.17"
stringSimilarity = "0.1.0"
subsamplingScaleImageView = "66e0db195d"
swipe = "1.3.0"
tapmoc = "0.4.0"
unifile = "e0def6b3dc"
voyager = "1.1.0-beta03"
xmlutil = "0.91.3"
@@ -76,7 +76,6 @@ xmlutil = "0.91.3"
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
android-desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" }
android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
android-shortcut-gradle = { module = "com.github.zellius:android-shortcut-gradle-plugin", version.ref = "android-shortcut" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
androidx-appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appCompat" }
@@ -175,6 +174,7 @@ sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialec
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
subsamplingScaleImageView = { module = "com.github.tachiyomiorg:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" }
swipe = { module = "me.saket.swipe:swipe", version.ref = "swipe" }
tapmoc-gradle = { module = "com.gradleup.tapmoc:tapmoc-gradle-plugin", version.ref = "tapmoc" }
unifile = { module = "com.github.tachiyomiorg:unifile", version.ref = "unifile" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenModel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
@@ -185,12 +185,19 @@ xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", v
[plugins]
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" }
android-application = { id = "com.android.application", version.ref = "android-gradle" }
android-library = { id = "com.android.library", version.ref = "android-gradle" }
android-test = { id = "com.android.test", version.ref = "android-gradle" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" }
google-services = { id = "com.google.gms.google-services", version.ref = "google-services" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-gradle" }
kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-gradle" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-gradle" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-gradle" }
kotlin-samWithReceiver = { id = "org.jetbrains.kotlin.plugin.sam.with.receiver", version.ref = "kotlin-gradle" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-gradle" }
moko-resources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko-resources" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
[bundles]
+16
View File
@@ -0,0 +1,16 @@
[versions]
# Used in [/gradle/build-config/src/main/kotlin/PluginKotlinMultiplatform.kt]
android-sdk-min = "26"
android-sdk-compile = "36"
android-sdk-target = "36"
android-ndk = "29.0.14206865"
java = "17"
[plugins]
android-application = { id = "mihon.plugins.android.application" }
android-base = { id = "mihon.plugins.android.base" }
android-library = { id = "mihon.plugins.android.library" }
android-test = { id = "mihon.plugins.android.test" }
compose = { id = "mihon.plugins.compose.android" }
kotlin-multiplatform = { id = "mihon.plugins.kotlin.multiplatform" }
spotless = { id = "mihon.plugins.spotless" }