diff --git a/main/src/main/scala/sbt/internal/Act.scala b/main/src/main/scala/sbt/internal/Act.scala index c2dd6bbb6cb..a490969c7c7 100644 --- a/main/src/main/scala/sbt/internal/Act.scala +++ b/main/src/main/scala/sbt/internal/Act.scala @@ -18,7 +18,15 @@ import sbt.internal.util.Types.idFun import sbt.ProjectExtra.{ failure => _, * } import java.net.URI import sbt.internal.CommandStrings.{ MultiTaskCommand, ShowCommand, PrintCommand } -import sbt.internal.util.{ AttributeEntry, AttributeKey, AttributeMap, IMap, Settings, Util } +import sbt.internal.util.{ + AttributeEntry, + AttributeKey, + AttributeMap, + IMap, + MessageOnlyException, + Settings, + Util, +} import sbt.util.Show import scala.collection.mutable @@ -485,11 +493,10 @@ object Act { def actParser(s: State): Parser[() => State] = requireSession(s, actParser0(s)) - private[this] def actParser0(state: State): Parser[() => State] = { - val extracted = Project extract state + private[this] def actParser0(state: State): Parser[() => State] = + val extracted = Project.extract(state) import extracted.{ showKey, structure } - import Aggregation.evaluatingParser - actionParser.flatMap { action => + actionParser.flatMap: action => val akp = aggregatedKeyParserSep(extracted) def warnOldShellSyntax(seps: Seq[String], keyStrings: String): Unit = if (seps.contains(":") || seps.contains("::")) { @@ -497,37 +504,68 @@ object Act { s"sbt 0.13 shell syntax is deprecated; use slash syntax instead: $keyStrings" ) } else () - def evaluate(pairs: Seq[(ScopedKey[_], Seq[String])]): Parser[() => State] = { + // If the task name matches, but the query is empty, we should succeed the parser, + // but fail the task. Otherwise, the composed parser would think we made a typo. + def emptyResult: Parser[() => State] = + Parser.success(() => throw MessageOnlyException("query result is empty")) + def evaluate(pairs: Seq[(ScopedKey[_], Seq[String])]): Parser[() => State] = val kvs = pairs.map(_._1) val seps = pairs.headOption.map(_._2).getOrElse(Nil) val preparedPairs = anyKeyValues(structure, kvs) - val showConfig = if (action == PrintAction) { - Aggregation.ShowConfig(true, true, println, false) - } else { - Aggregation.defaultShow(state, showTasks = action == ShowAction) - } - evaluatingParser(state, showConfig)(preparedPairs) map { evaluate => () => - { - val keyStrings = preparedPairs.map(pp => showKey.show(pp.key)).mkString(", ") - state.log.debug("Evaluating tasks: " + keyStrings) - warnOldShellSyntax(seps, keyStrings) - evaluate() - } - } - } - action match { - case SingleAction => akp.flatMap(evaluate) - case ShowAction | PrintAction | MultiAction => - rep1sep(akp, token(Space)) flatMap { pairs => - val flat: mutable.ListBuffer[(ScopedKey[_], Seq[String])] = mutable.ListBuffer.empty - pairs foreach { xs => - flat ++= xs - } - evaluate(flat.toList) - } - } - } - } + val showConfig = action match + case PrintAction => + Aggregation.ShowConfig( + settingValues = true, + taskValues = true, + print = println, + success = false + ) + case _ => Aggregation.defaultShow(state, showTasks = action == ShowAction) + Aggregation + .evaluatingParser(state, showConfig)(preparedPairs) + .map: evaluate => + () => + val keyStrings = preparedPairs.map(pp => showKey.show(pp.key)).mkString(", ") + state.log.debug("Evaluating tasks: " + keyStrings) + warnOldShellSyntax(seps, keyStrings) + evaluate() + for + optQuery <- queryOption.? + keys <- + action match + case SingleAction => akp + case ShowAction | PrintAction | MultiAction => + for pairs <- rep1sep(akp, token(Space)) + yield pairs.flatten + filter = mkFilter(optQuery, structure) + keys1 = applyQuery(keys, filter) + p <- + if keys.nonEmpty && keys1.isEmpty then emptyResult + else evaluate(keys1) + yield p + end actParser0 + + private def queryOption: Parser[ProjectQuery] = + ProjectQuery.parser <~ Space <~ "/" <~ Space + + private def applyQuery( + pairs: Seq[(ScopedKey[_], Seq[String])], + filter: Option[ProjectRef => Boolean] + ): Seq[(ScopedKey[_], Seq[String])] = + filter match + case None => pairs + case Some(f) => + pairs.filter((pair) => { + pair._1.scope.project.toOption match + case Some(ref: ProjectRef) => f(ref) + case _ => true + }) + + private def mkFilter( + optQuery: Option[ProjectQuery], + structure: BuildStructure + ): Option[ProjectRef => Boolean] = + optQuery.map(_.buildQuery(structure)) private[this] final class ActAction private[this] final val ShowAction, MultiAction, SingleAction, PrintAction = new ActAction diff --git a/main/src/main/scala/sbt/internal/Aggregation.scala b/main/src/main/scala/sbt/internal/Aggregation.scala index 289f0ff623b..cf64ed94f53 100644 --- a/main/src/main/scala/sbt/internal/Aggregation.scala +++ b/main/src/main/scala/sbt/internal/Aggregation.scala @@ -254,22 +254,17 @@ object Aggregation { else extra.aggregates.forward(ref) } - def aggregate[T, Proj]( - key: ScopedKey[T], + def aggregate[A1, Proj]( + key: ScopedKey[A1], rawMask: ScopeMask, extra: BuildUtil[Proj], reverse: Boolean = false - ): Seq[ScopedKey[T]] = { + ): Seq[ScopedKey[A1]] = val mask = rawMask.copy(project = true) - Dag.topologicalSort(key) { k => - if (reverse) - reverseAggregatedKeys(k, extra, mask) - else if (aggregationEnabled(k, extra.data)) - aggregatedKeys(k, extra, mask) - else - Nil - } - } + if reverse then Dag.topologicalSort(key)(reverseAggregatedKeys(_, extra, mask)) + else if !aggregationEnabled(key, extra.data) then Dag.topologicalSort(key)((k) => Nil) + else Dag.topologicalSort(key)(aggregatedKeys(_, extra, mask)) + def reverseAggregatedKeys[T]( key: ScopedKey[T], extra: BuildUtil[_], diff --git a/main/src/main/scala/sbt/internal/ProjectQuery.scala b/main/src/main/scala/sbt/internal/ProjectQuery.scala new file mode 100644 index 00000000000..2d0903d4ac2 --- /dev/null +++ b/main/src/main/scala/sbt/internal/ProjectQuery.scala @@ -0,0 +1,55 @@ +package sbt +package internal + +import sbt.internal.util.complete.{ DefaultParsers, Parser } +import sbt.Keys.scalaBinaryVersion +import DefaultParsers.* +import scala.annotation.nowarn +import scala.util.matching.Regex +import sbt.internal.util.AttributeKey + +private[sbt] case class ProjectQuery( + projectName: String, + params: Map[AttributeKey[?], String], +): + import ProjectQuery.* + private lazy val pattern: Regex = Regex("^" + projectName.replace(wildcard, ".*") + "$") + + @nowarn + def buildQuery(structure: BuildStructure): ProjectRef => Boolean = + (p: ProjectRef) => + val projectMatches = + if projectName == wildcard then true + else pattern.matches(p.project) + val scalaMatches = + params.get(Keys.scalaBinaryVersion.key) match + case Some(expected) => + val actualSbv = structure.data.get(Scope.ThisScope.in(p), scalaBinaryVersion.key) + actualSbv match + case Some(sbv) => sbv == expected + case None => true + case None => true + projectMatches && scalaMatches +end ProjectQuery + +object ProjectQuery: + private val wildcard = "..." + + // make sure @ doesn't match on this one + def projectName: Parser[String] = + charClass(c => c.isLetter || c.isDigit || c == '_' || c == '.').+.string + .examples(wildcard) + + def parser: Parser[ProjectQuery] = + (projectName ~ + token("@scalaBinaryVersion=" ~> StringBasic.map((scalaBinaryVersion.key, _))) + .examples("@scalaBinaryVersion=3") + .?) + .map { case (proj, params) => + ProjectQuery(proj, Map(params.toSeq: _*)) + } + .filter( + (q) => q.projectName.contains("...") || q.params.nonEmpty, + (msg) => s"$msg isn't a query" + ) +end ProjectQuery diff --git a/sbt-app/src/sbt-test/actions/query/build.sbt b/sbt-app/src/sbt-test/actions/query/build.sbt new file mode 100644 index 00000000000..df2bf23a288 --- /dev/null +++ b/sbt-app/src/sbt-test/actions/query/build.sbt @@ -0,0 +1,23 @@ +scalaVersion := "3.3.3" + +lazy val someTask = taskKey[Unit]("") + +lazy val root = (project in file(".")) + .aggregate(foo, bar, baz) + .settings( + name := "root", + ) + +lazy val foo = project +lazy val bar = project +lazy val baz = project + .settings( + scalaVersion := "2.12.19", + ) + +someTask := { + val x = target.value / (name.value + ".txt") + val s = streams.value + s.log.info(s"writing $x") + IO.touch(x) +} diff --git a/sbt-app/src/sbt-test/actions/query/test b/sbt-app/src/sbt-test/actions/query/test new file mode 100644 index 00000000000..51593d75e94 --- /dev/null +++ b/sbt-app/src/sbt-test/actions/query/test @@ -0,0 +1,24 @@ +> ... / someTask + +$ exists target/out/jvm/scala-3.3.3/root/root.txt +$ exists target/out/jvm/scala-3.3.3/foo/foo.txt +$ exists target/out/jvm/scala-3.3.3/bar/bar.txt +$ exists target/out/jvm/scala-2.12.19/baz/baz.txt + +> clean + +> b... / someTask + +$ absent target/out/jvm/scala-3.3.3/root/root.txt +$ absent target/out/jvm/scala-3.3.3/foo/foo.txt +$ exists target/out/jvm/scala-3.3.3/bar/bar.txt +$ exists target/out/jvm/scala-2.12.19/baz/baz.txt + +> clean + +> ...@scalaBinaryVersion=3 / someTask + +$ exists target/out/jvm/scala-3.3.3/root/root.txt +$ exists target/out/jvm/scala-3.3.3/foo/foo.txt +$ exists target/out/jvm/scala-3.3.3/bar/bar.txt +$ absent target/out/jvm/scala-2.12.19/baz/baz.txt