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

Posted

技术标签:

【中文标题】有人可以解释使用 SBT 的正确方法吗?【英文标题】:Can someone explain the right way to use SBT? 【发布时间】:2012-07-09 19:22:22 【问题描述】:

我要在这件事上出柜了!我不明白 SBT。在那里,我说过了,现在请帮助我。

条条大路通罗马,SBT也一样:入手SBTSBTSBT LauncherSBT-extras等,然后有不同的方式来包含和决定在存储库上。 有没有“最好”的方法?

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

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

最重要的可能是,您如何找到合适的存储库和版本以包含在您的项目中?我是否只是拔出一把砍刀并开始破解我的前进道路?我经常找到包括所有东西和厨房水槽的项目,然后我意识到 - 我不是唯一一个有点迷路的人。

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

一年多来,我一直在使用SBT 进行小型项目。我使用了SBT,然后使用了SBT Extras(因为它让一些头痛神奇地消失了),但我不确定为什么我应该使用其中一个。我只是因为不了解事物是如何组合在一起而感到有些沮丧(SBT 和存储库),并且认为如果可以用人类的术语来解释的话,它将为下一个以这种方式出现的人省去很多困难。

【问题讨论】:

“有 Scala-IDE 和 SBT”到底是什么意思?您使用 sbt 定义您的项目,并且 sbt 可以生成一个 ide (eclipse oder intellij) 项目。所以 SBT 是第一位的…… @Jan 我提到这是因为 Scala-IDE 使用 SBT 作为构建管理器。请参阅assembla.com/spaces/scala-ide/wiki/SBT-based_build_manager 并在他们提到“无需定义您的 SBT 项目文件”的帖子中往下看。我觉得很困惑。 好的。因为我通常使用 intellij(或 sublime)来编辑 scala,所以我不知道。我猜构建器会生成自己的 sbt 配置? @JacobusR Scala IDE 使用 SBT 构建项目源代码这一事实是一个实现细节,用户不需要担心这一点。确实有0个影响。在 Eclipse 之外,用户可以使用 SBT、Maven、Ant 等构建项目,这对 Scala IDE 没有任何影响。还有一件事,即使你有一个 SBT 项目,Scala IDE 也不在乎,即它不会寻找你的 Build.scala 来设置类路径,这就是为什么你实际上需要 sbteclipse 来生成Eclipse .classpath。希望这会有所帮助。 @Jan Scala IDE 增加了混乱,是的,提供有关设置良好 Scala 开发环境的更大图景以及适当编程工作流程的一些可靠指导的文档会非常方便。跨度> 【参考方案1】:

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

对于基于 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"

然后拔出砍刀,开始前进。如果幸运的话,您最终不会使用依赖于某些相同 jar 但版本不兼容的 jar。鉴于 Java 生态系统,您通常最终会包括所有内容和厨房水槽,并且需要付出一些努力来消除依赖关系或确保您不会丢失所需的依赖关系。

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

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

确保您理解:

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

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

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

最大限度地利用showinspect和tab补全来熟悉设置的实际值、它们的依赖关系、定义和相关设置。我不相信你会发现使用inspect 的关系在任何地方都有记录。如果有更好的方法我想知道。

【讨论】:

【参考方案2】:

我使用sbt的方式是:

    使用 sbt-extras - 只需获取 shell 脚本并将其添加到项目的根目录中 创建一个带有MyProject.scala 文件的project 文件夹,用于设置sbt。我更喜欢 build.sbt 方法 - 它是 scala 并且更灵活 创建一个project/plugins.sbt 文件并为您的IDE 添加适当的插件。 sbt-eclipse、sbt-idea 或 ensime-sbt-cmd,以便您可以为 eclipse、intellij 或 ensime 生成项目文件。 在项目的根目录中启动 sbt 并为您的 IDE 生成项目文件 利润

我不会费心检查 IDE 项目文件,因为它们是由 sbt 生成的,但可能有你想要这样做的原因。

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

【讨论】:

感谢您的好回答。我接受了另一个答案,因为它涵盖了更多的领域,并且投票赞成你的原因,它也非常好。如果可以的话,我会接受的。【参考方案3】:

使用 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)

【讨论】:

我的观点是,当有疑问时,添加更多魔法不太可能解决您的问题。【参考方案4】:

安装

