mirror of
https://github.com/bitsycore/compose-desktop-native.git
synced 2026-10-05 10:47:26 +00:00
The first version of this screen proved the vendored adaptive modules render, but it did not really demonstrate adaptation: it left rememberListDetailPaneScaffoldNavigator on its default directive, which is the WINDOW size class, and pinned the scaffold to a fixed 420dp. The window is the wrong input here. The screen does not get the window - the demo shell spends 190dp on the sidebar plus 48dp of padding - so defaulting folds ~240dp too late and leaves two panes side by side in a region already too cramped for them. The directive is now computed from the width the screen actually gets (BoxWithConstraints), which is what any pane-hosting app that is not full-window should do; the pane height tracks the window height instead of being a constant. The header prints the window size class and the pane-area size class side by side so the difference is visible while dragging. Also adds selection highlighting and a Back button that only appears while folded, so the single-pane mode is actually navigable. New `--adaptivetest` probe: drives one window wide -> narrow -> wide with setSize and asserts the scaffold folds and unfolds. This is what proves the resize chain end to end - SDL resize, the owner's snapshot-backed containerSize, recomposition, BoxWithConstraints re-measure, a new PaneScaffoldDirective on the navigator, a new scaffold value. The --screen=Adaptive --width=N screenshots could only ever prove the fold was right for the size the window OPENED at, since each run is a fresh process. The probe reads a small internal seam on the screen (gAdaptiveTwoPane); it is documented as a probe hook and is not UI. Verified on mingwX64: folds at 800dp and stays two-pane at 1100/1400dp, --adaptivetest passes repeatedly, and the JVM parity leg plus commonMain metadata still compile.
259 lines
14 KiB
Markdown
259 lines
14 KiB
Markdown
# Tooling
|
|
|
|
Helper scripts and workflows used to build, vendor, and verify Compose Desktop
|
|
Native. Each has a focused job; the deeper ones link to their own README.
|
|
|
|
For architecture, source-set layout, and vendoring rules, see
|
|
[CLAUDE.md](CLAUDE.md).
|
|
|
|
## Native libraries
|
|
|
|
SDL3 is built from source as a static library and linked straight into the
|
|
executable - the windowing, input, and platform-integration layer. One script
|
|
does it on every OS. Output lands in the gitignored `libs/`; the version is
|
|
pinned in `scripts/build-sdl/build-sdl.properties`.
|
|
|
|
```bash
|
|
python3 scripts/build-sdl/build-all.py # sdl3
|
|
python3 scripts/build-sdl/build-all.py sdl3 # rebuild the step
|
|
```
|
|
|
|
Needs `git`, `cmake`, and Python 3 on every host (`ninja` is fetched when
|
|
absent). Run it once per machine, or after bumping the pinned version.
|
|
|
|
The static lib is built **slim**: the port uses SDL only for video/window,
|
|
events, clipboard, file dialogs, GL/Metal contexts, the CPU-raster `SDL_Render`
|
|
blit, filesystem, cursor, locale, theme, and text input/IME. `build-all.py`
|
|
disables every unused subsystem - audio, joystick, haptic, hidapi, sensor,
|
|
power, camera, GPU, offscreen, virtual-joystick (all zero-reference), plus
|
|
tests/examples and the Windows D3D12 driver. If a consumer app ever needs one of
|
|
these (e.g. SDL audio), re-enable its `-DSDL_*` flag in `build-all.py` and
|
|
rebuild - a build-time change, no code edit.
|
|
|
|
## Vendoring upstream Compose
|
|
|
|
Most `androidx.compose.*` code is copied byte for byte from
|
|
`JetBrains/compose-multiplatform-core` into each module's gitignored
|
|
`src/vendor/`. A `compose-fork.txt` manifest per module lists which upstream
|
|
files it pulls; the pinned ref lives in `scripts/compose-fork/compose.properties`.
|
|
|
|
```bash
|
|
scripts/compose-fork/sync.sh # re-sync every module
|
|
scripts/compose-fork/sync.sh compose/ui/ui/compose-fork.txt # one module
|
|
```
|
|
|
|
Reach for this after a fresh checkout or a ref bump. Details:
|
|
[scripts/compose-fork/README.md](scripts/compose-fork/README.md).
|
|
|
|
Guardrails that keep the vendor tree honest:
|
|
|
|
```bash
|
|
python3 scripts/compose-fork/check-vendor-clean.py # src/vendor matches the pinned refs
|
|
python3 scripts/compose-fork/check-vendor-drift.py # hand-edited "manual vendors" match their pin
|
|
python3 scripts/compose-fork/audit-exclusions.py # every `!` exclusion is classified
|
|
```
|
|
|
|
`check-vendor-clean` fails if a hand-edit ever leaks into `src/vendor/`.
|
|
`check-vendor-drift` reads the `// VENDOR-BASE:` header on every edited copy and,
|
|
using the local upstream clone, reports whether the base actually changed since
|
|
the pin (needs reconciling) or is merely stale (safe to re-stamp).
|
|
|
|
`audit-exclusions` covers the other half: a manifest `!` line takes a file OUT of
|
|
the verbatim sync, and the local replacement is either a derived copy
|
|
(`// VENDOR-BASE:`, drift-tracked) or a fresh reimplementation
|
|
(`// VENDOR-REIMPL:`, deliberately not tracked - only a SIGNATURE change matters).
|
|
It classifies each one, flags any local counterpart carrying NEITHER marker
|
|
(invisible to the drift check, silently rotting) and any copy that has become
|
|
identical to upstream (drop it and re-vendor), and exits non-zero on an
|
|
unclassified file. Run all three on every ref bump.
|
|
|
|
### Coverage against upstream
|
|
|
|
How much of upstream's public API the port covers is measured, not guessed.
|
|
|
|
```bash
|
|
./gradlew apiDump && python3 scripts/compose-coverage.py # per-module coverage tables
|
|
python3 scripts/compose-coverage.py --missing ui-text # list uncovered declarations
|
|
```
|
|
|
|
## Verification
|
|
|
|
### One-command gate
|
|
|
|
`verify-mac.sh` is the runbook to run after any renderer or layout change. On
|
|
macOS or Linux it exercises the Skia renderer and exits non-zero on any
|
|
failure:
|
|
|
|
```bash
|
|
scripts/verify-mac.sh
|
|
```
|
|
|
|
It runs: the vendor drift and clean checks, a build of `:demo` and `:apidemo`,
|
|
the interaction probes, the parity sweep, a memory soak, and a frame-time spot
|
|
check. The Windows target is verified separately (see below).
|
|
|
|
### Parity: native vs JVM
|
|
|
|
`:demo` renders the same shared screens on two stacks: this port, and stock JVM
|
|
Compose Desktop. `parity.py` screenshots every screen on both and pixel-diffs
|
|
them, so a screen that diverges is a porting bug. It gates against per-screen
|
|
golden baselines.
|
|
|
|
```bash
|
|
python3 scripts/parity/parity.py # all screens
|
|
python3 scripts/parity/parity.py Buttons Shapes # a subset
|
|
python3 scripts/parity/parity.py --no-build # reuse the last renders
|
|
```
|
|
|
|
How to read the diff and the percentage: [scripts/parity/README.md](scripts/parity/README.md).
|
|
|
|
### Interaction probe
|
|
|
|
`probe.py` launches a native window, sends window-relative input (click, hover,
|
|
hold), and captures the client area. It packages the rigs used to reproduce
|
|
interaction bugs. Windows-only. Details:
|
|
[scripts/probe/README.md](scripts/probe/README.md).
|
|
|
|
### Frame profiler
|
|
|
|
Set `CDN_PROFILE=1` (or `CDN_PROFILE=<path>`) and run any app. Every couple of
|
|
seconds it writes per-phase main-loop timings (events, app pump, render, with
|
|
draw sub-phases and per-frame draw counters) to a file. `CDN_FORCERENDER=1`
|
|
renders every frame so steady-state timings read on an otherwise idle screen.
|
|
|
|
```bash
|
|
CDN_PROFILE=1 CDN_FORCERENDER=1 ./demo/build/bin/macosArm64/debugExecutable/demo.kexe
|
|
```
|
|
|
|
Note: `present` is vsync-blocking, so profile on the target refresh rate before
|
|
drawing conclusions about a frame-rate gap.
|
|
|
|
### Text metrics diagnostic
|
|
|
|
Set `CDN_TEXT_METRICS=1` and run any app to dump, per built paragraph, the font
|
|
scaler's `ascent`/`descent`/`leading`, the resolved `familyName` + `fontPx`, and
|
|
the resulting paragraph line box (`paraHeight`, `line0[ascent/descent/baseline/
|
|
height]`). Its job is the mac-vs-Windows vertical-spacing question (PLAN.md ยง1b):
|
|
the SAME `NotoSans.ttf` bytes go through a different Skia `FontMgr` scaler per
|
|
host (CoreText / fontconfig-FreeType / DirectWrite), which can pick different
|
|
metric tables. Run it on native AND on the JVM parity app on the SAME host and
|
|
diff the lines - Windows-native must match skiko-JVM-Windows.
|
|
|
|
```bash
|
|
CDN_TEXT_METRICS=1 demo.kexe --screen=Buttons --screenshot=/tmp/b.bmp
|
|
```
|
|
|
|
### Demo probes
|
|
|
|
`:demo` ships headless regression probes driven by CLI flags, used by the
|
|
verify runbook and in isolation:
|
|
|
|
```bash
|
|
demo.kexe --nav3test # Navigation 3 + ViewModel + lifecycle
|
|
demo.kexe --backtest # predictive-back / BackHandler
|
|
demo.kexe --clicktest # pointer -> upstream clickable
|
|
demo.kexe --scrolltest # wheel -> scrollable
|
|
demo.kexe --multiwintest # multi-window lifecycle
|
|
demo.kexe --adaptivetest # material3-adaptive folds/unfolds on a LIVE window resize
|
|
demo.kexe --soaktest # memory soak (CDN_SOAK_SCREEN, CDN_SOAK_STATIC, CDN_SOAK_CYCLES)
|
|
```
|
|
|
|
### Windows smoke
|
|
|
|
The Mac runbook cannot cover the Windows target. Before shipping, on a Windows
|
|
host: build the mingwX64 executables, run `scripts/probe/`, and run a publish
|
|
dry run (only Windows compiles the full common-metadata variant table).
|
|
|
|
```bat
|
|
gradlew.bat :demo:runDebugExecutableMingwX64
|
|
gradlew.bat :apidemo:runDebugExecutableMingwX64
|
|
```
|
|
|
|
mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from
|
|
the PUBLIC `https://maven.bitsycore.com/releases` (no credentials) as
|
|
`com.bitsycore.skiko:skiko:0.150.1-mingw.2` (macOS/Linux use official Skiko). The runtime `skiko-windows-x64.dll` is auto-provisioned next
|
|
to the executable by the bridge plugin - no manual copy. The fork itself is
|
|
published by a separate GitHub Actions workflow in the fork repo, out of band
|
|
from this repo's release flow.
|
|
|
|
## Consuming the port
|
|
|
|
To build a third-party app against the published klibs, apply the bridge Gradle
|
|
plugin and declare official Compose Multiplatform coordinates; the plugin swaps
|
|
in the port's klibs on native desktop targets. Setup:
|
|
[gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md).
|
|
|
|
## Versioning and releasing
|
|
|
|
### Version map
|
|
|
|
Versions live in a few places, some of which move in lockstep. There is no
|
|
single file to edit; know which axis you are changing.
|
|
|
|
| Version | Where | Notes |
|
|
|---------|-------|-------|
|
|
| Project release version | The git tag `vX.Y.Z`. `PUBLISH_VERSION` (from the tag) feeds `vPublishVersion` in `build.gradle.kts`, which strips the leading `v`. Groups mirror upstream per area (`com.bitsycore.compose.<area>:<module>`, e.g. `com.bitsycore.compose.ui:ui`); project-only modules use `com.bitsycore.compose.sdl` and `:desktop-native-window` is `com.bitsycore.compose` - see `groupFor()` in the root build. | Set by the tag, not edited by hand. A non-publish build is `0.0.0-SNAPSHOT`. |
|
|
| Vendored Compose (native side) | `COMPOSE_CORE_REF` and `COMPOSE_REF` in `scripts/compose-fork/compose.properties`, plus `compose` in `gradle/libs.versions.toml`. | Pin to a durable tag (not a `+dev` commit upstream may GC). Re-sync after changing. |
|
|
| Vendored ecosystem libs | `KOIN_REF`, `COIL_REF`, `PULSE_REF` in the same file, plus `koin` / `coil` in `gradle/libs.versions.toml`. | Same rule: durable tags. Each repo gets its own sibling sparse clone (`../cmp-ref-koin`, `../cmp-ref-coil`, `../cmp-ref-pulse-mvi`). Re-sync, then `apiDump` - these modules are BCV-validated like the rest. |
|
|
| JVM parity forcing | `compose` / `composeMaterial3` / `composeRuntime` in `gradle/libs.versions.toml` (read by `demo`, `apidemo`, `material-symbols` and the root build's forcing map). | Must be a version PUBLISHED to Maven Central. It may lag the vendored native ref (a documented skew) until the matching version is published; at v1.12.1 they are in lockstep. |
|
|
| Skiko | `skiko` (official) and `skikoMingw` (the fork) in `gradle/libs.versions.toml`; keep the fork's `-mingw.N` base equal to `skiko`. | macOS/Linux use official Skiko (`org.jetbrains.skiko`); mingwX64 uses the bitsycore fork (`com.bitsycore.skiko:skiko:0.150.1-mingw.2` from the public maven.bitsycore.com), published out of band by the fork repo's own workflow. Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses (the fork keeps upstream's `org.jetbrains.skiko.*` package names; only the Maven coord is rebranded). Verify with a throwaway `skikoRendererMain` compile if unsure. |
|
|
| SDL3 | `scripts/build-sdl/build-sdl.properties`. | Rebuild `libs/` with `build-all.py` after any change. |
|
|
| Bridge substituted version | Defaults to the bridge plugin's own published version; consumers override with the `composeDesktopNative.version` Gradle property. | The plugin publishes with the release tag, so a consumer on the matching plugin version resolves the right klibs automatically. |
|
|
|
|
### Bump the upstream Compose ref
|
|
|
|
Run this on each upstream bump; it is the flow that keeps the sync tax low.
|
|
|
|
1. Edit the relevant `*_REF` in `compose.properties` - `COMPOSE_CORE_REF` /
|
|
`COMPOSE_REF` for Compose, `KOIN_REF` / `COIL_REF` / `PULSE_REF` for the
|
|
ecosystem libs (and the matching entry in `libs.versions.toml` if the
|
|
coordinate version moved).
|
|
2. `scripts/compose-fork/sync.sh`
|
|
3. `python3 scripts/compose-fork/check-vendor-drift.py`. For any manual vendor
|
|
whose upstream base actually changed, reconcile by hand; otherwise re-stamp
|
|
its `// VENDOR-BASE:` header to the new ref. Then
|
|
`python3 scripts/compose-fork/audit-exclusions.py` - a newly-identical copy
|
|
should go back to verbatim vendoring, and an unclassified one needs a marker.
|
|
Finish with `./gradlew apiDump` and review the klib API diff.
|
|
4. Build and fix any fallout.
|
|
5. `scripts/verify-mac.sh` (green, including the soak and parity gates).
|
|
6. If a matching Compose version is now published to Maven, bump
|
|
`vComposeJvmVersion` in demo/apidemo/material-symbols to close the skew.
|
|
7. Run WIN-SMOKE on a Windows host.
|
|
8. Re-seed parity baselines only if metrics legitimately moved
|
|
(`parity.py --update-baselines`), and record why.
|
|
|
|
### Cut a release
|
|
|
|
1. Complete the ref-bump flow above and confirm it is green. For a STABLE
|
|
release, `vComposeJvmVersion` should match the vendored ref (no skew).
|
|
2. On a Windows host, run `gradlew :desktop-native-window:compileCommonMainKotlinMetadata`.
|
|
Only Windows compiles the full common-metadata variant table, and the root
|
|
KotlinMultiplatform publications live there; a macOS-only publish leaves the
|
|
roots without mingwX64 variants (this bit v0.1.15).
|
|
3. Refresh the klib API baselines: on a Windows host (JDK 21, matching CI) run
|
|
`./gradlew apiDump` and commit any diff. Not a hard gate yet - the API is
|
|
pre-stable and `apiCheck` isn't wired - but regenerating each release keeps
|
|
the baselines honest so the eventual gate is a no-op. `macosArm64` is inferred
|
|
from the linux/mingw ABIs (accurate for the target-independent Compose
|
|
surface); `:sdl-core` (cinterop) and `:material-symbols` (generated icon maps)
|
|
are excluded via `apiValidation.ignoredProjects`. Best host: Windows - it
|
|
builds the mingw slice (the fork's unique surface) for real and only infers
|
|
macos; the macos ABI is target-independent for the tracked pure-Kotlin modules.
|
|
4. `git tag vX.Y.Z && git push origin vX.Y.Z`. This triggers `.github/workflows/publish.yml`.
|
|
|
|
What the publish job does, so you can read a failure:
|
|
|
|
- Each host publishes only its own target's publications to GitHub Packages
|
|
under their per-area `com.bitsycore.compose.*` coords. Windows additionally publishes the root
|
|
KotlinMultiplatform metadata, the `jvm` publication, and the bridge plugin
|
|
(only Windows declares every target).
|
|
- The `MODULES` list in the workflow must stay in sync with the modules in
|
|
`settings.gradle.kts`.
|
|
- A partial publish (for example a dropped connection mid-upload) deletes this
|
|
host's half-published versions for the tag and retries, so a rerun is safe.
|
|
- The demo and apidemo release binaries are zipped and attached to the GitHub
|
|
Release for the tag.
|
|
|
|
4. Verify a consumer build against the new version resolves through the bridge.
|