company_banner

История успешного перевода ScreenPlay с QMake на CMake

Автор оригинала: Elias Steurer
  • Перевод
ScreenPlay — это опенсорсное приложение для Windows (а скоро — ещё и для Linux и macOS), предназначенное для работы с обоями и виджетами. Оно создано с использованием современных инструментов (C++/Qt/QML), активная работа над ним ведётся с первой половины 2017 года. Код проекта хранится на платформе GitLab.



Автор статьи, перевод которой мы сегодня публикуем, занимается разработкой ScreenPlay. Он столкнулся с рядом проблем, решить которые ему помог переход с QMake на CMake.

QMake и разработка больших проектов


▍Совместное использование кода в QMake — это очень неудобно


При разработке достаточно сложных приложений обычно лучше всего разбивать их на небольшие фрагменты, которыми удобно управлять. Например, если нужно представить приложение в виде основного исполняемого файла, к которому подключаются библиотеки, то, пользуясь QMake, сделать это можно лишь с помощью проекта, основанного на шаблоне subdirs. Это — проект, представленный файлом, скажем, с именем MyApp.pro, который содержит запись об используемом шаблоне и список папок проекта:

  TEMPLATE = subdirs
 
  SUBDIRS = \
            src/app \   # относительные пути
            src/lib \
            src/lib2

При таком подходе в нашем распоряжении оказываются несколько подпроектов, в которых нужно организовать совместное использование кода. Для того чтобы сообщить компилятору о том, где именно в других проектах ему нужно искать заголовочные файлы и файлы с исходным кодом, нужно передать линковщику сведения о том, какие именно библиотеки ему требуется подключать, и о том, где искать скомпилированные файлы. В QMake это делается через создание огромных .pri-файлов, которые используются исключительно для описания того, что нужно включить в проект. Это напоминает использование обычных C++-конструкций вида #include <xyz.h>. В результате оказывается, что, например, файл MyProjectName.pri включается в состав MyProjectName.pro. А для исправления проблемы, связанной с относительными путями, нужно добавить текущий абсолютный путь в каждую строку.

▍Внешние зависимости


Работа с внешними зависимостями, предназначенными для различных операционных систем, сводится, в основном, к копированию путей к соответствующим зависимостям и к вставке их в .pro-файл. Это — скучная и утомительная работа, так как у каждой ОС есть, в этом плане, свои особенности. Например, в Linux нет отдельных подпапок debug и release.

▍CONFIG += ordered — убийца производительности компиляции


Ещё один недостаток QMake заключается в периодическом возникновении проблем с компиляцией. Так, если в проекте есть множество подпроектов, которые представляют собой библиотеки, используемые в других подпроектах, то компиляция периодически завершается с ошибкой. Причиной ошибки может быть примерно такая ситуация: библиотека libA зависит от библиотек libB и libC. Но к моменту сборки libA библиотека libC ещё не готова. Обычно проблема исчезает при перекомпиляции проекта. Но то, что такое вообще происходит, указывает на серьёзные проблемы QMake. И эти проблемы не удаётся решить, пользуясь чем-то вроде libA.depends = libB. Вероятно (и, пожалуй, так оно и есть), я что-то делаю не так, но справиться с проблемой не удалось ни мне, ни моим коллегам. Единственный способ решить проблему с порядком сборки библиотек заключается в использовании настройки CONFIG += ordered, но из-за этого, за счёт отказа от параллельной сборки, сильно страдает производительность.

QBS и CMake


▍Почему QBS проигрывает CMake?


Сообщение о прекращении поддержки QBS (Qt Build System, система сборки Qt) стало для меня настоящим шоком. Я даже был одним из инициаторов попытки это изменить. При использовании QBS применяются приятные синтаксические конструкции, знакомые каждому, кто когда-либо писал QML-код. Не могу сказать того же о CMake, но после того, как я несколько месяцев поработал с этой системой сборки проектов, я могу с уверенностью заявить о том, что переход на неё с QBS был правильным решением, и о том, что я продолжу пользоваться CMake.

CMake, хотя и имеет некоторые недостатки синтаксического плана, работает надёжно. А проблемы QBS больше относятся к сфере политики, чем к технической стороне вопроса.

Это — один из основных факторов, заставляющих программистов, недовольных размером Qt (и в плане количества строк кода, и в плане размера библиотеки), искать альтернативу. Кроме того, многим крайне не нравится MOC. Это — метаобъектный компилятор, который преобразует C++-код, написанный с использованием Qt, в обычный C++. Благодаря этому компилятору можно, например, пользоваться удобными конструкциями, вроде тех, которые позволяют работать с сигналами.

▍Альтернативы QBS


В нашем распоряжении, помимо QBS, имеются такие системы сборки проектов, как build2, CMake, Meson, SCons. Они, за пределами экосистемы Qt, используются во многих проектах.

▍Плохая поддержка QBS в IDE


Насколько мне известно, единственной IDE, поддерживающей QBS, является QtCreator.

▍Блестящий союз vcpkg и CMake


Помните, как выше я возмущался проблемами с внешними зависимостями? Поэтому неудивительно то, сколько положительных эмоций у меня вызвал диспетчер пакетов vcpkg. Для установки зависимости достаточно одной команды! Полагаю, vcpkg может пригодиться любому C++-программисту.

О кажущейся непривлекательности синтаксиса CMake


Если судить о CMake по первой десятке ссылок найденных Google, то может показаться, что в этой системе используются весьма непривлекательные синтаксические конструкции. Но проблема тут в том, что Google выводит первыми старые материалы о CMake со Stack Overflow, датированные 2008 годом. Тут же попадаются и ссылки на старую документацию к версии CMake 2.8. Синтаксические конструкции, используемые при работе с CMake, могут быть очень даже симпатичными. Дело в том, что применение CMake предусматривает, в основном, использование конструкций, показанных ниже (это — сокращённый вариант файла CMakeList.txt из проекта ScreenPlay).

# Проверка минимальных требований
cmake_minimum_required(VERSION 3.16.0)

# Указание имени проекта. Оно потом будет использовано для именования 
# исполняемых файлов и для весьма полезного ${PROJECT_NAME}
project(ScreenPlay)

# Некоторые настройки Qt, касающиеся ресурсов и MOC
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)

# Это - лишь синтаксический сахар. Тут создаётся переменная src,
# содержащая список строк. Всё это потом будет использоваться в add_executable
set(src main.cpp
        app.cpp
        # Тут кое-что пропустим
        src/util.cpp
        src/create.cpp)

set(headers app.h
        src/globalvariables.h
        # И тут кое-что пропустим
        src/util.h
        src/create.h)

# Макрос Qt для больших ресурсов наподобие шрифтов
qt5_add_big_resources(resources  resources.qrc)

# Предлагаем CMake скомпилировать qml в C++ в режиме release
# ради повышения производительности!
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(qml qml.qrc)
else()
    qtquick_compiler_add_resources(qml qml.qrc )
endif()

# Предлагаем CMake найти эти библиотеки. Ранее мы, рассчитывая на это, настроили CMAKE_TOOLCHAIN_FILE
# и нам больше не нужно вручную редактировать относительные пути!
find_package(
  Qt5
  COMPONENTS Quick
             QuickCompiler
             Widgets
             Gui
             WebEngine
  REQUIRED)

# Внешние библиотеки vcpkg
find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(libzippp CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)

# У CMake есть две основные команды: 
# add_executable для создания исполняемых файлов
# add_library для создания библиотек
add_executable(${PROJECT_NAME} ${src} ${headers} ${resources} ${qml})

# Пользовательское свойство для отключения окна консоли в Windows
# https://stackoverflow.com/questions/8249028/how-do-i-keep-my-qt-c-program-from-opening-a-console-in-windows
set_property(TARGET ${PROJECT_NAME} PROPERTY WIN32_EXECUTABLE true)

# Предлагаем компилятору найти указанные зависимости. Чаще всего имя 
# библиотеки можно узнать у vcpkg. В противном случае можно поискать 
# dll/lib/so/dynlib в vcpkg/installed
# Если нужны зависимости внутри структуры проекта, можно
# просто добавить project(MyLib) к target_link_libraries.
# Никаких путей и ничего другого указывать не нужно.
target_link_libraries(${PROJECT_NAME}
    PRIVATE
    Qt5::Quick
    Qt5::Gui
    Qt5::Widgets
    Qt5::Core
    Qt5::WebEngine
    nlohmann_json::nlohmann_json
    libzippp::libzippp
    ScreenPlaySDK
    QTBreakpadplugin)

# Предлагаем CMake скопировать этот файл в директорию build в том случае, если он изменился.
# ${CMAKE_BINARY_DIR} - это директория build!
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/bin/assets/fonts)
configure_file(assets/fonts/NotoSansCJKkr-Regular.otf ${CMAKE_BINARY_DIR}/bin/assets/fonts COPYONLY)

Ninja ускоряет CMake


Роль CMake заключается лишь в том, чтобы генерировать инструкции для выбранной разработчиком системы сборки проектов. Это может оказаться огромным плюсом при работе с людьми, которые пользуются не Qt Creator, а Visual Studio. При использовании CMake можно (и нужно) выбрать Ninja в качестве системы сборки, используемой по умолчанию. Компиляция проектов с применением связки CMake+Ninja — это очень приятно. И то и другое можно найти в наборе инструментов Qt Maintenance. Кроме прочего, эти инструменты очень быстро обрабатывают изменения при итеративном подходе к разработке. На самом деле, всё работает так быстро, что при использовании Godot со SCons мне очень хочется и тут пользоваться CMake.

Vcpkg позволяет CMake проявить себя во всей красе


Управление зависимостями в C++-проектах — это непростая задача. Для её решения многие проекты даже размещают в своих Git-репозиториях необходимые DLL. А это плохо, так как из-за этого неоправданно увеличиваются размеры репозиториев (Git LFS мы тут не касаемся). Недостаток vcpkg заключается лишь в том, что этот диспетчер пакетов поддерживает лишь одну глобальную версию некоего пакета (то есть, приходится самостоятельно устанавливать разные версии vcpkg, но это — нечто вроде хака, да и нужно это редко). Правда, в планах развития проекта можно увидеть то, что он идёт в правильном направлении.

Для установки пакетов используется такая команда:

vcpkg install crashpad

Мы, при работе над ScreenPlay, просто создали скрипты install_dependencies_windows.bat и install_dependencies_linux_mac.sh для клонирования репозитория vcpkg, для его сборки и установки всех наших зависимостей. При работе с Qt Creator необходимо записывать в CMAKE_TOOLCHAIN_FILE относительный путь к vcpkg. Кроме того, vcpkg нужно сообщить о том, какую ОС и какую архитектуру мы используем.

    # Настройка QtCreator. Extras -> Tools -> Kits ->  -> CMake Configuration. Туда надо добавить следующее:
    CMAKE_TOOLCHAIN_FILE:STRING=%{CurrentProject:Path}/Common/vcpkg/scripts/buildsystems/vcpkg.CMake
    VCPKG_TARGET_TRIPLET:STRING=x64-windows

Нужно установить ещё какую-нибудь библиотеку? Для этого достаточно воспользоваться командой вида vcpkg install myLibToInstall.

Итоги


У подхода, когда пользуются всем самым новым и популярным, есть свои плюсы. Но что делать, например, когда системы сборки с большим потенциалом, вроде QBS, вдруг оказываются на обочине? В конечном счёте, разработчик сам принимает решения о том, чем ему пользоваться в его проектах. Именно поэтому я решил перевести свой проект на CMake. И, надо сказать, это было правильное решение. Сегодня, в 2020 году, CMake смотрится очень даже хорошо.

Пользуетесь ли вы CMake и vcpkg?



RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Комментарии 6

    +2
    Несколько лет назад перетаскивал большой древний с++ проект с tmake (предтеча qmake) на CMake. Если с маленькими проектами проблем не было, то большой проект вызвал вопросы, на которые не находились толковые ответы, например как лучше организовывать работу с библиотеками в большом проекте, с бинарными и заголовочными, с теми что являются частью проекта и внешними, но тоже являющимися частью разработки.

    Традиционно проблемы вызвали генерируемые файлы, которые никак не удавалось помирить с параллельной компиляцией без прописывания вручную зависимостей (так и не разобрался с этим).

    Смотрел, кстати, немного исходники CMake и меня неприятно удивило то, что поддержка Qt в нём прибита гвоздями, а не реализована стандартными средствами.
      0
      Традиционно проблемы вызвали генерируемые файлы, которые никак не удавалось помирить с параллельной компиляцией без прописывания вручную зависимостей (так и не разобрался с этим)
      Для этого используются реальные или виртуальные target. Но прописывать это всё нужно руками, да.
        0
        Для этого используются реальные или виртуальные target. Но прописывать это всё нужно руками, да.

        Ну да, так я это и сделал. Но как я себе это представляю в 2020 году? — Я каким-то образом задаю правило генерирования одних файлов из других, например *.hpp и *.cpp из файлов *.xxx и всё, на этом моя работа должна быть закончена.

        Если генерируемый *.hpp включается в *.cpp файл проекта, то cmake должен сам догадаться (иначе зачем он строит дерево зависимостей), что прежде чем начать компилировать этот *.cpp файл, ему следует сначала сгенерировать соответствующий *.hpp, который он подключает. Почему я должен задавать ещё что-то вручную? По-моему это явная и очень серьёзная недоработка, из-за которой, собственно им и пришлось прибивать qt гвоздями (я знаю, что там есть и другие проблемы).
          0

          Если вы правильно укажете файлы, которые генерирует ваша внешняя тулза (выхлоп), а потом добавите эти файлы в проект через target_sources, то всё должно работать.

            0
            У меня ситуация такая — есть заголовочный файл *.hpp, генерируемый из xxx файла. Этот заголовочный файл включают разные библиотеки и приложения проекта. Иногда непосредственно, иногда опосредовано, через заголовки других библиотек.

            Естественно я не хочу и не могу добавлять вручную этот файл в каждый из этих подпроектов, а хочу чтобы cmake сам, во время построения зависимостей увидел, что используется генерируемый заголовочный файл, и сделал соответствующие манипуляции. По-моему вполне законное желание и у CMake для этого всё есть.

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

              Как вы генерируете, можно пример?

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

    Самое читаемое