Reputation: 5977
the sbt task documentation shows an example of usage dependencies. It is very simple, artificial but it works! So I reproduced it in my project/scala.build
without problem.
Note that I choose global scope to make tasks available for any project and any configuration
import sbt._
import Keys._
object TestBuild extends Build {
lazy val sampleTask = taskKey[Int]("A sample task")
lazy val intTask = taskKey[Int]("An int task")
override lazy val settings = super.settings ++ Seq(
intTask := 1 + 2 ,
sampleTask := intTask.value + 1
)
}
Now I'm trying to do something useful and enrich existing sbt key definitions with task that collects compiled class names
import sbt._
import Keys._
import sbt.inc.Analysis
import xsbti.api.ClassLike
import xsbt.api.Discovery.{isConcrete, isPublic}
object TestBuild extends Build {
lazy val debugAPIs = taskKey[List[String]]("list of all top-level definitions")
override lazy val settings = super.settings ++ Seq(
debugAPIs := getAllTop( compile.value )
)
private def getAllTop(analysis : Analysis) : List[String] =
Tests.allDefs(analysis).toList collect {
case c : ClassLike if isConcrete(c) && isPublic(c) => c.name
}
}
Now I get error from sbt:
Reference to undefined setting:
{.}/*:compile from {.}/*:debugAPIs (/home/sbt/project/build.scala:11)
So I have two questions:
I'm more interested in the second question actually. I look for deep understanding of how sbt works because I'd like to write a plugin for it.
Upvotes: 1
Views: 62
Reputation: 13749
The problem is that you try to access a key value without a proper Scope.
The documentation gives us some hint here.
By default, all the keys associated with compiling, packaging, and running are scoped to a configuration and therefore may work differently in each configuration. The most obvious examples are the task keys compile, package, and run; but all the keys which affect those keys (such as source-directories or scalac-options or full-classpath) are also scoped to the configuration.
Let's first focus on a very simple example, which maybe doesn't make much sense, but illustrates the problem. Lets assume that you want to redefine the compile
task to itself.
override lazy val settings = super.settings ++ Seq (
compile := { compile.value }
)
Running this in SBT will give you an error, which is more or less like this
[error] {.}/*:compile from {.}/*:compile (/tmp/q-23723818/project/Build.scala:12)
[error] Did you mean compile:compile ?
We didn't specify the scope so SBT picked some defaults. The project was set to ThisBuild
(meaning no specific project) and configuration set to Global
. The setting was undefined in that context. However it's important to understand that a key is not a setting. The key can exist without scope, but the value of a key is attached to a scope. Note also that, if SBT won't find the value in the requested scope it can delegate to other scopes, but this is another topic.
How can we check this? Turns out that quite simple. Let's ignore the error, and let the SBT start.
If you type inspect compile
you'll see that the inspect will look in compile:compile
, where the value is defined. We can force it to look in a specific scope, e.g. inspect {.}/*:compile
, will look in scope that gave us the error.
> inspect {.}/*:compile
[info] No entry for key.
Indeed it's undefined.
How to solve the issue? You have to give SBT the scope you're looking for. Naively you could try to add a configuration scope.
// this will NOT work
override lazy val settings = super.settings ++ Seq (
compile in Compile := { (compile in Compile).value }
)
Well but there is no global compile, there is only compile per project. You could overcome the issue by not overriding global settings, but the settings for a specific project, and specifying Compile
configuration there.
lazy val root = project.in(file(".")).settings(Seq(
compile in Compile := {(compile in Compile).value}
): _*)
This would work,but what if you want to get the compile value regardless of where it is? This is where ScopeFilter comes in handy. Back to your original example. I assume you want to get compile's Analysis object from all the projects.
import sbt._
import Keys._
import sbt.inc.Analysis
import xsbti.api.ClassLike
import xsbt.api.Discovery.{isConcrete, isPublic}
object TestBuild extends Build {
val debugAPIs = taskKey[Seq[String]]("list of all top-level definitions")
val compileInAnyProject = ScopeFilter(inAnyProject, inConfigurations(Compile))
override lazy val settings = super.settings ++ Seq(
debugAPIs := {
getAllTop(compile.all(compileInAnyProject).value)
}
)
private def getAllTop(analyses : Seq[Analysis]) : Seq[String] =
analyses.flatMap { analysis =>
Tests.allDefs(analysis) collect { case c : ClassLike if isConcrete(c) && isPublic(c) => c.name }
}
}
What we created is a ScopeFilter
filtering for any project, and in that projects for Compile
configuration. Then we looked for all compile values.
You can configure the ScopeFilter
to match your needs, and only filter for specific projects/configurations or even tasks. But the key to understand the problem is to remember that in SBT settings are always scoped.
You have asked how it comes that the compile
is not defined globally but is available to every project. This is because there is Defaults.defaultSettings
which define it. And each project include it. If you removed super.settings
from your Build definition you'd see that among others compile
is undefined.
And as if you should do it this way. Well overriding settings
in your plugin is in general discouraged in Plugin Best Practices. However I recommend that you read it, together with Plugins chapter. It should give you an idea of how to proceed.
You can also get multiple values from multiple scopes by defining new task returning them. For example to get analyses with a project, you could use following piece of code.
object TestBuild extends Build {
val debugAPIs = taskKey[Seq[(String, String)]]("list of all top-level definitions")
val compileInAnyProject = ScopeFilter(inAnyProject, inConfigurations(Compile))
override lazy val settings = super.settings ++ Seq(
debugAPIs := {
getAllTop(analysisWithProject.all(compileInAnyProject).value)
}
)
lazy val analysisWithProject = Def.task { (thisProject.value, compile.value) }
private def getAllTop(analyses : Seq[(ResolvedProject, Analysis)]) : Seq[(String, String)] =
analyses.flatMap { case (project, analysis) =>
Tests.allDefs(analysis) collect { case c : ClassLike if isConcrete(c) && isPublic(c) => (project.id, c.name) }
}
}
Upvotes: 3