Files
amethyst/build.gradle.kts
Claude 4bad3679bd build: keep ktlint and the KMP metadata compiles out of the inner loop
spotlessApply hung off every module's preBuild, so ktlint ran in front
of every compile (3.3 CPU minutes of a cold build) and the build
rewrote its own sources while it ran. The pre-commit hook and CI's lint
job run spotlessCheck, and the agent hooks run spotlessApply.

The pre-push hook's metadata compiles are a third full compile of
:commons and :commonsUI: 8 of the 47 CPU minutes of a cold run, and 2.6
of the 6 after a one-line :commonsUI edit. A plain push now skips them
and CI's lint job still runs them; PREPUSH_KMP_METADATA=1 turns them on,
and the ngit-pr gate before a merge or ngit send uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fsjDqmcRg2F3kQpXmwgSy
2026-10-24 01:43:16 +00:00

180 lines
8.2 KiB
Kotlin

import com.android.build.gradle.tasks.GenerateResValues
import com.diffplug.gradle.spotless.SpotlessExtensionPredeclare
import java.util.Properties
// Local SonarQube analysis is opt-in: it activates only when `sonar.host.url`
// is present in local.properties (gitignored) AND a sonar task was requested,
// so neither developers who haven't opted in nor ordinary builds/IDE syncs of
// opted-in developers resolve or apply the scanner plugin. The Kotlin DSL
// compiles this buildscript {} section in an earlier stage that can't see the
// file's imports (hence the qualified Properties) or share code with the body,
// but it can publish values — the gate is computed once here and read below
// via the project's extra properties.
buildscript {
val localProperties = File(rootDir, "local.properties")
val sonarProperties =
java.util.Properties().apply {
if (localProperties.exists()) localProperties.inputStream().use { load(it) }
}
extra.set("sonarProperties", sonarProperties)
val sonarEnabled =
sonarProperties.getProperty("sonar.host.url") != null &&
gradle.startParameter.taskNames.any { it.substringAfterLast(":") in setOf("sonar", "sonarqube") }
extra.set("sonarEnabled", sonarEnabled)
if (sonarEnabled) {
repositories {
gradlePluginPortal()
}
dependencies {
// LGPL-3.0, build-time only — never linked into shipped artifacts.
classpath(libs.sonarqube.gradle.plugin)
}
}
}
plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.jetbrainsKotlinJvm) apply false
alias(libs.plugins.androidBenchmark) apply false
alias(libs.plugins.diffplugSpotless)
alias(libs.plugins.googleServices) apply false
alias(libs.plugins.jetbrainsComposeCompiler) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
alias(libs.plugins.serialization)
alias(libs.plugins.googleKsp) apply false
}
// Shared app version for all subprojects — read from gradle/libs.versions.toml.
// Android versionCode is the `appCode` entry in the same catalog (must be monotonic int).
// Desktop packageVersion inherits via project.version in desktopApp/build.gradle.kts.
val appVersion = libs.versions.app.get()
allprojects {
version = appVersion
configurations.configureEach {
resolutionStrategy.cacheChangingModulesFor(0, "seconds")
}
apply(plugin = "com.diffplug.spotless")
if (project === rootProject) {
// Declaring a format on `spotlessPredeclare` is what turns predeclaration on (resolving
// the formatter deps from the root project's repositories); the old
// `spotless { predeclareDeps() }` switch is deprecated and did nothing more.
configure<SpotlessExtensionPredeclare> {
kotlin {
ktlint("1.7.1")
}
}
} else {
spotless {
kotlin {
target("src/**/*.kt")
// Third-party sources vendored verbatim keep their own license header and
// formatting. Stamping our MIT header onto someone else's Apache-2.0 file
// would misstate its provenance, and reformatting it would make the next
// re-vendor a merge conflict instead of a copy.
targetExclude("src/main/java/zxingcpp/**/*.kt")
ktlint("1.7.1")
licenseHeaderFile(
rootProject.file(".spotless/copyright.kt"),
"@file:|package|import|class|object|sealed|open|interface|abstract ",
)
}
kotlinGradle {
target("*.gradle.kts")
}
}
}
}
subprojects {
afterEvaluate {
// The Kotlin Multiplatform plugin never registers a plain `test` task: it creates one
// task per target (jvmTest, androidUnitTest, linuxX64Test, ...) plus the `allTests`
// aggregate. A root `./gradlew test` therefore runs `test` only in the Java/Android
// modules that own one and *silently* skips every KMP module - Gradle only errors when
// no project at all has the task, and four of them do, so it exits 0 looking healthy.
// That hid ~8k tests (quartz, commons, commonsUI, quic, nestsClient, marmotQuic), which
// is most of this repo's suite. Register the alias so the documented command means what
// it says.
//
// It maps to jvmTest, not allTests, on purpose: allTests also drags in androidUnitTest
// and the native targets, half of which cannot run on a given host (macosArm64Test on
// Linux) and all of which change what `test` costs. jvmTest is exactly what the
// pre-push hook and CI already run for these modules, so the alias matches the coverage
// they expect rather than inventing a third definition of "the tests".
//
// The flip side: a KMP module's non-JVM targets stay outside `test` (CI names the ones
// it runs, like :commonsUI:testAndroidHostTest). quartz, whose androidHostTest suite was
// the reason this is not allTests, now lives in vitorpamplona/quartz.
if (plugins.hasPlugin("org.jetbrains.kotlin.multiplatform") &&
tasks.findByName("test") == null &&
tasks.findByName("jvmTest") != null
) {
tasks.register("test") {
group = "verification"
description = "Runs the JVM unit tests for this Kotlin Multiplatform module."
dependsOn("jvmTest")
}
}
// Formatting is not part of the build. spotlessApply used to hang off every module's
// preBuild, which put ktlint in front of every compile (about 3 min of a cold build) and
// had the build rewrite its own sources while it ran. The pre-commit hook and CI's lint
// job run spotlessCheck, and the agents' hooks run spotlessApply; by hand it is
// `./gradlew spotlessApply`.
}
}
// Second half of the opt-in local SonarQube support gated above in buildscript {}.
// All sonar.* entries in local.properties are forwarded as system properties, so
// `./gradlew sonar` behaves exactly like passing them via -Dsonar.xxx=... on the
// command line. sonar.projectKey/projectName default to the root project name
// ("Amethyst") and only need overriding in local.properties if desired.
val sonarEnabled = extra["sonarEnabled"] as Boolean
if (sonarEnabled) {
val sonarProperties = extra["sonarProperties"] as Properties
apply(plugin = "org.sonarqube")
sonarProperties
.stringPropertyNames()
.filter { it.startsWith("sonar.") }
.forEach { System.setProperty(it, sonarProperties.getProperty(it)) }
// The scanner's sonarResolver task reads AGP's generated-res-values provider
// but doesn't depend on the task that produces it — wire it up in every
// module that has both (today only :amethyst enables resValues, but the
// scanner defect is module-agnostic).
subprojects {
tasks.named { it == "sonarResolver" }.configureEach {
dependsOn(tasks.withType<GenerateResValues>())
}
}
}
val installGitHook = tasks.register<Copy>("installGitHook") {
val dotGit = File(rootProject.rootDir, ".git")
val hooksDir: File = if (dotGit.isFile) {
// Git worktree: .git is a file with "gitdir: <path>". Git reads hooks from the main
// repository's .git/hooks, which the worktree's gitdir names in its `commondir` file;
// a hook copied into the worktree's own gitdir never runs.
val gitDir = File(dotGit.readText().trim().replace("gitdir: ", ""))
val commonDir = File(gitDir, "commondir").takeIf { it.isFile }?.let { gitDir.resolve(it.readText().trim()).normalize() } ?: gitDir
File(commonDir, "hooks")
} else {
File(dotGit, "hooks")
}
from(File(rootProject.rootDir, ".git-hooks/pre-commit"))
from(File(rootProject.rootDir, ".git-hooks/pre-push"))
into(hooksDir)
filePermissions { unix("0777") }
}
tasks.getByPath(":amethyst:preBuild").dependsOn(installGitHook)