Для Scala проектов довольно распространённым является предоставление бинарных артефактов скомпилированных под несколько версий Scala компилятора. Как правило для целей создания нескольких версий одного артефакта в сообществе принято использовать SBT, где эта возможность есть прямо из коробки и настраивается в пару строк. Но что если мы хотим заморочится и создать билд для кросс компиляции не используя SBT?
Для одного из своих Java проектов я решил создать Scala фасад. Исторически весь проект собирается с помощью Gradle, и фасад было решено добавить в этот же самый проект в качестве сабмодуля. Gradle в целом может компилировать Scala модули с той лишь оговоркой что никакой кросс компиляции в поддержке не заявлено. Есть открытый тикет 2017 года и пара плагинов (1, 2), которые обещают добавить эту возможность в ваш проект, но с ними есть проблемы, как правило связанные с публикацией артефактов. И больше в целом ничего нет. Я решил проверить, как сложно на самом деле сконфирурировать билд для кросс компиляции без специальных плагинов и СМС.
Для начала опишем желаемый результат. Хотелось бы чтобы один и тот же набор исходников был скомпилирован тремя версиями Scala компилятора: 2.11, 2.12 и 2.13 (на этот момент самый актуальный 2.13.0-RC2). И так как в Scala 2.13 есть куча всяких назад несовместимых изменений в коллекциях, хотелось бы иметь возможность добавить дополнительные source сеты для кода, специфичного для каждого из компиляторов. Опять же, в SBT это все в добавляется в пару строчек конфигурации. Давайте смотреть что можно сделать в Gradle.
Первая трудность с которой приходиться столкнуться это то, что версия компилятора вычисляется из версии задекларированной зависимости на scala-library. Плюс, все зависимости, имеющие префикс версии Scala компилятора, тоже нужно менять. Т.е. для каждой версии компилятора лист зависимостей должен быть свой. В добавок, набор флагов для разных версий компилятора на самом деле разный. Некоторые флаги были переименованы между версиями, а какие-то просто помечены как устаревшие или убраны совсем. Я решил, что пытаться уловить все ньюансы разных компиляторов в одном билд файле кажется уж больно затруднительной задачей и ещё более затруднительной его дальнейшая поддержка. Поэтому решил поисследовать возможные другие способы решения этой задачи. А что если мы создадим несколько билд конфигураций для одной и той же структуры директорий проекта?
В декларации включения сабмодулей в Gradle проект можно указать директорию, в которой будет находится корень сабмодуля и имя файла, отвечающего за его конфигурацию. Давайте укажем одну и ту же директорию для нескольких импортов и создадим несколько копий build скрипта под каждую версию компилятора.
rootProject.name = 'test'
include 'java-library'
include 'scala-facade_2.11'
project(':scala-facade_2.11').with {
projectDir = file('scala-facade')
buildFileName = 'build-2.11.gradle'
}
include 'scala-facade_2.12'
project(':scala-facade_2.12').with {
projectDir = file('scala-facade')
buildFileName = 'build-2.12.gradle'
}
include 'scala-facade_2.13'
project(':scala-facade_2.13').with {
projectDir = file('scala-facade')
buildFileName = 'build-2.13.gradle'
}
Неплохо, но переодически мы можем получать странные ошибки компиляции связанные с тем, что все три скрипта сборки используют одну и туже билд директорию. Мы можем это исправить, задав их сами для каждого из билдов:
plugins {
id 'scala'
}
buildDir = 'build-2.12'
clean {
delete 'build-2.12'
}
// ...
Теперь совсем красиво. С одной лишь проблемой, что такой билд сведет с ума вашу любимую IDE и скорее всего дальнейшее редактирование вашего проекта придется вести по приборам. Я подумал, что это не большая беда, т.к. всегда можно просто закоментировать лишние импорты сабмодулей и превратить кросс билд в обычный билд, с которым ваша IDE скорее всего умеет работать.
А что насчёт дополнительных source сетов? Опять же, с раздельными файлами это оказалось довольно просто, создаем новую директорию и конфигурируем ее как source set.
// ...
sourceSets {
compat {
scala {
srcDir 'src/main/scala-2.12-'
}
}
main {
scala {
compileClasspath += compat.output
}
}
test {
scala {
compileClasspath += compat.output
runtimeClasspath += compat.output
}
}
}
// ...
// ...
sourceSets {
compat {
scala {
srcDir 'src/main/scala-2.13+'
}
}
main {
scala {
compileClasspath += compat.output
}
}
test {
scala {
compileClasspath += compat.output
runtimeClasspath += compat.output
}
}
}
// ...
Финальная структура проекта выглядит так:
Здесь можно еще повыделять отдельные общие куски во внешние файлы настройки и импортировать их в билд, дабы уменьшить количество повторений. Но по мне так и так получилось неплохо, декларативно, изолировано и совместимо со всеми возможными Gradle плагинами.
Итого, проблема была решена, гибкости Gradle хватило для того чтобы довольно изящно выразить весьма нетривияльный сетап, а кросс билд Scala возможен не только с использованием SBT и, если по той или иной причине вы используете Gradle для сборки Scala проекта, кросс компиляция как возможность вам так же доступна. Надеюсь кому-то этот пост будет полезен. Спасибо за внимание.