Files
amethyst/commonsUI/build.gradle.kts
T
Claude aff8695294 Merge branch 'main' into claude/kind-keller-itzy63
Main's 97136c22 moved DisappearingScaffold, SafeImeInsets, the emoji text
and the user-name text into commonsUI, and turned CrossfadeIfEnabled's
`accountViewModel` parameter into a LocalAnimationsEnabled composition
local. Cordn's coordinators screen was the last caller still naming the
old package and passing the removed argument; every other cordn screen
already sits on the app-level DisappearingScaffold wrapper, which main
kept.

Both conflicts were in the calendar ViewModel tests, where this branch
had mitigated a cross-test Main-dispatcher leak by yielding on
Dispatchers.Default before resetMain. Main's bc44d870 fixes the same leak
deterministically by joining the model scopes under a timeout, so those
two files take main's version whole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
2026-09-24 16:05:26 +00:00

245 lines
9.8 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
// `: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<KotlinNativeTarget>().configureEach {
binaries.withType<TestExecutable>().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)
// LruCache (KMP-ready)
implementation(libs.androidx.collection)
// Immutable collections
api(libs.kotlinx.collections.immutable)
// 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)
}
}
// 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)
// 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"))