Кросс-компиляция в docker. Почему бы и нет?
Что такое кросс-компиляция? Какие есть инструменты для сборки бинарных файлов для Windows в Linux? Как настроить docker-контейнер для всего этого? Вот лишь небольшая часть вопросов, которые будут обсуждаться ниже.
Инструменты
Кросс-компиляция позволяет получить исполняемый код для платформы, отличной от той, на которой запускается этот процесс.
В рамках данной статьи рассмотрим кросс-компиляцию для платформы Windows в Linux.
Примером кросс-компилятора может служить MinGW. По сути он предоствляет лишь инструмент для сборки приложения, но, если Вам необходимы сторонние библиотеки, которые не являются частью STL, придется собирать их и зависимости. Так же можно использовать готовые бинарные файлы, так как это описано в этой статье.
Упростить настройку сборки позволяет проект mxe, который предоставляет не только инструменты, но и библиотеки. С их списком можно ознакомиться на официальном сайте. При установке библиотек используется контроль зависимостей, т.е. будет установлен требуемый пакет и все необходимое для его работы. Инструменты поставляются в пред-настроенной конфигурации, например, для статической сборки 32-битных приложений. Это существенно облегчает сборку.
Среда mxe разворачивается в локальной папке пользователя. Для этого достаточно установить зависимости через пакетный менеджер и клонировать репозиторий. В корне репозитория находится Makefile, который и выполняет установку заданных в целях библиотек, добавляет инструменты для сборки и т.д.
Важно отметить, что среда сборки локализуется в пределах своей папки, это позволяет настроить индивидульную среду под каждое приложение.
Контейнеризируй это
Допустим, что сборка релиза для Windows у нас настроена на локальной машине. Релизы выходят довольно часто, в некоторых версиях добавляются новые библиотеки, а, некоторые, например, удаляются. В один прекрасный день начальник требует скинуть сборку релиза на новичка. Как ему настроить свою среду сборки? Какие библиотеки нужно взять из репозитория mxe, а для каких выполнить сборку из исходников?
На этот случай можно завести bash-скрипт, который будет разворачивать всю среду в заданной папке. И после пытаться поддерживать этот скрипт в актуальном состоянии. Но, как и документация к проекту, в один критически важный момент он может устареть.
Хорошим решением будет изолировать нашу среду для сборки внутри docker-контейнера. Сам docker-файл будет содержать самодостаточный набор инструкций по развертыванию среды, а наличие контейнера позволит избежать захламления домашней системы лишними библиотеками.
Собираем все вместе
Для демонстрации возьмем простой Qt-проект — SimpleQtProject. Этот проект собирается утилитой qmake, и состоит из одной единственной формы. Сделано это, конечно же, для простоты. Так же в папке docker лежит файл для сборки контейнера.
Рассмотрим docker-файл проекта. По сути он состоит из нескольких основных частей:
- установка зависимостей для системы сборки
- установка и настройка системы сборки
- компиляция проекта и копирование артефактов на хост-систему
Ниже рассмотрены лишь основные команды из файла, для полного ознакомления рекомендуется обратиться к репозиторию.
Пропустим первый пункт и перейдем непосредственно к установке mxe.
Клонируем репозиторий:
RUN mkdir /cross \
&& cd /cross \
&& git clone https://github.com/mxe/mxe.git \
&& cd mxe \
&& git checkout build-2019-06-02
На момент написания статьи последним релизом был build-2019–06–02. Здесь не используется ветка мастер по простой причине: необходима повторяемость сборки. В противном случае, при добавлении новых коммитов в мастер сборка может сломаться.
Далее настраиваем систему сборки:
RUN make MXE_TARGETS=i686-w64-mingw32.static qtbase -j4 JOBS=4
Данная команда добавит инструменты (экземпляры cmake и MinGW и пр.) для статической сборки проекта под 32-битную архитектуру, а после соберет с их помощью Qt.
Следующим шагом добавим в PATH путь к исполняемым файлам mxe:
ENV PATH="/cross/mxe/usr/bin:${PATH}"
После того, как среда сборки настроена, можно перейти непосредственно к последнему пункту:
ENTRYPOINT i686-w64-mingw32.static-qmake-qt5 /app/src/SimpleQtProject.pro \
&& make release \
&& cp release/SimpleQtProject.exe /app/res/
Здесь следует пояснить несколько моментов. Предполагается, что при запуске контейнера, в папку /app/src/ будет смонтирована папка с исходниками, содержащая *.pro файл, а в директории /app/res/ смонтировано место, куда следует сложить результаты сборки.
Ниже приведен пример команды для создания docker-image, ее необходимо запускать в папке docker рассматриваемого проекта:
docker build -t simple-qt-build --file windows.docker .
Сборка же запускается там же:
docker run --mount type=bind,source=$(pwd)/result/,target=/app/res --mount type=bind,source=$(pwd)/../,target=/app/src simple-qt-build
Перед выполнение команды необходимо создать папку result в директории docker для копирования результатов.
Кастомизация сборки
По умолчанию mxe предоставляет MinGW версии 5.5.0 (по крайней мере это справедливо для сборки build-2019–06–02).
Если в проекте используются новые фичи С++ 17, то такая версия компилятора неудовлетворительна. К счастью, среда сборки предоставляет более новые версии в виде отдельных плагинов. Для решения нашей задачи, необходимо в команду сборки библиотек добавить инструкцию по использованию соответствующего плагина:
make MXE_TARGETS=i686-w64-mingw32.static MXE_PLUGIN_DIRS=plugins/gcc7 qtbase -j4 JOBS=4
Данная команда создаст комплект для статической сборки 32-битных приложений с использованием компилятора седьмой версии (7.4.0). Если такой комплект уже существует, то он изменен не будет.
Со списком всех доступных плагинов можно ознакомиться на странице.
В директории mxe/src содержатся *.mk файлы, которые описывают параметры сборки того или иного пакета. При необходимости можно внести требуемые коррективы в уже существующий пакет или добавить свой собственный. Структура файлов описана вот тут — https://github.com/mxe/mxe/wiki/Add-a-New-Package:-Full-Version.
Для копирования pзависимостей проект mxe предоставляет утилиту copydlldeps.sh. Но это не единственный полезный инструмент, с их олным списокм можно ознакомиться на странице.
CMake и статическая линковка Qt
Так сложилось, что в своем проекте я использовал Qt и систему сборки CMake. Когда было принято решение о сборке проекта для Windows, отличным решением выглядело собрать все используя статическую линковку, чтобы пользователям предоставлять один бинарь, без каких-либо зависимостей.
Разбирая целую гору ошибок линковщика, удалось выяснить, что такая сборка из коробки не работает, вообще нигде. Дело в том, что qmake при использовании статической линковки генерирует *.cpp-файл, в котором находятся инструкции по импортированию плагинов, примерно такого вида:
// This file is autogenerated by qmake. It imports static plugin classes for
// static plugins specified using QTPLUGIN and QT_PLUGIN_CLASS. variables.
#include
Q_IMPORT_PLUGIN(QWindowsVistaStylePlugin)
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin)
Q_IMPORT_PLUGIN(QGifPlugin)
Q_IMPORT_PLUGIN(QICOPlugin)
Q_IMPORT_PLUGIN(QJpegPlugin)
Так же добавляются флаги и библиотеки для этапа линковки в Makefile.
Можно попробовать поэкспериментировать вот с такой конструкцией в CMakeLists.txt:
foreach(plugin ${Qt5Gui_PLUGINS})
get_target_property(_loc ${plugin} LOCATION)
message("Plugin ${plugin} is at location ${_loc}")
set(plugin_libs ${plugin_libs} ${_loc})
endforeach()
А дальше добавить в target_link_libraries
использование plugin_libs
. Но для меня такой подход не принес никаких результатов.
В конце концов, я пришел к решению динамически линковать (по возможности) все внешние библиотеки и копировать вместе с исполняемым файлом еще и необходимые dll с помощью copydlldeps.sh. Более подробно про разворачивание под Qt под Windows описано в статье.
В заключении
Выше было показано, как за несколько простых шагов можно настроить среду для кросс-компиляции проекта. Но, к сожалению, в реальных условиях все не так радужно.
Хоть проект mxe предоставляет внушительный список библиотек, но все равно он может не включать тех, которые нужны именно Вам, или включать слишком новые версии. Да, есть возможность самому создать пакет или, на худой конец, собрать библиотеку из исходников. Но не все можно сбилдить кросс-компилятором, так мне не удалось сделать это с проектом cpprestsdk, потому что ему нужен установленный vcpkg.
Многие проблемы, которые могут появиться при сборке проекта кросс-компилятором, характерны для кросс-платформенной разработки вообще. Например, у меня была странная ошибка из-за элемента перечисления ERROR
. Оказалось, что в одном из заголовочных файлов Windows имеется макрос с таким же именем. Его подстановка и ломала весь код.
Это дело каждого — использовать кросс-компиляцию или нет. Мне это принесло неплохой профит. Я настроил сборку под несколько дистрибутивов Linux и Windows в отдельных docker-контейнерах для своего проекта SecureDialogues, добавил один make-файл для запуска процесса поочередно для каждого контейнера. Далее запускаю make и через некоторое время получаю бинарные файлы для требуемых ОС в локальной папке.