在NixOS上使用IDEA进行JavaFX开发

前因

笔者的Java课最后大作业需要用到图形库,众所周知Java图形库稀少,大概只有JavaFX运用比较广泛,然而笔者在IDEA上配置JavaFX时遇到了本地原生库找不到依赖问题。虽然由于NixOS特殊的动态库管理这个问题还算正常,但是解决起来笔者花了不少功夫走了不少弯路……

环境

笔者使用的是nixpkgs上打包的IDEA: nixpkgs.jetbrains.idea-ultimate,版本为2023.1

使用的jdk是jetbrains fork的openjdk: nixpkgs.jetbrains.jdk,版本为17.0.6-b829.5(为了兼容性)

JavaFx使用的是Maven库中的17.0.6版本,由Gradle进行管理:

1
2
3
4
5
6
7
8
9
10
11
12
plugins {
id 'java'
id 'application'
id 'org.javamodularity.moduleplugin' version '1.8.12'
id 'org.openjfx.javafxplugin' version '0.0.13'
id 'org.beryx.jlink' version '2.25.0'
}

javafx {
version = '17.0.6'
modules = ['javafx.controls', 'javafx.fxml', 'javafx.media']
}

如果文章的解决方法不生效,不排除版本不同或者构建工具不同等原因。

问题

IDEA新建JavaFX项目,等待依赖下载/构建/索引完成后运行主类 HelloApplication,抛出错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Loading library glassgtk3 from resource failed: java.lang.UnsatisfiedLinkError: /home/frnks/.openjfx/cache/17.0.6/libglassgtk3.so: libXtst.so.6: cannot open shared object file: No such file or directory
java.lang.UnsatisfiedLinkError: /home/frnks/.openjfx/cache/17.0.6/libglassgtk3.so: libXtst.so.6: cannot open shared object file: No such file or directory
at java.base/jdk.internal.loader.NativeLibraries.load(Native Method)
at java.base/jdk.internal.loader.NativeLibraries$NativeLibraryImpl.open(NativeLibraries.java:388)
at java.base/jdk.internal.loader.NativeLibraries.loadLibrary(NativeLibraries.java:232)
at java.base/jdk.internal.loader.NativeLibraries.loadLibrary(NativeLibraries.java:174)
at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2389)
at java.base/java.lang.Runtime.load0(Runtime.java:755)
at java.base/java.lang.System.load(System.java:1953)
at javafx.graphics/com.sun.glass.utils.NativeLibLoader.installLibraryFromResource(NativeLibLoader.java:217)
at javafx.graphics/com.sun.glass.utils.NativeLibLoader.loadLibraryFromResource(NativeLibLoader.java:197)
at javafx.graphics/com.sun.glass.utils.NativeLibLoader.loadLibraryInternal(NativeLibLoader.java:138)
at javafx.graphics/com.sun.glass.utils.NativeLibLoader.loadLibrary(NativeLibLoader.java:54)
at javafx.graphics/com.sun.glass.ui.gtk.GtkApplication.lambda$new$6(GtkApplication.java:195)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:318)
at javafx.graphics/com.sun.glass.ui.gtk.GtkApplication.<init>(GtkApplication.java:179)
at javafx.graphics/com.sun.glass.ui.gtk.GtkPlatformFactory.createApplication(GtkPlatformFactory.java:41)
at javafx.graphics/com.sun.glass.ui.Application.run(Application.java:146)
at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.startup(QuantumToolkit.java:291)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:293)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:163)
at javafx.graphics/com.sun.javafx.application.LauncherImpl.startToolkit(LauncherImpl.java:659)
at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:410)
at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:364)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1082)

Openjfx似乎会自己建立一个 .openjfx/cache/${version} 文件夹用于存储native library。

openjfx读取原生动态库的代码:NativeLibLoader.java

笔者没有细读代码找出 NativeLibLoader 是从哪里找到动态库并且写入cache文件夹的,总之 ldd libglassgtk3.so 提示大部分动态库找不到,说明这些动态库并没有被 patchelf 过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> ldd libglassgtk3.so
ldd: warning: you do not have execution permission for `./libglassgtk3.so'
linux-vdso.so.1 (0x00007ffc67352000)
libgtk-3.so.0 => not found
libgdk-3.so.0 => not found
libcairo.so.2 => not found
libgdk_pixbuf-2.0.so.0 => not found
libgio-2.0.so.0 => not found
libgobject-2.0.so.0 => not found
libgthread-2.0.so.0 => not found
libglib-2.0.so.0 => not found
libXtst.so.6 => not found
libpthread.so.0 => /nix/store/1n2l5law9g3b77hcfyp50vrhhssbrj5g-glibc-2.37-8/lib/libpthread.so.0 (0x00007f9f5d56d000)
libc.so.6 => /nix/store/1n2l5law9g3b77hcfyp50vrhhssbrj5g-glibc-2.37-8/lib/libc.so.6 (0x00007f9f5d01a000)
/nix/store/1n2l5law9g3b77hcfyp50vrhhssbrj5g-glibc-2.37-8/lib64/ld-linux-x86-64.so.2 (0x00007f9f5d576000)

注意这个 cache 文件夹每次构建项目时都会被覆盖,所以不能简单地用系统中patch过的动态库覆盖目录中的动态库,详情见解决方法。

解决

其实解决方法非常简单,在这个loader代码中有个 loadLibraryInternal 方法,其中读取 java.library.path 参数作为路径并从中加载动态库,所以我们只要在Java虚拟机参数中添加这个参数,值为存有 patchelf 过的动态库的目录。

那么从哪里可以获得呢,最简单粗暴的方法就是 systemPackages 安装 openjfx17

1
2
3
> rg jfx
packages.nix
14: javaPackages.openjfx17

随后在 /nix/store 中找到并复制出来到自定义目录。

1
2
3
4
5
/nix/store/pz7cx3y7dwl92g42prf3xizyyj5wk9p8-openjfx-modular-sdk-17.0.6+3/modules_libs/javafx.graphics
> ls
javafx-swt.jar libglass.so libglassgtk3.so libjavafx_font_freetype.so libjavafx_iio.so libprism_es2.so
libdecora_sse.so libglassgtk2.so libjavafx_font.so libjavafx_font_pango.so libprism_common.so libprism_sw.so
> cp * ~/natives/openjfx17

IDEA新建应用程序配置,设置jdk、classpath、mainclass之后,在VM参数中添上 -Djava.library.path="/home/frnks/natives/openjfx17" 路径指向刚才自定义的目录

注意新版本IDEA默认将VM参数隐藏了,需要手动打开:

vm config

运行这个配置应该就可以正常启动窗口了~

笔者尝试过以下方法:

  • 空项目导入系统JavaFX库
  • 项目结构中修改Gradle引入的JavaFX的原生库路径
  • Gradle导入本地Repo

均因为各种技术原因/复用性不够未能成功

未能解决的问题

虽然窗口能够正常启动,但是Gradle运行仍然会抛出相同的错误,推测可能是 NativeLibLoader 虽然在 java.library.path 中找到了动态库,但是仍然会执行缓存动态库的操作,想要去除报错可能需要Override这个类的方法。但是麻烦不说,也可能会导致只能在NixOS环境下构建运行这个项目,得不偿失。