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、その他のビルドツールを使用するユーザーとの相互運用性が可能になります。

2.13.0-RC1などのScalaのプレリリースバージョンおよび2.10.xより前のバージョンでは、完全なバージョンがサフィックスとして使用されます。

このページの残りの部分では、sbtがクロスビルドの一部としてこれをどのように処理するかについて説明します。

クロスビルドライブラリの使用 

複数のバージョンのScalaに対してビルドされたライブラリを使用するには、インライン依存関係の最初の%%%に2倍にします。これにより、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でクロスビルドを有効にするためにプラグインは必要ありませんが、Scalaバージョンおよび異なるプラットフォーム間で並行してクロスビルドできるsbt-projectmatrixの使用を検討してください。

ステートフルなプロジェクトのクロスビルド 

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
  )

:二重公開を避けるために、ルートプロジェクトではcrossScalaVersionsNilに設定する必要があります。

crossScalaVersionsにリストされているすべてのバージョンに対してビルドするには、実行するアクションに+をプレフィックスとして付加します。例えば

> + test

この機能を使用する一般的な方法は、単一のScalaバージョン(+プレフィックスなし)で開発を行い、クロスビルド(+を使用)は時折およびリリース時に行うことです。

Scalaバージョンに応じて設定を変更する 

Scalaバージョンに応じていくつかの設定を変更する方法を次に示します。CrossVersion.partialVersion(scalaVersion.value)は、Scalaバージョンの最初の2つのセグメントを含む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バージョン固有のソースとして含まれます。

crossPathsfalseに設定することにより、Scalaバージョンソースディレクトリと_<scala-binary-version>公開規約の両方をオプトアウトできます。これは、Scala以外のプロジェクトに役立つ場合があります。

同様に、*.classファイルなどのビルド成果物は、デフォルトではtarget/scala-<scala binary version>であるcrossTargetディレクトリに書き込まれます。

Javaプロジェクトを使用したクロスビルド 

クロスビルドに純粋なJavaプロジェクトが含まれる場合は、特別な注意が必要です。次の例では、networkがJavaプロジェクトであり、corenetworkに依存する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に正確に1つのScalaバージョン(通常はscala212)を設定する必要があります。
  4. Scalaサブプロジェクトでは、crossScalaVersionsに複数のScalaバージョンを設定できますが、Javaサブプロジェクトを集計することは避ける必要があります。

Scalaバージョンの切り替え 

++ <version> [command]を使用すると、<version>がそのcrossScalaVersionsにリストされている場合、サブプロジェクトのビルドに使用されているScalaバージョンを一時的に切り替えることができます。

例えば

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

<version>は、リポジトリに公開されたScalaのバージョン、または++ /path/to/scala/homeのようにScalaホームディレクトリへのパスのいずれかである必要があります。詳細については、コマンドラインリファレンスを参照してください。

[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/)

場合によっては、crossScalaVersionsの値に関係なく、Scalaバージョンの切り替えを強制したい場合があります。そのためには、感嘆符付きで++ <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.binaryまたはCrossVersion.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バリアントを解決する点を除いて、%%を使用するのと同じです。

逆に、scalaVersionが2.13.xの場合に、ライブラリの_3バリアントを使用するCrossVersion.for2_13Use3があります

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

ライブラリ作成者への警告:一般的に、Scala 2.13ライブラリまたはその逆のライブラリに依存するScala 3ライブラリを公開することは安全ではありません。その理由は、エンドユーザーがクラスパスに同じxライブラリの2つのバージョンx_2.13x_3を持つことを防ぐためです。

クロスビルドライブラリの使用に関する詳細 

異なる Scala バージョンに対する挙動を細かく制御するには、ModuleIDcross メソッドを使用できます。これらは同等です。

"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 1.x で sbt-release を使用してクロス公開するには、次の回避策を使用してください。

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 を発明したことに由来します。