How to create an sbt project with subprojects

This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is a short recipe, Recipe 18.6, “How to create an SBT project with subprojects.”

Problem

You want to configure sbt to work with a main Scala project that depends on other subprojects you’re developing.

Solution

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:

import sbt._
import Keys._

/**
  * based on http://www.scala-sbt.org/release/docs/Getting-Started/Multi-Project
  */
object HelloBuild extends Build {
  // aggregate: running a task on the aggregate project will also run it
  // on the aggregated projects.
  // dependsOn: a project depends on code in another project.
  // without dependsOn, you'll get a compiler error: "object bar is not a
  // member of package com.alvinalexander".
  lazy val root = Project(id = "hello",
                        base = file(".")) aggregate(foo, bar) dependsOn(foo, bar)
  // sub-project in the Foo subdirectory
  lazy val foo = Project(id = "hello-foo",
                         base = file("Foo"))
  // sub-project in the Bar subdirectory
  lazy val bar = Project(id = "hello-bar",
                         base = file("Bar"))
}

To create your own example, you can either follow the instructions in the SBT Multi-Project documentation to create a main project with subprojects, or clone my SBT Subproject Example on GitHub, which I created to help you get started quickly.

Discussion

Creating a main project with subprojects is well documented on the sbt website, and the primary glue that defines the relationships between projects is the project/Build.scala file you create in your main project.

In the example shown, my main project depends on two subprojects, which are in directories named Foo and Bar beneath my project’s main directory. I reference these projects in the following code in my main project, so it’s necessary to tell sbt about the relationship between the projects:

package com.alvinalexander.subprojecttests

import com.alvinalexander.bar._
import com.alvinalexander.foo._

object Hello extends App {
    println(Bar("I'm a Bar"))
    println(Bar("I'm a Foo"))
}

The following output from the Unix tree command shows what the directory structure for my project looks like, including the files and directories for the main project, and the two subprojects:

|-- Bar
|   |-- build.sbt
|   +-- src
|       |-- main
|       |   |-- java
|       |   |-- resources
|       |   +-- scala
|       |       +-- Bar.scala
|       +-- test
|           |-- java
|           +-- resources
|-- Foo
|   |-- build.sbt
|   +-- src
|       |-- main
|       |   |-- java
|       |   |-- resources
|       |   +-- scala
|       |       +-- Foo.scala
|       +-- test
|           |-- java
|           +-- resources
|-- build.sbt
|-- project
|   |-- Build.scala
|
+-- src
    |-- main
    |   |-- java
    |   |-- resources
    |   +-- scala
    |       +-- Hello.scala
    +-- test
        |-- java
        |-- resources
        +-- scala
            +-- HelloTest.scala

To experiment with this yourself, I encourage you to clone my GitHub project.

Extra sbt sub-project 'run main' example

As an added note, I’ll show how to run a main method that’s in a sub-project.

Given this project:

Which contains this build.sbt file:

ThisBuild / scalaVersion := "2.13.3"
ThisBuild / organization := "com.innerproduct"
ThisBuild / version := "0.0.1-SNAPSHOT"
ThisBuild / fork := true

val CatsVersion = "2.2.0"
val CatsEffectVersion = "2.2.0"
val CatsTaglessVersion = "0.11"
val CirceVersion = "0.13.0"
val Http4sVersion = "0.21.4"
val LogbackVersion = "1.2.3"
val MunitVersion = "0.7.8"

val commonSettings =
  Seq(
    addCompilerPlugin(
      "org.typelevel" %% "kind-projector" % "0.11.1" cross CrossVersion.full
    ),
    libraryDependencies ++= Seq(
      "org.scalameta" %% "munit" % MunitVersion % Test
    ),
    testFrameworks += new TestFramework("munit.Framework")
  )

lazy val exercises = (project in file("exercises"))
  .settings(commonSettings)
  .settings(
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-effect" % CatsEffectVersion,
      "org.typelevel" %% "cats-effect-laws" % CatsEffectVersion % Test
    ),
    // remove fatal warnings since exercises have unused and dead code blocks
    scalacOptions --= Seq(
      "-Xfatal-warnings"
    )
  )

lazy val petstore = (project in file("case-studies") / "petstore")
  .dependsOn(exercises % "test->test;compile->compile")
  .settings(commonSettings)
  .settings(
    scalacOptions += "-Ymacro-annotations", // required by cats-tagless-macros
    libraryDependencies ++= Seq(
      "ch.qos.logback" % "logback-classic" % LogbackVersion,
      "io.circe" %% "circe-generic" % CirceVersion,
      "org.http4s" %% "http4s-blaze-server" % Http4sVersion,
      "org.http4s" %% "http4s-blaze-client" % Http4sVersion,
      "org.http4s" %% "http4s-circe" % Http4sVersion,
      "org.http4s" %% "http4s-dsl" % Http4sVersion,
      "org.typelevel" %% "cats-tagless-macros" % CatsTaglessVersion,
      "org.scalameta" %% "munit-scalacheck" % MunitVersion % Test
    )
  )

Start sbt in the root directory of the project. Then, to run the HelloWorld example that’s under the exercises/src/main/scala/com.innerproduct.ee/resources directory, run this command from inside the sbt shell:

sbt> exercises/runMain com.innerproduct.ee.resources.HelloWorld

When you do that you’ll see output that actually looks like this:

sbt:essential-effects-code> exercises/runMain com.innerproduct.ee.resources.HelloWorld
[warn] multiple main classes detected: run 'show discoveredMainClasses' to see the list
[info] running (fork) com.innerproduct.ee.resources.HelloWorld 
[info] Hello world!

See Also