@@ -14,15 +14,21 @@ interface SNIWrapper : Library {
1414 * Robust native library loading strategy to work with packagers like Conveyor.
1515 *
1616 * Resolution order (Linux):
17- * 1) System property override with absolute path:
17+ * 1) System.loadLibrary("tray") - FIRST to support external extractors like Conveyor
18+ * (app.jvm.extract-native-libraries=true) that provide the library in JVM library path
19+ * 2) System property override with absolute path:
1820 * -Dcomposetray.native.lib=/abs/path/libtray.so
19- * 2 ) System property pointing to a directory containing the library:
21+ * 3 ) System property pointing to a directory containing the library:
2022 * -Dcomposetray.native.lib.path=/abs/dir (expects libtray.so inside)
21- * 3) System.loadLibrary("tray") so that external extractors (e.g. Conveyor
22- * app.jvm.extract-native-libraries=true) can provide the library in the
23- * JVM library path.
24- * 4) Fallback to JNA resource/classpath discovery Native.load("tray").
23+ * 4) Fallback to JNA resource/classpath discovery Native.load("tray")
2524 */
25+ private fun tryLoadViaSystemLibrary (): Boolean = try {
26+ System .loadLibrary(" tray" )
27+ true
28+ } catch (_: Throwable ) {
29+ false
30+ }
31+
2632 private fun tryLoadViaSystemProperties (): Boolean {
2733 val explicitFile = System .getProperty(" composetray.native.lib" )?.trim()?.takeIf { it.isNotEmpty() }
2834 if (! explicitFile.isNullOrEmpty()) {
@@ -51,11 +57,6 @@ interface SNIWrapper : Library {
5157 return false
5258 }
5359
54- private fun tryLoadViaSystemLibrary (): Boolean = try {
55- System .loadLibrary(" tray" )
56- true
57- } catch (_: Throwable ) { false }
58-
5960 private fun isLinux (): Boolean = System .getProperty(" os.name" )?.lowercase()?.contains(" linux" ) == true
6061
6162 private fun archFolder (): String {
@@ -86,34 +87,59 @@ interface SNIWrapper : Library {
8687
8788 private fun preloadLinuxDependencies () {
8889 if (! isLinux()) return
90+
8991 val tmpBase = Files .createTempDirectory(" composetray-natives-" )
90- // Attempt to extract versioned first, then soname link
91- val base = " ${archFolder()} "
92- val candidates = listOf (
93- " $base /libdbusmenu-qt5.so.2.6.0 " ,
94- " $base /libdbusmenu-qt5.so.2"
92+
93+ // First, extract ALL libraries to the temp directory
94+ val base = archFolder()
95+ val libraries = listOf (
96+ " $base /libdbusmenu-qt5.so.2" ,
9597 )
96- var loaded = false
97- for (res in candidates) {
98- val p = extractResourceToDir(res, tmpBase)
99- if (p != null && p.toFile().exists()) {
100- try {
101- System .load(p.toAbsolutePath().toString())
102- loaded = true
103- break
104- } catch (_: Throwable ) {
105- // try next
106- }
107- }
98+
99+ // Extract all libraries first
100+ for (res in libraries) {
101+ extractResourceToDir(res, tmpBase)
108102 }
109- if (! loaded) {
110- // If extraction failed or not packaged, continue without preloading
111- } else {
112- // Make JNA search path include temp dir so that transitive loads can find siblings
103+
104+ // Add the temp directory to JNA's search path BEFORE loading
105+ try {
106+ NativeLibrary .addSearchPath(" tray" , tmpBase.toAbsolutePath().toString())
107+ // Also add to java.library.path for System.loadLibrary to find them
108+ val currentPath = System .getProperty(" java.library.path" , " " )
109+ val newPath = if (currentPath.isNotEmpty()) {
110+ " $currentPath :${tmpBase.toAbsolutePath()} "
111+ } else {
112+ tmpBase.toAbsolutePath().toString()
113+ }
114+ System .setProperty(" java.library.path" , newPath)
115+
116+ // Force ClassLoader to reload the library path
117+ // This is a hack but necessary for runtime modifications
113118 try {
114- NativeLibrary .addSearchPath(" tray" , tmpBase.toAbsolutePath().toString())
119+ val sysPathsField = ClassLoader ::class .java.getDeclaredField(" sys_paths" )
120+ sysPathsField.isAccessible = true
121+ sysPathsField.set(null , null )
115122 } catch (_: Throwable ) {
116- // ignore
123+ // Ignore - this hack might not work on all JVMs
124+ }
125+ } catch (_: Throwable ) {
126+ // ignore
127+ }
128+
129+ // Now load libraries in dependency order
130+ val loadOrder = listOf (
131+ " libdbusmenu-qt5.so.2"
132+ )
133+
134+ for (libName in loadOrder) {
135+ val libPath = tmpBase.resolve(libName)
136+ if (libPath.toFile().exists()) {
137+ try {
138+ System .load(libPath.toAbsolutePath().toString())
139+ } catch (e: Throwable ) {
140+ // Log but continue - some might already be loaded
141+ println (" Warning: Could not load $libName : ${e.message} " )
142+ }
117143 }
118144 }
119145 }
@@ -122,13 +148,36 @@ interface SNIWrapper : Library {
122148 // Preload Linux-only dependencies bundled in resources
123149 preloadLinuxDependencies()
124150
125- // 1) Property overrides
126- if (! tryLoadViaSystemProperties()) {
127- // 2) System library path
128- tryLoadViaSystemLibrary()
151+ var loaded = false
152+
153+ // 1) FIRST try System.loadLibrary - this supports Conveyor and standard JVM library paths
154+ if (tryLoadViaSystemLibrary()) {
155+ loaded = true
156+ println (" Loaded libtray via System.loadLibrary" )
157+ }
158+
159+ // 2) If not loaded, try property overrides
160+ if (! loaded && tryLoadViaSystemProperties()) {
161+ loaded = true
162+ println (" Loaded libtray via system properties" )
163+ }
164+
165+ // 3) Always call JNA's Native.load as final step
166+ // If library was already loaded above, JNA will detect this and just create the proxy
167+ // If not loaded yet, JNA will try to load from resources/classpath
168+ try {
169+ Native .load(" tray" , SNIWrapper ::class .java) as SNIWrapper
170+ } catch (e: UnsatisfiedLinkError ) {
171+ // If we thought we loaded it but JNA can't create proxy, provide helpful error
172+ if (loaded) {
173+ throw UnsatisfiedLinkError (
174+ " Library was loaded via System.loadLibrary or properties but JNA couldn't create proxy. " +
175+ " This might indicate a version mismatch or architecture incompatibility: ${e.message} "
176+ )
177+ } else {
178+ throw e
179+ }
129180 }
130- // 3) Always let JNA resolve as well (will succeed if already loaded)
131- Native .load(" tray" , SNIWrapper ::class .java) as SNIWrapper
132181 }
133182 }
134183
@@ -172,7 +221,8 @@ interface SNIWrapper : Library {
172221 fun set_context_menu (handle : Pointer ? , menu : Pointer ? )
173222 fun add_menu_action (menu_handle : Pointer ? , text : String? , cb : ActionCallback ? , data : Pointer ? ): Pointer ?
174223 fun add_disabled_menu_action (menu_handle : Pointer ? , text : String? , cb : ActionCallback ? , data : Pointer ? ): Pointer ?
175- fun add_checkable_menu_action (menu_handle : Pointer ? , text : String? , checked : Int , cb : ActionCallback ? , data : Pointer ? ): Pointer ? fun add_menu_separator(menu_handle: Pointer ? )
224+ fun add_checkable_menu_action (menu_handle : Pointer ? , text : String? , checked : Int , cb : ActionCallback ? , data : Pointer ? ): Pointer ?
225+ fun add_menu_separator (menu_handle : Pointer ? )
176226 fun create_submenu (menu_handle : Pointer ? , text : String? ): Pointer ?
177227 fun set_menu_item_text (menu_item_handle : Pointer ? , text : String? )
178228 fun set_menu_item_enabled (menu_item_handle : Pointer ? , enabled : Int )
@@ -199,4 +249,4 @@ interface SNIWrapper : Library {
199249
200250 // Debug mode management
201251 fun sni_set_debug_mode (enabled : Int )
202- }
252+ }
0 commit comments