Работая в многомодульном maven проекте, зачастую приходится вносить изменения в несколько связанных модулей одновременно. И если хочется собрать только задетые модули, то к сожалению maven не предоставляет ничего автоматического. Если чуть погуглить, то на stackoverflow можно найти простое однострочное решение:


mvn install -amd -pl $(svn st | colrm 1 8 | sed 's /.*  ' | xargs echo | sed 's- -,:-g' | sed 's ^ : ')

На этом можно было бы и закончить. Но мне хотелось большего — чего конкретнее и как я этого добивался под катом.


Для тех, кто плохо знаком с bash, mvn или svn, небольшое пояснение скрипта:


  1. Основная часть — mvn install -amd -pl project_list. Команда для мейвена, собрать проекты из списка project_list и их зависимые
  2. Всё, что внутри $(...) — получение локальных изменений в svn и вытаскивание названия проектов из этих изменений

Зачем это надо


Рассмотрим следующий простой сценарий:


  1. Добавляем новый метод в интерфейс Parentable в модуле А
  2. Реализуем этот метод в классе Child (implements Parentable) в модуле Б

Чтобы проверить сборку, нужно отдельно собирать А, потом Б. Если меняется только реализация в Child, то достаточно пересобирать только Б. Однако если меняется сигнатура метода, то нужно снова собирать оба модуля.


При такой работе приходится задумываться "а актуальна ли сборка проекта Б". Хорошо, когда проектов 2-3, но если их больше, да ещё и используется refactoring через IDE, то очень легко можно пропустить тот момент, когда сборка перестает быть актуальной.


Что не так со скриптом выше


Скрипт, представленный выше, решает этот вопрос топорно: он всегда собирает А и Б. Это не очень эффективно с точки зрения затрат времени, зато относительно безопасно, т.к. вы не забудете ничего собрать. Я пользовался этим подходом примерно месяц, но потом решил, что лучше забыть собрать, чем тратить это лишнее время.


Помимо этой проблемы, есть ряд других, связанных с особенностями maven, когда этот скрипт просто не работает. О них пойдет речь ниже.


Что было сделано


Я взял идею этого скрипта за основу и доработал напильником (в данном случае рашпилем). Целью было получить скрипт, который собирает всё, что мы изменили, но ещё не собрали. И при этом ничего больше.


Дорабатывать скрипт на bash'е дело неблагодарное по ряду причин. Поэтому разработка велась на python. Из плюсов:


  1. Кроссплатформенность
  2. Модульность
  3. Он есть почти у всех linux пользователей и многих window пользователей.
  4. Простота

Разумеется, хочется, чтобы просто взял и используешь, поэтому из зависимостей только стандартные библиотеки.


Итак, приступим к основной части статьи — описанию того, с каким проблемами пришлось столкнуться и какие решения были использованы.


Что может maven


Чтобы делать как можно меньше самому, неплохо бы разобраться, какой максимум можно выжать из maven. Максимум оказывается не очень большим. В мейвен можно собирать следующие области:


  1. Один проект
  2. Один проект и все его модули (рекурсивно)
  3. Список модулей внутри одного проекта.

Опция 3 выглядит самой подходящей и именно она используется во вступительном bash скрипте. Из плюсов, при работе с этой опцией можно указывать параметры:


  • -amd (also make dependents) — мэйвен также соберет все зависимые модули
  • -am (also make) — также соберутся модули, от которых наш список зависит

Для локальной сборки эти опции не очень полезны, т.к. сборку зависимостей (amd) можно возложить на CI и к тому же наша IDE может указать на ошибки компиляции без сборки maven'ом.


А сборка родительских проектов (-am) избыточна, т.к. мы можем вытащить неизмененные проекты из maven репозитория.


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


Проблема 1: собираем только фактические изменения


Чтобы понять, что у нас изменилось, удобнее всего использовать статус файлов в системе контроля версий. Однако VCS нам ничего не скажет о том, когда эти изменения были сделаны и включены ли они в сборку. Тем не менее, список всех изменений в VCS это наши активные проекты и отправная точка для дальнейшего анализа.


Чтобы узнать, насколько наши изменения совпадают со сборкой, мы сравниваем дату изменения файлов с датой последней сборки (target/artifact_id-version.jar).


Для первого приближения хорошо, но есть нюансы, которые необходимо учитывать:


  1. Если нам из VCS приходят чужие изменения, то наш проект снова нужно пересобирать
  2. target/artifact_id-version.jar это дефолтное значение. Может быть совершенно другим
  3. Измененные файлы могут не иметь отношения к сборке (например, файлик проекта для вашей IDE).

Пункт 1 можно решить анализом не только локально измененных файлов, но и всех файлов в активных проектах.


Две другие проблемы решаются просто получением необходимой информации из pom файла и учёту её при сборке.


Проблема 2: откат изменений


