Everyone, calm down, this is C++!
Everyone, calm down, this is C++!

Syn ack, Хабр!

С++ - это тяжелый выбор для кросс-платформенного open source проекта. Если вы выбрали С++, то вам нужно пройти следующие этапы:

  1. Сборочка. В С++ нет устоявшихся паттернов сборки. Разные платформы и ОС имеют разные требования для сборки. Если вы хотите показать миру ваше ПО, придется разработать сборочку, которая адаптирована под многообразие платформ и легко поддерживается мейнтейнерами эти

  2. Архитектура. В С++ нет устоявшихся паттернов архитектуры ПО, которая бы подходила для большинства разработчиков. Существуют множество библиотек для решения одних и тех же задач, но с разными архитектурными паттернами. Если вы хотите сделать библиотеку или чтобы ваше ПО могло расширяться другими разработчиками, придется продумать расширяемую архитектуру ПО.

  3. Распространение. В С++ нет устоявшихся паттернов для распространения вашего по. Даже вопрос, а куда выкладывать релизы является открытым и не имеет полностью рабочего решения. Вам придется продумать и разработать методы доставки вашего ПО до ваших пользователей.

Хабр и GitVerse объявили конкурс, в котором попросили поделится своим опытом участия в open source проекте:

“Твои «грабли» — это уже отловленные баги для тех, кто идёт следом”

Я наткнулся на “грабли” в процессе разработки open source проекта на С++: Daggy - Data Aggregation Utility and C/C++ developer library for data streams catching. Чтобы вы могли отловить мои баги, стоит разобраться, откуда возникла идея еще одного open source проекта.

Идея “еще одного сборщика логов” который на самом деле им не является

Everyone, calm down, this is microservicies!
Everyone, calm down, this is microservicies!

Я устроился С++ разработчиком в компанию, которая предоставляла услуги sip телефонии. Первое, с чем я столкнулся и с чем ранее дела не имел - это огромное количество микросервисов. Их было уже более сотни на момент, когда я туда устроился и они продолжали увеличиваться в количестве. Даже простейший sip звонок затрагивал около десятка микросервисов.

Перед всеми новыми сотрудниками стояла задача - спустя полгода работы сдать экзамен по sip протоколу. Каким образом подготовится к экзамену? Я мог просто выучить RFC 3261, но мне хотелось разобраться на практике, как работает sip-телефония. У меня было все необходимое для этого: телефоны, тестовый стенд, обрабатывающий sip звонки и root доступ к серверам стенда, в которых работали микросервисы. Но чтобы начать исследование я столкнулся с одной большой проблемой. Даже самый простой sip звонок проходил через десяток микросервисов.

Для исследования требовалось найти артефакты, соответствующие каждому звонку. Например, мне хотелось узнать следующего рода информацию о звонке:

  1. Через какие конкретно сервисы прошел звонок

  2. Какая служебная информация добавлялась в sip заголовки на каждом этапе звонка

  3. Какие конфиги были у микросервисов на момент обработки звонка

  4. Что именно отображалось в логах на момент, когда через микросервис проходил звонок

Обычно такого рода информацию можно посмотреть через ELK системы. На тот момент ELK система в компании была в самом зародыше, но у любой, даже полностью готовой такой системы есть существенный недостаток. ELK - это серверное решение. Добавить туда какую то кастомную информацию, например сборку pcap файлов для определенного трафика, это отдельная задача. И если туда что-то было добавлено для сбора информации, то подразумевается, что оно там и останется. 

