Введение
Давным давно, когда 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 и различные микроконтроллеры). Благодаря декларативному синтаксису, работать с этой системой сборки одно удовольствие, проекты получаются простыми и достигается максимальное переиспользование кода.
