有人能解释一下 SBT 的正确使用方法吗?

我要出柜了! 我不明白 SBT。好了,我说了,现在请帮帮我。

所有的路都通向罗马,SBT 也是如此: 从 SBT开始,有 SBTSBT LauncherSBT-extras等等,然后有不同的方法来包含和决定存储库。有没有“最好”的方法?

我这么问是因为有时候我会迷路。SBT 文档非常全面和完整,但我发现自己不知道什么时候使用 build.sbtproject/build.propertiesproject/Build.scalaproject/plugins.sbt

然后它变得有趣,有 Scala-IDESBT-把它们一起使用的正确方法是什么?什么先来,鸡还是蛋?

最重要的可能是,如何找到适合包含在项目中的存储库和版本?我是不是应该拿出一把大砍刀,开始向前砍呢?我经常发现项目包括一切和厨房水槽,然后我意识到-我不是唯一一个有点迷失的人。

举个简单的例子,现在,我正在开始一个全新的项目。我想使用 SLICKScala的最新功能,这可能需要最新版本的 SBT。我应该在哪个文件中定义它,它看起来应该是什么样子?我知道我可以得到这个工作,但我真的想要一个专家的意见,一切应该去(为什么应该去那里会有奖金)。

到目前为止,我已经在小型项目中使用 SBT一年多了。我使用了 SBTSBT Extras(因为它让一些头痛神奇地消失了) ,但我不确定为什么我应该使用这两种方法中的一种。我只是有点沮丧,因为我不理解事物是如何组合在一起的(SBT和存储库) ,并且认为如果能够用人类的术语来解释这个问题,它将会为下一个来到这里的家伙节省很多困难。

7834 次浏览

The way I use sbt is:

  1. Use sbt-extras - just get the shell script and add it to the root of you project
  2. Create a project folder with a MyProject.scala file for setting up sbt. I much prefer this over the build.sbt approach - it's scala and is more flexible
  3. Create a project/plugins.sbt file and add the appropriate plugin for your IDE. Either sbt-eclipse, sbt-idea or ensime-sbt-cmd so that you can generate project files for eclipse, intellij or ensime.
  4. Launch sbt in the root of your project and generate the project files for your IDE
  5. Profit

I don't bother checking in the IDE project files since they are generated by sbt, but there may be reasons you want to do that.

You can see an example set up like this here.

Most importantly is probably, how do you find the right repositories and versions to include in your project? Do I just pull out a machette and start hacking my way forward? I quite often find projects that include everything and the kitchen sink

For Scala-based dependencies, I would go with what the authors recommend. For instance: http://code.google.com/p/scalaz/#SBT indicates to use:

libraryDependencies += "org.scalaz" %% "scalaz-core" % "6.0.4"

Or https://github.com/typesafehub/sbteclipse/ has instructions on where to add:

addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0-RC1")

For Java-based dependencies, I use http://mvnrepository.com/ to see what's out there, then click on the SBT tab. For instance http://mvnrepository.com/artifact/net.sf.opencsv/opencsv/2.3 indicates to use:

libraryDependencies += "net.sf.opencsv" % "opencsv" % "2.3"

Then pull out the machette and start hacking your way forward. If you are lucky you don't end up using jars that depends on some of the same jars but with incompatible versions. Given the Java ecosystem, you often end up including everything and the kitchen sink and it takes some effort to eliminate dependencies or ensure you are not missing required dependencies.

As a simple example, right now, I'm starting a brand new project. I want to use the latest features of SLICK and Scala and this will probably require the latest version of SBT. What is the sane point to get started, and why?

I think the sane point is to build immunity to sbt gradually.

Make sure you understand:

  1. scopes format {<build-uri>}<project-id>/config:key(for task-key)
  2. the 3 flavors of settings (SettingKey, TaskKey, InputKey) - read the section called "Task Keys" in http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def

Keep those 4 pages open at all times so that you can jump and look up various definitions and examples:

  1. http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def
  2. http://www.scala-sbt.org/release/docs/Detailed-Topics/index
  3. http://harrah.github.com/xsbt/latest/sxr/Keys.scala.html
  4. http://harrah.github.com/xsbt/latest/sxr/Defaults.scala.html

