sudori part 6: sbt query
This is a blog post on sbt 2.x development, continuing from sbt 2.x remote cache, sbt 2.x remote cache with Bazel compatibility, sudori part 4, part 5 etc. I work on sbt 2.x in my own time with collaboration with the Scala Center and other volunteers, like Billy at EngFlow.
sbt query
In sbt 2.0 ideas I mentioned:
See sbt query. Query would be used to filter down the subprojects:
Once we have sbt-projectmatrix by default, there will be an increase in the number of subprojects. If you work on a large code base you might already have a lot of subprojects that you need to deal with.
For sbt 2.0, I’d like to propose a new mechanism of filtering down the subprojects during task aggregation:
sbt .../test
sbt abc.../test
sbt ...@scalaBinaryVersion=3/test
The idea of integrating with the existing slash syntax was proposed by Adrien Piquerez on the GitHub discussion thread.
What does query syntax do?
Here’s an example combined with print
command:
sbt:root> print .../name
foo / name
foo
bar / name
bar
baz / name
baz
name
root
...
here means match all subprojects, which is the same as saying print name
in sbt 1.x. Next, we can show the names of subprojects that starts with b only:
sbt:root> print b.../name
bar / name
bar
baz / name
baz
Next, we can show only the subprojects whose scalaBinaryVersion
is 3
:
sbt:root> print ...@scalaBinaryVersion=3/name
foo / name
foo
name
root
We can also test only the subprojects whose scalaBinaryVersion
is 3
:
sbt:root> ...@scalaBinaryVersion=3/test
[info] Passed: Total 0, Failed 0, Errors 0, Passed 0
[info] No tests to run for foo / Test / testQuick
[info] compiling 1 Scala source to target/out/jvm/scala-3.3.1/root/backend ...
[info] compiling 1 Scala source to target/out/jvm/scala-3.3.1/root/test-backend ...
[info] Passed: Total 0, Failed 0, Errors 0, Passed 0
[info] No tests to run for Test / testQuick
[success] elapsed time: 3 s, cache 58%, 7 disk cache hits, 5 onsite tasks
When we have Scala.JS and Native support, we’ll probalby add a way to filter by @platform
as well. I think it would look like:
sbt:root> ...@scalaBinaryVersion=3@platform=sjs1/test
details
The PR is at https://github.com/sbt/sbt/pull/7699.
the choice of ...
and @
Why use ...
and @
instead of, let’s say *
and ?
? The dot (.
) and at-sign (@
) seem to work on shell evironment without quoting. So I could write:
$ sbt --client .../test
On the other hand, if we used *
, we’d need to to quote the whole thing. *
might appear to be an intuitive choice, but we’d be creating affordance push door into a pitall. Consider the following:
$ sbt */test
In Bash, */test
will look for any directories that contains a file or directory named test
, so there’s a good chance it would match src/test
, so it would actually invoke:
$ sbt src/test
This would then fail with the following error message:
[error] Expected ID character
[error] Not a valid command: src (similar: set)
[error] Expected <unspecified>
[error] Expected '@scalaBinaryVersion='
[error] Expected project ID
[error] Expected configuration
[error] Expected ':'
[error] Expected key
[error] Not a valid key: src (similar: sources, ps, run)
[error] src/test
[error] ^
I can only picture people asking on GitHub issue or elsewhere why they get “Not a valid key: src” when they try to use the query, over and over.
act
command
sbt core concepts that I gave in Scala Days 2019 is five years ago as of this writing:
I start the talk by describing Command
as State => State
transformation function, which handles human and BSP interactions. Note that within a command tasks are executed in parallel, but the command-line interaction is sequential. The shell
command accepts inputs from the user, and the act
command lifts the task written in slash syntax into the task engine. This is also where the aggregation logic is handled, so we can introduce a new parser for the sbt query in the act
command to change the way aggregation is filtered.
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
Here, I’m using for comprehension to compose two parsers. First one checks if the first segment is an sbt query, and if so pass in askProject = false
, otherwise we will ask project foo / Compile / compile
like in sbt 1.x.
tab completion
The way tab completion works with JLine (I think) is to repeatedly try different parsers, so we have to make sure that Parser
would succeed, even if the sbt query might return nothing. So instead of outright failing at the parser level, we actually produce a dummy parser called emptyResult
, which would be used only to show an error message.
def emptyResult: Parser[() => State] =
Parser.success(() => throw MessageOnlyException("query result is empty"))
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
summary
https://github.com/sbt/sbt/pull/7699 implements an extention to slash syntax, which allows filtering of subprojects for sbt 2.x. Initially this will support scalaBinaryVersion
as parameter, but likely we’ll support platform
as well:
// TBD
sbt:root> ...@scalaBinaryVersion=3@platform=sjs1/test
The above would mean run (incremental) tests on all Scala.JS 1.x subprojects.