From 9208ba603c3455528c37f619080232558e16b3e2 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:38:13 +0100 Subject: [PATCH 01/15] add jlink & jpackage Java examples Co-authored-by: Tobias Roeser --- example/javalib/module/16-jlink/build.mill | 23 +++++ .../16-jlink/foo/resources/application.conf | 1 + .../module/16-jlink/foo/src/foo/Bar.java | 10 +++ .../module/16-jlink/foo/src/module-info.java | 4 + .../bar/resources/application.conf | 1 + example/javalib/module/17-jpackage/build.mill | 55 ++++++++++++ .../foo/resources/application.conf | 1 + .../module/17-jpackage/foo/src/foo/Foo.java | 12 +++ scalalib/src/mill/scalalib/JavaModule.scala | 7 ++ scalalib/src/mill/scalalib/JlinkModule.scala | 90 +++++++++++++++++++ .../src/mill/scalalib/JpackageModule.scala | 83 +++++++++++++++++ 11 files changed, 287 insertions(+) create mode 100644 example/javalib/module/16-jlink/build.mill create mode 100644 example/javalib/module/16-jlink/foo/resources/application.conf create mode 100644 example/javalib/module/16-jlink/foo/src/foo/Bar.java create mode 100644 example/javalib/module/16-jlink/foo/src/module-info.java create mode 100644 example/javalib/module/17-jpackage/bar/resources/application.conf create mode 100644 example/javalib/module/17-jpackage/build.mill create mode 100644 example/javalib/module/17-jpackage/foo/resources/application.conf create mode 100644 example/javalib/module/17-jpackage/foo/src/foo/Foo.java create mode 100644 scalalib/src/mill/scalalib/JlinkModule.scala create mode 100644 scalalib/src/mill/scalalib/JpackageModule.scala diff --git a/example/javalib/module/16-jlink/build.mill b/example/javalib/module/16-jlink/build.mill new file mode 100644 index 00000000000..9960b7b304f --- /dev/null +++ b/example/javalib/module/16-jlink/build.mill @@ -0,0 +1,23 @@ +//// SNIPPET:BUILD +package build +import mill._, javalib._ +import mill.javalib.Assembly._ +import mill.scalalib.JlinkModule + +object foo extends JavaModule with JlinkModule { + override def jlinkModuleName: T[String] = T { "foo" } + override def jlinkModuleVersion: T[Option[String]] = T { Option("1.0.0") } +} + +/** Usage + +> mill foo.jlinkAppImage + +> mill show foo.jlinkAppImage +".../out/foo/jlinkAppImage.dest/jlink-runtime" + +> ./out/foo/jlinkAppImage.dest/jlink-runtime/bin/jlink +Nov 27, 2024 7:21:12 PM foo.Bar main +INFO: Hello World! + +*/ \ No newline at end of file diff --git a/example/javalib/module/16-jlink/foo/resources/application.conf b/example/javalib/module/16-jlink/foo/resources/application.conf new file mode 100644 index 00000000000..9341faacf2a --- /dev/null +++ b/example/javalib/module/16-jlink/foo/resources/application.conf @@ -0,0 +1 @@ +Bar Application Conf diff --git a/example/javalib/module/16-jlink/foo/src/foo/Bar.java b/example/javalib/module/16-jlink/foo/src/foo/Bar.java new file mode 100644 index 00000000000..05a4a59718b --- /dev/null +++ b/example/javalib/module/16-jlink/foo/src/foo/Bar.java @@ -0,0 +1,10 @@ +package foo; + +import java.util.logging.Logger; + +public class Bar { + private static final Logger LOG = Logger.getLogger(Bar.class.getName()); + public static void main(String[] args) { + LOG.info("Hello World!"); + } +} \ No newline at end of file diff --git a/example/javalib/module/16-jlink/foo/src/module-info.java b/example/javalib/module/16-jlink/foo/src/module-info.java new file mode 100644 index 00000000000..19d27adadab --- /dev/null +++ b/example/javalib/module/16-jlink/foo/src/module-info.java @@ -0,0 +1,4 @@ +module foo +{ + requires java.logging; +} \ No newline at end of file diff --git a/example/javalib/module/17-jpackage/bar/resources/application.conf b/example/javalib/module/17-jpackage/bar/resources/application.conf new file mode 100644 index 00000000000..9341faacf2a --- /dev/null +++ b/example/javalib/module/17-jpackage/bar/resources/application.conf @@ -0,0 +1 @@ +Bar Application Conf diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill new file mode 100644 index 00000000000..4cc2189b714 --- /dev/null +++ b/example/javalib/module/17-jpackage/build.mill @@ -0,0 +1,55 @@ +//// SNIPPET:BUILD +package build +import mill._, javalib._ +import mill.javalib.Assembly._ +import mill.scalalib.JpackageModule + +object foo extends JavaModule with JpackageModule { + override def jpackageType = "pkg" + + def assemblyRules = Seq( + // all application.conf files will be concatenated into single file + Rule.Append("application.conf"), + // all *.conf files will be concatenated into single file + Rule.AppendPattern(".*\\.conf"), + ) +} + + +/** Usage + +> mill foo.assembly + +> unzip -p ./out/foo/assembly.dest/out.jar application.conf +Foo Application Conf + +> java -jar ./out/foo/assembly.dest/out.jar +Loaded application.conf from resources: Foo Application Conf + +> mill show foo.jpackageAppImage +".../out/foo/jpackageAppImage.dest/image" + + +# jpackageType accepts 3 values on macOS: "dmg" or "pkg" or "app-image" (default). +# macOS: jpackageType = "dmg" +> ls -l ./out/foo/jpackageAppImage.dest/image +-rw-r--r--@ 1 mac staff 6291968 Nov 25 15:14 foo-1.0.dmg + +# macOS: jpackageType = "pkg" +> ls -l ./out/foo/jpackageAppImage.dest/image +total 106360 +-rw-r--r-- 1 mac staff 54456037 Nov 25 15:19 foo-1.0.pkg + +# macOS: jpackageType = "app-image" +> ls -l ./out/foo/jpackageAppImage.dest/image +drwxr-xr-x 3 mac staff 96 Nov 25 15:10 foo.app/ + +> ./out/foo/jpackageAppImage.dest/image/foo.app/Contents/MacOS/foo +Loaded application.conf from resources: Foo Application Conf + + +# Linux: jpackageType = "app-image" +> ./out/foo/jpackageAppImage.dest/image/foo/bin/foo +Loaded application.conf from resources: Foo Application Conf + +*/ \ No newline at end of file diff --git a/example/javalib/module/17-jpackage/foo/resources/application.conf b/example/javalib/module/17-jpackage/foo/resources/application.conf new file mode 100644 index 00000000000..482e5cf3347 --- /dev/null +++ b/example/javalib/module/17-jpackage/foo/resources/application.conf @@ -0,0 +1 @@ +Foo Application Conf diff --git a/example/javalib/module/17-jpackage/foo/src/foo/Foo.java b/example/javalib/module/17-jpackage/foo/src/foo/Foo.java new file mode 100644 index 00000000000..87af722ed02 --- /dev/null +++ b/example/javalib/module/17-jpackage/foo/src/foo/Foo.java @@ -0,0 +1,12 @@ +package foo; + +import java.io.IOException; +import java.io.InputStream; + +public class Foo { + public static void main(String[] args) throws IOException { + InputStream inputStream = Foo.class.getClassLoader().getResourceAsStream("application.conf"); + String conf = new String(inputStream.readAllBytes()); + System.out.println("Loaded application.conf from resources: " + conf); + } +} diff --git a/scalalib/src/mill/scalalib/JavaModule.scala b/scalalib/src/mill/scalalib/JavaModule.scala index 44efaf281f6..7df4a18a644 100644 --- a/scalalib/src/mill/scalalib/JavaModule.scala +++ b/scalalib/src/mill/scalalib/JavaModule.scala @@ -360,6 +360,13 @@ trait JavaModule T.traverse(transitiveModuleRunModuleDeps)(_.localClasspath)().flatten } + /** + * Almost the same as [[transitiveLocalClasspath]], but using the [[jar]]s instead of [[localClasspath]]. + */ + def transitiveJars: T[Seq[PathRef]] = Task { + T.traverse(transitiveModuleCompileModuleDeps)(_.jar)() + } + /** * Same as [[transitiveLocalClasspath]], but with all dependencies on [[compile]] * replaced by their non-compiling [[bspCompileClassesPath]] variants. diff --git a/scalalib/src/mill/scalalib/JlinkModule.scala b/scalalib/src/mill/scalalib/JlinkModule.scala new file mode 100644 index 00000000000..f23da0f50f3 --- /dev/null +++ b/scalalib/src/mill/scalalib/JlinkModule.scala @@ -0,0 +1,90 @@ +package mill +package scalalib + +import mill._ + +/** + * Support building modular runtime images with the `jlink` tool, which is included in JDK 9 and later. + * + * The official `jlink` docs: https://docs.oracle.com/en/java/javase/23/docs/specs/man/jlink.html + */ +trait JlinkModule extends JavaModule { + + /** The base name for the runtime image */ + def jlinkImageName: T[String] = T { "jlink" } + + /** Name of the main module to be included in the runtime image */ + def jlinkModuleName: T[String] = T { "" } + + /** The main module's version number. */ + def jlinkModuleVersion: T[Option[String]] = T { None } + + /** The main class to use as the runtime entry point. */ + def jlinkMainClass: T[String] = T { finalMainClass() } + + /** Compress level for the runtime image. + * Valid values range between: + * "zip-0" (no compression) and "zip-9" (best compression) + * Defaults to "zip-6" */ + def jlinkCompressLevel: T[String] = T { "zip-6" } + + + /** + * Creates a Java module file (.jmod) from compiled classes + */ + def jmodPackage: T[PathRef] = T { + + val mainClass: String = finalMainClass() + val outputPath = T.dest / "jlink.jmod" + + val libs = T.dest / "libs" + val cp = runClasspath().map(_.path) + val jars = cp.filter(os.exists).zipWithIndex.map { case (p, idx) => + val dest = libs / s"${p.last}" + os.copy(p, dest, createFolders = true) + dest + } + + val classPath = jars.map(_.toString).mkString(sys.props("path.separator")) + val args = { + val baseArgs = Seq( + "jmod", + "create", + "--class-path", classPath.toString, + "--main-class", mainClass, + "--module-path", classPath.toString, + outputPath.toString + ) + + val versionArgs = jlinkModuleVersion().toSeq.flatMap { version => + Seq("--module-version", version) + } + + baseArgs ++ versionArgs + } + os.proc(args).call() + + PathRef(outputPath) + } + + /** Builds a custom runtime image using jlink */ + def jlinkAppImage: T[PathRef] = T { + val modulePath = jmodPackage().path.toString + val outputPath = T.dest / "jlink-runtime" + + val args = Seq( + "jlink", + "--launcher", s"${jlinkImageName()}=${jlinkModuleName()}/${jlinkMainClass()}", + "--module-path", modulePath, + "--add-modules", jlinkModuleName(), + "--output", outputPath.toString, + "--compress", jlinkCompressLevel().toString, + "--no-header-files", + "--no-man-pages" + ) + os.proc(args).call() + + PathRef(outputPath) + } +} + diff --git a/scalalib/src/mill/scalalib/JpackageModule.scala b/scalalib/src/mill/scalalib/JpackageModule.scala new file mode 100644 index 00000000000..e82517f4b3a --- /dev/null +++ b/scalalib/src/mill/scalalib/JpackageModule.scala @@ -0,0 +1,83 @@ +package mill +package scalalib + +import mill._ + +/** + * Support for building a native package / installer with the `jpackage` tool which comes bundled with JDK 14 and later. + * + * The official `jpackage` docs: https://docs.oracle.com/en/java/javase/23/docs/specs/man/jpackage.html + */ +trait JpackageModule extends JavaModule { + + /** The application name */ + def jpackageName: T[String] = T { artifactName() } + + /** The main class to use as the entry point to the native package / installer. */ + def jpackageMainClass: T[String] = T { finalMainClass() } + + /** + * The type of native package / installer to be created. + * + * Valid values are: + * "app-image" - any OS + * "dmg", "pkg" - macOS (native package, installer) + * "exe", "msi" - Windows (native package, installer) + * "rpm", "deb" - Linux + * + * If unspecified, defaults to "app-image" which will build a package native to the host platform. + * */ + def jpackageType: T[String] = T { "app-image" } + + /** + * The classpath used for the `jpackage` tool. The first entry needs to be the main jar. + * In difference to [[runClasspath]], it contains the built jars of all dependent modules. + */ + def jpackageRunClasspath: T[Seq[PathRef]] = T { + val recLocalClasspath = (localClasspath() ++ transitiveLocalClasspath()).map(_.path) + + val runCp = runClasspath().filterNot(pr => recLocalClasspath.contains(pr.path)) + + val mainJar = jar() + val recJars = transitiveJars() + + mainJar +: (recJars ++ runCp) + } + + /** Builds a native package of the main application. */ + def jpackageAppImage: T[PathRef] = T { + // materialize all jars into a "lib" dir + val libs = T.dest / "lib" + val cp = jpackageRunClasspath().map(_.path) + val jars = cp.filter(os.exists).zipWithIndex.map { case (p, idx) => + val dest = libs / s"${idx + 1}-${p.last}" + os.copy(p, dest, createFolders = true) + dest + } + + val appName = jpackageName() + val appType = jpackageType() + val mainClass = jpackageMainClass() + val mainJarName = jars.head.last + + val args: Seq[String] = Seq( + "jpackage", + "--type", + appType, + "--name", + appName, + "--input", + libs.toString(), + "--main-jar", + mainJarName, + "--main-class", + mainClass + ) + + // run jpackage tool + val outDest = T.dest / "image" + os.makeDir.all(outDest) + os.proc(args).call(cwd = outDest) + PathRef(outDest) + } +} From ff7e123c4eb35904def22a8355ce1a5e2a673234 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:06:37 +0100 Subject: [PATCH 02/15] remove unnecessary keywords from build files --- example/javalib/module/16-jlink/build.mill | 4 ++-- example/javalib/module/17-jpackage/build.mill | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/javalib/module/16-jlink/build.mill b/example/javalib/module/16-jlink/build.mill index 9960b7b304f..e1ba7929b5d 100644 --- a/example/javalib/module/16-jlink/build.mill +++ b/example/javalib/module/16-jlink/build.mill @@ -5,8 +5,8 @@ import mill.javalib.Assembly._ import mill.scalalib.JlinkModule object foo extends JavaModule with JlinkModule { - override def jlinkModuleName: T[String] = T { "foo" } - override def jlinkModuleVersion: T[Option[String]] = T { Option("1.0.0") } + def jlinkModuleName: T[String] = T { "foo" } + def jlinkModuleVersion: T[Option[String]] = T { Option("1.0") } } /** Usage diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index 4cc2189b714..44da679fb2c 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -5,7 +5,7 @@ import mill.javalib.Assembly._ import mill.scalalib.JpackageModule object foo extends JavaModule with JpackageModule { - override def jpackageType = "pkg" + def jpackageType = "pkg" def assemblyRules = Seq( // all application.conf files will be concatenated into single file From 4eae4997012bb9b8efa7b29b76ec10342ebe0398 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:26:26 +0100 Subject: [PATCH 03/15] ci: fix lint errors --- example/javalib/module/16-jlink/build.mill | 2 +- .../module/16-jlink/foo/src/foo/Bar.java | 11 ++--- .../module/16-jlink/foo/src/module-info.java | 7 ++- example/javalib/module/17-jpackage/build.mill | 5 +-- scalalib/src/mill/scalalib/JavaModule.scala | 2 +- scalalib/src/mill/scalalib/JlinkModule.scala | 44 +++++++++++-------- .../src/mill/scalalib/JpackageModule.scala | 18 ++++---- 7 files changed, 48 insertions(+), 41 deletions(-) diff --git a/example/javalib/module/16-jlink/build.mill b/example/javalib/module/16-jlink/build.mill index e1ba7929b5d..ec1a491fdfe 100644 --- a/example/javalib/module/16-jlink/build.mill +++ b/example/javalib/module/16-jlink/build.mill @@ -20,4 +20,4 @@ object foo extends JavaModule with JlinkModule { Nov 27, 2024 7:21:12 PM foo.Bar main INFO: Hello World! -*/ \ No newline at end of file +*/ diff --git a/example/javalib/module/16-jlink/foo/src/foo/Bar.java b/example/javalib/module/16-jlink/foo/src/foo/Bar.java index 05a4a59718b..a87ef2b02ad 100644 --- a/example/javalib/module/16-jlink/foo/src/foo/Bar.java +++ b/example/javalib/module/16-jlink/foo/src/foo/Bar.java @@ -3,8 +3,9 @@ import java.util.logging.Logger; public class Bar { - private static final Logger LOG = Logger.getLogger(Bar.class.getName()); - public static void main(String[] args) { - LOG.info("Hello World!"); - } -} \ No newline at end of file + private static final Logger LOG = Logger.getLogger(Bar.class.getName()); + + public static void main(String[] args) { + LOG.info("Hello World!"); + } +} diff --git a/example/javalib/module/16-jlink/foo/src/module-info.java b/example/javalib/module/16-jlink/foo/src/module-info.java index 19d27adadab..17b00987f29 100644 --- a/example/javalib/module/16-jlink/foo/src/module-info.java +++ b/example/javalib/module/16-jlink/foo/src/module-info.java @@ -1,4 +1,3 @@ -module foo -{ - requires java.logging; -} \ No newline at end of file +module foo { + requires java.logging; +} diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index 44da679fb2c..669f543d3bb 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -11,11 +11,10 @@ object foo extends JavaModule with JpackageModule { // all application.conf files will be concatenated into single file Rule.Append("application.conf"), // all *.conf files will be concatenated into single file - Rule.AppendPattern(".*\\.conf"), + Rule.AppendPattern(".*\\.conf") ) } - /** Usage > mill foo.assembly @@ -52,4 +51,4 @@ Loaded application.conf from resources: Foo Application Conf > ./out/foo/jpackageAppImage.dest/image/foo/bin/foo Loaded application.conf from resources: Foo Application Conf -*/ \ No newline at end of file +*/ diff --git a/scalalib/src/mill/scalalib/JavaModule.scala b/scalalib/src/mill/scalalib/JavaModule.scala index 7df4a18a644..aabc71fd0c3 100644 --- a/scalalib/src/mill/scalalib/JavaModule.scala +++ b/scalalib/src/mill/scalalib/JavaModule.scala @@ -360,7 +360,7 @@ trait JavaModule T.traverse(transitiveModuleRunModuleDeps)(_.localClasspath)().flatten } - /** + /** * Almost the same as [[transitiveLocalClasspath]], but using the [[jar]]s instead of [[localClasspath]]. */ def transitiveJars: T[Seq[PathRef]] = Task { diff --git a/scalalib/src/mill/scalalib/JlinkModule.scala b/scalalib/src/mill/scalalib/JlinkModule.scala index f23da0f50f3..096741ac96c 100644 --- a/scalalib/src/mill/scalalib/JlinkModule.scala +++ b/scalalib/src/mill/scalalib/JlinkModule.scala @@ -5,7 +5,7 @@ import mill._ /** * Support building modular runtime images with the `jlink` tool, which is included in JDK 9 and later. - * + * * The official `jlink` docs: https://docs.oracle.com/en/java/javase/23/docs/specs/man/jlink.html */ trait JlinkModule extends JavaModule { @@ -22,13 +22,14 @@ trait JlinkModule extends JavaModule { /** The main class to use as the runtime entry point. */ def jlinkMainClass: T[String] = T { finalMainClass() } - /** Compress level for the runtime image. - * Valid values range between: + /** + * Compress level for the runtime image. + * Valid values range between: * "zip-0" (no compression) and "zip-9" (best compression) - * Defaults to "zip-6" */ + * Defaults to "zip-6" + */ def jlinkCompressLevel: T[String] = T { "zip-6" } - /** * Creates a Java module file (.jmod) from compiled classes */ @@ -44,15 +45,18 @@ trait JlinkModule extends JavaModule { os.copy(p, dest, createFolders = true) dest } - + val classPath = jars.map(_.toString).mkString(sys.props("path.separator")) val args = { val baseArgs = Seq( - "jmod", + "jmod", "create", - "--class-path", classPath.toString, - "--main-class", mainClass, - "--module-path", classPath.toString, + "--class-path", + classPath.toString, + "--main-class", + mainClass, + "--module-path", + classPath.toString, outputPath.toString ) @@ -61,11 +65,11 @@ trait JlinkModule extends JavaModule { } baseArgs ++ versionArgs - } + } os.proc(args).call() PathRef(outputPath) - } + } /** Builds a custom runtime image using jlink */ def jlinkAppImage: T[PathRef] = T { @@ -74,11 +78,16 @@ trait JlinkModule extends JavaModule { val args = Seq( "jlink", - "--launcher", s"${jlinkImageName()}=${jlinkModuleName()}/${jlinkMainClass()}", - "--module-path", modulePath, - "--add-modules", jlinkModuleName(), - "--output", outputPath.toString, - "--compress", jlinkCompressLevel().toString, + "--launcher", + s"${jlinkImageName()}=${jlinkModuleName()}/${jlinkMainClass()}", + "--module-path", + modulePath, + "--add-modules", + jlinkModuleName(), + "--output", + outputPath.toString, + "--compress", + jlinkCompressLevel().toString, "--no-header-files", "--no-man-pages" ) @@ -87,4 +96,3 @@ trait JlinkModule extends JavaModule { PathRef(outputPath) } } - diff --git a/scalalib/src/mill/scalalib/JpackageModule.scala b/scalalib/src/mill/scalalib/JpackageModule.scala index e82517f4b3a..d981c34b250 100644 --- a/scalalib/src/mill/scalalib/JpackageModule.scala +++ b/scalalib/src/mill/scalalib/JpackageModule.scala @@ -5,7 +5,7 @@ import mill._ /** * Support for building a native package / installer with the `jpackage` tool which comes bundled with JDK 14 and later. - * + * * The official `jpackage` docs: https://docs.oracle.com/en/java/javase/23/docs/specs/man/jpackage.html */ trait JpackageModule extends JavaModule { @@ -16,17 +16,17 @@ trait JpackageModule extends JavaModule { /** The main class to use as the entry point to the native package / installer. */ def jpackageMainClass: T[String] = T { finalMainClass() } - /** - * The type of native package / installer to be created. - * - * Valid values are: - * "app-image" - any OS + /** + * The type of native package / installer to be created. + * + * Valid values are: + * "app-image" - any OS * "dmg", "pkg" - macOS (native package, installer) * "exe", "msi" - Windows (native package, installer) * "rpm", "deb" - Linux - * - * If unspecified, defaults to "app-image" which will build a package native to the host platform. - * */ + * + * If unspecified, defaults to "app-image" which will build a package native to the host platform. + */ def jpackageType: T[String] = T { "app-image" } /** From aac366e6de742a97deeab9d3aab7c21e4ef2ca92 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:47:55 +0100 Subject: [PATCH 04/15] jpackage: update CLI example to have a Swing GUI --- example/javalib/module/17-jpackage/build.mill | 22 ++++++----- .../module/17-jpackage/foo/src/foo/Bar.java | 39 +++++++++++++++++++ .../module/17-jpackage/foo/src/foo/Foo.java | 8 +++- .../17-jpackage/foo/src/module-info.java | 4 ++ 4 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 example/javalib/module/17-jpackage/foo/src/foo/Bar.java create mode 100644 example/javalib/module/17-jpackage/foo/src/module-info.java diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index 669f543d3bb..15e20b6396f 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -23,7 +23,12 @@ object foo extends JavaModule with JpackageModule { Foo Application Conf > java -jar ./out/foo/assembly.dest/out.jar -Loaded application.conf from resources: Foo Application Conf +Nov 28, 2024 12:45:43 PM foo.Foo readConf +INFO: Loaded application.conf from resources: Foo Application Conf + +Nov 28, 2024 12:45:44 PM foo.Bar lambda$main$0 +INFO: Hello World application started successfully + > mill show foo.jpackageAppImage ".../out/foo/jpackageAppImage.dest/image" @@ -32,23 +37,22 @@ Loaded application.conf from resources: Foo Application Conf # jpackageType accepts 3 values on macOS: "dmg" or "pkg" or "app-image" (default). # macOS: jpackageType = "dmg" > ls -l ./out/foo/jpackageAppImage.dest/image --rw-r--r--@ 1 mac staff 6291968 Nov 25 15:14 foo-1.0.dmg +-rw-r--r--@ 1 mac staff 60686513 Nov 28 12:36 foo-1.0.dmg # macOS: jpackageType = "pkg" > ls -l ./out/foo/jpackageAppImage.dest/image total 106360 --rw-r--r-- 1 mac staff 54456037 Nov 25 15:19 foo-1.0.pkg +-rw-r--r-- 1 mac staff 54460727 Nov 28 12:40 foo-1.0.pkg # macOS: jpackageType = "app-image" > ls -l ./out/foo/jpackageAppImage.dest/image -drwxr-xr-x 3 mac staff 96 Nov 25 15:10 foo.app/ +drwxr-xr-x 3 mac staff 96 Nov 28 12:34 foo.app/ > ./out/foo/jpackageAppImage.dest/image/foo.app/Contents/MacOS/foo -Loaded application.conf from resources: Foo Application Conf - +Nov 28, 2024 12:31:46 PM foo.Foo readConf +INFO: Loaded application.conf from resources: Foo Application Conf -# Linux: jpackageType = "app-image" -> ./out/foo/jpackageAppImage.dest/image/foo/bin/foo -Loaded application.conf from resources: Foo Application Conf +Nov 28, 2024 12:31:46 PM foo.Bar lambda$main$0 +INFO: Hello World application started successfully */ diff --git a/example/javalib/module/17-jpackage/foo/src/foo/Bar.java b/example/javalib/module/17-jpackage/foo/src/foo/Bar.java new file mode 100644 index 00000000000..abd6248c94e --- /dev/null +++ b/example/javalib/module/17-jpackage/foo/src/foo/Bar.java @@ -0,0 +1,39 @@ +package foo; + +import javax.swing.*; +import java.awt.*; +import java.util.logging.Logger; + +public class Bar { + private static final Logger LOGGER = Logger.getLogger(Bar.class.getName()); + + public static void main(String[] args) { + // Use SwingUtilities.invokeLater to ensure thread safety + SwingUtilities.invokeLater(() -> { + try { + // Set a modern look and feel + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + // Create the main window + JFrame frame = new JFrame("Hello World"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(300, 200); + frame.setLocationRelativeTo(null); + + // Create a label from the application.conf + JLabel label = new JLabel(Foo.readConf(), SwingConstants.CENTER); + label.setFont(new Font("Arial", Font.BOLD, 16)); + + // Add the label to the frame + frame.getContentPane().add(label); + + // Make the frame visible + frame.setVisible(true); + + LOGGER.info("Hello World application started successfully"); + } catch (Exception e) { + LOGGER.severe("Error initializing application: " + e.getMessage()); + } + }); + } +} \ No newline at end of file diff --git a/example/javalib/module/17-jpackage/foo/src/foo/Foo.java b/example/javalib/module/17-jpackage/foo/src/foo/Foo.java index 87af722ed02..f7a8e9f9820 100644 --- a/example/javalib/module/17-jpackage/foo/src/foo/Foo.java +++ b/example/javalib/module/17-jpackage/foo/src/foo/Foo.java @@ -2,11 +2,15 @@ import java.io.IOException; import java.io.InputStream; +import java.util.logging.Logger; public class Foo { - public static void main(String[] args) throws IOException { + private static final Logger LOGGER = Logger.getLogger(Foo.class.getName()); + + public static String readConf() throws IOException { InputStream inputStream = Foo.class.getClassLoader().getResourceAsStream("application.conf"); String conf = new String(inputStream.readAllBytes()); - System.out.println("Loaded application.conf from resources: " + conf); + LOGGER.info("Loaded application.conf from resources: " + conf); + return conf; } } diff --git a/example/javalib/module/17-jpackage/foo/src/module-info.java b/example/javalib/module/17-jpackage/foo/src/module-info.java new file mode 100644 index 00000000000..57513ced7a6 --- /dev/null +++ b/example/javalib/module/17-jpackage/foo/src/module-info.java @@ -0,0 +1,4 @@ +module foo { + requires java.logging; + requires java.desktop; +} From 43410b4b7ca733a2fbe07e1677ef2a9530ec9cce Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Thu, 28 Nov 2024 22:57:05 +0100 Subject: [PATCH 05/15] use javaHome() and fix lint errors --- example/javalib/module/16-jlink/build.mill | 3 + .../module/17-jpackage/foo/src/foo/Bar.java | 66 +++++++++---------- scalalib/src/mill/scalalib/JlinkModule.scala | 15 +++-- .../src/mill/scalalib/JpackageModule.scala | 3 +- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/example/javalib/module/16-jlink/build.mill b/example/javalib/module/16-jlink/build.mill index ec1a491fdfe..9bb387e1181 100644 --- a/example/javalib/module/16-jlink/build.mill +++ b/example/javalib/module/16-jlink/build.mill @@ -7,10 +7,13 @@ import mill.scalalib.JlinkModule object foo extends JavaModule with JlinkModule { def jlinkModuleName: T[String] = T { "foo" } def jlinkModuleVersion: T[Option[String]] = T { Option("1.0") } + def jlinkCompressLevel: T[String] = T { "2" } } /** Usage +export JAVA_HOME=/Users/mac/.sdkman/candidates/java/17.0.9-oracle/ + > mill foo.jlinkAppImage > mill show foo.jlinkAppImage diff --git a/example/javalib/module/17-jpackage/foo/src/foo/Bar.java b/example/javalib/module/17-jpackage/foo/src/foo/Bar.java index abd6248c94e..c01499441c4 100644 --- a/example/javalib/module/17-jpackage/foo/src/foo/Bar.java +++ b/example/javalib/module/17-jpackage/foo/src/foo/Bar.java @@ -1,39 +1,39 @@ package foo; -import javax.swing.*; import java.awt.*; import java.util.logging.Logger; +import javax.swing.*; public class Bar { - private static final Logger LOGGER = Logger.getLogger(Bar.class.getName()); - - public static void main(String[] args) { - // Use SwingUtilities.invokeLater to ensure thread safety - SwingUtilities.invokeLater(() -> { - try { - // Set a modern look and feel - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - - // Create the main window - JFrame frame = new JFrame("Hello World"); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setSize(300, 200); - frame.setLocationRelativeTo(null); - - // Create a label from the application.conf - JLabel label = new JLabel(Foo.readConf(), SwingConstants.CENTER); - label.setFont(new Font("Arial", Font.BOLD, 16)); - - // Add the label to the frame - frame.getContentPane().add(label); - - // Make the frame visible - frame.setVisible(true); - - LOGGER.info("Hello World application started successfully"); - } catch (Exception e) { - LOGGER.severe("Error initializing application: " + e.getMessage()); - } - }); - } -} \ No newline at end of file + private static final Logger LOGGER = Logger.getLogger(Bar.class.getName()); + + public static void main(String[] args) { + // Use SwingUtilities.invokeLater to ensure thread safety + SwingUtilities.invokeLater(() -> { + try { + // Set a modern look and feel + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + // Create the main window + JFrame frame = new JFrame("Hello World"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(300, 200); + frame.setLocationRelativeTo(null); + + // Create a label from the application.conf + JLabel label = new JLabel(Foo.readConf(), SwingConstants.CENTER); + label.setFont(new Font("Arial", Font.BOLD, 16)); + + // Add the label to the frame + frame.getContentPane().add(label); + + // Make the frame visible + frame.setVisible(true); + + LOGGER.info("Hello World application started successfully"); + } catch (Exception e) { + LOGGER.severe("Error initializing application: " + e.getMessage()); + } + }); + } +} diff --git a/scalalib/src/mill/scalalib/JlinkModule.scala b/scalalib/src/mill/scalalib/JlinkModule.scala index 096741ac96c..ff0ebe0e48c 100644 --- a/scalalib/src/mill/scalalib/JlinkModule.scala +++ b/scalalib/src/mill/scalalib/JlinkModule.scala @@ -2,6 +2,7 @@ package mill package scalalib import mill._ +import mill.util.Jvm /** * Support building modular runtime images with the `jlink` tool, which is included in JDK 9 and later. @@ -24,9 +25,13 @@ trait JlinkModule extends JavaModule { /** * Compress level for the runtime image. - * Valid values range between: - * "zip-0" (no compression) and "zip-9" (best compression) - * Defaults to "zip-6" + * On newer versions of OpenJDK, valid values range between: + * "zip-0" (no compression) and "zip-9" (best compression). + * + * On all versions of Oracle's JDK, valid values range between: + * 0 (no compression), 1 (constant string sharing) and 2 (ZIP). + * + * Assumes you are on a recent OpenJDK version thus defaults to "zip-6". */ def jlinkCompressLevel: T[String] = T { "zip-6" } @@ -49,7 +54,7 @@ trait JlinkModule extends JavaModule { val classPath = jars.map(_.toString).mkString(sys.props("path.separator")) val args = { val baseArgs = Seq( - "jmod", + Jvm.jdkTool("jmod", this.zincWorker().javaHome().map(_.path)), "create", "--class-path", classPath.toString, @@ -77,7 +82,7 @@ trait JlinkModule extends JavaModule { val outputPath = T.dest / "jlink-runtime" val args = Seq( - "jlink", + Jvm.jdkTool("jlink", this.zincWorker().javaHome().map(_.path)), "--launcher", s"${jlinkImageName()}=${jlinkModuleName()}/${jlinkMainClass()}", "--module-path", diff --git a/scalalib/src/mill/scalalib/JpackageModule.scala b/scalalib/src/mill/scalalib/JpackageModule.scala index d981c34b250..af6c47908bd 100644 --- a/scalalib/src/mill/scalalib/JpackageModule.scala +++ b/scalalib/src/mill/scalalib/JpackageModule.scala @@ -2,6 +2,7 @@ package mill package scalalib import mill._ +import mill.util.Jvm /** * Support for building a native package / installer with the `jpackage` tool which comes bundled with JDK 14 and later. @@ -61,7 +62,7 @@ trait JpackageModule extends JavaModule { val mainJarName = jars.head.last val args: Seq[String] = Seq( - "jpackage", + Jvm.jdkTool("jpackage", this.zincWorker().javaHome().map(_.path)), "--type", appType, "--name", From a49193ddf51966ad6bba3419b516672854e6740a Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:12:18 +0100 Subject: [PATCH 06/15] expand jpackage documentation --- example/javalib/module/17-jpackage/build.mill | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index 15e20b6396f..001028c34a6 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -14,6 +14,57 @@ object foo extends JavaModule with JpackageModule { Rule.AppendPattern(".*\\.conf") ) } +//// SNIPPET:END + + +// This example illustrates how to use Mill to generate a native package/installer +// using the `jpackage` tool. + +// JPMS (Java Platform Module System) is a modern distribution format that was designed +// to avoid several of the shortcomings of the ubiquitous JAR format, especially "JAR Hell". + +// A defining characteristic of module-based Java applications based on the JPMS format +// is that a `module-info.java` must be defined at the root of the module’s source-file hierarchy. +// The `module-info.java` must explicitly list modules that it depends on, and also list +// packages that it exports, to make the integrity of these relationships easy to verify, +// both at compile-time and run-time. + +// Starting with version 14, the JDK now ships with the `jpackage` tool which can +// assemble any module-based Java application into a native package/installer. + +// The above build file expects the following project layout: +// +//// SNIPPET:TREE +// +// ---- +// build.mill +// foo/ +// src/ +// Foo.java +// Bar.java +// +// module-info.java +// ---- +// +//// SNIPPET:END + +// The build defines a `foo` module that uses the `trait JpackageModule`. +// (Note: Mill also uses the term `Module` for traits xref:fundamentals/modules.adoc[Trait Module]. +// This is not to be confused with Java application code structured as modules according to the JPMS format.) + +// The `JpackageModule` will infer most of the options needed to assemble a native +// package/installer, but you can still customize its output. In our example, we specified: + +// def jpackageType = "pkg" + +// This tells `jpackage` to generate a `.pkg`, which is the native installer format on macOS. +// Valid values on macOS are: `dmg`, `pkg` and `app-image`. + +// Note that `jpackage` doesn't not support cross-targeting. Cross-targeting in this +// context means the `jpackage` binary that is shipped with a JDK installation on macOS +// cannot be used to produce a binary package for another OS like Windows or Linux. + + /** Usage @@ -34,17 +85,17 @@ INFO: Hello World application started successfully ".../out/foo/jpackageAppImage.dest/image" -# jpackageType accepts 3 values on macOS: "dmg" or "pkg" or "app-image" (default). -# macOS: jpackageType = "dmg" +// jpackageType accepts 3 values on macOS: "dmg" or "pkg" or "app-image" (default). +// macOS: jpackageType = "dmg" > ls -l ./out/foo/jpackageAppImage.dest/image -rw-r--r--@ 1 mac staff 60686513 Nov 28 12:36 foo-1.0.dmg -# macOS: jpackageType = "pkg" +// macOS: jpackageType = "pkg" > ls -l ./out/foo/jpackageAppImage.dest/image total 106360 -rw-r--r-- 1 mac staff 54460727 Nov 28 12:40 foo-1.0.pkg -# macOS: jpackageType = "app-image" +// macOS: jpackageType = "app-image" > ls -l ./out/foo/jpackageAppImage.dest/image drwxr-xr-x 3 mac staff 96 Nov 28 12:34 foo.app/ From df16ae4ffda2e07e9029450611d332696f2ab9d6 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:41:57 +0100 Subject: [PATCH 07/15] expand jlink documentation --- example/javalib/module/16-jlink/build.mill | 32 +++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/example/javalib/module/16-jlink/build.mill b/example/javalib/module/16-jlink/build.mill index 9bb387e1181..98313ba8120 100644 --- a/example/javalib/module/16-jlink/build.mill +++ b/example/javalib/module/16-jlink/build.mill @@ -9,10 +9,40 @@ object foo extends JavaModule with JlinkModule { def jlinkModuleVersion: T[Option[String]] = T { Option("1.0") } def jlinkCompressLevel: T[String] = T { "2" } } +//// SNIPPET:END + +// This example illustrates how to use Mill to generate a runtime image using the `jlink` tool. + +// Most of the work is done by the `trait JlinkModule` in two steps: + +// 1. it uses the `jmod` tool to create a `jlink.jmod` file for the main Java module. +// The main Java module is typically the module containing the `mainClass`. +// You can explicitly specify a `mainClass` by adding this line to your build file: +// def mainClass: T[Option[String]] = { Option("com.foo.app.Main") } +// If no `mainClass` is not explicitly specified, `JlinkModule` will infer it from `JavaModule` which is its parent trait . +// See xref:javalib/module-config.adoc[Java Module Configuration] to learn more on how to influence the inference process. + + +// 2. it then uses the `jlink` tool, to link the previously created `jlink.jmod` with a runtime image. + + +// Note with respect to the `jlinkCompressLevel` option, the version of `jlink` that +// ships with the JDK distribution from Oracle JDK will only accept [`0`, `1`, `2`] +// as valid values for compression, with `0` being "no compression" +// and 2 being "zip compression". + +// On recent builds of OpenJDK and related distributions, `jlink` will accept those values +// but it will issue a deprecation warning. +// Valid values on OpenJDK range between: ["zip-0" - "zip-9"]. + + + + /** Usage -export JAVA_HOME=/Users/mac/.sdkman/candidates/java/17.0.9-oracle/ +// To use the Oracle JDK, first set your `JAVA_HOME` environment variable +// export JAVA_HOME=/Users/mac/.sdkman/candidates/java/17.0.9-oracle/ > mill foo.jlinkAppImage From 91122dde1e5d749d3a99633896b1e110bcddb7a7 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Sat, 30 Nov 2024 12:19:17 +0100 Subject: [PATCH 08/15] fix lint errors & minor edits --- example/javalib/module/16-jlink/build.mill | 26 ++++++--------- example/javalib/module/17-jpackage/build.mill | 32 +++++++++---------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/example/javalib/module/16-jlink/build.mill b/example/javalib/module/16-jlink/build.mill index 98313ba8120..11ed853dccd 100644 --- a/example/javalib/module/16-jlink/build.mill +++ b/example/javalib/module/16-jlink/build.mill @@ -15,30 +15,24 @@ object foo extends JavaModule with JlinkModule { // Most of the work is done by the `trait JlinkModule` in two steps: -// 1. it uses the `jmod` tool to create a `jlink.jmod` file for the main Java module. -// The main Java module is typically the module containing the `mainClass`. -// You can explicitly specify a `mainClass` by adding this line to your build file: -// def mainClass: T[Option[String]] = { Option("com.foo.app.Main") } -// If no `mainClass` is not explicitly specified, `JlinkModule` will infer it from `JavaModule` which is its parent trait . +// 1. it uses the `jmod` tool to create a `jlink.jmod` file for the main Java module. +// The main Java module is typically the module containing the `mainClass`. +// If your build file doesn't explicitly specify a `mainClass`, `JlinkModule` will infer it from `JavaModule`, which is its parent trait. // See xref:javalib/module-config.adoc[Java Module Configuration] to learn more on how to influence the inference process. - +// You can explicitly specify a `mainClass` like so in your build file: +// def mainClass: T[Option[String]] = { Option("com.foo.app.Main") } // 2. it then uses the `jlink` tool, to link the previously created `jlink.jmod` with a runtime image. +// NOTE: With respect to the `jlinkCompressLevel` option, the version of `jlink` that +// ships with the JDK distribution from Oracle will only accept [`0`, `1`, `2`] +// as valid values for compression, with `0` being "no compression" +// and 2 being "ZIP compression". -// Note with respect to the `jlinkCompressLevel` option, the version of `jlink` that -// ships with the JDK distribution from Oracle JDK will only accept [`0`, `1`, `2`] -// as valid values for compression, with `0` being "no compression" -// and 2 being "zip compression". - -// On recent builds of OpenJDK and related distributions, `jlink` will accept those values +// On recent builds of OpenJDK and derivative distributions, `jlink` will accept those values // but it will issue a deprecation warning. // Valid values on OpenJDK range between: ["zip-0" - "zip-9"]. - - - - /** Usage // To use the Oracle JDK, first set your `JAVA_HOME` environment variable diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index 001028c34a6..e97b6c36c4d 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -16,20 +16,19 @@ object foo extends JavaModule with JpackageModule { } //// SNIPPET:END +// This example illustrates how to use Mill to generate a native package/installer +// using the `jpackage` tool. -// This example illustrates how to use Mill to generate a native package/installer -// using the `jpackage` tool. - -// JPMS (Java Platform Module System) is a modern distribution format that was designed +// JPMS (Java Platform Module System) is a modern distribution format that was designed // to avoid several of the shortcomings of the ubiquitous JAR format, especially "JAR Hell". -// A defining characteristic of module-based Java applications based on the JPMS format -// is that a `module-info.java` must be defined at the root of the module’s source-file hierarchy. -// The `module-info.java` must explicitly list modules that it depends on, and also list -// packages that it exports, to make the integrity of these relationships easy to verify, +// A defining characteristic of module-based Java applications based on the JPMS format +// is that a `module-info.java` must be defined at the root of the module’s source-file hierarchy. +// The `module-info.java` must explicitly list modules that it depends on, and also list +// packages that it exports, to make the integrity of these relationships easy to verify, // both at compile-time and run-time. -// Starting with version 14, the JDK now ships with the `jpackage` tool which can +// Starting with version 14, the JDK now ships with the `jpackage` tool which can // assemble any module-based Java application into a native package/installer. // The above build file expects the following project layout: @@ -42,17 +41,18 @@ object foo extends JavaModule with JpackageModule { // src/ // Foo.java // Bar.java -// +// // module-info.java // ---- // //// SNIPPET:END // The build defines a `foo` module that uses the `trait JpackageModule`. -// (Note: Mill also uses the term `Module` for traits xref:fundamentals/modules.adoc[Trait Module]. -// This is not to be confused with Java application code structured as modules according to the JPMS format.) -// The `JpackageModule` will infer most of the options needed to assemble a native +// NOTE: Mill also uses the term `Module` for traits xref:fundamentals/modules.adoc[Trait Module]. +// This is not to be confused with Java application code structured as modules according to the JPMS format. + +// The `JpackageModule` trait will infer most of the options needed to assemble a native // package/installer, but you can still customize its output. In our example, we specified: // def jpackageType = "pkg" @@ -60,12 +60,10 @@ object foo extends JavaModule with JpackageModule { // This tells `jpackage` to generate a `.pkg`, which is the native installer format on macOS. // Valid values on macOS are: `dmg`, `pkg` and `app-image`. -// Note that `jpackage` doesn't not support cross-targeting. Cross-targeting in this -// context means the `jpackage` binary that is shipped with a JDK installation on macOS +// NOTE: `jpackage` doesn't not support cross-targeting. Cross-targeting in this +// context means the `jpackage` binary shipped with a macOS JDK // cannot be used to produce a binary package for another OS like Windows or Linux. - - /** Usage > mill foo.assembly From 9725696efc543a17f2a944ba06b7a1d599a1418e Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Sat, 30 Nov 2024 17:28:22 +0100 Subject: [PATCH 09/15] docs: fix layout, re-wording and lint errors --- .../ROOT/pages/javalib/module-config.adoc | 8 ++++ example/javalib/module/16-jlink/build.mill | 29 ++++++++------ example/javalib/module/17-jpackage/build.mill | 40 +++++++++---------- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/docs/modules/ROOT/pages/javalib/module-config.adoc b/docs/modules/ROOT/pages/javalib/module-config.adoc index 2a3ea2a66d1..ce0aa429d5a 100644 --- a/docs/modules/ROOT/pages/javalib/module-config.adoc +++ b/docs/modules/ROOT/pages/javalib/module-config.adoc @@ -56,3 +56,11 @@ include::partial$example/javalib/module/3-override-tasks.adoc[] == Native C Code with JNI include::partial$example/javalib/module/15-jni.adoc[] + +== Java App and JVM Bundle using jlink + +include::partial$example/javalib/module/16-jlink.adoc[] + +== Java App, JVM Bundle and Installer using jpackage + +include::partial$example/javalib/module/17-jpackage.adoc[] diff --git a/example/javalib/module/16-jlink/build.mill b/example/javalib/module/16-jlink/build.mill index 11ed853dccd..720d4bcdec8 100644 --- a/example/javalib/module/16-jlink/build.mill +++ b/example/javalib/module/16-jlink/build.mill @@ -1,3 +1,6 @@ +// This example illustrates how to use Mill to generate a runtime image using the `jlink` tool. +// Starting with JDK 9, `jlink` bundles Java app code with a stripped-down version of the JVM. + //// SNIPPET:BUILD package build import mill._, javalib._ @@ -11,31 +14,33 @@ object foo extends JavaModule with JlinkModule { } //// SNIPPET:END -// This example illustrates how to use Mill to generate a runtime image using the `jlink` tool. - // Most of the work is done by the `trait JlinkModule` in two steps: -// 1. it uses the `jmod` tool to create a `jlink.jmod` file for the main Java module. +// 1.0. it uses the `jmod` tool to create a `jlink.jmod` file for the main Java module. // The main Java module is typically the module containing the `mainClass`. + // If your build file doesn't explicitly specify a `mainClass`, `JlinkModule` will infer it from `JavaModule`, which is its parent trait. // See xref:javalib/module-config.adoc[Java Module Configuration] to learn more on how to influence the inference process. // You can explicitly specify a `mainClass` like so in your build file: + +//// SNIPPET:BUILD // def mainClass: T[Option[String]] = { Option("com.foo.app.Main") } +//// SNIPPET:END + +// 2.0. it then uses the `jlink` tool, to link the previously created `jlink.jmod` with a runtime image. -// 2. it then uses the `jlink` tool, to link the previously created `jlink.jmod` with a runtime image. +// With respect to the `jlinkCompressLevel` option, on recent builds of OpenJDK and its descendants, +// `jlink` will accept [`0`, `1`, `2`] but it will issue a deprecation warning. +// Valid values on OpenJDK range between: ["zip-0" - "zip-9"]. -// NOTE: With respect to the `jlinkCompressLevel` option, the version of `jlink` that -// ships with the JDK distribution from Oracle will only accept [`0`, `1`, `2`] +// NOTE: The version of `jlink` that ships with the Oracle JDK will only accept [`0`, `1`, `2`] // as valid values for compression, with `0` being "no compression" // and 2 being "ZIP compression". -// On recent builds of OpenJDK and derivative distributions, `jlink` will accept those values -// but it will issue a deprecation warning. -// Valid values on OpenJDK range between: ["zip-0" - "zip-9"]. - /** Usage -// To use the Oracle JDK, first set your `JAVA_HOME` environment variable +// To use a specific JDK, first set your `JAVA_HOME` environment variable prior to running the build. + // export JAVA_HOME=/Users/mac/.sdkman/candidates/java/17.0.9-oracle/ > mill foo.jlinkAppImage @@ -44,7 +49,7 @@ object foo extends JavaModule with JlinkModule { ".../out/foo/jlinkAppImage.dest/jlink-runtime" > ./out/foo/jlinkAppImage.dest/jlink-runtime/bin/jlink -Nov 27, 2024 7:21:12 PM foo.Bar main +... foo.Bar main INFO: Hello World! */ diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index e97b6c36c4d..537d28871ee 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -1,3 +1,6 @@ +// This example illustrates how to use Mill to generate a native package/installer +// using the `jpackage` tool. + //// SNIPPET:BUILD package build import mill._, javalib._ @@ -16,19 +19,16 @@ object foo extends JavaModule with JpackageModule { } //// SNIPPET:END -// This example illustrates how to use Mill to generate a native package/installer -// using the `jpackage` tool. - // JPMS (Java Platform Module System) is a modern distribution format that was designed // to avoid several of the shortcomings of the ubiquitous JAR format, especially "JAR Hell". // A defining characteristic of module-based Java applications based on the JPMS format -// is that a `module-info.java` must be defined at the root of the module’s source-file hierarchy. +// is that a `module-info.java` must be defined at the root of the module’s source file hierarchy. // The `module-info.java` must explicitly list modules that it depends on, and also list // packages that it exports, to make the integrity of these relationships easy to verify, // both at compile-time and run-time. -// Starting with version 14, the JDK now ships with the `jpackage` tool which can +// Starting with version 14, the JDK ships with the `jpackage` tool which can // assemble any module-based Java application into a native package/installer. // The above build file expects the following project layout: @@ -49,8 +49,8 @@ object foo extends JavaModule with JpackageModule { // The build defines a `foo` module that uses the `trait JpackageModule`. -// NOTE: Mill also uses the term `Module` for traits xref:fundamentals/modules.adoc[Trait Module]. -// This is not to be confused with Java application code structured as modules according to the JPMS format. +// NOTE: The term `Module` is also used in Mill to refer to xref:fundamentals/modules.adoc[traits]. +// This is not to be confused with Java app code structured as modules according to the JPMS format. // The `JpackageModule` trait will infer most of the options needed to assemble a native // package/installer, but you can still customize its output. In our example, we specified: @@ -62,7 +62,7 @@ object foo extends JavaModule with JpackageModule { // NOTE: `jpackage` doesn't not support cross-targeting. Cross-targeting in this // context means the `jpackage` binary shipped with a macOS JDK -// cannot be used to produce a binary package for another OS like Windows or Linux. +// cannot be used to produce a native installer for another OS like Windows or Linux. /** Usage @@ -72,10 +72,10 @@ object foo extends JavaModule with JpackageModule { Foo Application Conf > java -jar ./out/foo/assembly.dest/out.jar -Nov 28, 2024 12:45:43 PM foo.Foo readConf +... foo.Foo readConf INFO: Loaded application.conf from resources: Foo Application Conf -Nov 28, 2024 12:45:44 PM foo.Bar lambda$main$0 +... foo.Bar ... INFO: Hello World application started successfully @@ -83,25 +83,25 @@ INFO: Hello World application started successfully ".../out/foo/jpackageAppImage.dest/image" -// jpackageType accepts 3 values on macOS: "dmg" or "pkg" or "app-image" (default). -// macOS: jpackageType = "dmg" +// On macOS, `jpackageType` accepts 3 values: "dmg" or "pkg" or "app-image" (default). + +// Setting `jpackageType = "dmg"` will produce: > ls -l ./out/foo/jpackageAppImage.dest/image --rw-r--r--@ 1 mac staff 60686513 Nov 28 12:36 foo-1.0.dmg +... foo-1.0.dmg -// macOS: jpackageType = "pkg" +// Setting `jpackageType = "pkg"` will produce: > ls -l ./out/foo/jpackageAppImage.dest/image -total 106360 --rw-r--r-- 1 mac staff 54460727 Nov 28 12:40 foo-1.0.pkg +... foo-1.0.pkg -// macOS: jpackageType = "app-image" +// Setting `jpackageType = "app-image"` will produce: > ls -l ./out/foo/jpackageAppImage.dest/image -drwxr-xr-x 3 mac staff 96 Nov 28 12:34 foo.app/ +... foo.app/ > ./out/foo/jpackageAppImage.dest/image/foo.app/Contents/MacOS/foo -Nov 28, 2024 12:31:46 PM foo.Foo readConf +... foo.Foo readConf INFO: Loaded application.conf from resources: Foo Application Conf -Nov 28, 2024 12:31:46 PM foo.Bar lambda$main$0 +... foo.Bar ... INFO: Hello World application started successfully */ From 7102a4bd5dbe9b58e6bf51fd7f348bb1093faadd Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Sun, 1 Dec 2024 04:10:41 +0100 Subject: [PATCH 10/15] fix internal ASCIIDOC xref --- docs/modules/ROOT/pages/javalib/module-config.adoc | 2 +- example/javalib/module/16-jlink/build.mill | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/javalib/module-config.adoc b/docs/modules/ROOT/pages/javalib/module-config.adoc index ce0aa429d5a..88a5e4f545a 100644 --- a/docs/modules/ROOT/pages/javalib/module-config.adoc +++ b/docs/modules/ROOT/pages/javalib/module-config.adoc @@ -34,7 +34,7 @@ include::partial$example/javalib/module/8-annotation-processors.adoc[] include::partial$example/javalib/module/9-docjar.adoc[] - +[[specifying-main-class]] == Specifying the Main Class include::partial$example/javalib/module/11-main-class.adoc[] diff --git a/example/javalib/module/16-jlink/build.mill b/example/javalib/module/16-jlink/build.mill index 720d4bcdec8..6aae51422cc 100644 --- a/example/javalib/module/16-jlink/build.mill +++ b/example/javalib/module/16-jlink/build.mill @@ -20,7 +20,7 @@ object foo extends JavaModule with JlinkModule { // The main Java module is typically the module containing the `mainClass`. // If your build file doesn't explicitly specify a `mainClass`, `JlinkModule` will infer it from `JavaModule`, which is its parent trait. -// See xref:javalib/module-config.adoc[Java Module Configuration] to learn more on how to influence the inference process. +// See xref:javalib/module-config.adoc#specifying-main-class[Specifying the Main Class] to learn more on how to influence the inference process. // You can explicitly specify a `mainClass` like so in your build file: //// SNIPPET:BUILD From b913aeea0ce1f03116c9658f3ccbf1cd025d8100 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Sun, 1 Dec 2024 04:14:06 +0100 Subject: [PATCH 11/15] ci: fix as there is no macOS runner --- example/javalib/module/17-jpackage/build.mill | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index 537d28871ee..1b626ed3ec0 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -86,22 +86,22 @@ INFO: Hello World application started successfully // On macOS, `jpackageType` accepts 3 values: "dmg" or "pkg" or "app-image" (default). // Setting `jpackageType = "dmg"` will produce: -> ls -l ./out/foo/jpackageAppImage.dest/image -... foo-1.0.dmg +// > ls -l ./out/foo/jpackageAppImage.dest/image +// ... foo-1.0.dmg // Setting `jpackageType = "pkg"` will produce: -> ls -l ./out/foo/jpackageAppImage.dest/image -... foo-1.0.pkg +// > ls -l ./out/foo/jpackageAppImage.dest/image +// ... foo-1.0.pkg // Setting `jpackageType = "app-image"` will produce: -> ls -l ./out/foo/jpackageAppImage.dest/image -... foo.app/ +// > ls -l ./out/foo/jpackageAppImage.dest/image +// ... foo.app/ > ./out/foo/jpackageAppImage.dest/image/foo.app/Contents/MacOS/foo -... foo.Foo readConf -INFO: Loaded application.conf from resources: Foo Application Conf +// ... foo.Foo readConf +// INFO: Loaded application.conf from resources: Foo Application Conf -... foo.Bar ... -INFO: Hello World application started successfully +// ... foo.Bar ... +// INFO: Hello World application started successfully */ From 05526250bf31b2fea04adb89cb5bccb754e44082 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Sun, 1 Dec 2024 04:51:02 +0100 Subject: [PATCH 12/15] tidy up formatting --- example/javalib/module/17-jpackage/build.mill | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index 1b626ed3ec0..d1997b5b24e 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -81,27 +81,29 @@ INFO: Hello World application started successfully > mill show foo.jpackageAppImage ".../out/foo/jpackageAppImage.dest/image" - +*/ // On macOS, `jpackageType` accepts 3 values: "dmg" or "pkg" or "app-image" (default). -// Setting `jpackageType = "dmg"` will produce: -// > ls -l ./out/foo/jpackageAppImage.dest/image +// Setting `def jpackageType = "dmg"` will produce: +// ---- +// ls -l ./out/foo/jpackageAppImage.dest/image // ... foo-1.0.dmg +// ---- -// Setting `jpackageType = "pkg"` will produce: -// > ls -l ./out/foo/jpackageAppImage.dest/image +// Setting `def jpackageType = "pkg"` will produce: +// ---- +// ls -l ./out/foo/jpackageAppImage.dest/image // ... foo-1.0.pkg +// ---- -// Setting `jpackageType = "app-image"` will produce: -// > ls -l ./out/foo/jpackageAppImage.dest/image +// Setting `def jpackageType = "app-image"` will produce: +// ---- +// ls -l ./out/foo/jpackageAppImage.dest/image // ... foo.app/ - -> ./out/foo/jpackageAppImage.dest/image/foo.app/Contents/MacOS/foo +// ./out/foo/jpackageAppImage.dest/image/foo.app/Contents/MacOS/foo // ... foo.Foo readConf // INFO: Loaded application.conf from resources: Foo Application Conf - // ... foo.Bar ... // INFO: Hello World application started successfully - -*/ +// ---- \ No newline at end of file From b45d5bcfd0c495241fc8bc93bf638f71c283bae8 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Sun, 1 Dec 2024 07:33:58 +0100 Subject: [PATCH 13/15] ci fix --- example/javalib/module/17-jpackage/build.mill | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index d1997b5b24e..cab39011c79 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -68,8 +68,8 @@ object foo extends JavaModule with JpackageModule { > mill foo.assembly -> unzip -p ./out/foo/assembly.dest/out.jar application.conf -Foo Application Conf +> mill show foo.assembly +".../out/foo/assembly.dest/out.jar" > java -jar ./out/foo/assembly.dest/out.jar ... foo.Foo readConf @@ -106,4 +106,4 @@ INFO: Hello World application started successfully // INFO: Loaded application.conf from resources: Foo Application Conf // ... foo.Bar ... // INFO: Hello World application started successfully -// ---- \ No newline at end of file +// ---- From 7166a24811fd9784fc54130dced51fa6fbe7c883 Mon Sep 17 00:00:00 2001 From: ayewo <20957603+ayewo@users.noreply.github.com> Date: Sun, 1 Dec 2024 16:40:37 +0100 Subject: [PATCH 14/15] fix ci ... --- example/javalib/module/17-jpackage/build.mill | 6 ++-- .../foo/resources/application.conf | 2 +- .../module/17-jpackage/foo/src/foo/Bar.java | 35 +++++++++++++++++-- .../module/17-jpackage/foo/src/foo/Foo.java | 18 +++++++++- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/example/javalib/module/17-jpackage/build.mill b/example/javalib/module/17-jpackage/build.mill index cab39011c79..ff88bb8bc8f 100644 --- a/example/javalib/module/17-jpackage/build.mill +++ b/example/javalib/module/17-jpackage/build.mill @@ -8,7 +8,7 @@ import mill.javalib.Assembly._ import mill.scalalib.JpackageModule object foo extends JavaModule with JpackageModule { - def jpackageType = "pkg" + def jpackageType = "app-image" def assemblyRules = Seq( // all application.conf files will be concatenated into single file @@ -72,12 +72,10 @@ object foo extends JavaModule with JpackageModule { ".../out/foo/assembly.dest/out.jar" > java -jar ./out/foo/assembly.dest/out.jar -... foo.Foo readConf INFO: Loaded application.conf from resources: Foo Application Conf - -... foo.Bar ... INFO: Hello World application started successfully +> mill foo.jpackageAppImage > mill show foo.jpackageAppImage ".../out/foo/jpackageAppImage.dest/image" diff --git a/example/javalib/module/17-jpackage/foo/resources/application.conf b/example/javalib/module/17-jpackage/foo/resources/application.conf index 482e5cf3347..4f562e845c8 100644 --- a/example/javalib/module/17-jpackage/foo/resources/application.conf +++ b/example/javalib/module/17-jpackage/foo/resources/application.conf @@ -1 +1 @@ -Foo Application Conf +Foo Application Conf \ No newline at end of file diff --git a/example/javalib/module/17-jpackage/foo/src/foo/Bar.java b/example/javalib/module/17-jpackage/foo/src/foo/Bar.java index c01499441c4..e0e623e8f9a 100644 --- a/example/javalib/module/17-jpackage/foo/src/foo/Bar.java +++ b/example/javalib/module/17-jpackage/foo/src/foo/Bar.java @@ -1,13 +1,42 @@ package foo; import java.awt.*; -import java.util.logging.Logger; +import java.io.IOException; import javax.swing.*; +import static foo.Foo.LOGGER; public class Bar { - private static final Logger LOGGER = Logger.getLogger(Bar.class.getName()); - public static void main(String[] args) { + public static boolean isCI() { + String[] ciEnvironments = { + "CI", + "CONTINUOUS_INTEGRATION", + "JENKINS_URL", + "TRAVIS", + "CIRCLECI", + "GITHUB_ACTIONS", + "GITLAB_CI", + "BITBUCKET_PIPELINE", + "TEAMCITY_VERSION" + }; + + for (String env : ciEnvironments) { + if (System.getenv(env) != null) { + return true; + } + } + + return false; + } + + public static void main(String[] args) throws IOException { + // Needed because Swing GUIs don't work in headless CI environments + if (isCI()) { + Foo.readConf(); + LOGGER.info("Hello World application started successfully"); + System.exit(0); + } + // Use SwingUtilities.invokeLater to ensure thread safety SwingUtilities.invokeLater(() -> { try { diff --git a/example/javalib/module/17-jpackage/foo/src/foo/Foo.java b/example/javalib/module/17-jpackage/foo/src/foo/Foo.java index f7a8e9f9820..f839da85062 100644 --- a/example/javalib/module/17-jpackage/foo/src/foo/Foo.java +++ b/example/javalib/module/17-jpackage/foo/src/foo/Foo.java @@ -2,10 +2,26 @@ import java.io.IOException; import java.io.InputStream; +import java.util.logging.Handler; +import java.util.logging.LogRecord; import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; public class Foo { - private static final Logger LOGGER = Logger.getLogger(Foo.class.getName()); + public static final Logger LOGGER = Logger.getLogger(Foo.class.getName()); + + static { + // Configure the logger to use a custom formatter + for (Handler handler : LOGGER.getParent().getHandlers()) { + handler.setFormatter(new SimpleFormatter() { + @Override + public String format(LogRecord record) { + // Return the log level, message, but omit the timestamp + return String.format("%s: %s%n", record.getLevel(), record.getMessage()); + } + }); + } + } public static String readConf() throws IOException { InputStream inputStream = Foo.class.getClassLoader().getResourceAsStream("application.conf"); From efd7c4d1394157285a73cab0d42f16d6da651322 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 2 Dec 2024 10:19:51 +0800 Subject: [PATCH 15/15] autoformat --- .../module/17-jpackage/foo/src/foo/Bar.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/example/javalib/module/17-jpackage/foo/src/foo/Bar.java b/example/javalib/module/17-jpackage/foo/src/foo/Bar.java index e0e623e8f9a..510bb668dd7 100644 --- a/example/javalib/module/17-jpackage/foo/src/foo/Bar.java +++ b/example/javalib/module/17-jpackage/foo/src/foo/Bar.java @@ -1,29 +1,30 @@ package foo; +import static foo.Foo.LOGGER; + import java.awt.*; import java.io.IOException; import javax.swing.*; -import static foo.Foo.LOGGER; public class Bar { public static boolean isCI() { String[] ciEnvironments = { - "CI", - "CONTINUOUS_INTEGRATION", - "JENKINS_URL", - "TRAVIS", - "CIRCLECI", - "GITHUB_ACTIONS", - "GITLAB_CI", - "BITBUCKET_PIPELINE", - "TEAMCITY_VERSION" + "CI", + "CONTINUOUS_INTEGRATION", + "JENKINS_URL", + "TRAVIS", + "CIRCLECI", + "GITHUB_ACTIONS", + "GITLAB_CI", + "BITBUCKET_PIPELINE", + "TEAMCITY_VERSION" }; for (String env : ciEnvironments) { - if (System.getenv(env) != null) { - return true; - } + if (System.getenv(env) != null) { + return true; + } } return false;