diff --git a/example/thirdparty/mockito/build.mill b/example/thirdparty/mockito/build.mill index f00044a7b13..08a5ddca83c 100644 --- a/example/thirdparty/mockito/build.mill +++ b/example/thirdparty/mockito/build.mill @@ -226,7 +226,7 @@ object `package` extends RootModule with MockitoModule { // def testIvyDeps = Agg(libraries.junit4, libraries.osgi) // def testRuntimeIvyDeps = Agg(libraries.equinox) // trait BundleModule extends MockitoModule{ -// def bundleName: String = millModuleSegments.parts.last +// def bundleName: String = millModuleSegments.last.value // override def millSourcePath = `osgi-test`.millSourcePath // def sources = Task.Sources(millSourcePath / "src" / bundleName / "java") // diff --git a/example/thirdparty/netty/build.mill b/example/thirdparty/netty/build.mill index 582bf65ca57..ce206b2b300 100644 --- a/example/thirdparty/netty/build.mill +++ b/example/thirdparty/netty/build.mill @@ -77,7 +77,7 @@ trait NettyModule extends NettyBaseModule { def ivyDeps = super.ivyDeps() ++ testIvyDeps() def forkWorkingDir = NettyModule.this.millSourcePath def forkArgs = super.forkArgs() ++ Seq( - "-Dnativeimage.handlerMetadataArtifactId=netty-" + NettyModule.this.millModuleSegments.parts.last + "-Dnativeimage.handlerMetadataArtifactId=netty-" + NettyModule.this.millModuleSegments.last.value ) } diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-compile-classpaths.json b/integration/ide/bsp-server/resources/snapshots/build-targets-compile-classpaths.json index f39f59fa7eb..564b6a8fd8d 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-compile-classpaths.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-compile-classpaths.json @@ -1,13 +1,5 @@ { "items": [ - { - "target": { - "uri": "file:///workspace/hello-java" - }, - "classpath": [ - "file:///workspace/hello-java/compile-resources" - ] - }, { "target": { "uri": "file:///workspace/hello-kotlin" @@ -26,6 +18,14 @@ "file:///coursier-cache/https/repo1.maven.org/maven2/org/scala-lang/scala-library//scala-library-.jar", "file:///workspace/hello-scala/compile-resources" ] + }, + { + "target": { + "uri": "file:///workspace/hello-java" + }, + "classpath": [ + "file:///workspace/hello-java/compile-resources" + ] } ] } \ No newline at end of file diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-dependency-modules.json b/integration/ide/bsp-server/resources/snapshots/build-targets-dependency-modules.json index 5dbf41724f1..b188222db81 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-dependency-modules.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-dependency-modules.json @@ -1,11 +1,5 @@ { "items": [ - { - "target": { - "uri": "file:///workspace/hello-java" - }, - "modules": [] - }, { "target": { "uri": "file:///workspace/hello-kotlin" @@ -27,6 +21,12 @@ "version": "" } ] + }, + { + "target": { + "uri": "file:///workspace/hello-java" + }, + "modules": [] } ] } \ No newline at end of file diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-dependency-sources.json b/integration/ide/bsp-server/resources/snapshots/build-targets-dependency-sources.json index 3be9d342e20..bbca9044124 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-dependency-sources.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-dependency-sources.json @@ -1,11 +1,5 @@ { "items": [ - { - "target": { - "uri": "file:///workspace/hello-java" - }, - "sources": [] - }, { "target": { "uri": "file:///workspace/hello-kotlin" @@ -22,6 +16,12 @@ "sources": [ "file:///coursier-cache/https/repo1.maven.org/maven2/org/scala-lang/scala-library//scala-library--sources.jar" ] + }, + { + "target": { + "uri": "file:///workspace/hello-java" + }, + "sources": [] } ] } \ No newline at end of file diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-javac-options.json b/integration/ide/bsp-server/resources/snapshots/build-targets-javac-options.json index 65e5269db0d..c2727ae068c 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-javac-options.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-javac-options.json @@ -1,15 +1,5 @@ { "items": [ - { - "target": { - "uri": "file:///workspace/hello-java" - }, - "options": [], - "classpath": [ - "file:///workspace/hello-java/compile-resources" - ], - "classDirectory": "file:///workspace/out/hello-java/compile.dest/classes" - }, { "target": { "uri": "file:///workspace/hello-kotlin" @@ -32,6 +22,16 @@ "file:///workspace/hello-scala/compile-resources" ], "classDirectory": "file:///workspace/out/hello-scala/compile.dest/classes" + }, + { + "target": { + "uri": "file:///workspace/hello-java" + }, + "options": [], + "classpath": [ + "file:///workspace/hello-java/compile-resources" + ], + "classDirectory": "file:///workspace/out/hello-java/compile.dest/classes" } ] } \ No newline at end of file diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-jvm-run-environments.json b/integration/ide/bsp-server/resources/snapshots/build-targets-jvm-run-environments.json index 40d001f52ff..4e6e491bc0e 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-jvm-run-environments.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-jvm-run-environments.json @@ -1,19 +1,5 @@ { "items": [ - { - "target": { - "uri": "file:///workspace/hello-java" - }, - "classpath": [ - "file:///workspace/hello-java/compile-resources", - "file:///workspace/hello-java/resources", - "file:///workspace/out/hello-java/compile.dest/classes" - ], - "jvmOptions": [], - "workingDirectory": "/workspace", - "environmentVariables": {}, - "mainClasses": [] - }, { "target": { "uri": "file:///workspace/hello-kotlin" @@ -44,6 +30,20 @@ "workingDirectory": "/workspace", "environmentVariables": {}, "mainClasses": [] + }, + { + "target": { + "uri": "file:///workspace/hello-java" + }, + "classpath": [ + "file:///workspace/hello-java/compile-resources", + "file:///workspace/hello-java/resources", + "file:///workspace/out/hello-java/compile.dest/classes" + ], + "jvmOptions": [], + "workingDirectory": "/workspace", + "environmentVariables": {}, + "mainClasses": [] } ] } \ No newline at end of file diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-jvm-test-environments.json b/integration/ide/bsp-server/resources/snapshots/build-targets-jvm-test-environments.json index 40d001f52ff..4e6e491bc0e 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-jvm-test-environments.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-jvm-test-environments.json @@ -1,19 +1,5 @@ { "items": [ - { - "target": { - "uri": "file:///workspace/hello-java" - }, - "classpath": [ - "file:///workspace/hello-java/compile-resources", - "file:///workspace/hello-java/resources", - "file:///workspace/out/hello-java/compile.dest/classes" - ], - "jvmOptions": [], - "workingDirectory": "/workspace", - "environmentVariables": {}, - "mainClasses": [] - }, { "target": { "uri": "file:///workspace/hello-kotlin" @@ -44,6 +30,20 @@ "workingDirectory": "/workspace", "environmentVariables": {}, "mainClasses": [] + }, + { + "target": { + "uri": "file:///workspace/hello-java" + }, + "classpath": [ + "file:///workspace/hello-java/compile-resources", + "file:///workspace/hello-java/resources", + "file:///workspace/out/hello-java/compile.dest/classes" + ], + "jvmOptions": [], + "workingDirectory": "/workspace", + "environmentVariables": {}, + "mainClasses": [] } ] } \ No newline at end of file diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-output-paths.json b/integration/ide/bsp-server/resources/snapshots/build-targets-output-paths.json index 167fc54a654..bf4062a1c07 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-output-paths.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-output-paths.json @@ -2,19 +2,19 @@ "items": [ { "target": { - "uri": "file:///workspace/hello-java" + "uri": "file:///workspace/hello-kotlin" }, "outputPaths": [] }, { "target": { - "uri": "file:///workspace/hello-kotlin" + "uri": "file:///workspace/hello-scala" }, "outputPaths": [] }, { "target": { - "uri": "file:///workspace/hello-scala" + "uri": "file:///workspace/hello-java" }, "outputPaths": [] }, diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-resources.json b/integration/ide/bsp-server/resources/snapshots/build-targets-resources.json index 7360d1c92ff..4076bbe0362 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-resources.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-resources.json @@ -2,19 +2,19 @@ "items": [ { "target": { - "uri": "file:///workspace/hello-java" + "uri": "file:///workspace/hello-kotlin" }, "resources": [] }, { "target": { - "uri": "file:///workspace/hello-kotlin" + "uri": "file:///workspace/hello-scala" }, "resources": [] }, { "target": { - "uri": "file:///workspace/hello-scala" + "uri": "file:///workspace/hello-java" }, "resources": [] }, diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-scalac-options.json b/integration/ide/bsp-server/resources/snapshots/build-targets-scalac-options.json index 65e5269db0d..c2727ae068c 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-scalac-options.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-scalac-options.json @@ -1,15 +1,5 @@ { "items": [ - { - "target": { - "uri": "file:///workspace/hello-java" - }, - "options": [], - "classpath": [ - "file:///workspace/hello-java/compile-resources" - ], - "classDirectory": "file:///workspace/out/hello-java/compile.dest/classes" - }, { "target": { "uri": "file:///workspace/hello-kotlin" @@ -32,6 +22,16 @@ "file:///workspace/hello-scala/compile-resources" ], "classDirectory": "file:///workspace/out/hello-scala/compile.dest/classes" + }, + { + "target": { + "uri": "file:///workspace/hello-java" + }, + "options": [], + "classpath": [ + "file:///workspace/hello-java/compile-resources" + ], + "classDirectory": "file:///workspace/out/hello-java/compile.dest/classes" } ] } \ No newline at end of file diff --git a/integration/ide/bsp-server/resources/snapshots/build-targets-sources.json b/integration/ide/bsp-server/resources/snapshots/build-targets-sources.json index 21295e603a6..91d3e35ef84 100644 --- a/integration/ide/bsp-server/resources/snapshots/build-targets-sources.json +++ b/integration/ide/bsp-server/resources/snapshots/build-targets-sources.json @@ -2,11 +2,11 @@ "items": [ { "target": { - "uri": "file:///workspace/hello-java" + "uri": "file:///workspace/hello-kotlin" }, "sources": [ { - "uri": "file:///workspace/hello-java/src", + "uri": "file:///workspace/hello-kotlin/src", "kind": 2, "generated": false } @@ -14,11 +14,11 @@ }, { "target": { - "uri": "file:///workspace/hello-kotlin" + "uri": "file:///workspace/hello-scala" }, "sources": [ { - "uri": "file:///workspace/hello-kotlin/src", + "uri": "file:///workspace/hello-scala/src", "kind": 2, "generated": false } @@ -26,11 +26,11 @@ }, { "target": { - "uri": "file:///workspace/hello-scala" + "uri": "file:///workspace/hello-java" }, "sources": [ { - "uri": "file:///workspace/hello-scala/src", + "uri": "file:///workspace/hello-java/src", "kind": 2, "generated": false } diff --git a/integration/ide/bsp-server/resources/snapshots/workspace-build-targets.json b/integration/ide/bsp-server/resources/snapshots/workspace-build-targets.json index 41a59014648..7547d4380a1 100644 --- a/integration/ide/bsp-server/resources/snapshots/workspace-build-targets.json +++ b/integration/ide/bsp-server/resources/snapshots/workspace-build-targets.json @@ -1,31 +1,5 @@ { "targets": [ - { - "id": { - "uri": "file:///workspace/hello-java" - }, - "displayName": "hello-java", - "baseDirectory": "file:///workspace/hello-java", - "tags": [ - "library", - "application" - ], - "languageIds": [ - "java" - ], - "dependencies": [], - "capabilities": { - "canCompile": true, - "canTest": false, - "canRun": true, - "canDebug": false - }, - "dataKind": "jvm", - "data": { - "javaHome": "file:///java-home/", - "javaVersion": "" - } - }, { "id": { "uri": "file:///workspace/hello-kotlin" @@ -94,6 +68,32 @@ } } }, + { + "id": { + "uri": "file:///workspace/hello-java" + }, + "displayName": "hello-java", + "baseDirectory": "file:///workspace/hello-java", + "tags": [ + "library", + "application" + ], + "languageIds": [ + "java" + ], + "dependencies": [], + "capabilities": { + "canCompile": true, + "canTest": false, + "canRun": true, + "canDebug": false + }, + "dataKind": "jvm", + "data": { + "javaHome": "file:///java-home/", + "javaVersion": "" + } + }, { "id": { "uri": "file:///workspace/mill-build" diff --git a/integration/invalidation/selective-execution/src/SelectiveExecutionTests.scala b/integration/invalidation/selective-execution/src/SelectiveExecutionTests.scala index 3a26bb7237f..1e59861bda6 100644 --- a/integration/invalidation/selective-execution/src/SelectiveExecutionTests.scala +++ b/integration/invalidation/selective-execution/src/SelectiveExecutionTests.scala @@ -58,7 +58,8 @@ object SelectiveExecutionTests extends UtestIntegrationTestSuite { // `selective.resolve` or `selective.run` and thingsstill work import tester._ - eval(("selective.prepare", "{foo.fooCommand,bar.barCommand}"), check = true) + // `selective.prepare` defaults to `__` if no selector is passed + eval(("selective.prepare"), check = true) modifyFile(workspacePath / "bar/bar.txt", _ + "!") val resolve = eval(("selective.resolve", "bar.barCommand"), check = true) diff --git a/main/define/src/mill/define/Module.scala b/main/define/src/mill/define/Module.scala index 73e3e7fcea2..b29a574dcfd 100644 --- a/main/define/src/mill/define/Module.scala +++ b/main/define/src/mill/define/Module.scala @@ -78,7 +78,8 @@ object Module { outer.getClass, implicitly[ClassTag[T]].runtimeClass, filter, - noParams = true + noParams = true, + Reflect.getMethods(_, scala.reflect.NameTransformer.decode) ) .map(_.invoke(outer).asInstanceOf[T]) } @@ -87,7 +88,11 @@ object Module { def reflectNestedObjects[T: ClassTag](filter: String => Boolean = Function.const(true)) : Seq[T] = { - Reflect.reflectNestedObjects02(outer.getClass, filter) + Reflect.reflectNestedObjects02( + outer.getClass, + filter, + Reflect.getMethods(_, scala.reflect.NameTransformer.decode) + ) .map { case (name, cls, getter) => getter(outer) } } } diff --git a/main/define/src/mill/define/Reflect.scala b/main/define/src/mill/define/Reflect.scala index 495a1b0b5be..506f158b693 100644 --- a/main/define/src/mill/define/Reflect.scala +++ b/main/define/src/mill/define/Reflect.scala @@ -1,36 +1,49 @@ package mill.define -import fastparse.NoWhitespace._ -import fastparse._ - -import java.lang.reflect.Modifier import scala.reflect.ClassTag -import scala.reflect.NameTransformer.decode +import java.lang.reflect.Method private[mill] object Reflect { + import java.lang.reflect.Modifier - def ident[_p: P]: P[String] = P(CharsWhileIn("a-zA-Z0-9_\\-")).! - - def standaloneIdent[_p: P]: P[String] = P(Start ~ ident ~ End) + def isLegalIdentifier(identifier: String): Boolean = { + var i = 0 + val len = identifier.length + while (i < len) { + val c = identifier.charAt(i) + if ( + 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' || c == '_' || c == '-' + ) { + i += 1 + } else { + return false + } + } + true + } - def isLegalIdentifier(identifier: String): Boolean = - parse(identifier, standaloneIdent(_)).isInstanceOf[Parsed.Success[_]] + def getMethods(cls: Class[_], decode: String => String): Array[(Method, String)] = + for { + m <- cls.getMethods + n = decode(m.getName) + if isLegalIdentifier(n) && (m.getModifiers & Modifier.STATIC) == 0 + } yield (m, n) def reflect( outer: Class[_], inner: Class[_], filter: String => Boolean, - noParams: Boolean - ): Seq[java.lang.reflect.Method] = { - val res = for { - m <- outer.getMethods - n = decode(m.getName) - if filter(n) && - isLegalIdentifier(n) && - (!noParams || m.getParameterCount == 0) && - (m.getModifiers & Modifier.STATIC) == 0 && - inner.isAssignableFrom(m.getReturnType) - } yield m + noParams: Boolean, + getMethods: Class[_] => Array[(java.lang.reflect.Method, String)] + ): Array[java.lang.reflect.Method] = { + val arr: Array[java.lang.reflect.Method] = getMethods(outer) + .collect { + case (m, n) + if filter(n) && + (!noParams || m.getParameterCount == 0) && + inner.isAssignableFrom(m.getReturnType) => + m + } // There can be multiple methods of the same name on a class if a sub-class // overrides a super-class method and narrows the return type. @@ -43,18 +56,16 @@ private[mill] object Reflect { // which messes up the comparison since all forwarders will have the // same `getDeclaringClass`. To handle these scenarios, also sort by // return type, so we can identify the most specific override - res - .sortWith((m1, m2) => - if (m1.getDeclaringClass.equals(m2.getDeclaringClass)) false - else m1.getDeclaringClass.isAssignableFrom(m2.getDeclaringClass) - ) - .sortWith((m1, m2) => + + arr.sortInPlaceWith((m1, m2) => + if (m1.getDeclaringClass.equals(m2.getDeclaringClass)) { m1.getReturnType.isAssignableFrom(m2.getReturnType) - ) - .reverse - .distinctBy(_.getName) - .sortBy(_.getName) - .toIndexedSeq + } else { + m1.getDeclaringClass.isAssignableFrom(m2.getDeclaringClass) + } + ) + + arr.reverseIterator.distinctBy(_.getName).toArray } // For some reason, this fails to pick up concrete `object`s nested directly within @@ -62,14 +73,16 @@ private[mill] object Reflect { // script/REPL runner always wraps user code in a wrapper object/trait def reflectNestedObjects0[T: ClassTag]( outerCls: Class[_], - filter: String => Boolean = Function.const(true) - ): Seq[(String, java.lang.reflect.Member)] = { + filter: String => Boolean = Function.const(true), + getMethods: Class[_] => Array[(java.lang.reflect.Method, String)] + ): Array[(String, java.lang.reflect.Member)] = { val first = reflect( outerCls, implicitly[ClassTag[T]].runtimeClass, filter, - noParams = true + noParams = true, + getMethods ) .map(m => (m.getName, m)) @@ -92,9 +105,10 @@ private[mill] object Reflect { def reflectNestedObjects02[T: ClassTag]( outerCls: Class[_], - filter: String => Boolean = Function.const(true) - ): Seq[(String, Class[_], Any => T)] = { - reflectNestedObjects0[T](outerCls, filter).map { + filter: String => Boolean = Function.const(true), + getMethods: Class[_] => Array[(java.lang.reflect.Method, String)] + ): Array[(String, Class[_], Any => T)] = { + reflectNestedObjects0[T](outerCls, filter, getMethods).map { case (name, m: java.lang.reflect.Method) => (name, m.getReturnType, (outer: Any) => m.invoke(outer).asInstanceOf[T]) case (name, m: java.lang.reflect.Field) => diff --git a/main/define/src/mill/define/Segment.scala b/main/define/src/mill/define/Segment.scala index df3f4c088ae..98ecc62aeda 100644 --- a/main/define/src/mill/define/Segment.scala +++ b/main/define/src/mill/define/Segment.scala @@ -8,6 +8,11 @@ sealed trait Segment { } object Segment { + import scala.math.Ordering.Implicits.seqOrdering + implicit def ordering: Ordering[Segment] = Ordering.by { + case Label(value) => (value, Nil) + case Cross(value) => ("", value) + } final case class Label(value: String) extends Segment final case class Cross(value: Seq[String]) extends Segment } diff --git a/main/define/src/mill/define/Segments.scala b/main/define/src/mill/define/Segments.scala index e5cd4d14260..7aa7759f84c 100644 --- a/main/define/src/mill/define/Segments.scala +++ b/main/define/src/mill/define/Segments.scala @@ -1,5 +1,7 @@ package mill.define +import scala.math.Ordering.Implicits.seqOrdering + /** * Models a path with the Mill build hierarchy, e.g. `amm.util[2.11].test.compile`. * Segments must start with a [[Segment.Label]]. @@ -17,6 +19,12 @@ case class Segments private (value: Seq[Segment]) { def startsWith(prefix: Segments): Boolean = value.startsWith(prefix.value) + def last: Segment.Label = value.last match { + case l: Segment.Label => l + case _ => + throw new IllegalArgumentException("Segments must start with a Label, but found a Cross.") + } + def parts: List[String] = value.toList match { case Nil => Nil case Segment.Label(head) :: rest => @@ -46,9 +54,11 @@ case class Segments private (value: Seq[Segment]) { case Segment.Cross(_) :: _ => throw new IllegalArgumentException("Segments must start with a Label, but found a Cross.") } + override lazy val hashCode: Int = value.hashCode() } object Segments { + implicit def ordering: Ordering[Segments] = Ordering.by(_.value) def apply(): Segments = new Segments(Nil) def apply(items: Seq[Segment]): Segments = new Segments(items) def labels(values: String*): Segments = Segments(values.map(Segment.Label)) diff --git a/main/eval/src/mill/eval/Plan.scala b/main/eval/src/mill/eval/Plan.scala index 7b019996b8c..11fbc1cb2e3 100644 --- a/main/eval/src/mill/eval/Plan.scala +++ b/main/eval/src/mill/eval/Plan.scala @@ -4,6 +4,9 @@ import mill.api.Strict import mill.define.{NamedTask, Segment, Segments, Task} import mill.util.MultiBiMap +import java.util.StringTokenizer +import scala.jdk.CollectionConverters.IteratorHasAsScala + private[mill] object Plan { def plan(goals: Agg[Task[_]]): (MultiBiMap[Terminal, Task[_]], Strict.Agg[Task[_]]) = { val transitive = Graph.transitiveTargets(goals) @@ -45,7 +48,12 @@ private[mill] object Plan { * suffix that uniquely distinguishes them. */ private def assignOverridenTaskSegments(overriddenEnclosings: Seq[String], t: NamedTask[Any]) = { - def splitEnclosing(s: String) = s.split("[.# ]").filter(_ != "") + // StringTokenizer is faster than String#split due to not using regexes + def splitEnclosing(s: String) = new StringTokenizer(s, ".# ") + .asIterator() + .asScala.map(_.asInstanceOf[String]) + .filter(_ != "") + .toArray val segments = t.ctx.segments val superSegmentStrings = overriddenEnclosings.map(splitEnclosing) diff --git a/main/resolve/src/mill/resolve/ParseArgs.scala b/main/resolve/src/mill/resolve/ParseArgs.scala index ae32b9089bf..3bb50370362 100644 --- a/main/resolve/src/mill/resolve/ParseArgs.scala +++ b/main/resolve/src/mill/resolve/ParseArgs.scala @@ -95,7 +95,7 @@ object ParseArgs { private def selector[_p: P]: P[(Option[Segments], Option[Segments])] = { def wildcard = P("__" | "_") - def label = mill.define.Reflect.ident + def label = P(CharsWhileIn("a-zA-Z0-9_\\-")).! def typeQualifier(simple: Boolean) = { val maxSegments = if (simple) 0 else Int.MaxValue diff --git a/main/resolve/src/mill/resolve/Resolve.scala b/main/resolve/src/mill/resolve/Resolve.scala index 9815d3c906c..0f71410a289 100644 --- a/main/resolve/src/mill/resolve/Resolve.scala +++ b/main/resolve/src/mill/resolve/Resolve.scala @@ -25,7 +25,8 @@ object Resolve { selector: Segments, nullCommandDefaults: Boolean, allowPositionalCommandArgs: Boolean, - resolveToModuleTasks: Boolean + resolveToModuleTasks: Boolean, + cache: ResolveCore.Cache ) = { Right(resolved.map(_.segments)) } @@ -41,21 +42,23 @@ object Resolve { selector: Segments, nullCommandDefaults: Boolean, allowPositionalCommandArgs: Boolean, - resolveToModuleTasks: Boolean + resolveToModuleTasks: Boolean, + cache: ResolveCore.Cache ) = { + val taskList = resolved.map { case r: Resolved.NamedTask => val instantiated = ResolveCore - .instantiateModule(rootModule, r.segments.init) - .flatMap(instantiateNamedTask(r, _)) + .instantiateModule(rootModule, r.segments.init, cache) + .flatMap(instantiateNamedTask(r, _, cache)) instantiated.map(Some(_)) case r: Resolved.Command => val instantiated = ResolveCore - .instantiateModule0(rootModule, r.segments.init) - .flatMap { case (mod, rootMod) => + .instantiateModule(rootModule, r.segments.init, cache) + .flatMap { mod => instantiateCommand( - rootMod, + rootModule, r, mod, args, @@ -66,19 +69,20 @@ object Resolve { instantiated.map(Some(_)) case r: Resolved.Module => - ResolveCore.instantiateModule(rootModule, r.segments).flatMap { + ResolveCore.instantiateModule(rootModule, r.segments, cache).flatMap { case value if resolveToModuleTasks => Right(Some(ModuleTask(value))) case value: TaskModule if !resolveToModuleTasks => val directChildrenOrErr = ResolveCore.resolveDirectChildren( rootModule, value.getClass, Some(value.defaultCommandName()), - value.millModuleSegments + value.millModuleSegments, + cache = cache ) directChildrenOrErr.flatMap(directChildren => directChildren.head match { - case r: Resolved.NamedTask => instantiateNamedTask(r, value).map(Some(_)) + case r: Resolved.NamedTask => instantiateNamedTask(r, value, cache).map(Some(_)) case r: Resolved.Command => instantiateCommand( rootModule, @@ -108,10 +112,17 @@ object Resolve { private def instantiateNamedTask( r: Resolved.NamedTask, - p: Module + p: Module, + cache: ResolveCore.Cache ): Either[String, NamedTask[_]] = { val definition = Reflect - .reflect(p.getClass, classOf[NamedTask[_]], _ == r.segments.parts.last, true) + .reflect( + p.getClass, + classOf[NamedTask[_]], + _ == r.segments.last.value, + true, + getMethods = cache.getMethods + ) .head ResolveCore.catchWrapException( @@ -130,7 +141,7 @@ object Resolve { ResolveCore.catchWrapException { val invoked = invokeCommand0( p, - r.segments.parts.last, + r.segments.last.value, rootModule.millDiscover.asInstanceOf[Discover], args, nullCommandDefaults, @@ -214,7 +225,8 @@ trait Resolve[T] { segments: Segments, nullCommandDefaults: Boolean, allowPositionalCommandArgs: Boolean, - resolveToModuleTasks: Boolean + resolveToModuleTasks: Boolean, + cache: ResolveCore.Cache ): Either[String, Seq[T]] def resolve( @@ -279,13 +291,15 @@ trait Resolve[T] { resolveToModuleTasks: Boolean ): Either[String, Seq[T]] = { val rootResolved = ResolveCore.Resolved.Module(Segments(), rootModule.getClass) + val cache = new ResolveCore.Cache() val resolved = ResolveCore.resolve( rootModule = rootModule, remainingQuery = sel.value.toList, current = rootResolved, querySoFar = Segments(), - seenModules = Set.empty + seenModules = Set.empty, + cache = cache ) match { case ResolveCore.Success(value) => Right(value) case ResolveCore.NotFound(segments, found, next, possibleNexts) => @@ -302,16 +316,18 @@ trait Resolve[T] { } resolved - .map(_.toSeq.sortBy(_.segments.render)) - .flatMap(handleResolved( - rootModule, - _, - args, - sel, - nullCommandDefaults, - allowPositionalCommandArgs, - resolveToModuleTasks - )) + .flatMap(r => + handleResolved( + rootModule, + r.sortBy(_.segments), + args, + sel, + nullCommandDefaults, + allowPositionalCommandArgs, + resolveToModuleTasks, + cache = cache + ) + ) } private[mill] def deduplicate(items: List[T]): List[T] = items diff --git a/main/resolve/src/mill/resolve/ResolveCore.scala b/main/resolve/src/mill/resolve/ResolveCore.scala index ea58308c7ef..763201a78f4 100644 --- a/main/resolve/src/mill/resolve/ResolveCore.scala +++ b/main/resolve/src/mill/resolve/ResolveCore.scala @@ -4,8 +4,7 @@ import mill.define._ import mill.util.EitherOps import java.lang.reflect.InvocationTargetException -import scala.collection.immutable.Seq -import scala.reflect.NameTransformer.decode +import java.lang.reflect.Method /** * Takes a single list of segments, without braces but including wildcards, and @@ -35,7 +34,7 @@ private object ResolveCore { sealed trait Result - case class Success(value: Set[Resolved]) extends Result { + case class Success(value: Seq[Resolved]) extends Result { assert(value.nonEmpty) } @@ -50,6 +49,31 @@ private object ResolveCore { case class Error(msg: String) extends Failed + /** + * Cache for modules instantiated during task and resolution. + * + * Instantiating modules can be pretty expensive (~1ms per module!) due to all the reflection + * and stuff going on, but because we only instantiate tasks and modules lazily on-demand, it + * can be quite hard to figure out up front when we actually need to instantiate things. So + * just cache all module instantiations and re-use them to avoid repeatedly instantiating the + * same module + */ + class Cache( + val instantiatedModules: collection.mutable.Map[Segments, Either[String, Module]] = + collection.mutable.Map(), + decodedNames: collection.mutable.Map[String, String] = collection.mutable.Map(), + methods: collection.mutable.Map[Class[_], Array[(java.lang.reflect.Method, String)]] = + collection.mutable.Map() + ) { + def decode(s: String): String = { + decodedNames.getOrElseUpdate(s, scala.reflect.NameTransformer.decode(s)) + } + + def getMethods(cls: Class[_]): Array[(Method, String)] = { + methods.getOrElseUpdate(cls, Reflect.getMethods(cls, decode)) + } + } + def catchWrapException[T](t: => T): Either[String, T] = { try Right(t) catch { @@ -71,17 +95,18 @@ private object ResolveCore { remainingQuery: List[Segment], current: Resolved, querySoFar: Segments, - seenModules: Set[Class[_]] = Set.empty + seenModules: Set[Class[_]], + cache: Cache ): Result = { def moduleClasses(resolved: Iterable[Resolved]): Set[Class[_]] = { resolved.collect { case Resolved.Module(_, cls) => cls }.toSet } remainingQuery match { - case Nil => Success(Set(current)) + case Nil => Success(Seq(current)) case head :: tail => - def recurse(searchModules: Set[Resolved]): Result = { - val results = searchModules + def recurse(searchModules: Seq[Resolved]): Result = { + val (failures, successesLists) = searchModules .map { r => val rClasses = moduleClasses(Set(r)) if (seenModules.intersect(rClasses).nonEmpty) { @@ -92,13 +117,13 @@ private object ResolveCore { tail, r, querySoFar ++ Seq(head), - seenModules ++ moduleClasses(Set(current)) + seenModules ++ moduleClasses(Set(current)), + cache ) } } .partitionMap { case s: Success => Right(s.value); case f: Failed => Left(f) } - val (failures, successesLists) = results val (errors, notFounds) = failures.partitionMap { case s: NotFound => Right(s) case s: Error => Left(s.msg) @@ -108,14 +133,14 @@ private object ResolveCore { else if (successesLists.flatten.nonEmpty) Success(successesLists.flatten) else notFounds.size match { case 1 => notFounds.head - case _ => notFoundResult(rootModule, querySoFar, current, head) + case _ => notFoundResult(rootModule, querySoFar, current, head, cache) } } (head, current) match { case (Segment.Label(singleLabel), m: Resolved.Module) => - val resOrErr: Either[String, Iterable[Resolved]] = singleLabel match { + val resOrErr: Either[String, Seq[Resolved]] = singleLabel match { case "__" => val self = Seq(Resolved.Module(m.segments, m.cls)) val transitiveOrErr = @@ -125,7 +150,8 @@ private object ResolveCore { None, current.segments, Nil, - seenModules + seenModules, + cache ) transitiveOrErr.map(transitive => self ++ transitive) @@ -135,7 +161,8 @@ private object ResolveCore { rootModule, m.cls, None, - current.segments + current.segments, + cache = cache ) case pattern if pattern.startsWith("__:") => @@ -148,7 +175,8 @@ private object ResolveCore { None, current.segments, typePattern, - seenModules + seenModules, + cache ) transitiveOrErr.map(transitive => self ++ transitive) @@ -160,7 +188,8 @@ private object ResolveCore { m.cls, None, current.segments, - typePattern + typePattern, + cache ) case _ => @@ -168,18 +197,19 @@ private object ResolveCore { rootModule, m.cls, Some(singleLabel), - current.segments + current.segments, + cache = cache ) } resOrErr match { case Left(err) => Error(err) - case Right(res) => recurse(res.toSet) + case Right(res) => recurse(res.distinct) } case (Segment.Cross(cross), m: Resolved.Module) => if (classOf[Cross[_]].isAssignableFrom(m.cls)) { - instantiateModule(rootModule, current.segments).flatMap { + instantiateModule(rootModule, current.segments, cache).flatMap { case c: Cross[_] => catchWrapException( if (cross == Seq("__")) for ((_, v) <- c.valuesToModules.toSeq) yield v @@ -203,67 +233,56 @@ private object ResolveCore { recurse( searchModules .map(m => Resolved.Module(m.millModuleSegments, m.getClass)) - .toSet ) } - } else notFoundResult(rootModule, querySoFar, current, head) + } else notFoundResult(rootModule, querySoFar, current, head, cache) - case _ => notFoundResult(rootModule, querySoFar, current, head) + case _ => notFoundResult(rootModule, querySoFar, current, head, cache) } } } + def instantiateModule( rootModule: BaseModule, - segments: Segments - ): Either[String, Module] = - instantiateModule0(rootModule, segments).map(_._1) + segments: Segments, + cache: Cache + ): Either[String, Module] = cache.instantiatedModules.getOrElseUpdate( + segments, { + segments.value.foldLeft[Either[String, Module]](Right(rootModule)) { + case (Right(current), Segment.Label(s)) => + assert(s != "_", s) + resolveDirectChildren0( + rootModule, + current.millModuleSegments, + current.getClass, + Some(s), + cache = cache + ).flatMap { + case Seq((_, Some(f))) => f(current) + case unknown => + sys.error( + s"Unable to resolve single child " + + s"rootModule: ${rootModule}, segments: ${segments.render}," + + s"current: $current, s: ${s}, unknown: $unknown" + ) + } - def instantiateModule0( - rootModule: BaseModule, - segments: Segments - ): Either[String, (Module, BaseModule)] = { - - segments.value.foldLeft[Either[String, (Module, BaseModule)]](Right(( - rootModule, - rootModule - ))) { - case (Right((current, currentRoot)), Segment.Label(s)) => - assert(s != "_", s) - resolveDirectChildren0( - rootModule, - current.millModuleSegments, - current.getClass, - Some(s) - ).flatMap { - case Seq((_, Some(f))) => - val res = f(current) - res.map { - case b: BaseModule => (b, b) - case b => (b, currentRoot) - } - case unknown => - sys.error( - s"Unable to resolve single child " + - s"rootModule: ${rootModule}, segments: ${segments.render}," + - s"current: $current, s: ${s}, unknown: $unknown" - ) - } + case (Right(current), Segment.Cross(vs)) => + assert(!vs.contains("_"), vs) - case (Right((current, currentRoot)), Segment.Cross(vs)) => - assert(!vs.contains("_"), vs) + catchWrapException( + current + .asInstanceOf[Cross[_]] + .segmentsToModules(vs.toList) + .asInstanceOf[Module] + ) - catchWrapException( - current - .asInstanceOf[Cross[_]] - .segmentsToModules(vs.toList) - .asInstanceOf[Module] -> currentRoot - ) + case (Left(err), _) => Left(err) + } - case (Left(err), _) => Left(err) } - - } + ) def resolveTransitiveChildren( rootModule: BaseModule, @@ -271,12 +290,14 @@ private object ResolveCore { nameOpt: Option[String], segments: Segments, typePattern: Seq[String], - seenModules: Set[Class[_]] - ): Either[String, Set[Resolved]] = { + seenModules: Set[Class[_]], + cache: Cache + ): Either[String, Seq[Resolved]] = { if (seenModules.contains(cls)) Left(cyclicModuleErrorMsg(segments)) else { - val errOrDirect = resolveDirectChildren(rootModule, cls, nameOpt, segments, typePattern) - val directTraverse = resolveDirectChildren(rootModule, cls, nameOpt, segments, Nil) + val errOrDirect = + resolveDirectChildren(rootModule, cls, nameOpt, segments, typePattern, cache) + val directTraverse = resolveDirectChildren(rootModule, cls, nameOpt, segments, Nil, cache) val errOrModules = directTraverse.map { modules => modules.flatMap { @@ -294,7 +315,8 @@ private object ResolveCore { nameOpt, m.segments, typePattern, - seenModules + cls + seenModules + cls, + cache )) } case Left(err) => Seq(Left(err)) @@ -346,10 +368,11 @@ private object ResolveCore { cls: Class[_], nameOpt: Option[String], segments: Segments, - typePattern: Seq[String] = Nil - ): Either[String, Set[Resolved]] = { + typePattern: Seq[String] = Nil, + cache: Cache + ): Either[String, Seq[Resolved]] = { val crossesOrErr = if (classOf[Cross[_]].isAssignableFrom(cls) && nameOpt.isEmpty) { - instantiateModule(rootModule, segments).map { + instantiateModule(rootModule, segments, cache).map { case cross: Cross[_] => for (item <- cross.items) yield { Resolved.Module(segments ++ Segment.Cross(item.crossSegments), item.cls) @@ -372,11 +395,9 @@ private object ResolveCore { filteredCrosses = crosses.filter { c => classMatchesTypePred(typePattern)(c.cls) } - direct0 <- resolveDirectChildren0(rootModule, segments, cls, nameOpt, typePattern) + direct0 <- resolveDirectChildren0(rootModule, segments, cls, nameOpt, typePattern, cache) direct <- Right(expandSegments(direct0)) - } yield { - direct.toSet ++ filteredCrosses - } + } yield direct ++ filteredCrosses } def resolveDirectChildren0( @@ -384,21 +405,22 @@ private object ResolveCore { segments: Segments, cls: Class[_], nameOpt: Option[String], - typePattern: Seq[String] = Nil + typePattern: Seq[String] = Nil, + cache: Cache ): Either[String, Seq[(Resolved, Option[Module => Either[String, Module]])]] = { def namePred(n: String) = nameOpt.isEmpty || nameOpt.contains(n) val modulesOrErr: Either[String, Seq[(Resolved, Option[Module => Either[String, Module]])]] = { if (classOf[DynamicModule].isAssignableFrom(cls)) { - instantiateModule(rootModule, segments).map { + instantiateModule(rootModule, segments, cache).map { case m: DynamicModule => m.millModuleDirectChildren - .filter(c => namePred(c.millModuleSegments.parts.last)) + .filter(c => namePred(c.millModuleSegments.last.value)) .filter(c => classMatchesTypePred(typePattern)(c.getClass)) .map(c => ( Resolved.Module( - Segments.labels(c.millModuleSegments.parts.last), + Segments.labels(c.millModuleSegments.last.value), c.getClass ), Some((x: Module) => Right(c)) @@ -407,10 +429,10 @@ private object ResolveCore { } } else Right { val reflectMemberObjects = Reflect - .reflectNestedObjects02[Module](cls, namePred) + .reflectNestedObjects02[Module](cls, namePred, cache.getMethods) .collect { case (name, memberCls, getter) if classMatchesTypePred(typePattern)(memberCls) => - val resolved = Resolved.Module(Segments.labels(decode(name)), memberCls) + val resolved = Resolved.Module(Segments.labels(cache.decode(name)), memberCls) val getter2 = Some((mod: Module) => catchWrapException(getter(mod))) (resolved, getter2) } @@ -420,15 +442,15 @@ private object ResolveCore { } val namedTasks = Reflect - .reflect(cls, classOf[NamedTask[_]], namePred, noParams = true) + .reflect(cls, classOf[NamedTask[_]], namePred, noParams = true, cache.getMethods) .map { m => - Resolved.NamedTask(Segments.labels(decode(m.getName))) -> + Resolved.NamedTask(Segments.labels(cache.decode(m.getName))) -> None } val commands = Reflect - .reflect(cls, classOf[Command[_]], namePred, noParams = false) - .map(m => decode(m.getName)) + .reflect(cls, classOf[Command[_]], namePred, noParams = false, cache.getMethods) + .map(m => cache.decode(m.getName)) .map { name => Resolved.Command(Segments.labels(name)) -> None } modulesOrErr.map(_ ++ namedTasks ++ commands) @@ -438,7 +460,8 @@ private object ResolveCore { rootModule: BaseModule, querySoFar: Segments, current: Resolved, - next: Segment + next: Segment, + cache: Cache ): NotFound = { val possibleNexts = current match { case m: Resolved.Module => @@ -446,7 +469,8 @@ private object ResolveCore { rootModule, m.cls, None, - current.segments + current.segments, + cache = cache ).toOption.get.map( _.segments.value.last ) @@ -454,6 +478,6 @@ private object ResolveCore { case _ => Set[Segment]() } - NotFound(querySoFar, Set(current), next, possibleNexts) + NotFound(querySoFar, Set(current), next, possibleNexts.toSet) } } diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index 0cf0f0b2507..ebcad880e70 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -249,7 +249,7 @@ trait MainModule extends BaseModule0 { .millDiscover .value .get(t.ctx.enclosingCls) - .flatMap(_._2.find(_.name == t.ctx.segments.parts.last)) + .flatMap(_._2.find(_.name == t.ctx.segments.last.value)) .headOption mainDataOpt match { diff --git a/main/src/mill/main/SelectiveExecution.scala b/main/src/mill/main/SelectiveExecution.scala index 5b0f1a71c27..c692e29cd73 100644 --- a/main/src/mill/main/SelectiveExecution.scala +++ b/main/src/mill/main/SelectiveExecution.scala @@ -11,7 +11,7 @@ private[mill] object SelectiveExecution { implicit val rw: upickle.default.ReadWriter[Metadata] = upickle.default.macroRW object Metadata { - def apply(evaluator: Evaluator, tasks: Seq[String]): Either[String, Metadata] = { + def compute(evaluator: Evaluator, tasks: Seq[String]): Either[String, Metadata] = { for (transitive <- plan0(evaluator, tasks)) yield { val inputTasksToLabels: Map[Task[_], String] = transitive .collect { case Terminal.Labelled(task: InputImpl[_], segments) => @@ -143,7 +143,7 @@ private[mill] object SelectiveExecution { if (oldMetadataTxt == "") Right(tasks.toSet) else { val oldMetadata = upickle.default.read[SelectiveExecution.Metadata](oldMetadataTxt) - for (newMetadata <- SelectiveExecution.Metadata(evaluator, tasks)) yield { + for (newMetadata <- SelectiveExecution.Metadata.compute(evaluator, tasks)) yield { SelectiveExecution.computeDownstream(evaluator, tasks, oldMetadata, newMetadata) .collect { case n: NamedTask[_] => n.ctx.segments.render } .toSet diff --git a/main/src/mill/main/SelectiveExecutionModule.scala b/main/src/mill/main/SelectiveExecutionModule.scala index 291836f68b1..e39abfe100f 100644 --- a/main/src/mill/main/SelectiveExecutionModule.scala +++ b/main/src/mill/main/SelectiveExecutionModule.scala @@ -16,7 +16,8 @@ trait SelectiveExecutionModule extends mill.define.Module { */ def prepare(evaluator: Evaluator, tasks: String*): Command[Unit] = Task.Command(exclusive = true) { - val res: Either[String, Unit] = SelectiveExecution.Metadata(evaluator, tasks) + val res: Either[String, Unit] = SelectiveExecution.Metadata + .compute(evaluator, if (tasks.isEmpty) Seq("__") else tasks) .map(SelectiveExecution.saveMetadata(evaluator, _)) res match {