Важнейшее руководство по Qmake

Опубликовано: 2022-03-11

Введение

qmake — это системный инструмент сборки, поставляемый с библиотекой Qt, который упрощает процесс сборки на разных платформах. В отличие от CMake и Qbs , qmake был частью Qt с самого начала и должен рассматриваться как «родной» инструмент. Излишне говорить, что IDE Qt по умолчанию — Qt Creator — имеет наилучшую поддержку qmake из коробки. Да, вы также можете выбрать системы сборки CMake и Qbs для нового проекта, но они не так хорошо интегрированы. Вполне вероятно, что поддержка CMake в Qt Creator со временем будет улучшена, и это станет хорошей причиной для выпуска второго издания этого руководства, ориентированного именно на CMake. Даже если вы не собираетесь использовать Qt Creator, вы все равно можете рассматривать qmake как вторую систему сборки, если вы создаете общедоступные библиотеки или плагины. Практически все сторонние библиотеки или плагины на основе Qt предоставляют файлы qmake, используемые для бесшовной интеграции в проекты на основе qmake. Лишь немногие из них обеспечивают двойную конфигурацию, например, qmake и CMake. Вы можете предпочесть использовать qmake, если к вам относится следующее:

  • Вы создаете кроссплатформенный проект на основе Qt
  • Вы используете Qt Creator IDE и большинство ее функций
  • Вы создаете автономную библиотеку/плагин для использования в других проектах qmake.

В этом руководстве описываются наиболее полезные функции qmake и приводятся реальные примеры для каждой из них. Читатели, плохо знакомые с Qt, могут использовать это руководство в качестве руководства по системе сборки Qt. Разработчики Qt могут относиться к этому как к поваренной книге при запуске нового проекта или могут выборочно применять некоторые функции к любому из существующих проектов с небольшим воздействием.

Иллюстрация процесса сборки qmake

Основное использование Qmake

Спецификация qmake записана в файлах .pro («проект»). Это пример самого простого файла .pro :

 SOURCES = hello.cpp

По умолчанию это создаст Makefile , который создаст исполняемый файл из единственного файла исходного кода hello.cpp .

Чтобы собрать двоичный файл (в данном случае исполняемый), вам нужно сначала запустить qmake для создания файла Makefile, а затем make (или nmake , или mingw32-make в зависимости от вашей цепочки инструментов) для сборки цели.

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

Понимание синтаксиса переменных

Изучая существующие проекты qmake, вы можете быть удивлены тем, как можно ссылаться на разные переменные: \(VAR,\){VAR} или $$(VAR) ...

Используйте эту мини-шпаргалку при принятии правил:

  • VAR = value Присвоить значение VAR
  • VAR += value Добавить значение в список VAR
  • VAR -= value Удалить значение из списка VAR
  • $$VAR или $${VAR} Получает значение VAR во время работы qmake.
  • $(VAR) Содержимое переменной среды во время работы Makefile (не qmake).
  • $$(VAR) Содержимое переменной среды во время работы qmake (не Makefile).

Общие шаблоны

Полный список переменных qmake можно найти в спецификации: http://doc.qt.io/qt-5/qmake-variable-reference.html.

Давайте рассмотрим несколько общих шаблонов для проектов:

 # Windows application TEMPLATE = app CONFIG += windows # Shared library (.so or .dll) TEMPLATE = lib CONFIG += shared # Static library (.a or .lib) TEMPLATE = lib CONFIG += static # Console application TEMPLATE = app CONFIG += console

Просто добавьте SOURCES += … и HEADERS += … , чтобы получить список всех ваших файлов с исходным кодом, и все готово.

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

Подпроекты

Наиболее распространенным вариантом использования является приложение, которое поставляется с одной или несколькими библиотеками и тестовыми проектами. Рассмотрим следующую структуру:

 /project ../library ..../include ../library-tests ../application

Очевидно, мы хотим иметь возможность построить все сразу, например:

 cd project qmake && make

Для достижения этой цели нам нужен файл проекта qmake в папке /project :

 TEMPLATE = subdirs SUBDIRS = library library-tests application library-tests.depends = library application.depends = library

ПРИМЕЧАНИЕ. Использование CONFIG += ordered считается плохой практикой — вместо этого лучше использовать .depends .

Эта спецификация предписывает qmake сначала собрать подпроект библиотеки, потому что от него зависят другие цели. Затем он может создавать library-tests и приложение в произвольном порядке, потому что они зависят друг от друга.

