August 5, 2024

Mill Build for Java Devs

Mill has its origin in the Scala world. However, it is well suited to build Java projects. Recently the official Mill documentation gained a growing Java section with many examples. So, I’m keeping this post short, as the official documentation has more information than I can cover.

You might be scared that Mill builds are written in Scala. Luckily, Mill is conservative with its Scala use to constructs most Java developers are already familiar with.

When to Consider Mill?

As I mentioned in my first Mill build, I do not recommend to default to Mill for projects which build well with Maven. Start considering Mill when feel like you are working "against" Maven instead of with Maven. Usually this arises if there are multiple aspects in you build that do fit well into a Maven lifecycle:

  • Build (many) parts with other tools. Examples:

    • Compile a web front-end and package

    • Build native binaries

    • Niche tooling, company internal tooling which is part of the build

  • Build the same source against different combination of libraries.

  • Complex pipeline which generates more resources or source code.

  • Complex assembling of things way after the Java things are build.

  • You benefit from caching for many steps.

  • The build speed with Maven is an issue.

Of course you pay a price. The biggest drawbacks:

  • IDE support is weak. However, add some complex Maven plugin setup and your IDE also struggles.

  • Mill is not declarative at all.

  • Changing the build file requires compiling it.

  • You must learn some Scala. However, other build tools might also bring their own language. Ex. Groovy/Kotlin in Gradle

Scala of the Whirlwind Tour Explained

Here is a bit more Scala background for the example in the whirlwind tour:

build.sc:
import mill._  // These are imports. In Java this would be import mill.*
import scalalib._

// A object is an object, yes. Its a direct way to create a single object, similar to a Singleton.
// 'with' is the 'implements' in Java.
object demo extends RootModule with JavaModule{
    // A 'def' declares a method. In Mill a method will be available as Task
    // The 'Agg' is class from mill to aggregate things. Think `new Agg` in Java. The `new` is usually left out in Scala.
    // The `ivy` is a String interpolator. It basically calls the `ivy` String processor to interpret the string as a dependency
    override def ivyDeps = Agg(
        ivy"org.json:json:20240303",
        ivy"com.h2database:h2:2.2.224")
}

In general Mill concepts are quite familiar for a Java developer:

  • Methods for tasks, which are declared with def taskName = …​

  • The inheritance is extends JavaModule. If more modules are mixed in, then it is extends JavaModule with MyExtra with FormatterSupport

  • Otherwise you can call methods etc like regular code. (Mostly, I’ll plan to explain more in the next blog post)

Otherwise, when you encounter a Scala constructs: You find their meaning after a quick search.

Less Plugins, Call Libraries/Tools Directly

If you come from the Maven world, you might search for a plugin to things beyond the basic Java build. There are plugins for Mill. However, often you do not a plugin. Instead you call the tool / library you need directly.

Maven loves Plugins
Figure 1. Maven loves Plugins
Mill Often Does Things Directly
Figure 2. Mill Often Does Things Directly

Example: Replacing frontend-maven-plugin

For example, you use NodeJS for the front-end part of your web app. To integrate that with Maven, there is the frontend-maven-plugin plugin. Therefore, you maybe try to look for a similar plugin in Mill.

However, calling external tools in Mill is easy and therefore you can get by without a plugin at all:

Here is an example of building a front-end with node for your Java project:

object `front-end` extends Module {
  def nodeJsVersion = "v20.17.0"

  // Important for Mill, specify all Source code, including config
  // Otherwise Mill will skip build steps because it didn't see source changes.
  def packageConfig = T.sources(millSourcePath / "package.json", millSourcePath / "package-lock.json")

  def sources = T.sources(millSourcePath/"src",millSourcePath/"css")

  // Most node commands expect that the dependencies are already installed.
  def installPackages = T {
    // Important: This task depends on the package description.
    val packages = packageConfig()
    val npm = nodeJsDist().path / "bin" / "npm"
    os.proc(npm, "install").call(cwd = millSourcePath, stdout = Inherit)
    packages
  }

  def buildFrontEnd = T {
    // node build process expects packages to be installed
    val _ = installPackages()
    val _ = sources()
    val npm = nodeJsDist().path / "bin" / "npm"
    os.proc(npm, "run", "build-my-frontend").call(cwd = millSourcePath, stdout = Inherit)
    PathRef(millSourcePath / "public")
  }

  // Download node first. (Linux only for this demo)
  def nodeJsDist = T {
    val downloadPath = s"https://nodejs.org/dist/$nodeJsVersion/node-$nodeJsVersion-linux-x64.tar.xz"

    val tarFile = mill.util.Util.download(downloadPath, RelPath("node.tar.xz"))
    os.proc("tar", "-xf", tarFile.path).call(cwd = T.dest)
    PathRef(T.dest / "node-v20.17.0-linux-x64")
  }
}

Example: Using a library to Render Markdown

Even further, you can include Java libraries into your build script. For example use a Markdown library to generate the documentation.

// The $ivy prefix will include this library into this build file
import $ivy.`org.commonmark:commonmark:0.22.0`
import org.commonmark.node._
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer

object docs extends Module{
  // regular instance for the markdown library
  val parser = Parser.builder.build
  val renderer = HtmlRenderer.builder.build

  def source = T.source(millSourcePath)

  def markdownFiles = T{
    os.list(source().path,sort = true).filter(p=>p.lastOpt.exists(n=>n.endsWith(".md")))
  }

  def html = T{
    for(file <- markdownFiles()){
      val content = os.read(file)
      val document = parser.parse(content)
      val html = renderer.render(document)
      val htmlFile = file.lastOpt.getOrElse(???) + ".html"
      os.write(T.dest/htmlFile,html)
    }
    PathRef(T.dest)
  }
}

Summary

Mill is a great build system for Java projects. Yes, you’ll have to learn some Scala instead of Kotlin/Groovy/CProgramming-via-XML. In return you get a build System which shines if you have various build steps which go beyond box standard Java builds.

Tags: Scala Mill Build Java Development