1. 交叉編譯

交叉編譯 

簡介 

不同版本的 Scala 可能在二進位檔案上不相容,即使它們保持來源碼相容性。此頁面說明如何使用 sbt 針對多個版本的 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-projectmatrix (它能夠跨 Scala 版本和不同平台平行進行交叉建置),但在 sbt 中啟用交叉建置不需要外掛程式。

狀態式地交叉建置專案 

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) 會傳回 Option[(Int, Int)],其中包含 Scala 版本的前兩個區段。

如果您包含需要 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.x 建置取得針對 2.11 編譯的 Dispatch 版本、為 2.12.x 建置取得針對 2.12 編譯的版本,依此類推。

覆寫發佈慣例 

crossVersion 設定可以覆寫發佈慣例

  • CrossVersion.disabled (沒有後綴)
  • CrossVersion.binary (_<scala-binary-version>)
  • CrossVersion.full (_<scala-version>)

預設值取決於 crossPaths 的值,會是 CrossVersion.binaryCrossVersion.disabled 其中之一。

因為(與 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 之間,它會移除任何用來區分變體但二進位相容的 Scala 工具鏈建置的尾綴 -bin-...

("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