Make maximum use of show and inspect and the tab completion to get familiar with actual values of settings, their dependencies, definitions and related settings. I don't believe the relationships you'll discover using inspect are documented anywhere. If there is a better way I want to know about it.

Use Typesafe Activator, a fancy way of calling sbt, which comes with project templates and seeds : https://typesafe.com/activator

Activator new


Fetching the latest list of templates...


Browse the list of templates: http://typesafe.com/activator/templates
Choose from these featured templates or enter a template name:
1) minimal-java
2) minimal-scala
3) play-java
4) play-scala
(hit tab to see a list of all templates)

Installation

brew install sbt or similar installs sbt which technically speaking consists of

When you execute sbt from terminal it actually runs the sbt launcher bash script. Personally, I never had to worry about this trinity, and just use sbt as if it was a single thing.

Configuration

To configure sbt for a particular project save .sbtopts file at the root of the project. To configure sbt system-wide modify /usr/local/etc/sbtopts. Executing sbt -help should tell you the exact location. For example, to give sbt more memory as one-off execute sbt -mem 4096, or save -mem 4096 in .sbtopts or sbtopts for memory increase to take effect permanently.

 Project structure

sbt new scala/scala-seed.g8 creates a minimal Hello World sbt project structure

.
├── README.md  // most important part of any software project
├── build.sbt  // build definition of the project
├── project    // build definition of the build (sbt is recursive - explained below)
├── src        // test and main source code
└── target     // compiled classes, deployment package

Frequent commands

test                                                // run all test
testOnly                                            // run only failed tests
testOnly -- -z "The Hello object should say hello"  // run one specific test
run                                                 // run default main
runMain example.Hello                               // run specific main
clean                                               // delete target/
package                                             // package skinny jar
assembly                                            // package fat jar
publishLocal                                        // library to local cache
release                                             // library to remote repository
reload                                              // after each change to build definition

Myriad of shells

scala              // Scala REPL that executes Scala language (nothing to do with sbt)
sbt                // sbt REPL that executes special sbt shell language (not Scala REPL)
sbt console        // Scala REPL with dependencies loaded as per build.sbt
sbt consoleProject // Scala REPL with project definition and sbt loaded for exploration with plain Scala langauage

Build definition is a proper Scala project

This is one of key idiomatic sbt concepts. I will try to explain with a question. Say you want to define a sbt task that will execute an HTTP request with scalaj-http. Intuitively we might try the following inside build.sbt

libraryDependencies +=  "org.scalaj" %% "scalaj-http" % "2.4.2"


val fooTask = taskKey[Unit]("Fetch meaning of life")
fooTask := {
import scalaj.http._ // error: cannot resolve symbol
val response = Http("http://example.com").asString
...
}

However this will error saying missing import scalaj.http._. How is this possible when we, right above, added scalaj-http to libraryDependencies? Furthermore, why does it work when, instead, we add the dependency to project/build.sbt?

// project/build.sbt
libraryDependencies +=  "org.scalaj" %% "scalaj-http" % "2.4.2"

The answer is that fooTask is actually part of a separate Scala project from your main project. This different Scala project can be found under project/ directory which has its own target/ directory where its compiled classes reside. In fact, under project/target/config-classes there should be a class that decompiles to something like

object $9c2192aea3f1db3c251d extends scala.AnyRef {
lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
lazy val root : sbt.Project = { /* compiled code */ }
}

We see that fooTask is simply a member of a regular Scala object named $9c2192aea3f1db3c251d. Clearly scalaj-http should be a dependency of the project defining $9c2192aea3f1db3c251d and not the dependency of the proper project. Hence it needs to be declared in project/build.sbt instead of build.sbt, because project is where the build definition Scala project resides.

To drive the point that build definition is just another Scala project, execute sbt consoleProject. This will load Scala REPL with the build definition project on the classpath. You should see an import along the lines of

import $9c2192aea3f1db3c251d

So now we can interact directly with build definition project by calling it with Scala proper instead of build.sbt DSL. For example, the following executes fooTask

