Skip to content

Commit b59b6c2

Browse files
alexarchambaultjodersky
authored andcommitted
Add BOM / dependency management support (com-lihaoyi#3924)
This PR adds support for user-specified BOMs and dependency management in Mill. BOM support allows users to pass the coordinates of an existing Maven "Bill of Material" (BOM), such as [this one](https://repo1.maven.org/maven2/io/quarkus/quarkus-bom/3.17.0/quarkus-bom-3.17.0.pom), that contains versions of dependencies, meant to override those pulled during dependency resolution. (They can also add exclusions to dependencies.) ```scala def bomDeps = Agg( ivy"io.quarkus:quarkus-bom:3.17.0" ) ``` It also allows users to specify the coordinates of a parent POM, which are taken into account just like a BOM: ```scala def parentDep = ivy"org.apache.spark::spark-parent:3.5.3" ``` (in line with `PublishModule#pomParentProject` that's been added recently) It allows users to specify "dependency management", which act like the dependencies listed in a BOM: versions in dependency management override those pulled transitively during dependency resolution, and exclusions in its dependencies are added to the same dependencies during dependency resolution. ```scala def dependencyManagement = Agg( ivy"com.google.protobuf:protobuf-java:4.28.3", ivy"org.java-websocket:Java-WebSocket:_" // placeholder version - this one only adds exclusions, no version override .exclude(("org.slf4j", "slf4j-api")) ) ``` BOM and dependency management also allow for "placeholder" versions: users can use `_` as version in their `ivyDeps`, and the version of that dependency will be picked either in dependency management or in BOMs: ```scala def bomDeps = Agg( ivy"com.google.cloud:libraries-bom:26.50.0" ) def ivyDeps = Agg( ivy"com.google.protobuf:protobuf-java:_" ) ``` A tricky aspect of that PR is that details about BOMs and dependency management have to be passed around via several paths: - in the current module: BOMs and dependency management have to be taken into account during dependency resolution of the module they're added to - via `moduleDeps`: BOMs and dependency management of module dependencies have to be applied to the dependencies of the module they come from - ~to transitive modules pulled via `moduleDeps`: BOMs and dependency management of a module dependency have to be applied to the dependencies of modules they pull transitively (if A depends on B and B depends on C, from A, the BOMs and dep mgmt of B apply to C's dependencies too)~ (worked out-of-the-box with the previous point, via `transitiveIvyDeps`) - via `ivy.xml`: when publishing to Ivy repositories (like during `pubishLocal`), BOMs and dep mgmt details need to be written in the `ivy.xml` file, so that they're taken into account when resolving that module from the Ivy repo - via POM files: when publishing to Maven repositories, BOMs and dep mgmt details need to be written to POMs, so that they're taken into account when resolving that module from the Maven repo Fixes com-lihaoyi#1975
1 parent a73ca5f commit b59b6c2

File tree

16 files changed

+988
-42
lines changed

16 files changed

+988
-42
lines changed

docs/modules/ROOT/pages/fundamentals/library-deps.adoc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,34 @@ def runIvyDeps = Agg(
9696

9797
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.
9898

99+
== Dependency management
100+
101+
Dependency management consists in listing dependencies whose versions we want to force. Having
102+
a dependency in dependency management doesn't mean that this dependency will be fetched, only
103+
that
104+
105+
* if it ends up being fetched transitively, its version will be forced to the one in dependency management
106+
107+
* if its version is empty in an `ivyDeps` section in Mill, the version from dependency management will be used
108+
109+
Dependency management also allows to add exclusions to dependencies, both explicit dependencies and
110+
transitive ones.
111+
112+
Dependency management can be passed to Mill in two ways:
113+
114+
* via external Maven BOMs, like https://repo1.maven.org/maven2/com/google/cloud/libraries-bom/26.50.0/libraries-bom-26.50.0.pom[this one],
115+
whose Maven coordinates are `com.google.cloud:libraries-bom:26.50.0`
116+
117+
* via the `depManagement` task, that allows to directly list dependencies whose versions we want to enforce
118+
119+
=== External BOMs
120+
121+
include::partial$example/fundamentals/library-deps/bom-1-external-bom.adoc[]
122+
123+
=== Dependency management task
124+
125+
include::partial$example/fundamentals/library-deps/bom-2-dependency-management.adoc[]
126+
99127
== Searching For Dependency Updates
100128

101129
include::partial$example/fundamentals/dependencies/1-search-updates.adoc[]

docs/modules/ROOT/pages/javalib/dependencies.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ include::partial$example/javalib/dependencies/1-ivy-deps.adoc[]
1313

1414
include::partial$example/javalib/dependencies/2-run-compile-deps.adoc[]
1515

16+
== Dependency Management
17+
18+
Mill has support for dependency management, see the
19+
xref:fundamentals/library-deps.adoc#_dependency_management[Dependency Management section]
20+
in xref:fundamentals/library-deps.adoc[].
1621

1722
== Unmanaged Jars
1823

docs/modules/ROOT/pages/kotlinlib/dependencies.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ include::partial$example/kotlinlib/dependencies/1-ivy-deps.adoc[]
1919

2020
include::partial$example/kotlinlib/dependencies/2-run-compile-deps.adoc[]
2121

22+
== Dependency Management
23+
24+
Mill has support for dependency management, see the
25+
xref:fundamentals/library-deps.adoc#_dependency_management[Dependency Management section]
26+
in xref:fundamentals/library-deps.adoc[].
27+
2228
== Unmanaged Jars
2329

2430
include::partial$example/kotlinlib/dependencies/3-unmanaged-jars.adoc[]

docs/modules/ROOT/pages/scalalib/dependencies.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ include::partial$example/scalalib/dependencies/1-ivy-deps.adoc[]
1616

1717
include::partial$example/scalalib/dependencies/2-run-compile-deps.adoc[]
1818

19+
== Dependency Management
20+
21+
Mill has support for dependency management, see the
22+
xref:fundamentals/library-deps.adoc#_dependency_management[Dependency Management section]
23+
in xref:fundamentals/library-deps.adoc[].
1924

2025
== Unmanaged Jars
2126

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Pass an external BOM to a `JavaModule` / `ScalaModule` / `KotlinModule` with `bomDeps`, like
2+
3+
//// SNIPPET:BUILD1
4+
package build
5+
import mill._, javalib._
6+
7+
object foo extends JavaModule {
8+
def bomDeps = Agg(
9+
ivy"com.google.cloud:libraries-bom:26.50.0"
10+
)
11+
def ivyDeps = Agg(
12+
ivy"io.grpc:grpc-protobuf"
13+
)
14+
}
15+
16+
// The version of grpc-protobuf (`io.grpc:grpc-protobuf`) isn't written down here, so the version
17+
// from the BOM, `1.67.1` is used.
18+
//
19+
// Also, by default, grpc-protobuf `1.67.1` pulls version `3.25.3` of protobuf-java (`com.google.protobuf:protobuf-java`) .
20+
// But the BOM specifies another version for that dependency, `4.28.3`, so
21+
// protobuf-java `4.28.3` ends up being pulled here.
22+
//
23+
// Several BOMs can be passed to `bomDeps`. If several specify a version for a dependency,
24+
// the version from the first one in the `bomDeps` list is used. If several specify exclusions
25+
// for a dependency, all exclusions are added to that dependency.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Pass dependencies to `depManagement` in a `JavaModule` / `ScalaModule` / `KotlinModule`, like
2+
3+
//// SNIPPET:BUILD1
4+
package build
5+
import mill._, javalib._
6+
7+
object foo extends JavaModule {
8+
def depManagement = Agg(
9+
ivy"com.google.protobuf:protobuf-java:4.28.3",
10+
ivy"io.grpc:grpc-protobuf:1.67.1"
11+
)
12+
def ivyDeps = Agg(
13+
ivy"io.grpc:grpc-protobuf"
14+
)
15+
}
16+
17+
// The version of grpc-protobuf (`io.grpc:grpc-protobuf`) isn't written down here, so the version
18+
// found in `depManagement`, `1.67.1` is used.
19+
//
20+
// Also, by default, grpc-protobuf `1.67.1` pulls version `3.25.3` of protobuf-java (`com.google.protobuf:protobuf-java`) .
21+
// But `depManagement` specifies another version for that dependency, `4.28.3`, so
22+
// protobuf-java `4.28.3` ends up being pulled here.
23+
24+
// One can also add exclusions via dependency management, like
25+
26+
object bar extends JavaModule {
27+
def depManagement = Agg(
28+
ivy"io.grpc:grpc-protobuf:1.67.1"
29+
.exclude(("com.google.protobuf", "protobuf-java"))
30+
)
31+
def ivyDeps = Agg(
32+
ivy"io.grpc:grpc-protobuf"
33+
)
34+
}
35+
36+
// Here, grpc-protobuf has an empty version in `ivyDeps`, so the one in `depManagement`,
37+
// `1.67.1`, is used. Also, `com.google.protobuf:protobuf-java` is excluded from grpc-protobuf
38+
// in `depManagement`, so it ends up being excluded from it in `ivyDeps` too.
39+
40+
// If one wants to add exclusions via `depManagement`, specifying a version is optional,
41+
// like
42+
43+
object baz extends JavaModule {
44+
def depManagement = Agg(
45+
ivy"io.grpc:grpc-protobuf"
46+
.exclude(("com.google.protobuf", "protobuf-java"))
47+
)
48+
def ivyDeps = Agg(
49+
ivy"io.grpc:grpc-protobuf:1.67.1"
50+
)
51+
}
52+
53+
// Here, given that grpc-protobuf is fetched during dependency resolution,
54+
// `com.google.protobuf:protobuf-java` is excluded from it because of the dependency management.

example/package.mill

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ object `package` extends RootModule with Module {
8282
object cross extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "cross"))
8383
object `out-dir` extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "out-dir"))
8484
object libraries extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "libraries"))
85+
object `library-deps` extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "library-deps"))
8586
}
8687

