mirror of
https://github.com/bitsycore/compose-desktop-native.git
synced 2026-10-05 10:47:26 +00:00
feat(bridge): substitute Koin / Coil 3 / Pulse MVI for consumers too The consumer-facing bridge plugin's table only covered Compose, so a third-party app declaring io.insert-koin / io.coil-kt.coil3 / com.bitsycore.lib coordinates would have resolved the upstream artifacts and failed on mingwX64. Added all 15 entries, with a note that the plugin table and the repo-internal FULL-COMMONIZATION BRIDGE are two halves of the same swap and must stay in sync. Docs: - CLAUDE.md: koin/ coil/ pulse/ in the module map (including WHY each is vendored and the source-set / skiko-branch shapes), the five pinned upstream repos, and the "check BOTH coordinate sets" rule with the duplicate-classes hazard that follows from it. - CLAUDE.md: the VENDOR-REIMPL marker next to VENDOR-BASE in the vendoring rules - a reimplementation is deliberately not drift-tracked, only a SIGNATURE change matters. - README.md: a table of what is vendored and why, so the answer to "does X work on this port" is visible without reading CLAUDE.md. - TOOLING.md: audit-exclusions.py alongside the other two guardrails, the ecosystem refs in the version map, and the ref-bump runbook generalised off Compose-only. - bridge README: the ecosystem coordinates a consumer can declare, plus the two caveats (nav2 is a dead end; pick one lifecycle coordinate set). - PLAN.md 7b: marked done, with the three findings worth remembering.
262 lines
11 KiB
Markdown
262 lines
11 KiB
Markdown
# Compose Desktop Native Bridge
|
|
|
|
`com.bitsycore.compose-desktop-native.bridge` lets an app declare the **official Compose
|
|
Multiplatform coordinates once in `commonMain`** and run on every platform CMP
|
|
supports **plus** the port's Kotlin/Native desktop targets (mingwX64,
|
|
linuxX64, linuxArm64, macosArm64 - no JVM).
|
|
|
|
Gradle dependency-substitution rules cannot ship inside a Maven artifact, so
|
|
this plugin carries them instead: on every configuration belonging to a native
|
|
desktop target it substitutes the official `org.jetbrains.compose.*`
|
|
coordinates for the published `com.bitsycore.compose.sdl:*` klibs. All other
|
|
targets (android / jvm / iOS / wasm) keep resolving the official artifacts
|
|
untouched. `org.jetbrains.compose.runtime` is never substituted - the official
|
|
runtime klibs serve every target.
|
|
|
|
## Usage
|
|
|
|
```kotlin
|
|
// settings.gradle.kts
|
|
pluginManagement {
|
|
repositories {
|
|
// Port artifacts + bridge plugin, no authentication needed.
|
|
maven("https://maven.bitsycore.com/releases")
|
|
gradlePluginPortal()
|
|
mavenCentral()
|
|
}
|
|
}
|
|
|
|
plugins {
|
|
id("com.bitsycore.compose-desktop-native.bridge") version "<release-version>"
|
|
}
|
|
|
|
dependencyResolutionManagement {
|
|
repositories {
|
|
// The port's own artifacts AND the bitsycore skiko fork
|
|
// (com.bitsycore.skiko:skiko / :skiko-mingwx64) both live here - public,
|
|
// no credentials. Windows (mingwX64) renders through the fork; macOS and
|
|
// Linux use the official org.jetbrains.skiko from Maven Central.
|
|
maven("https://maven.bitsycore.com/releases")
|
|
google()
|
|
mavenCentral()
|
|
}
|
|
}
|
|
```
|
|
|
|
> The port is also published to GitHub Packages
|
|
> (`https://maven.pkg.github.com/bitsycore/compose-desktop-native`) as an
|
|
> authenticated fallback - same coordinates, needs a PAT with `read:packages`.
|
|
|
|
```kotlin
|
|
// module build.gradle.kts - official coords, everywhere.
|
|
// Applying the plugin HERE exposes the `composeDesktopNative` extension, which
|
|
// reports the exact Compose versions the port tracks, so you never hand-match
|
|
// them against compose.properties.
|
|
plugins {
|
|
kotlin("multiplatform")
|
|
id("org.jetbrains.compose")
|
|
id("com.bitsycore.compose-desktop-native.bridge")
|
|
}
|
|
|
|
kotlin {
|
|
macosArm64(); linuxX64(); mingwX64() // + jvm()/android()/ios if you like
|
|
sourceSets {
|
|
commonMain.dependencies {
|
|
implementation("org.jetbrains.compose.runtime:runtime:${composeDesktopNative.composeRuntime}")
|
|
implementation("org.jetbrains.compose.ui:ui:${composeDesktopNative.compose}")
|
|
implementation("org.jetbrains.compose.foundation:foundation:${composeDesktopNative.compose}")
|
|
implementation("org.jetbrains.compose.material3:material3:${composeDesktopNative.composeMaterial3}")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`composeDesktopNative` exposes four read-only values: `compose` (ui /
|
|
foundation / animation / material), `composeMaterial3` (versioned separately
|
|
upstream), `composeRuntime`, and `version` (the port klib version being
|
|
substituted). You can still write literal versions if you prefer; the extension
|
|
just removes the drift.
|
|
|
|
### Ecosystem libraries
|
|
|
|
Substitution is not limited to Compose. Several popular libraries stop short of
|
|
Kotlin/Native desktop upstream; the port vendors them and the bridge swaps them
|
|
in on native desktop targets, so you declare the **official** coordinates and
|
|
they resolve everywhere:
|
|
|
|
```kotlin
|
|
commonMain.dependencies {
|
|
// Koin - koin-core already ships desktop-native klibs; these four are the
|
|
// ones upstream builds for apple + android only.
|
|
implementation("io.insert-koin:koin-core:4.2.2")
|
|
implementation("io.insert-koin:koin-compose:4.2.2")
|
|
implementation("io.insert-koin:koin-compose-viewmodel:4.2.2")
|
|
implementation("io.insert-koin:koin-compose-navigation3:4.2.2")
|
|
|
|
// Coil 3 - no mingwX64 upstream anywhere, and no desktop native at all for
|
|
// its compose layer.
|
|
implementation("io.coil-kt.coil3:coil-compose:3.6.2")
|
|
implementation("io.coil-kt.coil3:coil-network-ktor3:3.6.2")
|
|
implementation("io.coil-kt.coil3:coil-svg:3.6.2")
|
|
|
|
// Pulse MVI
|
|
implementation("com.bitsycore.lib:pulse:0.3.7")
|
|
implementation("com.bitsycore.lib:pulse-compose:0.3.7")
|
|
}
|
|
```
|
|
|
|
Two caveats:
|
|
|
|
- Koin's `koin-compose-viewmodel-navigation` is **not** substituted. It sits on
|
|
Navigation 2's `navigation-compose`, which publishes no mingwX64 or linux
|
|
klibs under either the `androidx.*` or `org.jetbrains.androidx.*`
|
|
coordinates. Use `koin-compose-navigation3` instead.
|
|
- Lifecycle comes from the **google** `androidx.lifecycle:*` coordinates here.
|
|
If your own build also pulls the `org.jetbrains.androidx.lifecycle:*` mirrors,
|
|
you will get the same classes twice - pick one.
|
|
|
|
### Settings-wide or per-module
|
|
|
|
The plugin applies at either level:
|
|
|
|
```kotlin
|
|
// settings.gradle.kts - every module of the build
|
|
plugins { id("com.bitsycore.compose-desktop-native.bridge") version "<v>" }
|
|
```
|
|
|
|
```kotlin
|
|
// build.gradle.kts - this module only (compose-plugin style)
|
|
plugins { id("com.bitsycore.compose-desktop-native.bridge") }
|
|
```
|
|
|
|
Note the difference between the two settings blocks: a
|
|
`pluginManagement { plugins { id(...) version ... } }` entry only PINS the
|
|
version (so module-level applications can omit it); the top-level
|
|
`plugins { }` block in settings is what actually applies it build-wide.
|
|
Rule of thumb: single app module → apply in the module; multi-module builds →
|
|
apply once in settings.
|
|
|
|
When applied from settings, the type-safe `composeDesktopNative` accessor is not
|
|
generated in the module scripts. The same values are still available through
|
|
project extra properties:
|
|
|
|
```kotlin
|
|
val compose = project.extra["composeDesktopNative.compose"] as String
|
|
val material3 = project.extra["composeDesktopNative.composeMaterial3"] as String
|
|
val runtime = project.extra["composeDesktopNative.composeRuntime"] as String
|
|
```
|
|
|
|
## compose.desktop.native - the application block for native
|
|
|
|
The native counterpart of `compose.desktop { application { mainClass } }`:
|
|
|
|
```kotlin
|
|
compose.desktop {
|
|
application { mainClass = "app.MainJvmKt" } // upstream jvm
|
|
native { entryPoint = "app.main" } // compose-desktop-native
|
|
}
|
|
```
|
|
|
|
Declares an executable with that entry point on every Kotlin/Native desktop
|
|
target - no `targets.withType<KotlinNativeTarget> { binaries.executable { … } }`
|
|
boilerplate. If you declare your own `binaries.executable { }` (e.g. for extra
|
|
linker flags), it keeps everything it configures; `entryPoint` only fills in
|
|
where the executable didn't set one, so the two compose.
|
|
|
|
### App icon
|
|
|
|
Declare the app icon inside `native { }` as PNG files:
|
|
|
|
```kotlin
|
|
compose.desktop {
|
|
native {
|
|
entryPoint = "app.main"
|
|
icon {
|
|
light.from("icons/app-32.png", "icons/app-128.png")
|
|
dark.from("icons/app-dark-32.png", "icons/app-dark-128.png") // optional
|
|
// exeIcon.from("icons/app-16.png", …, "icons/app-256.png") // optional
|
|
// resourceDir.set("icon") // data.kres subfolder (default)
|
|
// embedWindowsIcon.set(true) // embed the .exe icon (default)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The plugin (no Python / native tooling needed - pure-Kotlin codec):
|
|
|
|
- decodes each PNG to a raw RGBA blob and bundles it into `data.kres` under
|
|
`<resourceDir>/<pngBaseName>.rgba`. Your app selects it at runtime, matched
|
|
to the OS light/dark theme:
|
|
|
|
```kotlin
|
|
nativeComposeWindow(
|
|
title = "My App",
|
|
icon = AppWindowIcon(
|
|
light = listOf("icon/app-128.rgba", "icon/app-32.rgba"),
|
|
dark = listOf("icon/app-dark-128.rgba", "icon/app-dark-32.rgba"),
|
|
),
|
|
) { App() }
|
|
```
|
|
|
|
List one path per size you supply - the largest becomes the base and the rest
|
|
become alternate resolutions SDL picks from (title bar / taskbar / Alt-Tab).
|
|
The runtime icon uses core SDL only, so it works on every target.
|
|
|
|
- on **Windows**, assembles a multi-size `.ico` from `exeIcon` (or `light` when
|
|
`exeIcon` is unset), compiles it with `windres` (mingw-w64 binutils), and links
|
|
it into the `.exe` so Explorer and the pinned taskbar show it. Set
|
|
`embedWindowsIcon.set(false)` to skip this (e.g. if `windres` isn't installed).
|
|
The resource object is linked into every mingw executable - the one the plugin
|
|
creates via `native { entryPoint }` and hand-declared
|
|
`binaries.executable { }` ones alike.
|
|
|
|
The runtime window icon and the `.exe` icon can differ: point `light` at a
|
|
background-less mark (looks right in the taskbar) and `exeIcon` at the full
|
|
branded icon with a background (stays legible in Explorer) - the same split
|
|
apidemo uses.
|
|
|
|
## composeResources - zero setup
|
|
|
|
If the module also applies the official `org.jetbrains.compose` plugin, the
|
|
bridge completes the resources story on the native desktop targets: it
|
|
registers a `package<Variant>ComposeResources<Target>` task per native
|
|
executable that bundles the Compose plugin's prepared resources into
|
|
`data.kres` next to the binary (a STORED zip the port's runtime reads via
|
|
SDL_GetBasePath). Files under `src/commonMain/composeResources/` + the
|
|
generated `Res.*` accessors then work exactly like on every other platform -
|
|
drawables, strings (`values/*.xml`), fonts, raw files:
|
|
|
|
```kotlin
|
|
commonMain.dependencies {
|
|
implementation("org.jetbrains.compose.components:components-resources:<cmp-version>")
|
|
}
|
|
```
|
|
|
|
`compose.resources { packageOfResClass = … }` is honoured; source-set
|
|
overrides follow the default hierarchy (a `mingwX64Main` resource beats a
|
|
`commonMain` one).
|
|
|
|
`data.kres` entries are STORED by default (the runtime reads an entry with one
|
|
fseek+fread). Pass `-PcompressResources=true` (or set it in `gradle.properties`)
|
|
to DEFLATE them for a smaller distributable - the runtime inflates on read.
|
|
|
|
## Notes
|
|
|
|
- The substituted klib version defaults to the plugin's own version (both ship
|
|
from the same tag). Override with `composeDesktopNative.version=<x>` in
|
|
`gradle.properties` when mixing releases.
|
|
- `composeDesktopNative.substitution=false` disables the substitution half while
|
|
keeping the packaging / icon / `compose.desktop.native` DSL - for builds that
|
|
already provide the port modules another way (the port repo itself uses this:
|
|
its root build substitutes the official coords to project modules).
|
|
- Use `composeDesktopNative.compose` / `.composeMaterial3` / `.composeRuntime`
|
|
(above) for the official coords rather than hardcoding a version. Substitution
|
|
replaces the requested version on native, but your jvm/android targets resolve
|
|
the official artifacts at the version you write, so matching what the port
|
|
tracks keeps every target on the same API. The values come straight from the
|
|
release the plugin shipped with; no need to read `compose.properties` by hand.
|
|
- Requires Gradle 8.8+ when applied in settings (`gradle.lifecycle.beforeProject`).
|
|
- App windowing/main-loop (`com.bitsycore.compose:desktop-native-window`) and the icon
|
|
font module (`material-symbols`) are the port's own APIs - depend on them
|
|
directly; no substitution involved.
|