Files
Bitsy 82ffd7cf4d feat: migrate to Compose Multiplatform 1.12.0 + SDL3 3.4.16
Upstream baseline
- Pin both vendored refs to the STABLE v1.12.0 tag (core + umbrella); the
  native and JVM-parity legs are back in lockstep, no dev-build skew.
- Catalog: compose 1.12.0, composeRuntime 1.12.0. material3 stays
  1.12.0-alpha03 - that IS the version CMP 1.12.0 ships (its own train).
- Re-synced 1560 vendored files; drift + vendor-clean both clean.
- Retire the SelectionLayout.kt manual vendor: upstream fixed the same
  out-of-bounds at the source (lastSlot = currentSlot, not currentSlot + 1),
  so the fork's index clamps are redundant. Back to verbatim vendoring.
- Re-stamp 10 manual vendors to @ v1.12.0 (upstream unchanged base..pin).
- Regenerate the klib API dumps. These had already drifted on main (the last
  dump predates the scrollbar / IME changes), so the diff covers that too.

SDL3 3.4.16, skiko fork 0.150.1-mingw.2
- Rebuild the static libSDL3.a from release-3.4.16.
- CMP 1.12.0 uses skiko 0.150.1, unchanged, so the fork's Skia base still
  matches; bump to the -mingw.2 build and centralise it as `skikoMingw` in
  the version catalog instead of three hardcoded copies.
- Resolve the fork from the PUBLIC maven.bitsycore.com/releases instead of
  the credentialed GitHub Packages repo: building the Windows target no
  longer needs a GitHub token.

Gradle module paths mirror the directory
- Drop every projectDir redirect: :ui -> :compose:ui:ui,
  :animation-core -> :compose:animation:animation-core, and so on.
- INVARIANT (documented in settings.gradle.kts + CLAUDE.md): the leaf
  directory IS the published artifactId. KMP derives a target publication's
  coordinate as <project.name>-<target>, and a target disabled on the
  building host (all Apple targets on Windows) has no publication object to
  retarget - while the root kotlinMultiplatform metadata, which the WINDOWS
  publish job owns, still emits an available-at for it. A mismatched leaf
  silently ships a root module pointing at a nonexistent <leaf>-macosarm64.
  So compose/desktop/native/window and components/resources/library are
  renamed to their artifactIds. Published coordinates are unchanged.

Manual-vendor audit
- New scripts/compose-fork/audit-exclusions.py: classifies every `!` manifest
  exclusion as manual vendor (VENDOR-BASE, drift-tracked), reimplementation
  (VENDOR-REIMPL, deliberately not tracked), or orphan, and fails on any
  unclassified one.
- 14 project reimplementations were invisible to the drift checker by
  accident; mark them VENDOR-REIMPL. None needed reconciling (all verified
  <= 0.53 similarity to their upstream counterparts, none changed upstream
  between the old pin and v1.12.0).

Fix the JVM apidemo build
- Declare components-resources on jvmMain (decodeToImageBitmap /
  decodeToSvgPainter) and add the two missing cross-package imports. The JVM
  parity target had never compiled.

Verified on Windows: both native executables link and render, apidemo JVM
builds and launches, apiCheck green, common metadata compiles (the Windows
publish gate), publishToMavenLocal emits the same coordinates as before.
2026-09-12 00:03:13 +02:00

