fix(desktop): fix release build ProGuard rules that crash app on launch

ProGuard in release builds (packageReleaseDmg/Deb/Rpm) strips classes
accessed via reflection, JNI, or service loading — causing multiple
runtime failures:

- Jackson ExceptionInInitializerError: enum constants stripped, NPE in
  MapperConfig.collectFeatureDefaults() (#2929, 5000 sats bounty)
- JNA/VLCJ: static methods stripped, SIGABRT in native callbacks
- secp256k1/SQLite/kmp-tor: JNI loader classes stripped
- Coil/okhttp/okio: image loading and networking broken
- Kotlin metadata stripped: Jackson can't call default constructors

Changes:
- Upgrade ProGuard 7.7.0 → 7.9.1 (Kotlin 2.3 metadata support)
- Disable optimization (-dontoptimize) to prevent bytecode rewriting
  that produces VerifyError (Guardsquare/proguard#460)
- Add -keep rules for all JNI/reflection-dependent libraries
- Keep Kotlin @Metadata annotations for Jackson deserialization
- Suppress Kotlin 2.3 compile-time stub warnings

Tested: release DMG builds and launches without crash on macOS.

Closes #2929

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-05-18 16:52:27 +03:00
co-authored by Claude Opus 4.6
parent 34f5dcac69
commit 935ad84ac3
3 changed files with 56 additions and 3 deletions
+13 -1
View File
@@ -91,6 +91,9 @@ compose.desktop {
jvmArgs += "-Xmx2g"
// VLC plugin path fallback — used if JNA setenv and bundled discovery both fail
jvmArgs += "-Dvlc.plugin.path=\$APPDIR/resources/vlc/plugins"
// Forward platform-preview overrides from the gradle invocation to the
// launched app's JVM so `./gradlew :desktopApp:run -Damethyst.platform=GNOME`
// works in addition to the env-var form (`AMETHYST_PLATFORM=GNOME`).
@@ -101,7 +104,15 @@ compose.desktop {
nativeDistributions {
appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources"))
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
modules("java.management") // Required by kmp-tor TorRuntime
// Output of ./gradlew suggestRuntimeModules (+ java.management already present)
modules(
"java.instrument", // Runtime instrumentation (agent/profiler hooks)
"java.management", // Required by kmp-tor TorRuntime
"java.prefs", // java.util.prefs (desktop persistence)
"java.sql", // JDBC metadata (Jackson, SQLite driver)
"jdk.security.auth", // JAAS authentication callbacks
"jdk.unsupported", // sun.misc.Unsafe (VLCJ ByteBufferFactory)
)
packageName = "Amethyst"
packageVersion = appVersion
@@ -143,6 +154,7 @@ compose.desktop {
// whose declared return type the JVM verifier rejects (R8 doesn't hit
// this — it generates bridges differently from ProGuard).
buildTypes.release.proguard {
version.set("7.9.1") // Kotlin 2.3 metadata support
configurationFiles.from(project.file("compose-rules.pro"))
}
}
+16
View File
@@ -96,6 +96,16 @@
native <methods>;
}
# kmp-tor — loads native Tor daemon via JNI reflection
-keep class io.matthewnelson.** { *; }
# Coil image loader — uses ServiceLoader for decoder/fetcher registration
-keep class coil3.** { *; }
# OkHttp/Okio — platform detection and I/O via reflection
-keep class okhttp3.** { *; }
-keep class okio.** { *; }
# ============================================================================
# Optimize sub-pass — disable the one that produces invalid okio bytecode
# ============================================================================
@@ -185,3 +195,9 @@
# to detect logging. We ship slf4j-nop; keep it intact so detection succeeds.
-keep class org.slf4j.** { *; }
-dontwarn org.slf4j.**
# ============================================================================
# Kotlin 2.3 stdlib stubs — compile-time classes with no JVM runtime class
# ============================================================================
-dontwarn kotlin.concurrent.atomics.**
-dontwarn kotlin.jvm.internal.EnhancedNullability
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.desktop.service.media
import com.sun.jna.Function
import com.sun.jna.NativeLibrary
import uk.co.caprica.vlcj.binding.lib.LibC
import uk.co.caprica.vlcj.binding.support.runtime.RuntimeUtil
import uk.co.caprica.vlcj.factory.discovery.strategy.BaseNativeDiscoveryStrategy
@@ -36,6 +36,14 @@ class MacOsVlcDiscoverer :
arrayOf("libvlc\\.dylib", "libvlccore\\.dylib"),
arrayOf("%s/plugins"),
) {
/** Plugin path discovered during [setPluginPath], available after discovery. */
var discoveredPluginPath: String? = null
private set
/** Whether [setPluginPath] successfully set the process env var. */
var envVarSet: Boolean = false
private set
override fun supported(): Boolean {
val os = System.getProperty("os.name").lowercase()
return "mac" in os
@@ -52,5 +60,22 @@ class MacOsVlcDiscoverer :
return true
}
override fun setPluginPath(pluginPath: String?): Boolean = LibC.INSTANCE.setenv(PLUGIN_ENV_NAME, pluginPath, 1) == 0
override fun setPluginPath(pluginPath: String?): Boolean {
if (pluginPath == null) return false
discoveredPluginPath = pluginPath
return try {
// Call setenv directly via JNA Function API. This bypasses vlcj's
// LibC interface binding which fails on macOS 13+ because dlsym
// can't resolve the versioned symbol `setenv$3b99ba0d`.
val setenv = Function.getFunction("c", "setenv")
val result = setenv.invokeInt(arrayOf<Any>(PLUGIN_ENV_NAME, pluginPath, 1)) == 0
envVarSet = result
result
} catch (_: Throwable) {
// JNA Function call also failed — VlcjPlayerPool will use
// --plugin-path factory arg as fallback.
envVarSet = false
false
}
}
}