Suppose I have a Scala project with three sub-projects, with files like this:
foo/src/main/scala/Foo.scala
foo/src/main/resources/foo.txt
bar/src/main/scala/Bar.scala
bar/src/main/resources/bar.txt
baz/src/main/scala/Baz.scala
baz/src/main/resources/baz.txt
Foo.scala
contains a simple macro that reads a resource at a given path:
import scala.language.experimental.macros
import scala.reflect.macros.Context
object Foo {
def countLines(path: String): Option[Int] = macro countLines_impl
def countLines_impl(c: Context)(path: c.Expr[String]) = {
import c.universe._
path.tree match {
case Literal(Constant(s: String)) => Option(
this.getClass.getResourceAsStream(s)
).fold(reify(None: Option[Int])) { stream =>
val count = c.literal(io.Source.fromInputStream(stream).getLines.size)
reify(Some(count.splice))
}
case _ => c.abort(c.enclosingPosition, "Need a literal path!")
}
}
}
If the resource can be opened, countLines
returns the number of lines; otherwise it's empty.
The other two Scala source files just call this macro:
object Bar extends App {
println(Foo.countLines("/foo.txt"))
println(Foo.countLines("/bar.txt"))
println(Foo.countLines("/baz.txt"))
}
And:
object Baz extends App {
println(Foo.countLines("/foo.txt"))
println(Foo.countLines("/bar.txt"))
println(Foo.countLines("/baz.txt"))
}
The contents of the resources don't really matter for the purposes of this question.
If this is a Maven project, I can easily configure it so that the root project aggregates the three sub-projects and baz
depends on bar
, which depends on foo
. See this Gist for the gory details.
With Maven everything works as expected. Bar
can see the resources for foo
and bar
:
Some(1)
Some(2)
None
And Baz
can see all of them:
Some(1)
Some(2)
Some(3)
Now I try the same thing with SBT:
import sbt._
import Keys._
object MyProject extends Build {
lazy val root: Project = Project(
id = "root", base = file("."),
settings = commonSettings
).aggregate(foo, bar, baz)
lazy val foo: Project = Project(
id = "foo", base = file("foo"),
settings = commonSettings
)
lazy val bar: Project = Project(
id = "bar", base = file("bar"),
settings = commonSettings,
dependencies = Seq(foo)
)
lazy val baz: Project = Project(
id = "baz", base = file("baz"),
settings = commonSettings,
dependencies = Seq(bar)
)
def commonSettings = Defaults.defaultSettings ++ Seq(
scalaVersion := "2.10.2",
libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-compiler" % _)
)
}
But now Bar
can only see the resources in foo
:
Some(1)
None
None
And Baz
can only see foo
and bar
:
Some(1)
Some(2)
None
What's going on here? This SBT build file seems to me to be a pretty literal translation of the Maven configuration. I have no problem opening a console in the bar
project and reading /bar.txt
, for example, so why can't these projects see their own resources when calling a macro?
Create your subproject as a regular sbt project, but without a project subdirectory. Then, in your main project, define a project/Build.scala file that defines the dependencies between the main project and subprojects. This is demonstrated in the following example, which I created based on the sbt Multi-Project documentation:
You want to configure sbt to work with a main Scala project that depends on other subprojects you’re developing. Create your subproject as a regular sbt project, but without a project subdirectory. Then, in your main project, define a project/Build.scala file that defines the dependencies between the main project and subprojects.
unmanagedResources These are by default built up from unmanagedResourceDirectories, which by default is resourceDirectory, excluding files matched by defaultExcludes. managedResources By default, this is empty for standard projects. sbt plugins will have a generated descriptor file here.
This page discusses how sbt builds up classpaths for different actions, like compile, run, and test and how to override or augment these classpaths. In sbt, the classpath includes the Scala library and (when declared as a dependency) the Scala compiler. Classpath-related settings and tasks typically provide a value of type Classpath.
SBT does not add the resources of the current project into the build class-path. Probably because it is rarely needed.
You just have to pipe one (the resources) into the other (the classpath):
def commonSettings = Defaults.defaultSettings ++ Seq(
scalaVersion := "2.10.2",
libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-compiler" % _),
// add resources of the current project into the build classpath
unmanagedClasspath in Compile <++= unmanagedResources in Compile
)
In that project you would only need that for bar
and baz
.
UPDATE: Since sbt 0.13, <+=
and <++=
are unnecessary (and undocumented) thanks to the new macro-based syntax:
libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value
unmanagedClasspath in Compile ++= (unmanagedResources in Compile).value
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With