Skip to content

Commit 210916e

Browse files
authored
Make runtime deps transitive (#3774)
Fixes #3761 * Introduce `runModuleDeps`, the runtime version of `moduleDeps` and `compileModuleDeps` * We introduce `transitiveRunIvyDeps`, the runtime version of `transitiveIvyDeps`, and use it in most places as a replacement for `runIvyDeps` * I also flipped the ordering of `transitiveModuleDeps`/`transitiveRunModuleDeps` to put `this` on the right/last, to follow the rest of the module resolution logic where "upstream" things come on the left/first in the classpath ordering There are some behavioral changes, e.g. the A module's direct `compileModuleDeps` no longer end up on your `runClasspath`, as shown by the change in the test case `mill.scalalib.ScalaMultiModuleClasspathsTests.modCompile`. I think the new behavior is more correct than the old one? One (benign?) consequence of this change is that the contents of the various `*run*` tasks now have considerable overlap with the non-`run` tasks, e.g. `transitiveRunIvyDeps` overlaps with `transitiveIvyDeps`. The way transitive runtime dependencies are now managed, both "`run{Module,Ivy}Dep` of `{module,ivy}Dep`" and "`{module,ivy}Dep` of `run{Module,Ivy}Dep`" are both aggregated into the run classpath. I'm not sure if this matches what Maven does (it does according to chatgpt!) but it seems reasonable to me Added some additional unit tests to cover the new transitive behavior. The whole dependency-wiring stuff is pretty gnarly but hopefully the existing test suite will stop us from breaking too much (especially `mill.scalalib.ScalaMultiModuleClasspathsTests` which is pretty rigorous and we add to). This is a binary-compatible but semantically incompatible change, would be good to get it into 0.12.0 Best reviewed with `Hide Whitespace` enabled
1 parent d3b4149 commit 210916e

File tree

5 files changed

+434
-235
lines changed

5 files changed

+434
-235
lines changed

integration/feature/full-run-logs/src/FullRunLogsTests.scala

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,26 @@ object FullRunLogsTests extends UtestIntegrationTestSuite {
2727

2828
val res = eval(("--ticker", "true", "run", "--text", "hello"))
2929
res.isSuccess ==> true
30-
assert(res.out == "[46] <h1>hello</h1>")
30+
assert("\\[\\d+\\] <h1>hello</h1>".r.matches(res.out))
3131

3232
val expectedErrorRegex =
3333
s"""==================================================== run --text hello ================================================
3434
|======================================================================================================================
35-
|[build.mill-56/60] compile
36-
|[build.mill-56] [info] compiling 1 Scala source to ${tester.workspacePath}/out/mill-build/compile.dest/classes ...
37-
|[build.mill-56] [info] done compiling
38-
|[40/46] compile
39-
|[40] [info] compiling 1 Java source to ${tester.workspacePath}/out/compile.dest/classes ...
40-
|[40] [info] done compiling
41-
|[46/46] run
42-
|[46/46] ============================================ run --text hello ============================================<seconds-digits>s
35+
|[build.mill-<digits>/<digits>] compile
36+
|[build.mill-<digits>] [info] compiling 1 Scala source to ${tester.workspacePath}/out/mill-build/compile.dest/classes ...
37+
|[build.mill-<digits>] [info] done compiling
38+
|[<digits>/<digits>] compile
39+
|[<digits>] [info] compiling 1 Java source to ${tester.workspacePath}/out/compile.dest/classes ...
40+
|[<digits>] [info] done compiling
41+
|[<digits>/<digits>] run
42+
|[<digits>/<digits>] ============================================ run --text hello ============================================<digits>s
4343
|======================================================================================================================"""
4444
.stripMargin
4545
.replaceAll("(\r\n)|\r", "\n")
4646
.replace('\\', '/')
47-
.split("<seconds-digits>")
47+
.split("<digits>")
4848
.map(java.util.regex.Pattern.quote)
49-
.mkString("=? [\\d]+")
49+
.mkString("=? ?[\\d]+")
5050

5151
assert(expectedErrorRegex.r.matches(res.err.replace('\\', '/').replaceAll("(\r\n)|\r", "\n")))
5252
}

scalalib/src/mill/scalalib/JavaModule.scala

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ trait JavaModule
190190
moduleDeps
191191
}
192192

193+
/**
194+
* Same as [[moduleDeps]] but checked to not contain cycles.
195+
* Prefer this over using [[moduleDeps]] directly.
196+
*/
197+
final def runModuleDepsChecked: Seq[JavaModule] = {
198+
// trigger initialization to check for cycles
199+
recRunModuleDeps
200+
runModuleDeps
201+
}
202+
193203
/** Should only be called from [[moduleDepsChecked]] */
194204
private lazy val recModuleDeps: Seq[JavaModule] =
195205
ModuleUtils.recursive[JavaModule](
@@ -198,9 +208,26 @@ trait JavaModule
198208
_.moduleDeps
199209
)
200210

