4
votes

I have an SBT Scala multi-project with the following structure:

multiprojectRoot        
    project/SharedProjectBuildCode.scala
    project1
        src/sourceFiles
        project1-build.sbt
    project2
        src/sourceFiles
        project2-build.sbt
    projectN
        src/sourceFiles
        projectN-build.sbt
  • multiprojectRoot/project/SharedProjectBuildCode.scala: contains multi-project definitions that use dependsOn to create dependencies on local projects. For example:

    lazy val project2 = Project(
        ...
    ).dependsOn(project1)
    
  • multiprojectRoot/project2/project2-build.sbt: Contains the settings and dependencies for a given project. For example:

    name := "project2"
    
    libraryDependencies ++= Seq(          
              ...
              "my.company" % "project1" % "1.0"
    )
    

First dependency to project1 is declared with dependsOn on SharedProjectBuildCode.scala file and the second is created on standalone project2-build.sbt build definition file.

So, project2 definition contains either:

  • an ambiguous dependency to project1 or
  • a double dependency to project1

We want to keep this project structure, because is the best for our current workflow:

  • Independent .sbt files serve standalone deployment purposes for each project on our continuous delivery server.
  • Multi-project .scala file with dependsOn is used to facilitate development, allowing us to avoid things such as continuous publishLocal.

We need to have control for such dependency ambiguities someway. Can you help me?

2

2 Answers

1
votes

I think you should have in SharedProjectBuildCode.scala

lazy val root = Project(id = "Main-Project",
    base = file(".")) aggregate(project1, project2,..)

lazy val project2 = Project(id = "project2",
    base = file("project1")).dependsOn(project1)

...

And don't need to add as dependency in build.sbt anymore.

1
votes

I was able to control which dependency set loaded on each use case by using the rules of build files loading provided by SBT.

When you load SBT from a given root directory, it looks for *.sbt files on the root directory and also for *.scala on the root/project directory. If you have a multi-project build, then it also reads the definitions of .sbt files that are encountered on child projects, but it will not use project/.scala files on child projects:

.sbt build definition

Multi-project builds

So, I changed my multi-project build the following way:

multiprojectRoot        
    project/SharedProjectBuildCode.scala
    project1
        src/sourceFiles
        project/DeploymentOnlyCode.scala
        project1-build.sbt
    project2
        src/sourceFiles
        project/DeploymentOnlyCode.scala
        project2-build.sbt
    projectN
        src/sourceFiles
        project/DeploymentOnlyCode.scala
        projectN-build.sbt

This way, depending on the use case I run SBT from the multi-project root or a project internal directory:

  • Development: SBT is run from multiprojectRoot directory. It takes the advantages of having a multi-project build (such as using dependsOn and avoiding publishLocal).
  • Production: SBT is run from within a concrete project directory, such as multiprojectRoot/project2. It allows the project to be built as stand-alone, having all dependencies as explicit external (useful for declaring a sequence of dependencies on production, continuous integration server).

Now, a project has 3 instances of code that aggregates their attributes for a final build:

  1. multiprojectRoot/project/SharedProjectBuildCode.scala: Contains local dependencies and other code relevant for multi-project build.
  2. multiprojectRoot/project1/project1-build.sbt: Contains project build attributes, common for multi-project and standalone build of a project, such as name or dependencies that are always external. The same should be done for other multi-project projects of the same level, to be explicitly treated as external dependency artifacts.
  3. multiprojectRoot/project1/project/DeploymentOnlyCode.scala: Contains build attributes that will only be taken into consideration for stand-alone build. The same can be done on other sub-projects, if these require to define deployment specific attributes.

This also gives maximum control on how a project is built, whether is a releasable artifact or not, and handle source code relevant only for a given project, as a complete and independent piece.