Что такое кросс-компиляция? Какие есть инструменты для сборки бинарных файлов для Windows в Linux? Как настроить docker-контейнер для всего этого? Вот лишь небольшая часть вопросов, которые будут обсуждаться ниже.


Инструменты


Кросс-компиляция позволяет получить исполняемый код для платформы, отличной от той, на которой запускается этот процесс.


В рамках данной статьи рассмотрим кросс-компиляцию для платформы Windows в Linux.


Примером кросс-компилятора может служить Mingw-w64. По сути он предоствляет лишь инструмент для сборки приложения, но, если Вам необходимы сторонние библиотеки, которые не являются частью STL, придется собирать их и зависимости. Так же можно использовать готовые бинарные файлы, так как это описано в этой статье.


Упростить настройку сборки позволяет проект mxe, который предоставляет не только инструменты, но и библиотеки. С их списком можно ознакомиться на официальном сайте. При установке библиотек используется контроль зависимостей, т.е. будет установлен требуемый пакет и все необходимое для его работы. Инструменты поставляются в пред-настроенной конфигурации, например, для статической сборки 64-битных приложений. Это существенно облегчает сборку.


Среда 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=x86_64-w64-mingw32.static  qtbase  -j4 JOBS=4

Данная команда добавит инструменты (экземпляры cmake и Mingw-w64 и пр.) для статической сборки проекта под 64-битную архитектуру, а после соберет с их помощью Qt.


Следующим шагом добавим в PATH путь к исполняемым файлам mxe:


ENV PATH="/cross/mxe/usr/bin:${PATH}"

После того, как среда сборки настроена, можно перейти непосредственно к последнему пункту:


ENTRYPOINT x86_64-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=x86_64-w64-mingw32.static MXE_PLUGIN_DIRS=plugins/gcc7 qtbase  -j4 JOBS=4