$9c2192aea3f1db3c251d.fooTask.eval

build.sbt under root project is a spcial DSL that helps define the build definition Scala project under project/.

And build definition Scala project, can have its own build definition Scala project under project/project/ and so on. We say sbt is recursive.

sbt is parallel by-default

sbt builds DAG out of tasks. This allows it to analyse dependencies between tasks and execute them in parallel and even perform deduplication. build.sbt DSL is designed with this in mind, which might lead to initially surprising semantics. What do you think the order of execution is in the following snippet?

def a = Def.task { println("a") }
def b = Def.task { println("b") }
lazy val c = taskKey[Unit]("sbt is parallel by-default")
c := {
println("hello")
a.value
b.value
}

Intuitively one might think flow here is to first print hello then execute a, and then b task. However this actually means execute a and b in parallel, and before println("hello") so

a
b
hello

or because order of a and b is not guaranteed

b
a
hello

Perhaps paradoxically, in sbt it is easier to do parallel than serial. If you need serial ordering you will have to use special things like Def.sequential or Def.taskDyn to emulate for-comprehension.

def a = Def.task { println("a") }
def b = Def.task { println("b") }
lazy val c = taskKey[Unit]("")
c := Def.sequential(
Def.task(println("hello")),
a,
b
).value

is similar to

for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()

where we see there is no dependencies between components, whilst

def a = Def.task { println("a"); 1 }
def b(v: Int) = Def.task { println("b"); v + 40 }
def sum(x: Int, y: Int) = Def.task[Int] { println("sum"); x + y }
lazy val c = taskKey[Int]("")
c := (Def.taskDyn {
val x = a.value
val y = Def.task(b(x).value)
Def.taskDyn(sum(x, y.value))
}).value

is similar to

def a = Future { println("a"); 1 }
def b(v: Int) = Future { println("b"); v + 40 }
def sum(x: Int, y: Int) = Future { x + y }


for {
x <- a
y <- b(x)
c <- sum(x, y)
} yield { c }

where we see sum depends on and has to wait for a and b.

In other words

  • for applicative semantics, use .value
  • for monadic semantics use sequential or taskDyn

Consider another semantically confusing snippet as a result of the dependency building nature of value, where instead of

