Skip to content

Commit

Permalink
feat: queriable slash syntax (sbt query)
Browse files Browse the repository at this point in the history
**Problem**
We want a more flexible way of aggregating subprojects.

**Solution**
This implements a subproject filtering as a replacement of
the subproject axis in the act command.
  • Loading branch information
eed3si9n committed Sep 25, 2024
1 parent 864da87 commit 257c943
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 45 deletions.
104 changes: 71 additions & 33 deletions main/src/main/scala/sbt/internal/Act.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -485,49 +493,79 @@ 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("::")) {
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] = {
// 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
Expand Down
19 changes: 7 additions & 12 deletions main/src/main/scala/sbt/internal/Aggregation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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[_],
Expand Down
55 changes: 55 additions & 0 deletions main/src/main/scala/sbt/internal/ProjectQuery.scala
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions sbt-app/src/sbt-test/actions/query/build.sbt
Original file line number Diff line number Diff line change
@@ -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)
}
24 changes: 24 additions & 0 deletions sbt-app/src/sbt-test/actions/query/test
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 257c943

Please sign in to comment.