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 versionV = libs.versions.app.pkg.version.get()
  127. val pkgDirV = rootDir.resolve("packages/main-release")
  128. val appImageToolPath = rootDir.resolve(".gradle/tools/appimagetool-x86_64.AppImage").absolutePath
  129. // Downloads appimagetool before the build (only if not already present / on PATH).
  130. val ensureAppImageTool = tasks.register<EnsureAppImageToolTask>("ensureAppImageTool") {
  131. group = "mjdev"
  132. description = "Downloads appimagetool (single-file .AppImage builder) if not already present."
  133. toolPath.set(appImageToolPath)
  134. }
  135. // Wraps the jpackage app-image directory into a single, CLI-runnable .AppImage file.
  136. val packageAppImageFile = tasks.register<PackageAppImageTask>("packageAppImageFile") {
  137. group = "mjdev"
  138. description = "Builds a single-file .AppImage from the jpackage app-image (skips if appimagetool unavailable)."
  139. dependsOn(ensureAppImageTool, ":composeApp:packageReleaseAppImage")
  140. appName.set(appNameV)
  141. appImagePath.set(pkgDirV.resolve("app/$appNameV").absolutePath)
  142. outputPath.set(pkgDirV.resolve("appimage/$appNameV.AppImage").absolutePath)
  143. toolPath.set(appImageToolPath)
  144. }
  145. // Copies all distributables into releases/ with version in the filename
  146. // (mjdev-desktop-<version>.<ext>). Copy = configuration-cache safe.
  147. val collectReleases = tasks.register<Copy>("collectReleases") {
  148. group = "mjdev"
  149. description = "Copies the built distributables into releases/ (versioned filenames)."
  150. dependsOn(
  151. ":composeApp:packageReleaseDistributionForCurrentOS",
  152. ":composeApp:assembleRelease",
  153. packageAppImageFile,
  154. )
  155. // local copies so the rename closures capture plain Strings (configuration-cache safe)
  156. val base = "$appNameV-$versionV"
  157. into(layout.projectDirectory.dir("releases"))
  158. from(pkgDirV.resolve("deb")) { include("*.deb"); rename { "$base.deb" } }
  159. from(pkgDirV.resolve("rpm")) { include("*.rpm"); rename { "$base.rpm" } }
  160. from(pkgDirV.resolve("exe")) { include("*.exe"); rename { "$base.exe" } } // present only on a Windows host
  161. from(pkgDirV.resolve("appimage")) { include("*.AppImage"); rename { "$base.AppImage" } }
  162. from(rootDir.resolve("composeApp/build/outputs/apk/release")) {
  163. include("*.apk"); rename { "$base.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. }