With the main source sets now compiling on both platforms the native
test compilation finally ran, and it rejected a handful of test-side
forms the jvm toolchain silently accepts.
Backtick test names are mapped onto native symbols, where a comma is
an illegal character, so three names drop the comma; the wording keeps
the same meaning. java.lang.Math.PI has no common analogue and becomes
kotlin.math.PI, matching the already-qualified kotlin.math.sin call on
the same line. String.toByteArray() is jvm-only, and the byte-length
assertion for the APRS line budget switches to encodeToByteArray(),
the same replacement the production sources went through.
The previous round fixed the convention plugin and the native expect
declarations, which let both platforms compile far enough to reveal the
next layer: a handful of jvm-only call forms that survived the original
kotlin/native sweep because they look like plain kotlin.
String.toByteArray() and Charsets.UTF_8 live in java.nio.charset and do
not exist on kotlin/native; the stdlib equivalents encodeToByteArray()
compile everywhere and are byte-identical for utf-8, so the APRS packet
length budget and the ADIF field length calculation keep their exact
arithmetic. The DatabaseRepoTest helpers kept an InputStream return type
after their bodies were moved to ByteArray sources, and the java.io
import was gone with the sweep, so the android unit test task could not
resolve them; the fake source maps were already typed () -> ByteArray,
so the return type simply follows the data it now feeds.
The second CI round got past the missing configure import but still
failed in the same file: bare name accessors inside sourceSets { }
(commonMain.dependencies { ... }, commonTest, jvmTest) are kotlin-dsl
script syntax generated for .kts files. Plugin source compiled as plain
Kotlin has no such accessors on its classpath, so the four dependency
blocks failed with receiver type mismatches while every real member call
around them - jvmToolchain, jvm(), the ios targets, binaries.framework -
already resolved.
Configure the source sets through the container API instead:
sourceSets.getByName("commonMain").dependencies { ... }. getByName,
dependencies and implementation are all members on types that ship with
KGP 2.4.10, verified against the gradle plugin jars byte for byte.
getByName is safe at this point because jvm() above has just created the
jvm source sets synchronously; commonMain and commonTest exist as soon
as the multiplatform plugin is applied.
The iOS CI run failed in both jobs before reaching any product code: the
convention plugin itself did not compile. CoreDomainPlugin.kt used the
reified extensions.configure<KotlinMultiplatformExtension> { } form, but
unlike every other plugin in this directory it was missing the
org.gradle.kotlin.dsl.configure import. Without it only the member
overloads taking an explicit type parameter resolve, so the extension
receiver cannot be inferred and all twenty subsequent unresolved
references - jvmToolchain, jvm(), iosArm64(), binaries.framework,
sourceSets with the commonMain/commonTest/jvmTest accessors - are one
cascading failure, not twenty bugs.
build-logic is the first thing both CI jobs compile, and it has never
been compiled anywhere before this run, so the workflow is doing exactly
what it was added for: catching what no local machine can check.
The orbital maths, the satellite models and the repository contracts sat in a Kotlin/JVM
module, so an iOS target could not share a single line of them: java.lang.String.format,
InputStream, System.currentTimeMillis, java.util.Locale and org.json are all JVM-only, and
the tests that covered them used JUnit4. core:domain now declares jvm, iosArm64 and
iosSimulatorArm64 targets, its sources moved to commonMain/commonTest, and the JVM-only
pieces were replaced with multiplatform equivalents: java.lang.String.format by a shared
printf implementation, System.currentTimeMillis by kotlin.time.Clock, InputStream by
ByteArray, org.json by kotlinx-serialization, Locale by nothing at all. Tests that read
classpath resources (javaClass.classLoader) moved to jvmTest, because that is JVM-only
behaviour rather than a JVM-only API.
Auditing the migration against the old module turned up four things that were wrong rather
than merely ported:
- java.lang.String.format rounds the shortest decimal representation of a double half-up,
not the binary value: "%.3f" of 0.5005 is "0.501", because the stored double is
0.50049999999999994493. The shared implementation scaled in binary first and printed
"0.500", which would have changed APRS position packets and the Wavelog frequency fields
against the released Android app. It now takes the digits from the decimal representation
and rounds them with integer arithmetic, and jvmTest compares it against
String.format(Locale.ROOT, ...) over 40 000 sampled doubles plus the boundary cases, while
commonTest pins literals so the iOS run checks the same digits.
- The queue mutators lost the kotlin.jvm.Synchronized monitor each of them had. It is not a
JVM-only annotation - it is an optional expectation, so it still compiles in common code -
but the stdlib deprecated it for common use in 1.8 and made it an error in 2.1. The monitor
is a platform actual now: the JVM keeps the real monitor, since Compose and the upload
coroutine both reach the queue there, and iOS carries a documented placeholder until the
iOS side has a second thread to protect against.
- 107 assertions in DataParserTest and QthConverterTest were bare kotlin.assert calls, which
a build without -ea skips silently: they are assertTrue now, so the iOS run cannot pass
vacuously. The three Locale.setDefault cases (ar-EG, bn-BD, fa-IR) that used to guard APRS
output against Eastern Arabic digits moved to jvmTest instead of being deleted with the
Locale dependency - APRS-IS is an ASCII protocol, and Locale.setDefault does not exist on
iOS.
- @Volatile on the LoTW name cache would not have compiled for iOS either: kotlin.jvm's
variant is an error in common code since 2.1. kotlin.concurrent.Volatile is the
multiplatform annotation, and it is the stronger form: it takes effect on Kotlin/Native
rather than being ignored.
A second audit pass over the files the first one could not reach - the HTTP client, the
parsers, the queue and the injection - found three more:
- OkHttpHttpClient built its Request outside the try, so a URL OkHttp refuses to parse left
postQso/testToken/getStation as an exception, and neither caller catches one. The client it
replaced reported HTTP -1 and let the caller treat it as a failure; building the request
inside the try restores that, and a transport failure reports -1 again rather than 0.
- WavelogQueue's readers were stricter than the org.json ones they replaced. A timestamp
stored as 1234.0 (or "1234.0") read back as 0L instead of 1234 - a QSO uploaded as 1970 -
and a field holding an object or array threw the whole list away instead of falling back.
The readers coerce decimals, keep the old defaults and no longer throw, matching optLong,
optInt, optString and optBoolean.
- The ADIF dates went through the JVM default locale before, so a device set to Arabic wrote
Eastern Arabic digits into the QSO date. The shared formatter only ever produces ASCII,
which the locale cases in AprsPacketDefaultLocaleTest pin down.
Verified locally with ./gradlew jvmTest (343 tests, 0 failures) and the multiplatform gate
in check-multiplatform.sh, which now also refuses JVM-only stdlib APIs that resolve in common
code but fail to compile for iOS: @Synchronized, kotlin.jvm.Volatile, synchronized(),
toUpperCase/toLowerCase/capitalize, BigDecimal. The iOS targets themselves need the macOS
runner in .github/workflows/ios-kmp.yml.