sbt-assemblyやMaven Shade Pluginなどで実行可能なJARファイル(java -jar xxxxx.jar)を作成することが
できますが、このJARファイルの最初の方にシェルスクリプトやWindowsのバッチファイルの内容を差し込むことで、
単体のファイルとして実行することができるようになります。
Embulkが、この手法を使っています。
https://github.com/embulk/embulk/blob/v0.8.18/embulk-cli/src/main/sh/selfrun.sh
参考)
実行可能 jar をコマンドっぽく実行するために(java -jar 使いたくない)
実行可能JARファイルをバッチファイルまたはシェルスクリプトに結合して実行する - torutkの日記
確かにJARファイルだけだと、別に起動用のスクリプトもつけてあげないと苦しい時はある気もするというか、
単体で実行できるとそれはそれで便利かもしれません。
というわけで、自分でも試してみようと思います。
まずは素で作る
とりあえず、最初はなにも使わずに同様のものを作成したいと思います。そのあとで、sbtとMavenを使いましょう。
例えば、こういうJavaソースコードを用意。
app/Hello.java
package app; public class Hello { public static void main(String... args) { String word; if (args.length > 0) { word = args[0]; } else { word = "World"; } System.out.printf("Hello %s!!%n", word); } }
コンパイルして、実行可能JARファイルを作成します。
$ javac app/Hello.java $ jar -cvfm hello-app.jar META-INF/MANIFEST.MF app/Hello.class マニフェストが追加されました app/Hello.classを追加中です(入=558)(出=370)(33%収縮されました)
ここで、MANIFEST.MFは以下のとおり。
Manifest-Version: 1.0 Created-By: 1.8.0_121 (Oracle Corporation) Main-Class: app.Hello
これで、「java -jar」で実行できます、と。
$ java -jar hello-app.jar Hello World!! $ java -jar hello-app.jar Java Hello Java!!
で、例えばこんな感じの起動スクリプトを用意して
run.sh
java -jar "$0" "$@" exit $?
JARファイルとくっつけて、ファイルに実行権限を与えます。
$ cat run.sh hello-app.jar > hello-app $ chmod a+x hello-app
JARファイルの先頭に、スクリプトが追加された状態になります。
これで、JARファイルを直接実行できるようになります。
$ ./hello-app Hello World!! $ ./hello-app Java Hello Java!!
Windows向けの場合は、シェルスクリプトではなくてbatファイルの内容(改行コードはCRLF)にすればOKです。
これを、sbtとMavenでやってみます。
sbt+sbt-assembly
sbtで、実行可能JARファイルを作るといえば、sbt-assemblyです。
※依存関係もまとめて作成するので、単一のJARファイルにするという意味でも
GitHub - sbt/sbt-assembly: Deploy fat JARs. Restart processes. (port of codahale/assembly-sbt)
まずは、実行可能JARファイルを作れるように設定してみます。
build.sbt
name := "executable-jar" version := "1.0" scalaVersion := "2.12.1" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-unchecked", "-deprecation", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) mainClass in assembly := Some("app.Hello") assemblyJarName in assembly := "hello-app.jar"
プラグインの追加。
project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.4")
対象のソースコード。
src/main/scala/app/Hello.scala
package app object Hello { def main(args: Array[String]): Unit = { val word = args.toList.headOption.getOrElse("World") println(s"Hello ${word}!!") } }
実行可能JARファイルを作成。
> assembly
できあがり。
$ java -jar target/scala-2.12/hello-app.jar Hello World!! $ java -jar target/scala-2.12/hello-app.jar Scala Hello Scala!!
ここで、ドキュメントに習いシェルスクリプトを追加してみます。
sbt-assemblyの部分だけ抜粋すると、こんな感じです。
mainClass in assembly := Some("app.Hello") assemblyJarName in assembly := "hello-app.jar" import sbtassembly.AssemblyPlugin.defaultShellScript assemblyOption in assembly := (assemblyOption in assembly) .value .copy(prependShellScript = Some(defaultShellScript))
再度asemblyで実行可能JARファイルを作成して、「java -jar」ではなく直接実行してみます。
$ ./target/scala-2.12/hello-app.jar ./target/scala-2.12/hello-app.jar: 2: ./target/scala-2.12/hello-app.jar: : not found./target/scala-2.12/hello-app.jar: 2: ./target/scala-2.12/hello-app.jar: cannot create � 〜略〜
…思い切りコケました。
ここでちょっとファイルの先頭を見ると、どうも改行が足りていない様子。
$ head -n 2 ./target/scala-2.12/hello-app.jar #!/usr/bin/env sh exec java -jar "$0" "$@"PK躎JMETA-INF/MANIFEST.MF����=�0 ��R�� $�5+ 〜省略〜
そこで、とりあえずと改行を足してみます。
import sbtassembly.AssemblyPlugin.defaultShellScript
assemblyOption in assembly :=
(assemblyOption in assembly)
.value
.copy(prependShellScript = Some(defaultShellScript :+ System.lineSeparator))
今度はOKでした!
$ ./target/scala-2.12/hello-app.jar Hello World!! $ ./target/scala-2.12/hello-app.jar Scala Hello Scala!!
なお、この追加されるスクリプトの定義はこちらです。
https://github.com/sbt/sbt-assembly/blob/v0.14.4/src/main/scala/sbtassembly/AssemblyPlugin.scala#L20
prependShellScriptに設定する値を変えれば、カスタマイズも可能です。今回は自作のスクリプトを追加してみましょう。
ちょっと頑張って、LinuxとWindowsの両対応にしてみます。
こんな2つのスクリプトを用意。
Windows用。
※改行コードCRLFで作成
scripts/run.bat
: <<BAT @echo off java -jar %~f0 %* exit /b BAT
Linux用。
scripts/run.sh
exec java -jar "$0" "$@" exit 127
Embulkのスクリプトを、ものすごく簡易化したものですが。「:」とヒアドキュメントで、Windows用の部分がうまく無視されるように
なっていてすごいですね…。
で、これをくっつけます。
assemblyOption in assembly := (assemblyOption in assembly) .value .copy(prependShellScript = Some( Seq(scala.io.Source.fromFile("scripts/run.bat", "UTF-8").mkString + scala.io.Source.fromFile("scripts/run.sh", "UTF-8").mkString)) )
これでassemblyすると、WindowsとLinux両対応の実行可能ファイルができあがります。
Windowsの場合は、拡張子を.batにしてから実行。
>move hello-app.jar hello-app.bat >hello-app.bat Hello World!! >hello-app.bat Scala Hello Scala!!
$ ./target/scala-2.12/hello-app.jar Hello World!! $ ./target/scala-2.12/hello-app.jar Scala Hello Scala!!
Windowsの知識がなくて、ムダにハマりました…。
まあ、やりたいことはできたのでOKです。
Maven
Mavenの場合は、Spring BootのMaven Pluginを使うのが簡単かなと思います。
59. Installing Spring Boot applications
プラグインと設定。Spring Boot本体は、別になくてもOKです。
<build> <finalName>hello-app</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>1.5.2.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <executable>true</executable> </configuration> </plugin> </plugins> </build>
設定で変わっている点は、executableをtrueにしているところです。
Javaのソースコードとしては、こちらを用意。
src/main/java/app/Hello.java
package app; public class Hello { public static void main(String... args) { String word; if (args.length > 0) { word = args[0]; } else { word = "World"; } System.out.printf("Hello %s!!%n", word); } }
これでパッケージングすると、直接実行可能なJARファイルができあがります。
$ mvn package
実行。
$ ./target/hello-app.jar Hello World!! $ ./target/hello-app.jar Java Hello Java!!
デフォルトのスクリプトはCentOS、Ubuntuでテストされたものみたいです。
というわけで、JARファイルの先頭にえらいごっついスクリプトが入っています。
$ head -n 30 ./target/hello-app.jar #!/bin/bash # # . ____ _ __ _ _ # /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ # ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ # \\/ ___)| |_)| | | | | || (_| | ) ) ) ) # ' |____| .__|_| |_|_| |_\__, | / / / / # =========|_|==============|___/=/_/_/_/ # :: Spring Boot Startup Script :: # ### BEGIN INIT INFO # Provides: executable-jar # Required-Start: $remote_fs $syslog $network # Required-Stop: $remote_fs $syslog $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: executable-jar # Description: executable-jar # chkconfig: 2345 99 01 ### END INIT INFO [[ -n "$DEBUG" ]] && set -x # Initialize variables that cannot be provided by a .conf file WORKING_DIR="$(pwd)" # shellcheck disable=SC2153 [[ -n "$JARFILE" ]] && jarfile="$JARFILE" [[ -n "$APP_NAME" ]] && identity="$APP_NAME"
差し込むスクリプトを変更する場合は、embeddedLaunchScriptに対象のスクリプトファイルを指定します。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>1.5.2.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <executable>true</executable> <embeddedLaunchScript>scripts/run.sh</embeddedLaunchScript> </configuration> </plugin>
ここは、もうシェルスクリプトだけにしました…。
scripts/run.sh
exec java -jar "$0" "$@" exit 127
あとはパッケージングすればOKです。
まとめ
実行可能JARファイルの先頭にシェルスクリプト、もしくはbatファイルの内容を差し込んで、単体のファイルで実行可能なように
してみました。
別に起動用スクリプトがいらなくなるので便利は便利なんですけど、スクリプト内に「java -jar」が固定で書かれちゃうので、
JavaVMへのオプション(例えば-Xmx)とかシステムプロパティを設定したい時ってちょっと難しかったりします。
Embulkも「-J」をつけたものを見分けていましたし。
他にも、「JAVA_OPTS」とかの環境変数越しに渡すという手もあるでしょう。
まあ、方法としては覚えておいて、使えそうなところではトライしてみましょうというところでしょうか。