178 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Audit every `!excluded` line in the compose-fork.txt manifests.
The drift checker (check-vendor-drift.py) only sees files that carry a
`// VENDOR-BASE:` provenance line. This script covers the other half of the
question at a ref bump:
1. Which exclusions have a LOCAL counterpart (a manually vendored + edited
copy under src/{commonMain,nativeMain,...})? Those are fork maintenance.
2. Of those, which are MISSING a VENDOR-BASE line - i.e. invisible to the
drift checker and silently rotting?
3. Of those, which are now IDENTICAL to upstream at the pin (ignoring the
fork's header comment)? Those can go back to verbatim vendoring: delete
the local copy and drop the `!` line from the manifest.
4. Which exclusions have NO local counterpart? Those are upstream files the
port deliberately does not want (it replaces the whole subsystem).
Usage: python scripts/compose-fork/audit-exclusions.py
"""
import pathlib
import re
import subprocess
# ==================
# MARK: Config
# ==================
CLONES = {
"COMPOSE_CORE_REF": pathlib.Path(r"C:\Dev\cmp-ref"),
"COMPOSE_REF": pathlib.Path(r"C:\Dev\cmp-ref-compose-multiplatform"),
}
PIN = "v1.12.0"
def read_upstream(inClone, inPath):
"""Return the file's contents at the pinned ref, or None if absent."""
vResult = subprocess.run(
["git", "-C", str(inClone), "show", f"{PIN}:{inPath}"],
capture_output=True,
)
if vResult.returncode != 0:
return None
return vResult.stdout.decode("utf-8", errors="replace")
def strip_fork_header(inText):
"""Drop the fork's leading comment block + @file:Suppress so the body of a
manual vendor can be compared against upstream on equal terms."""
vLines = inText.replace("\r\n", "\n").split("\n")
vOut = []
vInHeader = True
for vLine in vLines:
if vInHeader:
vStripped = vLine.strip()
if (not vStripped
or vStripped.startswith("//")
or vStripped.startswith("@file:Suppress")):
continue
vInHeader = False
vOut.append(vLine)
return "\n".join(vOut).strip()
# ==================
# MARK: Index local sources
# ==================
vLocal = {}
for vPath in pathlib.Path(".").rglob("*.kt"):
vAsPosix = vPath.as_posix()
if "/src/vendor/" in vAsPosix or "/build/" in vAsPosix:
continue
if "/src/" not in vAsPosix:
continue
vLocal.setdefault(vPath.name, []).append(vPath)
# ==================
# MARK: Walk the manifests
# ==================
vRows = []
for vManifest in pathlib.Path(".").rglob("compose-fork.txt"):
if "build" in vManifest.parts:
continue
vModule = vManifest.parent
vRepoKey = "COMPOSE_CORE_REF"
vBase = None
for vLine in vManifest.read_text(encoding="utf-8").splitlines():
vStripped = vLine.strip()
vMatchRepo = re.match(r"SET_REPO=(\S+)@(\S+)", vStripped)
if vMatchRepo:
vRepoKey = vMatchRepo.group(2).strip("<>")
continue
vMatchFolder = re.match(r"SET_FOLDER=(\S+)", vStripped)
if vMatchFolder:
vBase = vMatchFolder.group(1)
continue
if not vStripped.startswith("!"):
continue
vUpRel = vStripped[1:].strip()
vUpPath = f"{vBase}/{vUpRel}" if vBase else vUpRel
vName = vUpRel.rsplit("/", 1)[-1]
vCands = [p for p in vLocal.get(vName, [])
if p.as_posix().startswith(vModule.as_posix())]
if not vCands and vName.endswith(".skiko.kt"):
vAlt = vName.replace(".skiko.kt", ".native.kt")
vCands = [p for p in vLocal.get(vAlt, [])
if p.as_posix().startswith(vModule.as_posix())]
vRows.append((vModule.as_posix(), vRepoKey, vUpPath, vCands))
# ==================
# MARK: Report
# ==================
vSeen = set()
vVendored, vReimpl, vUnclassified, vOrphan, vReVendorable = [], [], [], [], []
for vModule, vRepoKey, vUpPath, vCands in vRows:
if not vCands:
vOrphan.append((vModule, vUpPath))
continue
for vCand in vCands:
if vCand.as_posix() in vSeen:
continue
vSeen.add(vCand.as_posix())
vText = vCand.read_text(encoding="utf-8", errors="replace")
vUpstream = read_upstream(CLONES[vRepoKey], vUpPath)
vIdentical = (vUpstream is not None
and strip_fork_header(vText) == strip_fork_header(vUpstream))
vEntry = (vCand.as_posix(), vUpstream is not None, vIdentical)
if "VENDOR-BASE" in vText:
vVendored.append(vEntry)
elif "VENDOR-REIMPL" in vText:
vReimpl.append(vEntry)
else:
vUnclassified.append(vEntry)
if vIdentical:
vReVendorable.append(vCand.as_posix())
print("=== MANUAL VENDOR (VENDOR-BASE): derived copy + local edit ===")
print(" drift-tracked by check-vendor-drift.py; reconcile on every bump")
for vPath, vUpFound, vIdentical in sorted(vVendored):
print(f" {vPath}")
if not vUpFound:
print(" ! upstream path GONE at pin")
if vIdentical:
print(" ! IDENTICAL to upstream -> drop the local copy + the `!` line")
print()
print("=== REIMPL (VENDOR-REIMPL): project code, same signatures, own body ===")
print(" NOT drift-tracked by design; re-check only if the signatures change")
for vPath, vUpFound, vIdentical in sorted(vReimpl):
print(f" {vPath}")
if not vUpFound:
print(" ! upstream counterpart GONE at pin - exclusion may be dead")
if vIdentical:
print(" ! IDENTICAL to upstream -> should be a vendor, not a reimpl")
if vUnclassified:
print()
print("=== UNCLASSIFIED - ACTION REQUIRED ===")
print(" A local counterpart with neither marker. Decide which it is and")
print(" stamp it: VENDOR-BASE (derived copy) or VENDOR-REIMPL (project code).")
for vPath, vUpFound, vIdentical in sorted(vUnclassified):
print(f" {vPath}")
print()
print(f"=== ORPHAN: {len(vOrphan)} exclusions with no local file "
f"(upstream file deliberately not wanted) ===")
for vModule, vUpPath in sorted(vOrphan):
print(f" {vModule}: {vUpPath.rsplit('/', 1)[-1]}")
print()
print(f"summary: {len(vVendored)} manual vendor, {len(vReimpl)} reimpl, "
f"{len(vUnclassified)} UNCLASSIFIED, {len(vOrphan)} orphan, "
f"{len(vReVendorable)} re-vendorable")
raise SystemExit(1 if vUnclassified else 0)