|
| 1 | +// tag::header[] |
| 2 | + |
| 3 | +# Executable Assembly Jars in Mill |
| 4 | + |
| 5 | + |
| 6 | +:author: Li Haoyi |
| 7 | +:revdate: 2 January 2024 |
| 8 | +_{author}, {revdate}_ |
| 9 | + |
| 10 | +include::mill:ROOT:partial$gtag-config.adoc[] |
| 11 | + |
| 12 | +One feature of the https://mill-build.org[Mill JVM build tool] is that the |
| 13 | +assembly jars it creates are directly executable: |
| 14 | + |
| 15 | +```bash |
| 16 | +> ./mill show foo.assembly # generate the assembly jar |
| 17 | +"ref:v0:bd2c6c70:/Users/lihaoyi/test/out/foo/assembly.dest/out.jar" |
| 18 | + |
| 19 | +> out/foo/assembly.dest/out.jar # run the assembly jar directly |
| 20 | +Hello World |
| 21 | +``` |
| 22 | + |
| 23 | +Other JVM build tools also can generate assemblies, but most need you to run them |
| 24 | +via `java -jar` or `java -cp`, |
| 25 | +or require you to use https://docs.oracle.com/en/java/javase/11/tools/jlink.html[jlink] or |
| 26 | +https://docs.oracle.com/en/java/javase/17/docs/specs/man/jpackage.html[jpackage] |
| 27 | +which are much more heavyweight and troublesome to set up. Mill automates that, and while not |
| 28 | +groundbreaking, it is a nice convenience that makes your JVM |
| 29 | +code built with Mill fit more nicely into command-line centric workflows common in modern |
| 30 | +software systems. |
| 31 | + |
| 32 | +This article will discuss how Mill's executable assemblies are implemented, so perhaps |
| 33 | +other build tools and toolchains will be able to provide the same convenience |
| 34 | + |
| 35 | +// end::header[] |
| 36 | + |
| 37 | +## Trying Out Mill's Executable Assmeblies |
| 38 | + |
| 39 | +To try out Mill's executable assembly jars, you can reproduce the above steps |
| 40 | +with the following code and config: |
| 41 | + |
| 42 | +```scala |
| 43 | +// build.mill |
| 44 | +import mill._, javalib._ |
| 45 | + |
| 46 | +object foo extends JavaModule |
| 47 | +``` |
| 48 | + |
| 49 | +```java |
| 50 | +// foo/src/Foo.java |
| 51 | +package foo; |
| 52 | + |
| 53 | +public class Foo{ |
| 54 | + public static void main(String[] args){ |
| 55 | + System.out.println("Hello World"); |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +```bash |
| 61 | +> ./mill show foo.assembly |
| 62 | +"ref:v0:bd2c6c70:/Users/lihaoyi/test/out/foo/assembly.dest/out.jar" |
| 63 | + |
| 64 | +> /Users/lihaoyi/test/out/foo/assembly.dest/out.jar |
| 65 | +Hello World |
| 66 | +``` |
| 67 | + |
| 68 | +Mill's ``JavaModule``s come with `.assembly` tasks built in by default, without needing |
| 69 | +to install plugins like is necessary in other build tools like |
| 70 | +https://maven.apache.org/plugins/maven-assembly-plugin/usage.html[Maven] or |
| 71 | +https://github.com/sbt/sbt-assembly[SBT]. |
| 72 | + |
| 73 | +While the above example is a trivial single-module project, this also works for more complicated |
| 74 | +projects with multiple modules and third-party dependencies. The assembly jar will aggregate |
| 75 | +the code from all upstream modules and dependencies into a single `.jar` file that you can then |
| 76 | +execute from the command line. |
| 77 | + |
| 78 | +Most `.jar` files are not directly executable, hence the need for a `java -jar` or `java -cp` |
| 79 | +command to run them. To understand how Mill makes direct execution |
| 80 | +possible, we first need to understand what a `.jar` file is. |
| 81 | + |
| 82 | +## What is an Assembly Jar? |
| 83 | + |
| 84 | +An "assembly" jar is just a jar file that includes all transitive dependencies. |
| 85 | +What makes an assembly different from a "normal" jar is that it should (in theory) contain |
| 86 | +everything needed to run you JVM program. In contrast, most "normal" jars do not contain |
| 87 | +their dependencies, and you need to separately go download those dependencies and pass them in |
| 88 | +via `-classpath`/`-cp` before you can run your Java program. |
| 89 | + |
| 90 | +One thing that many people don't know is that jar files are just zip files. You can see |
| 91 | +that from the command line, where although you normally use `jar tf` to list the contents |
| 92 | +of a `.jar` file, `unzip -l` works as well: |
| 93 | + |
| 94 | +```bash |
| 95 | +> jar tf /Users/lihaoyi/test/out/foo/assembly.dest/out.jar |
| 96 | +META-INF/MANIFEST.MF |
| 97 | +META-INF/ |
| 98 | +foo/ |
| 99 | +foo/Foo.class |
| 100 | +``` |
| 101 | +```bash |
| 102 | +> unzip -l /Users/lihaoyi/test/out/foo/assembly.dest/out.jar |
| 103 | +Archive: /Users/lihaoyi/test/out/foo/assembly.dest/out.jar |
| 104 | +warning [/Users/lihaoyi/test/out/foo/assembly.dest/out.jar]: 203 extra bytes at beginning or within zipfile |
| 105 | + (attempting to process anyway) |
| 106 | + Length Date Time Name |
| 107 | +--------- ---------- ----- ---- |
| 108 | + 110 01-02-2025 12:05 META-INF/MANIFEST.MF |
| 109 | + 0 01-02-2025 12:05 META-INF/ |
| 110 | + 0 01-02-2025 12:05 foo/ |
| 111 | + 415 01-02-2025 12:05 foo/Foo.class |
| 112 | +--------- ------- |
| 113 | + 525 4 files |
| 114 | +``` |
| 115 | + |
| 116 | +In this case, the example project only has one `Foo.java` source file, compiled into a single |
| 117 | +`Foo.class` JVM class file. Larger projects will have multiple class files, including those |
| 118 | +from upstream modules and third-party dependencies. |
| 119 | + |
| 120 | +In addition to the compiled class files, jars also can contain metadata. For example, we can see |
| 121 | +this generated `out.jar` contains a `META-INF/MANIFEST.MF` file, which contains some basic |
| 122 | +metadata including the `Main-Class: foo.Foo` which is the entrypoint of the Java program: |
| 123 | + |
| 124 | +```bash |
| 125 | +$ unzip -p /Users/lihaoyi/test/out/foo/assembly.dest/out.jar META-INF/MANIFEST.MF |
| 126 | +warning [/Users/lihaoyi/test/out/foo/assembly.dest/out.jar]: 203 extra bytes at beginning or within zipfile |
| 127 | + (attempting to process anyway) |
| 128 | +Manifest-Version: 1.0 |
| 129 | +Created-By: Mill 0.12.4-23-2ff492 |
| 130 | +Tool: Mill-0.12.4-23-2ff492 |
| 131 | +Main-Class: foo.Foo |
| 132 | +``` |
| 133 | + |
| 134 | +The `warning: 203 extra bytes at beginning or within zipfile` is a hint that although |
| 135 | +this is a valid zip file, something unusual is going on, which leads us to the trick |
| 136 | +that we use to make the `out.jar` file executable |
| 137 | + |
| 138 | +## What is a Zip file? |
| 139 | + |
| 140 | +A https://en.wikipedia.org/wiki/ZIP_(file_format)[zip file] is an archive made of multiple smaller files, individually compressed, |
| 141 | +concatenated together followed by a "central directory" containing the _reverse offsets_ of |
| 142 | +every file within the archive, relative to the central directory. |
| 143 | + |
| 144 | +```graphviz |
| 145 | +digraph G { |
| 146 | + label="archive.zip" |
| 147 | + node [shape=box width=0 height=0 style=filled fillcolor=white] |
| 148 | + zip [shape=record label="<f0> Foo.class | <f1> MANIFEST.MF | <f3> ...other files... | <f2> central directory"] |
| 149 | + zip:f2:n -> zip:f1:n [label="reverse offsets"] |
| 150 | + zip:f2:n -> zip:f0:n |
| 151 | + zip:f2:n -> zip:f3:n |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +The typical way someone reads from a zip file is a follows: |
| 156 | + |
| 157 | +* Seek to the end of zip and find the central directory |
| 158 | +* Find the metadata containing the offset for the file you want |
| 159 | +* Seek backwards using that offset to the start of the entry you want |
| 160 | +* Read and decompress your entry |
| 161 | + |
| 162 | +Unlike `.tar.gz` files, the entries within a `.zip` file are compressed individually. This |
| 163 | +is convenient for use cases like Java classfiles where you want to lazily load them |
| 164 | +individually on-demand without having to first decompress the whole archive up front. |
| 165 | + |
| 166 | +## Executable Zip Archives |
| 167 | + |
| 168 | +One quirk of the above Zip format is that _the zip data does not need to start at the |
| 169 | +beginning of the file_! The zip data can be at the end of an arbitrarily long file, and |
| 170 | +as long as programs can scan to the end of the zip to find the central directory, they |
| 171 | +will be able to extract the zip. |
| 172 | + |
| 173 | +```graphviz |
| 174 | +digraph G { |
| 175 | + node [shape=box width=0 height=0 style=filled fillcolor=white] |
| 176 | + label="archive.zip" |
| 177 | + extra_label:s -> zip:fe:n [color=red penwidth=3] |
| 178 | + extra_label [color=white style=invisible] |
| 179 | + zip [shape=record label="<fe> ...extra data... | <f0> Foo.class | <f1> MANIFEST.MF | <f3> ...other files... | <f2> central directory"] |
| 180 | + zip:f2:n -> zip:f1:n |
| 181 | + zip:f2:n -> zip:f0:n |
| 182 | + zip:f2:n -> zip:f3:n |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +Thus, we can actually use the `.zip` format in two ways: |
| 187 | + |
| 188 | +1. As a `.zip` file, which is read and extracted starting from the end of the file on the right |
| 189 | +2. As something else, such as a bash script, which is read and executed starting from start of the file on the left |
| 190 | + |
| 191 | +This technique is used in common Zip |
| 192 | +https://en.wikipedia.org/wiki/Self-extracting_archives[self-extracting archives], where |
| 193 | +a short bash script is pre-pended to the zip archive that when run extracts the archive using |
| 194 | +`unzip`. Although |
| 195 | +this article is about Jars, `.jar` files are really just ``.zip``s with a different name! |
| 196 | +So we can prepend a bash script to our `.jar` file to |
| 197 | + |
| 198 | +* Run `java` with the current executable `"$0"` as the classpath |
| 199 | +* Pass any of the current executable's command-line arguments `"$@"`as the Java program's command-line arguments |
| 200 | +* Allow configuration of the `java` process (since we're no longer calling it ourselves) via a `JAVA_OPTS` environment variable |
| 201 | + |
| 202 | +```graphviz |
| 203 | +digraph G { |
| 204 | + label="out.jar" |
| 205 | + left [shape=plaintext label="bash script starts executing at start of file\nruns `java` passing itself as the classpath"] |
| 206 | + right [shape=plaintext label="`java` loads compiled classfiles from jar/zip\nby reading the central directory at end of file"] |
| 207 | + |
| 208 | + node [shape=box width=0 height=0 style=filled fillcolor=white] |
| 209 | + zip [shape=record label="<fe> exec java $JAVA_OPTS -cp \"$0\" 'foo.Foo' \"$@\" | <f0> Foo.class | <f1> MANIFEST.MF | <f3>...other files... | <f2> central directory"] |
| 210 | + zip:f2:n -> zip:f1:n |
| 211 | + zip:f2:n -> zip:f0:n |
| 212 | + zip:f2:n -> zip:f3:n |
| 213 | + left -> zip:fe:n [color=red penwidth=3] |
| 214 | + zip:f2:s -> right [dir=back color=red penwidth=3] |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +If you use `less out.jar` to look at what's inside the Jar file, it looks like this: |
| 219 | + |
| 220 | +```bash |
| 221 | +exec java $JAVA_OPTS -cp "$0" 'foo.Foo' "$@" |
| 222 | +PK^C^D^T^@^H^H^H^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@^T^@^Q^@META-INF/MANIFEST.MFUT^M^@^G<97>^Pvg<97>^Pvg<97>^Pvgeɱ |
| 223 | +<80> ^P^@<D0><FD><C0>^?<B8>^_81s<C9>1<A1><CD>-<DA>^OR^P<C4>^Cu<E9><EF>ESC^Z{<EB><8B><DC>JNcҕ<FA>(<D2><.<DA>=<F1>L7<ED><8F><C7>XjE<A3>^W<AB>^]ٕl<CE>n<B3> |
| 224 | +N<91><FA>%<FD>3ri^T*<8F><E1>1<8B><E8>CD<81><82>^WPK^G^HB?^Xo[^@^@^@n^@^@^@PK^C^D |
| 225 | +^@^@^@^@^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@ ^@^Q^@META-INF/UT^M^@^G<97>^Pvg<97>^Pvg<97>^PvgPK^C^D |
| 226 | +^@^@^@^@^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@^D^@^Q^@foo/UT^M^@^G<97>^Pvg<97>^Pvg<97>^PvgPK^C^D^T^@^H^H^H^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@^M^@^Q^@foo/Foo.classUT^M^@^G<97>^Pvg<97>^Pvg<97>^Pvgm<90><CB>J<C3>@^T<86><FF><D3>[<9A>4<DA><DA><DA>z-<E8>BH]<<98>^G<A8><BA>^Q<8A><8B><A0>B<A4>.\<A5><ED>X<A6>L2^R^S<C1><C7>҅<82>^K^_<C0><87>^R<CF>^DA<85><CE><E2><DC><E6><FB><FF>^C<E7><<F3><EB><FD>^C<C0> <FA>^NJ([<A8><B8><A8><A2>Fh-<A2><C7><C8>WQ2<F7>/'^K1<CD>^H<B5>c<99><C8><EC><94>P<F6>^FcESCu<D8>^V^^\^W^M<B8><FF><F0><F0><E9>!^S1S:gQ7(~<A4><F6><AF>R<99>da<96><8A>(^^ֱJh<9C>^K<A5><F4>ލN<D5><CC>A^Kk^V<DA>.:X't<96><88>^Hֽ<E9>T®^<F0>ga<C6><E3><F9>p0<B6><D0>c<E8>Nk^?<A4>5<A1>r<A6>g<82><D0>^Ld".<F2>x"<D2><EB>h<A2>xR<89>#<C9>&=<EF>v<99>^K<C1> u<<9E>N<C5>H^Z<B8><CE>^G^F<C3>><BA>|#<F3>J s%<8E>ESC<DC><F5>9^S<E7><EA><E1>ESC<E8><99>^K<C2>&<C7>Z1,<C3><C6>^V<B6>^?ЃB |
| 227 | +<D8>/<B0><DA>+<AF>h<FE><E2>N<E1>]<E5><B3>^Z<E1>N<B1>e<F7>ESCPK^G^H<94>r+6 ^A^@^@<9F>^A^@^@PK^A^B^T^@^T^@^H^H^H^@<B5>`"ZB?^^Xo[^@^@^@n^@^@^@^T^@ ^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@META-INF/MANIFEST.MFUT^E^@^G<97>^PvgPK^A^B |
| 228 | +^@ |
| 229 | +^@^@^@^@^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@ ^@ ^@^@^@^@^@^@^@^@^@^@^@<AE>^@^@^@META-INF/UT^E^@^G<97>^PvgPK^A^B |
| 230 | +^@ |
| 231 | +^@^@^@^@^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@^D^@ ^@^@^@^@^@^@^@^@^@^@^@<E6>^@^@^@foo/UT^E^@^G<97>^PvgPK^A^B^T^@^T^@^H^H^H^@<B5>`"Z<94>r+6 ^A^@^@<9F>^A^@^@^M^@ ^@^@^@^@^@^@^@^@^@^@^@^Y^A^@^@foo/Foo.classUT^E^@^G<97>^PvgPK^E^F^@^@^@^@^D^@^D^@ |
| 232 | +^A^@^@<85>^B^@^@^@^@ |
| 233 | +/Users/lihaoyi/test/out/foo/assembly.dest/out.jar (END) |
| 234 | +``` |
| 235 | + |
| 236 | +Here, you can see a single line of `exec java $JAVA_OPTS -cp "$0" 'foo.Foo' "$@"` which |
| 237 | +is the bash script we prepended to the zip, followed by the un-intelligible compressed |
| 238 | +class file data that makes up the `.jar`. Since now you are running the Java program |
| 239 | +via `./out.jar` instead of `java -jar`, we expose the `JAVA_OPTS` environment variable |
| 240 | +as a way to pass flags to the `java` command that ends up being run.] |
| 241 | + |
| 242 | +## What about Windows? |
| 243 | + |
| 244 | +The self-executing jar file above works by prepending a shell script. This works on Unix |
| 245 | +environments like Linux or Mac, but not on the Windows machines which are also very common. |
| 246 | + |
| 247 | +To fix this, we can replace our shell script zip prefix with a "universal" script that |
| 248 | +is both a valid `.sh` program as well as valid `.bat` program, the latter being the |
| 249 | +standard windows command line language. Thus, instead of: |
| 250 | + |
| 251 | +```bash |
| 252 | +exec java $JAVA_OPTS -cp "$0" 'foo.Foo' "$@" |
| 253 | +``` |
| 254 | + |
| 255 | +We can instead use: |
| 256 | + |
| 257 | +```bash |
| 258 | +@ 2>/dev/null # 2>nul & echo off & goto BOF |
| 259 | +: |
| 260 | +exec java $JAVA_OPTS -cp "$0" 'foo.Foo' "$@" |
| 261 | +exit |
| 262 | + |
| 263 | +:BOF |
| 264 | +setlocal |
| 265 | +@echo off |
| 266 | +java %JAVA_OPTS% -cp "%~dpnx0" foo.Foo %* |
| 267 | +endlocal |
| 268 | +exit /B %errorlevel% |
| 269 | +``` |
| 270 | + |
| 271 | + |
| 272 | +This universal launcher script is worth digging into. |
| 273 | + |
| 274 | +In a `sh` shell: |
| 275 | + |
| 276 | +* `@ 2>/dev/null # 2>nul & echo off & goto BOF` is an invalid command, but we ignore |
| 277 | + the error because we pipe it to `/dev/null` |
| 278 | + |
| 279 | +* It then runs the `exec java -cp` command |
| 280 | + |
| 281 | +* We `exit` the script before we hit the invalid shell code below |
| 282 | + |
| 283 | +In a `bat` environment: |
| 284 | + |
| 285 | +* We run the first line, doing nothing, until we hit `goto BOF`. This jumps over the `exec java` |
| 286 | + line which is not valid `bat` code, to go straight to the `:BOF` label |
| 287 | + |
| 288 | +* We then run `java -cp`, but with slightly different syntax from the unix/shell version above |
| 289 | + (e.g. `%~dpnx0` instead of `$0`) for windows/bat compatibility |
| 290 | + |
| 291 | +* We then `exit` the script, using `/B %errorlevel%` which is the windows syntax for propagating |
| 292 | + the exit code, before we hit the compressed data below which is not valid `bat` code. |
| 293 | + |
| 294 | +As a result, we have a short script that we can call either from `sh` or `bat`, |
| 295 | +that forwards arguments and the script itself (which is also a `.jar` file) to `java -cp`, |
| 296 | +and then forwards the exit code back from `java -cp` to the caller. Although the script may |
| 297 | +look fragile, the strong backwards compatibility of `.sh` and `.bat` scripts means that |
| 298 | +once working it is unlikely to break in future versions of Mac/Linux/Windows. |
| 299 | + |
| 300 | +If we look at the file using `less -n20`, we can now see our universal launcher script |
| 301 | +pre-pended to the blobs of compressed classfile data that make up the rest of the jar: |
| 302 | + |
| 303 | +```bash |
| 304 | +@ 2>/dev/null # 2>nul & echo off & goto BOF |
| 305 | +: |
| 306 | +exec java $JAVA_OPTS -cp "$0" 'foo.Foo' "$@" |
| 307 | +exit |
| 308 | + |
| 309 | +:BOF |
| 310 | +setlocal |
| 311 | +@echo off |
| 312 | +java %JAVA_OPTS% -cp "%~dpnx0" foo.Foo %* |
| 313 | +endlocal |
| 314 | +exit /B %errorlevel% |
| 315 | +PK^C^D^T^@^H^H^H^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@^T^@^Q^@META-INF/MANIFEST.MFUT^M^@^G<97>^Pvg<97>^Pvg<97>^Pvgeɱ |
| 316 | +<80> ^P^@<D0><FD><C0>^?<B8>^_81s<C9>1<A1><CD>-<DA>^OR^P<C4>^Cu<E9><EF>ESC^Z{<EB><8B><DC>JNcҕ<FA>(<D2><.<DA>=<F1>L7<ED><8F><C7>XjE<A3>^W<AB>^]ٕl<CE>n<B3> |
| 317 | +N<91><FA>%<FD>3ri^T*<8F><E1>1<8B><E8>CD<81><82>^WPK^G^HB?^Xo[^@^@^@n^@^@^@PK^C^D |
| 318 | +^@^@^@^@^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@ ^@^Q^@META-INF/UT^M^@^G<97>^Pvg<97>^Pvg<97>^PvgPK^C^D |
| 319 | +^@^@^@^@^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@^D^@^Q^@foo/UT^M^@^G<97>^Pvg<97>^Pvg<97>^PvgPK^C^D^T^@^H^H^H^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@^M^@^Q^@foo/Foo.classUT^M^@^G<97>^Pvg<97>^Pvg<97>^Pvgm<90><CB>J<C3>@^T<86><FF><D3>[<9A>4<DA><DA><DA>z-<E8>BH]<<98>^G<A8><BA>^Q<8A><8B><A0>B<A4>.\<A5><ED>X<A6>L2^R^S<C1><C7>҅<82>^K^_<C0><87>^R<CF>^DA<85><CE><E2><DC><E6><FB><FF>^C<E7><<F3><EB><FD>^C<C0> <FA>^NJ([<A8><B8><A8><A2>Fh-<A2><C7><C8>WQ2<F7>/'^K1<CD>^H<B5>c<99><C8><EC><94>P<F6>^FcESCu<D8>^V^^\^W^M<B8><FF><F0><F0><E9>!^S1S:gQ7(~<A4><F6><AF>R<99>da<96><8A>(^^ֱJh<9C>^K<A5><F4>ލN<D5><CC>A^Kk^V<DA>.:X't<96><88>^Hֽ<E9>T®^<F0>ga<C6><E3><F9>p0<B6><D0>c<E8>Nk^?<A4>5<A1>r<A6>g<82><D0>^Ld".<F2>x"<D2><EB>h<A2>xR<89>#<C9>&=<EF>v<99>^K<C1> u<<9E>N<C5>H^Z<B8><CE>^G^F<C3>><BA>|#<F3>J s%<8E>ESC<DC><F5>9^S<E7><EA><E1>ESC<E8><99>^K<C2>&<C7>Z1,<C3><C6>^V<B6>^?ЃB |
| 320 | +<D8>/<B0><DA>+<AF>h<FE><E2>N<E1>]<E5><B3>^Z<E1>N<B1>e<F7>ESCPK^G^H<94>r+6 ^A^@^@<9F>^A^@^@PK^A^B^T^@^T^@^H^H^H^@<B5>`"ZB?^^Xo[^@^@^@n^@^@^@^T^@ ^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@META-INF/MANIFEST.MFUT^E^@^G<97>^PvgPK^A^B |
| 321 | +^@ |
| 322 | +^@^@^@^@^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@ ^@ ^@^@^@^@^@^@^@^@^@^@^@<AE>^@^@^@META-INF/UT^E^@^G<97>^PvgPK^A^B |
| 323 | +^@ |
| 324 | +^@^@^@^@^@<B5>`"Z^@^@^@^@^@^@^@^@^@^@^@^@^D^@ ^@^@^@^@^@^@^@^@^@^@^@<E6>^@^@^@foo/UT^E^@^G<97>^PvgPK^A^B^T^@^T^@^H^H^H^@<B5>`"Z<94>r+6 ^A^@^@<9F>^A^@^@^M^@ ^@^@^@^@^@^@^@^@^@^@^@^Y^A^@^@foo/Foo.classUT^E^@^G<97>^PvgPK^E^F^@^@^@^@^D^@^D^@ |
| 325 | +^A^@^@<85>^B^@^@^@^@ |
| 326 | +/Users/lihaoyi/test/out/foo/assembly.dest/out.jar (END) |
| 327 | +``` |
| 328 | + |
| 329 | +We can run it directly on Mac/Linux: |
| 330 | + |
| 331 | +```bash |
| 332 | +> ./mill show foo.assembly # generate the assembly jar |
| 333 | +"ref:v0:bd2c6c70:/Users/lihaoyi/test/out/foo/assembly.dest/out.jar" |
| 334 | + |
| 335 | +> out/foo/assembly.dest/out.jar # run the assembly jar directly |
| 336 | +Hello World |
| 337 | +``` |
| 338 | + |
| 339 | +And we can run it on windows, although we need to rename |
| 340 | +`out.jar` to `out.bat` before executing it: |
| 341 | + |
| 342 | +```bash |
| 343 | +> ./mill show foo.assembly |
| 344 | +"ref:v0:bd2c6c70:C:\\Users\\haoyi\\test\\out\\foo\\assembly.dest\\out.jar" |
| 345 | + |
| 346 | +> cp out\foo\assembly.dest\out.jar out.bat |
| 347 | + |
| 348 | +> ./out.bat |
| 349 | +Hello World |
| 350 | +``` |
| 351 | + |
| 352 | +## Conclusion |
| 353 | + |
| 354 | +The executable assembly jars that Mill generates are very convenient; it means that |
| 355 | +you can use Mill to compile (almost) any Java program into an executable you can run with |
| 356 | +`./out.jar`, as long as you have the appropriate version of Java globally installed. This |
| 357 | +is much easier than setting up JLink or JPackage. You can even have an executable jar that |
| 358 | +runs on all of Mac/Linux/Windows just by carefully crafting a launcher script that runs |
| 359 | +on all platforms. |
| 360 | + |
| 361 | +The Mill JVM build tool provides these executable assembly jars out-of-the-box, the SBT |
| 362 | +build tool as part of the https://github.com/sbt/sbt-assembly[SBT Assembly] plugin, |
| 363 | +via the `prependShellScript` config. |
| 364 | +Maven and Gradle do not provide this by default but it is pretty easy to set up yourself |
| 365 | +simply by concatenating a shell script with an assembly jar, as described above. |
| 366 | + |
| 367 | +Although running Java programs via |
| 368 | +`java -jar` or `java -cp` is not a huge hardship, removing that friction really helps your |
| 369 | +Java programs and codebase feel like a first class citizen on the command-line. |
| 370 | + |
0 commit comments