f native image, just build a jar

This commit is contained in:
Gregor Lohaus
2026-05-29 12:33:32 +02:00
parent 08ad025f76
commit b98a18b49f
12 changed files with 128 additions and 447 deletions

View File

@@ -1,7 +1,7 @@
arguments=--init-script /home/anon/.eclipse/1927926929_linux_gtk_x86_64/configuration/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
arguments=--init-script /home/anon/Src/eclipse.jdt.ls/org.eclipse.jdt.ls.product/target/repository/configuration/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9))
connection.gradle.distribution=GRADLE_DISTRIBUTION(LOCAL_INSTALLATION(/home/anon/.sdkman/candidates/gradle/current))
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=

View File

@@ -1,24 +1,45 @@
# jprototerm
JavaFX canvas terminal prototype using `jlibghostty` for terminal emulation, Nix for the build environment, and GluonFX/GraalVM Native Image for the Linux binary.
JavaFX canvas terminal prototype using `jlibghostty` for terminal emulation and Nix for
the build environment. It builds a plain JavaFX application (JDK 25, JavaFX 25 via Gradle)
packaged as a Nix derivation — no GraalVM/GluonFX native image.
## Build
```sh
nix build
./result/bin/jprototerm
```
The package build uses GluonFX through Gradle so JavaFX native-image metadata is generated by the toolchain that is designed for it. In a strict pure Nix sandbox, Gradle dependencies must be vendored first with `gradle2nix` or a checked-in Maven/Gradle cache.
Install it into a profile (works on NixOS and on a plain Debian box with Nix installed):
```sh
nix profile add .
jprototerm
```
The flake bundles everything the app needs — the JDK 25 runtime, the Maven JavaFX modules
and their native libraries, and the gtk/glib/freetype/X11 libraries they load — **except**
the system OpenGL/graphics drivers. `libGL` is supplied by the host at runtime through a GL
shim in the launcher wrapper, so the same closure runs against NixOS, Mesa, or vendor
(e.g. NVIDIA) GPU drivers.
Gradle dependencies are vendored in `deps.json` for the pure Nix sandbox. Regenerate it
after changing dependencies in `build.gradle` (the update script writes `deps.json` in the
current directory):
```sh
$(nix build .#gradleDepsUpdateScript --no-link --print-out-paths)
```
For development:
```sh
nix develop
gradle -PjlibghosttyMavenRepo="$JLIBGHOSTTY_MAVEN_REPO" run
gradle -PjlibghosttyMavenRepo="$JLIBGHOSTTY_MAVEN_REPO" -Pgluonfx.mainClassName=com.gregor.jprototerm.Main nativeExecutable
gradle run
```
The Gradle project is the source of truth for native JavaFX builds.
The Gradle project is the source of truth for the JavaFX build.
## Config

View File

