Как стать автором
Обновить

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

Время на прочтение 9 мин
Количество просмотров 5.2K
Введение

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

Теги:
Хабы:
+2
Комментарии 6
Комментарии Комментарии 6

Публикации

Истории

Работа

QT разработчик
13 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн