Library Dependencies in Mill
Beside the dependencies between Mill modules, most non-trivial source projects have dependencies to other libraries.
Mill uses coursier to resolve and download dependencies. Once downloaded, they are located in the coursier specific cache locations. For more details about coursier, refer to the coursier documentation.
Dependencies in General
Mill dependencies have the simple form:
ivy"{organization}:{name}:{version}"
Additional attributes are also supported:
ivy"{organization}:{name}:{version}[;{attribute}={value}]*"
When working in other Java and Scala projects, you will find some synonyms, which typically all mean the same.
For example in the Maven ecosystem, the organization
is called the group
and the name
is called the artifact
.
The whole triplet is often called GAV
.
In Mill we use the additional term artifactId
which is identical to the name
when used in the normal form shown above.
When a different form is used, e.g. some double-colons are used between the parts, the artifactId
typically contains suffixes, but the name doesn’t.
def ivyDeps = Agg(
ivy"org.slf4j:slf4j-api:1.7.25"
)
Test dependencies (there is no test
scope)
One difference between Mill and other build tools like sbt or Maven is the fact, that tests are ordinary submodules on their own.
For convenience, most modules already come with a pre-configured trait for a test submodule,
which already inherits all dependencies of their parent module.
If you need additional test dependencies, you simply add them by overriding def ivyDeps
, as you would do with normal library dependencies.
When migrating a sbt project and seeing a dependency like this: "ch.qos.logback" % "logback-classic" % "1.2.3" % "test"
,
simply add it to the test module’s ivyDeps
as ordinary dependency.
There is no special test scope in Mill.
object main extends JavaModule {
object test extends JavaModuleTests {
def ivyDeps = Agg(
ivy"org.qos.logback:logback-classic:1.2.3"
)
}
}
Compile-only dependencies (provided
scope)
If you want to use a dependency only at compile time, you can declare it with the compileIvyDeps
task.
def compileIvyDeps = Agg(
ivy"org.slf4j:slf4j-api:1.7.25"
)
When Mill generated file to interact with package manager like pom.xml
for Maven repositories, such compile-only dependencies are mapped to the provided
scope.
Please note, that dependencies with provided
scope will never be resolved transitively. Hence, the name "provided", as the task runtime needs to "provide" them, if they are needed.
Runtime dependencies
If you want to declare dependencies to be used at runtime (but not at compile time), you can use the runIvyDeps
tasks.
def runIvyDeps = Agg(
ivy"ch.qos.logback:logback-classic:1.2.0"
)
It is also possible to use a higher version of the same library dependencies already defined in ivyDeps
, to ensure you compile against a minimal API version, but actually run with the latest available version.
Searching For Dependency Updates
Mill can search for updated versions of your project’s dependencies, if available from your project’s configured repositories. Note that it uses heuristics based on common versioning schemes, so it may not work as expected for dependencies with particularly weird version numbers. For example, given the following build:
package build
import mill._, scalalib._
trait MyModule extends ScalaModule {
def scalaVersion = "2.13.11"
}
object foo extends MyModule {
def moduleDeps = Seq(bar)
def ivyDeps = Agg(ivy"com.lihaoyi::mainargs:0.4.0")
}
object bar extends MyModule {
def ivyDeps = Agg(ivy"com.lihaoyi::scalatags:0.8.2")
}
You can run
> mill mill.scalalib.Dependency/showUpdates
> mill mill.scalalib.Dependency/showUpdates --allowPreRelease true # also show pre-release versions
Current limitations:
-
Only works for
JavaModule
modules (includingScalaModule
s,CrossScalaModule
s, etc.) and Maven repositories. -
Always applies to all modules in the build.
-
Doesn’t apply to
$ivy
dependencies used in the build definition itself.
Scala dependencies
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, that impedes 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. |
Detecting transitive dependencies
To render a tree of dependencies (transitive included) you can run mill myModule.ivyDepsTree
. Here is how the start of ./mill __.ivyDepsTree
looks like in the mill
project itself:
├─ ch.epfl.scala:bsp4j:2.1.0-M3
│ ├─ org.eclipse.lsp4j:org.eclipse.lsp4j.generator:0.12.0
│ │ ├─ org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.12.0
│ │ │ └─ com.google.code.gson:gson:2.9.1
│ │ └─ org.eclipse.xtend:org.eclipse.xtend.lib:2.24.0
│ │ ├─ org.eclipse.xtend:org.eclipse.xtend.lib.macro:2.24.0
│ │ │ └─ org.eclipse.xtext:org.eclipse.xtext.xbase.lib:2.24.0
...
│ │ ├─ com.lihaoyi:fastparse_2.13:2.3.0
│ │ │ ├─ com.lihaoyi:geny_2.13:0.6.0 -> 0.7.1 (possible incompatibility)
│ │ │ │ └─ org.scala-lang:scala-library:2.13.10
│ │ │ └─ com.lihaoyi:sourcecode_2.13:0.2.1 -> 0.3.0 (possible incompatibility)
After compiling your module(s) you can find and examine files such as ivyDeps.json
and transitiveIvyDeps.json
in your out
build’s folder for a given module.
After running the ivyDepsTree
command you’ll also find the ivyDepsTree.json
and ivyDepsTree.log
file that contain the output of the above ivyDepsTree
command.
You can observe the actual version being used by running mill show myModule.resolvedIvyDeps
. If you run mill myModule.resolvedIvyDeps
, the same information is available in out/myModule/resolvedIvyDeps.json
.
Figuring out where a dependency comes from
There will be times when you want to figure out where a dependency is coming
from. The output of ivyDepsTree
can be quite large in larger projects so the
command provides a nice utility to be able to target the part of the tree that
brings in a specific dependency.
For example, let’s again use the Mill codebase as an example. We’ll search the
tree in the main
module and try to find where the jsoniter-scala-core_2.13
artifact is coming from using the --whatDependsOn
argument:
❯ ./mill -i dev.run ~/Documents/scala-workspace/com-lihaoyi/mill main.ivyDepsTree --whatDependsOn com.github.plokhotnyuk.jsoniter-scala:jsoniter-scala-core_2.13
[33/33] main.ivyDepsTree
└─ com.github.plokhotnyuk.jsoniter-scala:jsoniter-scala-core_2.13:2.13.5
├─ io.get-coursier:coursier_2.13:2.1.0-RC1
└─ org.virtuslab.scala-cli:config_2.13:0.1.16
└─ io.get-coursier:coursier-cache_2.13:2.1.0-RC1
└─ io.get-coursier:coursier_2.13:2.1.0-RC1
By looking at the output we can see that it’s our dependency on coursier_2.13
that is bringing in the jsoniter-scala-core_2.13
artifact.
The --whatDependsOn
argument can also be repeated to target multiple
artifacts at once. Just repeat the --whatDependsOn <artifact>
pattern. Note
that the artifact pattern follows the org:artifact
convention. You can’t
include a version as the utility will show you all usages of the artifact
.
Also note that when using --whatDependsOn
on usage of --inverse
is forced
in order to make the tree appear in an inverted manner to more easily show you
where the dependency is coming from.
Excluding transitive dependencies
You can use the .exclude
method on a dependency. It accepts organization
and name
tuples, to be excluded.
Use the special name *
to match all organization
s or name
s.
fansi_2.12
library from transitive dependency set of pprint
.def deps = Agg(
ivy"com.lihaoyi::pprint:0.5.3".exclude("com.lihaoyi" -> "fansi_2.12")
)
You can also use .excludeOrg
or excludeName
:
There is also a short notation available:
fansi_2.12
library from transitive dependency set of pprint
.def deps = Agg(
ivy"com.lihaoyi::pprint:0.5.3;exclude=com.lihaoyi:fansi_2.12"
)
com.lihaoyi
libraries from transitive dependency set of pprint
.val deps = Agg(ivy"com.lihaoyi::pprint:0.5.3".excludeOrg("com.lihaoyi"))
Note: You can chain multiple exclusions with exclude
, excludeOrg
, and excludeName
.
pprint
.val deps = Agg(
ivy"com.lihaoyi::pprint:0.5.3"
.excludeName("fansi_2.12")
.excludeName("sourcecode")
)
Forcing versions
Please treat forceVersion as experimental; it has some bugs and isn’t production-ready (forced versions aren’t propagated to published artifacts). |
You can use the forceVersion
method to ensure the used version of a dependency is what you have declared.
-
You declare a dependency
val deps = Agg(ivy"com.lihaoyi::fansi:0.2.14")
-
There is another dependency,
val deps = Agg(ivy"com.lihaoyi::PPrint:0.8.1")
-
PPrint 0.8.1 uses fansi 0.4.0, so it is a transitive dependency
-
mill show myModule.resolvedIvyDeps | grep "fansi"
should show fansi 0.4.0 -
If you want to force to the older version (to prevent it being evicted, and replaced by 0.4.0) then you can use
val deps = Agg(ivy"com.lihaoyi::fansi:0.2.14".forceVersion())
-
mill show myModule.resolvedIvyDeps | grep "fansi"
should show fansi 0.2.14
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}
.