diff --git a/main/src/main/scala/sbt/internal/Act.scala b/main/src/main/scala/sbt/internal/Act.scala index c2dd6bbb6c..2522e7ff9d 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 @@ -49,6 +57,10 @@ object Act { private[sbt] val colonSeq: Seq[String] = Seq(":") private[sbt] val colonColonSeq: Seq[String] = Seq("::") + type KeysParser = Parser[Seq[ScopedKey[Any]]] + type KeysParserSep = Parser[Seq[(ScopedKey[Any], Seq[String])]] + type KeysParserFilter = Parser[Seq[(ScopedKey[Any], Option[ProjectQuery])]] + // this does not take aggregation into account def scopedKey( index: KeyIndex, @@ -57,7 +69,7 @@ object Act { keyMap: Map[String, AttributeKey[_]], data: Settings[Scope] ): Parser[ScopedKey[Any]] = - scopedKeySelected(index, current, defaultConfigs, keyMap, data) + scopedKeySelected(index, current, defaultConfigs, keyMap, data, askProject = true) .map(_.key.asInstanceOf[ScopedKey[Any]]) // the index should be an aggregated index for proper tab completion @@ -72,7 +84,8 @@ object Act { current, defaultConfigs, structure.index.keyMap, - structure.data + structure.data, + askProject = true, ) ) yield Aggregation.aggregate( @@ -81,6 +94,28 @@ object Act { structure.extra ) + def scopedKeyAggregatedFilter( + current: ProjectRef, + defaultConfigs: Option[ResolvedReference] => Seq[String], + structure: BuildStructure + ): KeysParserFilter = + for + optQuery <- queryOption.? + selected <- scopedKeySelected( + structure.index.aggregateKeyIndex, + current, + defaultConfigs, + structure.index.keyMap, + structure.data, + askProject = optQuery.isEmpty, + ) + yield Aggregation + .aggregate(selected.key, selected.mask, structure.extra) + .map(k => k.asInstanceOf[ScopedKey[Any]] -> optQuery) + + private def queryOption: Parser[ProjectQuery] = + ProjectQuery.parser <~ spacedSlash + def scopedKeyAggregatedSep( current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], @@ -91,7 +126,8 @@ object Act { current, defaultConfigs, structure.index.keyMap, - structure.data + structure.data, + askProject = true, ) yield Aggregation .aggregate(selected.key, selected.mask, structure.extra) @@ -102,24 +138,29 @@ object Act { current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], keyMap: Map[String, AttributeKey[_]], - data: Settings[Scope] + data: Settings[Scope], + askProject: Boolean, ): Parser[ParsedKey] = - scopedKeyFull(index, current, defaultConfigs, keyMap).flatMap { choices => - select(choices, data)(showRelativeKey2(current)) + scopedKeyFull(index, current, defaultConfigs, keyMap, askProject = askProject).flatMap { + choices => + select(choices, data)(showRelativeKey2(current)) } def scopedKeyFull( index: KeyIndex, current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], - keyMap: Map[String, AttributeKey[_]] + keyMap: Map[String, AttributeKey[_]], + askProject: Boolean, ): Parser[Seq[Parser[ParsedKey]]] = { val confParserCache : mutable.Map[Option[sbt.ResolvedReference], Parser[(ParsedAxis[String], Seq[String])]] = mutable.Map.empty def fullKey = - for { - rawProject <- optProjectRef(index, current) + for + rawProject <- + if askProject then optProjectRef(index, current) + else success(Omitted) proj = resolveProject(rawProject, current) confPair <- confParserCache.getOrElseUpdate( proj, @@ -131,7 +172,7 @@ object Act { ) (confAmb, seps) = confPair partialMask = ScopeMask(rawProject.isExplicit, confAmb.isExplicit, false, false) - } yield taskKeyExtra(index, defaultConfigs, keyMap, proj, confAmb, partialMask, seps) + yield taskKeyExtra(index, defaultConfigs, keyMap, proj, confAmb, partialMask, seps) val globalIdent = token(GlobalIdent ~ spacedSlash) ^^^ ParsedGlobal def globalKey = @@ -485,49 +526,59 @@ 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 => - val akp = aggregatedKeyParserSep(extracted) - def warnOldShellSyntax(seps: Seq[String], keyStrings: String): Unit = - if (seps.contains(":") || seps.contains("::")) { - state.log.warn( - s"sbt 0.13 shell syntax is deprecated; use slash syntax instead: $keyStrings" - ) - } else () - def evaluate(pairs: Seq[(ScopedKey[_], Seq[String])]): Parser[() => State] = { - val kvs = pairs.map(_._1) - val seps = pairs.headOption.map(_._2).getOrElse(Nil) + actionParser.flatMap: action => + val akp = aggregatedKeyParserFilter(extracted) + // 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(kvs: Seq[ScopedKey[_]]): Parser[() => State] = 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) + evaluate() + for + keys <- + action match + case SingleAction => akp + case ShowAction | PrintAction | MultiAction => + for pairs <- rep1sep(akp, token(Space)) + yield pairs.flatten + keys1 = applyQuery(keys, structure) + p <- + if keys.nonEmpty && keys1.isEmpty then emptyResult + else evaluate(keys1.map(_._1)) + yield p + end actParser0 + + private def applyQuery( + pairs: Seq[(ScopedKey[_], Option[ProjectQuery])], + structure: BuildStructure, + ): Seq[(ScopedKey[_], Option[ProjectQuery])] = + pairs.filter { + case (_, None) => true + case (keys, Some(query)) => + val f = query.buildQuery(structure) + keys.scope.project.toOption match + case Some(ref: ProjectRef) => f(ref) + case _ => true } - } private[this] final class ActAction private[this] final val ShowAction, MultiAction, SingleAction, PrintAction = new ActAction @@ -551,9 +602,6 @@ object Act { structure.data ) - type KeysParser = Parser[Seq[ScopedKey[Any]]] - type KeysParserSep = Parser[Seq[(ScopedKey[Any], Seq[String])]] - def aggregatedKeyParser(state: State): KeysParser = aggregatedKeyParser(Project extract state) def aggregatedKeyParser(extracted: Extracted): KeysParser = aggregatedKeyParser(extracted.structure, extracted.currentRef) @@ -567,6 +615,13 @@ object Act { currentRef: ProjectRef ): KeysParserSep = scopedKeyAggregatedSep(currentRef, structure.extra.configurationsForAxis, structure) + private[sbt] def aggregatedKeyParserFilter(extracted: Extracted): KeysParserFilter = + aggregatedKeyParserFilter(extracted.structure, extracted.currentRef) + private[sbt] def aggregatedKeyParserFilter( + structure: BuildStructure, + currentRef: ProjectRef + ): KeysParserFilter = + scopedKeyAggregatedFilter(currentRef, structure.extra.configurationsForAxis, structure) def keyValues[T](state: State)(keys: Seq[ScopedKey[T]]): Values[T] = keyValues(Project extract state)(keys) diff --git a/main/src/main/scala/sbt/internal/Aggregation.scala b/main/src/main/scala/sbt/internal/Aggregation.scala index 289f0ff623..68700681e5 100644 --- a/main/src/main/scala/sbt/internal/Aggregation.scala +++ b/main/src/main/scala/sbt/internal/Aggregation.scala @@ -254,22 +254,18 @@ 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 - } - } + Dag.topologicalSort(key): (k) => + if reverse then reverseAggregatedKeys(k, extra, mask) + else if aggregationEnabled(k, extra.data) then aggregatedKeys(k, extra, mask) + else Nil + 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 0000000000..2d0903d4ac --- /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/aggregate/project/Marker.scala b/sbt-app/src/sbt-test/actions/aggregate/project/Marker.scala index bffdbc0934..8a1cfec721 100644 --- a/sbt-app/src/sbt-test/actions/aggregate/project/Marker.scala +++ b/sbt-app/src/sbt-test/actions/aggregate/project/Marker.scala @@ -6,8 +6,8 @@ object Marker extends AutoPlugin: override def trigger = allRequirements override def requires = sbt.plugins.JvmPlugin object autoImport { - final lazy val Mark = TaskKey[Unit]("mark") - final def mark: Initialize[Task[Unit]] = mark(baseDirectory) + final lazy val mark = taskKey[Unit]("mark") + final def markTask: Initialize[Task[Unit]] = mark(baseDirectory) final def mark(project: Reference): Initialize[Task[Unit]] = mark(project / baseDirectory) final def mark(baseKey: SettingKey[File]): Initialize[Task[Unit]] = baseKey.toTaskable mapN { base => diff --git a/sbt-app/src/sbt-test/actions/aggregate/test b/sbt-app/src/sbt-test/actions/aggregate/test index fcd925ea85..ec7dc3ff3b 100644 --- a/sbt-app/src/sbt-test/actions/aggregate/test +++ b/sbt-app/src/sbt-test/actions/aggregate/test @@ -5,19 +5,19 @@ $ absent ran # single project, 'mark' defined -> set Mark := mark.value +> set mark := markTask.value > mark $ exists ran $ delete ran # single project, aggregate = true on Mark -> set Mark / aggregate := true +> set mark / aggregate := true > mark $ exists ran $ delete ran # single project, aggregate = false on Mark -> set Mark / aggregate := false +> set mark / aggregate := false > mark $ exists ran $ delete ran @@ -28,14 +28,14 @@ $ copy-file changes/build.sbt build.sbt > reload # define in root project only -> set Mark := mark.value +> set mark := markTask.value > mark $ exists ran $ absent sub/ran $ delete ran # define in sub project, but shouldn't run without aggregation -> set sub / Mark := mark(sub).value +> set sub / mark := mark(sub).value > mark $ exists ran $ absent sub/ran @@ -57,8 +57,8 @@ $ touch aggregate > reload # add tasks to each subproject -> set sub / Mark := mark(sub).value -> set sub2 / Mark := mark(sub2).value +> set sub / mark := mark(sub).value +> set sub2 / mark := mark(sub2).value # check that aggregation works when root project has no task > mark @@ -73,17 +73,17 @@ $ absent ran $ delete sub/ran sub/sub/ran # add task to root project -> set Mark := mark.value +> set mark := markTask.value # disable aggregation for sub/mark so that sub2/mark doesn't run -> set sub / Mark / aggregate := false +> set sub / mark / aggregate := false > mark $ exists ran sub/ran $ absent sub/sub/ran $ delete ran sub/ran # the aggregation setting in a leaf shouldn't affect whether it can be run directly -> set sub2 / Mark / aggregate := false +> set sub2 / mark / aggregate := false > sub2/mark $ exists sub/sub/ran $ absent ran sub/ran 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 0000000000..df2bf23a28 --- /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 0000000000..51593d75e9 --- /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