Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jlink & jpackage Java examples #4038

Merged
merged 17 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions example/javalib/module/16-jlink/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//// SNIPPET:BUILD
package build
import mill._, javalib._
import mill.javalib.Assembly._
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" }
}
//// 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`.
// 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".

// 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
// export JAVA_HOME=/Users/mac/.sdkman/candidates/java/17.0.9-oracle/

> 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!

*/
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bar Application Conf
11 changes: 11 additions & 0 deletions example/javalib/module/16-jlink/foo/src/foo/Bar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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!");
}
}
3 changes: 3 additions & 0 deletions example/javalib/module/16-jlink/foo/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module foo {
requires java.logging;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bar Application Conf
107 changes: 107 additions & 0 deletions example/javalib/module/17-jpackage/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//// SNIPPET:BUILD
package build
import mill._, javalib._
import mill.javalib.Assembly._
import mill.scalalib.JpackageModule

object foo extends JavaModule with JpackageModule {
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")
)
}
//// 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` 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"

// 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: `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

> unzip -p ./out/foo/assembly.dest/out.jar application.conf
Foo Application Conf

> java -jar ./out/foo/assembly.dest/out.jar
Nov 28, 2024 12:45:43 PM foo.Foo readConf
ayewo marked this conversation as resolved.
Show resolved Hide resolved
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"


// 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"
> 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"
> ls -l ./out/foo/jpackageAppImage.dest/image
drwxr-xr-x 3 mac staff 96 Nov 28 12:34 foo.app/

> ./out/foo/jpackageAppImage.dest/image/foo.app/Contents/MacOS/foo
Nov 28, 2024 12:31:46 PM foo.Foo readConf
INFO: 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

*/
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Foo Application Conf
39 changes: 39 additions & 0 deletions example/javalib/module/17-jpackage/foo/src/foo/Bar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package foo;

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());
}
});
}
}
16 changes: 16 additions & 0 deletions example/javalib/module/17-jpackage/foo/src/foo/Foo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package foo;

import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;

public class Foo {
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());
LOGGER.info("Loaded application.conf from resources: " + conf);
return conf;
}
}
4 changes: 4 additions & 0 deletions example/javalib/module/17-jpackage/foo/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module foo {
requires java.logging;
requires java.desktop;
}
7 changes: 7 additions & 0 deletions scalalib/src/mill/scalalib/JavaModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
103 changes: 103 additions & 0 deletions scalalib/src/mill/scalalib/JlinkModule.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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.
*
* 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.
* 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" }
ayewo marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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(
Jvm.jdkTool("jmod", this.zincWorker().javaHome().map(_.path)),
"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(
Jvm.jdkTool("jlink", this.zincWorker().javaHome().map(_.path)),
"--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)
}
}
Loading
Loading