8788
object depth extends Module {

main/util/src/mill/util/CoursierSupport.scala

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ trait CoursierSupport {
3131
ctx.fold(cache)(c => cache.withLogger(new TickerResolutionLogger(c)))
3232
}
3333

34+
private def isLocalTestDep(dep: Dependency): Option[Seq[PathRef]] = {
35+
val org = dep.module.organization.value
36+
val name = dep.module.name.value
37+
val classpathKey = s"$org-$name"
38+
39+
val classpathResourceText =
40+
try Some(os.read(
41+
os.resource(getClass.getClassLoader) / "mill/local-test-overrides" / classpathKey
42+
))
43+
catch { case e: os.ResourceNotFoundException => None }
44+
45+
classpathResourceText.map(_.linesIterator.map(s => PathRef(os.Path(s))).toSeq)
46+
}
47+
3448
/**
3549
* Resolve dependencies using Coursier.
3650
*
@@ -51,26 +65,8 @@ trait CoursierSupport {
5165
artifactTypes: Option[Set[Type]] = None,
5266
resolutionParams: ResolutionParams = ResolutionParams()
5367
): Result[Agg[PathRef]] = {
54-
def isLocalTestDep(dep: Dependency): Option[Seq[PathRef]] = {
55-
val org = dep.module.organization.value
56-
val name = dep.module.name.value
57-
val classpathKey = s"$org-$name"
58-
59-
val classpathResourceText =
60-
try Some(os.read(
61-
os.resource(getClass.getClassLoader) / "mill/local-test-overrides" / classpathKey
62-
))
63-
catch { case e: os.ResourceNotFoundException => None }
64-
65-
classpathResourceText.map(_.linesIterator.map(s => PathRef(os.Path(s))).toSeq)
66-
}
67-
68-
val (localTestDeps, remoteDeps) = deps.iterator.toSeq.partitionMap(d =>
69-
isLocalTestDep(d) match {
70-
case None => Right(d)
71-
case Some(vs) => Left(vs)
72-
}
73-
)
68+
val (localTestDeps, remoteDeps) =
69+
deps.iterator.toSeq.partitionMap(d => isLocalTestDep(d).toLeft(d))
7470

7571
val resolutionRes = resolveDependenciesMetadataSafe(
7672
repositories,
@@ -262,6 +258,7 @@ trait CoursierSupport {
262258

263259
val rootDeps = deps.iterator
264260
.map(d => mapDependencies.fold(d)(_.apply(d)))
261+
.filter(dep => isLocalTestDep(dep).isEmpty)
265262
.toSeq
266263

267264
val forceVersions = force.iterator

scalalib/src/mill/scalalib/CoursierModule.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,37 @@ object CoursierModule {
231231
sources: Boolean
232232
): Agg[PathRef] =
233233
resolveDeps(deps, sources, None)
234+
235+
/**
236+
* Processes dependencies and BOMs with coursier
237+
*
238+
* This makes coursier read and process BOM dependencies, and fill version placeholders
239+
* in dependencies with the BOMs.
240+
*
241+
* Note that this doesn't throw when a version placeholder cannot be filled, and just leaves
242+
* the placeholder behind.
243+
*
244+
* @param deps dependencies that might have placeholder versions ("_" as version)
245+
* @param resolutionParams coursier resolution parameters
246+
* @return dependencies with version placeholder filled
247+
*/
248+
def processDeps[T: CoursierModule.Resolvable](
249+
deps: IterableOnce[T],
250+
resolutionParams: ResolutionParams = ResolutionParams()
251+
): Seq[Dependency] = {
252+
val deps0 = deps
253+
.map(implicitly[CoursierModule.Resolvable[T]].bind(_, bind))
254+
val res = Lib.resolveDependenciesMetadataSafe(
255+
repositories = repositories,
256+
deps = deps0,
257+
mapDependencies = mapDependencies,
258+
customizer = customizer,
259+
coursierCacheCustomizer = coursierCacheCustomizer,
260+
ctx = ctx,
261+
resolutionParams = resolutionParams
262+
).getOrThrow
263+
res.processedRootDependencies
264+
}
234265
}
235266

236267
sealed trait Resolvable[T] {

scalalib/src/mill/scalalib/Dep.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ object Dep {
118118
}
119119

120120
(module.split(':') match {
121+
case Array(a, b) => Dep(a, b, "_", cross = empty(platformed = false))
122+
case Array(a, "", b) => Dep(a, b, "_", cross = Binary(platformed = false))
121123
case Array(a, b, c) => Dep(a, b, c, cross = empty(platformed = false))
122124
case Array(a, b, "", c) => Dep(a, b, c, cross = empty(platformed = true))
123125
case Array(a, "", b, c) => Dep(a, b, c, cross = Binary(platformed = false))

0 commit comments

Comments
 (0)