TornadoFXで書いたアプリをEXEファイルにして配布する

## はじめに
ここで言う配布とは会社等の内製ツールとしてチームへ配布する想定です。商売する等の目的で配布する場合はライセンスにご注意ください。

[前回](https://qiita.com/heyhachi/items/fe732883d35913eb07b3)はFXLauncherで色々と試みましたが、exeファイルの生成には失敗したので、別の方法を模索してみました。[こちらのページ](http://developersnote.jp/java/gradle-launch4j-jar-exe.html)を参考に手を加えています。

## 結論
Gradleプラグインの[us.kirchmeier.capsule](https://plugins.gradle.org/plugin/us.kirchmeier.capsule)と[edu.sc.seis.launch4j](https://plugins.gradle.org/plugin/edu.sc.seis.launch4j)を使って実現出来ました。exeファイルを生成する工程は前回も出てきたlaunch4jを使っています。

## 手順
順を追って説明していきます。IntelliJでTornadoFXプロジェクトを作成するところまでは前回と同じなので省きます。

### [1] fat jarを作る
us.kirchmeier.capsuleを使ってfat jarを作成します。build.gradleに以下の変更を加えます。

```gradle
version = '0.0.1'// 追加

buildscript {
    ext.kotlin_version = "1.2.60"
    ext.tornadofx_version = "1.7.17"
    ext.junit_version = "5.1.0"

    repositories {
        mavenLocal()
        mavenCentral()
        maven {
            url "https://plugins.gradle.org/m2/"// 追加
        }
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.junit.platform:junit-platform-gradle-plugin:1.1.0"
        classpath "us.kirchmeier:gradle-capsule-plugin:1.0.2" // 追加
    }
}

apply plugin: "kotlin"
apply plugin: "application"
apply plugin: "org.junit.platform.gradle.plugin"
apply plugin: "us.kirchmeier.capsule"// 追加

// ～中略～

// fatJarを生成するタスク
task fatJar(type: FatCapsule) {
    applicationClass mainClassName
}
```

この段階でコンソールから

```
gradle fatJar
```

をすると build/libs 以下にjarファイルが2つ生成されます。ファイル名のサフィックスに"-capsule"がついている方が目的のjarファイルです。

### [2] exeファイルを作る
edu.sc.seis.launch4jを使って作成します。まずはbuild.gradleを以下のように変更します。

```gradle
buildscript {
    ext.kotlin_version = "1.2.60"
    ext.tornadofx_version = "1.7.17"
    ext.junit_version = "5.1.0"

    repositories {
        mavenLocal()
        mavenCentral()
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.junit.platform:junit-platform-gradle-plugin:1.1.0"
        classpath "us.kirchmeier:gradle-capsule-plugin:1.0.2"
        classpath "edu.sc.seis.launch4j:launch4j:2.4.6" //追加
    }
}

apply plugin: "kotlin"
apply plugin: "application"
apply plugin: "org.junit.platform.gradle.plugin"
apply plugin: "us.kirchmeier.capsule"
apply plugin: "edu.sc.seis.launch4j"// 追加

// ～中略～

// exe化するタスク
createExe {
    headerType = 'gui' // cuiアプリならconsole
    copyConfigurable = project.tasks.fatJar.outputs.files
    jar = "lib/${project.tasks.fatJar.archiveName}" //対象のjarファイル（今回はfatJarタスクが生成したファイル）
    mainClassName = mainClassName
    bundledJrePath = 'jre-win' //実行時に参照するJREのパス（exeファイルからみた相対パス）
    bundledJre64Bit = true
    outputDir = "output"//exeファイルの出力先
}
```

createExeというタスクでexeファイルを生成します。

```
gradle createExe
```

クリーンビルドする場合は

```
gradle clean createExe
```


ビルドが成功すると、build/outputにexeファイルが出力されます。

オプションが色々とありますが、詳しいことは[gradle-launch4jのgithub](https://github.com/TheBoegl/gradle-launch4j)にある程度まとまっています。空欄のものもあるので全ては把握出来ませんが…。注意点はbundledJrePathで指定するパスはexeファイル実行時に参照するJREのパスです。ですので、このあとJREを同梱するタスクを追加しますが、そこで同梱したJREへのexeファイルからみた相対パスです。

### [3] 配布用にまとめてzipでアーカイブする
build.gradleにタスクを追加します。

```gradle
// アーカイブ時のテンポラリディレクトリ
ext {
    archiveTmpDir = "${buildDir}/${project.name}_tmp"
}

// アーカイブ前に対象のファイルをテンポラリディレクトリにコピーするタスク
task prePack(dependsOn: createExe) doLast {
    copy {
        from "${buildDir}/${createExe.outputDir}/${project.name}.exe"
        into archiveTmpDir
    }
    copy {
        from 'D:/Amazon Corretto/jre8'// 同梱するJREのパス。例えば左記のような感じ。
        into "${archiveTmpDir}/${createExe.bundledJrePath}"
    }
}

// zipでアーカイブ
task pack(type: Zip, dependsOn: prePack) {
    from archiveTmpDir
    archiveName "MyApp.zip"
}

```

この段階でコンソールで

```
gradle pack
```

クリーンビルドする場合は

```
gradle clean pack
```

とすると、build/distributions以下にMyApp.zipというzipファイルが出来上がります。このzipファイルを配布すればJREがインストールされていない環境でも動作するはずです。

## まとめ
ひとまずTornadoFXで作成したアプリをJAR、更にはexeファイルにまとめるという目的は達しましたが、JREがまるごと配布ファイルに含まれるということでHello World程度のアプリケーションでもzipファイルが巨大です。もっといいパッケージングの方法があるかもしれません。

また、今回は非常にシンプルなアプリであるため上手く動いている可能性があります。複雑なGUIアプリケーションをTornadoFXで作成した場合に上手くいかないかもしれません。

最後にbuild.gradle全体を載せます。

```gradle

version = '0.0.1'

buildscript {
    ext.kotlin_version = "1.2.60"
    ext.tornadofx_version = "1.7.17"
    ext.junit_version = "5.1.0"

    repositories {
        mavenLocal()
        mavenCentral()
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.junit.platform:junit-platform-gradle-plugin:1.1.0"
        classpath "us.kirchmeier:gradle-capsule-plugin:1.0.2"
        classpath "edu.sc.seis.launch4j:launch4j:2.4.6"
    }
}

apply plugin: "kotlin"
apply plugin: "application"
apply plugin: "org.junit.platform.gradle.plugin"
apply plugin: "us.kirchmeier.capsule"
apply plugin: "edu.sc.seis.launch4j"

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

repositories {
    mavenLocal()
    mavenCentral()
    maven {
        url "https://oss.sonatype.org/content/repositories/snapshots/"
    }
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

    compile "no.tornado:tornadofx:$tornadofx_version"

    testCompile "org.junit.jupiter:junit-jupiter-api:$junit_version"
    testRuntime "org.junit.jupiter:junit-jupiter-engine:$junit_version"
}

mainClassName = "com.example.demo.app.MyApp"

jar {
    manifest {
        attributes(
                "Class-Path": configurations.compile.collect { it.getName() }.join(" "),
                "Main-Class": mainClassName
        )
    }
    from(configurations.compile.collect { entry -> zipTree(entry) }) {
        exclude "META-INF/MANIFEST.MF"
        exclude "META-INF/*.SF"
        exclude "META-INF/*.DSA"
        exclude "META-INF/*.RSA"
    }
}

ext {
    archiveTmpDir = "${buildDir}/${project.name}_tmp"
}

task fatJar(type: FatCapsule) {
    applicationClass mainClassName
}

task prePack(dependsOn: createExe) doLast {
    copy {
        from "${buildDir}/${createExe.outputDir}/${project.name}.exe"
        into archiveTmpDir
    }
    copy {
        from '同梱したいJREへのパス' // 例えば D:/Amazon Corretto/jre8
        into "${archiveTmpDir}/${createExe.bundledJrePath}"
    }
}

task pack(type: Zip, dependsOn: prePack) {
    from archiveTmpDir
    archiveName "MyApp.zip"
}

createExe {
    headerType = 'gui'
    copyConfigurable = project.tasks.fatJar.outputs.files
    jar = "lib/${project.tasks.fatJar.archiveName}"
    mainClassName = mainClassName
    bundledJrePath = 'jre-win'
    bundledJre64Bit = true
    outputDir = "output"
}

```