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

时间:2012-07-10 10:20:16

标签: scala sbt

我已经离开衣柜了!我不明白SBT。在那里,我说,现在请帮助我。

所有道路都通往罗马,对于SBT来说也是如此:要开始使用SBT,有SBTSBT LauncherSBT-extras等,然后有不同的方法来包含和决定存储库。 有“最佳”方式吗?

我在问,因为有时候我有点失落。 SBT文档非常全面和完整,但我发现自己不知道何时使用build.sbtproject/build.propertiesproject/Build.scalaproject/plugins.sbt

然后变得有趣,有Scala-IDESBT - 将它们组合在一起的正确方法是什么?首先是鸡肉还是鸡蛋?

最重要的可能是,您如何找到要包含在项目中的正确的存储库和版本?我是否只是拔出一个小砍刀并开始攻击我的前进方向?我经常发现包括所有东西和厨房水槽的项目,然后我意识到 - 我不是唯一一个迷失方向的人。

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

我已经将SBT用于小型项目一年多了。我使用SBT然后使用SBT Extras(因为它使一些头痛神奇地消失了),但我不确定为什么我应该使用这一个或另一个。我只是因为不理解事情如何融合在一起而感到有些沮丧(SBT和知识库),并且认为如果可以用人的方式来解释它会拯救下一个如此困难的家伙。 / p>

4 个答案:

答案 0 :(得分:28)

  

最重要的可能是,您如何找到要包含在项目中的正确的存储库和版本?我是否只是拔出一把小砍刀并开始乱砍我前进的方向?我经常发现包含所有东西和厨房水槽的项目

对于基于Scala的依赖项,我会考虑作者的建议。例如:http://code.google.com/p/scalaz/#SBT表示使用:

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

https://github.com/typesafehub/sbteclipse/有关于添加位置的说明:

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

对于基于Java的依赖项,我使用http://mvnrepository.com/查看其中的内容,然后单击SBT选项卡。例如http://mvnrepository.com/artifact/net.sf.opencsv/opencsv/2.3表示使用:

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

然后拉出弯刀,开始向前砍伐。如果你很幸运,你最终不会使用依赖于某些相同罐子但具有不兼容版本的罐子。鉴于Java生态系统,您通常最终会包含所有内容和厨房接收器,并且需要花费一些精力来消除依赖关系或确保您不会错过所需的依赖关系。

  

作为一个简单的例子,现在,我正在开始一个全新的项目。我想使用SLICK和Scala的最新功能,这可能需要最新版本的SBT。什么是开始的理智点,为什么?

我认为理智的观点是build immunity to sbt gradually

确保您理解:

  1. 范围格式{<build-uri>}<project-id>/config:key(for task-key)
  2. 3种设置(SettingKeyTaskKeyInputKey) - 请阅读http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def
  3. 中名为“任务键”的部分

    始终打开这4页,以便您可以跳转并查找各种定义和示例:

    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
    5. 最大限度地利用showinspect 以及标签页完成,以熟悉设置的实际值,其依赖关系,定义和相关设置。我不相信您使用inspect发现的关系会记录在任何地方。如果有更好的方式我想知道它。

答案 1 :(得分:25)

我使用sbt的方式是:

  1. 使用sbt-extras - 只需获取shell脚本并将其添加到项目的根目录
  2. 创建一个project文件夹,其中包含MyProject.scala文件,用于设置sbt。我比build.sbt方法更喜欢这个 - 它是scala并且更灵活
  3. 创建project/plugins.sbt文件并为IDE添加适当的插件。 sbt-eclipse,sbt-idea或ensime-sbt-cmd,以便您可以为eclipse,intellij或ensime生成项目文件。
  4. 在项目的根目录中启动sbt并生成IDE的项目文件
  5. 利润
  6. 我不打算检查IDE项目文件,因为它们是由sbt生成的,但可能有理由这样做。

    您可以看到像here这样的示例设置。

答案 2 :(得分:1)

使用Typesafe Activator,一种调用sbt的奇特方式,它带有项目模板和种子: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)

答案 3 :(得分:0)

安装

从技术上讲是

brew install sbt或类似的安装sbt

当您从终端执行sbt时,它实际上会运行sbt启动器bash脚本。就我个人而言,我不必担心这种三位一体,而只需将sbt当作一件事情就可以使用。

配置

要为特定项目配置sbt,请将.sbtopts文件保存在项目的根目录下。要在系统范围内配置sbt,请修改/usr/local/etc/sbtopts。执行sbt -help应该会告诉您确切的位置。例如,要一次性为sbt提供更多内存,请执行sbt -mem 4096,或将-mem 4096保存在.sbtoptssbtopts中,以使内存增加永久生效。

项目结构

sbt new scala/scala-seed.g8创建一个最小的Hello World sbt项目结构

.
├── 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

常用命令

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

无数的贝壳

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

内部定义是一个适当的Scala项目

这是关键的惯用sbt概念之一。我将尝试用一个问题来解释。假设您要定义一个sbt任务,该任务将使用scalaj-http执行HTTP请求。凭直觉,我们可以在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
  ...
}

但是,这将出错,提示缺少import scalaj.http._。当我们在上方将scalaj-http添加到libraryDependencies时,这怎么可能?此外,为什么当我们将依赖项添加到project/build.sbt时为何起作用?

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