@@ -1,8 +1,6 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
// id 'org.graalvm.buildtools.native' version '1.1.1'
id 'com.gluonhq.gluonfx-gradle-plugin' version '1.0.28'
}
repositories {
@@ -19,20 +17,10 @@ dependencies {
}
javafx {
version = '22'
version = '25'
modules = [ 'javafx.controls', 'javafx.fxml' ]
}
gluonfx {
compilerArgs += [
'--features=com.gregor.jprototerm.PtyForeignRegistrationFeature',
'-H:+UnlockExperimentalVMOptions',
'-H:+ForeignAPISupport',
'--enable-native-access=ALL-UNNAMED',
"-H:ConfigurationFileDirectories=${file('src/main/resources/META-INF/native-image/com.gregor/jprototerm').absolutePath}".toString()
]
}
application {
mainClass = 'com.gregor.jprototerm.Main'
}

177
deps.json
View File

@@ -2,19 +2,6 @@
"!comment": "This is a nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the nixpkgs manual.",
"!version": 1,
"https://plugins.gradle.org/m2": {
"com/gluonhq#gluonfx-gradle-plugin/1.0.28": {
"jar": "sha256-uPpVnf9fWsdtMHkJZ1Mq7b/wa7kPhgobruE6k4T5ocE=",
"module": "sha256-si+dMCY5W/aA0W4x0Ef6xQfELR3vp3J53PFhn4EEMzk=",
"pom": "sha256-IAx/ryC66kqOVYmZtpK+jowA2AlfOaQjb5jgezrtDq0="
},
"com/gluonhq#substrate/0.0.68": {
"jar": "sha256-ZOchzSN8IPSQVnItGAvo3T6OG93G0lqU+BmemdFa6lE=",
"module": "sha256-Fe+E0f9bIizfOxyAwx85EhS7d+PHTFP3dVxIjjXiqVI=",
"pom": "sha256-pZ/ETzHMpevrtsXKycZDvlUKel5OLYwIlPzx3VhUwPQ="
},
"com/gluonhq/gluonfx-gradle-plugin#com.gluonhq.gluonfx-gradle-plugin.gradle.plugin/1.0.28": {
"pom": "sha256-kJ4KJdl1tThityoqOgZgWCk1DwjwOatuwKu+pqM2fVY="
},
"com/google/code/findbugs#jsr305/3.0.2": {
"jar": "sha256-dmrSoHg/JoeWLIrXTO7MOKKLn3Ki0IXuQ4t4E+ko0Mc=",
"pom": "sha256-GYidvfGyVLJgGl7mRbgUepdGRIgil2hMeYr+XWPXjf4="
@@ -23,95 +10,10 @@
"jar": "sha256-a0aS+ROiGx+2Axae54uo8+SrKvnXYq+cqIt5EmwcCtE=",
"pom": "sha256-hGDJUBJ8o1mHZhYeOLT/jWO01p+4MQoW4As1E1ABDBE="
},
"com/google/javascript#closure-compiler-main/v20210601": {
"pom": "sha256-5ndSQ8/yIPoBYQRJYMgINYm0RNBT8+UBh5RgadeelPc="
},
"com/google/javascript#closure-compiler-parent/v20210601": {
"pom": "sha256-fUctzCy/bEb70rPLjMbXvzoQQSJFcvz5wYM/0FwGMSw="
},
"com/google/javascript#closure-compiler/v20210601": {
"jar": "sha256-ZPFhxlo9ukLJpPWnnbM1rA51f8ukhs15KCBaxMJY7wg=",
"pom": "sha256-oG+6EUi6PBunTk69Xm47vQFxnUyJ1x/2spmAc1P5pnE="
},
"com/googlecode/plist#dd-plist/1.23": {
"jar": "sha256-PXlPKAjQ6Qx+yIlljoQ/kqJEFL1N8Y7PnxR4cmrxI4w=",
"pom": "sha256-irwhpc7IBIMPGpXg6ePumjP+vuugYUoKC39r1Q2sNoM="
},
"kr/motd/maven#os-maven-plugin/1.7.1": {
"jar": "sha256-9Hru+Ggh5SsrGHWJeL0EXwPXIikuMudHCCEixiKJUuA=",
"pom": "sha256-S3WABEIrljPdMY8p54Tx0YC9ilkgzVCvGTCGH21qVHY="
},
"net/java#jvnet-parent/3": {
"pom": "sha256-MPV4nvo53b+WCVqto/wSYMRWH68vcUaGcXyy3FBJR1o="
},
"org/apidesign#bck2brwsr/0.53": {
"pom": "sha256-4gloFSkouKZ2Knb3dv0+hATBY21FyUrQwIEo6Ldf84g="
},
"org/apidesign/bck2brwsr#aot/0.53": {
"jar": "sha256-3AYD3veZGDwAba3Km3tDtQBpvKvqVcdOwOyVnSGNrDo=",
"pom": "sha256-y8SBpobq+EucjgQZYORkL1LQldq7Qz7aDzYTKx4WVJs="
},
"org/apidesign/bck2brwsr#core/0.53": {
"jar": "sha256-Vx3cekElg279+ZloL2R2hq7o4PXAv1SSPuLq85CKBJA=",
"pom": "sha256-tv4AFJHrOGMb58k2MeoRKS5JJd/TTH1WSa5r31cyYxA="
},
"org/apidesign/bck2brwsr#rt/0.53": {
"pom": "sha256-djjJvzf/apO+0fXQd8BF3Gwa/8kNYJnEe0HFkFQA17g="
},
"org/apidesign/bck2brwsr#vm4brwsr/0.53": {
"jar": "sha256-40KkVFiKss+K1vy9ZAc0Zu72koER7uks/N9KkgUewYc=",
"pom": "sha256-Pu28aImG2vOir62p5SiF7TCRT0mDTsZcj8Oz0vyIkZc="
},
"org/bouncycastle#bcpkix-jdk15on/1.49": {
"jar": "sha256-RbdLj4RoUMIDmezT0GqHGgMApxmvPD1wxiGypu9wGMQ=",
"pom": "sha256-goBjpMwZV6CBydovX/HPKxTL6U8dliP4N+PqC832Nko="
},
"org/bouncycastle#bcprov-jdk15on/1.49": {
"jar": "sha256-lrS41G445aCLXubuwjxZUdlb2sNcCPk16Zm7vmsVup0=",
"pom": "sha256-2NH0XGsa+Sh+U7Hk7Z3OgPE/gM9jXKxbFsOnXxErGI4="
},
"org/graalvm/compiler#compiler/21.1.0": {
"jar": "sha256-J4rM6driWApOTY4mrucCNAYO5zE+SYssIbBp+VdIgAM=",
"pom": "sha256-29CpNDLziJ22QBzV3kn5+Zo+2N03tjqw+Vt0paolKe8="
},
"org/graalvm/nativeimage#objectfile/21.1.0": {
"jar": "sha256-GdR196ACgcJHimtLFSS4IBPtpNzouUZFoV+BKzd51+c=",
"pom": "sha256-k/ScoLxtNJd+KjQO5G5LR9r3mw7jijlgIzGx6U/oSwE="
},
"org/graalvm/nativeimage#pointsto/21.1.0": {
"jar": "sha256-wD2RSGqNOxFoEvpuzHXtEolCY8EjpLcM4gm9chQ0Lv8=",
"pom": "sha256-5Pq2drpWcEkosAg4UjNjv8kU1Z0QadOa0/uWkRYZ874="
},
"org/graalvm/nativeimage#svm-hosted-native-darwin-amd64/21.1.0": {
"pom": "sha256-wu8bdhdbxPrxshx3NXbYALoepTWdDy7D9Iyry5IKt1M="
},
"org/graalvm/nativeimage#svm-hosted-native-linux-amd64/21.1.0": {
"pom": "sha256-n6slhAlQ6gRkkClPvEwLxHkBuIsK+Ygs4ZigNVSs0i4="
},
"org/graalvm/nativeimage#svm-hosted-native-windows-amd64/21.1.0": {
"pom": "sha256-QAHwMgf+vnTMPY7b64oGMzJci3Nv9zvzGqaqdn+ih/E="
},
"org/graalvm/nativeimage#svm/21.1.0": {
"jar": "sha256-7mVFI+odQ02FQ7aO4yq0WpFYEUlpyMMrRftqaKtepIw=",
"pom": "sha256-Tc1jANLepe57NKSQEJg3oZAvt05vVXsqEbgjonmpA6E="
},
"org/graalvm/nativeimage/svm-hosted-native-darwin-amd64/21.1.0/svm-hosted-native-darwin-amd64-21.1.0": {
"tar.gz": "sha256-cRv2xfIXJSO9dk1zm70KdvkqzzyS1PgwuAWMfPwGBw0="
},
"org/graalvm/nativeimage/svm-hosted-native-linux-amd64/21.1.0/svm-hosted-native-linux-amd64-21.1.0": {
"tar.gz": "sha256-AmGma5Lf1uxxMYb1LOsvtHXyPEZfxnsOaORGdt0dhMA="
},
"org/graalvm/nativeimage/svm-hosted-native-windows-amd64/21.1.0/svm-hosted-native-windows-amd64-21.1.0": {
"tar.gz": "sha256-mk6t0oPU1iowP+pkYoNlXBmoob7DMJP2WHphCDWlS8Y="
},
"org/graalvm/sdk#graal-sdk/21.1.0": {
"jar": "sha256-I3XHwVkNToh7um4BwZkRh9E4BM+HTgNeR7ttW55/FBE=",
"pom": "sha256-Bfwh8sGsVPPlpN2kDvK5yMo4RwdIvxcyc75w9sGZudY="
},
"org/graalvm/truffle#truffle-api/21.1.0": {
"jar": "sha256-4IrvulJD4FTe4bwfX5EW6ZcZkXp03fxzntDEj3SSLro=",
"pom": "sha256-fC/b6jkSX8c923Iz8j5WRhjDQ+raeKllJNfELM7JhEU="
},
"org/openjfx#javafx-plugin/0.1.0": {
"jar": "sha256-Xq7sB5m0QGRrDKTP2iGaMttr4rpXktAyoNpKOlw4j6s=",
"module": "sha256-rf+3RA0kntF8BJOD1nBp+UU7F3gncMAFtoKkNBbYNmE=",
@@ -120,13 +22,6 @@
"org/openjfx/javafxplugin#org.openjfx.javafxplugin.gradle.plugin/0.1.0": {
"pom": "sha256-1tASf/Q2PQAXPDV6mByec+/wPDCl0Ohq2CtgVPrvqEE="
},
"org/ow2#ow2/1.5": {
"pom": "sha256-D4obEW52C4/mOJxRuE5LB6cPwRCC1Pk25FO1g91QtDs="
},
"org/ow2/asm#asm/9.2": {
"jar": "sha256-udT+TXGTjfOIOfDspCqqpkz4sxPWeNoDbwyzyhmbR/U=",
"pom": "sha256-37EqGyJL8Bvh/WBAIEZviUJBvLZF3M45Xt2M1vilDfQ="
},
"org/sonatype/oss#oss-parent/7": {
"pom": "sha256-tR+IZ8kranIkmVV/w6H96ne9+e9XRyL+kM5DailVlFQ="
},
@@ -150,68 +45,32 @@
"module": "sha256-TRoE8nqf0ULuQ4J1/u2+voUNf421lTOJ1SajE07F8/Y=",
"pom": "sha256-sG4IDPD+ItRgyQcfDiLqdd+wCd40JHcSLocA+jWX1p0="
},
"net/java/dev/jna#jna-platform/5.14.0": {
"jar": "sha256-rkys6zhAcwwlN/m3+1WgG6ulgChrQSKVFIi87lWMJEk=",
"pom": "sha256-bLoOBPnuyxZIYAB5O7J+EDsPTQSF4FVOK0wK7RPS7RY="
"org/openjfx#javafx-base/25": {
"pom": "sha256-XFYpcqK673qkB7J9Wc4XOl6lCht7dRgEO3/I92/v5Tc="
},
"net/java/dev/jna#jna/5.14.0": {
"jar": "sha256-NO0eHyf6iWvKUNvE6ZzzcylnzsOHp6DV40hsCWc/6MY=",
"pom": "sha256-4E4llRUB3yWtx7Hc22xTNzyUiXuE0+FJISknY+4Hrj0="
"org/openjfx#javafx-base/25/linux": {
"jar": "sha256-MkJZRruLjbBxfPovsuAOIc1InzW5ZitvrKGLYVpKlmk="
},
"org/jetbrains#annotations/13.0": {
"jar": "sha256-rOKhDcji1f00kl7KwD5JiLLA+FFlDJS4zvSbob0RFHg=",
"pom": "sha256-llrrK+3/NpgZvd4b96CzuJuCR91pyIuGN112Fju4w5c="
"org/openjfx#javafx-controls/25": {
"pom": "sha256-74cad6gX7nuDrKWKKe6yv5h2AvRseKHRXEYAgzpq1uM="
},
"org/jetbrains#annotations/24.0.1": {
"jar": "sha256-YWZtvOfkLmyFtDwE/PuCk6Idy1WzyA6GknDOQsAaazU=",
"pom": "sha256-mb7eKcAzHBlS7uBL+ZeN5TWpDJfi3v/6XgCTNRcZJbA="
"org/openjfx#javafx-controls/25/linux": {
"jar": "sha256-NzVeTZHGfoj9mBX2AeasW1Xd3p9em5P8j0qgRXfmkdM="
},
"org/jetbrains/kotlin#kotlin-stdlib/2.1.21": {
"jar": "sha256-JjvcZ54fYgEtt7CReWJ5ttcc829Hl6mP8azgWDXyAcg=",
"module": "sha256-DTc1BD1ot6WvtE0n3mX4Cp0rIIRicJwu27SP1bOVrYo=",
"pom": "sha256-xVJAhso+SaZ5ht+P3M1mA3uvXzxaNYt2+d1gm+57tjg="
"org/openjfx#javafx-fxml/25": {
"pom": "sha256-RopsFNQeVHnwNK4v4FPwyJEpfqJoo8dtf/047zyrsio="
},
"org/jetbrains/pty4j#pty4j/0.13.11": {
"jar": "sha256-AynuVSDzsBd6Cyve+sPExpzJ05FeTyEKi/Ob56s4h8g=",
"module": "sha256-eTWa/9ZV8g2eFDU5aDuke3OhqAOLLtP3uX3p3/RELhI=",
"pom": "sha256-NQ/chKePdmts52rZbY22ZL/3rjreQGLJKTblTCu4+S4="
"org/openjfx#javafx-fxml/25/linux": {
"jar": "sha256-OUrjL2TBIsFPvRDvSb3efbsFVpt6uOf58XDIGSS5Wis="
},
"org/openjfx#javafx-base/22": {
"pom": "sha256-XPr4AKyei29O5+mOaRZvN0PAo+xyLhx8idLaEzmgQIE="
"org/openjfx#javafx-graphics/25": {
"pom": "sha256-zB2jY7Id7uvymRWBk9qmIB+USw+Setv13DhL62qDOfQ="
},
"org/openjfx#javafx-base/22/linux": {
"jar": "sha256-guS1JYZGAz4MvYzoYKBZWGDIwNeOrfXKWQ9FNps7PQ8="
"org/openjfx#javafx-graphics/25/linux": {
"jar": "sha256-PlGLwX7lWFWaKsWKX3/UUmFRCNnVCI9lsTvuk5nDcis="
},
"org/openjfx#javafx-controls/22": {
"pom": "sha256-8AZSRCn/IPdz+tfjwXq9/qkkzMeqGamSoeQZPl/kCA8="
},
"org/openjfx#javafx-controls/22/linux": {
"jar": "sha256-4E5POGe18PR56yKJsDgwPOn7TR3vImvJHCkYlpvzW2s="
},
"org/openjfx#javafx-fxml/22": {
"pom": "sha256-N2R+Lpsq+3hcJKu2GjjD5p02av/TUyv6J+jIrm92tPI="
},
"org/openjfx#javafx-fxml/22/linux": {
"jar": "sha256-K5X/HQ+4GUIrIU2Xs423Bc2alyMK41P0kr3UiJujqdY="
},
"org/openjfx#javafx-graphics/22": {
"pom": "sha256-snXDI9Pbr1/anOegMkG/JEvfeJ5GFON+pMPjww4P858="
},
"org/openjfx#javafx-graphics/22/linux": {
"jar": "sha256-ACrpnQ8ML2Clh72fkRaP7lvZ2AJ4H0eXV1lWz0icJLs="
},
"org/openjfx#javafx/22": {
"pom": "sha256-ZO+kxQXukz+S0/4hqQ7RVKKs1Dhif6hKHYG7jRZWN58="
},
"org/slf4j#slf4j-api/2.0.13": {
"jar": "sha256-58KkjoUVuh9J+mN9V7Ti9ZCz9b2XQHrGmcOqXvsSBKk=",
"pom": "sha256-UYBc/agMoqyCBBuQbZhl056YI+NYoO62I3nf7UdcFXE="
},
"org/slf4j#slf4j-bom/2.0.13": {
"pom": "sha256-evJy16c44rmHY3kf/diWBA6L6ymKiP1gYhRAeXbNMQo="
},
"org/slf4j#slf4j-parent/2.0.13": {
"pom": "sha256-Z/rP1R8Gk1zqhWFaBHddcNgL/QOtDzdnA1H5IO0LtYo="
"org/openjfx#javafx/25": {
"pom": "sha256-55IzCPyt1/LGiwcgQfR9jnNVIj2EZVnutceA3EuivxM="
}
}
}

8
flake.lock generated
View File

@@ -70,11 +70,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1780043504,
"narHash": "sha256-AyyfTWcVLfq6e707oi95y/yWp3PnaBkeb6jYE1BspjI=",
"lastModified": 1780050576,
"narHash": "sha256-u06xuO3QnLDpajIOZwDdhwI0HGzMuXG7x1pR+4Zb+RA=",
"ref": "refs/heads/main",
"rev": "482305a1af38e84f075e75984923c4c885cabcc5",
"revCount": 16,
"rev": "d558d554b360a76d03c2fc09d327e3ec4aade878",
"revCount": 17,
"type": "git",
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
},

244
flake.nix
View File

@@ -11,112 +11,21 @@
let
supportedSystems = [ "x86_64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in {
packages = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
jlib = jlibghostty.packages.${system}.jlibghostty;
ghosttyVt = ghostty.packages.${system}.libghostty-vt;
javafxStaticSdkZip = pkgs.fetchurl {
url = "https://download2.gluonhq.com/substrate/javafxstaticsdk/openjfx-21-ea+11.3-linux-x86_64-static.zip";
hash = "sha256-ovoByMPwhvU54mtxGYyrgLxDLNn0tA3XUK3rtnGfAAM=";
};
patchedSubstrateJar =
let
substrateJar = pkgs.fetchurl {
url = "https://plugins.gradle.org/m2/com/gluonhq/substrate/0.0.68/substrate-0.0.68.jar";
hash = "sha256-ZOchzSN8IPSQVnItGAvo3T6OG93G0lqU+BmemdFa6lE=";
};
in
pkgs.runCommand "substrate-0.0.68-patched.jar"
{
nativeBuildInputs = [
pkgs.perl
pkgs.unzip
pkgs.zip
];
}
''
mkdir work
cd work
unzip -q ${substrateJar}
# Gluon Substrate hardcodes /usr/bin/pkg-config. Keep the
# replacement string the same byte length to avoid rewriting
# the Java class constant pool structure.
perl -0pi -e 's|/usr/bin/pkg-config|/tmp/nix/pkg-config|g' \
com/gluonhq/substrate/util/linux/LinuxLinkerFlags.class
if grep -a -q "/usr/bin/pkg-config" com/gluonhq/substrate/util/linux/LinuxLinkerFlags.class; then
echo "Failed to patch hardcoded pkg-config path in Substrate" >&2
exit 1
fi
zip -qr "$out" .
'';
gluonGraalvm = pkgs.stdenv.mkDerivation {
pname = "graalvm-java23-gluon";
version = "23+25.1-dev-2409082136";
src = pkgs.fetchurl {
url = "https://github.com/gluonhq/graal/releases/download/gluon-23%2B25.1-dev-2409082136/graalvm-java23-linux-amd64-gluon-23+25.1-dev.tar.gz";
hash = "sha256-/NyMutn3pT4ZKL2pkzPdBZghxg0ERK5VJ2bFQF0VBfU=";
};
nativeBuildInputs = [
pkgs.autoPatchelfHook
];
buildInputs = [
pkgs.stdenv.cc.cc.lib
pkgs.zlib
pkgs.freetype
pkgs.fontconfig
pkgs.alsa-lib
# Everything the JavaFX natives (and jlibghostty) dlopen at runtime, EXCEPT the
# system OpenGL/graphics drivers. libGL is intentionally left out: it is supplied
# by the host at runtime via the GL shim in the wrapper below, so the same closure
# works on NixOS and on a plain Debian box with vendor GPU drivers installed.
runtimeLibsFor = pkgs: ghosttyVt: [
pkgs.glib
pkgs.gtk3
pkgs.pango
pkgs.libx11
pkgs.libxext
pkgs.libxrender
pkgs.libxtst
pkgs.libxi
pkgs.libxrandr
pkgs.libxinerama
pkgs.libxcb
];
installPhase = ''
runHook preInstall
mkdir -p "$out"
cp -R . "$out/"
# The GluonFX Gradle plugin expects these two static libraries
# directly under linux-amd64, but this tarball keeps them one
# level deeper under linux-amd64/glibc.
ln -sfn ./glibc/libjvm.a "$out/lib/svm/clibraries/linux-amd64/libjvm.a"
ln -sfn ./glibc/liblibchelper.a "$out/lib/svm/clibraries/linux-amd64/liblibchelper.a"
runHook postInstall
'';
};
runtimeLibs = [
pkgs.glib
pkgs.gtk3
pkgs.pango
pkgs.alsa-lib
pkgs.ffmpeg.dev
pkgs.ffmpeg.lib
pkgs.cairo
pkgs.gdk-pixbuf
pkgs.harfbuzz
pkgs.freetype
pkgs.fontconfig
pkgs.fontconfig.lib
pkgs.libx11
pkgs.libx11.dev
pkgs.libxext
pkgs.libxrender
pkgs.libxtst
@@ -127,110 +36,83 @@
pkgs.libxcb
pkgs.libxxf86vm
pkgs.zlib
pkgs.zlib.dev
ghosttyVt
];
in {
packages = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
jlib = jlibghostty.packages.${system}.jlibghostty;
ghosttyVt = ghostty.packages.${system}.libghostty-vt;
runtimeLibs = runtimeLibsFor pkgs ghosttyVt;
jprototerm = pkgs.stdenv.mkDerivation (finalAttrs: {
pname = "jprototerm";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [
gluonGraalvm
pkgs.jdk25
pkgs.gradle_9
pkgs.makeWrapper
pkgs.patchelf
pkgs.pkg-config
];
buildInputs = runtimeLibs;
baseMitmCache = pkgs.gradle_9.fetchDeps {
mitmCache = pkgs.gradle_9.fetchDeps {
pkg = finalAttrs.finalPackage;
data = ./deps.json;
silent = false;
useBwrap = false;
};
mitmCache = pkgs.runCommand "jprototerm-deps-patched"
{
passthru.updateScript = finalAttrs.baseMitmCache.updateScript;
}
''
mkdir -p "$out"
cp -a ${finalAttrs.baseMitmCache}/. "$out/"
chmod -R u+w "$out"
substratePath="$out/https/plugins.gradle.org/m2/com/gluonhq/substrate/0.0.68/substrate-0.0.68.jar"
if [ ! -e "$substratePath" ]; then
echo "Could not find substrate jar in Gradle MITM cache: $substratePath" >&2
find "$out" -path '*substrate*' -print >&2
exit 1
fi
rm "$substratePath"
ln -s ${patchedSubstrateJar} "$substratePath"
'';
gradleBuildTask = "nativeBuild";
gradleUpdateTask = "nixDownloadDeps";
# Builds build/install/jprototerm/{bin,lib} with every runtime jar, including
# the maven javafx-*-linux jars that carry the platform natives.
gradleBuildTask = "installDist";
gradleFlags = [
"--no-build-cache"
"--stacktrace"
"--info"
"-Dorg.gradle.java.home=${gluonGraalvm}"
"-Dorg.gradle.java.home=${pkgs.jdk25}"
];
GRAALVM_HOME = "${gluonGraalvm}";
JAVA_HOME = "${gluonGraalvm}";
JAVA_HOME = "${pkgs.jdk25}";
JLIBGHOSTTY_MAVEN_REPO = "${jlib}/maven";
preConfigure = ''
export HOME="$TMPDIR/home"
export GRADLE_OPTS="-Duser.home=$HOME ''${GRADLE_OPTS:-}"
mkdir -p /tmp/nix
ln -sfn ${pkgs.pkg-config}/bin/pkg-config /tmp/nix/pkg-config
for gluonHome in "$HOME/.gluon" /build/.gluon; do
if mkdir -p "$gluonHome/substrate" 2>/dev/null; then
cp -f ${javafxStaticSdkZip} "$gluonHome/substrate/openjfx-21-ea+11.3-linux-x86_64-static.zip"
chmod u+w "$gluonHome/substrate/openjfx-21-ea+11.3-linux-x86_64-static.zip"
fi
done
'';
preBuild = ''
export HOME="$TMPDIR/home"
export GRADLE_OPTS="-Duser.home=$HOME ''${GRADLE_OPTS:-}"
mkdir -p /tmp/nix
ln -sfn ${pkgs.pkg-config}/bin/pkg-config /tmp/nix/pkg-config
for gluonHome in "$HOME/.gluon" /build/.gluon; do
if mkdir -p "$gluonHome/substrate" 2>/dev/null; then
cp -f ${javafxStaticSdkZip} "$gluonHome/substrate/openjfx-21-ea+11.3-linux-x86_64-static.zip"
chmod u+w "$gluonHome/substrate/openjfx-21-ea+11.3-linux-x86_64-static.zip"
fi
done
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibs}:$LD_LIBRARY_PATH"
'';
preGradleUpdate = ''
export HOME="$TMPDIR/home"
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibs}:$LD_LIBRARY_PATH"
'';
installPhase = ''
runHook preInstall
mkdir -p "$out/bin"
binary="$(find build/gluonfx -path '*/gvm/*' -prune -o -type f -perm -0100 -print | head -n1)"
if [ -z "$binary" ]; then
echo "Could not find native executable under build/gluonfx" >&2
find build/gluonfx -type f -perm -0100 >&2 || true
exit 1
fi
mkdir -p "$out/share/jprototerm"
cp -a build/install/jprototerm/lib "$out/share/jprototerm/lib"
cp "$binary" "$out/bin/jprototerm"
# JavaFX is a set of proper modular jars: put them on the module path and
# keep the application + plain dependency jars on the classpath, so the two
# worlds do not collide.
mkdir -p "$out/share/jprototerm/javafx"
mv "$out/share/jprototerm/lib"/javafx-*.jar "$out/share/jprototerm/javafx/"
wrapProgram "$out/bin/jprototerm" \
# Build an explicit colon-separated classpath. A "lib/*" glob would be
# expanded by the wrapper's shell before java sees it, breaking -cp.
classpath=""
for jar in "$out"/share/jprototerm/lib/*.jar; do
classpath="$classpath''${classpath:+:}$jar"
done
makeWrapper "${pkgs.jdk25}/bin/java" "$out/bin/jprototerm" \
--add-flags "--enable-native-access=ALL-UNNAMED,javafx.graphics" \
--add-flags "--module-path $out/share/jprototerm/javafx" \
--add-flags "--add-modules javafx.controls,javafx.fxml" \
--add-flags "-cp $classpath" \
--add-flags "com.gregor.jprototerm.Main" \
--prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath runtimeLibs}" \
--run 'glShimDir="''${XDG_RUNTIME_DIR:-/tmp}/jprototerm-gl"; mkdir -p "$glShimDir"; for lib in /lib/x86_64-linux-gnu/libGL.so.1 /lib/x86_64-linux-gnu/libGLX.so.0 /lib/x86_64-linux-gnu/libGLdispatch.so.0 /usr/lib/x86_64-linux-gnu/libGLX_nvidia.so* /usr/lib/x86_64-linux-gnu/libEGL_nvidia.so* /usr/lib/x86_64-linux-gnu/libnvidia*.so* /usr/lib/x86_64-linux-gnu/nvidia/current/lib*.so*; do [ -e "$lib" ] && ln -sfn "$lib" "$glShimDir/$(basename "$lib")"; done; export LD_LIBRARY_PATH="$glShimDir''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"; export __GLX_VENDOR_LIBRARY_NAME="''${__GLX_VENDOR_LIBRARY_NAME:-nvidia}"; if [ -e /usr/share/glvnd/egl_vendor.d/10_nvidia.json ]; then export __EGL_VENDOR_LIBRARY_FILENAMES="''${__EGL_VENDOR_LIBRARY_FILENAMES:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}"; fi' \
--set JLIBGHOSTTY_LIBRARY "${ghosttyVt}/lib/libghostty-vt.so" \
@@ -238,13 +120,6 @@
runHook postInstall
'';
postFixup = ''
binary="$out/bin/.jprototerm-wrapped"
currentRpath="$(patchelf --print-rpath "$binary" || true)"
filteredRpath="$(printf '%s' "$currentRpath" | tr ':' '\n' | grep -v 'libglvnd' | paste -sd: -)"
patchelf --set-rpath "$filteredRpath" "$binary"
'';
});
in {
default = jprototerm;
@@ -256,35 +131,12 @@
pkgs = import nixpkgs { inherit system; };
jlib = jlibghostty.packages.${system}.jlibghostty;
ghosttyVt = ghostty.packages.${system}.libghostty-vt;
runtimeLibs = [
pkgs.glib
pkgs.gtk3
pkgs.pango
pkgs.alsa-lib
pkgs.ffmpeg.dev
pkgs.ffmpeg.lib
pkgs.freetype
pkgs.fontconfig
pkgs.libx11
pkgs.libx11.dev
pkgs.libxext
pkgs.libxrender
pkgs.libxtst
pkgs.libxi
pkgs.libxcursor
pkgs.libxrandr
pkgs.libxinerama
pkgs.libxcb
pkgs.libxxf86vm
pkgs.zlib
pkgs.zlib.dev
ghosttyVt
];
runtimeLibs = runtimeLibsFor pkgs ghosttyVt;
in {
default = pkgs.mkShell {
packages = [
pkgs.gradle_9
pkgs.jdk23
pkgs.jdk25
pkgs.jdt-language-server
] ++ runtimeLibs;

View File

@@ -16,19 +16,16 @@ import java.util.Map;
/**
* A Linux PTY backed by libc via the Foreign Function & Memory API.
*
* <p>This replaces pty4j (which loads a JNA JNI shim that does not initialise under a
* GraalVM native image). It uses {@code posix_openpt}/{@code posix_spawnp} rather than
* {@code fork}/{@code forkpty}: doing work between {@code fork} and {@code exec} inside a
* multithreaded JVM is unsafe (only async-signal-safe calls are permitted), whereas
* {@code posix_spawn} performs the dangerous part in libc with no Java on the stack.
* <p>This replaces pty4j (which loads a JNA JNI shim). It uses
* {@code posix_openpt}/{@code posix_spawnp} rather than {@code fork}/{@code forkpty}:
* doing work between {@code fork} and {@code exec} inside a multithreaded JVM is unsafe
* (only async-signal-safe calls are permitted), whereas {@code posix_spawn} performs the
* dangerous part in libc with no Java on the stack.
*
* <p>The child gets a fresh session via {@code POSIX_SPAWN_SETSID}; it then opens the slave
* PTY itself (as fd 0, without {@code O_NOCTTY}) so the slave becomes its controlling
* terminal. glibc applies attribute flags (the setsid) before file actions, so the open
* happens in the new session.
*
* <p>FFM downcall descriptors are registered for native image by
* {@link PtyForeignRegistrationFeature}; keep the two in sync.
*/
public final class LinuxPty implements AutoCloseable {
static final Linker LINKER = Linker.nativeLinker();
@@ -40,7 +37,7 @@ public final class LinuxPty implements AutoCloseable {
static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("long");
static final ValueLayout.OfLong C_SIZE_T = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("size_t");
// Function descriptors. Mirrored in PtyForeignRegistrationFeature.
// Function descriptors.
static final FunctionDescriptor FD_INT_INT = FunctionDescriptor.of(C_INT, C_INT);
static final FunctionDescriptor FD_PTSNAME_R = FunctionDescriptor.of(C_INT, C_INT, C_POINTER, C_SIZE_T);
static final FunctionDescriptor FD_RW = FunctionDescriptor.of(C_LONG, C_INT, C_POINTER, C_SIZE_T);

View File

@@ -1,39 +0,0 @@
package com.gregor.jprototerm;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeForeignAccess;
import java.lang.foreign.Linker;
/**
* Registers the FFM downcall descriptors used by {@link LinuxPty} for GraalVM releases that
* do not consume {@code foreign.downcalls} from reachability-metadata.json. Mirrors
* jlibghostty's {@code GhosttyForeignRegistrationFeature}.
*
* <p>Wired in via {@code --features=com.gregor.jprototerm.PtyForeignRegistrationFeature}
* in the gluonfx compiler args.
*/
public final class PtyForeignRegistrationFeature implements Feature {
@Override
public void duringSetup(DuringSetupAccess access) {
downcall(LinuxPty.FD_INT_INT); // posix_openpt / grantpt / unlockpt / close
downcall(LinuxPty.FD_PTSNAME_R); // ptsname_r
downcall(LinuxPty.FD_RW); // read / write
downcall(LinuxPty.FD_KILL); // kill
downcall(LinuxPty.FD_WAITPID); // waitpid
downcall(LinuxPty.FD_SPAWN); // posix_spawnp
downcall(LinuxPty.FD_FA_INIT); // *_init / *_destroy
downcall(LinuxPty.FD_FA_ADDCLOSE); // posix_spawn_file_actions_addclose
downcall(LinuxPty.FD_FA_ADDDUP2); // posix_spawn_file_actions_adddup2
downcall(LinuxPty.FD_FA_ADDOPEN); // posix_spawn_file_actions_addopen
downcall(LinuxPty.FD_FA_ADDCHDIR); // posix_spawn_file_actions_addchdir_np
downcall(LinuxPty.FD_ATTR_SETFLAGS); // posix_spawnattr_setflags
// ioctl(int, unsigned long, ...) is variadic; register with the same linker option.
RuntimeForeignAccess.registerForDowncall(LinuxPty.FD_IOCTL, Linker.Option.firstVariadicArg(2));
}
private static void downcall(java.lang.foreign.FunctionDescriptor descriptor) {
RuntimeForeignAccess.registerForDowncall(descriptor);
}
}

View File

@@ -121,13 +121,24 @@ public final class TerminalCanvasView {
String cacheKey = paneCacheKey(pane, metrics);
int imageWidth = Math.max(1, (int) Math.ceil(pane.width()));
int imageHeight = Math.max(1, (int) Math.ceil(pane.height()));
if (cache.image == null || cache.canvas == null || cache.imageWidth != imageWidth || cache.imageHeight != imageHeight || !cacheKey.equals(cache.key)) {
// Allocate the offscreen buffers only when the pane size changes. Reallocating a
// full-pane Canvas + WritableImage on every content change churns ~20 MB per frame,
// which the native image's serial GC turns into Full-GC frame drops.
if (cache.canvas == null || cache.image == null || cache.imageWidth != imageWidth || cache.imageHeight != imageHeight) {
cache.canvas = new Canvas(imageWidth, imageHeight);
drawPaneContent(cache.canvas.getGraphicsContext2D(), pane, font, metrics, 0.0, 0.0, imageWidth, imageHeight, true);
cache.image = new WritableImage(imageWidth, imageHeight);
cache.canvas.snapshot(null, cache.image);
cache.imageWidth = imageWidth;
cache.imageHeight = imageHeight;
cache.key = null;
}
// Redraw and re-snapshot into the existing buffers only when content changed.
if (!cacheKey.equals(cache.key)) {
GraphicsContext cacheGc = cache.canvas.getGraphicsContext2D();
cacheGc.clearRect(0, 0, imageWidth, imageHeight);
drawPaneContent(cacheGc, pane, font, metrics, 0.0, 0.0, imageWidth, imageHeight, true);
cache.canvas.snapshot(null, cache.image);
cache.key = cacheKey;
}

View File

View File

@@ -1,5 +0,0 @@
{
"resources": {
"includes": []
}
}