Skip to content

Commit

Permalink
add jlink & jpackage Java examples
Browse files Browse the repository at this point in the history
Co-authored-by: Tobias Roeser <[email protected]>
  • Loading branch information
ayewo committed Nov 28, 2024
1 parent 41e21e0 commit 9208ba6
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 0 deletions.
23 changes: 23 additions & 0 deletions example/javalib/module/16-jlink/build.mill
Original file line number Diff line number Diff line change
@@ -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!

*/
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bar Application Conf
10 changes: 10 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,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!");
}
}
4 changes: 4 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,4 @@
module foo
{
requires java.logging;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bar Application Conf
55 changes: 55 additions & 0 deletions example/javalib/module/17-jpackage/build.mill
Original file line number Diff line number Diff line change
@@ -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

*/
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Foo Application Conf
12 changes: 12 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,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);
}
}
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
90 changes: 90 additions & 0 deletions scalalib/src/mill/scalalib/JlinkModule.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}

83 changes: 83 additions & 0 deletions scalalib/src/mill/scalalib/JpackageModule.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 9208ba6

Please sign in to comment.