1. 交叉构建

交叉构建 

简介 

尽管保持了源代码兼容性,但不同版本的 Scala 可能是二进制不兼容的。本页介绍如何使用 sbt 针对多个版本的 Scala 构建和发布项目,以及如何使用针对多个版本的 Scala 构建的库。

有关交叉构建 sbt 插件,请参见 交叉构建插件

发布约定 

用于指示库针对哪个版本的 Scala 编译的底层机制是在库名称后追加 _<scala-binary-version>。例如,当针对 Scala 2.12.0、2.12.1 或任何 2.12.x 版本编译时,将使用工件名称 dispatch-core_2.12。这种相当简单的方法允许与 Maven、Ant 和其他构建工具的用户互操作。

对于 Scala 的预发布版本,例如 2.13.0-RC1,以及 2.10.x 之前的版本,将使用完整版本作为后缀。

本页的其余部分介绍了 sbt 如何在交叉构建中为您处理这个问题。

使用交叉构建的库 

要使用针对多个版本的 Scala 构建的库,请在内联依赖项中的第一个 % 后面添加 %%。这告诉 sbt 它应该将当前用于构建库的 Scala 版本附加到依赖项的名称。例如

libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.13.3"

对于固定版本的 Scala,几乎等效的手动替代方法是

libraryDependencies += "net.databinder.dispatch" % "dispatch-core_2.12" % "0.13.3"

使用 sbt-projectmatrix 交叉构建项目 

虽然 sbt 中不需要任何插件来启用交叉构建,但请考虑使用 sbt-projectmatrix,它能够跨 Scala 版本和不同平台并行交叉构建。

有状态地交叉构建项目 

crossScalaVersions 设置中定义要构建的 Scala 版本。允许使用 Scala 2.10.2 或更高版本。例如,在 .sbt 构建定义中

lazy val scala212 = "2.12.18"
lazy val scala211 = "2.11.12"
lazy val supportedScalaVersions = List(scala212, scala211)

ThisBuild / organization := "com.example"
ThisBuild / version      := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := scala212

lazy val root = (project in file("."))
  .aggregate(util, core)
  .settings(
    // crossScalaVersions must be set to Nil on the aggregating project
    crossScalaVersions := Nil,
    publish / skip := true
  )

lazy val core = (project in file("core"))
  .settings(
    crossScalaVersions := supportedScalaVersions,
    // other settings
  )

lazy val util = (project in file("util"))
  .settings(
    crossScalaVersions := supportedScalaVersions,
    // other settings
  )

注意crossScalaVersions 必须在根项目中设置为 Nil,以避免双重发布。

要针对 crossScalaVersions 中列出的所有版本构建,请在要运行的操作之前添加 + 前缀。例如

> + test

使用此功能的典型方法是在单个 Scala 版本上进行开发(无 + 前缀),然后偶尔进行交叉构建(使用 +),并在发布时进行交叉构建。

根据 Scala 版本更改设置 

以下是根据 Scala 版本更改某些设置的方法。CrossVersion.partialVersion(scalaVersion.value) 返回包含 Scala 版本的前两个段的 Option[(Int, Int)]

例如,如果您包含一个依赖项,该依赖项需要用于 Scala 2.12 的宏天堂编译器插件和用于 Scala 2.13 的 -Ymacro-annotations 编译器选项,这将非常有用。

lazy val core = (project in file("core"))
  .settings(
    crossScalaVersions := supportedScalaVersions,
    libraryDependencies ++= {
      CrossVersion.partialVersion(scalaVersion.value) match {
        case Some((2, n)) if n <= 12 =>
          List(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full))
        case _                       => Nil
      }
    },
    Compile / scalacOptions ++= {
      CrossVersion.partialVersion(scalaVersion.value) match {
        case Some((2, n)) if n <= 12 => Nil
        case _                       => List("-Ymacro-annotations")
      }
    },
  )

Scala 版本特定源代码目录 

除了 src/main/scala/ 目录之外,src/main/scala-<scala binary version>/ 目录也包含在内,作为源代码目录。例如,如果当前子项目的 scalaVersion 为 2.12.10,则 src/main/scala-2.12 将包含在内,作为 Scala 版本特定的源代码。

