Qbs: Шаблон настольного приложения

от автора

Введение

Давным давно, когда Qbs только вышла, я начинал писать эту статью, но так её и не закончил… Кажется, пришло время ее дописать. С тех пор многое изменилось, у Qbs наконец-то появилась документация, но примеров (к сожалению) в ней по-прежнему не так много. В этой статье я расскажу как написать шаблон (почти) полноценного десктопного приложения с использованием Qt.Widgets. По-хорошему, было бы неплохо сделать это на чистом C++, но я слишком ленив, чтобы сделать тестовый UI с помощью нативного АПИ под 3 платформы. Для примера я написал простое приложение ("рыбу"), состоящее из основного приложения, библиотеки и плагина, которое мы и будем разбирать

Кого заинтересовало, добро пожаловать под кат.

image

Код расположен на гитхабе, и был протестирован под Windows, Linux и macOS.

Я не буду подробно описывать процесс сборки, установки и настройки Qbs, это достаточно подробно описано в документации.

Предвосхищая комментарий о том, что Qbs объявлена устаревшей в пользу CMake как система сборки для Qt, сразу отмечу, что сейчас проект развивается сообществом и недавно вышла новая версия.

И так, приступим.

Любой Qbs проект состоит из корневого элемента Project, который может в себе содержать один или несколько продуктов, а также ссылки над подпроекты (для организации иерархии, каждая вложенная папка содержит подпроект). Product — это результат сборки чего-либо — например, бинарник приложения, статическая/динамическая библиотека, сгенерённые файлы переводов, и тому подобное. Продукты могут зависеть либо от модулей (таких как модуль cpp, модули Qt), либо от других продуктов.

Корневой файл проекта тривиален:

Project {     name: "Qbs Fish"     minimumQbsVersion: "1.16"     references: [          "src/src.qbs",          "tests/tests.qbs",     ]     qbsSearchPaths: "qbs"      AutotestRunner {} }

Мы задаем имя проекта и минимальную версию Qbs. Для сборки необходима версия Qbs 1.16 так как используются некоторые фичи, добавленные только в этой версии (например, модуль freedesktop). Свойство references содержит ссылки на подпроекты (папки src и tests). AutotestRunner — это продукт, при сборке которого запускаются тесты. Переменная qbsSearchPaths отвечает за то, где Qbs будет искать пользовательские модули и айтемы и задается в виде относительного (от текущего файла) пути. Папка qbs содержит две подпапки — modules (для, гм, модулей) и imports (для айтемов).

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

Казалось бы, логично объявить пользовательский айтем, скажем, MyProduct, наследующий Product, вынести в него всё общее и отнаследовать от него MyApplication, MyLibrary, MyPlugin… К сожалению, тогда мы потеряем возможность использовать встроеные айтемы, такие как CppApplication, DynamicLibrary и иже с ними, так как Qbs не поддерживает множественное наследование. Эти айтемы предоставляют ряд удобных вещей — установку продукта, его отладочных символов и мультеплексирование (например, возможность собирать "fat binaries" под iOS).

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

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

