sbt 1.x からのマイグレーション
build.sbt DSL の Scala 3.x への移行
念の為書いておくと、sbt 1.x 系 2.x 系のどちらを使ってもScala 2.x および Scala 3.x 両方のプログラムをビルドすることが可能だ。ただし、build.sbt DSL を裏付ける Scala は sbt のバージョンによって決定される。sbt 2.0 では、Scala 3.7.x 系に移行する。
そのため、sbt 2.x 用のカスタムタスクや sbt プラグインを実装する場合、それは Scala 3.x を用いて行われることとなる。Scala 3.x に関する詳細はScala 3.x incompatibility table や Scala 2 with -Xsource:3 なども参照。
// This works on Scala 2.12.20 under -Xsource:3
import sbt.{ given, * }
Import given
Scala 2.x と 3.x の違いの 1つとして、型クラスのインスタンスの言語スコープへの取り込み方の違いが挙げられる。Scala 2.x では import FooCodec._ と書いたが、Scala 3 は、import FooCodec.given と書く。
// 以下は、sbt 1.x と 2.x の両方で動作する
import sbt.librarymanagement.LibraryManagementCodec.{ given, * }
後置記法の回避
特に ModuleID 関連で、 sbt 0.13 や 1.x のコード例で後置記法を見ることは珍しく無かった:
// 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.7.3"
settings(...) を介さず、上の例のように build.sbt に直書きされたセッティングをベア・セッティング (bare settings) と呼ぶ。
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 にスコープ付けされる場面は無くなったはずだ。コモン・セッティングの ThisBuild に対する利点として委譲の振る舞いが、分かりやすくなるということがある。これらのセッティングはプラグインのセッティングと、settings(...) の間に入るため、 Compile / scalacOptions といったセッティングを定義するのに使える。 ThisBuild ではそれは不可能だった。
キャッシュ・タスクへのマイグレーション
キャッシュ化のオプトアウトを含めキャッシュ・タスク参照。
IntegrationTest の廃止
IntegrationTest コンフィギュレーションの廃止に対応するためには、別のサブプロジェクトを定義して、普通のテストとして実装するのが推奨される。
スラッシュ構文へのマイグレーション
sbt 1.x は sbt 0.13 スタイル構文とスラッシュ構文の両方をサポートしたきた。2.x で sbt 0.13 構文のサポートが無くなるので、sbt シェルと build.sbt の両方で統一スラッシュ構文を使う:
<project-id> / Config / intask / key
具体的には、test:compile という表記はシェルからは使えなくなる。代わりに Test/compile と書く。build.sbt ファイルの半自動的なマイグレーションには統一スラッシュ構文のための syntactic Scalafix rule 参照。
scalafix --rules=https://gist.githubusercontent.com/eed3si9n/57e83f5330592d968ce49f0d5030d4d5/raw/7f576f16a90e432baa49911c9a66204c354947bb/Sbt0_13BuildSyntax.scala *.sbt project/*.scala
sbt プラグインのクロスビルド
sbt 2.x では、sbt プラグインを Scala 3.x と 2.12.x に対してクロスビルドすると自動的に 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.6.2", "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.6.2"
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.0-RC6"
}
},
)
%% の変更点
sbt 2.x において ModuleID の %% 演算子は多プラットフォームに対応するようになった。JVM系のサブプロジェクトは、%% は以前同様に Maven リポジトリにおける (_3 のような) Scala バージョンの接尾辞をエンコードする。
%%% 演算子のマイグレーション
Scala.JS や Scala Native の sbt 2.x の対応ができるようになった場合、%% は (_3 のような) Scala バージョンと (_sjs1 その他の) プラットフォーム接尾辞をエンコードできるようになっている。そのため、%%% は %% に置き換えれるようになるはずだ:
libraryDependencies += "org.scala-js" %% "scalajs-dom" % "2.8.0"
JVM ライブラリが必要な場合は .platform(Platform.jvm) を使う。
target に関する変更
sbt 2.x では、target ディレクトリが単一の target/ ディレクトリに統一され、それぞれのサブプロジェクトがプラットフォーム、Scala バージョン、id をエンコードしたサブディレクトリを作る。scripted test でこの変更点を吸収するために、exists、absent、delete のコマンドは glob式 ** および || をサポートする。
# before
$ 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
# after
$ absent target/**/src_managed/foo.txt
$ exists target/**/src_managed/bar.txt
# どちらでも ok
$ exists target/**/proj/src_managed/bar.txt || proj/target/**/src_managed/bar.txt
PluginCompat 技法
同じ *.scala ソースから sbt 1.x と 2.x 向けのプラグインを作るには「詰め木」的なコードとして PluginCompat という名前のオブジェクトをsrc/main/scala-2.12/ と src/main/scala-3/ の両方に入れて違いを吸収することができる。
Classpath 型のマイグレーション
sbt 2.x からは Classpath 型がSeq[Attributed[xsbti.HashedVirtualFileRef]] 型のエイリアスとなった。以下は、sbt 1.x と 2.x の両方のクラスパスに対応するための詰め木だ。
// src/main/scala-3/PluginCompat.scala
package sbtfoo
import java.nio.file.{ Path => NioPath }
import sbt.*
import xsbti.{ FileConverter, HashedVirtualFileRef, VirtualFile }
private[sbtfoo] object PluginCompat:
type FileRef = HashedVirtualFileRef
type Out = VirtualFile
def toNioPath(a: Attributed[HashedVirtualFileRef])(using conv: FileConverter): NioPath =
conv.toPath(a.data)
inline def toFile(a: Attributed[HashedVirtualFileRef])(using conv: FileConverter): File =
toNioPath(a).toFile()
def toNioPaths(cp: Seq[Attributed[HashedVirtualFileRef]])(using conv: FileConverter): Vector[NioPath] =
cp.map(toNioPath).toVector
inline def toFiles(cp: Seq[Attributed[HashedVirtualFileRef]])(using conv: FileConverter): Vector[File] =
toNioPaths(cp).map(_.toFile())
end PluginCompat
これが sbt 1.x 用だ:
// src/main/scala-2.12/PluginCompat.scala
package sbtfoo
import sbt.*
private[sbtfoo] object PluginCompat {
type FileRef = java.io.File
type Out = java.io.File
def toNioPath(a: Attributed[File])(implicit conv: FileConverter): NioPath =
a.data.toPath()
def toFile(a: Attributed[File])(implicit conv: FileConverter): File =
a.data
def toNioPaths(cp: Seq[Attributed[File]])(implicit conv: FileConverter): Vector[NioPath] =
cp.map(_.data.toPath()).toVector
def toFiles(cp: Seq[Attributed[File]])(implicit conv: FileConverter): Vector[File] =
cp.map(_.data).toVector
// This adds `Def.uncached(...)`
implicit class DefOp(singleton: Def.type) {
def uncached[A1](a: A1): A1 = a
}
}
これで PluginCompat.* を import して toNioPaths(...) などを使って sbt 1.x と 2.x の違いを吸収できる。特にこれは、クラスパス型の違いを吸収して NIO Path ベクトルに変換できることを示している。