通过将 crossPaths 设置为 false,您可以选择退出 Scala 版本特定源代码目录和 _<scala-binary-version> 发布约定。这可能对非 Scala 项目有用。

类似地,构建产品(例如 *.class 文件)将写入 crossTarget 目录,该目录默认情况下为 target/scala-<scala binary version>

与 Java 项目交叉构建 

在交叉构建涉及纯 Java 项目时,必须格外小心。假设在以下示例中,network 是一个 Java 项目,core 是一个依赖于 network 的 Scala 项目。

lazy val scala212 = "2.12.18"
lazy val scala211 = "2.11.12"
lazy val supportedScalaVersions = List(scala212, scala211)

ThisBuild / organization := "com.example"
ThisBuild / version      := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := scala212

lazy val root = (project in file("."))
  .aggregate(network, core)
  .settings(
    // crossScalaVersions must be set to Nil on the aggregating project
    crossScalaVersions := Nil,
    publish / skip := false
  )

// example Java project
lazy val network = (project in file("network"))
  .settings(
    // set to exactly one Scala version
    crossScalaVersions := List(scala212),
    crossPaths := false,
    autoScalaLibrary := false,
    // other settings
  )

lazy val core = (project in file("core"))
  .dependsOn(network)
  .settings(
    crossScalaVersions := supportedScalaVersions,
    // other settings
  )
  1. crossScalaVersions 必须在聚合项目(例如根项目)中设置为 Nil
  2. Java 子项目应将 crossPaths 设置为 false,这将关闭 _<scala-binary-version> 发布约定和 Scala 版本特定的源代码目录。
  3. Java 子项目在 crossScalaVersions 中应该只有一个 Scala 版本,以避免双重发布,通常为 scala212
  4. Scala 子项目可以在 crossScalaVersions 中有多个 Scala 版本,但必须避免聚合 Java 子项目。

切换 Scala 版本 

您可以使用 ++ <version> [command] 临时切换当前用于构建子项目的 Scala 版本,前提是 <version> 在它们的 crossScalaVersions 中列出。

例如

> ++ 2.12.18
[info] Setting version to 2.12.18
> ++ 2.11.12
[info] Setting version to 2.11.12
> compile

<version> 应该是已发布到仓库的 Scala 版本,或者应该是 Scala 主目录的路径,例如 ++ /path/to/scala/home。有关详细信息,请参见 命令行参考

当将 [command] 传递给 ++ 时,它将在支持给定 <version> 的子项目上执行该命令。

例如

> ++ 2.11.12 -v test
[info] Setting Scala version to 2.11.12 on 1 projects.
[info] Switching Scala version on:
[info]     core (2.12.18, 2.11.12)
[info] Excluding projects:
[info]   * root ()
[info]     network (2.12.18)
[info] Reapplying settings...
[info] Set current project to core (in build file:/Users/xxx/hello/)

有时,您可能希望强制切换 Scala 版本,而不管 crossScalaVersions 值如何。您可以为此使用带感叹号的 ++ <version>!

例如

> ++ 2.13.0-M5! -v
[info] Forcing Scala version to 2.13.0-M5 on all projects.
[info] Switching Scala version on:
[info]   * root ()
[info]     core (2.12.18, 2.11.12)
[info]     network (2.12.18)

交叉发布 

+ 的最终目的是交叉发布您的项目。也就是说,通过执行

> + publishSigned

您将项目提供给不同版本的 Scala 的用户。有关发布项目的更多详细信息,请参见 发布

为了使此过程尽可能快,将使用不同的输出和托管依赖项目录来处理不同的 Scala 版本。例如,在针对 Scala 2.12.7 构建时,

  • ./target/ 将变为 ./target/scala_2.12/
  • ./lib_managed/ 将变为 ./lib_managed/scala_2.12/

打包的 jar、war 和其他工件将在正常工件 ID 后追加 _<scala-version>,如上面的发布约定部分所述。

