VirtualAxis を用いた並列クロスビルド

sbt-projectmatrix は sbt のクロスビルドを改善するために、僕が実験として作っているプラグインで、本稿は前篇に続く第2弾だ。0.4.0 をリリースしたのでここで紹介する。

おさらい: 複数の Scala バージョンに対するビルド

sbt-projectmatrix をビルドに追加後、以下のようにして 2つの Scala バージョンを使ったマトリックスをセットアップする。

ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.10"
ThisBuild / version      := "0.1.0-SNAPSHOT"
 
lazy val core = (projectMatrix in file("core"))
  .settings(
    name := "core"
  )
  .jvmPlatform(scalaVersions = Seq("2.12.10", "2.11.12"))

これは coreJVM2_11coreJVM2_12 というサブプロジェクトを作る。 ++ スタイルのステートフルなクロスビルドと違って、これは並列にビルドする。これは変わっていない。

前篇ではこの考え方をクロス・プラットフォームやクロス・ライブラリへと応用させることを考えた。

0.2.0 の問題

Support for mixed-style matrix dependencies #13Support for pure Java subprojects #14 という 2つの issue が立てられて、0.2.0 の設計に限界があることに気づいた。0.2.0 は各列は以下のように表現される:

final class ProjectRow(
    val idSuffix: String,
    val directorySuffix: String,
    val scalaVersions: Seq[String],
    val process: Project => Project
) {}

これは、列が追跡できるもの (例えばプラットフォーム) を 1つの次元とプラスで特定の Scala バージョンに限定する。報告された issue はマトリックス内の列を別のマトリックス内の列へと少し弱めた制限を用いて関連付けようとしていると意味で同じ問題の変種であると言える。

VirtualAxis

sbt-projectmatrix 0.4.0 は VirtualAxis を導入するが、最初はこれが何かを理解しなくても sbt-projectmatrix 自体は使い始めることができる。

/** A row in the project matrix, typically representing a platform + Scala version.
 */
final class ProjectRow(
    val autoScalaLibrary: Boolean,
    val axisValues: Seq[VirtualAxis],
    val process: Project => Project
)
 
/** Virtual Axis represents a parameter to a project matrix row. */
sealed abstract class VirtualAxis {
  def directorySuffix: String
 
  def idSuffix: String
 
  /* The order to sort the suffixes if there were multiple axes. */
  def suffixOrder: Int = 50
}
 
object VirtualAxis {
  /**
   * WeakAxis allows a row to depend on another row with Zero value.
   * For example, Scala version can be Zero for Java project, and it's ok.
   */
  abstract class WeakAxis extends VirtualAxis
 
  /** StrongAxis requires a row to depend on another row with the same selected value. */
  abstract class StrongAxis extends VirtualAxis
 
  ....
}

ProjectRowVirtualAxis の集合となった。VirtualAxis の典型的な使いみちはプラットフォーム (JVM, JS, Native) や Scala バージョンを表すのに使う。VirtualAxis クラスは WeakAxisStrongAxis という 2つのサブクラスに分かれる。

StrongAxis は関連する列が同値を持つことを要請し、これはプラットフォームなどを表すのに便利だ。一方、WeakAxis は同じ値または全く値を持たないことを許容する。Scala バージョンはその 1例だ。

lazy val intf = (projectMatrix in file("intf"))
  .jvmPlatform(autoScalaLibrary = false)
 
lazy val core = (projectMatrix in file("core"))
  .dependsOn(intf)
  .jvmPlatform(scalaVersions = Seq("2.12.10", "2.11.12"))

上の例では core マトリックスは Scala バージョン 2.12.10 と 2.11.12 に対応する 2つの JVM 列を持つ。ScalaVersionAxisWeakAxis であるため、Scala バージョンを持たない intf マトリックスの JVM 列に依存することができる。

並列クロスライブラリビルド

並列クロスライブラリビルドもカスタム VirtualAxis を定義することで実装できる。project/LightbendConfigAxis.scala 内に以下を書く:

import sbt._
 
case class LightbendConfigAxis(idSuffix: String, directorySuffix: String) extends VirtualAxis.WeakAxis {
}

次に build.sbt:

ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
 
lazy val config12 = LightbendConfigAxis("Config1_2", "config1.2")
lazy val config13 = LightbendConfigAxis("Config1_3", "config1.3")
 
lazy val scala212 = "2.12.10"
lazy val scala211 = "2.11.12"
 
lazy val app = (projectMatrix in file("app"))
  .settings(
    name := "app"
  )
  .customRow(
    scalaVersions = Seq(scala212, scala211),
    axisValues = Seq(config12, VirtualAxis.jvm),
    settings = Seq(
      moduleName := name.value + "_config1.2",
      libraryDependencies += "com.typesafe" % "config" % "1.2.1",
    )
  )
  .customRow(
    scalaVersions = Seq(scala212, scala211),
    axisValues = Seq(config13, VirtualAxis.jvm),
    settings = Seq(
      moduleName := name.value + "_config1.3",
      libraryDependencies += "com.typesafe" % "config" % "1.3.3",
    )
  )

LightbendConfigAxisVirtualAxis.WeakAxis を継承することに注目してほしい。これによって、app マトリックスは LightbendConfigAxis を持たない他のマトリックスにも依存することができる。

生成されたサブプロジェクトの参照

サブプロジェクトを build.sbt 内で参照したい場合は、以下のようにする:

lazy val core212 = core.jvm("2.12.10")
 
lazy val appConfig12_212 = app.finder(config13, VirtualAxis.jvm)("2.12.10")
  .settings(
    publishMavenStyle := true
  )

Scala Native サポート

Tatsuno さん (@exoego) のお蔭で、sbt-projectmatrix は 0.3.0 から Scala.JS も Scala Native にも対応している。これを使うには別に sbt-scala-native もセットアップする必要がある:

lazy val core = (projectMatrix in file("core"))
  .settings(
    name := "core",
    Compile / run mainClass := Some("a.CoreMain")
  )
  .nativePlatform(scalaVersions = Seq("2.11.12"))

まとめ

  • sbt-projectmatrix を使うことで複数の Scala バージョンや JVM/JS/Native クロスプラットフォームの並列ビルドを行うことができる。
  • VirtualAxis は、Scala-Java 間の依存やカスタムのクロスライブラリといったより柔軟なマトリックス間依存を可能とする。