fix(ci): unbreak iOS compile and the arm64 desktop smoke test

Two red checks on this branch, from two unrelated causes.

1. `test-quartz-ios` — AddressableAuthorRelayLoaderSubAssembler moved into
   commonMain still calling `synchronized(lock)`. That resolves from
   kotlin-stdlib-jvm with no import, so it compiles on Android/JVM and only
   fails at `:commons:compileKotlinIosSimulatorArm64`. Swapped to
   `KmpLock.withLock {}` (reentrant on every platform, and `withLock` is
   inline so `commit()`'s early `return` still works).

   The `verifyKmpPurity` gate missed it because it only forbade the
   `kotlin.jvm.Synchronized` *annotation*, not the bare call. Added
   `synchronized(` to the forbidden list in both :commons and :quartz so the
   next one fails in seconds instead of at the iOS compile step.

2. `release-deb-launch (ubuntu-24.04-arm)` — pre-existing infra break, not
   from this branch (same failure on other PRs since ~Aug 5).
   libskiko-linux-arm64.so needs libEGL.so.1 and the runner has no libegl1,
   so the app died at startup. create-release.yml already fixes this via
   scripts/add-deb-libegl-dep.sh; the smoke test never adopted it. Added that
   step, and switched the install from `dpkg -i` (which does not resolve
   dependencies) to `apt-get install ./x.deb` so the declared libegl1 is
   actually pulled in — this now exercises the same artifact release ships.

Verified: :commons:compileKotlinIosSimulatorArm64, both verifyKmpPurity gates
(and confirmed the new pattern fails when the bug is reintroduced),
:commons:jvmTest, :amethyst:testPlayDebugUnitTest for the assembler test, and
the .deb libegl mechanism end-to-end in an arm64 ubuntu:24.04 container.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-08-10 15:40:15 -04:00
co-authored by Claude Opus 5
parent eaba40f2a6
commit d3921cd7f5
4 changed files with 33 additions and 6 deletions
+18 -2
View File
@@ -92,13 +92,29 @@ jobs:
chmod +x scripts/relax-deb-libicu.sh
scripts/relax-deb-libicu.sh desktopApp/build/compose/binaries/main-release/deb/*.deb
# Mirrors the same step in create-release.yml so this job exercises the
# exact .deb release ships. libskiko-linux-arm64.so has libEGL.so.1 in
# DT_NEEDED and jpackage does not scan lib/app/ for Depends, so without
# this the arm64 app dies at startup with
# UnsatisfiedLinkError: libEGL.so.1: cannot open shared object file
# See scripts/add-deb-libegl-dep.sh for the full rationale.
- name: Add libegl1 dep to arm64 .deb
run: |
set -euo pipefail
chmod +x scripts/add-deb-libegl-dep.sh
scripts/add-deb-libegl-dep.sh desktopApp/build/compose/binaries/main-release/deb/*.deb
- name: Install .deb
run: |
# Installed via apt (not `dpkg -i`) so the .deb's declared Depends are
# actually resolved — that is what pulls in libegl1 on the arm64
# runner, which does not ship it preinstalled.
#
# jpackage's post-install script runs xdg-desktop-menu which fails
# on CI runners ("No writable system menu directory"). The files are
# extracted successfully; only the menu registration fails. Allow the
# dpkg error, then verify the binary was actually installed.
sudo dpkg -i desktopApp/build/compose/binaries/main-release/deb/*.deb || true
# install error, then verify the binary was actually installed.
sudo apt-get install -y ./desktopApp/build/compose/binaries/main-release/deb/*.deb || true
echo "Installed files:"
dpkg -L amethyst | head -30
# Fail if the binary wasn't actually extracted
+4
View File
@@ -297,6 +297,10 @@ val verifyKmpPurity by tasks.registering {
"Thread.sleep" to "use kotlinx.coroutines.delay or platform-specific actual",
"java.util.UUID" to "use kotlin.uuid.Uuid",
"kotlin.jvm.Synchronized" to "use KmpLock.withLock {}",
// The bare call, not just the annotation: `synchronized(lock) {}` resolves
// from kotlin-stdlib-jvm with no import, so it compiles on Android/JVM and
// only fails at the iOS compile step. Catch it here instead.
"synchronized(" to "`synchronized` is JVM-only — use KmpLock.withLock {}",
"kotlin.jvm.Volatile" to "use kotlin.concurrent.Volatile",
)
val offenders =
@@ -27,6 +27,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryStat
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState
import com.vitorpamplona.amethyst.commons.service.BundledUpdate
import com.vitorpamplona.amethyst.commons.util.KmpLock
import com.vitorpamplona.amethyst.commons.util.withLock
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
@@ -46,8 +48,9 @@ class AddressableAuthorRelayLoaderSubAssembler(
val userFinder: UserFinderFilterAssembler,
) : IEoseManager {
// Private monitor: @Synchronized locks on `this`, which leaves the instance's monitor
// reachable to anything holding a reference to this assembler.
private val lock = Any()
// reachable to anything holding a reference to this assembler. KmpLock (not `synchronized`)
// because this file lives in commonMain and must compile for the iOS targets too.
private val lock = KmpLock()
// Only ever touched while holding [lock]. See commit() and destroy().
private var activeSubscriptions: Set<UserFinderQueryState> = emptySet()
@@ -92,7 +95,7 @@ class AddressableAuthorRelayLoaderSubAssembler(
* never call back into this class. Revisit if that changes.
*/
private fun commit(needed: Set<UserFinderQueryState>) {
synchronized(lock) {
lock.withLock {
if (destroyed) return
userFinder.subscribe((needed - activeSubscriptions).toList())
@@ -103,7 +106,7 @@ class AddressableAuthorRelayLoaderSubAssembler(
}
override fun destroy() {
synchronized(lock) {
lock.withLock {
destroyed = true
bundler.cancel()
userFinder.unsubscribe(activeSubscriptions.toList())
+4
View File
@@ -424,6 +424,10 @@ val verifyKmpPurity by tasks.registering {
"Thread.sleep" to "use kotlinx.coroutines.delay or platform-specific actual",
"java.util.UUID" to "use kotlin.uuid.Uuid",
"kotlin.jvm.Synchronized" to "use a KMP lock primitive",
// The bare call, not just the annotation: `synchronized(lock) {}` resolves
// from kotlin-stdlib-jvm with no import, so it compiles on Android/JVM and
// only fails at the iOS compile step. Catch it here instead.
"synchronized(" to "`synchronized` is JVM-only — use a KMP lock primitive",
"kotlin.jvm.Volatile" to "use kotlin.concurrent.Volatile",
)
val offenders =