8.1 sbt构建工具


8.1 sbt 构建工具

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 的灵魂

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 的可见性边界,拆分原则与包设计相同:稳定内核沉底、易变应用在上

⚠️ 常见坑三则:

  1. 在构建定义里写复杂 Scala 逻辑——它能跑,但两年后没人敢动。构建代码也要克制。
  2. 忘记 %%% 的区别,拉到 Scala 2 的老构件在 Scala 3 项目里炸出神秘错误。
  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

把 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 只跑失败。
  • 多模块按稳定度分层,公共变更的连锁范围决定团队速度。
  • 轻量场景用 scala-cli,别为了三行代码建 sbt 工程。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U