Если изменения были и мы с ними собрали артефакт, то при откате этих изменений, артефакт нужно пересобрать. Но статус в VCS не дает никакой информации о таких файлах. Следовательно стоит хранить эту информацию самим. Для этого, после анализа текущих изменившихся проектов сохраняем эту информацию где-нибудь и при следующей сборке объединяем текущие изменения и предыдущие, из сохраненного файла.


Проблема 3: модули, не входящие в основной проект


Правильной структурой maven проекта считается та, в которой каждый проект описан как модуль в родительском проекте. Т.е. выполнив mvn install в корне, мы должны задействовать все внутренние проекты.


Всё было бы проще, если бы всё было правильно. Но это не всегда так. В данном случае бывают отвязанные проекты. Правильного maven'а это не устраивает, и если вы подсунете ему такой проект в -pl, то он выплюнет в вас ошибкой, т.к. -pl работает только с теми проектами, которые указаны как submodules в вашем родительском проекте.


Чтобы решить проблему, нужно собирать такие проекты отдельно. Т.е. запуск мэйвена производится столько раз, сколько у нас несвязанных корневых проектов.


Для усложнения — может ещё возникнуть ситуация, когда эти проекты между собой связаны, поэтому собирать нужно в определенном порядке.


Проблема 4: синхронизация локального maven репозитория


Мейвен всегда делает сборки исходя из артефактов в локальном репозитории. Периодически он может синхронизировать локальный репозиторий и скачивать обновления из глобального репозитория.


Так может возникнуть ситуация, когда вы собрали ваши изменения и больше не меняете файлы. Но рано или поздно ваша локальная сборка будет вытеснена внешними изменениями. Одним из решений является проверка даты изменений не файла в target, а файла в локальном репозитории. И если они отличаются, то нужно делать новый билд.


Однако, уже собранные файлы в target остаются корректными и их оттуда никто не выгонит. И чтобы не делать двойную работу, можно их просто закидывать в локальный репозиторий, вместо того, чтобы делать полную сборку.


Проблема 5: многословный maven


Это не совсем проблема, но лично для меня всегда вопрос, зачем maven выдает столько логов. Разве что сидеть и медитировать, смотря на пробегающие строчки. Но если всё идет хорошо, мне эти логи нисколько не интересны. Если всё прошло плохо, то логи действительно полезны. Именно по такому принципу я и вывожу логи.


Как приятный бонус к чистому output'у — существенное повышение скорости сборки, т.к. IO далеко не самые дешевые операции.


Проблема 6: медленный maven против быстрых workaround'ов


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


Поэтому все проблемы решаются в меру сил и возможностей собственными силами, и мэйвен используется только один раз — для запуска самой сборки.


Однако все попытки обхода мейвена не полноценны и могут не учитывать ряда дополнительных факторов. Их хватает мне на текущей конфигурации проекта, но какие-нибудь более замудрённые конфигурации не учитываются.


Бонус 1: скрипт для билд-сервера


Всё что описано выше, было написано и используется для локальной сборки, когда мы делаем изменения сами.


Но после удачного опыта использования, я решил, что этот подход можно использовать и на нашем continuous integration сервере и собирать только то, что изменилось между ревизиями. Разумеется, при этом необходимо учитывать большинство проблем, описанных для локальной сборки. Так что оба варианта скрипта идут рука об руку.


Важно, чтобы такая инкрементальная сборка давала абсолютно такой же результат, как и полная. Этого можно легко добиться, используя тот самый maven параметр -amd, описанный выше. Т.е. собирается то, что изменено и/или задето.


Касаемо полученного выигрыша — в нашем случае, среднее время инкрементальной сборки в 10 раз меньше.


На TeamCity такая возможность есть из коробки. На Bamboo нет. Про другие CI не знаю


Бонус 2: мини-минификатор python проектов


Данный раздел не имеет ничего общего с maven или java, и может быть интересен только python'истам
Скрипт, который я тут описываю, пишется как проект с несколькими модулями, т.к. мне так проще работать с кодом.


Но делиться скриптом, который содержит несколько файлов это не очень удобно.


Как вариант, можно из скриптов собирать единый бинарник и выдавать его. Но в таком случае теряется возможность фиксов на лету и понимания, что там происходит.


Поэтому на скорую руку был написан ещё один скрипт, который принимает на вход python файлик, схлопывает его и все зависимые модули. На выходе получается единый файл. Заодно удаляются ненужные пустые строки.


Эпилог


Скрипт, который я тут описываю, используется мной и несколькими коллегами уже год. Скрипт для CI сейчас проходит тестовую эксплуатацию.


Производительность скрипта вполне сносная: его анализ и оптимизации я не замечаю (т.е. они меньше секунды) в рамках наших проектов.


Пока что это работает только с svn, т.к. потребности собирать в других VCS нет. Но по-желанию, можно легко добавить и другие. Благо от VCS там требуется не так много.


> Ссылка на проект


