Files
Bitsy 47445ab7ab docs: document the vendored ecosystem libraries + the VENDOR-REIMPL convention
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.
2026-09-12 00:39:04 +02:00

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.