From d3921cd7f547af9448ae3ddcd5570908e5aa53d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 10 Aug 2026 15:40:15 -0400 Subject: [PATCH] fix(ci): unbreak iOS compile and the arm64 desktop smoke test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/smoke-test-desktop.yml | 20 +++++++++++++++++-- commons/build.gradle.kts | 4 ++++ ...ddressableAuthorRelayLoaderSubAssembler.kt | 11 ++++++---- quartz/build.gradle.kts | 4 ++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/smoke-test-desktop.yml b/.github/workflows/smoke-test-desktop.yml index 6d8159ab0e..57b992ae73 100644 --- a/.github/workflows/smoke-test-desktop.yml +++ b/.github/workflows/smoke-test-desktop.yml @@ -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 diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 2fccbea2ce..952b39c596 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -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 = diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt index 68e5a40544..46dbae7ac7 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt @@ -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 = emptySet() @@ -92,7 +95,7 @@ class AddressableAuthorRelayLoaderSubAssembler( * never call back into this class. Revisit if that changes. */ private fun commit(needed: Set) { - 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()) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 8a93c5758d..4771e49055 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -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 =