import groovy.json.JsonSlurper import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jlleitschuh.gradle.ktlint.KtlintExtension import org.jlleitschuh.gradle.ktlint.reporter.ReporterType import java.io.ByteArrayOutputStream import java.util.zip.GZIPOutputStream plugins { // Version management plugin for dependency updates alias(libs.plugins.benManesVersions) // Kotlin plugins declared here with 'apply false' to centralize version management // This prevents "plugin loaded multiple times" errors in Gradle 9.2.1+ // Subprojects apply these plugins via version catalog: alias(libs.plugins.kotlinJvm) alias(libs.plugins.kotlinJvm) apply false // KMP plugin applied as 'apply false' to avoid root project conflict alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.kotlinSerialization) apply false alias(libs.plugins.kotlinSpring) apply false alias(libs.plugins.kotlinJpa) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.spring.boot) apply false alias(libs.plugins.spring.dependencyManagement) apply false // Dokka plugin applied at root to create multi-module collector tasks alias(libs.plugins.dokka) // Static analysis (enabled at root and inherited by subprojects) alias(libs.plugins.detekt) alias(libs.plugins.ktlint) } // ################################################################## // ### ALLPROJECTS CONFIGURATION ### // ################################################################## allprojects { group = "at.mocode" version = "1.0.0-SNAPSHOT" } subprojects { // FINALE KORREKTUR: Konsistente JVM-Target-Konfiguration für Java und Kotlin // basierend auf der Tech-Stack-Referenz. plugins.withType { extensions.configure { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(25)) } } } tasks.withType().configureEach { compilerOptions { jvmTarget.set(JvmTarget.JVM_25) freeCompilerArgs.addAll( "-opt-in=kotlin.RequiresOptIn", "-Xannotation-default-target=param-property", "-Xexpect-actual-classes", ) } } tasks.withType().configureEach { useJUnitPlatform { excludeTags("perf") } // Configure CDS in auto-mode to prevent bootstrap classpath warnings jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false") // Increase test JVM memory with a stable configuration minHeapSize = "512m" maxHeapSize = "2g" // Parallel test execution for better performance maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) } // --------------------------------------------------------------------------- // Frontend/JS build noise reduction // --------------------------------------------------------------------------- // (B) Avoid noisy "will be copied ... overwriting" logs for Kotlin/JS *CompileSync tasks. // The Kotlin JS plugin wires multiple resource sourcesets into the same destination. // We keep the first occurrence and exclude duplicates. tasks.withType().configureEach { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } tasks.withType().configureEach { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } // (A) Source map configuration is handled via `gradle.properties` (global Kotlin/JS settings) // to avoid compiler-flag incompatibilities across toolchains. // (B) JS test executable compilation/sync is currently very noisy (duplicate resource copying from jsMain + jsTest). // We disable JS/WASM JS test executables in CI/build to keep output warning-free. tasks.matching { val n = it.name n.contains("jsTest", ignoreCase = true) || n.contains("compileTestDevelopmentExecutableKotlinJs") || n.contains("compileTestDevelopmentExecutableKotlinWasmJs") || n.contains("TestDevelopmentExecutableCompileSync") }.configureEach { enabled = false } // Dedicated performance test task per JVM subproject plugins.withId("java") { val javaExt = extensions.getByType() // Ensure a full JDK toolchain with compiler is available (Gradle will auto-download if missing) javaExt.toolchain.languageVersion.set(JavaLanguageVersion.of(25)) tasks.register("perfTest") { description = "Runs tests tagged with 'perf'" group = "verification" // Use the regular test source set outputs testClassesDirs = javaExt.sourceSets.getByName("test").output.classesDirs classpath = javaExt.sourceSets.getByName("test").runtimeClasspath useJUnitPlatform { includeTags("perf") } shouldRunAfter("test") // Keep the same JVM settings for consistency jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false") maxHeapSize = "2g" dependsOn("testClasses") } } // Suppress Node.js deprecation warnings (e.g., DEP0040 punycode) during Kotlin/JS npm/yarn tasks // Applies to all Exec-based tasks (covers Yarn/NPM invocations used by Kotlin JS plugin) tasks.withType().configureEach { // Merge existing NODE_OPTIONS with --no-deprecation val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS") val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation" environment("NODE_OPTIONS", merged) // Also set the legacy switch to silence warnings entirely environment("NODE_NO_WARNINGS", "1") // Set a Chrome binary path to avoid snap permission issues environment("CHROME_BIN", "/usr/bin/google-chrome-stable") environment("CHROMIUM_BIN", "/usr/bin/chromium") environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium") } // ------------------------------ // Detekt & Ktlint default setup // ------------------------------ plugins.withId("io.gitlab.arturbosch.detekt") { extensions.configure(DetektExtension::class.java) { buildUponDefaultConfig = true allRules = false autoCorrect = false config.setFrom(files(rootProject.file("config/detekt/detekt.yml"))) basePath = rootDir.absolutePath } tasks.withType().configureEach { jvmTarget = "25" reports { xml.required.set(false) txt.required.set(false) sarif.required.set(false) html.required.set(true) } } } plugins.withId("org.jlleitschuh.gradle.ktlint") { extensions.configure(KtlintExtension::class.java) { android.set(false) outputToConsole.set(true) ignoreFailures.set(false) reporters { reporter(ReporterType.CHECKSTYLE) reporter(ReporterType.PLAIN) } } } } // ------------------------------------------------------------------ // Bundle Size Budgets for Frontend Shells (Kotlin/JS) // ------------------------------------------------------------------ // ✅ FIX: Klasse auf Top-Level verschieben data class Budget(val rawBytes: Long, val gzipBytes: Long) tasks.register("checkBundleBudget") { group = "verification" description = "Checks JS bundle sizes of frontend shells against configured budgets" doLast { val budgetsFile = file("config/bundles/budgets.json") if (!budgetsFile.exists()) { throw GradleException("Budgets file not found: ${budgetsFile.path}") } // Load budgets JSON as simple Map val text = budgetsFile.readText() @Suppress("UNCHECKED_CAST") val parsed = JsonSlurper().parseText(text) as Map> val budgets = parsed.mapValues { (_, v) -> val raw = (v["rawBytes"] as Number).toLong() val gz = (v["gzipBytes"] as Number).toLong() Budget(raw, gz) } fun gzipSize(bytes: ByteArray): Long { val baos = ByteArrayOutputStream() GZIPOutputStream(baos).use { it.write(bytes) } return baos.toByteArray().size.toLong() } val errors = mutableListOf() val report = StringBuilder() report.appendLine("Bundle Budget Report (per shell)") // Consider modules under :frontend:shells: as shells val shellPrefix = ":frontend:shells:" val shells = rootProject.subprojects.filter { it.path.startsWith(shellPrefix) } if (shells.isEmpty()) { report.appendLine("No frontend shells found under $shellPrefix") } shells.forEach { shell -> val key = shell.path.trimStart(':').replace(':', '/') // or use a colon form for budgets keys below val colonKey = shell.path.trimStart(':').replace('/', ':').trim() // ensure ":a:b:c" // Budgets are keyed by a Gradle path with colons but without leading colon in config for readability val budgetKeyCandidates = listOf( // e.g., frontend:shells:meldestelle-portal shell.path.removePrefix(":"), colonKey.removePrefix(":"), shell.name, ) val budgetEntry = budgetKeyCandidates.firstNotNullOfOrNull { budgets[it] } if (budgetEntry == null) { report.appendLine("- ${shell.path}: No budget configured (skipping)") return@forEach } // Locate distributions directory val distDir = shell.layout.buildDirectory.dir("distributions").get().asFile if (!distDir.exists()) { report.appendLine("- ${shell.path}: distributions dir not found (expected build/distributions) – did you build the JS bundle?") return@forEach } // Collect JS files under distributions (avoid .map and .txt) val jsFiles = distDir.walkTopDown().filter { it.isFile && it.extension == "js" && !it.name.endsWith(".map") }.toList() if (jsFiles.isEmpty()) { report.appendLine("- ${shell.path}: no JS artifacts found in ${distDir.path}") return@forEach } var totalRaw = 0L var totalGzip = 0L val topFiles = mutableListOf>() jsFiles.forEach { f -> val bytes = f.readBytes() val raw = bytes.size.toLong() val gz = gzipSize(bytes) totalRaw += raw totalGzip += gz topFiles += f.name to raw } val top = topFiles.sortedByDescending { it.second }.take(5) report.appendLine("- ${shell.path}:") report.appendLine(" raw: $totalRaw bytes (budget ${budgetEntry.rawBytes})") report.appendLine(" gzip: $totalGzip bytes (budget ${budgetEntry.gzipBytes})") report.appendLine(" top files:") top.forEach { (n, s) -> report.appendLine(" - $n: $s bytes") } if (totalRaw > budgetEntry.rawBytes || totalGzip > budgetEntry.gzipBytes) { errors += "${shell.path}: raw=$totalRaw/${budgetEntry.rawBytes}, gzip=$totalGzip/${budgetEntry.gzipBytes}" } } val outDir = layout.buildDirectory.dir("reports/bundles").get().asFile outDir.mkdirs() file(outDir.resolve("bundle-budgets.txt")).writeText(report.toString()) if (errors.isNotEmpty()) { val msg = buildString { appendLine("Bundle budget violations:") errors.forEach { appendLine(" - $it") } appendLine() appendLine("See report: ${outDir.resolve("bundle-budgets.txt").path}") } throw GradleException(msg) } else { println(report.toString()) println("Bundle budgets OK. Report saved to ${outDir.resolve("bundle-budgets.txt").path}") } } } // Composite verification task including static analyzers if present tasks.register("staticAnalysis") { group = "verification" description = "Run static analysis (detekt, ktlint) and architecture guards" // Plugins provide these tasks; only 'depend on' if tasks exist dependsOn( tasks.matching { it.name == "detekt" }, tasks.matching { it.name == "ktlintCheck" }, // ARCHITECTURE-TESTS: Replaced old archGuards with the new test module project(":platform:architecture-tests").tasks.named("test"), ) } // ################################################################## // ### DOKKA (Multi-Module) ### // ################################################################## // Apply Dokka (V2) automatically to Kotlin subprojects subprojects { plugins.withId("org.jetbrains.kotlin.jvm") { apply(plugin = "org.jetbrains.dokka") } plugins.withId("org.jetbrains.kotlin.multiplatform") { apply(plugin = "org.jetbrains.dokka") } } // Aggregate tasks to build multi-module docs in Markdown (GFM) and HTML // Unified V2 aggregator: builds docs via `dokkaGenerate` in subprojects and aggregates outputs val dokkaAll = tasks.register("dokkaAll") { group = "documentation" description = "Builds Dokka (V2) for all modules and aggregates outputs under build/dokka/all" // Trigger Dokka generation in all subprojects that have the Dokka plugin dependsOn( subprojects .filter { it.plugins.hasPlugin("org.jetbrains.dokka") } .map { "${it.path}:dokkaGenerate" }, ) doLast { val dest = layout.buildDirectory.dir("dokka/all").get().asFile if (dest.exists()) dest.deleteRecursively() dest.mkdirs() val modules = mutableListOf>() subprojects.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }.forEach { p -> // Dokka V2 writes into build/dokka/html val outHtml = p.layout.buildDirectory.dir("dokka/html").get().asFile if (outHtml.exists()) { val modulePath = p.path.trimStart(':').replace(':', '/') val targetDir = File(dest, modulePath) outHtml.copyRecursively(targetDir, overwrite = true) modules.add(p.name to modulePath) } } // Generate a simple index.html to navigate the modules val indexFile = File(dest, "index.html") val links = modules.sortedBy { it.first }.joinToString("\n") { (name, path) -> """
  • $name ($path)
  • """ } indexFile.writeText( """ Meldestelle Documentation

    Meldestelle Project Documentation

    Generated on ${java.time.LocalDateTime.now()}

      $links
    """.trimIndent(), ) println("[DOKKA] Aggregated Dokka V2 outputs into ${dest.absolutePath}") println("[DOKKA] Open ${dest.resolve("index.html").absolutePath} in your browser") } } // ################################################################## // ### DOKU-AGGREGATOR ### // ################################################################## // Leichter Aggregator im Root-Projekt, ruft die eigentlichen Tasks im :docs Subprojekt auf tasks.register("docs") { description = "Aggregates documentation tasks from :docs" group = "documentation" dependsOn(":docs:generateAllDocs") } // Wrapper-Konfiguration // Apply Node warning suppression on root project Exec tasks as well // Ensures aggregated Kotlin/JS tasks created at root (e.g., kotlinNpmInstall) inherit the env tasks.withType().configureEach { val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS") val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation" environment("NODE_OPTIONS", merged) environment("NODE_NO_WARNINGS", "1") // Set a Chrome binary path to avoid snap permission issues environment("CHROME_BIN", "/usr/bin/google-chrome-stable") environment("CHROMIUM_BIN", "/usr/bin/chromium") environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium") } tasks.wrapper { gradleVersion = "9.3.1" distributionType = Wrapper.DistributionType.BIN }