从 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.7.x。
这意味着如果您为 sbt 2.x 实现自定义任务或 sbt 插件,必须使用 Scala 3.x。有关 Scala 3.x 的详细信息,请参阅 Scala 3.x 不兼容性表 和 Scala 2 with -Xsource:3。
// This works on Scala 2.12.20 under -Xsource:3
import sbt.{ given, * }
导入 given
Scala 2.x 与 3.x 的区别之一是将类型类实例导入作用域的方式。Scala 2.x 使用 import FooCodec._,而 Scala 3 使用 import FooCodec.given。编写:
// The following works for both sbt 1.x and 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.1"
如上例所示,Bare settings(裸设置)是直接在 build.sbt 中编写且不带 settings(...) 的设置。
In sbt 1.x bare settings were project settings that applied only to the root subproject. In sbt 2.x, the bare settings in build.sbt are common settings that are injected to all subprojects.
name := "root" // all subprojects will be named root!
publish / skip := true // all subprojects will be skipped!
若要将某些设置仅应用于根子项目,可以使用多项目构建定义,或将设置限定在 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。
迁移到缓存任务
In sbt 2.x, all tasks are cached by default. To participate in caching, the task result type must provide a given for sjsonnew.JsonFormat. Any task whose result type lacks JsonFormat (e.g. complex objects like ParadoxProcessor, ClassLoader, Seq[PathMapping], or function types) will fail at build load time in sbt 2.
If you don't want to define the given, the easiest way to migrate is to wrap the tasks with Def.uncached(...) so sbt 2 skips caching and always re-executes them:
myTask := Def.uncached {
// task body returning a non-serializable type
}
When considering caching for a task, watch out for side-effecting tasks. When sbt 2 restores a task result from its disk cache, it returns the cached value without re-executing the task body. Any side effect (e.g. writing files, syncing mappings) is silently skipped. If a task is meant to produce a side effect every time it runs, wrap it in Def.uncached(...) so sbt 2 always re-executes it.
The sbt2-compat plugin provides Def.uncached as a compatibility shim on sbt 1.x (where it is a no-op). See Cached task reference for details, including build-wide and per-task opt-out options.
从 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 进行交叉构建:
// using sbt 2.x
lazy val plugin = (projectMatrix in file("plugin"))
.enablePlugins(SbtPlugin)
.settings(
name := "sbt-vimquit",
)
.jvmPlatform(scalaVersions = Seq("3.6.2", "2.12.20"))
如果使用 projectMatrix,请确保将插件移至 plugin/ 等子目录。否则,合成根项目也会包含 src/。
使用 sbt 1.x 交叉构建 sbt 插件
如需使用 sbt 1.x 进行交叉构建,请使用 sbt 1.10.2 或更高版本。
// using sbt 1.x
lazy val scala212 = "2.12.20"
lazy val scala3 = "3.6.2"
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.0-RC9"
}
},
)
%% 的变更
在 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 表达式 ** 以及 ||。
# before
$ 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
# after
$ absent target/**/src_managed/foo.txt
$ exists target/**/src_managed/bar.txt
# either is ok
$ exists target/**/proj/src_managed/bar.txt || proj/target/**/src_managed/bar.txt
In sbt 1.x, target.value resolves to the project root target/ directory. In sbt 2.x, it resolves to target/out/jvm/scala-<ver>/<project-name> instead. Plugins should be aware of this change during migration.
PluginCompat 技术
To use the same *.scala source but target both sbt 1.x and 2.x, we can create a shim, for example an object named PluginCompat in both src/main/scala-2.12/ and src/main/scala-3/. APIs commonly encountered during migrations are abstracted into the sbt2-compat plugin that can be used to avoid creating the shims manually. To use it in your sbt plugin, you can add it to your sbt plugin's build.sbt:
addSbtPlugin("com.github.sbt" % "sbt2-compat" % "<version>")
And import and use the conversion methods in your shared source:
import sbtcompat.PluginCompat._
// Use the conversion methods here
You can read more about sbt2-compat, the PluginCompat pattern and how to use them the following blog article: Migrating sbt plugins to sbt 2 with sbt2-compat plugin.
迁移 Classpath 类型
sbt 2.x changed the Classpath type to be an alias of Seq[Attributed[xsbti.HashedVirtualFileRef]] instead of Seq[Attributed[File]]. Any plugin that needs File or Path for I/O (classpath URLs, mappings, sync, validation) must convert these references. With sbt2-compat added and imported as above, use toNioPaths and toFiles, for example:
import sbtcompat.PluginCompat._
myTask := {
implicit val conv: FileConverter = fileConverter.value
val paths = toNioPaths((Compile / dependencyClasspath).value)
val files = toFiles((Compile / dependencyClasspath).value)
// ...
}
sbt2-compat also provides toNioPath, toFile, toFileRefsMapping, Def.uncached and other convenience methods to be used in the shared sources. See the sbt2-compat README for the up-to-date documentation on the API provided by the plugin.
Defining your own PluginCompat shims
For the APIs broken between sbt 1.x and 2.x that are not covered by sbt2-compat, you can define your own PluginCompat shims by creating separate source files under src/main/scala-2.12/PluginCompat.scala and src/main/scala-3/PluginCompat.scala. For example:
// 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 = ...
}
Then use your own PluginCompat shims in your sbt plugin:
import sbtfoo.PluginCompat._
myTask := {
someSharedMethod()
}
This pattern is compatible with sbt2-compat and can be used alongside it to absorb the differences between sbt 1.x and 2.x.