Merge pull request #2961 from nrobi144/fix/desktop-proguard-jackson-crash

fix(desktop): fix release build crashes (ProGuard, VLC, jlink modules)
This commit is contained in:
David Kaspar
2026-05-18 16:39:47 +02:00
committed by GitHub
4 changed files with 92 additions and 6 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
}
}
}
@@ -55,6 +55,9 @@ object VlcjPlayerPool {
private val idleThumbPlayers = ConcurrentLinkedQueue<EmbeddedMediaPlayer>()
private const val MAX_THUMB_POOL_SIZE = 2
// Cached plugin path for audio factory creation (set during init)
private var cachedPluginPath: String? = null
// Audio player pool (shared factory with --no-video)
private var audioFactory: MediaPlayerFactory? = null
private val allAudioPlayers = mutableListOf<MediaPlayer>()
@@ -76,12 +79,13 @@ object VlcjPlayerPool {
return try {
// Try bundled VLC first, then fall through to system VLC
val macOsDiscoverer = MacOsVlcDiscoverer()
val discovery =
try {
val nd =
NativeDiscovery(
BundledVlcDiscoverer(),
MacOsVlcDiscoverer(),
macOsDiscoverer,
)
val found = nd.discover()
if (found) {
@@ -99,7 +103,34 @@ object VlcjPlayerPool {
val systemDiscovery = NativeDiscovery().discover()
println("VLC: system discovery ${if (systemDiscovery) "succeeded" else "failed"}")
}
val f = MediaPlayerFactory("--no-xlib")
// Delete stale VLC plugin cache on macOS to avoid spam warnings
if ("mac" in System.getProperty("os.name").lowercase()) {
try {
val cacheDir = java.io.File(System.getProperty("user.home"), "Library/Caches/org.videolan.vlc")
cacheDir.listFiles()?.filter { it.name.startsWith("plugins") }?.forEach { it.delete() }
} catch (_: Throwable) {
// Best-effort cache cleanup
}
}
// Build factory args — add --plugin-path fallback if env var wasn't set
val factoryArgs = mutableListOf("--no-xlib")
if (!macOsDiscoverer.envVarSet) {
val pluginPath =
macOsDiscoverer.discoveredPluginPath
?: System.getProperty("vlc.plugin.path")
?: VlcResourceResolver.findVlcDir()?.let { "${it.absolutePath}/plugins" }
if (pluginPath != null) {
factoryArgs += "--plugin-path=$pluginPath"
println("VLC: using --plugin-path fallback: $pluginPath")
}
}
cachedPluginPath = macOsDiscoverer.discoveredPluginPath
?: System.getProperty("vlc.plugin.path")
val f = MediaPlayerFactory(*factoryArgs.toTypedArray())
factory = f
available.set(true)
println("VLC: MediaPlayerFactory created successfully")
@@ -184,7 +215,9 @@ object VlcjPlayerPool {
val af =
audioFactory ?: try {
MediaPlayerFactory("--no-video", "--no-xlib").also { audioFactory = it }
val audioArgs = mutableListOf("--no-video", "--no-xlib")
cachedPluginPath?.let { audioArgs += "--plugin-path=$it" }
MediaPlayerFactory(*audioArgs.toTypedArray()).also { audioFactory = it }
} catch (_: Throwable) {
return null
}