はじめに
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 に置く前提でフルパスで書く手もあるが、ここではちょっと頑張ってみる。
スクリプトがあるディレクトリは取得できるので、それを使ってフルパスを生成する。以下のようにすると、スクリプトのフルパスが取得できる。
set scriptdir to (path to me) as Unicode Text
ただ、これだと大昔の : 区切りパスで出てくるので、以下のようにして / 区切りに変換する。
set posixpath to POSIX path of scriptdir
ここまででスクリプトファイルのフルパスが取得できる。これをアプリケーション形式で出力すると、アプリケーションバンドルのディレクトリ(<アプリ名.app>/)がフルパスで取得できる。
メニューの「ファイル」→「書き出す」で、ファイルフォーマットを「アプリケーション」にする、この段階ではコード署名しない方がいいかも(書き換えられなくなるので)
これでアプリケーションバンドルができる。このアプリケーションバンドルの中にJavaプログラムを同梱して起動すれば良い。
アプリケーションバンドルへの追加
まずはスクリプトからjavaプログラムを実行する。
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はこんな感じ。これを「書き出す」でアプリケーションにして書き出す。
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アプリの起動と同時に終了するので、アイコンが2つにならない。なので、同じアイコンを指定しておくと割とまとも。ただ、URLスキームで起動とかが出来ないっぽい。(引数が渡せない)
<?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 を置く想定。
#!/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が出すアイコンの2つがDockに出てきてしまう。
スクリプトのアイコンは Info.plist に以下のエントリを追加すると消せる。
<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以降)
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を使う参照。
ディレクトリ分けして、アプリケーションバンドルには両方とも入れておく。
そしてapplescriptでCPUを判別して起動するjavaを変えれば良い。具体的には以下のようにする。
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でも判別できる。こんな感じ。
#!/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 とすると完成。