`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^

we have to write

val x = settingKey[String]("")
x := version.value

Note the syntax .value is about relationships in the DAG and does not mean

"give me the value right now"

instead it means something like

"my caller depends on me first, and once I know how the whole DAG fits together, I will be able to provide my caller with the requested value"

So now it might be a bit clearer why x cannot be assigned a value yet; there is no value yet available in the relationship building stage.

We can clearly see a difference in semantics between Scala proper and the DSL language in build.sbt. Here are few rules of thumbs that work for me

  • DAG is made out of expressions of type Setting[T]
  • In most cases we simply use .value syntax and sbt will take care of establishing relationship between Setting[T]
  • Occasionally we have to manually tweak a part of DAG and for that we use Def.sequential or Def.taskDyn
  • Once these ordering/relationship syntatic oddities are taken care of, we can rely on the usual Scala semantics for building the rest of the business logic of tasks.

 Commands vs Tasks

Commands are a lazy way out of the DAG. Using commands it is easy to mutate the build state and serialise tasks as you wish. The cost is we loose parallelisation and deduplication of tasks provided by DAG, which way tasks should be the prefered choice. You can think of commands as a kind of permanent recording of a session one might do inside sbt shell. For example, given

vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value

consider the output of the following session

sbt:root> x
[info] 13
sbt:root> show f
[info] 14
sbt:root> set x := 41
[info] Defining x
[info] The new value will be used by f
[info] Reapplying settings...
sbt:root> show f
[info] 42


In particular not how we mutate the build state with set x := 41. Commands enables us to make a permanent recording of the above session, for example

commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}

We can also make the command type-safe using Project.extract and runTask

commands += Command.command("cmd") { state =>
val log = state.log
import Project._
log.info(x.value.toString)
val (_, resultBefore) = extract(state).runTask(f, state)
log.info(resultBefore.toString)
val mutatedState = extract(state).appendWithSession(Seq(x := 41), state)
val (_, resultAfter) = extract(mutatedState).runTask(f, mutatedState)
log.info(resultAfter.toString)
mutatedState
}

Scopes

Scopes come into play when we try to answer the following kinds of questions

  • How to define task once and make it available to all the sub-projects in multi-project build?
  • How to avoid having test dependencies on the main classpath?

sbt has a multi-axis scoping space which can be navigated using slash syntax, for example,

show  root   /  Compile         /  compile   /   scalacOptions
|        |                  |             |
project    configuration      task          key


Personally, I rarely find myself having to worry about scope. Sometimes I want to compile just test sources

Test/compile

or perhaps execute a particular task from a particular subproject without first having to navigate to that project with project subprojB

subprojB/Test/compile

I think the following rules of thumb help avoid scoping complications

  • do not have multiple build.sbt files but only a single master one under root project that controls all other sub-projects
  • share tasks via auto plugins
  • factor out common settings into plain Scala val and explicitly add it to each sub-project

Multi-project build

Iinstead of multiple build.sbt files for each subproject

.
├── README.md
├── build.sbt                  // OK
├── multi1
│   ├── build.sbt              // NOK
│   ├── src
│   └── target
├── multi2
│   ├── build.sbt              // NOK
│   ├── src
│   └── target
├── project                    // this is the meta-project
│   ├── FooPlugin.scala        // custom auto plugin
│   ├── build.properties       // version of sbt and hence Scala for meta-project
│   ├── build.sbt              // OK - this is actually for meta-project
│   ├── plugins.sbt            // OK
│   ├── project
│   └── target
└── target

Have a single master build.sbt to rule them all

.
├── README.md
├── build.sbt                  // single build.sbt to rule theme all
├── common
│   ├── src
│   └── target
├── multi1
│   ├── src
│   └── target
├── multi2
│   ├── src
│   └── target
├── project
│   ├── FooPlugin.scala
│   ├── build.properties
│   ├── build.sbt
│   ├── plugins.sbt
│   ├── project
│   └── target
└── target

There is a common practice of factoring out common settings in multi-project builds

define a sequence of common settings in a val and add them to each project. Less concepts to learn that way.

for example

lazy val commonSettings = Seq(
scalacOptions := Seq(
"-Xfatal-warnings",
...
),
publishArtifact := true,
...
)


lazy val root = project
.in(file("."))
.settings(settings)
.aggregate(
multi1,
multi2
)
lazy val multi1 = (project in file("multi1")).settings(commonSettings)
lazy val multi2 = (project in file("multi2")).settings(commonSettings)

Projects navigation

projects         // list all projects
project multi1   // change to particular project

Plugins

Remember build definition is a proper Scala project that resides under project/. This is where we define a plugin by creating .scala files

.                          // directory of the (main) proper project
├── project
│   ├── FooPlugin.scala    // auto plugin
│   ├── build.properties   // version of sbt library and indirectly Scala used for the plugin
│   ├── build.sbt          // build definition of the plugin
│   ├── plugins.sbt        // these are plugins for the main (proper) project, not the meta project
│   ├── project            // the turtle supporting this turtle
│   └── target             // compiled binaries of the plugin

Here is a minimal auto plugin under project/FooPlugin.scala

object FooPlugin extends AutoPlugin {
object autoImport {
val barTask = taskKey[Unit]("")
}


import autoImport._


override def requires = plugins.JvmPlugin  // avoids having to call enablePlugin explicitly
override def trigger = allRequirements


override lazy val projectSettings = Seq(
scalacOptions ++= Seq("-Xfatal-warnings"),
barTask := { println("hello task") },
commands += Command.command("cmd") { state =>
"""eval println("hello command")""" :: state
}
)
}

The override

override def requires = plugins.JvmPlugin

should effectively enable the plugin for all sub-projects without having to call explicitly enablePlugin in build.sbt.

IntelliJ and sbt

Please enable the following setting (which should really be enabled by default)

use sbt shell

under

Preferences | Build, Execution, Deployment | sbt | sbt projects

Key references