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:
- libraries that you depend on accidentally
- 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
- compile the project
- extract all class references from classfiles
- extract list of classfiles from each library on classpath
- cross-reference 2. and 3.
Actual compile deps
First idea: bytecode analysis + jar traversal
- compile the project
- extract all class references from classfiles
- extract list of classfiles from each library on classpath
- 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
- Make a normal sbt project
- build.sbt
- project/build.properties
- src/main/scala
- 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
... is really easy!
// 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
- Create a Bintray repo
- Publish your plugin to it (sbt release)
- 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,189