Module {     property bool staticBuild: false     property bool frameworksBuild: qbs.targetOS.contains("macos") && !staticBuild      property bool enableAddressSanitizer: false     property bool enableUbSanitizer: false     property bool enableThreadSanitizer: false      property string libDirName: "lib"

Задать эти свойства с командной строки можно так:

qbs modules.buildconfig.staticBuild:true modules.buildconfig.enableUbSanitizer:true

Затем мы объявляем вспомогательные константы — относительные пути куда ставить части проекта (там много однотипных "свитчей" по целевой платфоме, поэтому приведу только пару таких "свитчей"):

    readonly property string appTarget:         qbs.targetOS.contains("macos") ? "Fish" : "fish"      readonly property string installAppPath: {         if (qbs.targetOS.contains("macos"))             return "Applications";         else if (qbs.targetOS.contains("windows"))             return ".";         else             return "bin";     }

Полный список констант

  • appTarget — имя главного бинарника (бандла на маке)
  • installAppPath — путь, куда будет установлен главный бинарник, например "Applications" на маке или "bin" на линуксе
  • installBinaryPath — путь, куда будут установлены вспомогательные бинарники (на маке кладутся внутрь главного бандла, на остальных платформах рядом с главным бинарником)
  • installLibraryPath — путь, куда ставить дллки
  • installPluginPath — путь, куда ставить плагины
  • installDataPath — путь, куда ставить ресурсы и данные

Наконец, мы объявляем общие свойства, такие как флаги компилятора и версия языка:

    Depends { name: "cpp" } // включаем модуль, реализующий поддержку С/С++      cpp.cxxLanguageVersion: "c++17" // задаем версию языка     cpp.separateDebugInformation: true // форсим отделение дебаг инфы      Properties {         condition: qbs.toolchain.contains("gcc")         cpp.cxxFlags: { // флаги компилятора (но не линковщика)             var flags = [];             if (enableAddressSanitizer)                 flags.push("-fno-omit-frame-pointer");             return flags;         }         cpp.driverFlags: { // флаги компилятора И линковщика             var flags = [];             if (enableAddressSanitizer)                 flags.push("-fsanitize=address");             if (enableUbSanitizer)                 flags.push("-fsanitize=undefined");             if (enableThreadSanitizer)                 flags.push("-fsanitize=thread");             return flags;         }     } }

Теперь, имея этот модуль, мы можем написать кастомные айтемы. Начнем с айтема, общего для всех библиотек и плагинов в проекте — MyLibrary. Мы наследуется от стандартного айтема Library и подключаем зависимость от необходимых модулей:

Library {     Depends { name: "buildconfig" }     Depends { name: "bundle" }     Depends { name: "cpp" }

buildconfig — это наш кастомный модуль
bundle — это модуль, реализующий поддержку бандлов на яблочных платформах
cpp — этот модуль мы уже видели выше, в нем живет поддержка C/C++/Objective-C.

Затем мы устанавливаем тип нашей библиотеки в зависимости от того, статический билд или нет, а также включаем или выключаем упаковку в бандл:

    type: buildconfig.staticBuild ? "staticlibrary" : "dynamiclibrary"      bundle.isBundle: buildconfig.frameworksBuild

Мы устанавливаем includePaths так, чтобы там находился родительский каталог для того, чтобы инклюды к нашим хедерам включали имя библиотеки: #include <library/header>. Также, мы объявляем всмомогательные дефайны, чтобы было проще разбираться с макросами импорта/эспорта (скорей бы модули!).

     cpp.includePaths: [".."]      cpp.defines: buildconfig.staticBuild                ? ["FISH_STATIC_LIBRARY"]                : ["FISH_LIBRARY"]

Дальше идет установка sonamePrefix и rpaths. Установка sonamePrefix нужна только на маке, так как по умолчанию soname включает полный путь к библиотеке (а не только имя библиотеки), поэтому мы заменяем абсолютный путь на "@rpath" — список относительных путей для поиска. Также мы задаем этот список (rpaths) равным одному элементу — текущей папке библиотеки (rpathOrigin, раскрывается в "$ORIGIN" на линуксе или "@loader_path" на маке). Таким образом, все наши библиотеки смогут искать зависимости рядом с собой:

cpp.sonamePrefix: qbs.targetOS.contains("macos") ? "@rpath" : undefined cpp.rpaths: cpp.rpathOrigin

Затем мы объявляем свойства, которые наша библиотека экспортирует, то есть те свойства, которые будут автоматически добавлены в продукты, которые зависят от нашей библиотеки:

    Export {         Depends { name: "cpp" }         cpp.includePaths: [".."]         cpp.defines: buildconfig.staticBuild ? ["FISH_STATIC_LIBRARY"] : []     }

Также как и выше, мы экспортируем includePaths, содержащие родительский каталог — теперь любой продукт, где бы он не находился в проекте, сможет делать #include <library/header>. Кроме того, в статической сборке вы делаем так, чтобы зависимые продукты объявляли макрос "FISH_STATIC_LIBRARY", иначе при включении заголовков нашей библиотеки, они будут пытаться импортировать символы из несуществующей dll (венда боль).

Наконец, мы говорим, куда ставить нашу библиотеку:

    install: !buildconfig.staticBuild     installDir: buildconfig.installLibraryPath     installDebugInformation: !buildconfig.staticBuild }

Теперь, когда у нас есть базовый айтем, мы можем создать пример готовой библиотеки FishLib. Этот код создаст нам бинарник libFishLib.so (FishLib.dll на винде, libFishLib.dylib на маке) и установит его и его отладочные символы в соответствующую папку:

MyLibrary {     name: "FishLib"     files: [         "class.cpp",         "class.h",         "fishlib_global.h",     ] }

Что может быть проще!

Базовый айтем для приложений сильно проще и единственное отличие — это то, как мы задаем rpaths:

    cpp.rpaths: FileInfo.joinPaths(cpp.rpathOrigin,                                    "..",                                    qbs.targetOS.contains("macos")                                    ? "Frameworks"                                    : buildconfig.installLibraryPath)

Результатом является "$ORIGIN/../lib/fish" на линуксе и "@loader_path/../Frameworks/" на маке — то есть мы поднимаемся на уровень выше от bin (или папки MacOS внутри бандла) и спускаемся в lib/fish (или Frameworks). В Windows библиотеки ставятся рядом с бинарником, так как там rpath не завезли.

Базовый айтем для плагинов и пример плагина я расписывать не буду, там всё тривиально, мы просто переиспользуем MyLibrary.qbs.

Итак, настало время собрать это всё в готовое приложение. Как обычно, мы наследуем базовый айтем и импортируем необходимые модули и продукты. Стоит отметить, что при включении зависимости от плагина, мы явно говорим о том, что с ним не надо линковаться (но его надо собрать до сборки нашего приложения). Также, зависимости можно подключать в только если выполнено условие, например, модуль ib доступен только на яблочных платформах:

MyApp {     Depends { name: "buildconfig" }     Depends { name: "ib"; condition: qbs.targetOS.contains("macos") }     Depends { name: "freedesktop" }     Depends { name: "Qt.core" }     Depends { name: "Qt.widgets" }     Depends { name: "FishLib" }     Depends { name: "FishPlugin"; cpp.link: false }

Мы задаем имя нашего продукта и имя бинарника так, чтобы файл назывался с большой буквы на Маке и с маленькой на Линуксе и Винде (Fish.app, fish и fish.exe, соответственно); "правильное" название хранится в buildconfig.appTarget:

    name: "Fish"     targetName: buildconfig.appTarget

Задаем список файлов:

    files: [         "Fish-Info.plist",         "fish.desktop",         "fish.rc",         "fish.xcassets",         "main.cpp",         "mainwindow.cpp",         "mainwindow.h",         "mainwindow.ui",     ]

Fish-Info.plist содержит свойства бандла на маке (например, копирайт). Минимальный Info.plist Qbs генерит сама, но мы можем переопределять свойства руками в файле или с помощью свойства bundle.infoPlist.

fish.desktop содержит свойства приложения в Линуксе — имя приложения в "меню пуск", какую команду запускать, какую иконку использовать. Этот файл установится автоматически благодаря зависимости от модуля freedesktop.

Аналогично, fish.rc содержит свойства экзешника в Windows (например, всё то же имя иконки).

Каталог fish.xcassets содержит исходники для иконки на маке, имя иконки мы задаем с помощью модуля ib:

    Properties {         condition: qbs.targetOS.contains("macos")         ib.appIconName: "Fish"     }

Qbs скомпилирует из png-файлов различного разрешения, находящихся в каталоге fish.xcassets/Fish.appiconset файл Fish.icns и пропишет имя иконки в результирующий Info.plist. Так как модуль ib доступен только на яблочных платформах, нужно завернуть установку его свойств в Properties, иначе получим ошибку о том, что свойства ib.appIconName нет.

Ставим иконку приложения на Линуксе. Модуль freedesktop позволяет ставить svg иконки в share/icons/hicolor/scalable/apps, но у меня нет векторного варианта иконки, поэтому ставим "ручками" в share/pixmaps:

    Group {         name: "fish.png"         condition: qbs.targetOS.contains("linux")         files: [ "fish.png" ]         qbs.install: true         qbs.installDir: "share/pixmaps"     }

В итоге получились следующие иерархии каталогов:

Linux

На самом деле, мы ставим в <install-root>/usr/local, но опустим префикс и install-root:

/bin /bin/fish /bin/fish.debug /bin/tool /bin/tool.debug /lib /lib/fish /lib/fish/libFishLib.so /lib/fish/libFishLib.so.debug /lib/fish/plugins /lib/fish/plugins/libFishPlugin.so /lib/fish/plugins/libFishPlugin.so.debug /share /share/applications /share/applications/fish.desktop /share/pixmaps /share/pixmaps/fish.png

macOS

Содержимое папок *dSYM/ опущено для простоты

/Applications/Fish.app /Applications/Fish.app.dSYM /Applications/Fish.app/Contents /Applications/Fish.app/Contents/Frameworks /Applications/Fish.app/Contents/Frameworks/FishLib.framework /Applications/Fish.app/Contents/Frameworks/FishLib.framework.dSYM /Applications/Fish.app/Contents/Info.plist /Applications/Fish.app/Contents/MacOS /Applications/Fish.app/Contents/MacOS/Fish /Applications/Fish.app/Contents/MacOS/tool /Applications/Fish.app/Contents/MacOS/tool.dSYM /Applications/Fish.app/Contents/PkgInfo /Applications/Fish.app/Contents/PlugIns /Applications/Fish.app/Contents/PlugIns/libFishPlugin.dylib /Applications/Fish.app/Contents/PlugIns/libFishPlugin.dylib.dSYM /Applications/Fish.app/Contents/Resources /Applications/Fish.app/Contents/Resources/Fish.icns

Windows

/FishLib.dll /FishLib.pdb /fish.exe /fish.pdb /plugins /plugins/FishPlugin.dll /plugins/FishPlugin.pdb /tool.exe /tool.pdb

Выведение

Как видно, Qbs позволяет собирать достаточно сложные проекты, из коробки поддерживает различные платформы (включая Android, iOS и различные микроконтроллеры). Благодаря декларативному синтаксису, работать с этой системой сборки одно удовольствие, проекты получаются простыми и достигается максимальное переиспользование кода.

ссылка на оригинал статьи https://habr.com/ru/post/183020/