101

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

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

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

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

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

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

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

4

4 回答 4

30

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

对于基于 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。什么是开始的理智点,为什么?

我认为理智的一点是逐渐建立对 sbt 的免疫力

确保您了解:

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

始终保持这 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

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

于 2012-07-11T07:52:47.740 回答
26

我使用 sbt 的方式是:

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

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

您可以在此处看到这样设置的示例。

于 2012-07-10T20:34:58.170 回答
0

使用 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)
于 2014-09-17T07:49:25.807 回答
0

安装

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

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

配置

.sbtopts为项目根目录下的特定项目保存文件配置 sbt 。配置 sbt 系统范围的 modify /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
  ...
}

但是,这会出错,说 missing 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只是一个名为 的常规 Scala 对象的成员$9c2192aea3f1db3c251d。显然scalaj-http应该是项目定义$9c2192aea3f1db3c251d的依赖项,而不是正确项目的依赖项。因此它需要在project/build.sbt而不是声明build.sbt,因为project它是构建定义 Scala 项目所在的位置。

为了说明构建定义只是另一个 Scala 项目,请执行sbt consoleProject. 这将使用类路径上的构建定义项目加载 Scala REPL。您应该会看到一个类似于以下内容的导入

import $9c2192aea3f1db3c251d

所以现在我们可以直接与构建定义项目进行交互,方法是使用 Scala 而不是build.sbtDSL 来调用它。例如,以下执行fooTask

$9c2192aea3f1db3c251d.fooTask.eval

build.sbtroot 项目下是一个特殊的 DSL,它帮助定义 Scala 项目下的构建定义project/

并且构建定义Scala项目,可以有自己的构建定义下的Scala项目project/project/等等。我们说sbt 是递归的

sbt 默认是并行的

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

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任务。然而,这实际上意味着执行a并行,并且b在此之前 println("hello")

a
b
hello

或者因为aandb的顺序不能保证

b
a
hello

也许自相矛盾的是,在 sbt 中并行比串行更容易。如果您需要串行订购,则必须使用特殊的东西,例如Def.sequentialorDef.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
  • 对于一元语义使用sequentialtaskDyn

考虑另一个语义上令人困惑的片段,因为 的依赖构建性质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还不能赋值;在关系建立阶段还没有可用的价值。

我们可以清楚地看到 Scala 本身和build.sbt. 这里有一些对我有用的经验法则

  • 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.extract和使命令类型安全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
}

范围

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

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

sbt 有一个多轴作用域空间,可以使用斜杠语法进行导航,例如,

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

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

Test/compile

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

subprojB/Test/compile

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

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

多项目构建

而不是每个子项目的多个 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

在多项目构建中分解出通用设置是一种常见做法

在 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

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调用build.sbt.

IntelliJ 和 sbt

请启用以下设置(默认情况下应该真正启用)

use sbt shell

在下面

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

主要参考资料

于 2020-06-07T14:38:30.047 回答