How to define build-scoped settings in multi-project .sbt builds? - scala

As of sbt 0.13, the recommended way to define all configuration is multi-project .sbt build definition. It is absolutely clear how to define settings for each project there; however, how one should define build-wide settings, for example, in ThisBuild or Global scopes?
With Scala build definitions it is clear: we have settings key coming from Build trait:
object MyBuild extends Build {
override lazy val settings = super.settings ++ Seq(
someKey := someValue
)
}
Now someKey's value will be build-scoped by default. Alternatively, it can be defined directly in .sbt file with ThisBuild scope:
someKey in ThisBuild := someValue
However, there is no natural place to define these options in multi-project .sbt builds. I suspect that they should be defined in one of the projects with ThisBuild scope:
val someProject = project
.settings(someKey in ThisBuild := someValue)
but this is very counterintuitive and not really clear: are such settings really applied globally or do they really belong to the specific project? What if I define the same key in multiple projects?
val project1 = project.settings(someKey in ThisBuild := someValue1)
val project2 = project.settings(someKey in ThisBuild := someValue2)
The documentation does not give any answers on this, unfortunately.
Note that I explicitly do not ask how to share global settings between builds or projects. I ask specifically about defining build-scoped settings.

Well, I've performed some experiments and found out the following. Here is an example build.sbt:
val testKey = settingKey[String]("A key for test")
testKey := "value in build.sbt"
testKey in ThisBuild := "build-scoped value in build.sbt"
testKey in Global := "global value in build.sbt"
lazy val root = (project in file("."))
.settings(
testKey := "value in root project",
testKey in ThisBuild := "build-scoped value in root project",
testKey in Global := "global value in root project"
)
lazy val child = project
.settings(
testKey := "value in child",
testKey in ThisBuild := "build-scoped value in child project",
testKey in Global := "global value in child project"
)
and here's what I see in SBT console:
[info] Set current project to root (in build file:/private/tmp/sbtt/)
> inspect testKey
[info] Setting: java.lang.String = value in build.sbt
[info] Description:
[info] A key for test
[info] Provided by:
[info] {file:/private/tmp/sbtt/}root/*:testKey
[info] Defined at:
[info] /private/tmp/sbtt/build.sbt:4
[info] Delegates:
[info] root/*:testKey
[info] {.}/*:testKey
[info] */*:testKey
[info] Related:
[info] */*:testKey
[info] {.}/*:testKey
[info] child/*:testKey
> testKey
[info] value in build.sbt
> root/testKey
[info] value in build.sbt
> child/testKey
[info] value in child
> {.}/testKey
[info] global value in child
> reload
[info] Loading global plugins from /Users/netvl/.sbt/0.13/plugins
[info] Set current project to root (in build file:/private/tmp/sbtt/)
> testKey
[info] value in build.sbt
> root/testKey
[info] value in build.sbt
> child/testKey
[info] value in child
> {.}/testKey
[info] build-scoped value in child project
> */testKey
[info] global value in child project
So, the conclusion is as follows.
Values defined at the topmost level in a multi-project SBT build seem to be scoped to the project build.sbt belongs to. That is, here build.sbt resides in the root directory, and so defining testKey in the file directly is the same as defining it in (project in file(".")).settings(...) clause. According to SBT output, they also take priority over those defined in .settings(...).
Keys in larger scopes like ThisBuild or Global seem to be able to be defined in any project, but "later" projects will then take priority. This is probably related to the order the projects are processed. I have expected that the order of evaluation of lazy vals would affect this, and I've tried adding .aggregate(child) to the root project to force the child project to be evaluated first, but it didn't seem to have any effect: globally scoped values from child project still have priority.
Therefore, it seems fine to define build-scoped or globally scoped values inside the root project or inside common settings list which is then used in every project. However, assigning different values to a key in build or global scope in different projects will likely lead to a weird behavior - only one of these values will take effect, and which one I don't know.
It would be very nice if all this were explained in the documentation, but at the moment I can't find anything on this behavior there.
I'd gladly accept someone else's answer which would provide more insight on how SBT works in this regard.

Related

scala.io.source.fromFile with sbt subProjects reading from root resource folder

I have a sbt build with multiple subProjects, when trying to use scala.io.source.fromFile it tries to read files from the root project and not from each individual subProject. This is what I need in some cases but in others I want to use the root resource.
What is the idiomatic way of reading from subproject and root resource folders in SBT with subprojects?
It depends on which scope you are calling resourceDirectory from. Here is an example of build with one root project and two sub-projects (from sbt REPL):
> resourceDirectory
[info] alpha/compile:resourceDirectory
[info] /Users/xyz/sbt-tutorial/sub-projects/sub-a/src/main/resources
[info] beta/compile:resourceDirectory
[info] /Users/xyz/sbt-tutorial/sub-projects/sub-b/src/main/resources
[info] root/compile:resourceDirectory
[info] /Users/xyz/sbt-tutorial/sub-projects/src/main/resources
As you can see, calling alpha/compile:resourceDirectory will get you the resource directory for the sub-project called alpha.
If you want the sbt DSL notation for this it will be:
myTask := {
val resDir = (resourceDirectory in (alpha, Compile)).value
...
}
You can replace alpha etc. with ThisBuild if you want to get the resource directory for the root project.

