Pre-Compiled Modules

Pre-compiled modules allow you to define a package.mill.yaml or build.mill.yaml file whose extends clause refers to a pre-compiled class (a subclass of mill.api.PrecompiledModule).

Pre-compiled modules are useful when you have a lot of similar modules, as is common in large codebases. Rather than code-generating and compiling a separate build script for each module, these identical modules can be compiled once in your mill-build/src or even compiled and published earlier to Maven Central, and from there used throughout your project.

In this example, foo/package.mill.yaml and bar/package.mill.yaml both extend millbuild.LineCountJavaModule, a custom PrecompiledModule subclass defined in mill-build/src/. This module counts the lines of source code and generates a resource file with the line count. It also has a nested test module containing the test suite configuration.

mill-build/src/LineCountJavaModule.scala (download, browse)
package millbuild
import mill.*, javalib.*

class LineCountJavaModule(val scriptConfig: mill.api.PrecompiledModule.Config)
    extends mill.javalib.JavaModule with mill.api.PrecompiledModule {

  override lazy val millDiscover = mill.api.Discover[this.type]

  object test extends JavaTests with mill.javalib.TestModule.Junit5

  /** Total number of lines in module source files */
  def lineCount: T[Int] = Task {
    allSourceFiles().map(f => os.read.lines(f.path).size).sum
  }

  /** Generate resources using lineCount of sources */
  override def resources: T[Seq[PathRef]] = Task {
    os.write(Task.dest / "line-count.txt", "" + lineCount())
    super.resources() ++ Seq(PathRef(Task.dest))
  }
}

LineCountJavaModule is then inherited by foo and bar:

foo/package.mill.yaml (download, browse)
extends: millbuild.LineCountJavaModule
mill-experimental-precompiled-module: true

object test:
  mvnDeps:
  - org.hamcrest:hamcrest:2.2
bar/package.mill.yaml (download, browse)
extends: millbuild.LineCountJavaModule
mill-experimental-precompiled-module: true
moduleDeps: !append [foo]

object test:
  mvnDeps:
  - org.hamcrest:hamcrest:2.2
  moduleDeps: !append [foo.test]

bar declares moduleDeps: !append [foo], so bar depends on foo and can use foo’s classes. Similarly, `bar.test declares moduleDeps: !append [foo.test], so bar's test module depends on `foo’s test module and can reuse its test utilities.

> ./mill resolve _
...
bar
...
foo
...

> ./mill resolve foo._
foo.test
...
foo.compile
...
foo.run
...
foo.lineCount
...

> ./mill resolve bar.test._
bar.test.compile
...
bar.test.testForked
...

> ./mill foo.run
Hello, Foo!

> ./mill bar.run
Hello, Bar!, Hello, Qux!

> ./mill show foo.lineCount
11

> ./mill show bar.lineCount
18

> ./mill foo.test
...testGreet...

> ./mill bar.test
...testGreetAll...
...testFooGreetFromBarTest...

Instead of generating Scala code from the YAML file, Mill instantiates the pre-compiled module class reflectively at runtime, similar to script modules. Pre-compiled modules are visible to resolve and can be referenced from other pre-compiled modules and scripts, but not from .mill files since they are instantiated at resolution time after .mill files are already compiled. They are enabled via the mill-experimental-precompiled-module: true key.