-
-
Notifications
You must be signed in to change notification settings - Fork 364
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add jlink & jpackage Java examples (#4038)
This intended to close #3638. The `JlinkModule` creates a runtime image in two steps: 1. it creates a `jlink.jmod` file for the module containing the `mainClass` using the [`jmod`](https://docs.oracle.com/en/java/javase/23/docs/specs/man/jmod.html) tool; 2. it then uses the [jlink](https://docs.oracle.com/en/java/javase/23/docs/specs/man/jlink.html) tool, to link the previously created `jlink.jmod` with a runtime image. The generated runtime image can be executed with: ```sh ./out/foo/jlinkAppImage.dest/jlink-runtime/bin/jlink ``` As for the `JpackageModule` implementation, I decided to reuse most of the `JpackageModule` code @lefou [linked](#3201) to the original issue. Of course his code completely avoids the `jmod` dance by using [`jpackage`](https://docs.oracle.com/en/java/javase/23/docs/specs/man/jpackage.html) to create a native package/installer directly. `jpackage` supports up to 3 different package/installer outputs on macOS: ```sh jpackageType = "dmg" ./out/foo/jpackageAppImage.dest/image/foo-1.0.dmg jpackageType = "pkg" ./out/foo/jpackageAppImage.dest/image/foo-1.0.pkg jpackageType = "app-image" ./out/foo/jpackageAppImage.dest/image/foo.app ``` Would be interested to hear your thoughts on the slight difference between how the 2 traits are implemented. --------- Co-authored-by: Li Haoyi <haoyi.sg@gmail.com>
- Loading branch information
Showing
14 changed files
with
487 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// 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._ | ||
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 | ||
|
||
// Most of the work is done by the `trait JlinkModule` in two steps: | ||
|
||
// 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#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 | ||
// 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. | ||
|
||
// 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: 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". | ||
|
||
/** Usage | ||
|
||
// 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 | ||
|
||
> mill show foo.jlinkAppImage | ||
".../out/foo/jlinkAppImage.dest/jlink-runtime" | ||
|
||
> ./out/foo/jlinkAppImage.dest/jlink-runtime/bin/jlink | ||
... foo.Bar main | ||
INFO: Hello World! | ||
|
||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Bar Application Conf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module foo { | ||
requires java.logging; | ||
} |
1 change: 1 addition & 0 deletions
1
example/javalib/module/17-jpackage/bar/resources/application.conf
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Bar Application Conf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// This example illustrates how to use Mill to generate a native package/installer | ||
// using the `jpackage` tool. | ||
|
||
//// SNIPPET:BUILD | ||
package build | ||
import mill._, javalib._ | ||
import mill.javalib.Assembly._ | ||
import mill.scalalib.JpackageModule | ||
|
||
object foo extends JavaModule with JpackageModule { | ||
def jpackageType = "app-image" | ||
|
||
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 | ||
|
||
// 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 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: 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: | ||
|
||
// 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 native installer for another OS like Windows or Linux. | ||
|
||
/** Usage | ||
|
||
> mill foo.assembly | ||
|
||
> mill show foo.assembly | ||
".../out/foo/assembly.dest/out.jar" | ||
|
||
> java -jar ./out/foo/assembly.dest/out.jar | ||
INFO: Loaded application.conf from resources: Foo Application Conf | ||
INFO: Hello World application started successfully | ||
|
||
> mill foo.jpackageAppImage | ||
|
||
> mill show foo.jpackageAppImage | ||
".../out/foo/jpackageAppImage.dest/image" | ||
*/ | ||
|
||
// On macOS, `jpackageType` accepts 3 values: "dmg" or "pkg" or "app-image" (default). | ||
|
||
// Setting `def jpackageType = "dmg"` will produce: | ||
// ---- | ||
// ls -l ./out/foo/jpackageAppImage.dest/image | ||
// ... foo-1.0.dmg | ||
// ---- | ||
|
||
// Setting `def jpackageType = "pkg"` will produce: | ||
// ---- | ||
// ls -l ./out/foo/jpackageAppImage.dest/image | ||
// ... foo-1.0.pkg | ||
// ---- | ||
|
||
// 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 | ||
// ... foo.Foo readConf | ||
// INFO: Loaded application.conf from resources: Foo Application Conf | ||
// ... foo.Bar ... | ||
// INFO: Hello World application started successfully | ||
// ---- |
1 change: 1 addition & 0 deletions
1
example/javalib/module/17-jpackage/foo/resources/application.conf
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Foo Application Conf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package foo; | ||
|
||
import static foo.Foo.LOGGER; | ||
|
||
import java.awt.*; | ||
import java.io.IOException; | ||
import javax.swing.*; | ||
|
||
public class Bar { | ||
|
||
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 { | ||
// 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()); | ||
} | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package foo; | ||
|
||
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 { | ||
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"); | ||
String conf = new String(inputStream.readAllBytes()); | ||
LOGGER.info("Loaded application.conf from resources: " + conf); | ||
return conf; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
module foo { | ||
requires java.logging; | ||
requires java.desktop; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.