SubProjects  

Overview

This page describes how to manage multiple projects within a project. The first section details the features of subproject support and the second section describes how to use this support.

Features

Summary

  • Declare dependencies between projects and between tasks in different projects
  • Execute tasks across multiple projects
  • Classpath of all dependencies is included when compiling a project
  • Proper inter-project source dependency handling
  • Can change to a project in an interactive session to work only on that project (and its dependencies)
  • Can run tasks in parallel

Details

With subproject support in sbt, you can specify project-level dependencies and execute tasks across multiple projects. For example, assume you have three projects A, B, and C and you specify that C depends on B and B depends on A. When you execute the compile action on project C, it will first execute compile on A, then B, and finally on C.

Additionally, the classpath of a project's dependency is available to that project. So, in the situation above, the classpath of C includes the A and B classpaths. This includes the compiled classes of A and B and all libraries of A and B in addition to those explicitly specified for C.

Source dependencies are tracked between projects, so that the right sources are recompiled when a dependency is updated. For example, assume each project has two classes. A has A1 and A2, B has B1 and B2, and C has C1 and C2. Let C1 depend on B1 and B1 depend on A1. Now, assume class A1 is modified. Executing compile on project C will recompile class A1 in project A, then class B1 in project B, and finally, class C1 in project C. A2 will only be recompiled if it depends on A1. B2 will only be recompiled if it depends on A1 or B1 (or A2 if A2 depends on A1). C2 will only be recompiled if it depends on A1 or B1 or C1 (or A2 or B2 if they required recompilation).

Finally, when working in an interactive session, you can switch to a specific project to only execute commands on that project and its dependencies. More details on this are given in the next section.

Usage

To demonstrate subprojects, consider a project for a simulator (flight simulator, circuit simulator, ...). It might have the backend as one project and the user interface as a separate project. Then, there might be some examples demonstrating using the backend as a library or some examples showing how to use the user interface. The following project definition (which goes in project/build as described in BuildConfiguration) would be one way of describing this project:

import sbt._

class SimulatorProject(info: ProjectInfo) extends ParentProject(info)
{
   lazy val core = project("core", "Simulator Core")
   lazy val ui = project("ui", "Simulator User Interface", core)
   
   lazy val examples = project("examples", "Simulator Examples", new Examples(_))
   
   class Examples(info: ProjectInfo) extends ParentProject(info)
   {
      lazy val embedded = project("embedded", "Embedded Simulator Example", core)
      lazy val standalone = project("standalone", "Standalone Simulator Example", ui)
   }
}

The first line

import sbt._

imports everything in the sbt package. All sbt classes for general use are currently in the sbt package. The next line

class SimulatorProject(info: ProjectInfo) extends ParentProject(info)

defines a class with the usual constructor signature and is a subclass of sbt.ParentProject, which is in turn a subclass of sbt.Project. ParentProject is the starting point for configuring a project that defines other projects. SimulatorProject will be the initial project on sbt startup and all actions will be performed on it.

The remaining lines define subprojects of the main project. The project method is defined in sbt.Project and has several variants. The main variant used here has the signature

def project(path: Path, name: String, deps: Project*): Project

A Path is described in Paths. The path provided to project specifies the directory containing the subproject and is relative to the project directory of the enclosing project (SimulatorProject in this case). name is the name of the project and is specified here to avoid the need to set the name property in every subproject directory like in a single project. The version of the new project is inherited from the enclosing project. The last argument is a list of dependencies of the project.

Implicit in using this method is that the project is of the default project type, sbt.DefaultProject. This can be changed by using the variant of project with another argument before the dependencies of type:

ProjectInfo => Project

As is shown for the definition of examples, this is usually just the constructor of a Project subclass.

The first note about Examples is that it is embedded in SimulatorProject. This is important so that sbt only finds one public, concrete, top-level subclass of Project and automatically loads it on startup. Alternatively, the class could be top-level but prefixed with protected. Next, Examples subclasses ParentProject and so it defines subprojects itself. These subprojects can depend on projects in the outer class, as is done in Examples.

Using a Project definition other than sbt.DefaultProject is also useful for projects containing code (usually subclasses of ScalaProject), although this is not shown here. This way, all project configuration can be done in the parent project if desired.

Build Definitions in Subprojects

Alternatively, each subproject can configured with its project directory as is done with a single project. In this case, the form of project used would be:

def project(path: Path, deps: Project*): Project

There are performance and type-checking disadvantages to this, however.

Project definitions in subprojects are built after the parent project. When you call the project method to define a subproject, the subproject is built and loaded. The consequence is that building and loading of subprojects is done separately and serially. There is a substantial performance overhead to running multiple, small compilations and this translates to a performance disadvantage when recompiling multiple subprojects. The disadvantage is likely less noticeable when reloading a single subproject build definition.

Second, when all project definitions at top-level, there is more static type information. If builds are defined in multiple locations, you can only refer to other projects by the Project type.

class Top(info: ProjectInfo) extends ParentProject(info)
{
    // create a subproject, with some custom information
    lazy val subA = project("a", "A", info => new SubProject("something", info))

    // define a subproject type that provides custom information
    class SubProject(val customInfo: String, info: ProjectInfo)
      extends DefaultProject(info)

    // couldn't do this if SubProject was separate
    lazy val printCustom =
        task { println(subA.customInfo); None }
}

Finally, any code shared between builds must be defined as plugins because the classpaths of subprojects are not visible to each other.

Parallel Builds

sbt can execute tasks in parallel. Add

  override def parallelExecution = true

to your root project definition to enable this.

sbt and Scala Versions

All projects in a multi-project build must use the same version of sbt and Scala for building. If a different version of sbt or Scala is set on a subproject, it is ignored. In a future release, this may generate a warning or an error.

Dependency Management

A ParentProject by default does not publish an artifact other than an Ivy file or a pom.

Project dependencies declared using the project method are automatically added to a generated ivy.xml or pom for each project. That is, for:

import sbt._
class SimulatorProject(info: ProjectInfo) extends ParentProject(info)
{
   lazy val core = project("core", "Simulator Core")
   lazy val ui = project("ui", "Simulator User Interface", core)
}

the generated ivy.xml for Simulator User Interface will declare a dependency on Simulator Core in the compile configuration. To change the default behavior, override deliverProjectDependencies in the depending project (here, Simulator User Interface). deliverProjectDependencies returns the module IDs (the type of object created by "org.example" % "name" % "1.0") for each immediate project dependency. You can add or remove projects from this list or modify the configuration mappings.

To remove core from ui:

  class UIProject(info: ProjectInfo) extends DefaultProject(info) {
    override def deliverProjectDependencies =
     super.deliverProjectDependencies.toList - core.projectID
  }

To declare core as being a dependency of ui in the test configuration:

  class UIProject(info: ProjectInfo) extends DefaultProject(info) {
    override def deliverProjectDependencies =
      super.deliverProjectDependencies - core.projectID ++ Seq(core.projectID % "test->default")
  }

To declare no dependencies on other subprojects:

  override def deliverProjectDependencies = Nil

If you don't want to publish a subproject, you can do the following steps:

  • Override the relevant actions to do nothing:
  •   def doNothing = task { None }
      override def publishLocalAction = doNothing
      override def deliverLocalAction = doNothing
      override def publishAction = doNothing
      override def deliverAction = doNothing
  • For each project that depends on that subproject, remove it from deliverProjectDependencies.:
  •     override def deliverProjectDependencies =
         super.deliverProjectDependencies.toList - subproject.projectID