Skip to content

v2: make the pure modules Kotlin Multiplatform (iOS port, phase 0) - #1075

Open
jaydeetay wants to merge 9 commits into
feature/render-gles3from
feature/ios-port
Open

jaydeetay wants to merge 9 commits into
feature/render-gles3from
feature/ios-port

Conversation

@jaydeetay

@jaydeetay jaydeetay commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Description

Phase 0 of the iOS port: the pure modules become Kotlin Multiplatform, so an iOS app can share
the math, astronomy, catalog, events and renderer-contract code instead of reimplementing it.
Android is unchanged. The Android modules and :data:generator consume each module's jvm
target exactly as they consumed the old kotlin("jvm") jar, and the generated skymap.db is
byte-identical.

Stacked on #1058. The eventual Metal backend follows the GLES3 backend; GLES1 is not ported.

Build

  • New skymap.pure-kmp convention: jvm + iosArm64 + iosSimulatorArm64 + iosX64 (the
    last one for simulators on Intel Macs). :core:math, :core:astronomy, :core:catalog,
    :core:events and :render:api use it. :data:generator and :konsist stay on
    skymap.pure-kotlin as JVM tools.
  • Sources moved to src/commonMain with git mv, so history is kept.
  • One expect/actual: NameNormalizer's NFD step (java.text.Normalizer on the JVM,
    Foundation on Apple). Plus two imports (kotlin.concurrent.Volatile, kotlin.jvm.JvmInline).

Tests run on every target

  • All suites moved to commonTest. Truth is JVM-only, so the new :core:testing provides
    Truth's names and semantics for exactly the subset in use. Each suite changed only its
    imports, plus assertThrows → assertFailsWith and a few JDK calls. The shim's own tests pin
    that every assertion can fail.
  • Kotlin/Native rejects commas in backtick test names, so ten names swap , for -.
  • The first iOS run found a real bug: Kotlin/Native's regex engine rejects \p{N}, so
    NameNormalizer could not even initialise on iOS. It now spells N out as
    \p{Nd}\p{Nl}\p{No}, which is the same set, and a new test covers Nl/No.
  • Search agrees across platforms. The catalog DB is normalized on the JVM, but each
    platform normalizes search queries itself. NameNormalizerCorpusTest re-normalizes all
    12,516 catalog names (exported by the new :data:generator:exportNameCorpus) on every target
    and requires the JVM's result. All agree on the iOS simulator. With the Apple NFD step
    disabled, it reports 2,109 mismatches, so the check isn't vacuous.
  • On a Mac with only the Command Line Tools, check skips the iOS link/test tasks with a
    warning instead of failing, so Android-only setups keep working.

Konsist: closes the :core:events gap, admits :core:testing, and bans java.*/javax.*
in the shared modules' non-jvm source sets. (Checked with probe files: the import fails the
gate in commonMain and passes in jvmMain.)

CI: a new macos-15 job runs iosSimulatorArm64Test.

Type of Change

  • Bug fix
  • New feature (Sky Map team only)
  • Translation (new or updated)
  • Documentation
  • Refactoring / code cleanup
  • Dependency upgrade
  • Other

Checklist

  • I've read the contributing guidelines
  • For major changes, I've emailed skymapdevs@gmail.com first
  • I've run the unit tests (./gradlew check from stardroid-v2/, or ./gradlew :app:test
    from stardroid-v1/, matching whichever module this PR touches)
  • I've tested on a device/emulator if applicable
  • If I have multiple commits, I've squashed them into one

No need to update CHANGELOG.md - we'll get Claude to do that when we cut a release.

Notes for Reviewers

  • Verified locally (Intel Mac, Xcode 26.6, iOS 26.5 simulator): ./gradlew check is green,
    and all 340 tests pass on both the JVM and the iOS simulator (iosX64Test). Per-module JVM
    counts match the pre-conversion suites, plus the shim's 12 tests, the new NameNormalizer
    case and the corpus test.
  • Not yet verified: the new macOS CI job. The workflow only triggers on PRs to master, so
    it won't run on this stacked PR until it is retargeted after v2: a selectable GL ES 3.0 renderer backend beside GLES1 #1058 merges (or is dispatched by
    hand on this branch).
  • Review mostly as renames. git diff -M shows almost every file as R09x with import-only
    changes. The substance is in build-logic/.../skymap.pure-kmp.gradle.kts, core/testing/,
    NameNormalizer*.kt, RecordingSkyRenderer.kt (ConcurrentHashMap → a common atomic
    reference) and ArchitectureTest.kt.
  • Docs: build-and-tooling.md has a new Kotlin Multiplatform section; code-overview.md,
    high-level-architecture.md and AGENTS.md are updated for the layout.

