Scala Library Dependencies

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

Adding Ivy Dependencies

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

object `package` extends RootModule with ScalaModule {
  def scalaVersion = "2.12.17"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::upickle:3.1.0",
    ivy"com.lihaoyi::pprint:0.8.1",
    ivy"${scalaOrganization()}:scala-reflect:${scalaVersion()}"
  )
}

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

  • Double :: syntax (e.g. ivy"com.lihaoyi::upickle:0.5.1") defines Scala dependencies

  • Triple ::: syntax (e.g. ivy"org.scalamacros:::paradise:2.1.1") defines dependencies cross-published against the full Scala version e.g. 2.12.4 instead of just 2.12. These are typically Scala compiler plugins or similar.

To select the test-jars from a dependency use the ;classifier=tests syntax, e.g.:

  • 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
pretty-printed using PPrint: Array("i", "am", "cow")
serialized using uPickle: ["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._, scalalib._

object foo extends ScalaModule {
  def scalaVersion = "2.13.8"
  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 ScalaModule {
  def scalaVersion = "2.13.8"
  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.

Keeping up-to-date with Scala Steward

It’s always a good idea to keep your dependencies up-to-date.

If your project is hosted on GitHub, GitLab, or Bitbucket, you can use Scala Steward to automatically open a pull request to update your dependencies whenever there is a newer version available.

Scala Steward can also keep your Mill version up-to-date.

Dependency Management

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

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._, scalalib._

object `package` extends RootModule with ScalaModule {
  def scalaVersion = "2.13.8"
  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._, scalalib._

object `package` extends RootModule with ScalaModule {
  def scalaVersion = "2.13.8"
  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 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 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._, scalalib._
import mill.define.ModuleRef
import coursier.maven.MavenRepository

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

object foo extends ScalaModule {
  def scalaVersion = "2.13.8"

  def ivyDeps = Agg(
    ivy"com.lihaoyi::scalatags:0.13.1",
    ivy"com.lihaoyi::mainargs:0.6.2"
  )

  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

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 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 ScalaModule {
  def scalaVersion = "2.13.8"
  def zincWorker = ModuleRef(CustomZincWorkerModule)
  // ... rest of your build definitions

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

> ./mill bar.compile

Scala Dependencies In Detail

Scala major releases up until version 2.13 are binary incompatible. That means, mixing dependencies of different binary platforms will result in non-working runtimes and obscure and hard to debug issues.

To easily pick only a compatible version, a convention was established to append the scala major version as a suffix to the package name.[1] E.g. to select the Scala 2.13 version of a library foo, the final artifactId will contain the additional suffix _2.13, such that the final artifactId is foo_2.13.

To always pick the right version and support cross compiling, you can omit the scala version and instead use a double colon (::) between the organization and the name, e.g. ivy"com.typesafe.akka:akka-actor_2.12:2.5.25". Your module needs to extends ScalaModule though.

If you want to use dependencies that are cross-published against the full Scala version, e.g. 2.12.12, you can use three colons (:::) between organization and name, e.g.: ivy"org.scalamacros:::paradise:2.1.1".

Example
def ivyDeps = Agg(
  // explicit scala version suffix, NOT RECOMMENDED!
  ivy"com.typesafe.akka:akka-actor_2.12:2.5.25",
  ivy"com.typesafe.akka::akka-actor:2.5.25",
  ivy"org.scalamacros:::paradise:2.1.1"
)

Scala 3 interoperability

Since the release of Scala 3, the binary compatibility story for Scala has changed. That means, Scala 3 dependencies can be mixed with Scala 2.13 dependencies. In fact, the Scala 3 standard library is the same as for Scala 2.13.

As Scala 3 and Scala 2.13 have different binary platforms, but their artifacts are in general compatible, this introduces new challenges.

There is currently no mechanism to bring the same dependency twice into the classpath (one for Scala 2.13 and one for Scala 3).

Using Scala 2.13 from Scala 3

If your Scala version is a Scala 3.x, but you want to use the Scala 2.13 version of a specific library, you can use the .withDottyCompat method on that dependency.

Example:
def scalaVersion = "3.2.1"
def ivyDeps = Agg(
  ivy"com.lihaoyi::upickle:2.0.0".withDottyCompat(scalaVersion()) //1
)
1 This will result in a Scala 2.13 dependency com.lihaoyi::upicke_2.13:2.0.0

Do you wonder where the name "dotty" comes from?

In the early development of Scala 3, the Scala 3 compiler was called "Dotty". Later, the name was changed to Scala 3, but the compiler project itself is still named "dotty".

The dotty compiler itself is an implementation of the Dependent Object Types (DOT) calculus, which is the new basis of Scala 3. It also enhances the type system to a next level and allows features like union-types and intersection-types.

ScalaJS dependencies

Scala.js introduces an additional binary platform axis. To the already required Scala version, there comes the Scala.js version.

You can use two colons (::) between name and version to define a Scala.js dependency. Your module needs to extends ScalaJSModule to accept Scala.js dependencies.

Scala Native dependencies

Scala Native introduces an additional binary platform axis. To the already required Scala version, there comes the Scala Native version.

You can use two colons (::) between name and version to define a Scala Native dependency. Your module needs to extends ScalaNativeModule to accept Scala Native dependencies.


1. Scala 2 versions have the unusual version format: {epoch}.{major}.{minor}.