macOSでJavaプログラムをちょっとだけまともにしたかった

# はじめに
jlinkして作ったアプリをmacOSで動かすとき、少しでもそれっぽくしたい。
結局ちょっと微妙なので、もっといい方法があるかも。

# アプリケーションバンドル
macOSのアプリはアプリケーションバンドルという形式のディレクトリ（アプリ名.app）になっていて、その下に決められた構成でファイルを置けば、普通のアプリっぽくなる。

アプリケーションバンドルは <アプリ名>.app/Contents/　の下に Info.plist を置いて、MacOS/<実行ファイル> とその他必要なファイルを Resourcesに置けば良い。

# Info.plist
Info.plistはXMLファイルなので、一応手で書けるが、今のところスクリプトエディタでJavaプログラムを実行するだけの applescript を作り、それをアプリケーションとして出力するのが簡単。

# apple script
apple scriptでのプログラムの実行は do shell script "コマンド" でできるが、問題はカレントディレクトリがわからない（実際には / になっているらしい）こと。
つまりJava実行ファイルをフルパスで指定しないといけない。

/Application に置く前提でフルパスで書く手もあるが、ここではちょっと頑張ってみる。

スクリプトがあるディレクトリは取得できるので、それを使ってフルパスを生成する。以下のようにすると、スクリプトのフルパスが取得できる。

```applescript
set scriptdir to (path to me) as Unicode Text
```

ただ、これだと大昔の : 区切りパスで出てくるので、以下のようにして / 区切りに変換する。

```applescript
set posixpath to POSIX path of scriptdir
```

ここまででスクリプトファイルのフルパスが取得できる。これをアプリケーション形式で出力すると、アプリケーションバンドルのディレクトリ（<アプリ名.app>/）がフルパスで取得できる。

メニューの「ファイル」→「書き出す」で、ファイルフォーマットを「アプリケーション」にする、この段階ではコード署名しない方がいいかも（書き換えられなくなるので）

これでアプリケーションバンドルができる。このアプリケーションバンドルの中にJavaプログラムを同梱して起動すれば良い。


# アプリケーションバンドルへの追加
まずはスクリプトからjavaプログラムを実行する。

```applescript
set cmdline to posixpath & "Contents/Resources/java/bin/app"
do shell script cmdline
```

という感じ、app は jlinkでできた起動スクリプト。
起動スクリプトが <アプリ名.app>/Contents/Resources/java/bin/app になるように、jlinkしてできたもの（app/build/image以下）はそのまま　<アプリ名.app>/Contents/Resources/java 以下にコピーする。

実行ファイル以外のファイルは Resources に置くらしいので、とりあえずそうしたが、別に MacOS 以下に置いても動くので、その辺りは適当で大丈夫。

アプリケーションバンドルの書き出しとjlinkで出来たimageの配置が終われば、一応macOSのアプリの形になる。ダブルクリックで起動できるはず。/Application ディレクトリに移動しても動くはず。

というわけでまとめると、apple scriptはこんな感じ。これを「書き出す」でアプリケーションにして書き出す。

```applescript
set scriptdir to (path to me) as Unicode Text
set posixpath to POSIX path of scriptdir
set cmdline to posixpath & "Contents/Resources/java/bin/app"
do shell script cmdline
```

# shell scriptでやる場合
結局は実行できればいいだけなので、shell scriptでも問題ない。Info.plistを作るのがめんどくさいだけ。
でもまあ、最低限こんな感じでいいらしい。icnsファイルの名前にはディレクトリは書かない、icnsファイルはResourcesディレクトリに置くこと。

shell scriptでやると、shell script自体はJavaアプリの起動と同時に終了するので、アイコンが２つにならない。なので、同じアイコンを指定しておくと割とまとも。ただ、URLスキームで起動とかが出来ないっぽい。（引数が渡せない）

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>アプリの名前</string>
<key>CFBundleIconFile</key>
<string>icnsファイルの名前</string>
<key>CFBundleExecutable</key>
<string>実行するファイルの名前（ここではスクリプトの名前）</string>
<key>CFBundleIdentifier</key>
<string>識別名 com.apple.Safariみたいなの</string>
</dict>
</plist>
```

実行するスクリプトは、やっぱりJava実行ファイルのフルパスが必要なので、こんな感じになる。ただ、apple script と違ってスクリプトそのもののフルパスになるので、`../` とかを使うか Resources ディレクトリではなく MacOS ディレクトリにJavaプログラムを置くなどする。
今回はMacOSディレクトリにjlinkで出来た bin, conf, legal, lib を置く想定。

```shell
#!/bin/bash
scrpath=`readlink -f "${BASH_SOURCE:-$0}"`
scrdir=`dirname "$scrpath"`/bin
cd $scrdir
./java -m <モジュール名>/<パッケージ名.クラス名> "$@"
```

`${BASH_SOURCE:-$0}` は、`source <script名>` とか `zsh` で実行した時のことを考慮してなのだが、今回は `#!/bin/bash` しているから多分 `$0` だけで大丈夫。でもまあ一応。ただディレクトリ名中に空白文字があったりするとダメかも。