答案是fooTask实际上是与主项目分开的 Scala项目的一部分。可以在project/目录下找到此不同的Scala项目,该目录具有自己的target/目录,其已编译类位于其中。实际上,在project/target/config-classes下应该有一个反编译为

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

我们看到fooTask只是名为$9c2192aea3f1db3c251d的常规Scala对象的成员。显然,scalaj-http应该是定义$9c2192aea3f1db3c251d的项目的依赖,而不是适当项目的依赖。因此,它需要在project/build.sbt中而不是在build.sbt中声明,因为project是构建定义Scala项目所在的地方。

要指出构建定义只是另一个Scala项目,请执行sbt consoleProject。这将使用类定义路径上的构建定义项目加载Scala REPL。您应该会看到

import $9c2192aea3f1db3c251d

因此,现在我们可以通过使用Scala属性而不是build.sbt DSL来调用它来直接与构建定义项目进行交互。例如,以下执行fooTask

$9c2192aea3f1db3c251d.fooTask.eval
根项目下的

build.sbt是一种特殊的DSL,可帮助定义project/下的构建定义Scala项目。

构建定义Scala项目,可以在project/project/下拥有自己的构建定义Scala项目,依此类推。我们说sbt is recursive

sbt默认是并行的

sbt根据任务构建DAG。这使它可以分析任务之间的依赖关系,并并行执行它们,甚至执行重复数据删除。 build.sbt DSL的设计考虑了这一点,这可能会导致最初令人惊讶的语义。您认为以下代码片段的执行顺序是什么?

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
}

直觉上,这里的流程可能是先打印hello,然后执行a,然后执行b任务。但这实际上意味着要在并行中执行ab,而在之前先执行{strong>

println("hello")

或者因为不能保证a b hello a的顺序

b

也许矛盾的是,在sbt中,并行比串行更容易。如果您需要序列订购,则必须使用b a hello Def.sequential之类的特殊功能来模拟理解

Def.taskDyn

类似于

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

我们看到的组件之间没有依赖性,而

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

相似
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

我们看到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 } 的位置取决于并且必须等待suma

换句话说

  • 对于适用性语义,请使用b
  • 对于 monadic 语义,使用.valuesequential

由于taskDyn的依赖关系建立性质,请考虑another在语义上令人困惑的代码段,其中而不是

value

我们必须写

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

请注意,语法val x = settingKey[String]("") x := version.value 与DAG中的关系有关,并不表示

“立即给我值”

相反,它意味着类似

“我的呼叫者首先取决于我,一旦我知道整个DAG如何组合在一起,我就能为我的呼叫者提供所请求的值”

因此,现在也许更清楚为什么无法为.value分配值了;在建立关系阶段还没有价值。

我们可以清楚地看到x中的Scala属性和DSL语言之间的语义差异。这是一些对我有用的经验法则

  • DAG由build.sbt类型的表达式组成
  • 在大多数情况下,我们仅使用Setting[T]语法,而sbt将负责在.value之间建立关系
  • 有时我们必须手动调整DAG的一部分,为此我们使用Setting[T]Def.sequential
  • 一旦解决了这些排序/关系语法上的奇怪问题,我们就可以依靠常用的Scala语义来构建任务的其余业务逻辑。

命令与任务

命令是脱离DAG的一种懒惰方式。使用命令很容易根据需要更改构建状态并序列化任务。代价是我们失去了DAG提供的任务的并行化和重复数据删除功能,因此应该优先选择任务。您可以将命令视为会话的一种永久记录,可以在Def.taskDyn内部进行。例如,给定

sbt shell

考虑以下会话的输出

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

尤其不是我们如何用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 改变构建状态。命令使我们能够永久记录上述会话,例如

set x := 41

我们还可以使用commands += Command.command("cmd") { state => "x" :: "show f" :: "set x := 41" :: "show f" :: state } Project.extract来使命令类型安全

runTask

范围

当我们尝试回答以下类型的问题时,作用域就会发挥作用

  • 如何一次定义任务并将其提供给多项目构建中的所有子项目?
  • 如何避免对主类路径具有测试依赖性?

sbt具有一个多轴作用域空间,例如,可以使用slash syntax进行导航,

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
}

就个人而言,我很少发现自己需要担心范围。有时我只想编译测试源

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

或者从特定子项目执行特定任务,而无需首先使用Test/compile 导航到该项目

project subprojB

我认为以下经验法则有助于避免范围界定并发症

  • 没有多个subprojB/Test/compile 文件,而根项目下只有一个主文件可以控制所有其他子项目
  • 通过自动插件共享任务
  • 将普通设置分解到普通的Scala build.sbt中,并将其显式添加到每个子项目中

多项目构建

代替每个子项目的多个build.sbt文件

val

拥有一个主人. ├── 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 来统治他们

build.sbt

在多项目构建中,通常使用factoring out common settings

在val中定义一系列常用设置并将其添加到每个 项目。更少的概念需要学习。

例如

.
├── 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

项目导航

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)

插件

记住构建定义是一个正确的Scala项目,它位于projects // list all projects project multi1 // change to particular project 下。在这里我们通过创建project/文件来定义插件

.scala

. // 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 下是最小的auto plugin

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
    }   
  )
}

应该有效地为所有子项目启用该插件,而不必在override def requires = plugins.JvmPlugin 中显式调用enablePlugin

IntelliJ和sbt

请启用以下设置(应由default启用)

build.sbt

use sbt shell

关键参考