201-
/** The compile-only direct dependencies of this module. */
211+
/** Should only be called from [[moduleDepsChecked]] */
212+
private lazy val recRunModuleDeps: Seq[JavaModule] =
213+
ModuleUtils.recursive[JavaModule](
214+
(millModuleSegments ++ Seq(Segment.Label("moduleDeps"))).render,
215+
this,
216+
m => m.runModuleDeps ++ m.moduleDeps
217+
)
218+
219+
/**
220+
* The compile-only direct dependencies of this module. These are *not*
221+
* transitive, and only take effect in the module that they are declared in.
222+
*/
202223
def compileModuleDeps: Seq[JavaModule] = Seq.empty
203224

225+
/**
226+
* The runtime-only direct dependencies of this module. These *are* transitive,
227+
* and so get propagated to downstream modules automatically
228+
*/
229+
def runModuleDeps: Seq[JavaModule] = Seq.empty
230+
204231
/** Same as [[compileModuleDeps]] but checked to not contain cycles. */
205232
final def compileModuleDepsChecked: Seq[JavaModule] = {
206233
// trigger initialization to check for cycles
@@ -217,16 +244,22 @@ trait JavaModule
217244
)
218245

219246
/** The direct and indirect dependencies of this module */
220-
def recursiveModuleDeps: Seq[JavaModule] = {
221-
// moduleDeps.flatMap(_.transitiveModuleDeps).distinct
222-
recModuleDeps
223-
}
247+
def recursiveModuleDeps: Seq[JavaModule] = { recModuleDeps }
248+
249+
/** The direct and indirect runtime module dependencies of this module */
250+
def recursiveRunModuleDeps: Seq[JavaModule] = { recRunModuleDeps }
224251

225252
/**
226253
* Like `recursiveModuleDeps` but also include the module itself,
227254
* basically the modules whose classpath are needed at runtime
228255
*/
229-
def transitiveModuleDeps: Seq[JavaModule] = Seq(this) ++ recursiveModuleDeps
256+
def transitiveModuleDeps: Seq[JavaModule] = recursiveModuleDeps ++ Seq(this)
257+
258+
/**
259+
* Like `recursiveModuleDeps` but also include the module itself,
260+
* basically the modules whose classpath are needed at runtime
261+
*/
262+
def transitiveRunModuleDeps: Seq[JavaModule] = recursiveRunModuleDeps ++ Seq(this)
230263

231264
/**
232265
* All direct and indirect module dependencies of this module, including
@@ -240,6 +273,17 @@ trait JavaModule
240273
(moduleDepsChecked ++ compileModuleDepsChecked).flatMap(_.transitiveModuleDeps).distinct
241274
}
242275

276+
/**
277+
* All direct and indirect module dependencies of this module, including
278+
* compile-only dependencies: basically the modules whose classpath are needed
279+
* at runtime.
280+
*
281+
* Note that `runModuleDeps` are defined to be transitive
282+
*/
283+
def transitiveModuleRunModuleDeps: Seq[JavaModule] = {
284+
(runModuleDepsChecked ++ moduleDepsChecked).flatMap(_.transitiveRunModuleDeps).distinct
285+
}
286+
243287
/** The compile-only transitive ivy dependencies of this module and all it's upstream compile-only modules. */
244288
def transitiveCompileIvyDeps: T[Agg[BoundDep]] = Task {
245289
// We never include compile-only dependencies transitively, but we must include normal transitive dependencies!
@@ -287,6 +331,17 @@ trait JavaModule
287331
T.traverse(moduleDepsChecked)(_.transitiveIvyDeps)().flatten
288332
}
289333

334+
/**
335+
* The transitive run ivy dependencies of this module and all it's upstream modules.
336+
* This is calculated from [[runIvyDeps]], [[mandatoryIvyDeps]] and recursively from [[moduleDeps]].
337+
*/
338+
def transitiveRunIvyDeps: T[Agg[BoundDep]] = Task {
339+
runIvyDeps().map(bindDependency()) ++
340+
T.traverse(moduleDepsChecked)(_.transitiveRunIvyDeps)().flatten ++
341+
T.traverse(runModuleDepsChecked)(_.transitiveIvyDeps)().flatten ++
342+
T.traverse(runModuleDepsChecked)(_.transitiveRunIvyDeps)().flatten
343+
}
344+
290345
/**
291346
* The upstream compilation output of all this module's upstream modules
292347
*/
@@ -298,7 +353,7 @@ trait JavaModule
298353
* The transitive version of `localClasspath`
299354
*/
300355
def transitiveLocalClasspath: T[Agg[PathRef]] = Task {
301-
T.traverse(transitiveModuleCompileModuleDeps)(_.localClasspath)().flatten
356+
T.traverse(transitiveModuleRunModuleDeps)(_.localClasspath)().flatten
302357
}
303358