Running a sub-project main class

I have a built.sbt that references a child project's main class as its own main class:
lazy val akka = (project in file("."))
.aggregate(api)
.dependsOn(api)
.enablePlugins(JavaAppPackaging)
lazy val api = project in file("api")
scalaVersion := "2.11.6"
// This is referencing API code
mainClass in (Compile, run) := Some("maslow.akka.cluster.node.ClusterNode")
artifactName := { (sv: ScalaVersion, module: ModuleID, artifact: Artifact) =>
s"""${artifact.name}.${artifact.extension}"""
}
name in Universal := name.value
packageName in Universal := name.value
However, each time I run sbt run I get the following error:
> run
[info] Updating {file:/Users/mark/dev/Maslow-Akka/}api...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Updating {file:/Users/mark/dev/Maslow-Akka/}akka...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Running maslow.akka.cluster.node.ClusterNode
[error] (run-main-0) java.lang.ClassNotFoundException: maslow.akka.cluster.node.ClusterNode
java.lang.ClassNotFoundException: maslow.akka.cluster.node.ClusterNode
at java.lang.ClassLoader.findClass(ClassLoader.java:530)
As I've been doing some research into the problem, I first switched to the project to api from akka and then opened up console. From there, it can't find the maslow package even though it most certainly exists. After that, I went into the api folder and ran sbt console and it accessed the aforementioned package just fine. After I do this, sbt run from the akka project works. Why?
The folder api is pulled in via git read-tree. There shouldn't be anything special about it. I'm using sbt 0.13.5
I think in a multi-project build a global line such as
mainClass in (Compile, run) := ...
will just be swallowed without consequences, as it doesn't refer to any project.
Probably the following works:
mainClass in (Compile, run) in ThisBuild := ...
Or you add it to the root project's settings:
lazy val akka = (project in file("."))
.aggregate(api)
.dependsOn(api)
.enablePlugins(JavaAppPackaging)
.settings(
mainClass in (Compile, run) := ...
)
The problem was this line: lazy val api = project in file("api")
From the docs:
When defining a dependency on another project, you provide a ProjectReference. In the simplest case, this is a Project object. (Technically, there is an implicit conversion Project => ProjectReference) This indicates a dependency on a project within the same build.
This indicates a dependency within the same build. Instead, what I needed was to use RootProject since api is an external build:
It is possible to declare a dependency on a project in a directory separate from the current build, in a git repository, or in a project packaged into a jar and accessible via http/https. These are referred to as external builds and projects. You can reference the root project in an external build with RootProject:
In order to solve this problem, I removed the project declarations out of build.sbt into project/Build.scala:
import sbt._
object MyBuild extends Build {
lazy val akka = Project("akka", file(".")).aggregate(api).dependsOn(api)
lazy val api = RootProject(file("api"))
}
To be clear, the problem was that my api sub-project was a ProjectRef and not a RootProject.

sbt 0.13.1 multi-project module not found when I change the sbt default scala library to scala 2.11.2