这意味着针对每个版本的 Scala 的每次构建的输出都独立于其他构建。sbt 将分别为每个版本解析您的依赖项。这样,例如,您可以获得针对 2.11 编译的 Dispatch 版本,用于您的 2.11.x 构建,针对 2.12 编译的版本,用于您的 2.12.x 构建,等等。

覆盖发布约定 

crossVersion 设置可以覆盖发布约定

  • CrossVersion.disabled(无后缀)
  • CrossVersion.binary_<scala-binary-version>
  • CrossVersion.full_<scala-version>

默认值是 CrossVersion.binaryCrossVersion.disabled,具体取决于 crossPaths 的值。

因为(与 Scala 库不同)Scala 编译器在补丁版本之间不向前兼容,所以编译器插件应该使用 CrossVersion.full

Scala 3 特定的交叉版本 

在 Scala 3 项目中,您可以使用 Scala 2.13 库

("a" % "b" % "1.0") cross CrossVersion.for3Use2_13

这等效于使用 %%,除了当 scalaVersion 为 3.x.y 时,它会解析库的 _2.13 变体。

相反,我们有 CrossVersion.for2_13Use3,当 scalaVersion 为 2.13.x 时,它使用库的 _3 变体

("a" % "b" % "1.0") cross CrossVersion.for2_13Use3

针对库作者的警告:通常不安全发布依赖于 Scala 2.13 库或反之亦然的 Scala 3 库。原因是防止最终用户在他们的类路径中具有同一 x 库的两个版本 x_2.13x_3

有关使用交叉构建库的更多信息 

您可以通过在ModuleID上使用cross方法,对不同 Scala 版本的行为进行细粒度控制。这些是等效的

"a" % "b" % "1.0"
("a" % "b" % "1.0").cross(CrossVersion.disabled)

这些是等效的

"a" %% "b" % "1.0"
("a" % "b" % "1.0").cross(CrossVersion.binary)

这将覆盖默认值,始终使用完整的 Scala 版本,而不是二进制 Scala 版本

("a" % "b" % "1.0").cross(CrossVersion.full)

CrossVersion.patch位于CrossVersion.binaryCrossVersion.full之间,它会剥离任何尾随的-bin-...后缀,该后缀用于区分变体但二进制兼容的 Scala 工具链构建。

("a" % "b" % "1.0").cross(CrossVersion.patch)

CrossVersion.constant修复了一个常数值

("a" % "b" % "1.0") cross CrossVersion.constant("2.9.1")

它等同于

"a" % "b_2.9.1" % "1.0"

常量交叉版本主要用于交叉构建,并且依赖项并非所有 Scala 版本都可用,或者它使用的约定与默认约定不同。

("a" % "b" % "1.0") cross CrossVersion.constant {
  scalaVersion.value match {
    case "2.9.1" => "2.9.0"
    case x => x
  }
}

关于 sbt-release 的说明 

sbt-release 通过复制粘贴 sbt 0.13 的+实现来实现交叉构建支持,因此至少从 sbt-release 1.0.10 开始,它无法与 sbt 1.x 的交叉构建正确配合使用,最初的原型是 sbt-doge。

要使用 sbt-release 与 sbt 1.x 进行交叉发布,请使用以下解决方法

ThisBuild / organization := "com.example"
ThisBuild / version      := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := scala212

import ReleaseTransformations._
lazy val root = (project in file("."))
  .aggregate(util, core)
  .settings(
    // crossScalaVersions must be set to Nil on the aggregating project
    crossScalaVersions := Nil,
    publish / skip := true,

    // don't use sbt-release's cross facility
    releaseCrossBuild := false,
    releaseProcess := Seq[ReleaseStep](
      checkSnapshotDependencies,
      inquireVersions,
      runClean,
      releaseStepCommandAndRemaining("+test"),
      setReleaseVersion,
      commitReleaseVersion,
      tagRelease,
      releaseStepCommandAndRemaining("+publishSigned"),
      setNextVersion,
      commitNextVersion,
      pushChanges
    )
  )

然后,它将使用真正的交叉 (+) 实现进行测试和发布。此技术的功劳归属于 James Roper 在 playframework#4520 以及后来发明了 releaseStepCommandAndRemaining