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 // `:commonsUI` is the Compose half of the shared layer: every composable, // icon, theme, Coil fetcher and Compose-resource (strings/fonts/files) that the // GUI front ends (Android, Desktop, iOS) render. It sits on top of `:commons` // (headless models, state, ViewModels, relay client) and is never a dependency // of `:cli`, which keeps Compose UI + Skiko off the CLI classpath. // Source files keep their `com.vitorpamplona.amethyst.commons.*` packages so // the module boundary is purely a build-graph constraint — consumers did not // have to change a single import when the split happened. // 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.20, 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_20`, "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) } kotlin { compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") } jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } } android { namespace = "com.vitorpamplona.amethyst.commons.ui" 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 — same compile-only spike as :commons. 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().configureEach { binaries.withType().configureEach { disableUiKitPrebuiltCache() } } sourceSets { commonMain { dependencies { // The headless half. `api` because every composable here takes // or returns commons types (Note, User, ViewModels, states). api(project(":commons")) api(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-runtime-compose ships // iOS variants; lifecycle-viewmodel-compose (the viewModel() // Composable helper) is Android-only and lives in jvmAndroid. 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) // NetworkHeaders on avatar requests (the local Blossom cache marker). Multiplatform, // Apache-2.0; already on the JVM classpath through coil-network-okhttp. implementation(libs.coil.network.core) // LruCache (KMP-ready) implementation(libs.androidx.collection) // Charts (Vico 3, Compose Multiplatform, Apache-2.0): the notification // summary builds its chart model off the main thread. `api` because the // model is part of that state holder's public surface. api(libs.vico.charts.compose) // Immutable collections api(libs.kotlinx.collections.immutable) // JSON trees for the LNURL-pay replies the payment stack reads. implementation(libs.kotlinx.serialization.json) // Compose Multiplatform Resources (strings, fonts, napplet shell files) 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) } } jvmTest { dependencies { // Compose Desktop on the test classpath so composables can be // rendered headlessly to a Skia surface (ImageComposeScene) and // asserted on. No new third-party dependency: this is the same // artifact jvmMain already uses, and it rasterises in software, // so it needs no display. implementation(compose.desktop.currentOs) // RouteForPointerTest stubs an Account (already the app's and androidHostTest's mock lib). implementation(libs.mockk) } } // Shared JVM code for both Android and Desktop val jvmAndroid = create("jvmAndroid") { dependsOn(commonMain.get()) dependencies { // Coil's OkHttp network fetcher (JVM-only). iOS will use // coil-ktor when the iOS Compose UI ships. implementation(libs.coil.okhttp) // OkHttp for the Blossom read-auth Coil fetcher. implementation(libs.okhttp) // executeAsync for PdfFetcher; already on every module that ships OkHttp. implementation(libs.okhttpCoroutines) // Markdown rendering (richtext-commonmark). The single // consumer (RenderMarkdown.kt) 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) } } androidMain { dependsOn(jvmAndroid) dependencies { // Android-specific Compose tooling implementation(libs.androidx.ui.tooling.preview) // WindowInsetsControllerCompat, for DisappearingScaffold's immersive status bar. implementation(libs.androidx.core.ktx) } } // iOS intermediate so iosArm64Main and iosSimulatorArm64Main share code. val iosMain = create("iosMain") { dependsOn(commonMain.get()) } getByName("iosArm64Main").dependsOn(iosMain) getByName("iosSimulatorArm64Main").dependsOn(iosMain) // Skiko-backed targets (desktop JVM + iOS) share pixel-format helpers // (org.jetbrains.skia.* resolves on both through Compose). Android is // deliberately NOT in this set — it renders through android.graphics. val skikoMain = create("skikoMain") { dependsOn(commonMain.get()) } getByName("jvmMain").dependsOn(skikoMain) iosMain.dependsOn(skikoMain) getByName("androidHostTest") { dependencies { implementation(libs.junit) // SafeImeInsetsTest mocks the android.view.View its per-window cache is keyed on. implementation(libs.mockk) implementation(libs.kotlinx.coroutines.test) } } getByName("androidDeviceTest") { dependencies { implementation(libs.androidx.junit) implementation(libs.androidx.espresso.core) } } } } compose.resources { publicResClass = true // Kept on the pre-split package so `Res` imports in every consumer // (amethyst, desktopApp, nappletHost) keep resolving unchanged. packageOfResClass = "com.vitorpamplona.amethyst.commons.resources" generateResClass = always } // iOS purity gate — shared task, see gradle/kmp-purity.gradle.kts. apply(from = rootProject.file("gradle/kmp-purity.gradle.kts"))