I made an sbt plugin and you should use it

  • what it does

  • how it works

  • how to write an sbt plugin

sbt-explicit-dependencies

Helps you find:

  1. libraries that you depend on accidentally
  2. dependencies that you don't need

Example

Undeclared dependencies

// in your build.sbt ...

libraryDependencies +=
  "org.typelevel" %% "cats-effect" % "1.0.0"
// somewhere in your src/main/scala ...

import cats.effect.IO
import cats.data.NonEmptyList

val foo: IO[Unit] = IO {
  println(NonEmptyList.of(1, 2, 3))
}

Example

Unused dependencies

// in your build.sbt ...

libraryDependencies +=
  "org.scalatest" %% "scalatest" % "3.0.5"

demo

How it works

declared

actual

undeclared compile dependencies

How it works

declared

actual

unused compile dependencies

Declared compile deps

  • libraryDependencies
  • filter out test deps, compiler plugins, etc.

Actual compile deps

How do we work out what libraries our project actually needs in order to compile?

Actual compile deps

First idea: bytecode analysis + jar traversal

import cats.data._

object Foo {
  val valid = Validated.valid("hello")
}
Constant pool:
  ...
  #23 = Utf8       cats/data/Validated$
  #24 = Class      #23 // cats/data/Validated$
  ...

Actual compile deps

First idea: bytecode analysis + jar traversal

  1. compile the project
  2. extract all class references from classfiles
  3. extract list of classfiles from each library on classpath
  4. cross-reference 2. and 3.

Actual compile deps

First idea: bytecode analysis + jar traversal

  1. compile the project
  2. extract all class references from classfiles
  3. extract list of classfiles from each library on classpath
  4. cross-reference 2. and 3.

TERRIBLE IDEA

Actual compile deps

sbt:example> inspect compile
[info] Task: xsbti.compile.CompileAnalysis
[info] Description:
[info]  Compiles sources.

compile analysis? 🤔

Actual compile deps

printActualDeps := {
  compile.in(Compile).value
    .asInstanceOf[sbt.internal.inc.Analysis]
    .relations
    .allLibraryDeps
    .foreach(println)
}
sbt:example> printActualDeps
/Users/chris/.ivy2/cache/org.typelevel/cats-effect_2.12/jars/cats-effect_2.12-0.10.1.jar
/Users/chris/.ivy2/cache/org.typelevel/cats-core_2.12/jars/cats-core_2.12-1.2.0.jar
/Users/chris/.ivy2/cache/com.chuusai/shapeless_2.12/bundles/shapeless_2.12-2.3.3.jar
/Users/chris/.sbt/boot/scala-2.12.6/lib/scala-library.jar
/Users/chris/.ivy2/cache/org.http4s/http4s-blaze-server_2.12/jars/http4s-blaze-server_2.12-0.18.16.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/rt.jar
/Users/chris/.ivy2/cache/com.google.guava/guava/bundles/guava-26.0-jre.jar

Writing an sbt plugin

  • Hello world
  • Testing
  • Publishing
  • Cross-building

sbt-hello-world

  1. Make a normal sbt project
    • build.sbt
    • project/build.properties
    • src/main/scala
  2. Enable the "SbtPlugin" sbt plugin
// no need to add anything to project/plugins.sbt 
enablePlugins(SbtPlugin)

scalaVersion := "2.12.8"
organization := "chris"

libraryDependencies ++= Seq(
  // whatever libraries you need ...
)
// src/main/scala/hello/HelloPlugin.scala

package hello

import sbt._

object HelloPlugin extends AutoPlugin {

  object autoImport {
    val sayHello = taskKey[Unit]("say hello")
  }

  // if you want your plugin to be enable automatically
  override def trigger = allRequirements

  override def projectSettings = Seq(
    autoImport.sayHello := {
      println("Hello world!")
    }
  )

}

Publish your plugin locally:

Add it to another sbt project:

$ sbt publishLocal
// project/plugins.sbt
addSbtPlugin("chris" % "sbt-hello-world" % "0.1.0-SNAPSHOT")

And try it out:

sbt:sbt-example> sayHello
Hello world! 

Testing your plugin

  • Unit tests
    • ScalaTest or whatever
    • Separate pure logic from sbt-specific stuff
  • Integration tests

Publishing your plugin

// project/plugins.sbt
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")
addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.9")
// build.sbt
organization := "com.foo.bar"
description := "my amazing plugin"
licenses += ("Apache-2.0", url("..."))
publishMavenStyle := false
bintrayRepository := "my-sbt-plugins"
bintrayOrganization in bintray := None

Publishing your plugin

  1. Create a Bintray repo
  2. Publish your plugin to it (sbt release)
  3. Get your plugin added to the community repo

Cross-building

sbt 0.13.x and 1.x are very similar,

but lots of classes got moved around

Cross-building

Type aliases in sbt version-specific package objects

package object explicitdeps {

  type Logger = sbt.util.Logger
  type ModuleID = sbt.librarymanagement.ModuleID
  type Binary = sbt.librarymanagement.Binary
  type Analysis = sbt.internal.inc.Analysis

  ...

}
package object explicitdeps {

  type Logger = sbt.Logger
  type ModuleID = sbt.ModuleID
  type Binary = sbt.CrossVersion.Binary
  type Analysis = sbt.inc.Analysis

  ...

}

src/main/scala-sbt-1.0/...

src/main/scala-sbt-0.13/...

everything else goes

in src/main/scala

Cross-building

Shared code must be valid in both 2.12.x and 2.10.x

package object explicitdeps {

  ...

  implicit class NodeSeqOps(nodeSeq: scala.xml.NodeSeq) {

    def \@(attributeName: String): String = 
      (nodeSeq \ ("@" + attributeName)).text

  }

}

src/main/scala-sbt-0.13/...

Add shims in version-specific package object:

Conclusion

  • use my plugin
  • and build your own!

LambdAle CFP opens soon!

sbt-explicit-dependencies

By Chris Birchall

sbt-explicit-dependencies

  • 2,162