brew install sbt 或类似的安装 sbt,从技术上讲包括

sbt 启动器脚本(bash 脚本)https://github.com/sbt/sbt-launcher-package sbt 启动器 jar (sbt-launcher.jar) https://github.com/sbt/launcher 核心 sbt (sbt.jar) https://github.com/sbt/sbt

当您从终端执行sbt 时,它实际上会运行 sbt 启动器 bash 脚本。就我个人而言,我从来不用担心这三位一体,只是把 sbt 当作一个单一的东西来使用。

配置

要为特定项目配置 sbt,请将 .sbtopts 文件保存在项目的根目录中。要配置 sbt 系统范围修改 /usr/local/etc/sbtopts。执行sbt -help 应该会告诉您确切的位置。例如,一次性执行 sbt -mem 4096 给 sbt 更多内存,或者将 -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 项目的一部分。这个不同的 Scala 项目可以在 project/ 目录下找到,该目录有自己的 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 的设计考虑到了这一点,这可能会导致最初令人惊讶的语义。你觉得下面sn-p的执行顺序是什么?

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 任务。然而这实际上意味着在 parallelbefore println("hello") so

中执行 ab
a
b
hello

或者因为ab的顺序无法保证

b
a
hello

也许自相矛盾的是,在 sbt 中并行比串行更容易。如果您需要串行订购,您将不得不使用特殊的东西,例如 Def.sequentialDef.taskDyn 来模拟 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

类似于

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 

我们看到sum 的位置取决于并且必须等待ab

换句话说

对于应用语义,使用.value 对于 monadic 语义使用sequentialtaskDyn

考虑 another 在语义上混淆 sn-p,因为 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

注意语法.value是关于DAG中的关系,并不意味着

“现在就给我价值”

相反,它意味着类似

“我的调用者首先依赖于我,一旦我知道整个 DAG 是如何组合在一起的,我就能够为我的调用者提供请求的值”

所以现在可能更清楚为什么x 还不能被赋值;在关系建立阶段还没有可用的价值。

我们可以在build.sbt 中清楚地看到 Scala 本身和 DSL 语言之间的语义差异。以下是一些对我有用的经验法则

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

命令与任务

命令是脱离 DAG 的一种懒惰方式。使用命令可以很容易地改变构建状态并根据需要序列化任务。代价是我们松散了 DAG 提供的任务的并行化和重复数据删除,哪种方式的任务应该是首选。您可以将命令视为一种会话的永久记录,可以在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.extractrunTask 使命令类型安全

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

范围

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

如何在多项目构建中定义一次任务,并使其可用于所有子项目? 如何避免测试依赖于主类路径?

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

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

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

Test/compile

或者可能从特定子项目执行特定任务,而无需先使用 project subprojB 导航到该项目

subprojB/Test/compile

我认为以下经验法则有助于避免范围界定的复杂性

没有多个build.sbt 文件,但在根项目下只有一个主文件,它控制所有其他子项目 通过自动插件共享任务 将常用设置分解为普通的 Scala val 并将其显式添加到每个子项目中

多项目构建

而不是为每个子项目创建多个 build.sbt 文件

.
├── 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 来统治他们所有

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

在多项目构建中有factoring out common settings 的常见做法

在 val 中定义一系列常用设置并将它们添加到每个 项目。以这种方式学习的概念更少。

例如

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         // list all projects
project multi1   // change to particular project

插件

请记住,构建定义是一个适当的 Scala 项目,位于 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

这是project/FooPlugin.scala下的最小auto plugin

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

应该有效地为所有子项目启用插件,而不必在build.sbt中显式调用enablePlugin

IntelliJ 和 sbt

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

use sbt shell

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

主要参考文献

sbt - A declarative DSL Task graph How to share sbt plugin configuration between multiple projects? Use sbt shell for build and import

【讨论】:

以上是关于有人可以解释使用 SBT 的正确方法吗?的主要内容,如果未能解决你的问题,请参考以下文章

有人可以解释 JPA 和 Hibernate 中的 mappedBy 吗?

有人可以解释如何使用 Ward 的方法来初始化 k-means 吗?

CUDA内核启动参数解释正确吗?

有人可以解释 C++ FAILED 函数吗?

有人可以解释我如何像我五岁那样进行 JSONP 调用吗? [复制]

有人可以用非流行语向我解释企业服务总线吗?