diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 0ec8f6836e..61485c6e68 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -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")) } } diff --git a/desktopApp/compose-rules.pro b/desktopApp/compose-rules.pro index bb824643d1..8241422ed6 100644 --- a/desktopApp/compose-rules.pro +++ b/desktopApp/compose-rules.pro @@ -96,6 +96,16 @@ native ; } +# 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 diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/MacOsVlcDiscoverer.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/MacOsVlcDiscoverer.kt index d7f9c02f2f..58d7f9fa54 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/MacOsVlcDiscoverer.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/MacOsVlcDiscoverer.kt @@ -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(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 + } + } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VlcjPlayerPool.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VlcjPlayerPool.kt index 18e08c306d..50ea042e58 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VlcjPlayerPool.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VlcjPlayerPool.kt @@ -55,6 +55,9 @@ object VlcjPlayerPool { private val idleThumbPlayers = ConcurrentLinkedQueue() 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() @@ -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 }