Scala Library Dependencies
This page goes into more detail about configuring third party dependencies
for ScalaModule
.
Adding Ivy Dependencies
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 just2.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
.
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.
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.
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:
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"
.
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.
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.
{epoch}.{major}.{minor}
.