Руководство по миграции с SBT 1.x на SBT 2.x

Примечание: Этот документ основан на официальной документации sbt 2.0 change summary и Migrating from sbt 1.x, а также на опыте миграции репозитория dicechess-reference-bot, где уже используется SBT 2.0.4.


📌 Обзор изменений

SBT 2.x — это значительное обновление, которое вносит много изменений в архитектуру и синтаксис. Основные изменения:

АспектSBT 1.xSBT 2.x
Язык для build.sbtScala 2.12Scala 3.8.x (метабилд)
СинтаксисПоддержка старого синтаксиса (0.13)Только slash-синтаксис
КэшированиеОпциональноВсе задачи кэшируются по умолчанию
ТестыПолный запуск каждый разИнкрементальные тесты (testFull для полного запуска)
Пути к target<project>/target/target/out/jvm/scala-<ver>/<project>/
ThisBuildИспользовался для общих настроекЗаменён на “common settings”
IntegrationTestПоддерживалсяУдалён
exportJarsfalse по умолчаниюtrue по умолчанию
Плагины.sbt или .scalaТребуют суффикс _sbt2_3

🚀 Подготовка к миграции

1. Обновите версию SBT

В файле project/build.properties измените версию SBT:

- sbt.version=1.12.14
+ sbt.version=2.0.4

2. Обновите плагины

Плагины для SBT 2.x публикуются с суффиксом _sbt2_3. Пример из dicechess-reference-bot:

// project/plugins.sbt
addSbtPlugin("org.scalameta"  % "sbt-scalafmt"        % "2.6.1")
addSbtPlugin("org.scoverage"  % "sbt-scoverage"       % "2.4.4")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.7")

⚠️ Важно: Проверьте совместимость версий плагинов с SBT 2.x на их страницах в репозиториях.


🔧 Основные изменения в коде

1. Scala 3 в метабилде

SBT 2.x использует Scala 3.8.x для DSL в build.sbt. Это означает:

  • Импорты: Используйте import sbt.{ given, * } для импорта всех необходимых сущностей.
  • Postfix-нотация: Избегайте postfix-нотации (например, withSources()). Используйте точечную нотацию:
    - libraryDependencies += "org.example"  "foo" % "1.0").withSources().withJavadoc()

2. Common Settings (Общие настройки)

В SBT 1.x настройки без ThisBuild применялись только к корневому проекту. В SBT 2.x они применяются ко всем подпроектам.

Проблема:

// SBT 2.x: это применится ко ВСЕМ подпроектам!
name := "root"
publish / skip := true

Решение:

  • Используйте LocalRootProject для настроек только корневого проекта:
    LocalRootProject / name := "root"
    LocalRootProject / publish / skip := true
  • Или явно определяйте настройки для каждого подпроекта.

Миграция ThisBuild: В SBT 2.x ThisBuild больше не нужен для общих настроек. Они автоматически становятся “common settings”.

3. Slash-синтаксис

SBT 2.x требует единого slash-синтаксиса для всех ключей. Старый синтаксис (key in (project, config)) больше не поддерживается.

Примеры:

- scalacOptions in Compile += "-Xlint"
+ Compile / scalacOptions += "-Xlint"
 
- fork in Test := true
+ Test / fork := true
 
- version in ThisBuild := "1.0.0"
+ ThisBuild / version := "1.0.0"

В консоли SBT:

- sbt:hello> test:compile
+ sbt:hello> Test/compile

🔧 Инструмент: Используйте Scalafix-правило для полуавтоматической миграции:

scalafix --rules=https://gist.githubusercontent.com/eed3si9n/57e83f5330592d968ce49f0d5030d4d5/raw/7f576f16a90e432baa49911c9a66204c354947bb/Sbt0_13BuildSyntax.scala *.sbt project/*.scala

4. Кэширование задач

