从 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(...) 的设置。

Warning

在 sbt 1.x 中,裸设置是仅应用于根子项目的项目设置。在 sbt 2.x 中,build.sbt 中的裸设置是注入到所有子项目的通用设置。

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。如果您的构建中此逻辑被破坏并产生 NullPointerExceptionFileSystemNotFoundException,请设置 exportJars := false。如需保留旧行为,请在构建中设置 exportJars := false。此变更由 sbt/sbt#7464 引入,另请参阅 blog

迁移到缓存任务

在 sbt 2.x 中,所有任务默认都会被缓存。为参与缓存,任务的结果类型必须提供 sjsonnew.JsonFormat 的 given。任何结果类型缺少 JsonFormat(例如 ParadoxProcessorClassLoaderSeq[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 的子目录。为在脚本化测试中适应此变更,existsabsentdelete 现已支持 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-compatPluginCompat 模式及其使用方法:Migrating sbt plugins to sbt 2 with sbt2-compat plugin

迁移 Classpath 类型

sbt 2.x 将 Classpath 类型更改为 Seq[Attributed[xsbti.HashedVirtualFileRef]] 的别名,而不再是 Seq[Attributed[File]]。任何需要 FilePath 进行 I/O(类路径 URL、映射、同步、验证)的插件必须转换这些引用。如上所示添加并导入 sbt2-compat 后,请使用 toNioPathstoFiles,例如:

import sbtcompat.PluginCompat._

myTask := {
  implicit val conv: FileConverter = fileConverter.value
  val paths = toNioPaths((Compile / dependencyClasspath).value)
  val files = toFiles((Compile / dependencyClasspath).value)
  // ...
}

sbt2-compat 还提供 toNioPathtoFiletoFileRefsMappingDef.uncached 和其他便捷方法,可在共享源中使用。有关插件提供的 API 的最新文档,请参阅 sbt2-compat README

定义您自己的 PluginCompat shim

对于 sbt2-compat 未覆盖的 sbt 1.x 和 2.x 之间损坏的 API,您可以通过在 src/main/scala-2.12/PluginCompat.scalasrc/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 之间的差异。