Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reading resources from a macro in an SBT project

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?

like image 363
Travis Brown Avatar asked Jun 16 '13 14:06

Travis Brown


People also ask

How do I create a subproject in SBT?

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:

How do I use sbt with Scala project?

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.

What are managed and unmanaged resources in SBT?

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.

How does sbt build up classpaths?

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.


1 Answers

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
like image 184
gourlaysama Avatar answered Oct 21 '22 16:10

gourlaysama