Sub-project management via sbt

image credit to Aeon

As your project grows, the need of splitting it into multiple ones might rise. There are couple good examples around how to do multi-project management by sbt, like this one.

The use case this post cover is little different.

We would like to extrac some logic from our project into a child one (a jar), later, other projects could import it.

So you see, this is more like a parent-child relationship:

  • A parent project.
  • Extrac some logic from parent project into a child project.
  • Publish a jar to artifactory from child project.
  • Create jar of parent project, with everything child project has. Later we still want to deploy our service with this jar.

Github example of what this post cover.


In the github example above, we simply the usecase into following:

  • Parent project with main entry Hello, and one test class HelloSpec.
  • Child project with one base trait HelloFromChild, and one base test trait HelloFromChildTest.
  • Class in parent project depends on trait inside child project.
  • Test class in parent depends on test trait inside child project.

Create Child Project

First order of business, create child project in build.sbt. Given the parent project has setting like following:

lazy val root = (project in file("."))

Let’s create child:

lazy val child  = Project("child", file("child"))

file(“child”) ask sbt to project with code under /child.

With IDE (IntelliJ), let’s create a folder tree next to /src : /child → /src →/main and /test. Under /main and /test, create /scala , then we can create package under it.

child project and its directory

Classpath

Create a trait inside childExample package:

//HelloFromChild.scala in child
package childExample

trait HelloFromChild {
lazy val greeting: String = "HelloFromChild"
}

To use child’s trait inside parent, we need to tell sbt that parent depends on child:

//build.sbt
lazy val root = (project in file("."))
.settings(
...
)
.dependsOn(child)

Then use child trait inside parent is a straight forward import:

package example

import childExample.HelloFromChild

object Hello extends HelloFromChild with App {
println(greeting)
}

How about test ? Let’s say we have a base test trait in child like this:

package childExample

import org.scalatest._

trait HelloFromChildTest extends FlatSpec with Matchers {
}

Let’s use it in parent’s test ?

package example

import childExample.HelloFromChildTest

class HelloSpec extends HelloFromChildTest {
"The Hello object" should "say hello" in {
Hello.greeting shouldEqual "HelloFromChild"
}
}

However above won’t just work.

sbt test complains about HelloFromChildTest:

Why ?

So by default, sbt won’t include child’s test class into parent’s classpath, we need to specify it by test->test:

//build.sbt
lazy val root = (project in file("."))
.settings(
...
)
.dependsOn(child % "compile->compile;test->test")

Now sbt test works:


Create & deploy a single jar from parent

We still would like to create a jar with everything we have in this project (parent + child). With assembly plugin, specify settings in parent project:

//build.sbt
lazy val assemblySettings = Seq(
assemblyJarName in assembly := name.value + "-assembly.jar",
)
lazy val root = (project in file("."))
.settings(
...
assemblySettings,
name := "Project-with-example",
...
)
.aggregate(child)
.dependsOn(child % "compile->compile;test->test")

Aggregate

You might notice sbt assembly create a parent jar, and a child jar.

Why ?

notice the .aggregate(child) in build.sbt above ? It’s telling sbt that every task we run on parent, we also want it on child. But do we need it ?

It depends.

In our use case, say during sbt test, if we would like to run all tests in parent and in child, aggregate make sense.

If you are not sure what a task under a project would do, ask sbt:

show clean task of root

Clean task of root(parent) will trigger clean task of child.

What if I don’t want certain task (say reStart) to be aggregated ? Just tell sbt in the setting:

#build.sbt
lazy val root = project.in(file("."))
.settings(
...
aggregate in reStart := false,

Publish to artifactory from child

If we would like to publish only from child project to some artifactory, specify the setting only under child would do:

//build.sbt
lazy val child  = Project("child", file("child"))
.settings(
...
publishTo := Some("Artifactory Realm" at "https://where-your-artifactory-is/jar-name;build.timestamp=" + new java.util.Date().getTime),
credentials += Credentials("Artifactory Realm", "where-your-artifactory-is", ${USER}, ${PWD}),
)

Then run publish task for child: sbt child/publish


I find it a good pratice to narrow down settings only to the project need.

Like the publishTo and credentials above, they only tie to child, even if you do sbt publish, it won’t publish parent — parent has no idea where to publish at all 😂