Files
amethyst/gradle/kmp-purity.gradle.kts
T
Claude ae0b70728e Merge remote-tracking branch 'origin/main' into claude/beautiful-brahmagupta-17clq5
# Conflicts:
#	commons/build.gradle.kts
#	quartz/build.gradle.kts
2026-09-12 23:11:15 +00:00

82 lines
4.2 KiB
Kotlin

// iOS purity gate shared by every KMP library module that targets Apple
// (`:quartz`, `:commons`, `:commonsUI`). Apply it from the module's build
// script with
//
// apply(from = rootProject.file("gradle/kmp-purity.gradle.kts"))
//
// It registers `verifyKmpPurity` (wired into `check`) which fails when an
// iOS-targeted source set references a JVM-only API. `verifyKmpPurity` is the
// fast Linux pre-check that runs in the lint job; the macOS iOS compile job is
// the authoritative one. Keeping the pattern table in one file means a new
// forbidden API lands in every module at once instead of drifting per copy.
val verifyKmpPurity = tasks.register("verifyKmpPurity") {
group = "verification"
description = "Fails if iOS-targeted source sets import JVM-only deps."
// Every source set that feeds an Apple target. Modules that don't have a
// given directory are skipped by the exists() filter, so the list is the
// union across modules (e.g. only :commonsUI has skikoMain today).
val checkedDirs =
listOf(
"src/commonMain", "src/commonTest",
"src/appleMain", "src/appleTest",
"src/nativeMain", "src/nativeTest",
"src/iosMain", "src/iosTest",
"src/skikoMain", "src/skikoTest",
"src/iosArm64Main", "src/iosArm64Test",
"src/iosSimulatorArm64Main", "src/iosSimulatorArm64Test",
"src/linuxMain", "src/linuxTest",
"src/linuxX64Main", "src/linuxX64Test",
"src/macosMain", "src/macosTest",
"src/macosArm64Main", "src/macosArm64Test",
).map { layout.projectDirectory.dir(it).asFile }
.filter { it.exists() }
inputs.files(checkedDirs)
doLast {
// Each pattern is paired with a short hint so the failure message
// points at the canonical KMP replacement.
val forbidden =
listOf(
"com.fasterxml.jackson" to "Jackson is JVM-only — use kotlinx.serialization",
"okhttp3" to "OkHttp is JVM-only — wrap behind expect/actual or use Ktor on iOS",
"System.currentTimeMillis" to "use TimeUtils.now()",
"Thread.sleep" to "use kotlinx.coroutines.delay or platform-specific actual",
"java.util.UUID" to "use kotlin.uuid.Uuid",
"kotlin.jvm.Synchronized" to "use a KMP lock primitive (KmpLock.withLock {} in commons)",
// The bare call, not just the annotation: `synchronized(lock) {}` resolves
// from kotlin-stdlib-jvm with no import, so it compiles on Android/JVM and
// only fails at the iOS compile step. Catch it here instead.
"synchronized(" to "`synchronized` is JVM-only — use a KMP lock primitive (KmpLock.withLock {} in commons)",
"kotlin.jvm.Volatile" to "use kotlin.concurrent.Volatile",
)
val offenders =
checkedDirs.flatMap { dir ->
dir
.walkTopDown()
.filter { it.isFile && it.extension == "kt" }
.flatMap { file ->
file.readLines().withIndex().mapNotNull { (idx, line) ->
val trimmed = line.trimStart()
// Skip KDoc / line-comment lines — those legitimately
// mention forbidden names (migration notes, doc refs).
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
return@mapNotNull null
}
forbidden.firstOrNull { (pattern, _) -> line.contains(pattern) }?.let { (hit, hint) ->
"${file.relativeTo(rootDir)}:${idx + 1}: '$hit' — $hint"
}
}
}
}
if (offenders.isNotEmpty()) {
throw GradleException(
"iOS-targeted source sets must not reference JVM-only APIs. " +
"Move the offending code to jvmAndroid/ or behind an expect/actual:\n " +
offenders.joinToString("\n "),
)
}
}
}
tasks.named("check").configure { dependsOn(verifyKmpPurity) }