🤖 Generated with Claude Code

jaydeetay and others added 4 commits September 23, 2026 18:21
:core:math, :core:astronomy, :core:catalog, :core:events and :render:api
move from kotlin("jvm") to Kotlin Multiplatform under a new skymap.pure-kmp
convention: a jvm target, which the Android modules and :data:generator
consume exactly as they consumed the old jar, plus iosArm64,
iosSimulatorArm64 and (for simulators on Intel Macs) iosX64. Sources move
to src/commonMain; history is preserved by git mv.

The architecture's bet held. Main code needed one expect/actual — the NFD
step of NameNormalizer (java.text.Normalizer on the JVM, Foundation's
decomposedStringWithCanonicalMapping on Apple) — and two imports
(kotlin.concurrent.Volatile, kotlin.jvm.JvmInline). All five modules
compile for every iOS target.

The tests move to commonTest so the goldens run on iOS too, which is the
point of sharing the core. Truth is JVM-only, so a new :core:testing module
provides Truth's names and semantics for exactly the subset in use; each
suite changed only its imports, plus assertThrows -> assertFailsWith and a
few JDK calls (Math.toRadians, Comparator.reverseOrder, ConcurrentHashMap).
The shim's own tests pin that every assertion can fail. Kotlin/Native
rejects commas in backtick names, so ten test names swap ", " for " - ".
All 326 existing tests pass on the JVM with unchanged per-module counts.

Linking and running iOS test binaries needs a full Xcode, not just the
Command Line Tools. On a Mac without it, check now skips the iOS link/test
tasks with a warning rather than failing, so an Android-only setup can
still run check.