Я буду крайне рад, если мой подход и скрипт позволит ещё кому-то экономить свое время и силы на сборку с мэйвеном


Вопросы, исправления и дополнения крайне приветствуются.

Поделиться с друзьями
-->

Комментарии (12)


  1. m1ld
    07.03.2017 11:32

    У вашего решения есть один недостаток на который вы же и указали – зависимости между модулями. После сборки измененных модулей нужно также проверить сборку и всех зависимых модулей.
    По моему мнению лучше не пытаться использовать один vcs репозиторий для всех модулей.
    В идеале: один репозиторий – один модуль.

    Любой билд сервер в наше время отследит изменение в модуле и соберет его. Если этим же билд сервером собираются другие зависимые модули, то они также будут им собраны и протестированы, и так до последней связи. Если есть зависимости не между maven проектами, то можно настроить триггеры сборки для них.

    PS: идеальный билд сервер может собирать только изменившиеся модули в одном vcs репозитории, хотя это и не всегда возможно.


    1. bugy
      07.03.2017 11:42

      Спасибо за комментарий.
      В целом, предложенное решение в первую очередь полезно для локальной сборки. В локальной сборке этим недостатком можно пренебречь.
      Адаптация для билд-сервера это просто приятный бонус. Если этот самый билд-сервер не настолько умный. Если все зависимые модули лежат в одном большом проекте, то этого недостатка в предложенном подходе не будет.


      По моему мнению лучше не пытаться использовать один vcs репозиторий для всех модулей.
      В идеале: один репозиторий – один модуль.
      Отчасти согласен. С точки зрения идеальной организации кода это правильно. Но если у вас сотни или тысячи модулей, то порой гораздо удобнее их хранить вместе. В том числе для работы в IDE.


    1. sndl
      07.03.2017 13:33

      В идеале: один репозиторий – один модуль.


      Не совсем согласен, от модулей маленького размера можно получить много преимуществ и не придется создавать сотни репозиториев.

      С описанным в статье нам в компании очень помогает Pants, там эта функциональность идет из коробки

      ./pants changed --include-dependees=transitive --changes-since=f61ad76d5d37d8dae6f1c298e1365af423d9892e
      

      Вот такой командой можно например получить список всех модулей, которые затронули изменения после определенного git коммита.

      Дальше для этих модулей мы можем выполнять тесты, собирать артефакты, если требуется и так далее.

      В результате имеем преимущества одного репозитория и сборка не страдает.


      1. bugy
        07.03.2017 13:48

        @sndl, я правильно понимаю, что Pants это не дополнение к maven, а альтернатива? Т.е. если кто-то уже на maven, глубоко и серьезно, то Pants не поможет?


        Так-то есть gradle, который инкрементальный из коробки (помимо других преимуществ перед maven). Но смена билд системы это не всегда легко и просто


        1. sndl
          07.03.2017 13:55

          Да, это альтернатива, не дополнение.

          Даже если в Gradle решаются проблемы с вычленением измененных модулей, он все равно другой по архитектуре. Там не поощряются (как минимум раньше) модули маленького (пакетного) размера.

          Bazel — в свою очередь сравнимое с Pants


        1. olegchir
          08.03.2017 15:24

          почему-то от общения с градлями у меня осталось ощущение, что несмотря на инкрементальность, они гораздо более тормозные сами по себе


          1. bugy
            09.03.2017 00:50

            Интересно. У меня на предыдущем проекте к гредлу не было совсем никаких вопросов в плане производительности.
            Однако сейчас делаю мини-проект на андроид и гредл действительно долго собирает. Но сдается мне, что там слишком много лишнего от самого андроида.


  1. hibissscus
    07.03.2017 13:42

    Базель вам в помощь https://bazel.build/ он сам определяет измененные зависимости и пересобирает только задетые части. ну или есть очень хорошая вещь под названием http://hotswapagent.org/ агент для IDEA на базе https://dcevm.github.io/ Happy Hot Swapping!


  1. primetalk
    07.03.2017 13:58

    В нашем проекте мы схожую проблему решаем с помощью gradle. В перспективе можно будет отправить maven на заслуженный отдых (люди к maven привыкли/прикипели и не готовы сразу с ним расстаться).


    1. bugy
      07.03.2017 14:04

      Могу вам только позавидовать :) Gradle эту проблему действительно решает очень хорошо.
      К сожалению, в моем случае перейти на gradle не так просто, но "мы работаем над этим..."


  1. Zapped
    07.03.2017 17:27

    но в данном случае приходится работать с тем, что уже есть и менять ничего в проекте для этого нельзя.

    а почему нельзя-то?


    1. bugy
      07.03.2017 19:36

      На текущий момент слишком много заточено на мэйвен. Просто так взять и перейти на что-то более современное будет слишком трудозатратно и не будет иметь много смысла (ведь всё и сейчас работает).
      В стратегическом плане, конечно, рано или поздно перейдем, я думаю, но до этого момента нужно ещё как-то дожить.