Структура каталогов проекта

Связывание библиотек

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

  1. Укажите -I , чтобы указать пути поиска для директив #include.
  2. Укажите -L , чтобы указать пути поиска для компоновщика.
  3. Укажите -l , чтобы указать, какую библиотеку необходимо связать.

Поскольку мы хотим, чтобы все подпроекты можно было перемещать, мы не можем использовать абсолютные или относительные пути. Например, мы не будем делать так: INCLUDEPATH += ../library/include и, конечно же, мы не можем ссылаться на бинарный файл библиотеки (файл .a) из временной папки сборки. Следуя принципу «разделения ответственности», мы можем быстро понять, что файл проекта приложения должен абстрагироваться от деталей библиотеки. Вместо этого библиотека несет ответственность за то, чтобы указать, где найти заголовочные файлы и т. д.

Давайте воспользуемся директивой qmake include() для решения этой проблемы. В проекте библиотеки мы добавим еще одну спецификацию qmake в новый файл с расширением .pri (расширение может быть любым, но здесь i означает include). Итак, у библиотеки будет две спецификации: library.pro и library.pri . Первый используется для создания библиотеки, второй используется для предоставления всех деталей, необходимых для потребляющего проекта.

Содержимое файла library.pri будет следующим:

 LIBTARGET = library BASEDIR = $${PWD} INCLUDEPATH *= $${BASEDIR}/include LIBS += -L$${DESTDIR} -llibrary

BASEDIR указывает папку проекта библиотеки (точнее, расположение текущего файла спецификации qmake, в нашем случае это library.pri ). Как вы могли догадаться, INCLUDEPATH будет оцениваться как /project/library/include . DESTDIR — это каталог, в который система сборки помещает выходные артефакты, такие как (файлы .o .a .so .dll или .exe). Обычно это настраивается в вашей среде IDE, поэтому вам никогда не следует делать никаких предположений о том, где находятся выходные файлы.

В файле application.pro просто добавьте include(../library/library.pri) , и все готово.

Давайте рассмотрим, как строится проект приложения в этом случае:

  1. Topmost project.pro — это проект поддиректоров. Это говорит нам о том, что проект библиотеки должен быть собран в первую очередь. Итак, qmake входит в папку с библиотекой и строит ее с помощью library.pro . На этом этапе создается library.a , которая помещается в папку DESTDIR .
  2. Затем qmake входит в подпапку приложения и анализирует файл application.pro . Он находит директиву include(../library/library.pri) , которая указывает qmake немедленно прочитать и интерпретировать ее. Это добавляет новые определения к переменным INCLUDEPATH и LIBS , так что теперь компилятор и компоновщик знают, где искать включаемые файлы, двоичные файлы библиотеки и какую библиотеку связывать.

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

С помощью этой настройки вы можете легко переместить проект библиотеки в другой проект qmake и включить его, тем самым ссылаясь на файл .pri . Именно так сторонние библиотеки распространяются сообществом.

config.pri

Для сложного проекта очень характерно наличие некоторых общих параметров конфигурации, которые используются многими подпроектами. Чтобы избежать дублирования, вы можете снова использовать директиву include() и создать config.pri в папке верхнего уровня. У вас также могут быть общие «утилиты» qmake, используемые совместно с вашими подпроектами, подобно тому, что мы обсудим далее в этом руководстве.

Копирование артефактов в DESTDIR

