dependency is a library to parse and handle Java and Scala dependencies, written as strings like
org::name:1.0. It's used by coursier and
Scala CLI, among others. It features:
-
support for both Java and Scala dependencies (simply or fully cross-versioned, Scala.js and Scala Native dependencies)
-
support for exclusions and custom parameters (classifiers, override URLs for artifacts, …)
-
support for Ivy module attributes
-
toStringrepresentation that can be parsed back -
module matchers, accepting
*wildcards -
string interpolators, validating dependencies and modules at compile-time
-
Scala binary version computation
Add io.get-coursier::dependency::0.3.4 to your build:
// Mill
def mvnDeps = Seq(mvn"io.get-coursier::dependency::0.3.4")
// sbt
libraryDependencies += "io.get-coursier" %%% "dependency" % "0.3.4"The examples below assume dependency._ is imported:
import dependency._dependency is published for Scala 2.12, 2.13, and 3 for the JVM, and for Scala 3 only for Scala.js and Scala Native. It is compatible with JDK back to 8.
The parsers and string interpolators below all accept the same syntax.
The separators before and after the name say how it should be cross-versioned:
| Dependency | Kind | Name with Scala 2.13.6 and platform sjs1 |
|---|---|---|
org:name:1.0 |
Java | name |
org::name:1.0 |
Scala | name_2.13 |
org:::name:1.0 |
Scala, fully cross-versioned | name_2.13.6 |
org::name::1.0 |
Scala, with platform | name_sjs1_2.13 |
org:::name::1.0 |
Scala, fully cross-versioned, with platform | name_sjs1_2.13.6 |
Modules are written the same way, without a version. As there's no version to
separate from the name, a platform suffix is then marked by a single trailing :, like in
org::name: or org:::name:.
Other parts of the syntax:
| Dependency | Meaning |
|---|---|
org:name;attr1=a;attr2=b:1.0 |
Ivy module attributes, also accepted in modules |
org:name |
No version: version is empty |
org:name:[1.0,2.0) |
Version interval |
org:name:1.0:runtime |
Inline configuration, added to userParams as $inlineConfiguration |
org:name:1.0,exclude=other%lib |
Excludes the Java module other:lib |
org:name:1.0,exclude=other%%lib |
Excludes the Scala module other::lib |
org:name:1.0,key=value,flag |
Custom parameters |
Organizations, names, and versions can't contain / or \.
toString gives back that syntax, so dependencies and modules can be parsed back:
val syntaxDep = dep"org::name::1.0,exclude=other%%lib,key=value"
assert(syntaxDep.toString == "org::name::1.0,exclude=other%%lib,key=value")| Module | Dependency | Name attributes |
|---|---|---|
Module |
Dependency |
None: the name is final |
ScalaModule |
ScalaDependency |
Scala ones: applyParams must be called to get the final name |
AnyModule |
AnyDependency |
Either of the above |
applyParams returns a Module or a Dependency. Parsers return an AnyModule or an
AnyDependency, as the kind of their input is only known at runtime. String interpolators
return the most precise type:
val javaModule: Module = mod"org:name"
val scalaModule: ScalaModule = mod"org::name"
val javaDependency: Dependency = dep"org:name:1.0"
val scalaDependency: ScalaDependency = dep"org::name:1.0"Exclusions are kept in a CovariantSet, a set that, unlike Set, is covariant in its element type.
import dependency.parser.DependencyParser
val input = "io.get-coursier::coursier:2.0.6"
val maybeDependency: Either[String, AnyDependency] = DependencyParser.parse(input)
val dep = maybeDependency.toOption.getParsing errors are returned as a Left, while invalid input passed to the string interpolators
below is a compile-time error:
assert(DependencyParser.parse("coursier") == Left("malformed module: coursier"))
assert(DependencyParser.parse("io/get-coursier:coursier:2.0.6") == Left("organization io/get-coursier contains invalid '/'"))val params = ScalaParameters("2.13.6")
val javaDep: Dependency = dep.applyParams(params)
assert(javaDep.toString == "io.get-coursier:coursier_2.13:2.0.6")val platformParams = ScalaParameters("2.13.6").copy(platform = Some("sjs1"))
val otherJavaDep: Dependency = dep"io.get-coursier::coursier::2.0.6".applyParams(platformParams)
assert(otherJavaDep.toString == "io.get-coursier:coursier_sjs1_2.13:2.0.6")The platform, like sjs1 for Scala.js 1.x or native0.5 for Scala Native 0.5.x, is only added to
dependencies with a :: before their version:
val noPlatformDep: Dependency = dep"io.get-coursier::coursier:2.0.6".applyParams(platformParams)
assert(noPlatformDep.toString == "io.get-coursier:coursier_2.13:2.0.6")val dep1: AnyDependency = dep"io.get-coursier::coursier:2.0.6"
val depName = "coursier"
val ver = "2.0.6"
val dep2: AnyDependency = dep"io.get-coursier::$depName:$ver"The interpolated strings are validated at compile-time. Interpolated values (depName, ver above)
are inserted as is afterwards, so any separator they contain, like :, ends up in the name or
version, rather than being interpreted. Interpolated values must be Strings: anything else, like
an Option[String] or an Int, is a compile-time error.
assert(dep.organization == "io.get-coursier")
assert(dep.name == "coursier")
assert(javaDep.name == "coursier_2.13")
assert(dep.version == "2.0.6")import dependency.parser.ModuleParser
val moduleInput = "io.get-coursier::coursier"
val maybeModule: Either[String, AnyModule] = ModuleParser.parse(moduleInput)
val mod = maybeModule.toOption.getval javaMod: Module = mod.applyParams(params)
assert(javaMod.toString == "io.get-coursier:coursier_2.13")val mod1: AnyModule = mod"io.get-coursier::coursier"
val modName = "coursier"
val mod2: AnyModule = mod"io.get-coursier::$modName"Like for dependencies, interpolated values (modName above) are inserted as is, after the
interpolated string has been validated at compile-time.
assert(mod.organization == "io.get-coursier")
assert(mod.name == "coursier")
assert(javaMod.name == "coursier_2.13")ModuleMatcher matches modules against a module whose organization, name, and attribute values
can contain * wildcards:
val matcher = ModuleMatcher(mod"io.get-coursier::coursier-*")
assert(matcher.matches(mod"io.get-coursier::coursier-cache"))
assert(!matcher.matches(mod"io.get-coursier::dependency"))Name attributes (Scala module or not, fully cross-versioned or not, platform suffix or not) are
taken into account too, unless matchNameAttributes is false:
assert(!matcher.matches(mod"io.get-coursier:coursier-cache"))
val looseMatcher = ModuleMatcher(mod"io.get-coursier:coursier-*", matchNameAttributes = false)
assert(looseMatcher.matches(mod"io.get-coursier:coursier-cache"))
assert(looseMatcher.matches(mod"io.get-coursier::coursier-cache"))Matchers can also be parsed, and ModuleMatcher.all matches any module:
val parsedMatcher: Either[String, ModuleMatcher] = ModuleMatcher.parse("io.get-coursier::coursier-*")
assert(ModuleMatcher.all.matches(mod"io.get-coursier::coursier"))Exclusions can be specified in the string representations of dependencies:
val depWithExclusions = dep"io.get-coursier::coursier:2.0.6,exclude=io.argonaut%%argonaut,exclude=org.fusesource.jansi%jansi"
assert(depWithExclusions.exclude == CovariantSet(mod"io.argonaut::argonaut", mod"org.fusesource.jansi:jansi"))(Note the use of % as a separator in excluded dependencies).
Custom parameters can be passed in dependencies:
val depWithParams = dep"io.get-coursier::coursier:2.0.6,url=https://dl.cs/cs.jar,intransitive"
assert(depWithParams.userParams == Seq("url" -> Some("https://dl.cs/cs.jar"), "intransitive" -> None))dependency only keeps these parameters, it's up to users to give them a meaning. When
converting to a coursier interface dependency, classifier,
type, ext, and intransitive are taken into account. Other parameters, like url above, are
ignored.
assert(ScalaVersion.binary("2.13.6") == "2.13")
assert(ScalaVersion.binary("3.0.0") == "3")assert(ScalaVersion.jsBinary("1.5.1") == Some("1"))
assert(ScalaVersion.nativeBinary("0.4.0") == Some("0.4"))Add io.get-coursier::dependency-interface:0.3.4 to your build:
// Mill
def mvnDeps = Seq(mvn"io.get-coursier::dependency-interface:0.3.4")
// sbt
libraryDependencies += "io.get-coursier" %% "dependency-interface" % "0.3.4"These conversions only accept Module and Dependency, so applyParams must be called first
on Scala modules and dependencies:
import dependency.api.ApiConverter
ApiConverter.module(mod"io.get-coursier::coursier".applyParams(ScalaParameters("2.13.6"))) // coursierapi.Module
ApiConverter.dependency(dep"io.get-coursier::coursier:2.0.6".applyParams(ScalaParameters("2.13.6"))) // coursierapi.DependencyAlternatively, the dependency-interface module also provides implicit conversions for these:
import dependency.api.ops._
mod"io.get-coursier::coursier".applyParams(ScalaParameters("2.13.6")).toCs // coursierapi.Module
dep"io.get-coursier::coursier:2.0.6".applyParams(ScalaParameters("2.13.6")).toCs // coursierapi.Dependencydependency is built with Mill. The mill launcher is in the repository,
and downloads Mill and a JVM itself, so nothing needs to be installed beforehand (except on Linux
systems with a glibc too old for Mill's native launcher, where it falls back to one that needs a
JVM):
$ ./mill -i __.test # run the tests
$ ./mill -i __.mimaReportBinaryIssues # check binary compatibility
$ ./mill -i mdoc # compile the README examples
mdoc runs mdoc with the coursier launcher, which the
build downloads itself (its version is pinned in mill-build/src/Deps.scala). It leaves README.md
as is, and only checks that the examples in scala mdoc code blocks compile and run.
Versions are deduced from v* git tags. Pushes to main publish snapshots, and pushing a tag
uploads a release to Sonatype Central, where it still has to be released manually. Snapshots and
releases are also published to GitHub Packages, on a
best-effort basis.
Licensed under the Apache 2.0 license.