Simple Java Modules
This page documents the usage of simple config-based Java modules defined by
build.mill.yaml
and package.mill.yaml
files. These are less flexible but easier to get started with than the
full build.mill
or package.mill
build files, which makes them ideal for small
projects which do not need additional flexibility.
Common Configuration Overrides for Simple Modules
This example shows some of the common tasks you may want to override on a
JavaModule
: specifying the mainClass
, adding additional
sources/resources, and setting compilation/run
options.
extends: [mill.javalib.JavaModule]
mvnDeps:
- org.thymeleaf:thymeleaf:3.1.1.RELEASE
mainClass: "foo.Foo2"
sources: ["./src", "./custom-src"]
resources: ["./resources", "./custom-resources"]
forkArgs: ["-Dmy.custom.property=my-prop-value"]
forkEnv: { "MY_CUSTOM_ENV": "my-env-value" }
> ./mill run
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value
MY_CUSTOM_ENV: my-env-value
> ./mill show assembly
".../out/assembly.dest/out.jar"
> ./out/assembly.dest/out.jar # mac/linux
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value
Packaging and Publishing Simple Modules
Mill Single-File Projects have the full flexibility of the Mill build tool, just configured
using the YAML build header syntax rather than a separate
build.mill
file. That means you can configure the JVM version,
compile flags or runtime flags, create
Executable Assemblies
or Graal Native Images,
Publishing to Maven Central
etc.
The example below shows a few of those options configured in the YAML build header of a single-file project, and then made of use from the command line:
extends: [mill.javalib.JavaModule, mill.javalib.PublishModule, mill.javalib.NativeImageModule]
jvmId: "graalvm-community:24"
nativeImageOptions: ["--no-fallback"]
publishVersion: "0.0.1"
artifactName: "example"
pomSettings:
description: "Example"
organization: "com.lihaoyi"
url: "https://github.com/com.lihaoyi/example"
licenses: ["MIT"]
versionControl: "https://github.com/com.lihaoyi/example"
developers: [{"name": "Li Haoyi", "email": "example@example.com"}]
> ./mill nativeImage
> out/nativeImage.dest/native-executable
Hello Graal Native: 24...
> ./mill publishLocal
Publishing Artifact(com.lihaoyi,example...,0.0.1) to ivy repo ...
Apart from publishing locally, you can also publish this single-file project to Sonatype Maven Central via:
> ./mill mill.javalib.SonatypeCentralPublishModule/
Most configuration def
s in Mill can be used to configure single-file projects, and
most tasks and commands can also be used as well. This gives you a lot of flexibility
in working with your single-file project until it becomes complex enough to need
a dedicated build.mill
file.
Custom Script Module Classes
By default, single-file Mill script modules inherit their behavior from the standard
mill.javalib.JavaModule
, mill.scalalib.ScalaModule
, or mill.scalalib.KotlinModule
.
However, you can also customize it to inherit from a custom Module
class that you define
as part of your meta-build in mill-build/src/
. For example, if we want to add a resource
file generated by processing the source file of the script, this can be done in a custom
LineCountJavaModule
as shown below:
extends: [millbuild.LineCountJavaModule]
package millbuild
import mill.*, javalib.*
trait LineCountJavaModule extends mill.javalib.JavaModule {
/** Total number of lines in module source files */
def lineCount = Task {
allSourceFiles().map(f => os.read.lines(f.path).size).sum
}
/** Generate resources using lineCount of sources */
override def resources = Task {
os.write(Task.dest / "line-count.txt", "" + lineCount())
super.resources() ++ Seq(PathRef(Task.dest))
}
}
> ./mill run
...
Line Count: 17
> ./mill show lineCount
17
Your custom LineCountJavaModule
must be a class
take two parameters
val millScriptFile: os.Path
, and override val moduleDeps: Seq[JavaModule]
that will be
populated by Mill, and inherit from mill.simple.Java.Base
or some other subclass of
mill.simple.SimpleModule
. This can then be used via //| extends: LineCountJavaModule
in the header of your script file.
Custom script module classes allows you to customize the semantics of your Java, Scala, or Kotlin
single-file script modules. If you have a large number of scripts with a similar configuration,
or you need customizations that cannot be done in the YAML build header, placing these
customizations in a custom script module class can let you centrally define the behavior
and standardize it across all scripts that inherit it via extends.
Depending on Programmatic Modules from Simple Modules
package build
import mill.*, javalib.*
object bar extends JavaModule {
def mvnDeps = Seq(mvn"org.thymeleaf:thymeleaf:3.1.1.RELEASE")
}
extends: [mill.javalib.JavaModule]
moduleDeps: [build.bar]
mvnDeps: [net.sourceforge.argparse4j:argparse4j:0.9.0]
> ./mill foo.run --text hello
<h1>hello</h1>
Single-File Scripts
Mill also allows you to run single-file Java programs (often referred to as scripts) easily from the command-line, even those that contain third-party dependencies or other such configuration. These can be useful as a replacement for Bash scripts, letting you write small scripts or programs in Java with full access to third-party libraries and other build-tool features.
For example, given the Java program below, it can be run directly using Mill:
//| mvnDeps:
//| - "net.sourceforge.argparse4j:argparse4j:0.9.0"
//| - "org.thymeleaf:thymeleaf:3.1.1.RELEASE"
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.Namespace;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
public class Foo {
public static String generateHtml(String text) {
Context context = new Context();
context.setVariable("text", text);
return new TemplateEngine().process("<h1 th:text=\"${text}\"></h1>", context);
}
public static void main(String[] args) {
ArgumentParser parser = ArgumentParsers.newFor("template")
.build()
.defaultHelp(true)
.description("Inserts text into a HTML template");
parser.addArgument("-t", "--text").required(true).help("text to insert");
Namespace ns = null;
try {
ns = parser.parseArgs(args);
} catch (Exception e) {
System.out.println(e.getMessage());
System.exit(1);
}
System.out.println(generateHtml(ns.getString("text")));
}
}
> ./mill Foo.java --text hello
compiling 1 Java source to...
<h1>hello</h1>
> ./mill Foo.java:run --text hello
<h1>hello</h1>
The ./mill Foo.{language-ext}
syntax is shorthand for ./mill Foo.{language-ext}:run
.
You can also call other tasks on your script modules, such as Foo.{langauge-ext}:assembly
below:
> ./mill show Foo.java:assembly # show the output of the assembly task
".../out/Foo.java/assembly.dest/out.jar"
> java -jar ./out/Foo.java/assembly.dest/out.jar --text hello
<h1>hello</h1>
> ./out/Foo.java/assembly.dest/out.jar --text hello # mac/linux
<h1>hello</h1>
Script files can have test suites, usually written in a separate test script. The test script
specifies what script it tests via moduleDeps
, and can have its own mvnDeps
in addition
to those of the upstream script. The test script can then exercise functions from the upstream
script as shown below:
//| moduleDeps: [Foo.java]
//| mvnDeps:
//| - "com.google.guava:guava:33.3.0-jre"
import static com.google.common.html.HtmlEscapers.htmlEscaper;
public class FooTest {
public static void main(String[] args) {
var result = Foo.generateHtml("hello");
assert(result == "<h1>hello</h1>");
System.out.println(result);
var result2 = Foo.generateHtml("<hello>");
var expected2 = "<h1>" + htmlEscaper().escape("<hello>") + "</h1>";
assert(result2 == expected2);
System.out.println(result2);
}
}
> ./mill FooTest.java
<h1>hello</h1>
<h1><hello></h1>
Again, you can pass the name of the task explicitly via :
, e.g. :run
below
> ./mill FooTest.java:run # specifying the test task explicitly
<h1>hello</h1>
<h1><hello></h1>
For more details on Mill script files, see
Configuring Single-File Projects
The rest of the examples below discuss the Mill config for building larger projects
beyond the single-file project format discussed above, using a dedicated build.mill
file to provide additional flexibility in setting up your project.