Сначала небольшая предыстория. В начале 2010-х, я сделал небольшую утилиту-конвертер для BIN файлов эмулятора БК-0010 в WAV файлы. Утилита была написана на Python с целью максимальной переносимости, работала без проблем и я на какое то время забыл о ней. Но в 2016м появился пользователь "неИТшник", понятия не имеющий про Python и как его устанавливать. Он хотел простой исполняемый файл-монолит, который "просто бы работал". Мне его просьба показалась логичной и я решил переработать утилиту в виде набора бинарных исполняемых файлов для основных платформ.
Python и Java не давали такую возможность (если конечно не было желания раздуть утилиту на много десятков мегабайт). Потенциально решение можно было сделать на C/C++, но при таком целевом охвате платформ, сложности с кросс-компиляцией выходили бы за рамки отведенного на задачу времени (а мне надо было поддерживать кросс-сборку для Windows, Linux и MacOS в 64 и 32 битных вариантах). Так что я обратил внимание на набирающий популярность язык Go, который к тому времени уже стал достаточно зрелым и единственным, кто без "плясок с бубном" обеспечивает всю требуемую кросс-компиляцию прямо из коробки (!).
Одно неудобство, которое меня сильно раздражало в Go (особенно как Java разработчика) — слабая продуманность организации структуры Go проектов и потребность в постоянной настройке параметров среды. Может быть разработчики Go изначально планировали какое-то специфическое использование или была ориентация на контейнеры, но я привык к "вкусненькому" и так как моим основным инструментом в Java является Maven, то решил сделать плагин осуществляющий вызовы к утилитам и командам GoSDK с автоматическим формированием нужных переменных окружения.
Делать просто плагин, который вызывал бы go утилиту, было неинтересно и я решил пойти с шага — установки GoSDK на хост-платформу. Сразу после старта, проверяется наличие GoSDK в своей конфигурационной директории (я выбрал путь по умолчанию ~/.mvnGoLang
) и при отсутствии требуемой версии, производится автоматическая загрузка и распаковка архива GoSDK с официального сайта. Это позволило делать переносимые Go проекты, не заботясь о том, предустановлен и сконфигурирован ли инструмент нужной версии. Конечно, многочисленными настройками я предусмотрел возможность использования предустановленной версии и добавил настройки для всех шагов в процессе, так как для многих критичны такие вопросы как например отключение проверки SSL сертификатов или вообще осуществление HTTP запросов вовне (как например для проекта Keycloak).
Следующим этапом, я обернул в Maven-задачи (goals) все предоставляемые на тот момент команды утилиты go. Так как была вероятность, что с новой версией GoSDK появится еще какая-то новая команда, то была добавлена задача custom
позволяющая пользователю определять нужную команду самостоятельно. По умолчанию, в рамках maven-фаз (phases), задачи исполняются в следующем порядке:
- clean как
default-clean
в фазe clean - fix как
default-fix
в фазе validate - get как
default-get
в фазе initialize - generate как
default-generate
в фазе generate-sources - fmt как
default-fmt
в фазе process-sources - test как
default-test
в фазе test - build как
default-build
в фазе package - в качестве
default-install
в фазе install выполняется внутренняя задача mvninstall - install как
default-deploy
в фазе deploy
Любой из базовых шагов может быть отключен переводом задачи в несуществующую фазу:
<execution>
<id>default-fix</id>
<phase>none</phase>
</execution>
Во внутренней реализации, задачи были разделены на "работающие с зависимостями" и "не требующие разрешения зависимостей", после появления поддержки режима модулей, большинство перекочевало в "работающие с зависимостями".
Структуру Go-maven проекта, я постарался приблизить к стандартной принятой для Go структуре папок (т.е. /src
и /bin
в корне проекта). Но так как Maven заточен под Java, то напрямую не удалось "сломать" его подход к организации структуры проекта и сделать этот шаг невидимым для пользователя, поэтому базовая конфигурация плагина смотрится немного непривычно даже многим знакомым с maven:
<build>
```${basedir}/src</sourceDirectory>
<directory>${basedir}/bin</directory>
<plugins>
<plugin>
<groupId>com.igormaznitsa</groupId>
<artifactId>mvn-golang-wrapper</artifactId>
<version>2.3.3</version>
<extensions>true</extensions>
<configuration>
<goVersion>1.12.9</goVersion>
</configuration>
</plugin>
</plugins>
</build>
WARNING! По каким то причинам хабр может неправильно отображать при форматировании XML участок
<sourceDirectory>${basedir}/src</sourceDirectory>
Как видите, приходится напрямую определять папку с исходными текстами /src
и папку с результатом /bin
. Это с одной стороны минус, с другой стороны возможность к изменению их локации.
Целиком минималистичный pom.xml для одномодульного проекта а-ля Hello world, выглядит следующим образом:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.igormaznitsa</groupId>
<artifactId>mvn-golang-helloworld</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>mvn-golang</packaging>
<build>
```${basedir}/src</sourceDirectory>
<directory>${basedir}/bin</directory>
<plugins>
<plugin>
<groupId>com.igormaznitsa</groupId>
<artifactId>mvn-golang-wrapper</artifactId>
<version>2.3.3</version>
<extensions>true</extensions>
<configuration>
<goVersion>1.12.9</goVersion>
</configuration>
</plugin>
</plugins>
</build>
</project>
Обратите внимание, что packaging проекта обозначен как mvn-golang. Этой конфигурации достаточно, что бы на базе исходных текстов в папке src, построить исполняемый файл и положить его в результирующую папку bin. Go build cache будет так же создан в папке /bin
(как /bin/.goBuildCache
по умолчанию) и при clean будет стираться вместе с этой временной папкой.
В фазе install вызывается внутренняя задача mvninstall
, которая просто пакует весь проект в zip архив и размещает его в maven-репозитории как сгенерированный артефакт. Изначально я просто складировал эти артефакты, но с версии 2.3.2 был добавлен механизм для поддержки их как стандартных maven-зависимостей и появилась возможность разработки проектов с "расшариванием" общего кода через maven репозиторий. Понятно, что класть в репозиторий сгенерированные бинарные результаты как артефакты — плохая идея из-за требований по кросс-платформенности и поэтому содержимое папки /bin
в артефакт не пакуется.
Подключение другого проекта с packaging mvn-golang
в качестве maven-зависимости (dependency) выглядит примерно так:
<dependency>
<groupId>com.igormaznitsa</groupId>
<artifactId>mvn-go-test-mix-terminal</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>mvn-golang</type>
</dependency>
С 2.3.3 версии была добавлена поддержка работы с механизмом Go модулей, но по умолчанию она не активирована (для обратной совместимости) и включается при помощи конфигурационного параметра:
<moduleMode>true</moduleMode>
Когда механизм Go-модулей был еще на стадии экспериментов, я добавил поддержку работы с версиями зависимостей через прямые вызовы CVS утилит, был сделан тестовый пример. Но сейчас думаю, что такой механизм уже не представляет большого интереса и выгоднее пользоваться стандартными зависимостями через модули. Тем более, что плагин умеет препроцессировать go.mod
файлы на время построения проекта, подменяя пути к локальным папкам, если идет работа в рамках многомодульного проекта.
Так как Maven предусматривает возможность шаблонизации при помощи архетипов, то я сделал два архетипа:
- com.igormaznitsa:mvn-golang-hello:2.3.3 для простого одномодульного архетипа
- com.igormaznitsa:mvn-golang-hello-multi:2.3.3 для многомодульного проекта с разделяемым исходным кодом
Пример работы с архетипом одномодульного проекта, можно увидеть на анимации ниже
.
Так же можно просто клонировать "Hello World" проект и поиграться:
git clone https://github.com/raydac/mvn-golang-example.git
Нередко возникает резонный вопрос — а для чего всё это надо и какие бонусы дает использование Maven в процессе построения Go-проекта? Попробую ответить на него по пунктам:
- Maven это очень зрелая, кросс-платформенная среда построения проектов, поддерживаемая практически всеми CI платформами, например Jenkins, при помощи Go2XUnit можно конвертировать результаты тестов в формат отображаемый репортирующими плагинами.
- Maven кросс-платформенен и поддерживается всеми ОС, будучи включенным во все репозитории. Наличие гибко активируемых профилей, позволяет легко настраивать процесс под различные платформы.
- Maven имеет ОГРОМНОЕ количество плагинов, что позволяет легко получать синергетический эффект скажем скрещивая Go и GWT, подключая в проект ANTLR, генерируя Go на базе Protobuf дескрипторов и даже добавляя препроцессинг. Всё это можно организовать и вручную через командные файлы, но если есть официальные поддерживаемые плагины, то выгоднее использовать их.
- Проект становится легко переносимым между машинами и платформами, а переключение версии GoSDK производится изменением одной строки.
- Простота организации многомодульных проектов, с разделением исходного кода через Maven репозиторий.
- Инструмент знаком и привычен Java-разработчикам, что облегчает их адаптацию при переключении на разработку на Golang или при разработке мультиязыковых решений Go+Java.
- Создается возможность псевдо-разработки на Go в средах поддерживающих Maven, даже если для них отсутствует какая-либо поддержка этой платформы, например в NetBeans IDE.
Крупный недостаток решения, на мой взгляд, тут один — ограниченное количество разработчиков знакомых с Maven среди Golang-сообщества. Для перешедших на Golang с C/C++ понятно, что ближе и роднее make сложно что-то найти, так же никто не отменял "нативные" Go-билд системы. Я заметил, что по каким то своим причинам, многие разработчики не любят смешивать платформы.
Итак, я кратко показал один из путей использования Maven при разработке Go проектов с помощью mvn-golang-wrapper плагина. Проект плагина оформлен как OSS-проект и выложен на GitHub. Если кто то заинтересуется и будет использовать в своих проектах, то не стесняйтесь задавать вопросы и "репортить баги". Я постарался сделать набор примеров на разные случаи жизни (на которых плагин и тестирую), но всего не охватить.
Тестовые примеры, идущие в проекте плагина, используют dev-версию, так что если будет желание к их локальному построению, после клонирования проекта, то для этого требуется сначала произвести билд dev-версии плагина, при помощи команды в корневом каталоге проекта:
mvn install
после чего, можно заходить в любой подпроект mvn-golang-examples
и строить его при помощи
mvn clean install
так же можно запустить построение всех примеров из корня проекта, при помощи
mvn clean install -Pexamples
Плагин поддерживает многопоточную сборку проектов и её можно ускорить при помощи соответствующего аргумента, разбив например на 6 потоков
mvn clean install -Pexamples -T6
За время разработки, проект накопил приличное количество "наворотов", которые я решил не освещать в этой небольшой статье. Информацию о параметрах с небольшими примерами конфигурации можно найти в mind map данного плагина (исходный файл в формате SciaReto находится здесь):
Автор: raydac