mirror of https://github.com/sbt/sbt.git
feat: queriable slash syntax (sbt query)
**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.
This commit is contained in:
parent
b5d3a6d258
commit
e61ae80088
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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[_],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue