Откуда ноги растут
Запрещенных веществ у нас не было (хотя в конце могут возникнуть сомнения). Было кое-что поинтереснее, если вы понимаете о чем я. Да, это легаси win-only Qt проект, начатый еще до начала времен под Qt 4.
Наша компания занимается разработкой и производством средств мониторинга и диагностики энергетического оборудования. В промышленности много старых проектов, тут они никого не пугают, особенно когда у вас аппаратно-программные комплексы. Но иногда приходится разгребать это, и в этот раз это досталось мне. А конкретно мне досталось эдакое сервисное ПО для нашего железа, которое бы работало с ним по разным протоколам.
Со временем захотелось упростить управление зависимостями, выкинуть велосипеды, потом целевой платформой стал и Linux, и в архитектуры добавился арм. Всё это играло в пользу cmake. К тому же cmake поддерживают самые прогрессивные IDE, такие как CLion и msvs, а qmake - QtCreator (KDevelop? нет, спасибо). Конечно еще есть make, autotools и msbuild но как-то хотелось иметь один проект под все платформы, а не вот это всё.
Немного про системы сборки
Со временем проекты становятся все больше и больше, собирать их всё сложнее, если совсем маленький проект из main.cpp мы можем собрать одной командой то, когда проект состоит из сотни файлов, у вас не хватит нервов, чтобы набирать команды постоянно. Системы сборки призваны упростить этот процесс, программист заранее описывает некий набор команд, который затем выполняются каждый раз для сборки проекта.
В действительности, система сборки - это некий набор скриптов, содержащих команды компилятору о том, как собирать наши таргеты. Он снимает эту нагрузку с программиста, и тем самым нам приходится писать короткие скрипты, которые система сборки уже преобразует в полные команды компилятору. Самые известные системы сборки это - make, autotools и ninja, но есть и множество других.
Если вы вдруг думали, что make это компилятор, то нет, это своего рода враппер над компилятором.
Но несмотря на то, что появились системы сборки, которые значительно упростили жизнь программистам, они остаются платформо-зависимы и по сей день. Дальше было два пути:
Сделать системы сборки платформо-независимы - сложно и тяжело (примерно, как сделать бинарник, запускаемый на *nix и windows).
Добавить уровень абстракций - проще
Некоторые упорно пошли по первому пути, ну а второй путь - появление мета систем сборки.
Теперь программисты пишут скрипты уже для мета систем сборки, а они, в свою очередь, генерируют скрипты для систем сборки. Т.е. еще один враппер, но теперь у нас один фронт-енд (мета система сборки) и множество бэков (просто систем сборки). Например, мы используем cmake как фронт. Для windows у нас бэком будет msbuild, который в свою очередь будет враппером над msvc. Для *nix у нас бэк - это make, который выступит враппером над gcc.
Не секрет что The Qt Company начиная с Qt версии 6 отказывается от QMake в пользу CMake для сборки самого Qt. Параллельно с этим Qbs был объявлен deprecated. Правда, надо отдать должное сообществу, Qbs до сих пор развивается. Но при чем тут Qbs то? Дело в том, что Qbs появился как замена qmake.
Хотели как лучше, а вышло как в том анекдоте...
У нас ведь есть основная система сборки - QMake и, казалось бы, с ней всё хорошо. Но давайте посмотрим на активность в репозитарии. К сожалению, посмотреть статистику по годам нельзя, но никто не мешает сделать это локально, получаем:
В 2020 было коммитов меньше чем в любой год до этого, а в 2021 и того меньше будет. Активность 2019 года связана с появлением Qt 6, а к возможностям самого qmake имеет опосредованное отношение. Если посмотреть на сами коммиты, то это в основном фиксы, а не добавление нового функционала. Таким образом можно предположить, что QMake поддерживается на остаточной основе и бурного развития не планируется.
Qmake хорош?
Только потому что QMake мета система сборки - она намного удобнее чем make или autotools. Но есть и другие немаловажные моменты. Условный hello world на любой написать несложно. Но вот чем дальше в лес... тем сложнее. Тут нам на руку играет такой фактор как популярность - проще найти ответ на любой вопрос на SO/в гугле. Глянем на результаты 2021 Annual C++ Developer Survey "Lite". А точнее нас интересует только один момент What build tools do you use? (Check all that apply).
Можно смело сказать, что QMake и в 2021 году входит в тройку самых популярных мета систем сборки (ninja, make не мета), а значит найти ответы на многие вопросы будет не так и сложно, хоть в документации и опущено множество моментов.
Почему многие все еще выбирают qmake?
Простота - кратно проще чем тот же пресловутый cmake
Документация - сильная сторона всех Qt проектов (хотя есть и исключения)
Большая база знаний - недокументированные аспекты qmake можно как минимум нагуглить.
Простота подключения Qt библиотек - долгие годы всё было вокруг qmake, поэтому в некоторых моментах qmake выигрывает у cmake сих пор (статическая сборка и плагины)
Идеально для небольшого Qt проекта, не так ли? Вот именно, поэтому qmake и по сей день является рабочим решением и его рано выкидывать на свалку истории.
Кратко
Я не призываю вас переходить здесь и сейчас на CMake, а QMake является более простой и понятной системой для начинающих (имхо), да и ее возможностей может хватать для многих проектов.
А что не так?
Идеологически qmake больше подходит для проектов, где один pro файл - одна цель, т.е. TEMPLATE = lib
или app
. Если же вдруг нам этого недостаточно, и нам захотелось использовать TEMPLATE = subdirs
, то надо быть готовым прыгать по разложенным специально для нас граблям (про грабли попозже). Конечно вы сможете заставить это всё работать, но чего это будет стоить...
Кроссплатформенность у нас есть и довольно неплохая, реализована через mkspecs (аналог cmake-toolchains). С кросс-компиляцией сильно хуже, у меня так и не получилось её завести как надо, возможно руки виноваты, но с cmake почему-то было не сильно сложно.
Добавим сюда весьма смутное будущее, или наоборот ясное, учитывая рассмотренное выше. Недостаточно для того, чтобы пойти налево? Тогда qmake вам подходит.
Согласно уже упомянутому выше Annual C++ Developer Survey - самая больная тема в C++ разработке это управление зависимостями. Так что этот момент нельзя игнорировать.
К этому мы отдельно вернемся позже. А сейчас скажем просто, что qmake не слишком в этом хорош или даже совсем ничего не умеет, если сторонняя библиотека не содержит pro файл.
Если по пунктам:
Сложность управления разделенными большими проектами
Отсутствие будущего
Сложности с кросс-компиляцией
Управление не Qt зависимостями
Cmake лучше?
Или те же грабли, только вид сбоку? Возьму на себя смелость найти ответ на этот вопрос.
Начнем с самого первого пункта, который нам не нравится в qmake - сложность управления большими проектами, разделенными на множество отдельных модулей. CMake спроектирован иначе, это плюс для больших проектов, но повышает сложность вхождения (настолько что им хоть детей пугать). Тут нет явного разделения на app, lib и subdirs. Всегда есть один корневой проект, а все остальные могут быть его подпроектами (add_subdirectory
), а могут и не быть. Т.е. subdirs по умолчанию включен, но может не использоваться.
Наш проект интересен и сложен тем, что у нас разные целевые ОС и архитектуры. Просто прикинем, что нам надо всё собрать под 4 разных варианта: Windows x86, Windows x86_64, Linux Debian amd64, Linux Debian armhf. По итогу имеем три архитектуры и две ОС. Ну да, по итогу еще немного отстрелянных ног и набитых шишек (бесценный опыт).
Если у вас вдруг возник вопрос, то да, мы тащим qt в embedded. Но в оправдание скажу, что это сильно сэкономило время разработки т.к. qt части не надо переписывать на чистые плюсы, а можно взять as is.
Мы не используем MinGW под Windows, только msvc; кросс-компилируем clang'ом, под amd64 на ci тоже собираем clang'ом а так можно использовать и gcc, но баг компилятора иногда заставляет перейти на другой. В случае cmake стоит сказать и про генераторы - везде используется Ninja но поддерживается и Visual Studio генератор как запасной вариант. Это важно т.к что работает для одного, иногда не работает для другого, дело даже не в том что один multi-config.
CMakeLists первоначально выглядел как-то не очень.
Звучит совсем плохо? Но qmake не позволяет нам выбрать генератор (систему сборки), поэтому под окошками придется страдать, используя JOM, а под никсами - make. За большие возможности приходится платить - так можно описать весь cmake одной фразой.
Будущее cmake? Это де-факто стандартная система сборки в C++, вряд ли этого мало.
Кросс-компиляция в cmake работает через cmake-toolchains, достаточно собрать правильно окружение, написать toolchain файл и всё это будет полностью прозрачно для файла проекта. Т.е. в самом файле проекта не надо ставить никакие условия и флажки отдельно для кросс компиляции. Особо рукастые кросс-компилируют под embedded, используя cmake и компиляторы не из большой тройки. Здесь всё ограничивается вашей фантазией (иногда и отсутствующим генератором).
Управление зависимостями или самое сложное. Cmake предоставляет на самом деле множество способов для этого. Настолько много, что можно встретить споры на тему, что же именно лучше использовать и почему. Cmake тут полностью следует идеалогии языка: одну задачу можно решить множеством способов.
Попробуем сравнить детально
Сложность управления разделенными большими проектами
Возьмем простой пример. У нас есть App1, App2 и lib1, lib2. Каждый App зависит от каждой lib. Если всё немного упросить, то мы получим файлы следующего содержания:
Сравните сами:
qmake, src/root.pro
TEMPLATE = subdirs
SUBDIRS = \
lib1 \ # relative paths
lib2 \
...
App1 \
App2
App1.depends = lib1 lib2 ...
App2.depends = lib1 lib2 ...
cmake, src/CMakeLists.txt
add_subdirectory(lib1)
add_subdirectory(lib2)
add_subdirectory(App1)
add_subdirectory(App2)
В обоих случаях мы перечисляем поддиректории для включения. Но дальше в qmake надо явно указать, что конечный исполняемый файл зависит от собираемой библиотеки. Иначе они будут собираться одновременно, и можно получить ошибки линковки при чистой сборке (считай UB). В cmake это решили иначе и намного лаконичнее, позже обратим на это внимание.
Library
Идем дальше, опишем сначала наши библиотеки. Для qmake прикручен велосипед, который обязывает нас в директории lib1 создавать библиотеку с точно таким же именем и именем файла но упрощает жизнь потом - уменьшает количество boilerplate кода (подробнее можно почитать тут https://quantron-systems.com/ru/article/6). Вообще-то странно что нам нужны какие-то велосипеды для такого маленького проекта, не так ли? Если у вас возник такой же вопрос, то может стоит переехать на cmake?
Самое интересное, что этот хак я так и не смог заставить работать под *nix. В конце концов плюнул и выкинул qmake просто.
qmake, src/lib1/lib1.pro
QT += core network xml
## укажем необходимые Qt компоненты
TARGET = lib1$${LIB_SUFFIX}
## укажем таргет
TEMPLATE = lib
## скажем, что мы собираем библиотеку
DEFINES += LIB1_LIBRARY
## добавить define, возможно где-то пригодится
include(lib1.pri)
## укажем .pri файл, содержащий перечисление исходников
qmake, src/lib1/lib1.pri
SOURCES += \
src.cpp \
...
HEADERS += \
hdr.h \
...
Разделение на pri и pro использовано специально, чтобы один файл содержал все директивы для библиотеки, а второй - только перечислял исходники и заголовки. Не имеет никакого значения, но мне было так удобнее ориентироваться.
cmake, src/lib1/CMakeLists.txt
project(gen LANGUAGES CXX)
## укажем проект и используемые языки
find_package(
QT NAMES Qt6 Qt5
COMPONENTS Core Network Xml
REQUIRED)
## укажем, что мы хотим найти пакет Qt6 или Qt5
find_package(
Qt${QT_VERSION_MAJOR}
COMPONENTS Core Network Xml
REQUIRED)
## укажем, что из найденного пакета нам нужны такие компоненты
add_library(
lib1 STATIC
hdr.h
...
src.cpp
...)
## укажем, что мы хотим собрать статическую библиотеку
target_link_libraries(
lib1
PRIVATE Qt${QT_VERSION_MAJOR}::Core
PRIVATE Qt${QT_VERSION_MAJOR}::Xml
PRIVATE Qt${QT_VERSION_MAJOR}::Network)
## и слинковать ее с такими-то библиотеками
target_compile_definitions(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_LIBRARY)
## также добавим макрос
Тут может показаться что cmake многословный и перегруженный, но директива target_link_libraries
позволяет указать какой тип связывания мы хотим, в qmake же мы получим PUBLIC по умолчанию и дальше только флаги линкера/компилятора. find_package
на первых порах выглядит громоздким, но на деле оказывается очень гибким и удобным инструментом. Пока опустим lib2 и другие.
Переменная QT_VERSION_MAJOR
в старых версиях не устанавливается, будьте внимательны. Тогда получить ее можно так:
if (NOT QT_VERSION_MAJOR)
set(QT_VERSION ${Qt5Core_VERSION})
string(SUBSTRING ${QT_VERSION} 0 1 QT_VERSION_MAJOR)
endif()
Application
Посмотрим, как будет выглядеть наш App1.
qmake, src/App1/App1.pro
QT += core gui network widgets xml
TARGET = App1
VERSION = 1.0.0
## указываем версию
QMAKE_TARGET_COMPANY = Company
QMAKE_TARGET_COPYRIGHT = Company
QMAKE_TARGET_PRODUCT = Product
## укажем информацию о нашем исполняемом файле
TEMPLATE = app
## теперь мы собираем исполняемый файл
RC_ICONS = ../../logo.ico
## иконку здесь указывать проще, но все равно win-only
QMAKE_SUBSTITUTES += config.h.in
## шаблоны для генерируемых файлов
## готовый файл config.h будет лежать рядом с шаблоном
include(App1.pri)
LIBRARIES += lib1 \
...
lib2
## а это хак, перечисляющий от чего зависит наш App1
Внутренности App1.pri опустил, они нам не важны, т.к. там только перечисление исходников и заголовков.
qmake, src/App1/config.h.in - добавим немного полезной информации
#pragma once
#define PROGNAME '"$$TARGET"'
#define PROGVERSION '"$$VERSION"'
#define PROGCAPTION '"$$TARGET v"'
#define SOFTDEVELOPER '"$$QMAKE_TARGET_COMPANY"'
cmake, src/App1/CMakeLists.txt
project(App1)
set(PROJECT_VERSION_MAJOR 1)
set(PROJECT_VERSION_MINOR 0)
set(PROJECT_VERSION_PATCH 0)
## здесь версию можно указать разными способами
## мы укажем так
configure_file(
${CMAKE_SOURCE_DIR}/config.h.in
## взять такой файл за шаблон
${CMAKE_CURRENT_BINARY_DIR}/config.h
## сгенерировать из него новый по такому то пути
@ONLY)
configure_file(
${CMAKE_SOURCE_DIR}/versioninfo.rc.in
${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc
## аналогичная генерация, но уже rc файлов
@ONLY)
## генерируемые файлы
find_package(
QT NAMES Qt6 Qt5
COMPONENTS Core Xml Widgets Network
REQUIRED)
find_package(
Qt${QT_VERSION_MAJOR}
COMPONENTS Core Xml Widgets Network
REQUIRED)
add_executable(${PROJECT_NAME}
main.cpp
...
../../icon.rc # это тоже иконка но windows-only
${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc # windows-only
)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
## добавим в include directories нашу директорию, где будут лежать
## сгенерированные файлы
if(CMAKE_BUILD_TYPE STREQUAL "Release")
set_property(TARGET ${PROJECT_NAME} PROPERTY WIN32_EXECUTABLE true)
endif()
## куда ж без костылей, говорим, что запускать надо гуй без консоли
target_link_libraries(
${PROJECT_NAME}
lib1
...
lib2
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Xml
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Network
)
Почти в 2 раза больше строк для cmake, что ж это такое-то...
cmake, src/config.h.in
#define PROGNAME "@PROJECT_NAME@"
#define PROGVERSION "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@"
#define PROGCAPTION "@PROJECT_NAME@ v"
#define SOFTDEVELOPER "@SOFTDEVELOPER@"
cmake, src/versioninfo.rc.in
1 TYPELIB "versioninfo.rc"
1 VERSIONINFO
FILEVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, 0
PRODUCTVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, 0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x4L
FILETYPE 0x2L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "@SOFTDEVELOPER@"
VALUE "FileDescription", "@PROJECT_NAME@"
VALUE "FileVersion","@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0"
VALUE "InternalName", "@PROJECT_NAME@"
VALUE "LegalCopyright", "Copyright (c) 2021 @SOFTDEVELOPER@"
VALUE "OriginalFilename", "@PROJECT_NAME@.exe"
VALUE "ProductName", "@PROJECT_NAME@"
VALUE "ProductVersion","@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0"
## здесь мы тоже указываем информацию о нашем
## исполняемом файле
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
Заголовок про системы сборки, а тут почему-то .rc файлы...? А всё просто, cmake не предоставляет возможности указать иконку или информацию о запускаемом файле через переменные (в отличие от, внезапно, qmake), поэтому нам нужен rc файл.
Но все-таки что .rc файлы windows-only, как и QMAKE_TARGET_* RC_ICONS. Вообще-то в qmake тоже можно использовать сгенерированный rc файл, но зачем если встроенных переменных будет достаточно, и qmake всё сделает сам. Так что вся магия и rc файлы просто спрятаны от нас в qmake.
Директива configure_file
аналогична QMAKE_SUBSTITUTES,
но с одним крайне важным отличием. Вы можете указать путь, по которому будет сгенерирован файл, а в qmake он будет рядом с исходным. Это неважно, если вам нужно использовать его только один раз. Но что если нам нужно генерировать по одному шаблону несколько файлов? Например, вытаскивать версию через информацию текущего коммита. А ничего, тогда придется страдать. Для каждой цели в случае qmake придется иметь копию этого файла в другой директории, иначе они будут затираться. Вообще cmake предоставляет больше способов работы с путями.
Вернемся немного назад, вспомним строки в самом первом .pro файле - App1.depends = lib1 lib2 ...
Cmake под капотом имеет схожий инструмент, только для пользователя он выглядит намного удобнее. Всё это работает через директиву target_link_libraries(<target> ... <item>... ...)
. Здесь target зависит от item, т.е. item должен быть собран перед тем, как target будет с ним линковаться. Если вы используете рекомендуемый синтаксис, т.е. item это - A library target name (item должна быть создана директивой add_library()
или быть IMPORTED
библиотекой), то всё само прекрасно соберется и слинкуется а при пересборке библиотеки, она будет слинкована снова. Надо заметить, что это намного удобнее нежели реализация в qmake. Почему этого нет в qmake?...
Можно сказать, что cmake предоставляет больше возможностей, но и больше приходится писать руками. CMake всё больше напоминает один известный ЯП...
Управление зависимостями
Тут у нас есть решения как общие для обеих систем сборки, так и специфичные для каждой. Начнем с общих.
Пакетные менеджеры, а конкретно conan предоставляют удобные пути интеграции с обеими системами сборки. Но есть небольшой нюанс - основной путь интеграции в qmake. Он не является прозрачным и теперь мы полностью будем зависеть от conan'a. Больше нельзя будет собрать проект без conan'a. Прекрасно? Остальные языки тоже зависят от пакетников, но там они часть языка.
А вот для cmake дела нынче обстоят иначе. Есть три основных генератора: cmake, cmake_find_package, cmake_find_package_multi. Первый аналогичен таковому для qmake - подсаживает нас на пакетник,а вот два последних дают полностью прозрачную интеграции, что несомненно является большим плюсом, например мы можем на Windows линковаться с библиотекой из conan'a а под Linux с библиотекой из пакетов без особых проблем. Тут есть много но и если, которые связаны с кривыми рецептами в conan'e отчасти, но сама возможность имеется и многие кейсы покрывает. Так что небольшая магия присутствует. Небольшой пример:
find_package(hidapi REQUIRED) ## найдет как системный dev пакет так и из conan'a
if (UNIX)
target_link_libraries(${PROJECT_NAME} PRIVATE hidapi-hidraw) # debian package
endif()
if (WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE hidapi::hidapi) # conan
endif()
Специально вытащил такой пример, hidapi под никсами и hidapi под окошками - разные библиотеки с одним api, т.е. под никсами это либо через libusb либо через hidraw,а вот окошками только один вариант.
Но что делать если нашей библиотеки нет в пакетном менеджере (или в репах нашего дистрибутива)? А это случается часто, надеюсь когда-нибудь в нашем ужасном С++ мире будет пакетный менеджер с библиотеками на любой чих (привет npm).
В случае qmake у нас не так чтобы много возможностей. Если желаемая библиотека предоставляет возможности интеграции (например содержит .pro файл), то всё в шоколаде, например как тут https://github.com/QtExcel/QXlsx/blob/master/HowToSetProject.md, 4 строчки и всё в ажуре. Но вот если желаемая библиотека не поддерживает qmake... ничего вы не сделаете, кроме как сначала собрать и разложить всё по папочкам.
С cmake ситуация отличается примерно целиком и полностью, он из коробки предоставляет интерфейс подхватывания и сборки сторонних либ, даже если они не поддерживают cmake - ExternalProject. Конечно если желаемая либа содержит идеальный CMakeLists, то строчек надо написать что-то в районе 4-х тоже (снова там же пример https://github.com/QtExcel/QXlsx/issues/49#issuecomment-907870633) или вообще пойти через add_subdirectory
, тогда можно ограничиться и 1 строчкой, управлять версиями через git submodule. Но мир костылестроения большой, поэтому предположим ситуацию, что требуемая библиотека поддерживает только qmake и ничего более (вариант патчить и контрибьютить в Open Source пока отложим). Например LimeReport, я намеренно указал старый коммит, т.к. позже поправил CMakeLists. Можно построить весьма занятный велосипед, бессмысленный и беспощадный но интересный. А если поддерживает, но нам хочется патчить и собирать по-своему, то можно уже чуть лаконичнее, например, тот же QXlsx. CMake дает много возможностей даже здесь, главное ими научиться пользоваться.
Вывод
QMake как система сборки хороша, максимально проста для вхождения и удобна. Если вы пишете маленький Qt-only проект или строго для одной платформ с одним компилятором, то всё прекрасно в вашем мире, но как только вам понадобится выйти за рамки дозволенного...
CMake сложен, один хороший человек сказал, что его стоит рассматривать как отдельный ЯП, вынужден с ним согласиться, потому что писать приходится много. Он позволяет очень много, настолько много что порой рождается такое.
Если у вас сложный проект, вы хотите жонглировать зависмостями, использовать одну кодовую базу на самых разных ОС и архитектурах или просто быть на гребне волны и не остаться за бортом, то ваш выбор cmake.
Можно провести такие аналогии, то qmake - js/python, а cmake - C++.
P.S. В статье опущены генераторные выражение т.к. аналогов в qmake попросту нет.
Отдельное спасибо выражаю товарищам из https://t.me/qt_chat, https://t.me/probuildsystems и переводчику/автору статьи т.к. именно благодаря им всё получилось.
Старое состояние проекта можно посмотреть здесь, а новое соответственно доступно без привязки к коммиту, если вдруг захочется.