TL;DR
- 2025年6月現在のScalaエコシステムでも、sbt-crossprojectを使ったマルチプラットフォームプロジェクトを作成し、すべてのプラットフォームでビルドおよび実行を行うことができました
- Scala NativeやScala.jsもScala3に対応してくれたおかげで、Scala3でマルチプラットフォームプロジェクトを作れるようになっていました
初めに
JVM、ネイティブ、ブラウザなどの複数のプラットフォームをターゲットとするプロジェクトを扱うフレームワークとして、Scalaにはsbt-crossprojectというものがあります1。
しかし、sbt-crossprojectは最終更新が2年前で止まっており、そのGitter8テンプレートは約8年前に更新が止まっています。そのため、sbt-crossprojectが現在でも使い続けられるかという心配がありましたが、主に各種バージョン調整のみで現在最新のScalaエコシステムを使用することができました。
本記事はその報告です。
前提条件
scalac
や sbt
2 の最新版がインストールされているものとします。まだ環境をお持ちでない場合、Coursierをインストールして環境構築を行ってください。
また、ネイティブ向けのバイナリを作成したい場合、LLVMなどが必要になります。詳細はScala Nativeのセットアップガイドを参照してください。
プロジェクトテンプレートの適用
- 適当なプロジェクトディレクトリを作成し、そこに
cd
しておきます -
sbt new portable-scala/sbt-crossproject.g8
を実行し、プロジェクトテンプレートを適用します。適用後のディレクトリ構成は以下のようになります
.
│ build.sbt
│
├─bar
│ ├─js
│ │ └─src
│ │ └─main
│ │ └─scala
│ │ Foo.scala
│ │
│ ├─jvm
│ │ └─src
│ │ └─main
│ │ └─scala
│ │ Foo.scala
│ │
│ ├─native
│ │ └─src
│ │ └─main
│ │ └─scala
│ │ Foo.scala
│ │
│ └─shared
│ └─src
│ └─main
│ └─scala
│ Bar.scala
│
└─project
build.properties
plugins.sbt
このうち、project
以下にはプロジェクトの共通設定等、それ以外のディレクトリ(テンプレートでは bar
のみ)にはサブプロジェクトが入ります。
また、bar/js
, bar/jvm
, bar/native
のそれぞれはブラウザ(JavaScript)、JVM、ネイティブそれぞれの固有コードやファサードなどを置き、bar/shared
に全プラットフォーム共通のコードを置きます。
各種バージョンの調整
build.properties
project/build.properties
の sbt.version
を最新のsbtのバージョンに書き換えておきます。
執筆時点での sbt の最新版は 1.11.2
なので、project/build.properties
を以下のように書き換えます。
sbt.version=1.11.2
plugins.sbt
各sbtプラグインの最新のバージョン番号を調査し、project/plugins.sbt
の当該箇所を書き換えてそのバージョンを使用させるようにします。
執筆時点(2025-06-30)では、書き換え後のproject/plugins.sbt
は以下のようになります。
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8")
build.sbt
sbtのビルドスクリプトである、build.sbt
を以下のように書き換えます。scalaVersion
は最新のものを使用してください。
import sbtcrossproject.CrossPlugin.autoImport.crossProject
val sharedSettings = Seq(
scalaVersion := "3.7.1"
)
lazy val bar = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.settings(sharedSettings)
.jsSettings(
scalaJSUseMainModuleInitializer := true
)
コードの最新バージョンへの追従
JSのFoo.scala
bar/js/src/main/scala/Foo.scala
は現在のScala.jsでは古い書き方になっているため、bar/jvm/src/main/scala/Foo.scala
など、他の Foo.scala
と同一の内容で上書きしておきます。
実行
ここまでのところで、プロジェクトが各プラットフォームで正しくビルドおよび実行できることを確認します。
プロジェクトのルートディレクトリで sbt
コマンドを叩いて sbtを起動し、表示された対話画面で barJVM/run
, barNative/run
, barJS/run
をそれぞれ実行します。
これらのビルドおよび実行が正常に行われることを確認してください。
実戦に向けた改良
ここまでで、最新のScalaエコシステム上でマルチプラットフォームプロジェクトを運用するための最低限の処置を行いました。
ここからは、作成したプロジェクトを実戦で使えるように改良を加えていきます。
サブプロジェクトの名称変更
このプロジェクトの(唯一の)サブプロジェクトは現在 bar
という名前ですが、これはあまり実戦的ではありません。そのため、これをより適当名前、ここでは例として "main" に書き換えておきます。
以下の手順を実行してください。
- プロジェクトルートの
bar
というディレクトリをmain
にリネームする -
build.sbt
のlazy val bar = ...
という個所をlazy val main = ...
に書き換える
Foo.scala
のリネーム
main/{jvm,native,js}/src/main/scala/Foo.scala
の名前もあまりよろしくないため、これらをすべて Main.scala
という名前にリネームしてしまいます。
Main.scala
のScala3化
Scalaは近年、2から3へとメジャーバージョンアップがあったのですが、このテンプレートでは追従できていません。
Scala3ではScala2で冗長だった箇所などが整理されたほか、各種括弧の省略ルールなどが強化され、よりエレガントな書き方ができるようになりました。
その恩恵にあずかるため、Main.scala
もすべてScala3準拠にアップデートしておきます。
すべての Main.scala
を以下のように書き換えます:
@main def main(): Unit =
println(Bar.a)
これらが終わったら再度 sbt
の対話画面で reload
を実行した後 mainJVM/run
, mainNative/run
, mainJS/run
を実行し、ビルド等が正しく行われることを確認します。
なお、サブプロジェクト名を変更した影響で、実行時に入力する文字列も bar{JVM,Native,JS}
から main{JVM,Native,JS}
に変更となっていることに注意してください。
なお、Bar.scala
はどうせ消してしまうので、Scala3には対応させません。
この後
この後は必要に応じて、パッケージを切る、main/shared
内にアプリケーションの実体を書いていく、などの作業を行っていきます。