Java Library Dependencies

This page goes into more detail about configuring third party dependencies for JavaModule.

Basic Dependencies Configuration

This example shows some of the common configuration options related to declaring dependencies in .mill.yaml files:

build.mill.yaml (download, browse)
extends: JavaModule

# Add library dependencies that are visible to the compiler at compile-time but
# not visible at runtime. This is commonly used for "optional" dependencies that
# downstream users may or may not include depending on their needs
compileMvnDeps:
- javax.servlet:servlet-api:2.5
- org.eclipse.jetty:jetty-server:9.4.42.v20210604
- org.eclipse.jetty:jetty-servlet:9.4.42.v20210604

# Add library dependencies that are visible at runtime but not at compile-time.
# Typically used for code invoked reflectively or via classpath scanning
runMvnDeps:
- javax.servlet:servlet-api:2.5
- org.eclipse.jetty:jetty-server:9.4.42.v20210604
- org.eclipse.jetty:jetty-servlet:9.4.42.v20210604

# Add additional Maven repositories to resolve dependencies from
repositories:
- https://oss.sonatype.org/content/repositories/releases

# Directly add jars on disk to your module classpath. Note that this does not
# resolve transitive dependencies of those jars, and so you need to ensure you
# include any transitive dependencies yourself
unmanagedClasspath:
- lib/nanojson-1.8.jar

Run-time Dependencies

If you want to use additional dependencies at runtime or override dependencies and their versions at runtime, you can do so with runMvnDeps. Runtime dependencies are not available during compilation, but are available at runtime. They are transitive, and so if you run or create an assembly of a module it will include the runtime dependencies of all upstream modules on the classpath

Compile-time Dependencies

You can also declare compile-time-only dependencies with compileMvnDeps. These are present in the compile classpath, but will not propagate to the transitive dependencies. Typically, Mill assumes that a module with compile-time dependencies will only be run after someone includes the equivalent run-time dependencies in a later build step. e.g. in the case above, bar defines the compile-time dependencies, and foo then depends on bar and includes the runtime dependencies. That is why we can run foo as show below:

Compile-time dependencies are translated to provided-scoped dependencies when publish to Maven or Ivy-Repositories.

compileMvnDeps are non-transitive: a module does not automatically aggregate them from its upstream dependencies. They must be defined in every module that they are required in, either explicitly or via a trait that the module inherits from.

> ./mill runMain foo.Foo '{"name":"John","age":30}'     # mac/linux
Key: name, Value: John
Key: age, Value: 30

> ./mill runMainBackground bar.Bar

> curl http://localhost:${PORT:-8080}
<html><body>Hello World!</body></html>

Unmanaged Jars

In most scenarios you should rely on mvnDeps/moduleDeps and let Mill manage the compilation/downloading/caching of classpath jars for you, as Mill will automatically pull in transitive dependencies which are generally needed for things to work, and avoids including different versions of the same classfiles or jar which can cause confusion. But in the rare case you receive a jar or folder-full-of-classfiles from somewhere and need to include it in your project, unmanagedClasspath is the way to do it.

build.mill.yaml (download, browse)
extends: JavaModule
unmanagedClasspath:
- lib/nanojson-1.8.jar

You can override unmanagedClasspath to point it at any jars you place on the filesystem, e.g. in the above snippet any jars that happen to live in the lib/ folder.

> ./mill run '{"name":"John","age":30}'     # mac/linux
Key: name, Value: John
Key: age, Value: 30

Programmable Dependencies Configuration

Downloading Unmanaged Jars

You can also override unmanagedClasspath to point it at jars that you want to download from arbitrary URLs. requests.get comes from the Requests-Scala library, one of Mill’s Bundled Libraries.

build.mill (download, browse)
package build
import mill.*, javalib.*

object `package` extends JavaModule {
  def unmanagedClasspath = Task {
    if (Task.offline) Task.fail("Cannot download classpath when in offline-mode") (1)
    else {
      os.write(
        Task.dest / "fastjavaio.jar",
        requests.get.stream(
          "https://github.com/williamfiset/FastJavaIO/releases/download/1.1/fastjavaio.jar"
        )
      )
      Seq(PathRef(Task.dest / "fastjavaio.jar"))
    }
  }
}
> ./mill run "textfile.txt" # mac/linux
I am cow
hear me moo
I weigh twice as much as you

Tasks like unmanagedClasspath are cached, so your jar is downloaded only once and re-used indefinitely after that. This is usually not a problem, because usually URLs follow the rule that Cool URIs don’t change, and so jars downloaded from the same URL will always contain the same contents.

> ./mill --offline run "textfile.txt"
I am cow
hear me moo
I weigh twice as much as you
An unmanaged jar downloaded via requests.get is still unmanaged: even though you downloaded it from somewhere, it requests.get does not know how to pull in transitive dependencies or de-duplicate different versions on the classpath All the same caveats you need to worry about when dealing with unmanaged jars apply here as well.

Repository Configuration

By default, dependencies are resolved from Maven Central, the standard package repository for JVM languages like Java, Kotlin, or Scala. You can also add your own resolvers by overriding the repositories task in the module:

build.mill (download, browse)
package build
import mill.*, javalib.*
import mill.api.ModuleRef

object foo extends JavaModule {

  def mvnDeps = Seq(
    mvn"net.sourceforge.argparse4j:argparse4j:0.9.0",
    mvn"org.thymeleaf:thymeleaf:3.1.1.RELEASE",
    mvn"org.slf4j:slf4j-nop:2.0.7"
  )

  def repositories = Seq("https://oss.sonatype.org/content/repositories/releases")
}

Mill uses the Coursier dependency resolver, and reads Coursier config files automatically. The list of valid repository strings is documented here:

For more specialized custom repositories which do not follow the standard URL format, you can override def repositoriesTask to instantiate a custom coursier.Repository instance programmatically.

You can configure Coursier to use an alternate download location for Maven Central artifacts via a mirror.properties file:

central.from=https://repo1.maven.org/maven2
central.to=http://example.com:8080/nexus/content/groups/public

The default location of this config file on each OS is as follows:

  • Linux: ~/.config/coursier/mirror.properties

  • MacOS: ~/Library/Preferences/Coursier/mirror.properties

  • Windows: C:\Users\<user_name>\AppData\Roaming\Coursier\config\mirror.properties

You can also set the environment variable COURSIER_MIRRORS or the jvm property coursier.mirrors to specify config file location.

To add custom resolvers to the initial bootstrap of the build, you can create a custom JvmWorkerModule (named after the Zinc Incremental compiler used to compile the build.mill files) and override the jvmWorker method in your Module by pointing it to that custom object:

object CustomJvmWorkerModule extends JvmWorkerModule, CoursierModule {
  def repositories = Seq("https://oss.sonatype.org/content/repositories/releases")
}

object bar extends JavaModule {
  def jvmWorker = ModuleRef(CustomJvmWorkerModule)
  // ... rest of your build definitions

  def repositories = Seq("https://oss.sonatype.org/content/repositories/releases")
}
> ./mill foo.run --text hello

> ./mill bar.compile

See Import Libraries and Plugins for customizing repositories used to resolve //| mvnDeps used in your build.mill file

Dependency Management

Mill has support for dependency management, see the Dependency Management section in Library Dependencies in Mill.