Java Library Dependencies

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

Adding Ivy Dependencies

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

object `package` extends RootModule with JavaModule {
  def ivyDeps = Agg(
    ivy"com.fasterxml.jackson.core:jackson-databind:2.13.4"
  )
}

You can define the ivyDeps field to add ivy dependencies to your module, named after the Ivy Dependency Resolver and JVM package repository format. The Agg factory function constructs an Agg[Dep] collection, which is a collection of `Dep`s without duplicates, a type common in Mill builds.

  • Single : syntax (e.g. "ivy"org.testng:testng:6.11") defines Java dependencies

To select the test-jars from a dependency use the following syntax:

  • ivy"org.apache.spark::spark-sql:2.4.0;classifier=tests.

Please consult the Library Dependencies in Mill section for more details.

> ./mill run i am cow
JSONified using Jackson: ["i","am","cow"]

Runtime and Compile-time Dependencies

If you want to use additional dependencies at runtime or override dependencies and their versions at runtime, you can do so with runIvyDeps.

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

object foo extends JavaModule {
  def moduleDeps = Seq(bar)
  def runIvyDeps = Agg(
    ivy"javax.servlet:servlet-api:2.5",
    ivy"org.eclipse.jetty:jetty-server:9.4.42.v20210604",
    ivy"org.eclipse.jetty:jetty-servlet:9.4.42.v20210604"
  )
  def mainClass = Some("bar.Bar")
}

You can also declare compile-time-only dependencies with compileIvyDeps. These are present in the compile classpath, but will not propagate to the transitive dependencies.

object bar extends JavaModule {
  def compileIvyDeps = Agg(
    ivy"javax.servlet:servlet-api:2.5",
    ivy"org.eclipse.jetty:jetty-server:9.4.42.v20210604",
    ivy"org.eclipse.jetty:jetty-servlet:9.4.42.v20210604"
  )
}

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:

> ./mill foo.runBackground

> curl http://localhost:8079
<html><body>Hello World!</body></html>
Compile-time dependencies are translated to provided-scoped dependencies when publish to Maven or Ivy-Repositories.

Both compileIvyDeps and runIvyDeps 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.

Unmanaged Jars

In most scenarios you should rely on ivyDeps/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 (download, browse)
package build
import mill._, javalib._

object `package` extends RootModule with JavaModule {
  def unmanagedClasspath = Task {
    if (!os.exists(millSourcePath / "lib")) Agg()
    else Agg.from(os.list(millSourcePath / "lib").map(PathRef(_)))
  }
}

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

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 RootModule with JavaModule {
  def unmanagedClasspath = Task {
    os.write(
      Task.dest / "fastjavaio.jar",
      requests.get.stream(
        "https://github.com/williamfiset/FastJavaIO/releases/download/1.1/fastjavaio.jar"
      )
    )
    Agg(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.

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 third party 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 Config

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 repositoriesTask task in the module:

build.mill (download, browse)
package build
import mill._, javalib._
import mill.define.ModuleRef
import coursier.maven.MavenRepository

val sonatypeReleases = Seq(
  MavenRepository("https://oss.sonatype.org/content/repositories/releases")
)

object foo extends JavaModule {

  def ivyDeps = Agg(
    ivy"net.sourceforge.argparse4j:argparse4j:0.9.0",
    ivy"org.thymeleaf:thymeleaf:3.1.1.RELEASE"
  )

  def repositoriesTask = Task.Anon { super.repositoriesTask() ++ sonatypeReleases }
}

Mill uses the Coursier dependency resolver, and reads Coursier config files automatically.

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

Note theses default config file locatations:

  • 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 ZincWorkerModule (named after the Zinc Incremental compiler used to compile the build.mill files) and override the zincWorker method in your ScalaModule by pointing it to that custom object:

object CustomZincWorkerModule extends ZincWorkerModule with CoursierModule {
  def repositoriesTask = Task.Anon { super.repositoriesTask() ++ sonatypeReleases }
}

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

  def repositoriesTask = Task.Anon { super.repositoriesTask() ++ sonatypeReleases }
}
> ./mill foo.run --text hello

> ./mill bar.compile