...czyli weekendowe śledztwo dlaczego Netbeans zainstalowany ze snap'a używa Javy innej niż domyślna.

Tło historii

Na jednej z maszyn mam zainstalowane kilka różnych wersji Javy:

$ update-java-alternatives -l
ibm-java80-jdk-x86_64          80         /usr/lib/jvm/ibm-java80-jdk-x86_64
ibm-java80-jre-x86_64          80         /usr/lib/jvm/ibm-java80-jre-x86_64
java-1.11.0-openjdk-amd64      1111       /usr/lib/jvm/java-1.11.0-openjdk-amd64
java-1.8.0-openjdk-amd64       1081       /usr/lib/jvm/java-1.8.0-openjdk-amd64
java-1.9.0-openjdk-amd64       1091       /usr/lib/jvm/java-1.9.0-openjdk-amd64

W tym domyślną jdk8:

$ java -version
openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-2ubuntu0.16.04.1-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)

Mam też aktualną wersję Netbeans zainstalowaną ze snap'a. Niestety podczas uruchamiania Netbeans używa Javy IBM (której?) i wywala się z takim pięknym komunikatem:

JVMJ9VM007E Command-line option unrecognised: --add-opens=java.base/java.net=ALL-UNNAMED
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

Oczywiście mogę podać jdk jako parametr z lini komend i Netbeans uruchomia się normalnie, np dla każdej z poniższych (w tym IBMowej) Netbeans działa poprawnie:

netbeans --jdkhome /usr/lib/jvm/java-1.11.0-openjdk-amd64
netbeans --jdkhome /usr/lib/jvm/java-1.8.0-openjdk-amd64
netbeans --jdkhome /usr/lib/jvm/ibm-java80-jdk-x86_64

Hipoteza 1 + Własny snap

Hipoteza pierwsza: snap nie respektuje ustawień update-java-alternatives.

Żeby to sprawdzić postanowiłem napisać najprostszy program, który wyświetli dane javy za pomoca której został uruchomiony oraz spakować go do własnego snap'a.

Kod w javie:

public class Main {

    public static void main(String[] args) {
        System.out.println(System.getProperty("java.home"));
        System.out.println(System.getProperty("java.vendor"));
        System.out.println(System.getProperty("java.vendor.url"));
        System.out.println(System.getProperty("java.version"));
    }
}

Budowanie snap'a

Temat budowania snapów nadaje się na osobny post, teraz będzie tylko skrócona wersja ;)

  1. Do budowania paczek snap służy polecenie snapcraft
  2. Definicja paczki powinna być w pliku <projekt>/snap/snapcraft.yaml

Pierwsza wersja snapcraft.yaml (powstała na podstawie przykładu dot. javy z oficjalnej dokumentacji):

name: javaversion
version: '0.1' 
summary: Checking default java in snaps
description: |
  Checking default java in snaps
  So I can test what's wrong with netbeans

confinement: devmode

apps:
  javaversion:
    command: desktop-launch $SNAP/snapjavasample-0.1/bin/snapjavasample

parts:
  javaversion:
    after: [desktop-glib-only]
    plugin: gradle
    source: .
    override-build: |
      export JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64"
      ./gradleW distZip
      unzip build/distributions/snapjavasample-*.zip -d $SNAPCRAFT_PART_INSTALL/
    build-packages:
      - unzip
      - openjdk-8-jdk

Budujemy:

snapcraft build --debug

Instalujemy:

snap install javaversion_0.1_amd64.snap --devmode --dangerous

...i sprawdzamy:

$ javaversion
/snap/javaversion/x1/usr/lib/jvm/java-8-openjdk-amd64/jre
Oracle Corporation
http://java.oracle.com/
1.8.0_191

wtf? Nie do końca o to mi chodziło. Ewidentnie javę mamy w snapie. Sprawdźmy:

$ unsquashfs -l javaversion_0.1_amd64.snap |grep "java-8-openjdk-amd64/jre/bin/java"
 squashfs-root/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java

$ ls -lh javaversion_0.1_amd64.snap 
  -rw-r--r-- 1 ihsahn ihsahn 88M maj  5 13:50 javaversion_0.1_amd64.snap

Plik snap ma rozmiar 88M i zawiera całe openjdk... czyli inaczej niż Netbeans.

Otóż okazuje się, że można podać snapcraftowi, które pliki powinien brać pod uwagę podczas tworzenia paczki: filesets Dla pewności zmieniłem też confinment na taki jakiego używa Netbeans: classic

Poprawiony snapcraft.yaml:

name: javaversion
version: '0.1' 
summary: Checking default java in snaps
description: |
  Checking default java in snaps
  So I can test what's wrong with netbeans

confinement: classic
grade: devel
architectures: [ amd64 ]

