Кросс-компиляция Scala в Gradle проекте

в 20:40, , рубрики: crosscompilation, gradle, scala

Для 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 скрипта под каждую версию компилятора.

settings.gradle

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'
}

Неплохо, но переодически мы можем получать странные ошибки компиляции связанные с тем, что все три скрипта сборки используют одну и туже билд директорию. Мы можем это исправить, задав их сами для каждого из билдов:

build-2.12.gradle

plugins {
    id 'scala'
}

buildDir = 'build-2.12'

clean {
    delete 'build-2.12'
}

// ...

Теперь совсем красиво. С одной лишь проблемой, что такой билд сведет с ума вашу любимую IDE и скорее всего дальнейшее редактирование вашего проекта придется вести по приборам. Я подумал, что это не большая беда, т.к. всегда можно просто закоментировать лишние импорты сабмодулей и превратить кросс билд в обычный билд, с которым ваша IDE скорее всего умеет работать.

А что насчёт дополнительных source сетов? Опять же, с раздельными файлами это оказалось довольно просто, создаем новую директорию и конфигурируем ее как source set.

build-2.12.gradle

// ...
sourceSets {
    compat {
        scala {
            srcDir 'src/main/scala-2.12-'
        }
    }
    main {
        scala {
            compileClasspath += compat.output
        }
    }
    test {
        scala {
            compileClasspath += compat.output
            runtimeClasspath += compat.output
        }
    }
}
// ...

build-2.13.gradle

// ...
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 проекта, кросс компиляция как возможность вам так же доступна. Надеюсь кому-то этот пост будет полезен. Спасибо за внимание.

Автор: Alex Simkin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js