Skip to content

Commit b5e677b

Browse files
authored
Include ExternalModule and ScriptModule prefixes when rendering task selectors (#6102)
Fixes #6095 We preserve the distinction of `ExternalModule`/`ScriptModule` information in the first `Segment.Label` in `Segments`: - If it starts with a `./`, it must be an `ScriptModule`, with the rest of the token being the filesystem path to the script - If it ends with a `/`, it must be an `ExternalModule`, with the rest of the token being the JVM package path to the singleton This PR also tweaks the `out/` folder layout from external modules, preserving the `.`s rather than replacing them with `/`s. This should help avoid filesystem collisions between external modules and `build.mill` modules with similar JVM paths, at least for most external modules which have at least one `.` Covered by an integration test
1 parent e255361 commit b5e677b

File tree

27 files changed

+141
-77
lines changed

27 files changed

+141
-77
lines changed

core/api/daemon/src/mill/api/daemon/Segments.scala

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import scala.math.Ordering.Implicits.seqOrdering
55
/**
66
* Models a path with the Mill build hierarchy, e.g. `amm.util[2.11].test.compile`.
77
*
8-
* `.`-separated segments are [[Segment.Label]]s,
9-
* while `[]`-delimited segments are [[Segment.Cross]]s
8+
* - `.`-separated segments are [[Segment.Label]]s,
9+
* - `[]`-delimited segments are [[Segment.Cross]]s
10+
* - If the first segment starts with `./`, it refers to a single-file script
11+
* - If the first segment ends with `/`, it refers to an external module
1012
*/
1113
final case class Segments private (value: Seq[Segment]) {
1214

@@ -30,7 +32,7 @@ final case class Segments private (value: Seq[Segment]) {
3032

3133
def render: String = {
3234
def renderCross(cross: Segment.Cross): String = "[" + cross.value.mkString(",") + "]"
33-
value.toList match {
35+
def renderValue(valueList: List[Segment]) = valueList match {
3436
case Nil => ""
3537
case head :: rest =>
3638
val headSegment = head match
@@ -42,6 +44,15 @@ final case class Segments private (value: Seq[Segment]) {
4244
}
4345
headSegment + stringSegments.mkString
4446
}
47+
48+
value.toList match {
49+
// ScriptModule segments always starts with `./`
50+
case Segment.Label(s"./$first") :: Nil => s"./$first"
51+
case Segment.Label(s"./$first") :: next :: rest => s"./$first:${renderValue(next :: rest)}"
52+
// ExternalModule segments always ends with '/'
53+
case Segment.Label(s"$first/") :: next :: rest => s"$first/${renderValue(next :: rest)}"
54+
case valueList => renderValue(valueList)
55+
}
4556
}
4657
override lazy val hashCode: Int = value.hashCode()
4758
}

core/api/src/mill/api/Evaluator.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ trait Evaluator extends AutoCloseable with EvaluatorApi {
4848
allowPositionalCommandArgs: Boolean = false,
4949
resolveToModuleTasks: Boolean = false
5050
): mill.api.Result[List[Resolved]] = {
51-
// These are used in the overrides.
52-
val _ = scriptArgs
53-
val _ = selectMode
54-
val _ = allowPositionalCommandArgs
55-
val _ = resolveToModuleTasks
56-
5751
mill.api.Result.Success(Nil)
5852
}
5953

core/api/src/mill/api/ExecutionPaths.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ object ExecutionPaths {
3535
// Dollar sign `$` is our masking-character
3636
private val Dollar = "[$]".r
3737
// Forward-slashed are reserved for directory delimiters
38-
private val Slash = "/".r
3938

4039
private val steps: Seq[String => String] = Seq(
4140
// Step 1: mask all existing dollar signs, so we can use the dollar as masking character
@@ -46,13 +45,11 @@ object ExecutionPaths {
4645
case s => s
4746
},
4847
// Step 3: Replace colon (:) with $colon
49-
s => Colon.replaceAllIn(s, Matcher.quoteReplacement("$colon")),
50-
// Step 4: Replace slash (/) with $slash
51-
s => Slash.replaceAllIn(s, Matcher.quoteReplacement("$slash"))
48+
s => Colon.replaceAllIn(s, Matcher.quoteReplacement("$colon"))
5249
)
5350

5451
def sanitizePathSegment(segment: String): os.PathChunk = {
5552
// sanitize and implicitly convert to PathChunk
56-
steps.foldLeft(segment) { (segment, f) => f(segment) }
53+
os.SubPath(steps.foldLeft(segment) { (segment, f) => f(segment) })
5754
}
5855
}

core/api/src/mill/api/ExternalModule.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ abstract class ExternalModule(using
2727
!" #".exists(millModuleEnclosing0.value.contains(_)),
2828
"External modules must be at a top-level static path, not " + millModuleEnclosing0.value
2929
)
30-
override def moduleSegments: Segments = {
31-
Segments(millModuleEnclosing0.value.split('.').map(Segment.Label(_)).toIndexedSeq)
32-
}
30+
31+
// Include a trailing `/` in the external module first segment to distinguish it
32+
// from script modules or normal build.mill modules
33+
override def moduleSegments: Segments = Segments.labels(millModuleEnclosing0.value + "/")
3334
}
3435

3536
object ExternalModule {

core/api/src/mill/api/internal/Resolved.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package mill.api.internal
33
import mill.api.daemon.Segments
44

55
private[mill] sealed trait Resolved {
6+
def rootModule: RootModule0
67
def segments: Segments
78
def cls: Class[?]
9+
def fullSegments: Segments = rootModule.moduleSegments ++ segments
810
}
911

1012
private[mill] object Resolved {
@@ -23,7 +25,7 @@ private[mill] object Resolved {
2325
}
2426
}
2527

26-
case class Module(segments: Segments, cls: Class[?]) extends Resolved
27-
case class Command(segments: Segments, cls: Class[?]) extends Resolved
28-
case class NamedTask(segments: Segments, cls: Class[?]) extends Resolved
28+
case class Module(rootModule: RootModule0, segments: Segments, cls: Class[?]) extends Resolved
29+
case class Command(rootModule: RootModule0, segments: Segments, cls: Class[?]) extends Resolved
30+
case class NamedTask(rootModule: RootModule0, segments: Segments, cls: Class[?]) extends Resolved
2931
}

core/api/test/src/mill/api/ExecutionPathsTests.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ object ExecutionPathsTests extends TestSuite {
1616
// do not collide with the applied `$`-masking character
1717
"a$colonb" -> "a$$colonb",
1818
// replace not just the first $
19-
"a$$b" -> "a$$$$b",
20-
// replace a forward slash,
21-
"a/b" -> "a$slashb"
19+
"a$$b" -> "a$$$$b"
2220
)
2321
val noReplace = Seq(
2422
"con10.json"

core/exec/test/src/mill/exec/ModuleTests.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ object ModuleTests extends TestSuite {
5555
zresult == Right(Result(30, 1)),
5656
os.read(check.execution.outPath / "z.json").contains("30"),
5757
os.read(
58-
check.outPath / "mill/exec/TestExternalModule/x.json"
58+
check.outPath / "mill.exec.TestExternalModule/x.json"
5959
).contains("13"),
6060
os.read(
61-
check.outPath / "mill/exec/TestExternalModule/inner/y.json"
61+
check.outPath / "mill.exec.TestExternalModule/inner/y.json"
6262
).contains("17")
6363
)
6464
}

core/resolve/src/mill/resolve/Resolve.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private[mill] object Resolve {
2727
resolveToModuleTasks: Boolean,
2828
cache: ResolveCore.Cache
2929
) = {
30-
Result.Success(resolved.map(_.segments))
30+
Result.Success(resolved.map(_.fullSegments))
3131
}
3232

3333
private[mill] override def deduplicate(items: List[Segments]): List[Segments] = items.distinct
@@ -400,7 +400,7 @@ private[mill] trait Resolve[T] {
400400
sel: Segments,
401401
cache: ResolveCore.Cache
402402
): ResolveCore.Result = {
403-
val rootResolved = Resolved.Module(Segments(), rootModule.getClass)
403+
val rootResolved = Resolved.Module(rootModule, Segments(), rootModule.getClass)
404404
ResolveCore.resolve(
405405
rootModule = rootModule,
406406
remainingQuery = sel.value.toList,

core/resolve/src/mill/resolve/ResolveCore.scala

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ private object ResolveCore {
9191
): Result = {
9292

9393
def moduleClasses(resolved: Iterable[Resolved]): Set[Class[?]] = {
94-
resolved.collect { case Resolved.Module(_, cls) => cls }.toSet
94+
resolved.collect { case Resolved.Module(_, _, cls) => cls }.toSet
9595
}
9696

9797
remainingQuery match {
@@ -139,7 +139,7 @@ private object ResolveCore {
139139
case (Segment.Label(singleLabel), m: Resolved.Module) =>
140140
val resOrErr: mill.api.Result[Seq[Resolved]] = singleLabel match {
141141
case "__" =>
142-
val self = Seq(Resolved.Module(m.segments, m.cls))
142+
val self = Seq(Resolved.Module(rootModule, m.segments, m.cls))
143143
val transitiveOrErr =
144144
resolveTransitiveChildren(
145145
rootModule,
@@ -163,7 +163,7 @@ private object ResolveCore {
163163

164164
case pattern if pattern.startsWith("__:") =>
165165
val typePattern = pattern.split(":").drop(1)
166-
val self = Seq(Resolved.Module(m.segments, m.cls))
166+
val self = Seq(Resolved.Module(rootModule, m.segments, m.cls))
167167

168168
val transitiveOrErr = resolveTransitiveChildren(
169169
rootModule,
@@ -176,7 +176,7 @@ private object ResolveCore {
176176

177177
transitiveOrErr.map(transitive =>
178178
(self ++ transitive).collect {
179-
case r @ Resolved.Module(segments, cls)
179+
case r @ Resolved.Module(_, segments, cls)
180180
if classMatchesTypePred(typePattern)(cls) =>
181181
r
182182
}
@@ -192,7 +192,7 @@ private object ResolveCore {
192192
cache
193193
).map {
194194
_.collect {
195-
case r @ Resolved.Module(segments, cls)
195+
case r @ Resolved.Module(_, segments, cls)
196196
if classMatchesTypePred(typePattern)(cls) => r
197197
}
198198
}
@@ -237,7 +237,7 @@ private object ResolveCore {
237237
case mill.api.Result.Success(searchModules) =>
238238
recurse(
239239
searchModules
240-
.map(m => Resolved.Module(m.moduleSegments, m.getClass))
240+
.map(m => Resolved.Module(rootModule, m.moduleSegments, m.getClass))
241241
)
242242
}
243243

@@ -376,7 +376,7 @@ private object ResolveCore {
376376
instantiateModule(rootModule, segments, cache).map {
377377
case cross: Cross[_] =>
378378
for (item <- cross.items) yield {
379-
Resolved.Module(segments ++ Segment.Cross(item.crossSegments), item.cls)
379+
Resolved.Module(rootModule, segments ++ Segment.Cross(item.crossSegments), item.cls)
380380
}
381381

382382
case _ => Nil
@@ -385,9 +385,12 @@ private object ResolveCore {
385385

386386
def expandSegments(direct: Seq[(Resolved, Option[Module => mill.api.Result[Module]])]) = {
387387
direct.map {
388-
case (Resolved.Module(s, cls), _) => Resolved.Module(segments ++ s, cls)
389-
case (Resolved.NamedTask(s, enclosing), _) => Resolved.NamedTask(segments ++ s, enclosing)
390-
case (Resolved.Command(s, enclosing), _) => Resolved.Command(segments ++ s, enclosing)
388+
case (Resolved.Module(rootModule, s, cls), _) =>
389+
Resolved.Module(rootModule, segments ++ s, cls)
390+
case (Resolved.NamedTask(rootModule, s, enclosing), _) =>
391+
Resolved.NamedTask(rootModule, segments ++ s, enclosing)
392+
case (Resolved.Command(rootModule, s, enclosing), _) =>
393+
Resolved.Command(rootModule, segments ++ s, enclosing)
391394
}
392395
}
393396

@@ -417,6 +420,7 @@ private object ResolveCore {
417420
.map(c =>
418421
(
419422
Resolved.Module(
423+
rootModule,
420424
Segments.labels(c.moduleSegments.last.value),
421425
c.getClass
422426
),
@@ -429,7 +433,8 @@ private object ResolveCore {
429433
.reflectNestedObjects02[Module](cls, namePred, cache.getMethods)
430434
.collect {
431435
case (name, memberCls, getter) =>
432-
val resolved = Resolved.Module(Segments.labels(cache.decode(name)), memberCls)
436+
val resolved =
437+
Resolved.Module(rootModule, Segments.labels(cache.decode(name)), memberCls)
433438
val getter2 =
434439
Some((mod: Module) => mill.api.ExecResult.catchWrapException(getter(mod)))
435440
(resolved, getter2)
@@ -441,14 +446,14 @@ private object ResolveCore {
441446
val namedTasks = Reflect
442447
.reflect(cls, classOf[Task.Named[?]], namePred, noParams = true, cache.getMethods)
443448
.map { m =>
444-
Resolved.NamedTask(Segments.labels(cache.decode(m.getName)), cls) ->
449+
Resolved.NamedTask(rootModule, Segments.labels(cache.decode(m.getName)), cls) ->
445450
None
446451
}
447452

448453
val commands = Reflect
449454
.reflect(cls, classOf[Task.Command[?]], namePred, noParams = false, cache.getMethods)
450455
.map(m => cache.decode(m.getName))
451-
.map { name => Resolved.Command(Segments.labels(name), cls) -> None }
456+
.map { name => Resolved.Command(rootModule, Segments.labels(name), cls) -> None }
452457

453458
modulesOrErr.map(_ ++ namedTasks ++ commands)
454459
}

example/fundamentals/out-dir/1-out-files/build.mill

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ out/mill-daemon
125125
> cat out/mill-profile.json
126126
[
127127
{
128-
"label": "mill.javalib.JvmWorkerModule.internalWorker",
128+
"label": "mill.javalib.JvmWorkerModule/internalWorker",
129129
"millis": 0,
130130
"cached": true,
131131
"valueHashChanged": false,

0 commit comments

Comments
 (0)