initial skills
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
---
|
||||
name: kotlin-multiplatform
|
||||
description: |
|
||||
Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific,
|
||||
source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets
|
||||
(Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues.
|
||||
Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation,
|
||||
build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.
|
||||
---
|
||||
|
||||
# Kotlin Multiplatform: Platform Abstraction Decisions
|
||||
|
||||
Expert guidance for KMP architecture in Amethyst - deciding what to share vs keep platform-specific.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Making platform abstraction decisions:
|
||||
- "Should I create expect/actual or keep Android-only?"
|
||||
- "Can I share this ViewModel logic?"
|
||||
- "Where does this crypto/JSON/network implementation belong?"
|
||||
- "This uses Android Context - can it be abstracted?"
|
||||
- "Is this code in the wrong module?"
|
||||
- Preparing for iOS/web/wasm targets
|
||||
- Detecting incorrect placements
|
||||
|
||||
## Abstraction Decision Tree
|
||||
|
||||
**Central question:** "Should this code be reused across platforms?"
|
||||
|
||||
Follow this decision path (< 1 minute):
|
||||
|
||||
```
|
||||
Q: Is it used by 2+ platforms?
|
||||
├─ NO → Keep platform-specific
|
||||
│ Example: Android-only permission handling
|
||||
│
|
||||
└─ YES → Continue ↓
|
||||
|
||||
Q: Is it pure Kotlin (no platform APIs)?
|
||||
├─ YES → commonMain
|
||||
│ Example: Nostr event parsing, business rules
|
||||
│
|
||||
└─ NO → Continue ↓
|
||||
|
||||
Q: Does it vary by platform or by JVM vs non-JVM?
|
||||
├─ By platform (Android ≠ iOS ≠ Desktop)
|
||||
│ → expect/actual
|
||||
│ Example: Secp256k1Instance (uses different security APIs)
|
||||
│
|
||||
├─ By JVM (Android = Desktop ≠ iOS/web)
|
||||
│ → jvmAndroid
|
||||
│ Example: Jackson JSON parsing (JVM library)
|
||||
│
|
||||
└─ Complex/UI-related
|
||||
→ Keep platform-specific
|
||||
Example: Navigation (Activity vs Window too different)
|
||||
|
||||
Final check:
|
||||
Q: Maintenance cost of abstraction < duplication cost?
|
||||
├─ YES → Proceed with abstraction
|
||||
└─ NO → Duplicate (simpler)
|
||||
```
|
||||
|
||||
### Real Examples from Codebase
|
||||
|
||||
**Crypto → expect/actual:**
|
||||
```kotlin
|
||||
// commonMain - expect declaration
|
||||
expect object Secp256k1Instance {
|
||||
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
|
||||
}
|
||||
|
||||
// androidMain - uses Android Keystore
|
||||
// jvmMain - uses Desktop JVM crypto
|
||||
// iosMain - uses iOS Security framework
|
||||
```
|
||||
**Why:** Each platform has different security APIs.
|
||||
|
||||
**JSON parsing → jvmAndroid:**
|
||||
```kotlin
|
||||
// quartz/build.gradle.kts
|
||||
val jvmAndroid = create("jvmAndroid") {
|
||||
api(libs.jackson.module.kotlin)
|
||||
}
|
||||
```
|
||||
**Why:** Jackson is JVM-only, works on Android + Desktop, not iOS/web.
|
||||
|
||||
**Navigation → platform-specific:**
|
||||
- Android: `MainActivity` (Activity + Compose Navigation)
|
||||
- Desktop: `Window` + sidebar + MenuBar
|
||||
**Why:** UI paradigms fundamentally different.
|
||||
|
||||
## Mental Model: Source Sets as Dependency Graph
|
||||
|
||||
Think of source sets as a dependency graph, not folders.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ commonMain = Contract (pure Kotlin) │
|
||||
│ - Business logic, protocol, data models │
|
||||
│ - No platform APIs │
|
||||
└────────────┬────────────────────────────────┘
|
||||
│
|
||||
├──────────────────────┬────────────────────
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────┐ ┌──────────────────┐
|
||||
│ jvmAndroid │ │ iosMain │
|
||||
│ JVM libs shared │ │ iOS common │
|
||||
│ - Jackson │ │ │
|
||||
│ - OkHttp │ └────┬─────────────┘
|
||||
└───┬───────────┬───┘ │
|
||||
│ │ ├─→ iosX64Main
|
||||
▼ ▼ ├─→ iosArm64Main
|
||||
┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main
|
||||
│android │ │jvmMain │
|
||||
│Main │ │(Desktop) │
|
||||
└─────────┘ └──────────┘
|
||||
|
||||
Future: jsMain, wasmMain
|
||||
```
|
||||
|
||||
**Key insight:** jvmAndroid is NOT a platform - it's a shared JVM layer.
|
||||
|
||||
## The jvmAndroid Pattern
|
||||
|
||||
**Unique to Amethyst.** Shares JVM libraries between Android + Desktop.
|
||||
|
||||
### When to Use jvmAndroid
|
||||
|
||||
Use jvmAndroid when:
|
||||
- ✅ JVM-specific libraries (Jackson, OkHttp, url-detector)
|
||||
- ✅ Android implementation = Desktop implementation (same JVM)
|
||||
- ✅ Library doesn't work on iOS/web
|
||||
|
||||
Do NOT use jvmAndroid for:
|
||||
- ❌ Pure Kotlin code (use commonMain)
|
||||
- ❌ Platform-specific APIs (use androidMain/jvmMain)
|
||||
- ❌ Code that should work on all platforms
|
||||
|
||||
### Example from quartz/build.gradle.kts
|
||||
|
||||
```kotlin
|
||||
// Must be defined BEFORE androidMain and jvmMain
|
||||
val jvmAndroid = create("jvmAndroid") {
|
||||
dependsOn(commonMain.get())
|
||||
|
||||
dependencies {
|
||||
api(libs.jackson.module.kotlin) // JSON parsing - JVM only
|
||||
api(libs.url.detector) // URL extraction - JVM only
|
||||
implementation(libs.okhttp) // HTTP client - JVM only
|
||||
}
|
||||
}
|
||||
|
||||
// Both depend on jvmAndroid
|
||||
jvmMain { dependsOn(jvmAndroid) }
|
||||
androidMain { dependsOn(jvmAndroid) }
|
||||
```
|
||||
|
||||
**Why Jackson in jvmAndroid, not commonMain?**
|
||||
- Jackson is JVM-specific library
|
||||
- Works on Android (runs on JVM)
|
||||
- Works on Desktop (runs on JVM)
|
||||
- Does NOT work on iOS (not JVM) or web (not JVM)
|
||||
|
||||
**Web/wasm consideration:** For future web support, consider migrating from Jackson → kotlinx.serialization (see Target-Specific Guidance).
|
||||
|
||||
## What to Abstract vs Keep Platform-Specific
|
||||
|
||||
Quick decision guidelines based on codebase patterns:
|
||||
|
||||
### Always Abstract
|
||||
- **Crypto** (Secp256k1, encryption, signing)
|
||||
- **Core protocol logic** (Nostr events, NIPs)
|
||||
- **Why:** Needed everywhere, platform security APIs vary
|
||||
|
||||
### Often Abstract
|
||||
- **I/O operations** (file reading, caching)
|
||||
- **Logging** (platform logging systems differ)
|
||||
- **Serialization** (if using kotlinx.serialization)
|
||||
- **Why:** Commonly reused, platform implementations available
|
||||
|
||||
### Sometimes Abstract
|
||||
- **Business logic:** YES - state machines, data processing
|
||||
- **UI state:** NO - ViewModels platform-specific
|
||||
- **Why:** Separate concerns, business logic reusable
|
||||
|
||||
### Rarely Abstract
|
||||
- **UI components** (composables with platform dependencies)
|
||||
- **Why:** Platform paradigms differ (bottom nav vs sidebar)
|
||||
|
||||
### Never Abstract
|
||||
- **Navigation** (Activity vs Window fundamentally different)
|
||||
- **Permissions** (Android vs iOS APIs incompatible)
|
||||
- **Platform UX patterns**
|
||||
- **Why:** Too platform-specific, abstraction creates leaky APIs
|
||||
|
||||
### Evidence from shared-ui-analysis.md
|
||||
|
||||
| Component | Shared? | Rationale |
|
||||
|-----------|---------|-----------|
|
||||
| PubKeyFormatter, ZapFormatter | ✅ YES | Pure Kotlin, no platform APIs |
|
||||
| TimeAgoFormatter | ⚠️ ABSTRACTED | Needs StringProvider for localized strings |
|
||||
| Navigation (INav) | ❌ NO | Activity vs Window too different |
|
||||
| AccountViewModel | ⚠️ PARTIAL | Business logic → IAccountState (shared), UI state → platform ViewModels |
|
||||
| Image loading (Coil) | ⚠️ ABSTRACTED | Coil 3.x supports KMP, needs expect/actual wrapper |
|
||||
|
||||
## expect/actual Mechanics
|
||||
|
||||
**When to use:** Code needed by 2+ platforms, varies by platform.
|
||||
|
||||
### Pattern Categories from Codebase
|
||||
|
||||
**Objects (singletons):**
|
||||
```kotlin
|
||||
// 24 expect declarations found, common pattern:
|
||||
expect object Secp256k1Instance { ... }
|
||||
expect object Log { ... }
|
||||
expect object LibSodiumInstance { ... }
|
||||
```
|
||||
|
||||
**Classes (instantiable):**
|
||||
```kotlin
|
||||
expect class AESCBC { ... }
|
||||
expect class DigestInstance { ... }
|
||||
```
|
||||
|
||||
**Functions (utilities):**
|
||||
```kotlin
|
||||
expect fun platform(): String
|
||||
expect fun currentTimeSeconds(): Long
|
||||
```
|
||||
|
||||
**See** [references/expect-actual-catalog.md](references/expect-actual-catalog.md) for complete catalog with rationale.
|
||||
|
||||
## Target-Specific Guidance
|
||||
|
||||
### Android, JVM (Desktop), iOS - Current Primary Targets
|
||||
|
||||
**Status:** Mature patterns, stable APIs
|
||||
|
||||
**Android (androidMain):**
|
||||
- Uses Android framework (Activity, Context, etc.)
|
||||
- secp256k1-kmp-jni-android for crypto
|
||||
- AndroidX libraries
|
||||
|
||||
**Desktop JVM (jvmMain):**
|
||||
- Uses Compose Desktop (Window, MenuBar, etc.)
|
||||
- secp256k1-kmp-jni-jvm for crypto
|
||||
- Pure JVM libraries
|
||||
|
||||
**iOS (iosMain):**
|
||||
- Active development, framework configured
|
||||
- Architecture targets: iosX64Main, iosArm64Main, iosSimulatorArm64Main
|
||||
- Platform APIs via platform.posix, Security framework
|
||||
|
||||
### Web, wasm - Future Targets
|
||||
|
||||
**Status:** Not yet implemented, consider for future-proofing
|
||||
|
||||
**Constraints to know:**
|
||||
- ❌ No platform.posix (file I/O different)
|
||||
- ❌ No JVM libraries (Jackson, OkHttp won't work)
|
||||
- ❌ Different async model (JS event loop vs threads)
|
||||
|
||||
**Future-proofing tips:**
|
||||
1. Prefer pure Kotlin in commonMain
|
||||
2. Use kotlinx.* libraries:
|
||||
- kotlinx.serialization instead of Jackson
|
||||
- ktor instead of OkHttp (ktor supports web)
|
||||
- kotlinx.datetime instead of custom date handling
|
||||
3. Avoid platform.posix for file operations
|
||||
4. Test abstractions work without JVM assumptions
|
||||
|
||||
**Example migration path:**
|
||||
```kotlin
|
||||
// Current: jvmAndroid (JVM-only)
|
||||
api(libs.jackson.module.kotlin)
|
||||
|
||||
// Future: commonMain (all platforms)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
```
|
||||
|
||||
## Integration: When to Invoke Other Skills
|
||||
|
||||
### Invoke gradle-expert
|
||||
|
||||
Trigger gradle-expert skill when encountering:
|
||||
- Dependency conflicts (e.g., secp256k1-android vs secp256k1-jvm version mismatch)
|
||||
- Build errors related to source sets
|
||||
- Version catalog issues (libs.versions.toml)
|
||||
- "Duplicate class" errors
|
||||
- Performance/build time issues
|
||||
|
||||
**Example trigger:**
|
||||
```
|
||||
Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1
|
||||
```
|
||||
→ Invoke gradle-expert for dependency conflict resolution.
|
||||
|
||||
### Flags to Raise
|
||||
|
||||
**Platform code in commonMain:**
|
||||
```kotlin
|
||||
// ❌ INCORRECT - Android API in commonMain
|
||||
expect fun getContext(): Context // Context is Android-only!
|
||||
```
|
||||
→ Flag: "Android API in commonMain won't compile on other platforms"
|
||||
|
||||
**Duplicated business logic:**
|
||||
```kotlin
|
||||
// ❌ INCORRECT - Same logic in both
|
||||
// androidMain/.../CryptoUtils.kt
|
||||
fun validateSignature(...) { ... }
|
||||
|
||||
// jvmMain/.../CryptoUtils.kt
|
||||
fun validateSignature(...) { ... } // Duplicated!
|
||||
```
|
||||
→ Flag: "Business logic duplicated, should be in commonMain or expect/actual"
|
||||
|
||||
**Reinventing wheel - suggest KMP alternatives:**
|
||||
- Custom date/time → kotlinx.datetime
|
||||
- OkHttp → ktor (supports web)
|
||||
- Jackson → kotlinx.serialization
|
||||
- Custom UUID → kotlinx.uuid (when stable)
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Over-Abstraction
|
||||
**Problem:** Creating expect/actual for UI components
|
||||
```kotlin
|
||||
// ❌ BAD
|
||||
expect fun NavigationComponent(...)
|
||||
```
|
||||
**Why:** Navigation paradigms too different (Activity vs Window)
|
||||
**Fix:** Keep platform-specific, accept duplication
|
||||
|
||||
### 2. Under-Sharing
|
||||
**Problem:** Duplicating business logic across platforms
|
||||
```kotlin
|
||||
// ❌ BAD - duplicated in androidMain and jvmMain
|
||||
fun parseNostrEvent(json: String): Event { ... }
|
||||
```
|
||||
**Why:** Bug fixes need to be applied twice, tests duplicated
|
||||
**Fix:** Move to commonMain (pure Kotlin) or create expect/actual
|
||||
|
||||
### 3. Leaky Abstractions
|
||||
**Problem:** Platform code in commonMain
|
||||
```kotlin
|
||||
// commonMain - ❌ BAD
|
||||
import android.content.Context // Won't compile on iOS!
|
||||
```
|
||||
**Fix:** Use expect/actual or dependency injection
|
||||
|
||||
### 4. Premature Abstraction
|
||||
**Problem:** Creating expect/actual before second platform needs it
|
||||
```kotlin
|
||||
// ❌ BAD - only used on Android currently
|
||||
expect fun showNotification(...)
|
||||
```
|
||||
**Why:** Wrong abstraction boundaries, wasted effort
|
||||
**Fix:** Wait until iOS actually needs it, then abstract
|
||||
|
||||
### 5. Wrong Source Set
|
||||
**Problem:** JVM libraries in commonMain
|
||||
```kotlin
|
||||
// commonMain - ❌ BAD
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
```
|
||||
**Why:** Jackson won't compile on iOS/web
|
||||
**Fix:** Move to jvmAndroid or migrate to kotlinx.serialization
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Code Type | Recommended Location | Reason |
|
||||
|-----------|---------------------|--------|
|
||||
| Pure Kotlin business logic | commonMain | Works everywhere |
|
||||
| Nostr protocol, NIPs | commonMain | Core logic, no platform APIs |
|
||||
| JVM libs (Jackson, OkHttp) | jvmAndroid | Android + Desktop only |
|
||||
| Crypto (varies by platform) | expect in commonMain, actual in platforms | Different security APIs per platform |
|
||||
| I/O, logging | expect in commonMain, actual in platforms | Platform implementations differ |
|
||||
| State (business logic) | commonMain or commons/jvmAndroid | Reusable StateFlow patterns |
|
||||
| State (UI) | Platform ViewModels | Platform-specific lifecycle |
|
||||
| UI formatters (pure) | commons/commonMain | Reusable, no dependencies |
|
||||
| UI components (complex) | Platform-specific | Paradigms differ |
|
||||
| Navigation | Platform-specific only | Activity vs Window too different |
|
||||
| Permissions | Platform-specific only | APIs incompatible |
|
||||
| Platform UX (menus, etc.) | Platform-specific only | Native feel required |
|
||||
|
||||
## See Also
|
||||
|
||||
- [references/abstraction-examples.md](references/abstraction-examples.md) - Good/bad abstraction examples with rationale
|
||||
- [references/source-set-hierarchy.md](references/source-set-hierarchy.md) - Visual hierarchy with Amethyst examples
|
||||
- [references/expect-actual-catalog.md](references/expect-actual-catalog.md) - All 24 expect/actual pairs with "why abstracted"
|
||||
- [references/target-compatibility.md](references/target-compatibility.md) - Platform constraints and future-proofing
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/validate-kmp-structure.sh` - Detect incorrect placements, validate source sets
|
||||
- `scripts/suggest-kmp-dependency.sh` - Suggest KMP library alternatives (ktor, kotlinx.serialization, etc.)
|
||||
@@ -0,0 +1,311 @@
|
||||
# Abstraction Examples from Amethyst Codebase
|
||||
|
||||
Real examples of abstraction decisions with rationale.
|
||||
|
||||
## Good Abstractions (Why They Work)
|
||||
|
||||
### 1. Secp256k1Instance - Crypto Signing
|
||||
|
||||
**Location:** expect in commonMain, actual in androidMain/jvmMain/iosMain
|
||||
|
||||
**Code:**
|
||||
```kotlin
|
||||
// quartz/src/commonMain/.../Secp256k1Instance.kt
|
||||
expect object Secp256k1Instance {
|
||||
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
|
||||
fun verifySchnorr(signature: ByteArray, hash: ByteArray, pubKey: ByteArray): Boolean
|
||||
}
|
||||
```
|
||||
|
||||
**Why abstracted:**
|
||||
- Used by all platforms (Android, Desktop, iOS)
|
||||
- Security APIs fundamentally different:
|
||||
- Android: secp256k1-kmp-jni-android (Android Keystore integration)
|
||||
- Desktop: secp256k1-kmp-jni-jvm (pure JVM crypto)
|
||||
- iOS: Native Security framework
|
||||
- Core protocol requirement (Nostr signatures)
|
||||
|
||||
**Decision rationale:** Always abstract crypto - varies by platform security APIs, critical for all platforms.
|
||||
|
||||
---
|
||||
|
||||
### 2. Log - Platform Logging
|
||||
|
||||
**Location:** expect object in commonMain
|
||||
|
||||
**Code:**
|
||||
```kotlin
|
||||
// quartz/src/commonMain/.../Log.kt
|
||||
expect object Log {
|
||||
fun d(tag: String, message: String)
|
||||
fun w(tag: String, message: String, throwable: Throwable?)
|
||||
fun e(tag: String, message: String, throwable: Throwable?)
|
||||
}
|
||||
```
|
||||
|
||||
**Why abstracted:**
|
||||
- Used throughout quartz module (protocol library)
|
||||
- Logging systems differ:
|
||||
- Android: android.util.Log
|
||||
- Desktop: println or logging framework
|
||||
- iOS: NSLog or OSLog
|
||||
- Simple interface, easy to implement
|
||||
|
||||
**Decision rationale:** Often abstract logging - platform systems differ, widely used, simple interface.
|
||||
|
||||
---
|
||||
|
||||
### 3. Platform Utils - Time & Platform Name
|
||||
|
||||
**Location:** expect functions in commonMain
|
||||
|
||||
**Code:**
|
||||
```kotlin
|
||||
// quartz/src/commonMain/.../Platform.kt
|
||||
expect fun platform(): String
|
||||
expect fun currentTimeSeconds(): Long
|
||||
```
|
||||
|
||||
**Why abstracted:**
|
||||
- Used by Nostr event creation (timestamps)
|
||||
- Platform name for debugging
|
||||
- Simple utilities, clear platform boundary
|
||||
|
||||
**Decision rationale:** Platform utilities are good abstraction candidates - simple, useful everywhere.
|
||||
|
||||
---
|
||||
|
||||
### 4. Jackson JSON (jvmAndroid Pattern)
|
||||
|
||||
**Location:** jvmAndroid source set
|
||||
|
||||
**Code:**
|
||||
```kotlin
|
||||
// quartz/build.gradle.kts
|
||||
val jvmAndroid = create("jvmAndroid") {
|
||||
api(libs.jackson.module.kotlin) // JVM-only library
|
||||
}
|
||||
```
|
||||
|
||||
**Why jvmAndroid (not commonMain):**
|
||||
- Jackson is JVM-specific library
|
||||
- Works on Android (JVM) + Desktop (JVM)
|
||||
- Does NOT work on iOS (not JVM) or web (not JVM)
|
||||
- Performance-critical JSON parsing
|
||||
|
||||
**Decision rationale:** Use jvmAndroid for JVM libraries shared between Android and Desktop.
|
||||
|
||||
**Future consideration:** For web support, migrate to kotlinx.serialization (works on all platforms).
|
||||
|
||||
---
|
||||
|
||||
## Bad/Over-Abstractions (Why They Failed)
|
||||
|
||||
### 1. Navigation Abstraction (Avoided)
|
||||
|
||||
**What COULD have been done:**
|
||||
```kotlin
|
||||
// ❌ Over-abstraction - DON'T DO THIS
|
||||
expect interface Navigator {
|
||||
fun navigate(route: String)
|
||||
fun popBackStack()
|
||||
}
|
||||
```
|
||||
|
||||
**Why NOT abstracted:**
|
||||
- Navigation paradigms fundamentally different:
|
||||
- Android: Activity + Compose Navigation + back stack
|
||||
- Desktop: Window + screen state + no back stack concept
|
||||
- Complex APIs don't map well
|
||||
- Creates leaky abstraction
|
||||
|
||||
**Actual approach:** Keep platform-specific
|
||||
- Android: `INav` interface + Compose Navigation
|
||||
- Desktop: Simple screen enum + state
|
||||
|
||||
**Decision rationale:** Never abstract navigation - platforms too different, abstraction would be leaky.
|
||||
|
||||
---
|
||||
|
||||
### 2. String Resources (Abstraction Planned)
|
||||
|
||||
**Current state:** Platform-specific (over-duplication)
|
||||
|
||||
**Problem:**
|
||||
```kotlin
|
||||
// Android uses R.string.*
|
||||
Text(stringResource(R.string.post_not_found))
|
||||
|
||||
// Desktop uses hardcoded strings
|
||||
Text("Post not found")
|
||||
```
|
||||
|
||||
**Why NOT yet abstracted:** Waiting for second platform to fully implement UI, then will create StringProvider interface.
|
||||
|
||||
**Planned abstraction:**
|
||||
```kotlin
|
||||
// commonMain
|
||||
interface StringProvider {
|
||||
fun get(key: String): String
|
||||
}
|
||||
|
||||
// androidMain
|
||||
class AndroidStringProvider(context: Context): StringProvider { ... }
|
||||
|
||||
// jvmMain
|
||||
class DesktopStringProvider: StringProvider { ... }
|
||||
```
|
||||
|
||||
**Lesson:** Don't abstract prematurely - wait until second platform needs it, then create proper abstraction.
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Code (Why NOT Abstracted)
|
||||
|
||||
### 1. MainActivity (Android Activity)
|
||||
|
||||
**Location:** amethyst/src/main/.../MainActivity.kt
|
||||
|
||||
**Code:**
|
||||
```kotlin
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
AmethystTheme {
|
||||
AccountScreen(accountStateViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why platform-specific:**
|
||||
- AppCompatActivity is Android framework
|
||||
- Activity lifecycle unique to Android
|
||||
- enableEdgeToEdge() is Android-specific API
|
||||
- No equivalent on Desktop (uses Window)
|
||||
|
||||
**Decision rationale:** Android Activity is platform-specific by nature.
|
||||
|
||||
---
|
||||
|
||||
### 2. Desktop Window & MenuBar
|
||||
|
||||
**Location:** desktopApp/src/jvmMain/.../Main.kt
|
||||
|
||||
**Code:**
|
||||
```kotlin
|
||||
fun main() = application {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Amethyst"
|
||||
) {
|
||||
MenuBar {
|
||||
Menu("File") {
|
||||
Item("New Note", onClick = { ... }, shortcut = KeyShortcut(Key.N, ctrl = true))
|
||||
Item("Quit", onClick = ::exitApplication)
|
||||
}
|
||||
}
|
||||
NavigationRail { ... } // Sidebar navigation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why platform-specific:**
|
||||
- Window, MenuBar, NavigationRail are Compose Desktop APIs
|
||||
- Keyboard shortcuts (Ctrl+N) are desktop paradigm
|
||||
- Sidebar navigation vs Android bottom nav
|
||||
- No equivalent on Android
|
||||
|
||||
**Decision rationale:** Desktop UX patterns are platform-specific by nature.
|
||||
|
||||
---
|
||||
|
||||
### 3. AccountViewModel (Android ViewModel)
|
||||
|
||||
**Location:** amethyst/.../AccountStateViewModel.kt
|
||||
|
||||
**Partially abstracted:**
|
||||
- Business logic → IAccountState interface (can be shared)
|
||||
- UI state + lifecycle → AndroidX ViewModel (Android-only)
|
||||
|
||||
**Why not fully abstracted:**
|
||||
- AndroidX ViewModel lifecycle tied to Android
|
||||
- Desktop doesn't need ViewModel (simpler state management)
|
||||
- SavedStateHandle is Android-specific
|
||||
|
||||
**Decision rationale:** Extract business logic to interface, keep UI state platform-specific.
|
||||
|
||||
---
|
||||
|
||||
## Migration Examples (Android → Shared)
|
||||
|
||||
### Example 1: PubKeyFormatter (Pure Kotlin)
|
||||
|
||||
**Before:**
|
||||
```kotlin
|
||||
// amethyst/ui/note/PubKeyFormatter.kt
|
||||
fun String.toDisplayHexKey(): String {
|
||||
return "${take(8)}:${takeLast(8)}"
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```kotlin
|
||||
// commons/commonMain/formatters/PubKeyFormatter.kt
|
||||
fun String.toDisplayHexKey(): String {
|
||||
return "${take(8)}:${takeLast(8)}"
|
||||
}
|
||||
|
||||
// Both apps use it
|
||||
import com.vitorpamplona.amethyst.commons.formatters.toDisplayHexKey
|
||||
```
|
||||
|
||||
**Why successful:**
|
||||
- Pure Kotlin, no platform dependencies
|
||||
- Widely reused
|
||||
- Simple utility function
|
||||
|
||||
---
|
||||
|
||||
### Example 2: TimeAgoFormatter (Requires Abstraction)
|
||||
|
||||
**Problem:**
|
||||
```kotlin
|
||||
// Uses Android R.string.*
|
||||
fun timeAgo(timestamp: Long): String {
|
||||
return context.getString(R.string.x_minutes_ago, minutes)
|
||||
}
|
||||
```
|
||||
|
||||
**Solution:** Abstract string resources
|
||||
```kotlin
|
||||
// commonMain
|
||||
fun timeAgo(timestamp: Long, stringProvider: StringProvider): String {
|
||||
return stringProvider.get("x_minutes_ago", minutes)
|
||||
}
|
||||
|
||||
// androidMain
|
||||
stringProvider = AndroidStringProvider(context)
|
||||
|
||||
// jvmMain
|
||||
stringProvider = DesktopStringProvider()
|
||||
```
|
||||
|
||||
**Why successful:** Clear platform boundary (string resources), useful on both platforms.
|
||||
|
||||
---
|
||||
|
||||
## Decision Pattern Summary
|
||||
|
||||
| Pattern | Abstract? | Why |
|
||||
|---------|-----------|-----|
|
||||
| Pure Kotlin utilities | ✅ YES | No platform dependency, easy |
|
||||
| Crypto APIs | ✅ YES (expect/actual) | Platform security APIs differ |
|
||||
| JVM libraries | ⚠️ jvmAndroid | Works on Android+Desktop only |
|
||||
| UI components (simple) | ✅ YES | Composables work cross-platform |
|
||||
| UI components (complex) | ❌ NO | Platform dependencies |
|
||||
| Navigation | ❌ NO | Paradigms too different |
|
||||
| ViewModels | ⚠️ PARTIAL | Business logic yes, UI state no |
|
||||
| String resources | ⚠️ PLANNED | Needs abstraction layer |
|
||||
@@ -0,0 +1,163 @@
|
||||
# Complete expect/actual Catalog
|
||||
|
||||
All 24 expect declarations in Amethyst quartz module with rationale.
|
||||
|
||||
| # | Name | Type | Purpose | Why Abstracted | Files |
|
||||
|---|------|------|---------|----------------|-------|
|
||||
| 1 | AESCBC | class | AES CBC encryption | Platform crypto APIs differ | quartz/.../ciphers/AESCBC.kt |
|
||||
| 2 | AESGCM | class | AES GCM encryption | Platform crypto APIs differ | quartz/.../ciphers/AESGCM.kt |
|
||||
| 3 | DigestInstance | class | Hash digests (SHA256) | Platform implementations | quartz/.../diggest/DigestInstance.kt |
|
||||
| 4 | MacInstance | class | MAC (HMAC) operations | Platform crypto APIs | quartz/.../mac/MacInstance.kt |
|
||||
| 5 | Sha256 | object | SHA256 hashing | Platform-specific optimizations | quartz/.../sha256/Sha256.kt |
|
||||
| 6 | LargeCache | object | Large object caching | Platform storage APIs differ | quartz/.../cache/LargeCache.kt |
|
||||
| 7 | UriParser | object | URI parsing | Platform URL APIs differ | quartz/.../UriParser.kt |
|
||||
| 8 | UrlEncoder | object | URL encoding | Platform encoding differs | quartz/.../UrlEncoder.kt |
|
||||
| 9 | Urls | object | URL utilities | Platform URL handling | quartz/.../Urls.kt |
|
||||
| 10 | Platform | functions | platform(), currentTimeSeconds() | Platform name & time APIs | quartz/.../Platform.kt |
|
||||
| 11 | Rfc3986 | object | RFC 3986 URL normalization | Used in jvmAndroid | quartz/.../Rfc3986.kt |
|
||||
| 12 | Secp256k1Instance | object | Bitcoin crypto (secp256k1) | Different libs per platform | quartz/.../Secp256k1Instance.kt |
|
||||
| 13 | SecureRandom | object | Cryptographically secure random | Platform random APIs differ | quartz/.../SecureRandom.kt |
|
||||
| 14 | StringExt | functions | String utilities | Platform string handling | quartz/.../StringExt.kt |
|
||||
| 15 | UnicodeNormalizer | object | Unicode normalization | Platform text APIs | quartz/.../UnicodeNormalizer.kt |
|
||||
| 16 | GZip | object | GZip compression | Platform compression APIs | quartz/.../GZip.kt |
|
||||
| 17 | LibSodiumInstance | object | NaCl/libsodium (NIP-44 encryption) | Different libs per platform | quartz/.../LibSodiumInstance.kt |
|
||||
| 18 | Log | object | Logging | Platform logging systems | quartz/.../Log.kt |
|
||||
| 19 | BigDecimal | class | Arbitrary precision decimal | Not in Kotlin common stdlib | quartz/.../BigDecimal.kt |
|
||||
| 20 | BitSet | class | Bit set data structure | Not in Kotlin common stdlib | quartz/.../BitSet.kt |
|
||||
| 21 | ServerInfoParser | object | Server info parsing (NIP-96) | Platform JSON parsing | quartz/.../nip96.../ServerInfoParser.kt |
|
||||
| 22 | EventHasherSerializer | object | Event hashing | Platform-specific optimizations | quartz/.../nip01Core.../EventHasherSerializer.kt |
|
||||
| 23 | OptimizedJsonMapper | object | JSON mapping | Platform JSON libraries | quartz/.../nip01Core.../OptimizedJsonMapper.kt |
|
||||
| 24 | Address | data class | Address data structure | Platform-specific string handling | quartz/.../nip01Core.../Address.kt |
|
||||
|
||||
## Pattern Analysis
|
||||
|
||||
### Objects (Singletons) - 19 total
|
||||
Most common pattern for platform-specific singletons:
|
||||
- Crypto: Secp256k1Instance, LibSodiumInstance, Sha256
|
||||
- I/O: UriParser, UrlEncoder, GZip
|
||||
- Utils: Log, Platform, SecureRandom
|
||||
|
||||
### Classes (Instantiable) - 4 total
|
||||
For objects that need to maintain state:
|
||||
- AESCBC, AESGCM (cipher state)
|
||||
- DigestInstance, MacInstance (hash/MAC state)
|
||||
- BigDecimal, BitSet (data structures)
|
||||
|
||||
### Functions - 2 total
|
||||
Simple utilities:
|
||||
- platform(), currentTimeSeconds()
|
||||
|
||||
## Why Abstracted Categories
|
||||
|
||||
### Crypto (8 items)
|
||||
**Always abstract:** Security APIs fundamentally different across platforms
|
||||
- Android: Android Keystore, secp256k1-android
|
||||
- Desktop: JVM crypto, secp256k1-jvm
|
||||
- iOS: Security framework, native crypto
|
||||
|
||||
### I/O & Platform Utils (7 items)
|
||||
**Often abstract:** File systems, URLs, compression differ
|
||||
- Platform storage APIs
|
||||
- URL handling varies
|
||||
- Compression libraries differ
|
||||
|
||||
### Data Structures (2 items)
|
||||
**Abstract when missing:** Not available in Kotlin common stdlib
|
||||
- BigDecimal, BitSet not in common
|
||||
|
||||
### JSON/Parsing (3 items)
|
||||
**Platform-specific optimization:** Uses platform JSON libraries
|
||||
- Android/Desktop: Jackson (via jvmAndroid)
|
||||
- iOS: Native parsers
|
||||
|
||||
### Logging (1 item)
|
||||
**Always abstract:** Platform logging systems differ
|
||||
- Android: android.util.Log
|
||||
- Desktop: println or logging framework
|
||||
- iOS: NSLog or OSLog
|
||||
|
||||
## Actual Implementation Examples
|
||||
|
||||
### Simple Object Pattern
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect object Log {
|
||||
fun d(tag: String, message: String)
|
||||
}
|
||||
|
||||
// androidMain
|
||||
actual object Log {
|
||||
actual fun d(tag: String, message: String) {
|
||||
android.util.Log.d(tag, message)
|
||||
}
|
||||
}
|
||||
|
||||
// jvmMain
|
||||
actual object Log {
|
||||
actual fun d(tag: String, message: String) {
|
||||
println("[$tag] $message")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Object with Dependencies
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect object Secp256k1Instance {
|
||||
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
|
||||
}
|
||||
|
||||
// androidMain - uses JNI bindings
|
||||
actual object Secp256k1Instance {
|
||||
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
|
||||
return fr.acinq.secp256k1.Secp256k1.signSchnorr(data, privKey, null)
|
||||
}
|
||||
}
|
||||
|
||||
// jvmMain - different JNI library
|
||||
actual object Secp256k1Instance {
|
||||
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
|
||||
return fr.acinq.secp256k1.Secp256k1.signSchnorr(data, privKey, null)
|
||||
}
|
||||
}
|
||||
|
||||
// iosMain - native iOS implementation
|
||||
actual object Secp256k1Instance {
|
||||
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
|
||||
// Uses iOS Security framework or native lib
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Class Pattern
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect class BigDecimal {
|
||||
constructor(value: String)
|
||||
fun add(other: BigDecimal): BigDecimal
|
||||
override fun toString(): String
|
||||
}
|
||||
|
||||
// jvmAndroid (works on Android + Desktop)
|
||||
actual typealias BigDecimal = java.math.BigDecimal
|
||||
|
||||
// iosMain
|
||||
actual class BigDecimal {
|
||||
private val value: NSDecimalNumber
|
||||
actual constructor(value: String) {
|
||||
this.value = NSDecimalNumber(value)
|
||||
}
|
||||
// ... implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Decision Patterns
|
||||
|
||||
Ask for each declaration:
|
||||
1. **Used by 2+ platforms?** → YES (otherwise platform-specific)
|
||||
2. **Pure Kotlin possible?** → NO (otherwise commonMain)
|
||||
3. **Varies by platform?** → YES (expect/actual)
|
||||
4. **JVM-only library?** → NO (otherwise jvmAndroid)
|
||||
@@ -0,0 +1,332 @@
|
||||
# Source Set Hierarchy in Amethyst
|
||||
|
||||
Visual guide to source set organization with concrete examples from the codebase.
|
||||
|
||||
## Hierarchy Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ commonMain │
|
||||
│ Pure Kotlin, no platform APIs │
|
||||
│ Examples: │
|
||||
│ - Nostr event parsing (TextNoteEvent, MetadataEvent) │
|
||||
│ - Business logic (data validation, crypto algorithms) │
|
||||
│ - Data models (@Immutable data classes) │
|
||||
│ Dependencies: kotlin-stdlib, kotlinx-coroutines │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌────────────┴────────────┬───────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐
|
||||
│ jvmAndroid │ │ iosMain │ │ Future: │
|
||||
│ JVM libraries │ │ iOS common │ │ jsMain │
|
||||
│ Examples: │ │ Examples: │ │ wasmMain │
|
||||
│ - Jackson JSON │ │ - Platform API │ └──────────────┘
|
||||
│ - OkHttp HTTP │ │ - Actuals for │
|
||||
│ - url-detector │ │ crypto/I/O │
|
||||
│ Dependencies: │ │ Dependencies: │
|
||||
│ - Jackson │ │ - Platform libs │
|
||||
│ - OkHttp │ └───────┬───────────┘
|
||||
└────┬─────────┬───┘ │
|
||||
│ │ ├─→ iosX64Main (simulator Intel)
|
||||
│ │ ├─→ iosArm64Main (device ARM64)
|
||||
│ │ └─→ iosSimulatorArm64Main (Apple Silicon)
|
||||
▼ ▼
|
||||
┌──────────┐ ┌───────────┐
|
||||
│android │ │ jvmMain │
|
||||
│Main │ │ (Desktop) │
|
||||
│Examples: │ │ Examples: │
|
||||
│- Activity│ │- Window │
|
||||
│- ViewModel│ │- MenuBar │
|
||||
│- Android │ │- Desktop │
|
||||
│ APIs │ │ Compose │
|
||||
│Deps: │ │ Deps: │
|
||||
│- secp256k│ │- secp256k │
|
||||
│ 1-android│ │ 1-jvm │
|
||||
│- androidx│ │- Compose │
|
||||
│ │ │ Desktop │
|
||||
└──────────┘ └───────────┘
|
||||
```
|
||||
|
||||
## Dependency Flow
|
||||
|
||||
```
|
||||
Code in commonMain
|
||||
↓ can use
|
||||
Nothing (only Kotlin stdlib)
|
||||
|
||||
Code in jvmAndroid
|
||||
↓ can use
|
||||
commonMain + JVM libraries (Jackson, OkHttp)
|
||||
|
||||
Code in androidMain
|
||||
↓ can use
|
||||
commonMain + jvmAndroid + Android framework
|
||||
|
||||
Code in jvmMain
|
||||
↓ can use
|
||||
commonMain + jvmAndroid + JVM + Compose Desktop
|
||||
|
||||
Code in iosMain
|
||||
↓ can use
|
||||
commonMain + iOS platform APIs
|
||||
```
|
||||
|
||||
## Real Examples from Amethyst
|
||||
|
||||
### commonMain - Pure Kotlin
|
||||
|
||||
**File:** `quartz/src/commonMain/.../TextNoteEvent.kt`
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
class TextNoteEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseThreadedEvent(...) {
|
||||
// Pure Kotlin - works everywhere
|
||||
override fun indexableContent() = "Subject: " + subject() + "\n" + content
|
||||
}
|
||||
```
|
||||
|
||||
**Why commonMain:**
|
||||
- Pure Kotlin code
|
||||
- No platform APIs
|
||||
- Data class with business logic
|
||||
- Needed by all platforms
|
||||
|
||||
---
|
||||
|
||||
### jvmAndroid - JVM Libraries
|
||||
|
||||
**File:** `quartz/build.gradle.kts`
|
||||
|
||||
```kotlin
|
||||
val jvmAndroid = create("jvmAndroid") {
|
||||
dependsOn(commonMain.get())
|
||||
|
||||
dependencies {
|
||||
// Normalizes URLs
|
||||
api(libs.rfc3986.normalizer)
|
||||
|
||||
// Performant Parser of JSONs into Events
|
||||
api(libs.jackson.module.kotlin)
|
||||
|
||||
// Parses URLs from Text
|
||||
api(libs.url.detector)
|
||||
|
||||
// Websockets API
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttpCoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
jvmMain { dependsOn(jvmAndroid) } // Desktop gets Jackson, OkHttp
|
||||
androidMain { dependsOn(jvmAndroid) } // Android gets Jackson, OkHttp
|
||||
```
|
||||
|
||||
**Why jvmAndroid:**
|
||||
- Jackson, OkHttp are JVM-only libraries
|
||||
- Works on Android (JVM) and Desktop (JVM)
|
||||
- Does NOT work on iOS (not JVM) or web (not JVM)
|
||||
|
||||
**Usage in code:**
|
||||
```kotlin
|
||||
// Can use Jackson in jvmAndroid source set
|
||||
val mapper = ObjectMapper()
|
||||
val event = mapper.readValue(json, Event::class.java)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### androidMain - Android Platform
|
||||
|
||||
**File:** `amethyst/src/main/.../MainActivity.kt`
|
||||
|
||||
```kotlin
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge() // Android API
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent { // Compose for Android
|
||||
AmethystTheme {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel()
|
||||
AccountScreen(accountStateViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why androidMain:**
|
||||
- AppCompatActivity is Android framework
|
||||
- Activity lifecycle Android-specific
|
||||
- AndroidX libraries (viewModel())
|
||||
|
||||
**Dependencies:**
|
||||
```kotlin
|
||||
androidMain {
|
||||
dependsOn(jvmAndroid) // Gets Jackson, OkHttp
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
api(libs.secp256k1.kmp.jni.android) // Android crypto
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### jvmMain - Desktop Platform
|
||||
|
||||
**File:** `desktopApp/src/jvmMain/.../Main.kt`
|
||||
|
||||
```kotlin
|
||||
fun main() = application {
|
||||
val windowState = rememberWindowState(
|
||||
width = 1200.dp,
|
||||
height = 800.dp
|
||||
)
|
||||
|
||||
Window( // Compose Desktop API
|
||||
onCloseRequest = ::exitApplication,
|
||||
state = windowState,
|
||||
title = "Amethyst"
|
||||
) {
|
||||
MenuBar { // Desktop-specific
|
||||
Menu("File") {
|
||||
Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true))
|
||||
}
|
||||
}
|
||||
NavigationRail { ... } // Sidebar
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why jvmMain:**
|
||||
- Window, MenuBar, NavigationRail are Compose Desktop
|
||||
- Keyboard shortcuts desktop paradigm
|
||||
- Different UX from Android (sidebar vs bottom nav)
|
||||
|
||||
**Dependencies:**
|
||||
```kotlin
|
||||
jvmMain {
|
||||
dependsOn(jvmAndroid) // Gets Jackson, OkHttp
|
||||
dependencies {
|
||||
implementation(libs.secp256k1.kmp.jni.jvm) // Desktop crypto
|
||||
implementation(compose.desktop.currentOs)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### iosMain - iOS Platform
|
||||
|
||||
**File:** `quartz/build.gradle.kts`
|
||||
|
||||
```kotlin
|
||||
iosMain {
|
||||
dependsOn(commonMain.get())
|
||||
dependencies {
|
||||
// iOS platform dependencies
|
||||
}
|
||||
}
|
||||
|
||||
val iosX64Main by getting { dependsOn(iosMain.get()) }
|
||||
val iosArm64Main by getting { dependsOn(iosMain.get()) }
|
||||
val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) }
|
||||
```
|
||||
|
||||
**Why iosMain:**
|
||||
- iOS platform APIs
|
||||
- Native crypto (Security framework)
|
||||
- Different from Android/Desktop
|
||||
|
||||
**Architecture targets:**
|
||||
- iosX64Main: Intel simulator
|
||||
- iosArm64Main: Device (iPhone, iPad)
|
||||
- iosSimulatorArm64Main: Apple Silicon simulator
|
||||
|
||||
---
|
||||
|
||||
## Build Order Matters
|
||||
|
||||
**CRITICAL:** jvmAndroid must be defined BEFORE androidMain and jvmMain:
|
||||
|
||||
```kotlin
|
||||
// ✅ CORRECT ORDER
|
||||
val jvmAndroid = create("jvmAndroid") { ... }
|
||||
jvmMain { dependsOn(jvmAndroid) }
|
||||
androidMain { dependsOn(jvmAndroid) }
|
||||
|
||||
// ❌ WRONG - Build error
|
||||
androidMain { dependsOn(jvmAndroid) } // jvmAndroid not defined yet!
|
||||
val jvmAndroid = create("jvmAndroid") { ... }
|
||||
```
|
||||
|
||||
See comment in quartz/build.gradle.kts:131:
|
||||
```kotlin
|
||||
// Must be defined before androidMain and jvmMain
|
||||
val jvmAndroid = create("jvmAndroid") { ... }
|
||||
```
|
||||
|
||||
## Choosing the Right Source Set
|
||||
|
||||
Decision flowchart:
|
||||
|
||||
```
|
||||
Q: Where should this code go?
|
||||
|
||||
├─ Pure Kotlin? (no platform APIs)
|
||||
│ └─ commonMain
|
||||
│
|
||||
├─ JVM library? (Jackson, OkHttp)
|
||||
│ └─ jvmAndroid
|
||||
│
|
||||
├─ Android API? (Activity, Context)
|
||||
│ └─ androidMain
|
||||
│
|
||||
├─ Desktop API? (Window, MenuBar)
|
||||
│ └─ jvmMain
|
||||
│
|
||||
└─ iOS API? (platform.posix, Security)
|
||||
└─ iosMain
|
||||
```
|
||||
|
||||
## Future: Web/wasm Source Sets
|
||||
|
||||
**Not yet implemented**, but structure would be:
|
||||
|
||||
```
|
||||
commonMain
|
||||
├─→ jsMain (JavaScript/Web)
|
||||
│ └─ JS-specific: DOM APIs, fetch
|
||||
│
|
||||
└─→ wasmMain (WebAssembly)
|
||||
└─ wasm-specific: limited APIs
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- Cannot use jvmAndroid (Jackson, OkHttp)
|
||||
- Cannot use platform.posix
|
||||
- Must use pure Kotlin or web-compatible libs (ktor, kotlinx.serialization)
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Source Set | Extends | Can Use | Example Code |
|
||||
|------------|---------|---------|--------------|
|
||||
| commonMain | - | Kotlin stdlib only | TextNoteEvent, business logic |
|
||||
| jvmAndroid | commonMain | JVM libs (Jackson, OkHttp) | JSON parsing, HTTP |
|
||||
| androidMain | jvmAndroid | Android framework | Activity, ViewModel |
|
||||
| jvmMain | jvmAndroid | JVM + Compose Desktop | Window, MenuBar |
|
||||
| iosMain | commonMain | iOS platform | Security framework |
|
||||
| iosX64Main | iosMain | Simulator (Intel) | Architecture-specific |
|
||||
| iosArm64Main | iosMain | Device (ARM64) | Architecture-specific |
|
||||
| jsMain | commonMain | JS/DOM | Web (future) |
|
||||
| wasmMain | commonMain | wasm APIs | WebAssembly (future) |
|
||||
@@ -0,0 +1,345 @@
|
||||
# Target Compatibility Guide
|
||||
|
||||
Current targets (Android, JVM/Desktop, iOS) and future targets (web, wasm) with constraints.
|
||||
|
||||
## Current Primary Targets
|
||||
|
||||
### Android (androidMain)
|
||||
|
||||
**Status:** ✅ Mature, production-ready
|
||||
|
||||
**Runtime:** JVM (Dalvik/ART)
|
||||
|
||||
**Available:**
|
||||
- Android framework (Activity, Context, Intent, etc.)
|
||||
- AndroidX libraries (ViewModel, Navigation, etc.)
|
||||
- JVM libraries via jvmAndroid (Jackson, OkHttp)
|
||||
- Platform-specific crypto: secp256k1-kmp-jni-android
|
||||
|
||||
**Constraints:**
|
||||
- Mobile UX paradigms (bottom navigation, vertical scroll)
|
||||
- Touch-first interaction
|
||||
- Limited screen space
|
||||
- Battery/performance constraints
|
||||
|
||||
**Example code:**
|
||||
```kotlin
|
||||
// androidMain
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Android-specific lifecycle
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### JVM / Desktop (jvmMain)
|
||||
|
||||
**Status:** ✅ Active development, functional
|
||||
|
||||
**Runtime:** JVM
|
||||
|
||||
**Available:**
|
||||
- Pure JVM libraries
|
||||
- JVM libraries via jvmAndroid (Jackson, OkHttp)
|
||||
- Compose Desktop (Window, MenuBar, etc.)
|
||||
- Platform-specific crypto: secp256k1-kmp-jni-jvm
|
||||
|
||||
**Constraints:**
|
||||
- Desktop UX paradigms (sidebar, menus, keyboard shortcuts)
|
||||
- Keyboard + mouse interaction
|
||||
- Larger screen space
|
||||
- Different navigation patterns (no back stack)
|
||||
|
||||
**Example code:**
|
||||
```kotlin
|
||||
// jvmMain
|
||||
fun main() = application {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Amethyst"
|
||||
) {
|
||||
MenuBar { ... } // Desktop-specific
|
||||
NavigationRail { ... } // Sidebar
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### iOS (iosMain + architecture targets)
|
||||
|
||||
**Status:** ⚠️ In development, framework configured
|
||||
|
||||
**Runtime:** Native iOS
|
||||
|
||||
**Source sets:**
|
||||
- iosMain (common iOS code)
|
||||
- iosX64Main (Intel simulator)
|
||||
- iosArm64Main (device - iPhone/iPad)
|
||||
- iosSimulatorArm64Main (Apple Silicon simulator)
|
||||
|
||||
**Available:**
|
||||
- iOS platform APIs (platform.posix, Foundation, etc.)
|
||||
- Native crypto (Security framework)
|
||||
- SwiftUI integration (via KMP framework)
|
||||
|
||||
**NOT available:**
|
||||
- JVM libraries (Jackson, OkHttp)
|
||||
- jvmAndroid source set
|
||||
- JVM-specific APIs
|
||||
|
||||
**Constraints:**
|
||||
- Mobile UX (similar to Android)
|
||||
- Swift/Objective-C interop
|
||||
- XCFramework distribution
|
||||
- CocoaPods integration
|
||||
|
||||
**Example code:**
|
||||
```kotlin
|
||||
// iosMain
|
||||
actual object Secp256k1Instance {
|
||||
actual fun signSchnorr(...): ByteArray {
|
||||
// Use iOS Security framework
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**XCFramework setup:**
|
||||
```kotlin
|
||||
// quartz/build.gradle.kts
|
||||
kotlin {
|
||||
listOf(iosX64(), iosArm64(), iosSimulatorArm64())
|
||||
.forEach { target ->
|
||||
target.binaries.framework {
|
||||
baseName = "quartz-kmpKit"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Targets
|
||||
|
||||
### Web / JavaScript (jsMain)
|
||||
|
||||
**Status:** ❌ Not implemented, consider for future
|
||||
|
||||
**Runtime:** JavaScript (browser or Node.js)
|
||||
|
||||
**Available:**
|
||||
- Kotlin/JS stdlib
|
||||
- JS/DOM APIs
|
||||
- kotlinx.* libraries (serialization, coroutines, datetime)
|
||||
- ktor-client (HTTP)
|
||||
|
||||
**NOT available:**
|
||||
- ❌ JVM libraries (Jackson, OkHttp)
|
||||
- ❌ jvmAndroid source set
|
||||
- ❌ platform.posix (no file system access like native)
|
||||
- ❌ Blocking APIs (different async model - JS event loop)
|
||||
|
||||
**Constraints:**
|
||||
- Single-threaded event loop
|
||||
- No blocking calls
|
||||
- Different async patterns (Promises, async/await)
|
||||
- Browser security (CORS, no file system)
|
||||
|
||||
**Migration path from current code:**
|
||||
|
||||
| Current (jvmAndroid) | Web-compatible alternative |
|
||||
|---------------------|---------------------------|
|
||||
| Jackson JSON | kotlinx.serialization |
|
||||
| OkHttp HTTP | ktor-client |
|
||||
| java.math.BigDecimal | Kotlin BigDecimal (coming) |
|
||||
| Blocking I/O | Suspending functions |
|
||||
|
||||
**Example migration:**
|
||||
```kotlin
|
||||
// Current: jvmAndroid
|
||||
val mapper = ObjectMapper()
|
||||
val event = mapper.readValue(json, Event::class.java)
|
||||
|
||||
// Future: commonMain (works on web)
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val event = json.decodeFromString<Event>(jsonString)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### WebAssembly (wasmMain)
|
||||
|
||||
**Status:** ❌ Not implemented, experimental Kotlin/Wasm
|
||||
|
||||
**Runtime:** WebAssembly
|
||||
|
||||
**Available:**
|
||||
- Kotlin/Wasm stdlib
|
||||
- Limited kotlinx.* libraries
|
||||
- wasm-specific APIs
|
||||
|
||||
**NOT available:**
|
||||
- ❌ JVM libraries
|
||||
- ❌ Full platform.posix
|
||||
- ❌ Many kotlinx libraries (limited wasm support)
|
||||
|
||||
**Constraints:**
|
||||
- Even more limited than JS
|
||||
- Experimental Kotlin support
|
||||
- Limited library ecosystem
|
||||
|
||||
**Recommendation:** Focus on web (jsMain) first, wasm later.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Target Compatibility Matrix
|
||||
|
||||
| Feature | Android | JVM/Desktop | iOS | Web (JS) | wasm |
|
||||
|---------|---------|-------------|-----|----------|------|
|
||||
| Pure Kotlin | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| kotlinx.coroutines | ✅ | ✅ | ✅ | ✅ | ⚠️ |
|
||||
| kotlinx.serialization | ✅ | ✅ | ✅ | ✅ | ⚠️ |
|
||||
| kotlinx.datetime | ✅ | ✅ | ✅ | ✅ | ⚠️ |
|
||||
| ktor-client | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Jackson JSON | ✅ (jvmAndroid) | ✅ (jvmAndroid) | ❌ | ❌ | ❌ |
|
||||
| OkHttp | ✅ (jvmAndroid) | ✅ (jvmAndroid) | ❌ | ❌ | ❌ |
|
||||
| platform.posix | ❌ | ❌ | ✅ | ❌ | ⚠️ |
|
||||
| Compose Multiplatform | ✅ | ✅ | ⚠️ (experimental) | ⚠️ (experimental) | ❌ |
|
||||
|
||||
Legend:
|
||||
- ✅ Full support
|
||||
- ⚠️ Limited/experimental
|
||||
- ❌ Not available
|
||||
|
||||
---
|
||||
|
||||
## Future-Proofing Recommendations
|
||||
|
||||
### For Web Compatibility
|
||||
|
||||
**DO:**
|
||||
- ✅ Use kotlinx.serialization instead of Jackson
|
||||
- ✅ Use ktor-client instead of OkHttp
|
||||
- ✅ Use kotlinx.datetime instead of java.time
|
||||
- ✅ Use suspending functions (non-blocking)
|
||||
- ✅ Keep business logic in commonMain
|
||||
|
||||
**DON'T:**
|
||||
- ❌ Put JVM libraries in commonMain
|
||||
- ❌ Use platform.posix for critical features
|
||||
- ❌ Use blocking I/O
|
||||
- ❌ Depend on threading (use coroutines)
|
||||
|
||||
**Example:**
|
||||
```kotlin
|
||||
// ❌ NOT web-compatible
|
||||
// jvmAndroid
|
||||
fun parseJson(json: String): Event {
|
||||
val mapper = ObjectMapper() // Jackson - JVM only
|
||||
return mapper.readValue(json, Event::class.java)
|
||||
}
|
||||
|
||||
// ✅ Web-compatible
|
||||
// commonMain
|
||||
@Serializable
|
||||
data class Event(...)
|
||||
|
||||
fun parseJson(json: String): Event {
|
||||
return Json.decodeFromString<Event>(json) // Works everywhere
|
||||
}
|
||||
```
|
||||
|
||||
### Current Migration Priorities
|
||||
|
||||
**High priority:** (Needed for web)
|
||||
1. Migrate Jackson → kotlinx.serialization
|
||||
2. Migrate OkHttp → ktor-client
|
||||
3. Move business logic to commonMain
|
||||
|
||||
**Medium priority:** (Nice to have)
|
||||
1. Abstract date/time handling → kotlinx.datetime
|
||||
2. Remove platform.posix usage where possible
|
||||
3. Use suspending functions over blocking
|
||||
|
||||
**Low priority:** (Future optimization)
|
||||
1. wasm-specific optimizations
|
||||
2. Platform-specific performance tuning
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Patterns
|
||||
|
||||
### Android vs iOS Differences
|
||||
|
||||
| Aspect | Android | iOS |
|
||||
|--------|---------|-----|
|
||||
| **Activity/ViewController** | Activity | UIViewController |
|
||||
| **Navigation** | Compose Navigation | UINavigationController |
|
||||
| **Lifecycle** | onCreate, onResume, etc. | viewDidLoad, viewWillAppear |
|
||||
| **Permissions** | Runtime permissions | Info.plist + runtime |
|
||||
| **Crypto** | secp256k1-android | Security framework |
|
||||
| **Storage** | Room, SharedPreferences | Core Data, UserDefaults |
|
||||
|
||||
### Desktop vs Mobile Differences
|
||||
|
||||
| Aspect | Desktop | Mobile |
|
||||
|--------|---------|--------|
|
||||
| **Navigation** | Sidebar | Bottom nav |
|
||||
| **Input** | Keyboard + mouse | Touch |
|
||||
| **Screen** | Large, landscape | Small, portrait |
|
||||
| **Windows** | Multi-window | Single app |
|
||||
| **Shortcuts** | Keyboard shortcuts (Ctrl+N) | None |
|
||||
| **Menus** | MenuBar | Bottom sheets |
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Per-Target Testing
|
||||
|
||||
**Android:**
|
||||
- Unit tests: androidTest
|
||||
- Instrumented: androidInstrumentedTest
|
||||
- Device/emulator testing
|
||||
|
||||
**Desktop:**
|
||||
- Unit tests: jvmTest
|
||||
- Manual desktop app testing
|
||||
|
||||
**iOS:**
|
||||
- Unit tests: iosTest (iosX64Test, iosArm64Test, etc.)
|
||||
- Simulator/device testing
|
||||
|
||||
**Web (future):**
|
||||
- Unit tests: jsTest
|
||||
- Browser testing (Selenium, Playwright)
|
||||
|
||||
### Shared Testing
|
||||
|
||||
**commonTest:**
|
||||
- Business logic tests
|
||||
- Pure Kotlin code
|
||||
- Works on all platforms
|
||||
|
||||
```kotlin
|
||||
// commonTest
|
||||
class EventParsingTest {
|
||||
@Test
|
||||
fun parseTextNoteEvent() {
|
||||
// Tests run on all platforms
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Current Focus:** Android, JVM/Desktop, iOS (active development)
|
||||
|
||||
**Future Considerations:** Web (requires migration from Jackson/OkHttp)
|
||||
|
||||
**Key Decision:** Prefer kotlinx.* libraries over JVM-specific libs for future web compatibility.
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/bin/bash
|
||||
# Suggests KMP library alternatives for JVM-specific dependencies
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_ROOT="${1:-.}"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "=== KMP Dependency Suggestions ==="
|
||||
echo
|
||||
|
||||
# Colors
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
SUGGESTIONS_FOUND=0
|
||||
|
||||
# Check for Jackson (suggest kotlinx.serialization)
|
||||
echo "📦 Checking for Jackson JSON..."
|
||||
if grep -r "jackson" */build.gradle.kts 2>/dev/null | grep -q "implementation\|api"; then
|
||||
echo -e "${YELLOW}⚠ Found Jackson dependency${NC}"
|
||||
echo " Current: Jackson (JVM-only)"
|
||||
echo -e " ${GREEN}Suggest: kotlinx.serialization${NC} (works on all platforms)"
|
||||
echo
|
||||
echo " Migration:"
|
||||
echo " // Remove:"
|
||||
echo " api(libs.jackson.module.kotlin)"
|
||||
echo
|
||||
echo " // Add to commonMain:"
|
||||
echo " implementation(libs.kotlinx.serialization.json)"
|
||||
echo
|
||||
echo " // Code change:"
|
||||
echo " // Before (Jackson):"
|
||||
echo " val mapper = ObjectMapper()"
|
||||
echo " val event = mapper.readValue(json, Event::class.java)"
|
||||
echo
|
||||
echo " // After (kotlinx.serialization):"
|
||||
echo " @Serializable"
|
||||
echo " data class Event(...)"
|
||||
echo " val event = Json.decodeFromString<Event>(json)"
|
||||
echo
|
||||
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
|
||||
else
|
||||
echo -e "${GREEN}✓ Not using Jackson (or already using kotlinx.serialization)${NC}"
|
||||
fi
|
||||
|
||||
# Check for OkHttp (suggest ktor)
|
||||
echo
|
||||
echo "📦 Checking for OkHttp..."
|
||||
if grep -r "okhttp" */build.gradle.kts 2>/dev/null | grep -q "implementation\|api"; then
|
||||
echo -e "${YELLOW}⚠ Found OkHttp dependency${NC}"
|
||||
echo " Current: OkHttp (JVM-only)"
|
||||
echo -e " ${GREEN}Suggest: ktor-client${NC} (works on all platforms)"
|
||||
echo
|
||||
echo " Migration:"
|
||||
echo " // Remove:"
|
||||
echo " implementation(libs.okhttp)"
|
||||
echo
|
||||
echo " // Add to commonMain:"
|
||||
echo " implementation(libs.ktor.client.core)"
|
||||
echo " // Platform-specific engines:"
|
||||
echo " // androidMain: implementation(libs.ktor.client.android)"
|
||||
echo " // jvmMain: implementation(libs.ktor.client.cio)"
|
||||
echo " // iosMain: implementation(libs.ktor.client.darwin)"
|
||||
echo
|
||||
echo " // Code change:"
|
||||
echo " // Before (OkHttp):"
|
||||
echo " val client = OkHttpClient()"
|
||||
echo " val request = Request.Builder().url(url).build()"
|
||||
echo " val response = client.newCall(request).execute()"
|
||||
echo
|
||||
echo " // After (ktor):"
|
||||
echo " val client = HttpClient()"
|
||||
echo " val response: String = client.get(url)"
|
||||
echo
|
||||
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
|
||||
else
|
||||
echo -e "${GREEN}✓ Not using OkHttp (or already using ktor)${NC}"
|
||||
fi
|
||||
|
||||
# Check for java.time (suggest kotlinx.datetime)
|
||||
echo
|
||||
echo "📦 Checking for java.time usage..."
|
||||
if find */src -name "*.kt" 2>/dev/null | xargs grep -l "import java.time\." >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}⚠ Found java.time imports${NC}"
|
||||
echo " Current: java.time (JVM-only)"
|
||||
echo -e " ${GREEN}Suggest: kotlinx.datetime${NC} (works on all platforms)"
|
||||
echo
|
||||
echo " Migration:"
|
||||
echo " // Add to commonMain:"
|
||||
echo " implementation(libs.kotlinx.datetime)"
|
||||
echo
|
||||
echo " // Code change:"
|
||||
echo " // Before (java.time):"
|
||||
echo " import java.time.Instant"
|
||||
echo " val now = Instant.now()"
|
||||
echo
|
||||
echo " // After (kotlinx.datetime):"
|
||||
echo " import kotlinx.datetime.Clock"
|
||||
echo " val now = Clock.System.now()"
|
||||
echo
|
||||
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
|
||||
else
|
||||
echo -e "${GREEN}✓ Not using java.time (or already using kotlinx.datetime)${NC}"
|
||||
fi
|
||||
|
||||
# Check for java.math.BigDecimal
|
||||
echo
|
||||
echo "📦 Checking for java.math.BigDecimal usage..."
|
||||
if find */src -name "*.kt" 2>/dev/null | xargs grep -l "import java.math.BigDecimal" >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}⚠ Found java.math.BigDecimal imports${NC}"
|
||||
echo " Current: java.math.BigDecimal (JVM-only)"
|
||||
echo -e " ${BLUE}Note:${NC} KMP BigDecimal not yet in stable kotlinx"
|
||||
echo
|
||||
echo " Options:"
|
||||
echo " 1. Use expect/actual (current approach in quartz)"
|
||||
echo " 2. Wait for kotlinx.decimal (proposal stage)"
|
||||
echo " 3. Use third-party KMP library (e.g., bignum)"
|
||||
echo
|
||||
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
|
||||
else
|
||||
echo -e "${GREEN}✓ Not using java.math.BigDecimal directly${NC}"
|
||||
fi
|
||||
|
||||
# Check for platform.posix usage
|
||||
echo
|
||||
echo "📦 Checking for platform.posix usage..."
|
||||
if find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "import platform.posix\." >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}⚠ Found platform.posix in commonMain${NC}"
|
||||
echo " Current: platform.posix (native platforms only, not web)"
|
||||
echo -e " ${GREEN}Suggest:${NC} Abstract file I/O with expect/actual"
|
||||
echo
|
||||
echo " For web compatibility:"
|
||||
echo " - iOS/Native: platform.posix"
|
||||
echo " - Web: Use kotlinx-io or ktor file APIs"
|
||||
echo " - Create expect/actual for file operations"
|
||||
echo
|
||||
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
|
||||
else
|
||||
echo -e "${GREEN}✓ Not using platform.posix in commonMain${NC}"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo
|
||||
echo "=== Summary ==="
|
||||
if [ "$SUGGESTIONS_FOUND" -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ No JVM-specific dependencies found!${NC}"
|
||||
echo " Your code is ready for web/wasm targets."
|
||||
else
|
||||
echo -e "${YELLOW}Found $SUGGESTIONS_FOUND suggestion(s) for KMP alternatives${NC}"
|
||||
echo
|
||||
echo "Priority recommendations:"
|
||||
echo " 1. ${GREEN}High:${NC} Jackson → kotlinx.serialization (enables web support)"
|
||||
echo " 2. ${GREEN}High:${NC} OkHttp → ktor-client (enables web support)"
|
||||
echo " 3. ${GREEN}Medium:${NC} java.time → kotlinx.datetime"
|
||||
echo " 4. ${GREEN}Low:${NC} Consider web compatibility for platform.posix usage"
|
||||
echo
|
||||
echo "Resources:"
|
||||
echo " - kotlinx.serialization: https://github.com/Kotlin/kotlinx.serialization"
|
||||
echo " - ktor: https://ktor.io/docs/client.html"
|
||||
echo " - kotlinx.datetime: https://github.com/Kotlin/kotlinx-datetime"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
# Validates KMP source set structure and detects common issues
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_ROOT="${1:-.}"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "=== Validating KMP Structure ==="
|
||||
echo
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
ISSUES_FOUND=0
|
||||
|
||||
# Check 1: jvmAndroid defined before androidMain/jvmMain
|
||||
echo "📋 Checking source set definition order..."
|
||||
if [ -f "quartz/build.gradle.kts" ]; then
|
||||
jvmandroid_line=$(grep -n "val jvmAndroid = create" quartz/build.gradle.kts | cut -d: -f1)
|
||||
android_line=$(grep -n "androidMain {" quartz/build.gradle.kts | cut -d: -f1)
|
||||
jvm_line=$(grep -n "jvmMain {" quartz/build.gradle.kts | cut -d: -f1)
|
||||
|
||||
if [ -n "$jvmandroid_line" ] && [ -n "$android_line" ] && [ -n "$jvm_line" ]; then
|
||||
if [ "$jvmandroid_line" -lt "$android_line" ] && [ "$jvmandroid_line" -lt "$jvm_line" ]; then
|
||||
echo -e "${GREEN}✓${NC} jvmAndroid defined before androidMain and jvmMain"
|
||||
else
|
||||
echo -e "${RED}✗${NC} jvmAndroid must be defined BEFORE androidMain and jvmMain"
|
||||
ISSUES_FOUND=$((ISSUES_FOUND + 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check 2: Platform code in commonMain (Android imports)
|
||||
echo
|
||||
echo "📋 Checking for platform code in commonMain..."
|
||||
android_imports_in_common=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^import android\." || true)
|
||||
if [ -n "$android_imports_in_common" ]; then
|
||||
echo -e "${RED}✗${NC} Found Android imports in commonMain:"
|
||||
echo "$android_imports_in_common" | sed 's/^/ /'
|
||||
echo " Fix: Move to androidMain or create expect/actual"
|
||||
ISSUES_FOUND=$((ISSUES_FOUND + 1))
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} No Android imports in commonMain"
|
||||
fi
|
||||
|
||||
# Check 3: JVM libraries in commonMain (Jackson, OkHttp)
|
||||
echo
|
||||
echo "📋 Checking for JVM libraries in commonMain..."
|
||||
jvm_imports_in_common=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^import com.fasterxml.jackson\|^import okhttp3\." || true)
|
||||
if [ -n "$jvm_imports_in_common" ]; then
|
||||
echo -e "${RED}✗${NC} Found JVM library imports in commonMain:"
|
||||
echo "$jvm_imports_in_common" | sed 's/^/ /'
|
||||
echo " Fix: Move to jvmAndroid or migrate to kotlinx.serialization/ktor"
|
||||
ISSUES_FOUND=$((ISSUES_FOUND + 1))
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} No JVM library imports in commonMain"
|
||||
fi
|
||||
|
||||
# Check 4: Unmatched expect/actual declarations
|
||||
echo
|
||||
echo "📋 Checking expect/actual pairs..."
|
||||
expect_files=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^expect " || true)
|
||||
if [ -n "$expect_files" ]; then
|
||||
for file in $expect_files; do
|
||||
# Extract declarations
|
||||
expects=$(grep "^expect \(class\|object\|fun\|interface\)" "$file" | sed 's/expect //' | awk '{print $2}' | sed 's/[({].*$//')
|
||||
|
||||
# Check for actuals in platform source sets
|
||||
for expect_name in $expects; do
|
||||
actual_count=0
|
||||
for platform in androidMain jvmMain iosMain; do
|
||||
platform_dir=$(dirname "$file" | sed "s/commonMain/$platform/")
|
||||
platform_file="${platform_dir}/$(basename "$file")"
|
||||
if [ -f "$platform_file" ] && grep -q "actual.*$expect_name" "$platform_file"; then
|
||||
actual_count=$((actual_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$actual_count" -eq 0 ]; then
|
||||
echo -e "${YELLOW}⚠${NC} No actual implementations found for: $expect_name in $file"
|
||||
echo " Check: androidMain, jvmMain, iosMain"
|
||||
ISSUES_FOUND=$((ISSUES_FOUND + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} No expect declarations to validate"
|
||||
fi
|
||||
|
||||
# Check 5: Duplicated business logic across platforms
|
||||
echo
|
||||
echo "📋 Checking for potential code duplication..."
|
||||
# This is a heuristic check - look for similar function names in different platform source sets
|
||||
common_functions=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -h "^fun " | awk '{print $2}' | sed 's/[({<].*$//' | sort -u || true)
|
||||
if [ -n "$common_functions" ]; then
|
||||
for func in $common_functions; do
|
||||
android_count=$(find */src/androidMain -name "*.kt" 2>/dev/null | xargs grep -l "^fun $func" | wc -l)
|
||||
jvm_count=$(find */src/jvmMain -name "*.kt" 2>/dev/null | xargs grep -l "^fun $func" | wc -l)
|
||||
|
||||
if [ "$android_count" -gt 0 ] && [ "$jvm_count" -gt 0 ]; then
|
||||
echo -e "${YELLOW}⚠${NC} Function '$func' found in both androidMain and jvmMain"
|
||||
echo " Consider: Move to commonMain or jvmAndroid if truly shared"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo
|
||||
echo "=== Summary ==="
|
||||
if [ "$ISSUES_FOUND" -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All checks passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Found $ISSUES_FOUND issue(s)${NC}"
|
||||
echo
|
||||
echo "Common fixes:"
|
||||
echo " 1. Platform code in commonMain → Move to androidMain or create expect/actual"
|
||||
echo " 2. JVM libraries in commonMain → Move to jvmAndroid or migrate to kotlinx.*"
|
||||
echo " 3. Missing actual implementations → Implement in all target platforms"
|
||||
echo " 4. Duplicated logic → Move to commonMain or jvmAndroid"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user