Для моих целей ELK был избыточным и дорогим решением. Я хотел экспериментировать и исследовать систему таким образом, чтобы:

  1. Можно было быстро добавлять, удалять и модифицировать любого рода источники информации, которую мне необходимо было собирать для анализа флоу звонков. Например, если я хочу посмотреть, что приходит по http протоколу на определенный сервис во время звонка, то я могу легко добавить сбор pcap, а потом удалить его из конфига, если посчитаю, что мне это не нужно.

  2. Можно было формировать устоявшиеся паттерны для сбора информаци, а также сниппеты для определенных типов (например, такие сниппеты

  3. Сбор информации можно легко включить на момент выполнения теста (например во время sip звонка) и легко выключить после теста. Это позволяет автоматически получить информацию для исследования, актуальную на момент проведения теста. Больше не нужно в ручную ходить по серверам и искать самостоятельно участки логов и других артефактов, актуальные для момента проведения звонка.

Всем мои требования по сути сводились к следующему флоу исследования работы sip телефонии:

  1. Создаю конфиг, в нем указываю команды, которые будут “тейлить” нужные мне логи, pcap-ы и стягивать нужные мне конфиги

  2. Запускаю с этим конфигом агрегатор

  3. Провожу тест с sip звонком

  4. Останавливаю агрегатор

  5. Локально получаю весь набор нужной мне информации, актуальный на момент звонка, которую могу исследовать

  6. Если я понимаю, что мне чего-то не хватает, то возвращаюсь к шагу один. Иначе сохраняю конфиг для будущего повторения эксперимента.

Поиск готовых решений привел к следующим результатам:

  1. Нет утилиты, которая позволяет по конфигу удаленно запускать одновременно неограниченное количество команд и стримить их вывод локально

  2. Есть bash решения. Посидев на форумах линуксойдов, я нашел некоторые решения с баш скриптами, но все они подразумевали эмпирический подход через ручной запуск процессов для стриминга. Я же хотел запускать команды в декларативном стиле, чтобы упростить себе повторяемость экспериментов и исследований

  3. Скрипты не кросс-платформенные. Вся наша разработка велась на Windows рабочих машинах (наш основной sip сервер работал в Windows), поэтому желательно было найти вариант, что мог работать в windows.

Самый ближайший аналог, который мог бы реализовать описанную выше потребность, это ansible обладающий следующими дополнительными возможностями:

  1. Позволяет собирать вывод команд в runtime (есть актуальный proposal для логов, но не для команд)

  2. Группирует вывод команд по сессиям

Смирившись с тем, что готовых решений нет, я решил написать простую утилиту. Поскольку нужна была работа с асинхронными процессами, я выбрал известную мне на тот момент технологию С++ Qt. Тогда я выбрал этот стек технологий, потому что хотел быстрого решения, но и сегодня я бы сделал тот же самый выбор (об этом поговорим в разделе архитектуры). 

Множество выходных, библиотека qtssh из исходников qt creator, библиотеки Qt Core и Qt Network и первая версия утилиты была готова. Она работала под windows и принимала на вход конфиги, в которых можно было перечислить команды для агрегирования, запускалась из консоли и останавливалась по нажатию CTRL+C. К сожалению, я не могу расшарить рабочие конфиги, поскольку они содержат чувствительные к публикации данные (такие, как номера портов, пути до логов, конфигов и т.п.). Можно посмотреть небольшой скринкаст или потратить 10 минут на небольшой workshop, чтобы понять, как работает утилита.

Workshop. Как работает утилита
  • У вас установлен линукс

  • Вы хотите понять, какой именно трафик и записи в лог добавляются в вашей системе в состоянии покоя

  • Устанавливаем daggy

  • Пишем простой конфиг background.yaml:

sources:
 localhost:
   type: local
   commands:
     log:
       exec: journalctl -f -n 0
       extension: log
     net:
       exec: tcpdp -i any -s 0 port not 22 -w -
       extension: pcap
  • Запускаем daggy с этим конфигом (sudo нужно для tcpdump):

Запуск daggy
Запуск daggy
  • В директории, в которой вы запустили daggy создаться папка с сессией утилиты. На скрине это сессия 17-05-25_18-30-47-753_background

  • Ждем то время, за которое вы хотите снять информацию о системе в состоянии покоя

  • Прерываем программу по CTRL+C

Остановка daggy
Остановка daggy
  • У вас есть локальная папка в которой собраны два файла: localhost-log.log и localhost-net.pcap

Папка с сессией daggy
Папка с сессией daggy
  • Исследуем файлы

  • Если считаем, что нужна дополнительная информация, то возвращаемся к редактированию конфига и повторяем эксперимент

  • Если нужно исследовать удаленный host, то вместо type: local, можем указать type: ssh или type: ssh2.

Утилита не просто помогла мне сдать экзамен по sip телефонии, но и спустя полгода стала местным стандартом в исследованиях проблем межсервисного взаимодействия микросервисов на тестовых окружениях. В начале я испытывал большие сложности с тем, чтобы объяснить, а для чего нужна утилита. Пытаясь описать ее через агрегацию логов, большинство просто не понимало, “зачем нужен еще один агрегатор логов”. И только когда мне удалось показать принцип ее работы коллегам из своей команды, она начала разрастаться по сарафанному радио по всей компании. Впоследствии к багам стало нормой прикладывать папки с сохраненными сессиями утилиты, разработчики и тестировщики использовали ее в процессе своей работы, а ее конфиги разрастались до более сотни одновременно запускаемых команд. 

Но проблема позиционирования проекта никуда не ушла. Я до сих пор не понимаю, как вкратце объяснить, зачем проект нужен. Но факт в том, что как минимум в одной крупной компании проект оказался очень востребован и продолжил использоваться там даже после того, как я оттуда уволился. Это дало мне вдохновение, чтобы заняться полноценным развитием этого проекта, как open source решения. На первом этапе я решил просто выложить проект в open source и обеспечить его кросс-платформенную сборку. Казалось, что никаких сложностей с этим быть не должно, ведь я уже использую кросс-платформенную технологию Qt.

После завершения разработки основного функционала, достаточно просто выложить свой модуль для штатного пакетного менеджера. Так работает для многих языков программирования, но не для С++. Для С++ твои проблемы только начинаются.

Daggy 1.0 и сборочка

Everyone, calm down, this is C++ build!
Everyone, calm down, this is C++ build!

Первая публикация проекта началась с выбора подходящего названия. Я выбрал Daggy, как аббревиатуру от Data Aggregation Utility. Кроме того, английское слово daggy переводится как неопрятный, неряшливый, что частично описывает флоу работы с утилитой. Мы небрежно набрасываем конфиги для агрегации, которые заранее не можем точно определить и только в процессе исследования познаем, какую именно информацию нам нужно агрегировать. Этот подход разительно отличается от ELK систем и ansible скриптов, в которых мы сразу же указываем ровно то, что хотим получить или выполнить.

С названием определились, теперь нужно было придумать версионирование проекта. Версия 0.x была уже обкатана внутри компании, где этот проект был впервые применен и поэтому первая стабильная версия имела номер 1.0. Но кроме непосредственно номера версии, для нового проекта требовалось интегрировать ее вычисление во время сборки.

Версионирование проекта

Хорошая сборка должна иметь свойство повторяемости. Два основных признака повторяемости сборки, это:

  1. У вас есть взаимно однозначное соответствие между версией вашего ПО и коммитом в гит репозитории. Это значит, что по коммиту можно сказать, какая версия ПО соберется, а по версии ПО можно сказать, какому коммиту он соответствует.

  2. При повторной сборке одинаковой версии/коммита вы получите одинаковый результат.

Нельзя сказать, что сборка daggy соответствует требованию повторяемости сборки полностью, но стремится к этому за счет реализации следующих принципов:

  1. Daggy использует семантическое версионирование. При этом первые три цифры, major.minor.patch, берутся из последнего проставленного тега в текущей ветке сборки, а последняя, tweak, рассчитывается как разница в количестве между текущим коммитом и коммитом что содержит последний релизный тег. При этом нулевая цифра для tweak всегда означает, что это релизная сборка. Например:

    • Версия 2.2.1.0 - релизная версия

    • Версия 2.2.1.5 - версия в процессе разработки, что на 5 коммитов отличается от релизной версии 2.2.1.0

  2. Гит репозиторий содержит в себе скрипты сборки в виде github actions (о них поговорим ниже).

Python скрипт вычисляющий версию из git
import os
import re

class GitVersion():
    def __init__(self):
        os.chdir(os.path.dirname(os.path.realpath(__file__)))

    @property
    def tag(self):
        stream = os.popen("git describe --match v[0-9]* --abbrev=0 --tags")
        return stream.read().strip()

    @property
    def version(self):
        version = f"{self.tag[1:]}.{self.build}"
        return version

    @property
    def default_branch(self):
        stream = os.popen("git config --get init.defaultBranch")
        result = stream.read().strip()
        if not result:
            result = "master"
        return result
    
        
    @property
    def build(self):
        stream = os.popen("git rev-list {}.. --count".format(self.tag))
        return stream.read().strip()

    @property
    def branch(self):
        stream = os.popen("git branch --show-current")
        return stream.read().strip()
    
    @property
    def full(self):
        return f"{self.version}-{self.branch}"
    
    @property
    def standard(self):
        standard = f"{self.version}-{self.branch}"
        if self.branch == self.default_branch or re.match("release/.*", self.branch):
            standard = f"{self.version}"
        return standard
    
    @property
    def commit(self):
        stream = os.popen("git rev-parse HEAD")
        return stream.read().strip()


    def __str__(self):
        return f"""
        Tag: {self.tag}
        Version: {self.version}
        Full: {self.full}
        Branch: {self.branch}
        Build: {self.build}
        Standard: {self.standard}
        Commit: {self.commit}
        """

if __name__ == "__main__":
    git_version = GitVersion()
    print(git_version)

Данный подход имеет и очевидный недостаток. Параллельные ветки, имеющие одинакового родителя, могут иметь одинаковую версию разработки. Этот недостаток нивелируется дополнительной информацией, которую можно собрать при сборке проекта из гита (например имя ветки или хеш коммита). Для сборок из гит я использую расширенное версионирование и кроме VERSION дополнительно использую FULL_VERSION определение. Отличия FULL_VERSION от VERSION:

  1. Если проект собирается не из master или релиз ветки, FULL_VERSION будет в себе содержать имя ветки в качестве постфикса. Например: 2.2.0.5-fix/termination, а VERSION стандартное значение 2.2.0.5

  2. Если FULL_VERSION не определено, то это значит, что сборка была осуществлена без git. Например, во время сборки пакетов для пакетных менеджеров.

CMake функция, что вычисляет и проставляет версию проекта из git
include (CMakeParseArguments)

macro(SET_GIT_VERSION)
    cmake_parse_arguments(GIT_VERSION
                          ""
                          ""
                          ""
                          ${ARGN}
    )
    if(NOT DEFINED VERSION)
        find_package(Git REQUIRED)
        if (NOT GIT_FOUND)
            message(FATAL_ERROR "Git not found")
        endif()
        execute_process(COMMAND
                "${GIT_EXECUTABLE}"
                describe
                --match v[0-9]*
                --abbrev=0
                --tags
                WORKING_DIRECTORY
                "${CMAKE_CURRENT_SOURCE_DIR}"
                OUTPUT_VARIABLE
                VERSION_TAG
                OUTPUT_STRIP_TRAILING_WHITESPACE)
        execute_process(COMMAND
                "${GIT_EXECUTABLE}"
                rev-list
                ${VERSION_TAG}..
                --count
                WORKING_DIRECTORY
                "${CMAKE_CURRENT_SOURCE_DIR}"
                OUTPUT_VARIABLE
                VERSION_BUILD
                OUTPUT_STRIP_TRAILING_WHITESPACE)
        execute_process(COMMAND
                "${GIT_EXECUTABLE}"
                branch
                --show-current
                WORKING_DIRECTORY
                "${CMAKE_CURRENT_SOURCE_DIR}"
                OUTPUT_VARIABLE
                VERSION_BRANCH
                OUTPUT_STRIP_TRAILING_WHITESPACE
        )

        execute_process(COMMAND
                "${GIT_EXECUTABLE}"
                config
                --get
                init.defaultBranch
                WORKING_DIRECTORY
                "${CMAKE_CURRENT_SOURCE_DIR}"
                OUTPUT_VARIABLE
                GIT_DEFAULT_BRANCH
                OUTPUT_STRIP_TRAILING_WHITESPACE
        )

        if (NOT GIT_DEFAULT_BRANCH)
            set(GIT_DEFAULT_BRANCH master)
        endif()

        string(REGEX REPLACE "^v" "" VERSION ${VERSION_TAG})
        set(VERSION ${VERSION}.${VERSION_BUILD})
        set(VERSION_FULL ${VERSION}-${VERSION_BRANCH})
        if (VERSION_BRANCH STREQUAL ${GIT_DEFAULT_BRANCH} OR VERSION_BRANCH MATCHES "release/.*")
            set(VERSION_STANDARD ${VERSION})
        else()
            set(VERSION_STANDARD ${VERSION_FULL})
        endif()

        execute_process(COMMAND
                "${GIT_EXECUTABLE}"
                rev-parse
                HEAD
                WORKING_DIRECTORY
                "${CMAKE_CURRENT_SOURCE_DIR}"
                OUTPUT_VARIABLE
                VERSION_COMMIT
                OUTPUT_STRIP_TRAILING_WHITESPACE
        )
    endif()
    message(STATUS
        "Version was set:\n"
        "Tag: ${VERSION_TAG}\n"
        "Version: ${VERSION}\n"
        "Full: ${VERSION_FULL}\n"
        "Branch: ${VERSION_BRANCH}\n"
        "Build: ${VERSION_BUILD}\n"
        "Standard: ${VERSION_STANDARD}\n"
        "Commit: ${VERSION_COMMIT}"
    )
endmacro()

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

Кросс-платформенная conan сборка для portable версии утилиты

Версия daggy 0.x распространялась только в качестве windows бинарного файла, который я собрал у себя на компьютере и полностью со статической линковкой. Для Daggy 1.0 я посчитал, что стоит реализовать следующие требования к системе сборки.

Динамическая линковка с зависимостями. Это было необходимо по следующим причинам:

  1. Qt очень не любит линковаться статически. Пока у вас только один бинарный файл, вы можете не заметить проблем. Но уже в версии 1.0 я задумывался, чтобы вынести основной функционал утилиты в отдельную библиотеку. Проблему можно легко воспроизвести, если вы попытаетесь собрать daggy 2.0 со статической линковкой Qt. В daggy 2.0 это проблема выглядит следующим образом:

    1. libDaggyCore. Основная библиотека, которая линкуется с Qt Core и с Qt Network. Библиотека обладает интерфейсом и include файлами, поставляемые вместе с проектом, поэтому ее желательно собирать как динамическую библиотеку.

    2. Daggy. Утилита, что линкуется с libDaggyCore и с Qt Core (для использования QCoreApplication)

    3. Qt. Поскольку Qt используется и в daggy и в libDaggyCore, то при статической линковке Qt вы получите конфликт глобальных объявлений внутри самой Qt библиотеки (проблема двойного проникновения Qt в ваш бинарный файл). Поэтому, нужно либо линковать libDaggyCore тоже статически, или использовать динамическую линковку с Qt.

      Qt double penetration problem
      Qt double penetration problem
  2. Мейнтейнеры linux дистрибутивов не рекомендуют статическую линковку. Если вы хотите добавить ваш пакет в linux репозитории, то избегайте статической линковки на сколько это возможно, ее очень не любят мейнтейнеры.

  3. Проблемы с лицензией из-за статической линковки. Несмотря на то, что daggy распространяется под лицензией MIT, проблемы могут возникнуть у тех, кто будет использовать проект. Если бы я так и оставил только статическую линковку, то всем потребителям моего проекта все

Сборка должна позволять нативную интеграцию в как можно большее количество платформ. На практике это значит, что ваша сборка должна быть легко поддерживаемой мейнтейнерами дистрибутивов и пакетных менеджеров. Для daggy это удалось достичь за счет разделения cmake сборки на два уровня абстракции:

  1. СMakeLists.txt в корне пректа. Верхнеуровневый cmake, используется только для разработки и позволяет запустить проект из IDE. Например, для того, чтобы можно было запустить проект из Qt Creator, нужно:

    • Установленный в системе conan

    • Conan_provider.cmake файл из репозитория cmake wrapper

    • Прописать глобальные опции cmake:

      1. -DQT_CREATOR_SKIP_CONAN_SETUP=ON

      2. -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${путь до директории с провайдером}/conan_provider.cmake

    • Открыть CMakeLists.txt из корня репозитория. В этом случае считаются текущие настройки окружения Qt Creator, затем они трансформируются в conan профиль и весь проект соберется вместе с зависимостями и с этими настройками

  2. CMakeLists.txt в src. Основной файл с описанием проекта использующий имеющий следующие особенности:

    • Все внешние зависимости подключаются через find_package

    • Version.cmake высчитывает версию из git репозитория при сборке или берет ее из переменной VERSION, если она была выставлена как cmake definition

src/CMakeLists.txt может подключаться различными окружениями сборки
src/CMakeLists.txt может подключаться различными окружениями сборки

Сборка as code. В С++ часто распространены большие инструкции для сборки, в которых сначала необходимо настроить свое локальное окружение. Я посчитал, что для open source такой подход порочен, поскольку делает сборку проекта не прозрачной и тяжелой для ее поддержки другими разработчиками. Чтобы сборка была прозрачной и легко воспроизводимой, она должна быть частью кода репозитория проекта.

Conan portable build - это основная система сборки для разработки и частично для распространения. На выходе она производит 4 архива с утилитой и библиотекой под разные платформы. Архивы под разные платформы генерируются с помощью cpack. Каждый архив тянет за собой все зависимости в виде динамических библиотек, за исключением C/C++ окружения.

Артефакты сборки conan portable build
Артефакты сборки conan portable build

Формально, можно было бы остановится на этой conan portable системе сборки, но она имеет следующие недостатки:

  1. Бинарники без подписи враждебно воспринимаются в Windows и MacOS системах. Придется заставлять пользователя игнорировать предупреждения о безопасности, но я сталкивался и с тем, что антивирус просто удалял мой бинарный файл и не давал его даже распаковать из архива. Для windows есть вариант получить бесплатную подпись с помощью проекта SignPath. Для macos есть вариант регистрации аккаунта разработчика или распространение через homebrew. И SignPath и homebrew варианты будут описаны в следующем разделе.

  2. Conan все еще нельзя использовать как пакетный менеджер С++. Conan держится на энтузиазме его разработчиков. Поэтому, на текущий момент, он имеет следующие проблемы:

    • Qt официально не поддерживает conan. Последняя версия Qt, что в нем опубликована, это qt6.7.3. Эта версия вышла в сентябре 2024г. С тех пор в марте 2025 у Qt вышел релиз 6.9.0, но его до сих пор не смогли вмержить в conan-center-index. При этом digia делала некоторые попытки самостоятельно оседлать conan движение и выпустила свой conan репозиторий, но похоже там все заглохло. В Qt Creator есть так же поддержка conan, но у меня она не завелась из-за наличия скрипта, что вычисляет версию из git и инклудится в мой conan рецепт.

    • Отсутствуют релизы. Основной поддерживаемый conan репозиторий - это conan-center-index. В нем отсутствуют релизы как таковые. Это значит, что если вы жестко не укажете версию зависимостей (включая хеш суммы бинарных пакетов), то вы рискуете в один из моментов просто потерять собираемость ваших зависимостей.

    • Conan нельзя использовать в качестве системного пакетного менеджера. Не смотря на то, что conan есть в качестве пакета во многих дистрибутивах, использовать его как дефолтный менеджер пакетов для С++ проектов вряд ли возможно. Представим себе ситуацию, что вы обновили gcc до версии 15. В gcc 15 убрали некоторые инклуды по умолчанию. Если ваш пакет явно не инклудит <cstdint>, но при этом использует типы оттуда, то он просто не соберется у конечного пользователя. Частично проблему могла бы решить полная пересборка пакетов с выходом новых версий компиляторов и тогда мы бы увидели некое подобие релизов. На текущий момент собираемость conan пакетов лежит на плечах конкретных мейнтейнеров. Собираемость же всего репозитория или какого-то подмножества зависимостей никак не гарантируется.

    • Conan подходит для фиксации текущей версии сборки, но не для непрерывного выпуска обновлений. Исходя из описанных выше проблем, каждое обновление версий зависимостей может превратиться в “приключение на 20 минут” для каждой из платформ. При этом есть риск, что вы наткнетесь на проблемы, которые без патча ваших зависимостей просто не решаются. Это большая боль не только conan, но и всего мира С++ разработки. На текущий момент, эта проблема решается только в контексте релизных сборок linux дистрибутивов (ниже поговорим о сборке Fedora)

  3. Portable версии не интегрируются в систему. Нужно самостоятельно прописывать утилиту в PATH и следить за ее обновлениями. При этом после каждого обновления придется убеждать систему, что вы не вирус обновляете.

RPATH и portable сборка. Как убить систему одним пакетом

RPATH - это путь, который вы можете прописать внутри вашего бинарного файла, по которому он будет искать библиотеки и другие исполняемые файлы. Мейнтейнеры не любят, когда используют rpath, однако для portable сборок на macos и linux установка rpath необходима. В daggy portable сборке rpath для исполняемых файлов выглядит вот так:

if(CONAN_BUILD)
    if(APPLE)
        set_target_properties(${TARGET} PROPERTIES INSTALL_RPATH "@executable_path/../${CMAKE_INSTALL_LIBDIR}")
    elseif(UNIX)
        set_target_properties(${TARGET} PROPERTIES INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}")
    endif()
endif()

Для основной библиотеки DaggyCore rpath прописывается по другому:

if(CONAN_BUILD)
    if(APPLE)
        set_target_properties(${TARGET} PROPERTIES INSTALL_RPATH "@loader_path")
    elseif(UNIX)
        set_target_properties(${TARGET} PROPERTIES INSTALL_RPATH "$ORIGIN")
    endif()
endif()

Данная схема описывает следующее расположение ваших файлов:

  1. bin - утилита. Утилита ожидает зависимости в ‘../lib’

  2. lib - библиотека. Библиотека ожидает свои зависимости в ‘.’

Эта схема установки rpath подходит только для zip архивов вашего приложения, но не подходит для deb/rpm пакетов. Если создать rpm/deb пакет, что устанавливает нашу библиотеку libDaggyCore в /usr/lib, то зависимости daggy просто перепишут системные библиотеки своими. Например, если в наших зависимостях openssl, то он просто заменит системную openssl библиотеку и это может привести к неработоспособности всей системы.

Чтобы этого не происходило, для зависимостей вашей утилиты и приложения стоит создать отдельную папку и тогда схема rpath будет выглядеть сложнее:

  1. bin - утилита. Утилита ожидает зависимости в ‘../lib’ и ‘../lib/daggy’

  2. lib - библиотека. Библиотека ожидает зависимости в ‘./daggy’

Для бинарного файла:

if(CONAN_BUILD)
    if(APPLE)
        set_target_properties(${TARGET} PROPERTIES INSTALL_RPATH "@executable_path/../${CMAKE_INSTALL_LIBDIR};@executable_path/../${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}")
    elseif(UNIX)
        set_target_properties(${TARGET} PROPERTIES INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR};$ORIGIN/../${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}")
    endif()
endif()

Для библиотеки:

if(CONAN_BUILD)
    if(APPLE)
        set_target_properties(${TARGET} PROPERTIES INSTALL_RPATH "@loader_path/${PROJECT_NAME}")
    elseif(UNIX)
        set_target_properties(${TARGET} PROPERTIES INSTALL_RPATH "$ORIGIN/${PROJECT_NAME}")
    endif()
endif()

Отдельно про устройство conan сборки писать тут не буду, в интернете и так достаточно туториалов на эту тему. Отмечу лишь, что при всех недостатках conan, я считаю, что  с его первым релизом историю языка С++ можно делить на до и после. Теперь проблема сборки на С++ хотя бы имеет прогнозируемость и управляемость. Попробуйте без conan, например, быстро убедиться, соберется ли ваш проект на новой платформе и с новым компилятором. Но для того, чтобы можно было собирать ваш проект под разными платформами нужно еще управление окружениями сборки. 

Проект daggy для окружений и скриптов сборки использует github actions.

Скрипты сборки github actions

Github actions - это хорошо задокументированная технология, поэтому тут остановлюсь только на основных или не очевидных аспектах:

  1. Бесплатно для open source. Не все агенты доступны для бесплатного использования, но те что есть мне хватает (windows, ubuntu, macs-arm, macos-x64). При этом лимитов на использование нет.

  2. Скрипты сборки хранятся вместе с репозиторием. И это очень хорошо. В теории можно добиться полной повторяемости сборки, вплоть до совпадения хеш сумм бинарников если:

    • Прописать версии агентов, на которых выполняется сборка

    • Прописать хеши всех зависимостей в conan или использовать conan lock files.

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

    • Добавить github attestations для регистрации сборки и ее дальнейшей верификации всеми потребителями. Необязательный шаг, но прибавит доверия к вашей повторяемой сборке.

  3. Есть кеширование. Это очень необходимая для проекта фича, потому что полная сборка зависимостей daggy для каждой из платформ занимает около часа. Кеш хранится только для ветки и наследников. Это значит, что если вы создали кеш для master ветки, то он будет доступен для всех веток, что отбренчевались от мастера. Но если вы создали кеш в feature ветке, а потом слили ее в мастер, то в мастере этот кеш доступен не будет.

  4. Изоляция workflows. Нельзя передавать окружения из одного workflow в другой. Это значит, что если вы в процессе одного workflow установили python и conan, получили артефакты сборки, то другой workflow не сможет получить это окружение. По этой причине пришлось отдельно делать workflow который сначала проверяет есть ли кеш, отдельный кеш для сборки зависимостей, если кеш оказался пустой и отдельный workflow для непосредственной сборки

daggy github actions
daggy github actions
Интересный факт. Apple Silicon vs Apple Intel

Сборка daggy на Apple Silicon агенте в среднем происходит в 1.5/2 раза быстрее, чем на Apple Intel агенте

Общие выводы по сборочке

Если оценить время затраченное на сборочку, то на это наверное ушло не менее половины всего затраченного на проект времени. Однако, мне во многом повезло, потому что я мог не бороться с легаси сборкой, а сразу сделал сборку с чистого листа, не имея никаких ограничений. В большинстве случаев сборка на С++ - это легаси месиво из кастомных perl/ruby/python/bash/make скриптов.

Если вы так же соберетесь делать свою сборочку для кросс-платформенного проекта, то озаботьтесь сразу же следующими вопросами:

  1. Как вы будете выставлять версию и какие гарантии предоставляет ваше версионирование

  2. Каким образом вы будете скачивать зависимости, линковаться с ними во время сборки и деплоить их в составе вашего ПО. Немаловажный вопрос, как вы предоставите окружение для разработчика

  3. Если в ваших зависимостях есть проекты уровня Qt Framework, подумайте о кешировании ваших зависимостей.

  4. Нужна ли вам совместимость с пакетными менеджерами платформ, куда вы планируете поставлять ваше ПО

Если у вас нет ответов на какие то из вышеперечисленных вопросов, то вы можете посмотреть, как это сделано для проекта daggy. Но проблема не только в борьбе со сборкой зависимостей под разные платформы, но и с тем, что на разных платформах ваш проект будет вести себя по разному. Например, я использовал ssh утилиту для удаленного подключения и запуска команд для стриминга, но внезапно оказалось, что ssh для windows не умеет создавать master соединения. Как обходить подобного рода проблемы и организовать архитектуру кросс-платформенного приложения, чтобы она была максимально адаптирована под конкретную платформу обсудим в следующем разделе.

Daggy 2.0 и архитектура

Everyone, calm down, this is C++ architecture!
Everyone, calm down, this is C++ architecture!

Основной задачей релиза Daggy версии 1.0 был вывод проекта из состояния домашней утилиты в кроссплатформенный публичный проект. В релизе Daggy 2.0 была пересмотрена вся концепция проекта и заложен архитектурный каркас для его дальнейшего развития.

В предыдущем разделе мы рассматривали флоу работы с утилитой daggy. Суть этого флоу, что мы включали утилиту только на время проведения теста. Во время работы утилиты мы складировали все данные в отдельной папке. Но ключевая особенность этого флоу в том, что когда утилита запущена, она не просто подсоединяется к источнику и извлекает оттуда данные. Утилита стримит потоки данных в run-time!

Исходя из этой особенности у меня возникла мысль, а что если daggy рассмотреть не как утилиту для агрегации данных, а как некоторую экосистему, которая позволяет описывать данные для стриминга в декларативном стиле и выполнять с ними действия на ходу? Например, мы можем сделать gui приложение, что сочетает в себе wireshark, видео просмотрщик, kibana, которые могут отображаться по разному в зависимости от расширения, что было указано в конфиге для стрима. Или некоторая утилита, что не сохраняет потоки данных, а позволяет производить с ними операции на ходу, например, используя скриптовый язык lua? При такой постановке вопроса стало очевидно, что утилита daggy - это лишь один из возможных вариантов реализации концепции декларативного стриминга и обработки данных.

У меня были сложности с тем, чтобы описать зачем нужна утилита daggy. Теперь я попробую описать концепцию на порядок сложнее: экосистема декларативного стриминга и агрегации.

Экосистема декларативного стриминга и агрегации

Daggy System for declarative streaming and aggregation
Daggy System for declarative streaming and aggregation

Весь флоу декларативного стриминга и обработки данных можно описать в следующих терминах:

  1. Вокруг нас в каком то виде существуют потоки данных (data). 

  2. Данные существуют не сами по себе, а в определенных окружениях (environments)

  3. В какой то момент мы хотим начать собирать данные и для этого создаем сессию (session)

  4. Поскольку окружения разные, то для того, чтобы начать извлекать из них потоки данных нам необходимы провайдеры источников данных (data source provider)

  5. Для каждого провайдера мы описываем то, каким образом он будет из окружения извлекать данные (data source commands)

  6. Совокупность всех провайдеров и команд для одной сессии мы называем источниками данных (data sources)

  7. Daggy Core берет на вход источники данных, а на выходе создает сессию, в которой каждая команда (command) имеет уникальный идентификатор и тип. Для каждой команды Daggy Core поставляет непрерывный поток данных (stream)

  8. Потоки данных могут просматриваться в рантайме просмотрщиком (streams viewer) или собираться различными агрегаторами (streams aggregator).

В терминах перечисленных выше, утилита daggy является streams aggregator. При этом библиотека DaggyCore позволяет добавить streams viewer, но пока нет реализации этой концепции.

Флоу работы с библиотекой такой можно описать через интерфейс daggy::Core:

  • В конструкторе указываем на data sources (задаем имя сессии или оно сгенерируется автоматически):

Core(Sources sources,
         QObject* parent = nullptr);
Core(QString session,
     Sources sources,
     QObject* parent = nullptr);
  • Подготавливаем провайдеры, указанные в Sources, к работе и запускаем стримминг:

std::error_code prepare();
std::error_code start() noexcept;
  • Теперь можно подключиться к сигналам через Qt signals slots механизм и слушать стримы комманд:

signals:
    void stateChanged(DaggyStates state);

    void dataProviderStateChanged(QString provider_id,
                                  DaggyProviderStates state);
    void dataProviderError(QString provider_id,
                           std::error_code error_code);

    void commandStateChanged(QString provider_id,
                             QString command_id,
                             DaggyCommandStates state,
                             int exit_code);
    void commandStream(QString provider_id,
                       QString command_id,
                       sources::commands::Stream stream);
    void commandError(QString provider_id,
                      QString command_id,
                      std::error_code error_code);

На момент написания статьи в проекте было реализовано три провайдера данных:

  1. Local - локальный источник данных, запускает процессы локально

  2. Ssh - удаленный источник данных, запускающий процессы по протоколу ssh и использующий утилиту ssh

  3. Ssh2 - удаленный источник данных, запускающий процессы по протоколу ssh2 и использующий библиотеку libssh2

Но зачем понадобилось два провайдера для запуска ssh процессов? Потому что кросс-платформенной поддержки ssh не существует.

Ssh vs ssh2 providers

В версии daggy 0.x я использовал qtssh библиотеку, которая как таковой библиотекой и не являлась. Это был просто копипаст из исходников Qt Creator. В версии daggy 1.0 вопрос лицензии проекта был одним из требований сборки, а любой копипаст, даже самых открытых лицензий, всегда нежелателен и на него очень плохо смотрят мейнтейнеры. Поэтому, в версии daggy 1.0 встал вопрос о полноценной реализации ssh протокола.

Изучив вопрос, оказалось, что версий протокола ssh как минимум две. ssh2 отличается от протокола ssh в следующих аспектах:

  1. Ssh2 поддерживает создание каналов в пределах одного tcp соединения. Многие могут возразить, что стандартная ssh утилита имеет функциональность ControlMaster, но с этим есть две проблемы:

    • Это надстройка openssh, а не свойство протокола. На стороне клиента при этом используется unix domain socket. 

    • По этой причине отсутствует реализация ControlMaster для Windows. Libssh2 в тоже время работает одинаково во всех платформах

  2. Ssh2 поддерживает более безопасные алгоритмы шифрования и подписи

Мой выбор пал на библиотеку libssh2 и я написал Qt обертку над этой библиотекой. Довольно долго этой реализации мне было достаточно, пока я не наткнулся на ограничения этой библиотеки. Я столкнулся с очень сложным ssh подключением. Подключение шло через прокси сервера и с чтением ключей по протоколу pkcs11 с usb девайса. Реализовать такой флоу для библиотеки libssh2 вряд ли возможно, потому что в ее интерфейсе нет вообще ничего про чтение токенов по протоколу pkcs11. Кроме того, в ssh конфигах есть десятки полей, которые очень долго переносить на реализацию ssh2. Поэтому я решил добавить еще один провайдер, который работает с ssh как с утилитой.

Ssh провайдер запускает каждую ssh команду как процесс. Для linux и macos создает мастер соединение, а для Windows каждый раз создает новое ssh подключение. Конечный выбор провайдера можно сделать по следующему алгоритму:

  1. Если у вас простое ssh подключение (по логину и паролю или через ключ в файле), то всегда выбирайте ssh2

  2. Если у вас сложное ssh подключение (через прокси, с чтением pkcs11 и т.д.), то выбирайте ssh провайдер.

Никогда не собирайте трафик с 22 порта

В workshop по утилите мы использовали следующую команду для стриминга трафика:

net:
   exec: tcpdump -i any -s 0 port not 22 -w -
   extension: pcap

Но почему нужно исключить port 22, если нам интересен весь трафик? Проблема в том, что если вы запустите такую же команду для удаленного окружения через ssh или ssh2 провайдеры, то получите лавинообразное увеличение трафика, которое будет просто брутфорсить сетевой трафик удаленного хоста по следующему алгоритму:

  1. Вы подключаетесь к удаленному хосту по 22 порту

  2. Вы запускаете команду tcpdump, что отправляет вам весь трафик, включая трафик по 22 порту

  3. Весь переданный трафик на ваш локальный хост дублируется и передается в следующей итерации

  4. С каждой итерацией вы будете получать x2 увеличение трафика пока не упретесь в пределы сетевого соединения между вами и удаленным хостом.

Это одна из причин, почему daggy лучше использовать только на тестовых окружениях или запускать ее только на уже известных и отлаженных data sources конфигурациях. Невнимательный подбор команд может просто вывести из строя продакшн окружение.

Запуск одних и тех же команд через разные провайдеры не всегда возможен

Может показаться, что команды могут запускаться одинаково через разные провайдеры, но это не так. В предыдущем абзаце мы разобрали, что для удаленных ssh подключений нужно исключить 22-ой порт. Но даже для ssh и ssh2 провайдеров есть разница.

Например, если вы запускаете уже известную нам команду для прослушивания всего трафик через ssh2 провайдер, то она успешно отработает. Но для ssh нужно внести дополнительные правки, а именно перенаправление stderr в /dev/null:

net:
   exec: tcpdump -i any -s 0 port not 22 -w - 2>/dev/null
   extension: pcap

Перенаправление ошибок в /dev/null необходимо, потому что ssh провайдер запускает команды как процессы ssh утилиты. Ssh утилита через stdout отправляет весь трафик, включая трафик с ошибками, что не является частью pcap файла. Поэтому, если вы не добавите “2>/dev/null” и попробуете открыть pcap файл через wireshark, то он вам справедливо выдаст ошибку о несоответствии формату.

Кроме 2-х ssh провайдеров есть еще один, local провайдер, который играет важную роль в тестировании.

Автотесты для DaggyCore

Local провайдер позволяет запускать локальные процессы, но используется также для автоматического тестирования (через команду ctest). Тестирование ssh сложнее автоматизировать,чем тестирование локального окружения, поэтому только Local provider тестируется с помощью Qt Tests framework. Вот так выглядит основной тест для DaggyCore:

void DaggyCoreLocalTests::startAndTerminateTest()
{
    QFETCH(QString, type);
    QFETCH(QString, data);

    Sources sources;
    QString error;
    if (type == "json")
        sources = std::move(*sources::convertors::json(data, error));
    else
        sources = std::move(*sources::convertors::yaml(data, error));

    QVERIFY(error.isEmpty());
    QCOMPARE(sources, test_sources);

    Core core(std::move(sources));
    const auto& session = core.session();
    QCOMPARE(core.prepare(error), errors::success);

    QVERIFY(core.state() == DaggyNotStarted);
    QCOMPARE(core.sources(), test_sources);

    QSignalSpy states_spy(&core, &Core::stateChanged);
    QSignalSpy streams_spy(&core, &Core::commandStream);

    QTimer::singleShot(0, &core, [&]()
    {
        auto result = core.start();
        QCOMPARE(result, errors::success);
    });

    QVERIFY(states_spy.wait());
    QVERIFY(!states_spy.isEmpty());
    auto arguments = states_spy.takeFirst();
    QCOMPARE(arguments.at(0).value<DaggyStates>(), DaggyStarted);

    QTimer::singleShot(3000, &core, [&]()
    {
        core.stop();
    });


    QVERIFY(states_spy.wait());
    QVERIFY(!states_spy.isEmpty());
    arguments = states_spy.takeFirst();
    QCOMPARE(arguments.at(0).value<DaggyStates>(), DaggyFinishing);

    QVERIFY(states_spy.wait());
    QVERIFY(!states_spy.isEmpty());
    arguments = states_spy.takeFirst();
    QCOMPARE(arguments.at(0).value<DaggyStates>(), DaggyFinished);
    streams_spy.wait();
    QVERIFY(!streams_spy.isEmpty());

    QMap<QString, QList<sources::commands::Stream>> streams;
    for (auto command_stream : streams_spy) {
        auto command_id = command_stream[1].toString();
        auto stream = command_stream[2].value<sources::commands::Stream>();
        QVERIFY(!stream.part.isEmpty());
        QCOMPARE(stream.meta.session, session);
        streams[command_id].push_back(stream);
    }

    auto stream_keys = streams.keys();
    std::sort(stream_keys.begin(), stream_keys.end());

    QList<QString> sources_keys;
    for (const auto& command : test_sources) {
        sources_keys += command.commands.keys();
    }
    std::sort(sources_keys.begin(), sources_keys.end());

    QCOMPARE(stream_keys, sources_keys);

    QVERIFY(!streams["pingpong_once"].isEmpty());
    QVERIFY(!streams["pingpong_restart"].isEmpty());
    QVERIFY(!streams["pingpong"].isEmpty());
}

Этот тест является smoke тестом, который проверяет общую работоспособность DaggyCore. Поскольку он запускается с использованием local провайдера, то он может легко запускаться при встраивании в любые системы сборки, включая пакетные менеджеры. Обратите внимание на асинхронные вызовы. 

Наличие в Qt своего Tests Framework одна из причин, почему Qt С++ стек был выбран в качестве основного стека технологий для разработки daggy, но это не единственная причина.

Почему Qt C++?

Когда стал понятен общий замысел проекта, можно вернутся к вопросу о выборе технологий.

Причины, по которым я остановился в выборе Qt C++:

  1. Qt позволяет проектировать простые асинхронные интерфейсы. Обратите внимание на тест в предыдущем абзаце и на эту строку: QSignalSpy streams_spy(&core, &Core::commandStream); Одной строкой мы подписываемся на стрим и больше ничего делать не нужно. Таким образом, если вы сумели побороть сборку и используете Qt в вашем приложении, то для того, чтобы начать стримить данные достаточно указать экземпляру DaggyCore на источник данных и еще одной строкой подсоединить signal к нужному slot. Все, кто используют Qt у себя в проектах легко могут смержиться с другими Qt библиотеками имеющими асинхронный интерфейс. Я не встречал настолько простой концепции асинхронного взаимодействия.

  2. Qt имеет свой framework для асинхронного тестирования. Простая асинхронная модель позволяет писать простые асинхронные тесты

  3. Qt и С++ не имеют ограничений в реализации. Qt из коробки имеет каскад приложения в виде QtCoreApplication и простую асинхронную модель работы приложения. Мы получаем возможность писать приложения любого рода, такие как Gui, нагруженные сервисы, системные приложения или консольные утилиты. 

  4. Qt имеет port для python. Теоретически все, что написано как Qt класс может быть портировано в python библиотеку. Для python работа с данными в контексте data science имеет важное значение. Правда, я пока сборку для python не осилил (но пытался).

Qt C++ стек конечно же имеет и серьезные недостатки:

  1. Сложность сборки С++/Qt проектов. Как собирать Qt кроссплатформенные приложение было описано выше. Это довольно не простой процесс для любого С++ проекта, а Qt добавляет еще больше сложности.

  2. Qt не воспринимают как framework для не gui приложений. До сих пор встречаюсь с этим убеждением, что Qt только для интерфейсов. Это сильно тормозит внедрение Qt в проекты и соответственно будет тормозить и внедрение любого проекта на Qt.

Daggy в версии 0.x была домашней утилитой для Windows, что позволяла аггрегировать данные на тестовых стендах компании, в которой я работал.

Daggy в версии 1.0 стала кроссплатформенной утилитой для агрегации данных.

Daggy в версии 2.0 превратилась в экосистему для декларативного стриминга и сбора данных. Утилита для агрегации в daggy 2.0 стала частным случаем этой экосистемы. Сложность проекта увеличилась, а значит нужно снова задуматься, а каким образом проект распространять и позиционировать?

Daggy. Распространение и позиционирование

Everyone, calm down, this is C++ architecture!
Everyone, calm down, this is C++ architecture!

До сих пор самым успешным сценарием распространения проекта daggy был тот, когда мне удалось просто показать коллегам, как работает утилита. Я хотел масштабировать этот подход на мир open source, но сразу же столкнулся с основным препятствием: для С++ не существует простого пути, чтобы просто установить артефакты вашего проекта в системе. Мне пришлось рассмотреть и разработать несколько способов доставки приложения и библиотеки до потенциальных пользователей. Начнем с кросс-платформенных вариантов.

Portable архивы

В разделе о conan portable сборке было описано, каким образом можно этот вариант реализовать.

Плюсы распространения:

  1. Единый подход во всех платформах. Хотя сами по себе архивы не одинаковые и имеет свои особенности для каждой из платформ

  2. Простое решение. Достаточно cmake + cpack для создания архивов и больше никакой дополнительной работы

Минусы:

  1. Windows и MacOS настроены враждебно по отношению к вам. На Windows и MacOS ваши бинарные архивы воспринимаются системой “как вирусы”, поэтому каждый потенциальный пользователь вашего проекта должен прописать в системе разрешение на запуск и линковку ваших бинарников 

  2. Нужны дополнительные действия пользователя. Чтобы пользователь мог запустить или слинковаться с библиотекой из вашего проекта, нужно чтобы он организовать окружение сборки.

На первый взгляд кажется, что минусы не существенны, но просто поставьте себя на место пользователя:

  1. Вы видите open source проект у которого еще нет звезд и форков или их очень мало

  2. Все же решаетесь посмотреть и скачиваете архив с portable версией

  3. MacOS/Windows предупреждают вас “о угрозе вируса”

  4. Вы просите ОС довериться скачанным файлам и теперь пытаетесь слинковаться с библиотекой, а также каким либо образом установить portable версию утилиты в системе.

Не слишком ли много действий для того, чтобы просто посмотреть, как работает проект? Как много пользователей решится на такое тем более, что описание проекта сложнее чем флоу его использования? Я посчитал, что portable версия недостаточна и нужно найти более нативное решение.

Conan Center Index

Мы рассматривали conan в качестве менеджера зависимостей для проекта, однако через него можно точно так же и распространять наш С++ проект. 

Плюсы:

  1. Идеальное решение для библиотек. Если вам удалось добавить библиотеку в conan-center-index, то всем conan пользователям достаточно добавить вашу библиотеку в зависимости.

Минусы:

  1. Сложно добиться аппрува. Первый мой пулл реквест в conan-center-index прошел достаточно быстро, но второй висит уже неделю без движения на стадии “получить аппрув для сборки”.

  2. Нет стабильной поддержки зависимостей. Последняя версия Qt 6.7.3, что есть в репозитории, вышла еще в сентябре 2024г. На текущий момент последняя версия Qt 6.9.0, вышла еще в марте. При этом есть два закрытых, но не вмерженных пулл реквеста с обновлением версии qt (первый, второй).

  3. Плохая повторяемость сборки. Пользователь может обновить систему и утратить собираемость вашей библиотеки.

Qt Marketplace

Идея Qt Marketplace - создать место для распространения Qt приложений, но идея скорее мертва, чем жива. На текущий момент, это просто витрина Qt приложений, без инфраструктуры для их распространения.

Проект Daggy представлен на этой витрине.

На этом кросс-платформенные варианты закончились, рассмотрим платформо зависимые.

Windows

Самым правильным вариантом распространения windows приложений является распространение через microsoft store. Тем более, что регистрация там бесплатная и можно зарегистрироваться от лица проекта. Однако, store имеет строгие правила валидации, из которых следует, что нельзя запускать процессы системы из вашего приложения. По этой причине я видел рекомендацию собирать qt без qprocess. Но для реализации daggy требуется запуск и менеджмент процессов, поэтому пришлось этот вариант отбросить сразу же.

Как такого пакетного менеджера в Windows нет, но есть проект https://chocolatey.org/. Я видел его использование для систем сборок, например, в github actions его можно использовать в качестве менеджера зависимостей для windows, но на практике ни разу не сталкивался и кажется с точки зрения конечных пользователей он не очень распространен. Поэтому, chocolatey я отложил до лучшего времени.

Последним вариантом остается распространение msi пакетов, что является наиболее распространенной практикой для Windows. Но чтобы msi пакет штатно воспринимался системой, он должен быть подписан.

Для подписи проекта можно воспользоваться сервисом signpath. Проект дает возможность подписывать open source проекты бесплатной подписью. На хабре есть хорошая статья, как получить подпись и установить ее для сборки в github actions. На момент написания статьи я не успел получить подпись и интегрировал только тестовый сертификат. Как только получу подпись, откроется вариант создания полноценного msi пакета для windows.

MacOS

Распространение проекта через apple store я сразу отбросил, потому что нужна платная регистрация как Apple разработчика (а я им по большому счету не являюсь). Но для macos есть вполне хороший вариант распространения через homebrew менеджер пакетов, который хоть и не является официальным, но очень распространен среди macos пользователей.

Плюсы homebrew:

  • Стабильная и согласованная версия core зависимостей. У homebrew нет релизов, но есть core подмножество пакетов, которые поддерживают в максимально актуальном состоянии и которые при этом не конфликтуют друг с другом. В моем случае, я указал зависимость на три библиотеки, все из которых входят в homebrew core и при установке daggy будут ссылаться на последние версии:

  depends_on "libssh2"
  depends_on "qt"
  depends_on "yaml-cpp"
  • Простая поддержка third-party репозиториев. Homebrew поддерживает концепцию taps для third-party-repositories. Вот так можно установить daggy с помощью homebrew:

brew install --build-from-source synacker/daggy/daggy

Минусы homebrew:

  1. Только MacOS. Есть также поддержка linux, но мой пакет на linux не собрался (разбираться не стал)

  2. Если не добавить ваш проект в Core, то сборка вашего tap может сломаться в любой момент. Плюс наличия Core версий может стать и минусом. Сборка вашего проекта может сломаться после несовместимого апдейта зависимостей из core версии. Защититься от этого теоретически можно, указав версии конкретных рецептов, но такие рецепты, во-первых, должны поддерживаться разработчиками, и во-вторых не факт что вы получите собираемость с другими вашими зависимостями в таком случае.

Ссылка на формулу для homebrew.

Fedora

Как добавить свой пакет в Fedora, можно почитать здесь. Если в кратце, то процесс состоит из следующих пунктов:

  1. Создаете задачу “review request” с описанием проекта.

  2. Ждете аппрува от maintainer sponsor

  3. Ищите мейнтенера для вашего проекта или становитесь им самим.

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

После получения аппрува для моего пакета, я еще стал его мейнтейнером. И на мой взгляд, в Fedora реализована лучшая концепция для распространения С/С++ приложений на текущий момент. Вы получаете каждые полгода гарантированно собираемый репозиторий с С++ пакетами, который еще и содержит почти самые свежие версии. При этом если вы хотите обновится, то вы переходите на следующий релиз, а вся боль с разрешением конфликтов ваших зависимостей была разрешена мейнтейнерами Fedora. 

Ссылка на репозиторий для пакета Fedora.

Справившись с Fedora я хотел аналогичным образом добавить пакет и в debian, но почти сразу понял, что я просто не понимаю, как сделать deb пакета, готовый для добавления в debian репозиторий.

Debian, да что с тобой не так?

Для того, чтобы создать rpm пакет для Fedora, есть один общий туториал описывающий путь, как это сделать. Для того, чтобы сделать аналогичный deb пакет для debian существует бесконечное множество вариантов, и я до сих пор не понимаю, какой из них актуальный.

Я даже обратился к debian сообществу на reddit, в котором спросил какой-то общепринятый и актуальный вариант, как подготовить debian пакет. Я получил множество ответов следующего вида:

  1. Вот тебе мои скрипты, они работали прошлый раз

  2. Что сложного, нужно просто разобраться как устроен deb пакет полностью

  3. Вот моя статья из начала 2000-х, но я не уверен что она еще актуальная

  4. Попробуй вот этот вариант, я слышал, что он работал у сына маминой подруги

Больше всего мне понравился комментарий от одного из пользователей debian, который потратил два года на поиск рабочего варианта:

К сожалению, я был не готов погружаться в debian настолько глубоко, чтобы найти свой рабочий вариант. В итоге я просто оставил эту идею.

Остальные Linux дистрибутивы

Кроме Fedora я знаю, что проект в каком то виде добавлен в Arch Linux и FreeBSD. Но добавлением туда занимались уже другие люди и тут я не могу рассказать каких либо подробностей.

Рад, что вы спросили!

Когда повидал некоторый С++ в open source
Когда повидал некоторый С++ в open source

С++ является кошмарным выбором для вашего Open Source проекта. Уже больше 5 лет я пытаюсь в свободное от работы время добиться решения простой задачи: как сделать так, чтобы потенциальный пользователь мог просто попробовать на практике то, что я сделал. 

Много раз я думал переписать уже все на go или rust и забыть про проблемы распространения C++ приложений как кошмарный сон. Но каждый раз я понимаю, что любой другой язык не даст мне такой свободы в реализации своих идей относительно проекта.

Я вижу мир, как непрекращающиеся потоки данных, что переплетаются и влияют друг на друга. Я хочу видеть и исследовать эти потоки. Я надеюсь, что когда нибудь проблемы вида “undefined reference” или “Package xaw7 was not found in the pkg-config search path” перестанут меня преследовать после каждой неосторожной попытки обновить систему или зависимости проекта. И тогда я наконец смогу заняться развитием экосистемы для декларативного стриминга и агрегации.

Проблемы сборки и распространения C++ проекта преследуют меня и связывают мне руки. Если бы я заранее знал, какой путь меня ожидает, то я вряд ли бы по нему пошел. Но сейчас я не хочу останавливаться и буду продолжать делать то, что должен и да свершится то, чему суждено. Кто знает, может наконец придет ИИ и поможет продвинуться по проекту дальше?

P.S.: ИИ помоги

Самое важное в этой ситуации не оказаться Марвином!
Самое важное в этой ситуации не оказаться Марвином!

Недавно в С++ сообществе состоялся подкаст, на котором обсуждалась угроза замены разработчиков ИИ агентами.

У меня есть список задач, готов ими поделиться с любым ИИ-агентом. Если появится ИИ-агент, который реально сможет взяться за любую из задач (а может даже за несколько), я готов эти задачи сформулировать точнее. Текущий список задач проекта Daggy:

  1. Добавить msi установщик. Уровень сложности: легкий. Нужно расширить cpack макросы до создания msi пакета. Msi пакет должен быть подписан. 

  2. Автотесты для ssh/ssh2 провайдеров. Уровень сложности: средний. Чтобы тестировать ssh/ssh2 провайдеры, нужно специально настраивать окружение для них, поэтому пока есть только ручные тесты.

  3. Добиться мержа conan библиотеки. Уровень сложности: легкий. Если на момент чтения этой статьи библиотека все еще не вмержена, то нужно разобраться в причинах и как это исправить.

  4. Разобраться с поддержкой последней версии Qt в conan-center-index. Уровень сложности: средний. Нужно понять, почему обновление Qt в conan-center-index встало полгода назад. Скорее всего нужно будет связаться с мейнтенерами conan-center-index или стать одним из мейнтейнеров для решения проблемы поддержки Qt. По итогу нужно добиться стабильного попадания последней версии Qt в conan-center-index.

  5. Деплой daggy библиотеки в python пакет. Уровень сложности: средний. Необходимо разобраться, как можно портировать Qt библиотеку как python пакет. Основная сложность как добиться линковки с Qt framework, что поставляется в python как wheels. Кроме того, нужно решить каким образом линковаться с libssh2 и libyaml-cpp в составе python модуля.

  6. Добавить telnet провайдер. Уровень сложности: средний. Telnet - распространенный протокол управления в embedded и малоресурсных устройствах. Нужно выбрать библиотеку для реализации telnet, встроить ее в систему сборки и реализовать интерфейс daggy::providers::IProvider.

  7. Добавить websocket провайдер. Уровень сложности: средний. В Qt уже есть QWebSocket, поэтому скорее всего не понадобится интеграция новой зависимости в систему сборки. Нужно реализовать интерфейс daggy::providers::IProvider и сделать websoket провайдер одним из провайдеров по умолчанию.

  8. Добавить video провайдер. Уровень сложности: средний. QMediaPlayer умеет работать с видео потоками данных, но надо понять, можно ли из него извлекать фреймы видеопотока. Нужно реализовать интерфейс daggy::providers::IProvider и сделать video провайдер одним из провайдеров по умолчанию.

  9. Proof-of-concept GUI Stream Viewer. Уровень сложности: высокий. Разработать макет gui приложения, которая умеет считывать data source файлы и строить отображение потоков. Нужно рассмотреть поддержку отображения следующий типов потоков: видео (mp4, mpeg…), текстовые (log, txt,...), сетевые (pcap, ngpcap, …).

Если вы после прочтения этой статьи проявили интерес к проекту и хотите участвовать в его разработке, то я готов героически сопротивляться атаке ИИ на С++ разработчиков и поделиться задачей с вами!

Комментарии (8)


  1. IIopy4uk
    19.05.2025 06:13

    По поводу распространения: что если использовать CQtDeployer?


    1. synacker Автор
      19.05.2025 06:13

      Это просто скрипт, который позволяет собрать зависимости Qt. Кроме них в проекте ещё есть зависимости и он не решает проблемы доставки до пользователей приложения и библиотеки


  1. kozlyuk
    19.05.2025 06:13

    Много раз я думал переписать уже все на go или rust и забыть про проблемы распространения C++ приложений как кошмарный сон. Но каждый раз я понимаю, что любой другой язык не даст мне такой свободы в реализации своих идей относительно проекта.

    Какой именно свободы не дают другие языки? Для задач Daggy (IO-bound, асинхронность) и условий применения (простое распространение на разные ОС) использовать Go сам бог велел.


    1. synacker Автор
      19.05.2025 06:13

      Gui к go не прикрутить потом. Кроме того как спроектировать расширяемый асинхронный интерфейс не очень понятно, чтобы можно было его в плагины вынести например


  1. Playa
    19.05.2025 06:13

    Читаю-читаю про употребление колючек conan'а, думаю ну вот сейчас автор наконец напишет "а давайте теперь попробуем vcpkg" но вот что-то не написал и продолжил употреблять колючки и делать какие-то выводы :(


    1. synacker Автор
      19.05.2025 06:13

      Когда проект начал, о нем было известно ещё меньше, чем о conan. Надо будет как нибудь попробовать


  1. nv13
    19.05.2025 06:13

    Я понимаю, что Вам хочется, чтобы проект был более удобным для вхождения потенциального пользователя, но уж если он его заинтересовал, то уверен, что тот приложит некоторые усилия чтобы его попробовать даже просто в исходниках. Ну, или я сильно отстал от тенденций и переоцениваю потенциальных пользователей и контрибьютеров)


    1. synacker Автор
      19.05.2025 06:13

      В этом и суть проблемы. Чтобы заинтересовал, нужно чтобы попробовал. По крайней мере мне только так удавалось его продвигать.