Files
amethyst/commons/build.gradle.kts
T
Claude 18c0c4291f chore(deps): update dependencies across all modules
Bump to latest stable (or latest within an existing pre-release track),
skipping stable→alpha/beta jumps for production safety.

- kotlin 2.4.0 → 2.4.10 (+ kotlinTest, compose compiler); ksp 2.3.9 → 2.3.10
- composeBom 2026.06.00 → 2026.06.01; composeRuntimeAnnotation 1.11.3 → 1.11.4
- firebaseBom 34.15.0 → 34.16.0; jacksonModuleKotlin 2.22.0 → 2.22.1
- kotlinxCollectionsImmutable 0.5.0 → 0.5.1; sqlite 2.6.2 → 2.7.0
- composemediaplayer 0.10.0 → 0.11.3; zoomable 2.12.0 → 2.13.0
- jlatexmath 1.4 → 1.5; jna 5.14.0 → 5.19.1 (nestsClient)
- benchmark 1.5.0-alpha06 → alpha07; genaiPrompt 1.0.0-beta2 → beta3
- bump DisableCacheInKotlinVersion guard 2_4_0 → 2_4_10 (iOS test workaround)

appfunctions held at alpha09 (appfunctions-service has no alpha10 published).
AGP 9.3.0, gms 4.5.0, kotlin stable, ktor, coil, media3, navigation, and other
androidx libs already at their newest stable; alpha-only newer releases skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TreeiMiHrhWce6PbnA5qWz
2026-07-19 14:06:37 +00:00