Данная команда создаст комплект для статической сборки 64-битных приложений с использованием компилятора седьмой версии (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.<plugin> variables.
#include <QtPlugin>
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 и через некоторое время получаю бинарные файлы для требуемых ОС в локальной папке.

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


  1. Punk_Joker
    24.06.2019 17:41

    На этот случай можно завести bash-скрипт, который будет разворачивать всю среду в заданной папке. И после пытаться поддерживать этот скрипт в актуальном состоянии. Но, как и документация к проекту, в один критически важный момент он может устареть.

    Хорошим решением будет изолировать нашу среду для сборки внутри docker-контейнера.

    Использование докера не отменяет возможность устаревания. Докер не про автоматизацию, а про изоляцию и единство окружения. В докер файле придется писать все те же строки из bash скрипта.


    1. sqglobe Автор
      24.06.2019 18:21
      +1

      Необходимо сделать пояснение о назначении bash-скрипта в примере. Он разворачивает среду для сборки приложения, ставит нужные библиотеки и пр. Далее, в процессе разработки, программист устанавливает необходимые компоненты в уже развернутой среде и добавляет команды в указанный bash-скрипт. Забытая зависимость всплывет при повторном разворачивании всей среды.


      docker-файл содержит аналогичные команды скрипту bash, но в отличии от него используется намного чаще, как минимум для сборки каждого релиза. Соответственно, пропущенная зависимость будет выявлена гораздо быстрее в этом случае.


      1. Punk_Joker
        24.06.2019 18:38

        Почему чаще? Допустим в одной из компаний, где я работал, докер не использовался. Но процесс сборки производился на выделенном сервере с использованием скриптов. И как результат, сборка была повторяема и одна для всех без использования докера.


        1. sqglobe Автор
          24.06.2019 19:36

          Почему чаще?

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


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


          Но процесс сборки производился на выделенном сервере с использованием скриптов.

          А как это выглядело? Скрипт устанавливает необходимые библиотеки или просто запускает сборку? Просто у нас в компании так же используется сборка на выделенном сервере, но make-скрипты просто запускают саму сборку. Предполагается, что все зависимости уже были установлены заранее.


          1. Punk_Joker
            25.06.2019 10:31

            Аналогично. Только сборка. Я просто хотел сказать, что докер никак не решает вопрос автоматизации и поддержания актуальности. Разработчик по прежнему может вносить правки не в скрипт а в докер файл, и история повторяется.


  1. gudvinr
    24.06.2019 19:34

    Зачем уж вы так сразу учите людей плохому тону, когда человечество вовсю бороздит просторы космоса переходит на 64-битную архитектуру везде где только можно, а вы тут Qt под 32 бита собираете.


    Потом на убунту половина интернета ругается, что они убирают 32-битные зависимости из репозиториев. А разруха не в дистрибутивах, она в головах.


    1. sqglobe Автор
      25.06.2019 10:57

      Вы совершенно правы. В туториале лучше убрать упоминание 32-битной архитектуры.


      Однако, хочу отметить, что сборка приложения под такую архитектуру делает возможным запуск продукта на большем количестве ПК.


      1. gudvinr
        25.06.2019 12:13

        Насколько большем? А то это как вилами по воде.
        Сейчас новые устройства (с Windows в частности, а Linux/Mac и подавно) с 32 битными ОС найти очень сложно, а процессоры без поддержки 64 бит и подавно. Даже Android/iOS последние годы имеют тенденцию перехода на ARM64.


        Если и найти, то будут ли адекватно и с достаточным уровнем оптимизации работать в такой среде современные приложения — тоже вопрос, конечно. Это не значит, что современные приложения пишутся абы как (что порой верно), просто наборы 64-битных инструкций дают больший простор для оптимизации даже на этапе компиляции. Разработчики крупных пакетов программ тоже постепенно отказываются от поддержки.


        Не спорю конечно, есть тонкости, есть легаси которое переписать нельзя/сложно, есть конторы (преимущественно государственные), где целевые машины на 32 битных виндах, но в таких случаях и с докером проблемы вероятно будут.


        Суть в том, что новые приложения так точно писать не стоит, потому что зачем? Достаточный уровень проверки обеспечить как минимум трудно ввиду того что систему такую найти сложно и нужно в 2 раза больше времени на это потратить, а целевая аудитория исчезающе мала зачастую.


  1. iig
    24.06.2019 22:35

    Для поддержания актуальности скриптов хорошо использовать git/hg/… Для изоляции достаточно chroot.


    1. eri
      27.06.2019 20:15

      python3 собранный в chroot без костылей — не работает часть модулей связанная с криптографией и потоками. devfs нужна для нормальной сборки.


  1. AntonSazonov
    25.06.2019 12:07

    MinGW — это компилятор который создаёт код для платформы Windows и который работает под управлением Windows. Так что это плохой пример кросс-компилятора.


    1. eri
      25.06.2019 13:04

      1. AntonSazonov
        25.06.2019 13:24

        Сам лично учавствовал в проекте mingw-builds под руководством товарища niXman. Это набор скриптов который собирал компилятор MinGW в ОС Windows.
        То, о чём пишете вы и автор статьи называется MinGW Cross-compiler, по сути, это просто GCC Cross-compiler, только target у первого всегда mingw32.
        Сама расшифровка абривиатуры MinGW говорит о том что он работает в ОС Windows.
        Название компилятора MinGW который хостится не на ОС Windows — некорректно. Опять же, это просто GCC Cross-compiler.


        1. eri
          27.06.2019 20:11

          о чём пишете вы и автор статьи называется MinGW Cross-compiler


          плохой пример кросс-компилятора.


          не противоречите сами себе?


    1. sqglobe Автор
      25.06.2019 13:24

      В статье была допущена ошибка в имени кросс-компилятора. Правильное название — mingw-w64


      1. AntonSazonov
        25.06.2019 13:30

        Ошибка не в этом. Прочтите комментарий чуть выше.
        По сути вашей ошибки тут нет, во всём виновата путаница в названиях.


  1. DirectoriX
    25.06.2019 12:48

    Делал аналогичную штуку, только при запуске контейнера автоматически срабатывает qmake && make -j $(nproc)
    Ссылка на GitHub