Konsist: closes the known :core:events gap, admits :core:testing, and adds
a rule that shared modules' non-jvm source sets import no java.*/javax.*
(verified with a probe file in commonMain, which fails, and in jvmMain,
which passes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Linux check job cannot build Apple targets, so the multiplatform
modules' commonTest suites would otherwise only ever run on the JVM. A
macos-15 (Apple silicon) job runs iosSimulatorArm64Test and caches ~/.konan,
since the Kotlin/Native toolchain and its LLVM download on first use.

Not yet exercised: this commit is the first time the job runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build-and-tooling.md gains a Kotlin Multiplatform section (targets, layout,
the one expect/actual, :core:testing, the Xcode rule), updates the module
graph and convention-plugin table, closes the :core:events Konsist gap note,
and records that D20's deferred "layer 3" turned out structural. The
architecture doc, code overview and AGENTS.md point at the new layout so
new shared code and tests land in commonMain/commonTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first run of the shared core's tests on the iOS simulator failed every
NameNormalizer test with FileFailedToInitializeException: Kotlin/Native's
regex engine rejects \p{N} (the Number category) with "No such character
class", although it accepts \p{L}, \p{M} and every subcategory. The
separator pattern now spells N out as \p{Nd}\p{Nl}\p{No}, which is the
same set by Unicode's definition, and a new test pins that letter-numbers
(Nl) and other-numbers (No) still count as word characters.

No change on Android: the generated skymap.db is byte-identical before and
after (sha256 71cc23b8…, 3448 objects, 19686 names).

All 339 tests now pass on both the JVM and the iOS simulator (iosX64,
Xcode 26.6, iOS 26.5 runtime).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Avoid failing if allTests task absent

Registering a configuration hook on tasks.named("allTests") causes Gradle
configuration failure whenever allTests is not created by the KMP plugin (for
example, when running tasks on Linux/Windows where Apple targets are automatically
omitted, or when targets are disabled). Avoid tasks.named("allTests") and instead
use tasks.matching { it.name == "allTests" }.configureEach { ... } or conditionally
locate it with tasks.findByName to prevent UnknownTaskException.

stardroid-v2/build-logic/src/main/kotlin/skymap.pure-kmp.gradle.kts [57-79]

 if (System.getProperty("os.name").startsWith("Mac")) {
     val xcodeInstalled =
         providers
             .exec {
                 commandLine("xcrun", "xcodebuild", "-version")
                 isIgnoreExitValue = true
             }.result
             .map { it.exitValue == 0 }
     tasks.withType<KotlinNativeLink>().configureEach {
         onlyIf("Xcode is installed") { xcodeInstalled.get() }
     }
     tasks.withType<KotlinNativeTest>().configureEach {
         onlyIf("Xcode is installed") { xcodeInstalled.get() }
     }
-    tasks.named("allTests") {
+    tasks.matching { it.name == "allTests" }.configureEach {
         val modulePath = path
         doFirst {
             if (!xcodeInstalled.get()) {
                 logger.warn("$modulePath: iOS tests SKIPPED — Xcode is not installed.")
             }
         }
     }
 }
Suggestion importance[1-10]: 8

__

Why: On macOS without Apple targets active or depending on Kotlin Multiplatform lifecycle task registration timing, tasks.named("allTests") will throw UnknownTaskException during configuration if allTests does not exist. Using tasks.matching { it.name == "allTests" }.configureEach prevents Gradle configuration from breaking unexpectedly.

Medium

jaydeetay and others added 2 commits September 26, 2026 21:03
The catalog DB stores names normalized on the JVM at generation time, but
each platform normalizes search queries with its own Unicode tables (NFD,
Regex, lowercase). A name the two disagree on is one iOS search can never
find, and the \p{N} bug fixed last commit showed the tables really do
differ even where the code is common.

:data:generator gains an exportNameCorpus task (NameCorpusExport) that
writes every distinct catalog name with its JVM-normalized form: 12,516
names across Latin, Cyrillic, Greek, Devanagari, Hangul and more. The new
commonTest NameNormalizerCorpusTest re-normalizes all of them on each
target and reports any disagreement with code points. :core:catalog's test
tasks run the export first and pass its path in SKYMAP_NAME_CORPUS; the
simulator needs SIMCTL_CHILD_SKYMAP_NAME_CORPUS, because xcrun simctl
forwards only prefixed variables. The test fails loudly if the variable is
missing, which is how that was found. :core:testing gains
environmentVariable/readTextFile (JVM and Apple actuals) for this pattern.

All 12,516 names agree on the iOS simulator. The check is not vacuous:
with the Apple NFD step disabled it reports 2,109 mismatches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The FTS entity's comment framed `simple` purely as an Android 10
workaround, which invites "restoring" unicode61 once Android 10 support
goes. It shouldn't be restored: unicode61 strips diacritics from Latin
script only, and 1,724 of the catalog's 12,516 names carry marks on
non-Latin letters (Thai, Greek, Devanagari, Katakana, Arabic, Cyrillic...).
Tested against SQLite with remove_diacritics=2: "andromeda*" finds
Andrómeda, but "τουκανας*" and "золотоі*" find nothing. Folding in
NameNormalizer covers every script, and is identical across SQLite builds
and platforms, which NameNormalizerCorpusTest checks.

The comment now says so, and catalog-and-schema.md stops describing a
"unicode tokenizer with diacritic removal" (and an @FTS5 Room doesn't
have). Comment and doc only; no behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jaydeetay
jaydeetay added this pull request to stack #1077 September 28, 2026 22:40
jaydeetay and others added 3 commits September 30, 2026 16:18
Brings in the GLES3 branch's ground hemisphere (GlowPrimitive removed,
Ground/GroundRamp/MoonShading added to :render:api), the per-frame
LabelFader allocation fix, the constant parity guard, and master at
2.1.2:Artemis.

Resolutions for the multiplatform layout:
- GroundRamp.kt and MoonShading.kt land in :render:api's commonMain, and
  GroundRampTest in commonTest, where git's directory-rename detection put
  them. GroundRampTest moves to kotlin.test and :core:testing's assertThat.
- PrimitivesTest takes the branch's side (its glow tests go with
  GlowPrimitive).
- Test names with commas (three in GroundRampTest, one new in
  LabelFaderTest) swap ", " for " - ", which Kotlin/Native requires.

All :render:api tests pass on the JVM and the iOS simulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linux CI never compiles the iOS targets, so a test named with a comma or
parentheses, legal on the JVM, only failed later on macOS: the GLES3
branch's latest merge brought in four. The Konsist gate, which runs in
the Linux check, now flags any backtick name in the shared modules'
non-jvm source sets using anything but letters, digits, spaces, hyphens
and apostrophes. Probed: a test named `a, b` fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decision log renumbered the phase-0 entries when it caught up with
public work: the name-corpus check is D126 and the simple tokenizer is
D127, not D115 and D116 (D115 is now the minSdk 28 decision).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant