create jar folder and run using java:
java --class-path (sbt --error "print stageJars")/"*" de.rmgk.CoolApp "<marquee> yeah! </marquee>"
generate native image config:
GraalVM/bin/java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/de.rmgk.CoolApp/generated --class-path (sbt --error "print stageJars")/"*" de.rmgk.CoolApp "<marquee> yeah! </marquee>"
compile with native image:
native-image --class-path (sbt --error "print stageJars")/"*" de.rmgk.CoolApp -o CoolApp
For reasons unclear to me, a ton of explanations of how to bundle Scala programs are incredibly overengineered, complicated, annoying, and brittle.
First, sbt has a built-in way to generate jars using:
sbt package
sbt has a show
and print
command to get the output of tasks you run, in case of package, this provides you with the path to your jar:
sbt --error "print package"
# evaluates to: …/easy-sbt-bundling/target/scala-3.4.1/coolapp_3-0.1.0.jar
Note, the should --error
suppresses other sbt output, but might not work depending on your sbt runner (my sbt is an alias to cs launch sbt -- $argv
)
We can now actually try to run the jar using java:
java -jar (sbt --error "print package")
Note, this is fish shell syntax, the parenthesis evaluates the inner command if you use some other shell you may have to add a $ in front of the parenthesis.
Unfortunately, the above jar only contains the code for your application, not any of its dependencies. Notably, this means the Scala standard library is missing, which you always need.
Luckily, sbt has all the information we need:
sbt --error "print fullClasspathAsJars"
This will print all the paths for all the jars the app needs to run, although, in a format that is not super useful.
This project contains a small sbt plugin in project/JarExport.scala
which adds a stageJars
command, that uses the above fullClasspathAsJars, and copies all those jars into a folder for easy access:
sbt --error "print stageJars"
# evaluates to: …/easy-sbt-bundling/target/jars
ls (sbt --error "print stageJars")
# coolapp_3-0.1.0.jar jsoup-1.17.2.jar scala3-library_3-3.4.1.jar scala-library-2.13.12.jar
This allows us to actually run the application using java. The example application here takes some HTML snippet, uses jsoup to parse it, and print a formatted result:
java --class-path (sbt --error "print stageJars")/"*" de.rmgk.CoolApp "<marquee> yeah! </marquee>"
Or, without the nested commands, we run:
java --class-path "easy-sbt-bundling/target/jars/*" de.rmgk.CoolApp "<marquee> yeah! </marquee>"
The important part is, that you can give java a class path argument that contains a *
which will load all jars in that folder. Be careful to not have the *
expanded by your shell.
The de.rmgk.CoolApp
is the main class you want to execute, you do have to pass this.
If you install graalvm you can use the same to generate a native binary:
native-image --class-path (sbt --error "print stageJars")/"*" de.rmgk.CoolApp -o CoolApp
Unfortunately, this will fail, because we some reflection which needs configuration, fortunately this can be automatically generated by running the program with some arguments:
GraalVM/bin/java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/de.rmgk.CoolApp/generated --class-path (sbt --error "print stageJars")/"*" de.rmgk.CoolApp "<marquee> yeah! </marquee>"
This uses the graalvm java binary (adapt the above to your path!) and adds the “agent” that generates the configuration files. It stores those files inside a resource folder, which is included in your jar and will be automatically found by the native-image tool when compiling the jar.
(Note, while this repo does not, it’s generally a good idea to commit the resulting config files)
So now running that again should work:
native-image --class-path (sbt --error "print stageJars")/"*" de.rmgk.CoolApp -o CoolApp
The -o defines the name of the output binary, we can run it with:
./CoolApp "<marquee> yeah! </marquee>"
Now, you hopefully understand a little bit better how packaging of java applications works, and when you want to do something slightly different, everything is just some easy commands away.