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 all 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
10 changes: 9 additions & 1 deletion docs/modules/ROOT/pages/javalib/module-config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -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[]
55 changes: 55 additions & 0 deletions example/javalib/module/16-jlink/build.mill
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!

*/
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 @@
// 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
// ----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Foo Application Conf
69 changes: 69 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,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());
}
});
}
}
32 changes: 32 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,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;
}
}
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
Loading
Loading