В SBT 2.x все задачи кэшируются по умолчанию. Это означает:

  • Результаты задач сохраняются на диск и могут быть восстановлены.
  • Если тип результата задачи не поддерживает сериализацию (например, ClassLoader, Seq[PathMapping]), задача не скомпилируется.

Решения:

  1. Определите JsonFormat для вашего типа (рекомендуется для производительности).
  2. Отключите кэширование для задачи:
    myTask := Def.uncached {
      // Код задачи, который возвращает несериализуемый тип
    }
  3. Используйте плагин sbt2-compat для совместимости с SBT 1.x:
    addSbtPlugin("com.github.sbt" % "sbt2-compat" % "<version>")

⚠️ Внимание: Кэширование пропускает побочные эффекты (например, запись файлов). Если задача должна выполнять побочные эффекты при каждом запуске, используйте Def.uncached.

5. Инкрементальные тесты

В SBT 2.x задача test стала инкрементальной и кэшируемой:

  • Тесты запускаются только если что-то изменилось с последнего запуска.
  • Для полного запуска всех тестов используйте testFull.

Пример:

# Запустить только изменённые тесты
sbt test
 
# Запустить ВСЕ тесты
sbt testFull

Фильтрация тестов:

# Запустить только тесты из ExampleTest
sbt "test ...ExampleTest"

6. Изменения в target/

В SBT 2.x структура каталога target унифицирована:

  • SBT 1.x: <project>/target/
  • SBT 2.x: target/out/jvm/scala-<version>/<project>/

Пример:

- target/streamz/managed/src_managed/foo.txt
+ target/out/jvm/scala-3.8.4/streamz/managed/src_managed/foo.txt

Для скриптовых тестов: Используйте глобальные выражения (**):

# Проверка наличия файла
$ exists target/**/src_managed/foo.txt
 
# Удаление файла
$ absent target/**/src_managed/foo.txt

7. exportJars по умолчанию true

В SBT 1.x exportJars был false по умолчанию. В SBT 2.x он стал true. Это может сломать:

  • getResource("/")
  • resource.toURI

Решение: Если ваш код ломается, установите exportJars := false:

exportJars := false

8. Удаление IntegrationTest

Конфигурация IntegrationTest удалена. Миграция:

  1. Создайте отдельный подпроект.
  2. Реализуйте тесты как обычные тесты.

9. **Изменения в “ стал платформенно-осведомлённым:

  • Для JVM: работает как раньше (кодирует суффикс версии Scala, например _3).
  • Для Scala.js/Scala Native: кодирует и версию Scala, и суффикс платформы (_sjs1, _native0.4 и т.д.).

**Миграция `% “scalajs-dom” % “2.8.0”

  • libraryDependencies += “org.scala-js” “foo” % “1.0”).platform(Platform.jvm)

### 10. **Изменения в `ModuleID`**
Некоторые методы `ModuleID` теперь требуют **точечной нотации** (не postfix):
```diff
- "org.example"  "foo" % "1.0").withSources().withJavadoc()

🔄 Миграция CI/CD пайплайнов

1. Запуск последовательности команд

В SBT 2.x последовательность команд должна передаваться как одна строка с точкой с запятой:

- sbt clean compile test
+ sbt "clean ; compile ; test"

2. sbt server и переменные окружения

В SBT 2.x sbt server переиспользуется между шагами CI/CD. Это означает:

  • Все шаги используют переменные окружения, переданные в первом вызове sbt.
  • Если ваш пайплайн полагается на разные переменные окружения (например, JAVA_OPTS), вы должны:
    • Либо передавать все переменные на уровне задания.
    • Либо явно завершать sbt после каждого шага:
      sbt "clean ; compile ; test ; shutdown"

3. Пути к артефактам тестов

Артефакты тестов (например, отчёты) теперь хранятся в подкаталогах target/out. Обновите пути:

# GitHub Actions пример
path: target/out/**/test-reports/*.xml

4. Глобальный базовый каталог

В SBT 2.x глобальный базовый каталог следует стандарту каталогов:

  • Windows: %LOCALAPPDATA%/sbt/2
  • Unix: $XDG_CONFIG_HOME/sbt/2 или $HOME/.config/sbt/2

🛠️ Миграция плагинов

1. Кросс-компиляция плагинов

Если вы разрабатываете плагин для SBT 1.x и 2.x:

  • Используйте projectMatrix для кросс-компиляции:
    lazy val plugin = (projectMatrix in file("plugin"))
      .enablePlugins(SbtPlugin)
      .settings(
        name := "sbt-myplugin",
      )
      .jvmPlatform(scalaVersions = Seq("3.8.4", "2.12.20"))
  • Укажите версию SBT в зависимости от версии Scala:
    (pluginCrossBuild / sbtVersion) := {
      scalaBinaryVersion.value match {
        case "2.12" => "1.5.8"
        case _      => "2.0.4"
      }
    }

2. Плагин sbt2-compat

Используйте sbt2-compat для совместимости между SBT 1.x и 2.x:

addSbtPlugin("com.github.sbt" % "sbt2-compat" % "<version>")

Пример использования:

import sbtcompat.PluginCompat._
 
myTask := {
  implicit val conv: FileConverter = fileConverter.value
  val paths = toNioPaths((Compile / dependencyClasspath).value)
  val files = toFiles((Compile / dependencyClasspath).value)
  // ...
}

3. Техника PluginCompat

Для API, которые сломались между SBT 1.x и 2.x, создайте шим PluginCompat:

// src/main/scala-2.12/PluginCompat.scala
package sbtfoo
object PluginCompat {
  def someMethod(): Unit = ...
}
 
// src/main/scala-3/PluginCompat.scala
package sbtfoo
object PluginCompat {
  def someMethod(): Unit = ...
}

📝 Пример миграции: build.sbt

До (SBT 1.x):

import sbt._
import Keys._
 
// Общие настройки для всех подпроектов
ThisBuild / version := "1.0.0"
ThisBuild / scalaVersion := "3.8.4"
 
// Настройки для корневого проекта
name := "my-project"
organization := "com.example"
 
// Подпроект
lazy val core = (project in file("core"))
  .settings(
    name := "my-project-core",
    libraryDependencies += "org.typelevel"  "cats-core" % "2.10.0").withSources()
  )
 
// Тесты (IntegrationTest удалён)
Test / fork := true
 
// Кэширование
myCustomTask := Def.uncached {
  // Код с побочными эффектами
}

🧪 Тестирование миграции

  1. Локальное тестирование:

    • Запустите sbt compile и убедитесь, что проект компилируется.
    • Запустите sbt test и проверьте, что тесты проходят.
    • Проверьте пути к target/ и артефактам.
  2. Тестирование в CI/CD:

    • Обновите скрипты CI/CD согласно изменениям выше.
    • Убедитесь, что все шаги работают с новой версией SBT.
  3. Проверка кэширования:

    • Запустите задачу дважды и убедитесь, что второй раз она использует кэш.
    • Проверьте, что Def.uncached работает для задач с побочными эффектами.

📚 Полезные ресурсы


✅ Чеклист миграции

  • Обновлена версия SBT в project/build.properties
  • Обновлены плагины (суффикс _sbt2_3)
  • Заменён старый синтаксис на slash-синтаксис
  • Проверены ThisBuild настройки (заменены на common settings или LocalRootProject)
  • Удалены все упоминания IntegrationTest
  • Проверено кэширование задач (Def.uncached для несериализуемых типов)
  • Обновлены пути к target/ в скриптах и CI/CD
  • Проверено поведение exportJars
  • Обновлены CI/CD пайплайны (последовательность команд, переменные окружения)
  • Протестирована кросс-компиляция (если применимо)
  • Проверена работа плагинов с SBT 2.x

💡 Совет: Начните миграцию с небольшого проекта (например, dicechess-bot-scala), чтобы понять все нюансы, прежде чем мигрировать более сложные проекты, такие как dicechess-engine-scala или dicechess-analytics.