I use the sbt 0.13.1 create the two modules, and I create project/MyBuild.scala to compile this two modules.
MyBuild.scala:
import sbt._
import Keys._
object MyBuild extends Build {
lazy val task = project.in(file("task"))
lazy val root = project.in(file(".")) aggregate(task) dependsOn task
}
When I change the scala library to 2.11.2 by set scalaHome. It will go to maven download the task.jar and failed, that's very strange. Is it a sbt bug?
There is the github test project address: test-sbt-0.13.1
This is because you have defined a custom scalaVersion in your main build.sbt which means it is defined for the root project only. The task project will use the default value:
jjst#ws11:test-sbt-0.13.1$ sbt
[...]
> projects
[info] In file:/home/users/jjost/dev/test-sbt-0.13.1/
[info] * root
[info] task
> show scalaVersion
[info] task/*:scalaVersion
[info] 2.10.4
[info] root/*:scalaVersion
[info] 2.11.2-local
As a consequence, artifacts generated by the task subproject won't be available for the root project to use.
You can solve this by making sure that your projects use the same scalaVersion. The easiest way to do so while keeping the same project structure would be to share common settings like the scala version across projects like so:
object MyBuild extends Build {
val commonSettings = Seq(
scalaVersion := "2.11.2-local",
scalaHome := Some(file("/usr/local/scala-2.11.2/"))
)
lazy val task = (project.in(file("task"))).
settings(commonSettings: _*)
lazy val root = (project.in(file("."))).
settings(commonSettings: _*).
aggregate(task).
dependsOn(task)
}
In practice, you might want to put common settings shared between projects in a dedicated file under project/common.scala, as recommended in Effective sbt.
I'd recommend you to move the sources of root project from root to subfolder (task2), and add them as aggregated. It will remove aggregating and depending on same project:
object MyBuild extends Build {
lazy val task = project.in(file("task"))
lazy val task2 = project.in(file("task2")) dependsOn task
lazy val root = project.in(file(".")) aggregate(task, task2)
}
This works for me, but it's strange that such non-standard project structure works fine with default scalaHome. Seems to be a problem in the way in which sbt resolves such dependency as external.
P.S. This answer doesn't cover the whole story. See #jjst's answer to clarify more.

Make one sbt config depend on another

The sbt documentation shows example of how to declare dependency only between projects. But I'm positive that there are ways to declare one config be dependent on another, just like the Test configuration uses classpath from the Compile configuration.
How can I declare my own configuration so that it would depend on the Compile config generated classpath?
I take a more close look to suggested solution and bunch of question arose again. So I reopen the question
I can not deduce sbt behavior solely from the delegate relation between Test and Compile config.
The source directories turn out to be completely different for Test and Compile config despite being delegated.
> show test:unmanagedSourceDirectories
[info] List(./src/test/scala, ./src/test/java)
> show test:scalaSource
[info] ./src/test/scala
> show test:javaSource
[info] ./src/test/java
> show compile:unmanagedSourceDirectories
[info] List(./src/main/scala, ./src/main/java)
> show compile:scalaSource
[info] ./src/main/scala
> show compile:javaSource
[info] ./src/main/java
And other significant keys such as unmanagementClasspath and fullClasspath are not SettingsKeys that may be naturally stacked with delegating. They are full-fledged TaskKeys that stores complex procedure for generating class-paths behind them.
So, the question is still actual: How can I mimic exporting classes from Compile to Test config for my custom defined config? And there is closely related optional question: how this is actually done for aforementioned pre-defined configs?
You can make one configuration extend another. You do that by using extend method.
lazy val MyConfig = config("myConfig") extend(Compile)
lazy val root = project.in(file(".")).
configs(MyConfig).
settings(inConfig(MyConfig)(Defaults.compileSettings ++ Defaults.compileInputsSettings): _*)
Extend will delegate to Compile in this case, for settings which are undefined in the MyConfig configuration.
You can check it by running SBT and executing for example show myConfig:managedClasspath, the output should be exactly the same as for show compile:managedClasspath.
If you inspect your new configuration's managedClasspath you'll see that it delegates to the compile.
[info] Delegates:
[info] myConfig:managedClasspath
[info] compile:managedClasspath
[info] *:managedClasspath
[info] {.}/myConfig:managedClasspath
[info] {.}/compile:managedClasspath
[info] {.}/*:managedClasspath
[info] */myConfig:managedClasspath
[info] */compile:managedClasspath
[info] */*:managedClasspath
As I've stated above, SBT will only delegate to the setting if it's not defined in the given configuration.
For example, if you do not define any specific compiler options for myConfig the settings will be taken from compile.
> show compile:scalacOptions
[info] List()
> show myConfig:scalacOptions
[info] List()
Changing setting in compile configuration will have an effect on myConfig:
> set scalacOptions in Compile += "-Xexperimental"
> show compile:scalacOptions
[info] List(-Xexperimental)
> show myConfig:scalacOptions
[info] List(-Xexperimental)
Overriding the setting in myConfig will make SBT to use the setting defined in that configuration, while Compile will have its own value:
> set scalacOptions in MyConfig := Seq("-Xcheck-null")
> show compile:scalacOptions
[info] List(-Xexperimental)
> show myConfig:scalacOptions
[info] List(-Xcheck-null)
Note the delegation is one way. Change to the MyConfig has no influence on the Compile configuration.
You can check the documentation for details.

How does sbt choose which Scala version to use?

I have multiple projects in my sbt build. I'm trying to upgrade to Scala 2.10 from 2.9.1, so in my build.sbt file I put
scalaVersion := "2.10.0"
This seemed to work, because in my top-level project in sbt I get:
> scala-version
[info] 2.10.0
However, when I switch to one of the other projects:
> project web-client
[info] Set current project to web-client (in build file:/C:/Users/...
[web-client] $ scala-version
[info] 2.9.1
You see the version has now changed back to 2.9.1! How do I force the same Scala version to be used across all my projects?
I found out scoping the scalaVersion to ThisBuild will set it for all sub-projects. Details are here: http://www.scala-sbt.org/release/docs/Getting-Started/Multi-Project.html at the bottom, but here is what it says:
To set it only once, it is enough to write, in the main build.sbt file, the following line:
scalaVersion in ThisBuild := "2.10.0"
SBT has a default Scala version. You need to add the scalaVersion setting to all subprojects if you wish to change it. The most common way of doing that is having a "common settings" value that is added to all projects at the root level, through project/Build.scala.