apps:
  javaversion:
    command: snapjavasample-0.1/bin/snapjavasample

parts:
  javaversion:
    plugin: gradle
    source: .
    filesets:
        javaversion: [snapjavasample-0.1/*]
    override-build: |
      export JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64"
      ./gradlew distZip
      unzip build/distributions/snapjavasample-*.zip -d $SNAPCRAFT_PART_INSTALL/
    stage:
      - $javaversion  
    build-packages:
      - unzip
      - openjdk-8-jdk

Przez zbudowaniem trzeba jeszcze wyczyścić cache:

snapcraft clean javaversion -s pull

...i usunąć starą paczkę (bo nie zmieniłem wersji):

snap remove javaversion

Potem ponownie

snapcraft build --debug,

oraz install (tym razem z --classic zamiast -devmode)

snap install javaversion_0.1_amd64.snap ---dangerous --classic

...i sprawdzamy:

/usr/lib/jvm/java-8-openjdk-amd64/jre
Oracle Corporation
http://java.oracle.com/
1.8.0_191

Hipoteza upadła: nowo wyprodukowany snap widzi poprawnie domyślną javę...

Przy okazji: nowy snap (bez javy) "waży" już tylko 8kb ...

Testowy program wraz z definicja snap'a

Hipoteza 2 + skyrpty uruchamiające Netbeans

Hipoteza druga: Netbeans podczas uruchamiania sam znajduje jakąś lewą javę

Ok, najpierw trzeba zobaczyć co się odpala dokładnie w snap'ie: według źródłowego pliku snapcraft jest to netbeans/bin/netbeans. Czyli standardowy skrypt wykonujący Netebans'a, zobaczymy co powie nam bash w trybie "debug". Żeby oczywiście wszystko miało sens, wykonanie komendy powinno się odbywać w takim samym środowisku jak działa snap: dla każdej zainstalowanej paczki możemy "zalogować" się do powłoki, w tym przypadku będzie to:  snap run --shell netbeans

i potem już z górki:

$ cd /snap/netbeans/current/netbeans/bin
bash -x netbeans

...i fragment ostatniej linijki

++ exec /bin/bash /snap/netbeans/current/netbeans/platform/lib/nbexec --userdir /data/11.0 --cachedir /cache/11.0 --jdkhome '' --branding nb --clusters ...

interesujący nas fragment: --jdkhome '' - czyli na tym etapie nie ma jeszcze rozpoznanej wersji javy.

nbexec również jest plikiem shell, szybkie przeglądniecie kodu i mamy następujący fragment


	   javac=`which javac`
		if [ -z "$javac" ] ; then
			java=`which java`
			if [ ! -z "$java" ] ; then
				java=`resolve_symlink "$java"`
				jdkhome=`dirname $java`"/.."
			fi
		else
			javac=`resolve_symlink "$javac"`
			jdkhome=`dirname $javac`"/.."
		fi

Czyli jeśli Netbeans znajdzie javac to używa jej ścieżki, a jeśli nie, to szuka java. hm..

# javac -version
javac 1.7.0

?! Wygląda na to, że update-java-alternatives nie zmieniało wskazań na javac i to już od.. dawna.

Jeszcze jeden test żeby się upewnić:

 $ update-alternatives --get-selections |grep javac
javac                          auto     /usr/lib/j2sdk1.7-ibm/bin/javac

No i mamy winnego!

update-java-alternatives

W debianie/Ubuntu domyślną javę można ustawiać zbiorczo (dla wszystkich komend związanych z javą) poprzez update-java-alternatives (które pod spodem woła update-alternatives) albo ręcznie dla każdej komendy poprzez update-alternatives.

Żeby update-java-alternatives mogło przełączyć zainstalowane jre/jdk - taka instalacja musi zawierać też ukryty plik *.jinfo. Plik taki definiuje jakie komendy obsługuje dana instalacja oraz ich lokalizacje. Przykładowo dla OpenJdk 1.8.0:

$ cat /usr/lib/jvm/.java-1.8.0-openjdk-amd64.jinfo 
name=java-8-openjdk-amd64
alias=java-1.8.0-openjdk-amd64
priority=1081
section=main

hl rmid /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/rmid
hl java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java
hl keytool /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/keytool
hl jjs /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/jjs
hl pack200 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/pack200
hl rmiregistry /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/rmiregistry
hl unpack200 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/unpack200
hl orbd /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/orbd
hl servertool /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/servertool
hl tnameserv /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/tnameserv
hl jexec /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/jexec
jre policytool /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/policytool
jdkhl idlj /usr/lib/jvm/java-8-openjdk-amd64/bin/idlj
jdkhl jdeps /usr/lib/jvm/java-8-openjdk-amd64/bin/jdeps
jdkhl wsimport /usr/lib/jvm/java-8-openjdk-amd64/bin/wsimport
jdkhl rmic /usr/lib/jvm/java-8-openjdk-amd64/bin/rmic
jdkhl jinfo /usr/lib/jvm/java-8-openjdk-amd64/bin/jinfo
jdkhl jsadebugd /usr/lib/jvm/java-8-openjdk-amd64/bin/jsadebugd
jdkhl native2ascii /usr/lib/jvm/java-8-openjdk-amd64/bin/native2ascii
jdkhl jstat /usr/lib/jvm/java-8-openjdk-amd64/bin/jstat
jdkhl javac /usr/lib/jvm/java-8-openjdk-amd64/bin/javac
jdkhl javah /usr/lib/jvm/java-8-openjdk-amd64/bin/javah
jdkhl jps /usr/lib/jvm/java-8-openjdk-amd64/bin/jps
jdkhl jstack /usr/lib/jvm/java-8-openjdk-amd64/bin/jstack
jdkhl jrunscript /usr/lib/jvm/java-8-openjdk-amd64/bin/jrunscript
jdkhl javadoc /usr/lib/jvm/java-8-openjdk-amd64/bin/javadoc
jdkhl javap /usr/lib/jvm/java-8-openjdk-amd64/bin/javap
jdkhl jar /usr/lib/jvm/java-8-openjdk-amd64/bin/jar
jdkhl extcheck /usr/lib/jvm/java-8-openjdk-amd64/bin/extcheck
jdkhl schemagen /usr/lib/jvm/java-8-openjdk-amd64/bin/schemagen
jdkhl xjc /usr/lib/jvm/java-8-openjdk-amd64/bin/xjc
jdkhl jmap /usr/lib/jvm/java-8-openjdk-amd64/bin/jmap
jdkhl jstatd /usr/lib/jvm/java-8-openjdk-amd64/bin/jstatd
jdkhl jhat /usr/lib/jvm/java-8-openjdk-amd64/bin/jhat
jdkhl jdb /usr/lib/jvm/java-8-openjdk-amd64/bin/jdb
jdkhl serialver /usr/lib/jvm/java-8-openjdk-amd64/bin/serialver
jdkhl wsgen /usr/lib/jvm/java-8-openjdk-amd64/bin/wsgen
jdkhl jcmd /usr/lib/jvm/java-8-openjdk-amd64/bin/jcmd
jdkhl jarsigner /usr/lib/jvm/java-8-openjdk-amd64/bin/jarsigner
jdk appletviewer /usr/lib/jvm/java-8-openjdk-amd64/bin/appletviewer
jdk jconsole /usr/lib/jvm/java-8-openjdk-amd64/bin/jconsole
plugin mozilla-javaplugin.so /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/IcedTeaPlugin.so

Skoro plik jest i jest poprawny (tak wygląda) dlaczego więc update-java-alternatives nie przełączyło javac? Bo ktoś w międzyczasie zmienił tag z jdk na jdkhl w plikach *.jinfo którego update-java-laternatives (w tej wersji) nie obsługuje

Problem jest chyba dośc powszechny, bo jak już wiadomo czego szukać można trafić na sporo podobnych problemów:

itd itp...

Dlaczego gradle działa pomimo tego?

Ano dlatego, że w przeciwieństwie do netbeans szuka komendy java i zakłada, że w tym samym drzewie będzie też javac (w przypadku gdy mamy tam tylko jre trzeba mu ekstra podać mu ścieżkę do jdk)

Rozwiązanie problemu z Netbeans

Zmiana konfiguracji Netbeans

Czyli lokalne nadpisanie domyślnej konfiguracji:

mkdir ~/snap/netbeans/common/data/11.0/etc/
echo 'netbeans_jdkhome="/usr/lib/jvm/java-8-openjdk-amd64"' >> ~/snap/netbeans/common/data/11.0/etc/netbeans.conf

w ten sposób uniezależniamy się też od innych błędów związanych z update-java-alternatives albo od zmian domyślnej javy w ogóle.

Naprawa update-java-alternatives

Czyli przywrócenie rozpoznawalnych tag'ów w pliku *.jinfo

sudo sed -i 's/^hl/jre/p' /usr/lib/jvm/.java-1.8.0-openjdk-amd64.jinfo
sudo sed -i 's/^jdkhl/jdk/p' /usr/lib/jvm/.java-1.8.0-openjdk-amd64.jinfo 

Potem już tylko na nowo trzeba ustawić domyślną javę:

$ sudo update-java-alternatives -s java-1.8.0-openjdk-amd64

i test...

$ javac -version
  javac 1.8.0_191

voila!

Literatura