331 lines
14 KiB
Kotlin

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.DisableCacheInKotlinVersion
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCacheApi
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable
// Disables the Kotlin/Native compiler cache for an iOS test binary so the
// Compose ui-uikit klib recompiles fresh instead of linking the broken prebuilt
// cache (see the call site in the `kotlin {}` block). The version guard makes
// Kotlin re-surface this workaround once we move past 2.4.10, so it can be
// dropped when a newer Compose/Kotlin pairing fixes the cache. Wrapped in a
// helper because @OptIn only applies to declarations, not bare statements.
@OptIn(KotlinNativeCacheApi::class)
fun TestExecutable.disableUiKitPrebuiltCache() =
disableNativeCache(
DisableCacheInKotlinVersion.`2_4_10`,
"Compose ui-uikit prebuilt cache references UIViewLayoutRegion (iOS 17+); " +
"linking the iOS test binary fails under Xcode 16.4.",
)
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKotlinMultiplatformLibrary)
alias(libs.plugins.jetbrainsComposeCompiler)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.serialization)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
jvm {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
android {
namespace = "com.vitorpamplona.amethyst.commons"
compileSdk =
libs.versions.android.compileSdk
.get()
.toInt()
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
androidResources.enable = true
withHostTest {
isReturnDefaultValues = true
}
withDeviceTest {
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
// iOS targets — Phase 2 spike. Compile-only for now (no framework binary
// configured yet). Reveals which transitive deps need iOS variants and
// which commonMain files still reach for platform-only APIs.
iosArm64()
iosSimulatorArm64()
// Compose Multiplatform 1.11.x ships an `org.jetbrains.compose.ui:ui-uikit`
// prebuilt Kotlin/Native cache whose CMPLayoutRegion object hard-references
// the UIKit class `UIViewLayoutRegion` (introduced in iOS 17). Linking the
// iOS *test* executable against that cache under Xcode 16.4 fails with
// ld: Undefined symbols: _OBJC_CLASS_$_UIViewLayoutRegion
// because the cached object was built for a newer simulator SDK (18.5) than
// the test binary is being linked for (14.0). Disabling the native cache for
// the iOS test binaries makes ui-uikit recompile against the active SDK,
// where the symbol resolves. See disableUiKitPrebuiltCache() above and
// https://kotl.in/disable-native-cache
targets.withType<KotlinNativeTarget>().configureEach {
binaries.withType<TestExecutable>().configureEach {
disableUiKitPrebuiltCache()
}
}
sourceSets {
commonMain {
dependencies {
implementation(project(":quartz"))
// Compose Multiplatform
implementation(libs.jetbrains.compose.ui)
implementation(libs.jetbrains.compose.foundation)
implementation(libs.jetbrains.compose.runtime)
implementation(libs.jetbrains.compose.material3)
implementation(libs.jetbrains.compose.ui.tooling.preview)
// Lifecycle (KMP since 2.8.0). lifecycle-viewmodel and
// lifecycle-runtime-compose ship iOS variants;
// lifecycle-viewmodel-compose (the viewModel() Composable
// helper) is Android-only and lives in jvmAndroid below.
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
// Image loading (Coil 3 - KMP). The okhttp network fetcher is
// JVM-only and lives in jvmAndroid; iOS will pull coil-ktor
// when that target wires its actual.
implementation(libs.coil.compose)
// LruCache (KMP-ready)
implementation(libs.androidx.collection)
// Immutable collections
api(libs.kotlinx.collections.immutable)
// JSON for custom-feed definitions (KMP — replaces Jackson
// for the one commonMain serializer that was blocking iOS).
implementation(libs.kotlinx.serialization.json)
// Compose Multiplatform Resources
implementation(libs.jetbrains.compose.components.resources)
// KMP syntax highlighter (Apache-2.0) for the git code browser.
implementation(libs.highlights)
}
}
commonTest {
dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}
// Shared JVM code for both Android and Desktop
val jvmAndroid =
create("jvmAndroid") {
dependsOn(commonMain.get())
dependencies {
// Audio-rooms ViewModel needs the listener orchestration +
// audio pipeline types (NestsListener, AudioRoomPlayer,
// AudioPlayer interface). The :nestsClient module is
// jvmAndroid-only today (its QUIC + Opus + AudioRecord/Track
// stacks are JVM-bound), so the dep lives here, not in
// commonMain. iOS will need an audio-rooms reroute when
// Phase 5 lands.
implementation(project(":nestsClient"))
// Coil's OkHttp network fetcher (JVM-only). iOS will use
// coil-ktor when the iOS Compose UI ships.
implementation(libs.coil.okhttp)
// OkHttp (+ coroutines bridge) for the link-preview fetcher
// (service/preview/UrlPreview). JVM-only; iOS will swap to
// Ktor when its UI ships.
implementation(libs.okhttp)
implementation(libs.okhttpCoroutines)
// Markdown rendering (richtext-commonmark). The single
// consumer (RenderMarkdown.kt) already lives in jvmAndroid.
// iOS support pending Phase 3 markdown decision.
implementation(libs.markdown.commonmark)
implementation(libs.markdown.ui)
implementation(libs.markdown.ui.material3)
// viewModel() Compose helper. AndroidX publishes this
// artifact for android/jvmStubs/linuxx64Stubs but not iOS,
// so it stays in jvmAndroid until we either swap to the
// org.jetbrains.androidx.lifecycle variant or accept a
// platform-specific ViewModel access pattern on iOS.
implementation(libs.androidx.lifecycle.viewmodel.compose)
}
}
jvmMain {
dependsOn(jvmAndroid)
dependencies {
// Desktop-specific Compose
implementation(compose.desktop.currentOs)
implementation(libs.jetbrains.compose.ui.tooling)
// Secure key storage via OS keychain (macOS/Windows/Linux)
implementation(libs.java.keyring)
// EXIF stripping for image uploads (used by service/upload/MediaCompressor).
implementation(libs.commons.imaging)
// Image re-encode + progressive downscale (used by service/upload/ImageReencoder).
// Pure-Java, MIT. See docs/plans/2026-06-08-feat-desktop-image-compression-plan.md.
implementation(libs.thumbnailator)
// Native OS notification bridges — Nucleus per-OS JNI shims.
// macOS: UNUserNotificationCenter. Windows: WinRT Toasts. Linux: freedesktop D-Bus.
// Only the matching-OS module's native lib loads at runtime; the others
// stay dormant on the classpath.
implementation("io.github.kdroidfilter:nucleus.notification-macos:1.15.7")
implementation("io.github.kdroidfilter:nucleus.notification-windows:1.15.7")
implementation("io.github.kdroidfilter:nucleus.notification-linux:1.15.7")
}
}
androidMain {
dependsOn(jvmAndroid)
dependencies {
// Android-specific Compose tooling
implementation(libs.androidx.ui.tooling.preview)
// Secure key storage via Android Keystore
implementation(libs.androidx.security.crypto.ktx)
implementation(libs.androidx.datastore.preferences)
}
}
// iOS intermediate so iosArm64Main and iosSimulatorArm64Main share code.
val iosMain =
create("iosMain") {
dependsOn(commonMain.get())
}
getByName("iosArm64Main").dependsOn(iosMain)
getByName("iosSimulatorArm64Main").dependsOn(iosMain)
getByName("androidHostTest") {
dependencies {
implementation(libs.junit)
// Bitcoin secp256k1 bindings
implementation(libs.secp256k1.kmp.jni.jvm)
}
}
// jvmTest needs the JVM secp256k1 bindings whenever a test
// exercises a real signer (e.g. UploadOrchestratorTest, which
// signs Blossom auth events end-to-end).
getByName("jvmTest") {
dependencies {
implementation(libs.secp256k1.kmp.jni.jvm)
}
}
getByName("androidDeviceTest") {
dependencies {
implementation(libs.androidx.junit)
implementation(libs.androidx.espresso.core)
}
}
}
}
compose.resources {
publicResClass = true
packageOfResClass = "com.vitorpamplona.amethyst.commons.resources"
generateResClass = always
}
// JVM tests run AWT-backed code (ImageIO, Thumbnailator, BufferedImage) — pin
// headless mode so a stray Toolkit.getDefaultToolkit() in a transitive dep
// never bounces the macOS Dock during CI/local test runs.
tasks.withType<Test>().configureEach {
if (name == "jvmTest") {
jvmArgs("-Djava.awt.headless=true")
}
}
// iOS purity gate — same shape as :quartz:verifyKmpPurity. See the rationale
// there. Commons gains this gate once FeedDefinitionSerializer.kt has been
// migrated off Jackson; future commonMain code must not reintroduce JVM-only
// JSON / HTTP deps.
val verifyKmpPurity by tasks.registering {
group = "verification"
description = "Fails if iOS-targeted source sets import JVM-only deps."
val checkedDirs =
listOf(
"src/commonMain", "src/commonTest",
"src/appleMain", "src/appleTest",
"src/nativeMain", "src/nativeTest",
"src/iosMain", "src/iosTest",
"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 KmpLock.withLock {}",
"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) }