gradle jlinkで作れる起動スクリプトは無視して作っていて、直接javaを起動している。起動スクリプトを実行するようにしても大丈夫なはず。


# アイコン
もうちょっと見た目をまともにするならば、アプリケーションのアイコンとDockに出てくるアイコンをなんとかする。アプリケーションバンドルのアイコンは、アプリケーションバンドルを選択して「情報を見る」を開き、icnsファイルを左上の小さいアイコン表示にドラッグ・ドロップすると変えられる。
icnsファイルは色々な大きさのアイコンをまとめたもの、作り方は後で。

Dockアイコンが割と厄介。そのままだとスクリプトが出すアイコンとjavaが出すアイコンの２つがDockに出てきてしまう。
スクリプトのアイコンは Info.plist に以下のエントリを追加すると消せる。

```xml
<plist version="1.0">
<dict>
     .
     .
     .
    <key>LSUIElement</key>
    <true/>
     .
     .
     .
</dict>
</plist>
```

javaの方で消すこともできる。起動スクリプトの java 実行のところに、-Dapple.awt.UIElement=true と追加すれば良い。
ただ、こちらは一瞬Javaアイコンが出てすぐ消えるみたいな動きになるっぽい。

スクリプトのアイコンを消した場合、そのままだとJavaのDuke（だっけ？）のアイコンが出てくる。これはJavaプログラムの方で変えられるが、こっちも一瞬Javaアイコンが出て、指定したやつに変わるという微妙な動き。
こちらでは icns ファイルは使えないっぽいので、普通のpngにしておく。勝手に大きさは調整されるので、適当に大きいサイズ（512x512とか）の方が良いかも。

gradleを使ってjava-applicationで作ったプロジェクトなら、resourcesにicon.pngを置いておけば良い。

実際のコードは以下の通り（Java 9以降）
```java
import java.net.URL;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.Taskbar;

public class App {
    .
    .
    .
    try {
        URL iconurl = App.class.getClassLoader().getResource("icon.png");
        Image iconimg = Toolkit.getDefaultToolkit().getImage(imgurl);
        Taskbar taskbar = Taskbar.getTaskbar();
        taskbar.setIconImage(img);
    }
    catch (Exception e) {
        // アイコンが設定できないだけなので、とりあえず何もしなくても動くはず     
    }
    .
    .
    .
}
```

ここまでやれば起動時のDock表示がちょっと変な以外は一応アプリっぽいかと。

一度applescriptでアプリケーションバンドルを作ったら、gradle jlinkで自動的にコピーされるようにしておけばより良いかも。

# Intel/ARM両対応
今時 m1, m2 非対応は無いし、Intelを完全に切るのもちょっとという感じなので、両対応してみる。OpenJDKはそれぞれ出ているので、両方用意して、gradle では targetPlatform　を指定して、それぞれの実行モジュールをビルドする。
targetPlatformの指定は[graldeでjlinkを使う](https://qiita.com/jkomatsu/items/dfdc27d7ee2e4ad6caa1)参照。

ディレクトリ分けして、アプリケーションバンドルには両方とも入れておく。

そしてapplescriptでCPUを判別して起動するjavaを変えれば良い。具体的には以下のようにする。

```applescript
set cputype to CPU type of (system info)
if cputype contains "Intel" then
    do shell script "intel用コマンド"
else
    do shell script "ARM用コマンド"
end if
```

shell scriptでも判別できる。こんな感じ。

```shell
#!/bin/bash
cputype=`uname -m`
if [ "$cputype" == "x86_64" ]; then
    "intel用コマンド"
else
    "ARM用コマンド"
fi
```

# icnsファイルの作り方
標準で iconutil というツールが入っているので、それを使う。
まずはいろんな解像度のアイコンをpngファイルで用意する。そして、ファイル名を以下のようにしておく。`@2x`がつくのはdpiが高い環境用。

| ファイル名 | 解像度 |
|:-:|:-:|
| `icon_16x16.png`  |  16x16 |
| `icon_16x16@2x.png`  | 32x32  |
| `icon_32x32.png`  | 32x32  |
| `icon_32x32@2x.png`  | 64x64  |
| `icon_128x128.png`  | 128x128  |
| `icon_128x128@2x.png`  | 256x256  |
| `icon_256x256.png`  | 256x256  |
| `icon_256x256@2x.png`  | 512x512  |
| `icon_512x512.png`  | 512x512  |
| `icon_512x512@2x.png`  | 1024x1024  |

このファイルを <アイコン名>.iconset という名前のディレクトリに入れておく。
そうしたら、iconutil -c icns <アイコン名>.iconset とすると完成。