Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Intertwined dependencies between sbt plugin and projects within multi-project build that uses the plugin itself

Tags:

plugins

scala

sbt

I'm developing a library that includes an sbt plugin. Naturally, I'm using sbt to build this (multi-project) library. My (simplified) project looks as follows:

myProject/                        # Top level of library
  -> models                       # One project in the multi-project sbt build.
      -> src/main/scala/...       # Defines common models for both sbt-plugin and framework
  -> sbt-plugin                   # The sbt plugin build
      -> src/main/scala/...
  -> framework                    # The framework. Ideally, the sbt plugin is run as part of 
      -> src/main/scala/...       # compiling this directory.
  -> project/                     # Multi-project build configuration

Is there a way to have the sbt-plugin defined in myProject/sbt-plugin be hooked into the build for myProject/framework all in a unified build?

Note: similar (but simpler) question: How to develop sbt plugin in multi-project build with projects that use it?

like image 938
saeta Avatar asked May 24 '16 21:05

saeta


1 Answers

Is there a way to have the sbt-plugin defined in myProject/sbt-plugin be hooked into the build for myProject/framework all in a unified build?

I have a working example on Github eed3si9n/plugin-bootstrap. It's not super pretty, but it kind of works. We can take advantage of the fact that sbt is recursive.

The project directory is another build inside your build, which knows how to build your build. To distinguish the builds, we sometimes use the term proper build to refer to your build, and meta-build to refer to the build in project. The projects inside the metabuild can do anything any other project can do. Your build definition is an sbt project.

By extension, we can think of the sbt plugins to be library- or inter-project dependencies to the root project of your metabuild.

meta build definition (project/plugins.sbt)

In this example, think of the metabuild as a parallel universe or shadow world that has parallel multi-build structure as the proper build (root, model, sbt-plugin).

To reuse the source code from model and sbt-plugin subprojects in the proper build, we can re-create the multi-project build in the metabuild. This way we don't need to get into the circular dependency.

addSbtPlugin("com.eed3si9n" % "sbt-doge" % "0.1.5")

lazy val metaroot = (project in file(".")).
  dependsOn(metaSbtSomething)

lazy val metaModel = (project in file("model")).
  settings(
    sbtPlugin := true,
    scalaVersion := "2.10.6",
    unmanagedSourceDirectories in Compile :=
      mirrorScalaSource((baseDirectory in ThisBuild).value.getParentFile / "model")
  )

lazy val metaSbtSomething = (project in file("sbt-plugin")).
  dependsOn(metaModel).
  settings(
    sbtPlugin := true,
    scalaVersion := "2.10.6",
    unmanagedSourceDirectories in Compile :=
      mirrorScalaSource((baseDirectory in ThisBuild).value.getParentFile / "sbt-plugin")
  )

def mirrorScalaSource(baseDirectory: File): Seq[File] = {
  val scalaSourceDir = baseDirectory / "src" / "main" / "scala"
  if (scalaSourceDir.exists) scalaSourceDir :: Nil
  else sys.error(s"Missing source directory: $scalaSourceDir")
}

When sbt loads up, it will build metaModel and metaSbtSomething first, and use metaSbtSomething as a plugin to your proper build.

If you have any other plugins you need you can just add it to project/plugins.sbt normally as I've added sbt-doge.

proper build (build.sbt)

The proper build looks like a normal multi-project build. As you can see framework subproject uses SomethingPlugin. Important thing is that they share the source code, but the target directory is completely separated, so there are no interference once the proper build is loaded, and you are changing code around.

import Dependencies._

lazy val root = (project in file(".")).
  aggregate(model, framework, sbtSomething).
  settings(inThisBuild(List(
      scalaVersion := scala210,
      organization := "com.example"
    )),
    name := "Something Root"
  )

// Defines common models for both sbt-plugin and framework
lazy val model = (project in file("model")).
  settings(
    name := "Something Model",
    crossScalaVersions := Seq(scala211, scala210)
  )

// The framework. Ideally, the sbt plugin is run as part of building this.
lazy val framework = (project in file("framework")).
  enablePlugins(SomethingPlugin).
  dependsOn(model).
  settings(
    name := "Something Framework",
    crossScalaVersions := Seq(scala211, scala210),
    // using sbt-something
    somethingX := "a"
  )

lazy val sbtSomething = (project in file("sbt-plugin")).
  dependsOn(model).
  settings(
    sbtPlugin := true,
    name := "sbt-something",
    crossScalaVersions := Seq(scala210)
  )

demo

In the SomethingPlugin example, I'm defining something task that uses foo.Model.x.

package foo

import sbt._

object SomethingPlugin extends AutoPlugin {
  def requries = sbt.plugins.JvmPlugin
  object autoImport {
    lazy val something = taskKey[Unit]("")
    lazy val somethingX = settingKey[String]("")
  }
  import autoImport._
  override def projectSettings = Seq(
    something := { println(s"something! ${Model.x}") }
  )
}

Here's how we can invoke something task from the build:

Something Root> framework/something
something! 1
[success] Total time: 0 s, completed May 29, 2016 3:01:07 PM

1 comes from foo.Model.x, so this demonstrates that we are using the sbt-something plugin in framework subproject, and that the plugin is using metaModel.

like image 72
Eugene Yokota Avatar answered Nov 15 '22 04:11

Eugene Yokota