JavaFXアプリケーションのMac用インストーラをJenkinsでビルドする

パッケージJava製品開発担当の大です。こんにちは。

JavaFXでは、昨年リリースされた2.2からネイティブパッケージ機能が追加されました。

Windows用のEXEとMSI、Mac用のDMG、Linux用のRPMとDEBが用意されています。ランタイム同梱なのでサイズは大きくなりますが、別途Javaを用意する必要もなく簡単にインストールできてすぐ動かせるのは魅力ですね。

NetBeansを使用すると、上記の記事のとおりにbuild.xmlに-post-jfx-deployターゲットを追加して<fx:deploy>を呼び出すだけで、dist/bundles/以下にappとdmgが作成されました。

Mac用インストーラ

Mac用インストーラ(クリックで拡大)

ここまでは簡単だったのですが、問題はこの後に起きました。Jenkinsでこのbuild.xmlを使用して同じようにビルドしたところ、dmgの作成で失敗してしまったのです。

ビルドの失敗と理由

Jenkinsのコンソールに出ていた例外は以下のようなものでした。


-post-jfx-deploy:
Using base JDK at: /Library/Java/JavaVirtualMachines/jdk1.7.0_11.jdk
Creating app bundle: /path/to/project/dist/bundles/Hoge.app
Building DMG package for Hoge
[fx:deploy] java.io.IOException: Exec failed with code 1 command [[osascript, /var/folders/56/9jr2s4296nng77md_k0jy51m0000gq/T/build1352470815167055270.fxbundler/macosx/Hoge-dmg-setup.scpt] in unspecified directory
[fx:deploy] at com.sun.javafx.tools.packager.bundlers.IOUtils.exec(IOUtils.java:131)
(省略)
[fx:deploy] at org.apache.tools.ant.launch.Launcher.run(Launcher.java:280)
[fx:deploy] at org.apache.tools.ant.launch.Launcher.main(Launcher.java:109)

appはちゃんと作成されているのですが、dmgの作成のところでIOExceptionが出ています。さらに、失敗したdmgがマウントされっぱなしになっていました。

ググってみると、同様の現象がOracleのフォーラムに報告されていました。

どうやら、dmgの作成はAppleScriptで実行されており、その過程でファインダーをデスクトップに表示する必要があるようですね。

ssh経由でビルドしたとしても、同じユーザがデスクトップを表示していれば成功するはずだ、とあります。今回のプロジェクトの環境では、Jenkinsは別マシンのWindows上で動作しており、スレーブとしてMacをssh経由で使用していたので、さっそくsshのログインユーザと同じユーザでデスクトップを表示してから再度ビルドを実行してみたところ、確かに成功しました。

しかし、ビルド用のユーザがずっとログインしてデスクトップを表示していなければならないというのは不便です。この回答では、もうひとつ、カスタムdmgを作成する方法も紹介されています。


Otherwise, you can do custom dmg along these lines:
- create .dmg image once from manual build
- convert .dmg file to read-write form
- remove content of you application folder (but keep top level app directory)
- add this dmg to the build (should be able to compress it if size is concern)
- at the build time - mount it, copy .app content into the image, then convert .dmg into compressed read only form

面倒くさそうなので、誰かなんとかしてくれてないかなぁと思ってしばらくネット上をうろうろしてたのですが、この回答がJIRAを経て

Oracleのサイトのトラブルシューティングに掲載されてました。

自前でなんとかするしかなさそうです。

なんとかしてみる

というわけで、シェルスクリプトを書きました。

  • build-dmg.sh
#!/bin/sh

if [ $# -ne 3 ]; then
  echo "Usage: $0 DIST_DIR APP_NAME IS_REMOTE"
  exit 1
fi

WRITABLE=Writable.sparseimage
DIST_DIR=$1
APP_NAME=$2
IS_REMOTE=$3

if [ "$IS_REMOTE" != "true" ]; then
  [ -f "$WRITABLE" ] && rm "$WRITABLE"  
  hdiutil convert "$DIST_DIR/$APP_NAME.dmg" -format UDSP -o "$WRITABLE"
else
  [ -f "$WRITABLE" ] || { echo "$WRITABLE not found!"; exit 1; }
  [ -f "$DIST_DIR/$APP_NAME.dmg" ] && rm "$DIST_DIR/$APP_NAME.dmg"

  hdiutil attach "$WRITABLE"
  rm -rf "/Volumes/$APP_NAME/$APP_NAME.app/*"
  ditto "$DIST_DIR/$APP_NAME.app/Contents" "/Volumes/$APP_NAME/$APP_NAME.app/Contents"
  hdiutil detach "/Volumes/$APP_NAME"
  hdiutil compact "$WRITABLE"
  hdiutil convert "$WRITABLE" -format UDBZ -o "$DIST_DIR/$APP_NAME.dmg"
fi

リモートでないなら、書き込み可能なイメージを作成。
リモートなら書き込み可能なイメージにappの中身をコピーしてdmgを作成します。

適当なフォルダ(ここではinstaller)を作って、build-dmg.shを保存しておきます。

build-dmg.shを呼び出すbuild.xmlは以下のようにしました。

  • build.xml
    <condition property="is.remote.mac" value="true" else="false">
        <and>
            <isset property="is.remote" />
            <os family="mac" />
        </and>
    </condition>
    
    <target name="build-native-bundle" unless="${is.remote.mac}">
        <fx:deploy width="${javafx.run.width}" height="${javafx.run.height}" nativeBundles="all"
             outdir="${basedir}/${dist.dir}" outfile="${application.title}"><!-- verbose="true"-->
             <fx:preferences install="true" />
             <fx:info title="${application.title}" vendor="${application.vendor}"/>
             <fx:application name="${application.title}" mainClass="${javafx.main.class}" />
             <fx:resources>
                 <fx:fileset dir="${basedir}/${dist.dir}" includes="${application.title}.jar"/>
             </fx:resources>
        </fx:deploy>
    </target>

    <target name="-post-jfx-deploy">
        <antcall target="build-native-bundle" inheritall="true" />            
        <exec dir="installer" executable="./build-dmg.sh" osfamily="mac">
            <arg value="${basedir}/${dist.dir}/bundles" />
            <arg value="${application.title}" />
            <arg value="${is.remote.mac}" />
        </exec>
    </target>

リモートのmac以外では、普通に<fx:deploy>を呼び出します。
その後、macならbuild-dmg.shを呼び出します。

最低限、一度はdmgの作成に成功している必要がありますので、ローカルでNetBeansでビルドするなりantコマンドを実行するなりしてください。


-post-jfx-deploy:

build-native-bundle:
Using base JDK at: /Library/Java/JavaVirtualMachines/jdk1.7.0_11.jdk
Creating app bundle: /path/to/project/dist/bundles/Hoge.app
Building DMG package for Hoge
Result DMG installer for Hoge: /path/to/project/dist/bundles/Hoge.dmg
[exec] Driver Descriptor Map(DDM:0) を読み込み中...
[exec] Apple(Apple_partition_map:1) を読み込み中...
[exec] disk image(Apple_HFS:2) を読み込み中...
[exec] (Apple_Free:3) を読み込み中...
[exec] 経過時間: 4.258s
[exec] 速度:36.3M バイト/秒
[exec] 節約率:6.4%
[exec] created: /path/to/project/installer/Writable.sparseimage
[exec]

Jenkins側では、Antに渡すプロパティとして「is.remote=true」を設定しておきます。


-post-jfx-deploy:

build-native-bundle:
[exec] /dev/disk1 Apple_partition_scheme
[exec] /dev/disk1s1 Apple_partition_map
[exec] /dev/disk1s2 Apple_HFS /Volumes/Hoge
[exec] ditto: /Volumes/Hoge/Hoge.app/Contents/PlugIns/jdk1.7.0_11.jdk/Contents/Home/jre/lib/jfxrt.jar: No space left on device
[exec] ditto: /Volumes/Hoge/Hoge.app/Contents/PlugIns/jdk1.7.0_11.jdk/Contents/Home/jre/lib/libjfxwebkit.dylib: No space left on device
[exec] ditto: /Volumes/Hoge/Hoge.app/Contents/PlugIns/jdk1.7.0_11.jdk/Contents/Home/jre/lib/rt.jar: No space left on device
[exec] "disk1" unmounted.
[exec] "disk1" ejected.
[exec] 圧縮を開始中...
[exec] 空き領域を再び利用可能にしています...
[exec] 圧縮の仕上げ中...
[exec] 空き領域 10.6 MB のうち 7 MB を再び利用可能にしました。
[exec] イメージ作成エンジンを準備中...
[exec] Driver Descriptor Map(DDM:0) を読み込み中...
[exec] (CRC32 $9BDAE9D5:Driver Descriptor Map(DDM:0))
[exec] Apple(Apple_partition_map:1) を読み込み中...
[exec] (CRC32 $430A9B91:Apple(Apple_partition_map:1))
[exec] disk image(Apple_HFS:2) を読み込み中...
[exec] (CRC32 $1490B69E:disk image(Apple_HFS:2))
[exec] (Apple_Free:3) を読み込み中...
[exec] (CRC32 $00000000:(Apple_Free:3))
[exec] リソースを追加中...
[exec] 経過時間: 9.680s
[exec] ファイルサイズ:50667865 バイト、チェックサム:CRC32 $4AF7DCF0
[exec] 処理されたセクタ数:338479、317657 圧縮されました
[exec] 速度:16.0M バイト/秒
[exec] 節約率:70.8%
[exec] created: /path/to/project/dist/bundles/Hoge.dmg

これで、ビルド用のユーザがログアウトしている状態でもJenkinsでビルドできるようになりました。

でも良く見ると、なんだかdittoでエラーメッセージ(?)が出てますね。。。作成されたdmgの中を見てみると、ちゃんとファイルは全部コピーされているようですし、インストールも出来て動くんだけどなぁ。なんだろう。。。

時間あるときにもうちょっと調べてみます。

補足

<fx:deploy>は、デプロイ後にスクリプトを実行するという機構をもともと持っています。「package/macosx/Hoge-post-image.sh」を作成すれば、自動で実行してくれます。(<fx:deploy>の属性に verbose="true" を指定して実行すると色々教えてくれます。)

今回は「Macでリモートのときは<fx:deploy>自体実行したくない」という条件だったので使えませんでしたが、通常はこちらを使うほうがスマートだと思います。