build.gradle.kts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
  2. import org.jlleitschuh.gradle.ktlint.KtlintExtension
  3. import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
  4. plugins {
  5. // alias(libs.plugins.devtools.ksp) apply false
  6. alias(libs.plugins.android.application) apply false
  7. alias(libs.plugins.android.library) apply false
  8. alias(libs.plugins.compose) apply false
  9. alias(libs.plugins.compose.compiler) apply false
  10. alias(libs.plugins.kotlin.jvm) apply false
  11. alias(libs.plugins.kotlin.multiplatform) apply false
  12. alias(libs.plugins.app.icon.generator) apply false
  13. alias(libs.plugins.qodana)
  14. // alias(libs.plugins.kover)
  15. alias(libs.plugins.changelog) apply false
  16. alias(libs.plugins.gradle.ktlint)
  17. alias(libs.plugins.manes.versions)
  18. alias(libs.plugins.kotlin.android) apply false
  19. id("AiAgentPlugin")
  20. // id("com.github.gmazzo.buildconfig") version "5.6.5"
  21. }
  22. // ============================================================================
  23. // Code quality + dependency reporting -> /reports
  24. // - ben-manes: stable-only dependency-update report (rejects alpha/rc/dev/tethys/snapshot)
  25. // - ktlint: auto-format after build + report; report-only (never breaks the build)
  26. // - After every build: auto-format code, ktlint report, and (gated) dependency report.
  27. // Dependency check is ON by default; disable with -PdepCheck=false
  28. // ============================================================================
  29. // A version is "non-stable" if it is a milestone/preview build (alpha, beta, rc,
  30. // eap, dev, snapshot, tethys, …) — we never auto-suggest those over a stable current.
  31. fun isNonStable(version: String): Boolean {
  32. val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
  33. val regex = "^[0-9,.v-]+(-r)?$".toRegex()
  34. return !(stableKeyword || regex.matches(version))
  35. }
  36. val depCheckEnabled: Boolean = (findProperty("depCheck") as String?)?.toBooleanStrictOrNull() ?: true
  37. val reportsDir: Directory = rootProject.layout.projectDirectory.dir("reports")
  38. allprojects {
  39. apply(plugin = "com.github.ben-manes.versions")
  40. apply(plugin = "org.jlleitschuh.gradle.ktlint")
  41. extensions.configure<KtlintExtension> {
  42. ignoreFailures.set(true) // report only — never fail the build
  43. android.set(true)
  44. filter {
  45. // never lint/format generated or build output (compose resources, KSP, etc.)
  46. exclude { it.file.path.contains("${File.separator}build${File.separator}") }
  47. exclude { it.file.path.contains("generated") }
  48. }
  49. reporters {
  50. reporter(ReporterType.PLAIN)
  51. reporter(ReporterType.HTML)
  52. reporter(ReporterType.CHECKSTYLE)
  53. }
  54. }
  55. tasks.withType<DependencyUpdatesTask>().configureEach {
  56. rejectVersionIf { isNonStable(candidate.version) && !isNonStable(currentVersion) }
  57. outputDir = reportsDir.dir("dependencies").asFile.absolutePath
  58. reportfileName = "dependency-updates"
  59. outputFormatter = "plain,html"
  60. }
  61. }
  62. // Collect ktlint reports from every module into /reports/ktlint/<module>
  63. val collectKtlintReports by tasks.registering(Copy::class) {
  64. group = "reporting"
  65. description = "Collects ktlint reports from all modules into /reports/ktlint"
  66. allprojects.forEach { p ->
  67. from(p.layout.buildDirectory.dir("reports/ktlint")) { into(p.name) }
  68. }
  69. into(reportsDir.dir("ktlint"))
  70. duplicatesStrategy = DuplicatesStrategy.INCLUDE
  71. // ensure reports are produced before we collect them
  72. mustRunAfter(allprojects.flatMap { p -> p.tasks.matching { it.name == "ktlintCheck" || it.name == "ktlintFormat" } })
  73. }
  74. // Runs after every build: auto-format, ktlint report, and (gated) dependency-update report -> /reports
  75. val postBuildCodeCheck by tasks.registering {
  76. group = "verification"
  77. description = "Auto-formats code (ktlintFormat) and runs the gated dependency-update report into /reports"
  78. // Auto-format on build. Safe because .editorconfig disables no-unused-imports — the one
  79. // rule that stripped receiver/extension-member imports and broke compilation.
  80. dependsOn(":composeApp:ktlintFormat")
  81. if (depCheckEnabled) dependsOn(":composeApp:dependencyUpdates")
  82. finalizedBy(collectKtlintReports)
  83. }
  84. allprojects {
  85. tasks.matching { it.name == "build" }.configureEach {
  86. finalizedBy(rootProject.tasks.named("postBuildCodeCheck"))
  87. }
  88. }
  89. //buildConfig {
  90. // generateAtSync = true
  91. // sourceSets {
  92. // main {
  93. // packageName = "org.mjdev.desktop"
  94. // useKotlin = true
  95. // useJava = true
  96. // useGroovy = true
  97. // useScala = true
  98. // useXml = true
  99. // useProperties = true
  100. // }
  101. // }
  102. // buildConfigField("APP_NAME", project.name)
  103. // buildConfigField("APP_VERSION", provider { "\"${project.version}\"" })
  104. // buildConfigField("APP_SECRET", "Z3JhZGxlLWphdmEtYnVpbGRjb25maWctcGx1Z2lu")
  105. // buildConfigField<String>("OPTIONAL", null)
  106. // buildConfigField("BUILD_TIME", System.currentTimeMillis())
  107. // buildConfigField("FEATURE_ENABLED", true)
  108. // buildConfigField("MAGIC_NUMBERS", intArrayOf(1, 2, 3, 4))
  109. // buildConfigField("STRING_LIST", arrayOf("a", "b", "c"))
  110. // buildConfigField("MAP", mapOf("a" to 1, "b" to 2))
  111. // buildConfigField("FILE", File("aFile"))
  112. // buildConfigField("URI", uri("https://example.io"))
  113. // buildConfigField("com.github.gmazzo.buildconfig.demos.kts.SomeData", "DATA", "SomeData(\"a\", 1)")
  114. //}
  115. //dependencies {
  116. // kover(project(":composeApp"))
  117. //}
  118. //kover { reports { total { xml { onCheck = true } } } }
  119. // Aggregate task: build every distributable that this host can produce.
  120. // - desktop: Deb + AppImage + Rpm (per the targetFormats in composeApp), release mode
  121. // - android: release APK (assembleRelease)
  122. // - ios: only when an iOS Kotlin target is configured AND the host is macOS
  123. // (Kotlin/Native for iOS cannot build on Linux/Windows) — wired below if present.
  124. // captured at configuration time -> all the tasks below stay configuration-cache safe
  125. val appNameV = libs.versions.app.name.get()
  126. val pkgDirV = rootDir.resolve("packages/main-release")
  127. val appImageToolPath = rootDir.resolve(".gradle/tools/appimagetool-x86_64.AppImage").absolutePath
  128. // Downloads appimagetool before the build (only if not already present / on PATH).
  129. val ensureAppImageTool = tasks.register<EnsureAppImageToolTask>("ensureAppImageTool") {
  130. group = "mjdev"
  131. description = "Downloads appimagetool (single-file .AppImage builder) if not already present."
  132. toolPath.set(appImageToolPath)
  133. }
  134. // Wraps the jpackage app-image directory into a single, CLI-runnable .AppImage file.
  135. val packageAppImageFile = tasks.register<PackageAppImageTask>("packageAppImageFile") {
  136. group = "mjdev"
  137. description = "Builds a single-file .AppImage from the jpackage app-image (skips if appimagetool unavailable)."
  138. dependsOn(ensureAppImageTool, ":composeApp:packageReleaseAppImage")
  139. appName.set(appNameV)
  140. appImagePath.set(pkgDirV.resolve("app/$appNameV").absolutePath)
  141. outputPath.set(pkgDirV.resolve("appimage/$appNameV.AppImage").absolutePath)
  142. toolPath.set(appImageToolPath)
  143. }
  144. // Collects all distributables into releases/ with stable (version-less) names so each
  145. // build overwrites the previous file. Declarative Sync = configuration-cache safe.
  146. val collectReleases = tasks.register<Sync>("collectReleases") {
  147. group = "mjdev"
  148. description = "Copies the built distributables into releases/ (stable names, overwrites)."
  149. dependsOn(
  150. ":composeApp:packageReleaseDistributionForCurrentOS",
  151. ":composeApp:assembleRelease",
  152. packageAppImageFile,
  153. )
  154. // local copies so the rename closures capture plain Strings (configuration-cache safe;
  155. // capturing the script-level vals makes the closure hold a null script reference on reload)
  156. val name = appNameV
  157. into(layout.projectDirectory.dir("releases"))
  158. from(pkgDirV.resolve("deb")) { include("*.deb"); rename { "$name.deb" } }
  159. from(pkgDirV.resolve("rpm")) { include("*.rpm"); rename { "$name.rpm" } }
  160. from(pkgDirV.resolve("exe")) { include("*.exe"); rename { "$name.exe" } } // present only on a Windows host
  161. from(pkgDirV.resolve("appimage")) { include("*.AppImage") } // already named <app>.AppImage
  162. from(rootDir.resolve("composeApp/build/outputs/apk/release")) {
  163. include("*.apk"); rename { "$name.apk" }
  164. }
  165. }
  166. val buildAll = tasks.register("buildAll") {
  167. group = "mjdev"
  168. description = "Builds all distributables this host can produce, collects them into releases/ (stable names), and generates reports into reports/ — like every build."
  169. dependsOn(collectReleases, postBuildCodeCheck)
  170. }
  171. // Attach iOS framework build only when an iOS target actually exists in composeApp
  172. // (it never does on a Linux/Windows host — Kotlin/Native iOS requires macOS + Xcode).
  173. project(":composeApp").afterEvaluate {
  174. val iosTask = tasks.names.firstOrNull {
  175. it.startsWith("linkReleaseFrameworkIos") || it == "embedAndSignAppleFrameworkForXcode"
  176. }
  177. if (iosTask != null) {
  178. buildAll.configure { dependsOn("${this@afterEvaluate.path}:$iosTask") }
  179. logger.lifecycle("buildAll: iOS target found — wiring ${this@afterEvaluate.path}:$iosTask")
  180. } else {
  181. logger.info("buildAll: no iOS target configured (needs macOS + Xcode + an ios* target in composeApp) — iOS skipped")
  182. }
  183. }