null null - 3 months ago 57
Scala Question

SBT - Multi project merge strategy and build sbt structure when using assembly

I have a project that consists of multiple smaller projects, some with dependencies upon each other, for example, there is a utility project that depends upon commons project.
Other projects may or may not depend upon utilities or commons or neither of them.

In the build.sbt I have the assembly merge strategy at the end of the file, along with the tests in assembly being {}.

My question is: is this correct, should each project have it's own merge strategy and if so, will the others that depend on it inherit this strategy from them? Having the merge strategy contained within all of the project definitions seems clunky and would mean a lot of repeated code.

This question applied to the tests as well, shouls each project have the line for whether tests should be carried out or not, or will that also be inherited?

Thanks in advance. If anyone knows of a link to a sensible (relatively complex) example that'd also be great.

Answer

In my day job I currently work on a large multi-project. Unfortunately its closed source so I can't share specifics, but I can share some guidance.

  1. Create a rootSettings used only by the root/container project, since it usually isn't part of an assembly or publish step. It would contain something like:

    lazy val rootSettings := Seq(
      publishArtifact := false,
      publishArtifact in Test := false
    )
    
  2. Create a commonSettings shared by all the subprojects. Place the base/shared assembly settings here:

    lazy val commonSettings := Seq(
    
      // We use a common directory for all of the artifacts
      assemblyOutputPath in assembly := baseDirectory.value /
        "assembly" / (name.value + "-" + version.value + ".jar"),
    
      // This is really a one-time, global setting if all projects
      // use the same folder, but should be here if modified for
      // per-project paths.
      cleanFiles <+= baseDirectory { base => base / "assembly" },
    
      test in assembly := {},
    
      assemblyMergeStrategy in assembly := {
        case "BUILD" => MergeStrategy.discard
        case "logback.xml" => MergeStrategy.first
        case other: Any => MergeStrategy.defaultMergeStrategy(other)
      },
    
      assemblyExcludedJars in assembly := {
        val cp = (fullClasspath in assembly).value
        cp filter { _.data.getName.matches(".*finatra-scalap-compiler-deps.*") }
      }
    )
    
  3. Each subproject uses commonSettings, and applies project-specific overrides:

    lazy val fubar = project.in(file("fubar-folder-name"))
      .settings(commonSettings: _*)
      .settings(
        // Project-specific settings here.
        assemblyMergeStrategy in assembly := {
          // The fubar-specific strategy
          case "fubar.txt" => MergeStrategy.discard
          case other: Any =>
            // Apply inherited "common" strategy
            val oldStrategy = (assemblyMergeStrategy in assembly).value
            oldStrategy(other)
        }
      )
      .dependsOn(
        yourCoreProject,
        // ...
      )
    
  4. And BTW, if using IntelliJ. don't name your root project variable root, as this is what appears as the project name in the recent projects menu.

    lazy val myProjectRoot = project.in(file("."))
      .settings(rootSettings: _*)
      .settings(
        // ...
      )
      .dependsOn(
        // ...
      )
      .aggregate(
        fubar,
        // ...
      )