从 sbt 1.x 迁移
将 build.sbt DSL 改为 Scala 3.x
提醒一下,用户可以使用 sbt 1.x 或 sbt 2.x 构建 Scala 2.x 或 Scala 3.x 程序。但 build.sbt DSL 所基于的 Scala 版本由 sbt 版本决定。在 sbt 2.0 中,我们正在迁移到 Scala 3.8.x。
这意味着如果您为 sbt 2.x 实现自定义任务或 sbt 插件,必须使用 Scala 3.x。有关 Scala 3.x 的详细信息,请参阅 Scala 3.x 不兼容性表 和 Scala 2 with -Xsource:3。
// 此代码在 -Xsource:3 下适用于 Scala 2.12.20
import sbt.{ given, * }
导入 given
Scala 2.x 与 3.x 的区别之一是将类型类实例导入作用域的方式。Scala 2.x 使用 import FooCodec._,而 Scala 3 使用 import FooCodec.given。编写:
// 以下代码在 sbt 1.x 和 2.x 中均适用
import sbt.librarymanagement.LibraryManagementCodec.{ given, * }
避免后缀
sbt 0.13 和 1.x 的示例中常见使用后缀表示法,尤其是 ModuleID:
// BAD
libraryDependencies +=
"com.github.sbt" % "junit-interface" % "0.13.2" withSources() withJavadoc()
以上代码在 sbt 2.x 中加载失败:
-- Error: /private/tmp/foo/build.sbt:9:61 --------------------------------------
9 | "com.github.sbt" % "junit-interface" % "0.13.2" withSources() withJavadoc()
| ^^
|can't supply unit value with infix notation because nullary method withSources
in class ModuleIDExtra: (): sbt.librarymanagement.ModuleID takes no arguments;
use dotted invocation instead: (...).withSources()
要修复此问题,请使用正常的(点号)函数调用表示法:
// GOOD
libraryDependencies +=
("com.github.sbt" % "junit-interface" % "0.13.2").withSources().withJavadoc()
裸设置变更
version := "0.1.0"
scalaVersion := "3.8.4"
如上例所示,Bare settings(裸设置)是直接在 build.sbt 中编写且不带 settings(...) 的设置。
name := "root" // 所有子项目将命名为 root!
publish / skip := true // 所有子项目将被跳过!
若要将某些设置仅应用于根子项目,可以使用多项目构建定义,或将设置限定在 LocalRootProject 下:
LocalRootProject / name := "root"
LocalRootProject / publish / skip := true
迁移 ThisBuild
在 sbt 2.x 中,裸设置不应再限定到 ThisBuild。新的 common settings(通用设置)相比 ThisBuild 的一个优势是它采用更可预测的委托行为。这些设置插入在插件设置与 settings(...) 中定义的设置之间,这意味着可以用于定义诸如 Compile / scalacOptions 之类的设置,而 ThisBuild 无法做到这一点。
exportJars 的变更
exportJars 默认为 true,之前为 false。这可能会破坏 getResource("/") 和 resource.toURI。如果您的构建中此逻辑被破坏并产生 NullPointerException 和 FileSystemNotFoundException,请设置 exportJars := false。如需保留旧行为,请在构建中设置 exportJars := false。此变更由 sbt/sbt#7464 引入,另请参阅 blog。
迁移到缓存任务
在 sbt 2.x 中,所有任务默认都会被缓存。为参与缓存,任务的结果类型必须提供 sjsonnew.JsonFormat 的 given。任何结果类型缺少 JsonFormat(例如 ParadoxProcessor、ClassLoader、Seq[PathMapping] 等复杂对象或函数类型)的任务将在 sbt 2 的构建加载时失败。
如果不想定义 given,最简单的迁移方式是用 Def.uncached(...) 包装任务,使 sbt 2 跳过缓存并始终重新执行它们:
myTask := Def.uncached {
// 返回不可序列化类型的任务体
}
在考虑任务缓存时,需注意有副作用的任务。当 sbt 2 从磁盘缓存恢复任务结果时,它会返回缓存值而不重新执行任务体。任何副作用(例如写文件、同步映射)都会被静默跳过。如果任务每次运行都应产生副作用,请用 Def.uncached(...) 包装,使 sbt 2 始终重新执行它。
sbt2-compat 插件在 sbt 1.x 上提供 Def.uncached 作为兼容性 shim(其中它是无操作)。有关详情,包括构建范围和每任务退出选项,请参阅缓存任务参考文档。
从 IntegrationTest 迁移
要从 IntegrationTest 配置迁移,请创建单独的子项目并作为普通测试实现。
迁移到斜杠语法
sbt 1.x 同时支持 sbt 0.13 风格语法和斜杠语法。sbt 2.x 移除了对 sbt 0.13 语法的支持,请在 sbt shell 和 build.sbt 中均使用斜杠语法:
<project-id> / Config / intask / key
例如,test:compile 在 shell 中不再有效。请改用 Test/compile。有关 build.sbt 文件的半自动迁移,请参阅 syntactic Scalafix rule for unified slash syntax。
scalafix --rules=https://gist.githubusercontent.com/eed3si9n/57e83f5330592d968ce49f0d5030d4d5/raw/7f576f16a90e432baa49911c9a66204c354947bb/Sbt0_13BuildSyntax.scala *.sbt project/*.scala
交叉构建 sbt 插件
在 sbt 2.x 中,如果您使用 Scala 3.x 和 2.12.x 交叉构建 sbt 插件,它将自动针对 sbt 1.x 和 sbt 2.x 进行交叉构建:
// 使用 sbt 2.x
lazy val plugin = (projectMatrix in file("plugin"))
.enablePlugins(SbtPlugin)
.settings(
name := "sbt-vimquit",
)
.jvmPlatform(scalaVersions = Seq("3.8.4", "2.12.20"))
如果使用 projectMatrix,请确保将插件移至 plugin/ 等子目录。否则,合成根项目也会包含 src/。
使用 sbt 1.x 交叉构建 sbt 插件
如需使用 sbt 1.x 进行交叉构建,请使用 sbt 1.10.2 或更高版本。
// 使用 sbt 1.x
lazy val scala212 = "2.12.20"
lazy val scala3 = "3.8.4"
ThisBuild / crossScalaVersions := Seq(scala212, scala3)
lazy val plugin = (project in file("plugin"))
.enablePlugins(SbtPlugin)
.settings(
name := "sbt-vimquit",
(pluginCrossBuild / sbtVersion) := {
scalaBinaryVersion.value match {
case "2.12" => "1.5.8"
case _ => "2.0.1"
}
},
)
%% 的变更
在 sbt 2.x 中,ModuleID 的 %% 运算符已具有平台感知能力。对于 JVM 子项目,%% 与之前一样工作,在 Maven 仓库中编码 Scala 后缀(例如 _3)。
迁移 %%% 运算符
当 Scala.JS 或 Scala Native 在 sbt 2.x 中可用时,%% 将同时编码 Scala 版本(如 _3)和平台后缀(如 _sjs1)。因此,%%% 可替换为 %%:
libraryDependencies += "org.scala-js" %% "scalajs-dom" % "2.8.0"
需要 JVM 库时请使用 .platform(Platform.jvm)。
target 的变更
在 sbt 2.x 中,target 目录统一为工作目录中的单个 target/ 目录,每个子项目创建编码平台、Scala 版本和子项目 ID 的子目录。为在脚本化测试中适应此变更,exists、absent 和 delete 现已支持 glob 表达式 ** 以及 ||。
# 之前
$ absent target/out/jvm/scala-3.3.1/clean-managed/src_managed/foo.txt
$ exists target/out/jvm/scala-3.3.1/clean-managed/src_managed/bar.txt
# 之后
$ absent target/**/src_managed/foo.txt
$ exists target/**/src_managed/bar.txt
# 均可
$ exists target/**/proj/src_managed/bar.txt || proj/target/**/src_managed/bar.txt
在 sbt 1.x 中,target.value 解析为项目根 target/ 目录。在 sbt 2.x 中,它改为解析为 target/out/jvm/scala-<ver>/<project-name>。插件在迁移过程中应注意此变更。
迁移 CI 流水线
有一些行为变更可能影响 CI 流水线。
运行一系列命令
现在必须将命令序列作为以分号分隔的带引号字符串提供:
sbt "clean ; compile ; test"
以前,您可以写 sbt clean compile test。现在这会产生错误 "Expected whitespace character"。
具有多个步骤的 sbt server
如果 CI 流水线包含多个运行 sbt 的步骤,后续步骤将复用第一次调用启动的 sbt server,而不是每次启动新的 sbt 进程。因此,这些后续步骤将继续使用传递给第一次会话的环境变量。
如果您的流水线依赖于向每个会话传递不同的环境变量(如 JAVA_OPTS),则必须在作业级别提供所有变量使其对所有 sbt 调用相同,或在每个步骤之后或步骤之间关闭 sbt。
sbt "clean ; compile ; test ; shutdown"
请注意,无论您是直接运行 sbt 还是通过在内部调用 sbt 的第三方操作(如 sbt-dependency-submission),此情况均适用。
测试产物
测试结果等输出产物现在存储在 target/out 下的子目录中,因此您可能需要更新用于测试发布和产物归档的路径:
path: target/out/**/test-reports/*.xml
全局基础目录的变更
在 sbt 2.x 中,全局基础目录遵循目录标准。Windows 上的默认值为 %LOCALAPPDATA%/sbt/2,否则为 $XDG_CONFIG_HOME/sbt/2 或 $HOME/.config/sbt/2。详情请参阅 sbt 参考。
PluginCompat 技术
为使用相同的 *.scala 源但同时面向 sbt 1.x 和 2.x,我们可以创建一个 shim,例如在 src/main/scala-2.12/ 和 src/main/scala-3/ 中创建名为 PluginCompat 的对象。迁移过程中常遇到的 API 已被抽象到 sbt2-compat 插件中,可用于避免手动创建 shim。要在 sbt 插件中使用它,可将其添加到 sbt 插件的 build.sbt:
addSbtPlugin("com.github.sbt" % "sbt2-compat" % "<version>")
然后在共享源中导入并使用转换方法:
import sbtcompat.PluginCompat._
// 在此处使用转换方法
您可以在以下博客文章中了解更多关于 sbt2-compat、PluginCompat 模式及其使用方法:Migrating sbt plugins to sbt 2 with sbt2-compat plugin。
迁移 Classpath 类型
sbt 2.x 将 Classpath 类型更改为 Seq[Attributed[xsbti.HashedVirtualFileRef]] 的别名,而不再是 Seq[Attributed[File]]。任何需要 File 或 Path 进行 I/O(类路径 URL、映射、同步、验证)的插件必须转换这些引用。如上所示添加并导入 sbt2-compat 后,请使用 toNioPaths 和 toFiles,例如:
import sbtcompat.PluginCompat._
myTask := {
implicit val conv: FileConverter = fileConverter.value
val paths = toNioPaths((Compile / dependencyClasspath).value)
val files = toFiles((Compile / dependencyClasspath).value)
// ...
}
sbt2-compat 还提供 toNioPath、toFile、toFileRefsMapping、Def.uncached 和其他便捷方法,可在共享源中使用。有关插件提供的 API 的最新文档,请参阅 sbt2-compat README。
定义您自己的 PluginCompat shim
对于 sbt2-compat 未覆盖的 sbt 1.x 和 2.x 之间损坏的 API,您可以通过在 src/main/scala-2.12/PluginCompat.scala 和 src/main/scala-3/PluginCompat.scala 下创建单独的源文件来定义自己的 PluginCompat shim。例如:
// src/main/scala-2.12/PluginCompat.scala
package sbtfoo
import sbt._
import Keys._
object PluginCompat {
def someSharedMethod(): Unit = ...
}
// src/main/scala-3/PluginCompat.scala
package sbtfoo
import sbt._
import Keys._
object PluginCompat {
def someSharedMethod(): Unit = ...
}
然后在 sbt 插件中使用您自己的 PluginCompat shim:
import sbtfoo.PluginCompat._
myTask := {
someSharedMethod()
}
此模式与 sbt2-compat 兼容,可与其一起使用以吸收 sbt 1.x 和 2.x 之间的差异。