Часто в проектах есть какие-то «другие» файлы, которые нужно распространять вместе с библиотекой или приложением. Нам просто нужно иметь возможность копировать все такие файлы в DESTDIR в процессе сборки. Рассмотрим следующий фрагмент:

 defineTest(copyToDestDir) { files = $$1 for(FILE, files) { DDIR = $$DESTDIR FILE = $$absolute_path($$FILE) # Replace slashes in paths with backslashes for Windows win32:FILE ~= s,/,\\,g win32:DDIR ~= s,/,\\,g QMAKE_POST_LINK += $$QMAKE_COPY $$quote($$FILE) $$quote($$DDIR) $$escape_expand(\\n\\t) } export(QMAKE_POST_LINK) }

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

Поместите этот код в /project/copyToDestDir.pri , чтобы вы могли include() его в требующие подпроекты следующим образом:

 include(../copyToDestDir.pri) MYFILES += \ parameters.conf \ testdata.db ## this is copying all files listed in MYFILES variable copyToDestDir($$MYFILES) ## this is copying a single file, a required DLL in this example copyToDestDir($${3RDPARTY}/openssl/bin/crypto.dll)

Примечание: DISTFILES был введен для той же цели, но работает только в Unix.

Генерация кода

Отличным примером генерации кода в качестве предварительно созданного шага является случай, когда проект C++ использует Google protobuf. Давайте посмотрим, как мы можем protoc выполнение протокола в процесс сборки.

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

 A.proto <= B.proto

Если бы мы сначала сгенерировали код для A.proto (для создания A.pb.h и A.pb.cxx ) и передали его компилятору, он просто потерпит неудачу, потому что зависимость B.pb.h еще не существует. Чтобы решить эту проблему, нам нужно пройти все этапы генерации прототипа кода, прежде чем создавать результирующий исходный код.

Я нашел отличный фрагмент для этой задачи здесь: https://github.com/jmesmon/qmake-protobuf-example/blob/master/protobuf.pri

Это довольно большой скрипт, но вы уже должны знать, как его использовать:

 PROTOS = A.proto B.proto include(protobuf.pri)

При просмотре protobuf.pri вы можете заметить общий шаблон, который можно легко применить к любой пользовательской компиляции или генерации кода:

 my_custom_compiler.name = my custom compiler name my_custom_compiler.input = input variable (list) my_custom_compiler.output = output file path + pattern my_custom_compiler.commands = custom compilation command my_custom_compiler.variable_out = output variable (list) QMAKE_EXTRA_COMPILERS += my_custom_compiler

Объемы и условия

Часто нам нужно определить объявления специально для данной платформы, такой как Windows или MacOS. Qmake предлагает три предопределенных индикатора платформы: win32, macx и unix. Вот синтаксис:

 win32 { # add Windows application icon, not applicable to unix/macx platform RC_ICONS += icon.ico }

Области могут быть вложенными, могут использовать операторы ! , | и даже подстановочные знаки:

 macx:debug { # include only on Mac and only for debug build HEADERS += debugging.h } win32|macx { HEADERS += windows_or_macx.h } win32-msvc* { # same as win32-msvc|win32-mscv.net }

Примечание. Unix определяется в Mac OS! Если вы хотите протестировать Mac OS (не универсальный Unix), используйте условие unix:!macx .

В Qt Creator условия области debug и release не работают должным образом. Чтобы они работали правильно, используйте следующий шаблон:

 CONFIG(debug, debug|release) { LIBS += ... } CONFIG(release, debug|release) { LIBS += ... }

Полезные функции

Qmake имеет ряд встроенных функций, которые добавляют больше автоматизации.

Первый пример — функция files() . Предполагая, что у вас есть шаг генерации кода, который создает переменное количество исходных файлов. Вот как вы можете включить их все в SOURCES :

 SOURCES += $$files(generated/*.c)

Это найдет все файлы с расширением .c в generated и добавит их в переменную SOURCES .

Второй пример аналогичен предыдущему, но теперь при генерации кода создается текстовый файл, содержащий имена выходных файлов (список файлов):

 SOURCES += $$cat(generated/filelist, lines)

Это просто прочитает содержимое файла и обработает каждую строку как запись для SOURCES .

Примечание. Полный список встроенных функций можно найти здесь: http://doc.qt.io/qt-5/qmake-function-reference.html.

Обработка предупреждений как ошибок

В следующем фрагменте используется функция условной области, описанная ранее:

 *g++*: QMAKE_CXXFLAGS += -Werror *msvc*: QMAKE_CXXFLAGS += /WX

Причина этой сложности в том, что у MSVC есть другой флаг для включения этой опции.

Создание версии Git

Следующий фрагмент полезен, когда вам нужно создать определение препроцессора, содержащее текущую версию ПО, полученную из Git:

 DEFINES += SW_VERSION=\\\"$$system(git describe --always --abbrev=0)\\\"

Это работает на любой платформе, пока доступна команда git . Если вы используете теги Git, то будет отображаться самый последний тег, даже если ветвь была продолжена. Измените команду git describe , чтобы получить вывод по вашему выбору.

Заключение

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

Хотите узнать, как сделать ваше приложение Qt лучше? Попробуйте: Как получить формы с закругленными углами в C++ с помощью кривых Безье и QPainter: пошаговое руководство