从 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 默认值已从 false 改为 true,在编译或测试时将 JAR 文件添加到类路径中。这将减少类路径上的文件数量并降低哈希开销。
作为副作用,如果您调用了 Path.of(...),这可能会破坏从资源 URI 到 Path 的转换。我们建议按如下方式迁移代码:
val str = new String(
getClass.getResourceAsStream("/test-resource.txt").readAllBytes()
)
通过将 exportJars 改回 false 来选择退出。此变更在 #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.7"
}
},
)
%% 的变更
在 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>。插件在迁移过程中应注意此变更。
Fork 运行的工作目录
自 sbt 2.0.4 起,fork 运行的工作目录从子项目的 baseDirectory 改为构建的工作目录。这是为了使客户端运行的行为类似于 sbt 1.x 的进程内 run 任务。
您可以使用 Compile / run / baseDirectory 将其改回 baseDirectory:
Compile / run / baseDirectory := baseDirectory.value
测试 ClassLoader
自 sbt 2.0.5 起,closeClassLoaders 设置默认为 true,用于关闭进程内的临时测试 ClassLoader。这是为了修复在 Windows 上观察到的 AccessDeniedException。关闭临时测试 ClassLoader 在大多数情况下没有问题,但某些在测试任务完成前未关闭所有线程的测试或库可能会出现 ClassNotFound 等运行时错误。
要解决此问题,您可以按如下方式 fork 测试:
Test / fork := true
迁移 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 之间的差异。