304359
/**
@@ -493,7 +548,7 @@ trait JavaModule
493548
* excluding upstream modules and third-party dependencies, but including unmanaged dependencies.
494549
*
495550
* This is build from [[localCompileClasspath]] and [[localRunClasspath]]
496-
* as the parts available "before compilation" and "after compiliation".
551+
* as the parts available "before compilation" and "after compilation".
497552
*
498553
* Keep in sync with [[bspLocalClasspath]]
499554
*/
@@ -563,7 +618,7 @@ trait JavaModule
563618

564619
def resolvedRunIvyDeps: T[Agg[PathRef]] = Task {
565620
defaultResolver().resolveDeps(
566-
runIvyDeps().map(bindDependency()) ++ transitiveIvyDeps(),
621+
transitiveRunIvyDeps() ++ transitiveIvyDeps(),
567622
artifactTypes = Some(artifactTypes())
568623
)
569624
}
@@ -877,7 +932,7 @@ trait JavaModule
877932
printDepsTree(
878933
args.inverse.value,
879934
Task.Anon {
880-
transitiveCompileIvyDeps() ++ runIvyDeps().map(bindDependency())
935+
transitiveCompileIvyDeps() ++ transitiveRunIvyDeps()
881936
},
882937
validModules
883938
)()
@@ -890,7 +945,7 @@ trait JavaModule
890945
Task.Command {
891946
printDepsTree(
892947
args.inverse.value,
893-
Task.Anon { runIvyDeps().map(bindDependency()) },
948+
Task.Anon { transitiveRunIvyDeps() },
894949
validModules
895950
)()
896951
}
@@ -1046,7 +1101,7 @@ trait JavaModule
10461101
},
10471102
Task.Anon {
10481103
defaultResolver().resolveDeps(
1049-
runIvyDeps().map(bindDependency()) ++ transitiveIvyDeps(),
1104+
transitiveRunIvyDeps() ++ transitiveIvyDeps(),
10501105
sources = true
10511106
)
10521107
}

scalalib/src/mill/scalalib/ScalaModule.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ trait ScalaModule extends JavaModule with TestModule.ScalaModuleBase { outer =>
481481
)
482482
}
483483
val bind = bindDependency()
484-
runIvyDeps().map(bind) ++ transitiveIvyDeps() ++
484+
transitiveRunIvyDeps() ++ transitiveIvyDeps() ++
485485
Agg(ivy"com.lihaoyi:::ammonite:${ammVersion}").map(bind)
486486
}
487487
}

scalalib/test/src/mill/scalalib/ScalaIvyDepsTests.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@ object ScalaIvyDepsTests extends TestSuite {
1717
}
1818
}
1919

20+
object TransitiveRunIvyDeps extends TestBaseModule {
21+
object upstream extends JavaModule {
22+
def ivyDeps = Agg(ivy"org.slf4j:slf4j-api:2.0.16")
23+
def runIvyDeps = Agg(ivy"ch.qos.logback:logback-classic:1.5.10")
24+
}
25+
26+
object downstream extends JavaModule {
27+
// Make sure runIvyDeps are transitively picked up from normal `moduleDeps`
28+
def moduleDeps = Seq(upstream)
29+
}
30+
}
31+
32+
object TransitiveRunIvyDeps2 extends TestBaseModule {
33+
object upstream extends JavaModule {
34+
def ivyDeps = Agg(ivy"org.slf4j:slf4j-api:2.0.16")
35+
def runIvyDeps = Agg(ivy"ch.qos.logback:logback-classic:1.5.10")
36+
}
37+
38+
object downstream extends JavaModule {
39+
// Make sure both ivyDeps and runIvyDeps are transitively picked up from `runModuleDeps`
40+
def runModuleDeps = Seq(upstream)
41+
}
42+
}
43+
2044
def tests: Tests = Tests {
2145

2246
test("ivyDeps") - UnitTester(HelloWorldIvyDeps, resourcePath).scoped { eval =>
@@ -33,5 +57,23 @@ object ScalaIvyDepsTests extends TestSuite {
3357
)
3458
}
3559

60+
test("transitiveRun") - UnitTester(TransitiveRunIvyDeps, resourcePath).scoped { eval =>
61+
val Right(result2) = eval.apply(TransitiveRunIvyDeps.downstream.runClasspath)
62+
63+
assert(
64+
result2.value.exists(_.path.last == "logback-classic-1.5.10.jar")
65+
)
66+
}
67+
68+
test("transitiveLocalRuntimeDepsRun") - UnitTester(TransitiveRunIvyDeps2, resourcePath).scoped {
69+
eval =>
70+
val Right(result2) = eval.apply(TransitiveRunIvyDeps2.downstream.runClasspath)
71+
72+
assert(
73+
result2.value.exists(_.path.last == "logback-classic-1.5.10.jar"),
74+
result2.value.exists(_.path.last == "slf4j-api-2.0.16.jar")
75+
)
76+
}
77+
3678
}
3779
}

0 commit comments

Comments
 (0)