Введение
Давным давно, когда Qbs только вышла, я начинал писать эту статью, но так её и не закончил… Кажется, пришло время ее дописать. С тех пор многое изменилось, у Qbs наконец-то появилась документация, но примеров (к сожалению) в ней по-прежнему не так много. В этой статье я расскажу как написать шаблон (почти) полноценного десктопного приложения с использованием Qt.Widgets. По-хорошему, было бы неплохо сделать это на чистом C++, но я слишком ленив, чтобы сделать тестовый UI с помощью нативного АПИ под 3 платформы. Для примера я написал простое приложение ("рыбу"), состоящее из основного приложения, библиотеки и плагина, которое мы и будем разбирать
Кого заинтересовало, добро пожаловать под кат.
Код расположен на гитхабе, и был протестирован под 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"
}
В итоге получились следующие иерархии каталогов:
На самом деле, мы ставим в <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
Содержимое папок *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
/FishLib.dll
/FishLib.pdb
/fish.exe
/fish.pdb
/plugins
/plugins/FishPlugin.dll
/plugins/FishPlugin.pdb
/tool.exe
/tool.pdb
Выведение
Как видно, Qbs позволяет собирать достаточно сложные проекты, из коробки поддерживает различные платформы (включая Android, iOS и различные микроконтроллеры). Благодаря декларативному синтаксису, работать с этой системой сборки одно удовольствие, проекты получаются простыми и достигается максимальное переиспользование кода.