sbt(simple build tool)是 Scala 的事实标准构建工具:构建定义本身是 Scala 代码,增量编译与交互式会话是它的两大招牌。也有 mill、scala-cli 等新选项,但读懂 sbt 仍是进入任何 Scala 项目的门票。
在空目录执行 sbt new scala/scala3.g8 生成项目骨架,核心是构建定义文件:
// 构建定义(sbt 语法示意) ThisBuild / scalaVersion := "3.4.2" lazy val root = (project in file(".")) .settings( name := "order-service", libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.typesafe.akka" %% "akka-actor-typed" % "2.6.20" ) )
依赖坐标的两个百分号 %% 表示"自动拼上当前 Scala 版本后缀"——跨 Scala 2/3 共存生态的关键机关。
日常 90% 的时间在交互会话里:
$ sbt sbt> compile # 增量编译 sbt> ~compile # 保存即重编,挂机监听 sbt> test sbt> console # 进入带全部依赖的 REPL sbt> run
sbt 按方法粒度追踪修改,只重编受影响的代码片断并重新拼装。配合 ~testQuick(只跑上次失败的用例),改一行到看到红绿的循环能压到秒级。这个反馈循环是 Scala 开发体验的命脉——编译器再聪明,等 30 秒的团队迟早放弃类型检查。

lazy val core = project.settings(libraryDependencies += "org.typelevel" %% "cats-core" % "2.12.0") lazy val api = project.dependsOn(core) lazy val it = project.dependsOn(api).configs(IntegrationTest)
模块边界就是 2.2 的可见性边界,拆分原则与包设计相同:稳定内核沉底、易变应用在上。
⚠️ 常见坑三则:
- 在构建定义里写复杂 Scala 逻辑——它能跑,但两年后没人敢动。构建代码也要克制。
- 忘记
%%与%的区别,拉到 Scala 2 的老构件在 Scala 3 项目里炸出神秘错误。- 全量 clean compile 当习惯——增量编译坏掉时先查循环依赖,别绕过它。
小项目或脚本可以直接用 scala-cli:单文件、内联依赖声明、零构建文件,是 1.1 环境 + 本书练习的轻量替代。
背景:把前七章的温度工具做成团队可依赖的服务模块。操作从零建工程到发布,全程只动两个文件:
// build.sbt ThisBuild / scalaVersion := "3.4.2" ThisBuild / organization := "com.camp" lazy val core = project.in(file("core")) .settings( name := "temp-core", libraryDependencies += "org.scalameta" %% "munit" % "1.0.0" % Test ) lazy val app = project.in(file("app")) .dependsOn(core) // 模块依赖:app 能用 core 的一切 .settings( name := "temp-app", Compile / mainClass := Some("temp.Main") )
日常循环三个命令:sbt Compile / compile 改完即编;sbt "core / testOnly *.TempSpec" 只跑一个模块的测试;sbt app / assembly 出胖包。解读: 前缀是增量监听,all 任务跨模块并行,多模块工程的心智模型就是"每个 lazy val 一个模块、dependsOn 连线"。变式:接 CI 时把 sbt +publishLocal 换成 sbt-ci-release 插件,签名与版本快照全自动。
第一个,冲突的 implicit 从两个传递依赖同时进入 classpath——用 evictionError 排查、dependencyTree 看谁带进来的,再用 dependencyOverrides 钉住版本。第二个,cache 半损坏导致诡异编译错误——删掉项目里 project/target 与 target 后重来,比反复重启 sbt 管用。
把 build.sbt 当配置文件背命令是下策,正确姿势是把它当一段 Scala 程序读:lazy val core = project.settings(...) 就是在定义一个值,%% 与 % 只是两个普通方法(二进制兼容标记与作用域标记)。想通这一点,遇到 "org" %% "name" % "ver" % Test 这种叠罗汉就不再恐惧——那只是方法链。三个必备快捷键:shell 里输入模块名直接切换当前模块;test 只跑当前模块;reload 在改完 build 定义后重载。
再补一条多人协作约定:把公共配置抽进 ThisBuild 与一个 settings 变量,新模块加入时只写增量,构建文件才不会随模块数线性膨胀;同时给每个模块显式声明 scalaVersion 继承自 ThisBuild,避免有人本地改出分裂版本;另外把 scalafmt 挂进 sbt(添加插件后 sbt scalafmtCheck 一键校验),格式争论就此终结。
还有一个被低估的命令值得进清单:sbt inspect 路径(如 inspect app / Compile / mainClass)能打印一个设置的完整定义链——谁覆盖了谁、来自哪个文件,构建行为诡异时它是唯一的显微镜。
%% 拼版本后缀,坐标体系来自 Maven 中央仓库。~compile 挂机、testQuick 只跑失败。