Создание и поддержка своего собственного APT репозитория

С появлением нового проекта - sdr-server, у меня стало слишком много приложений, которые нужно как-то устанавливать. И всё бы ничего, но каждое приложение в свою очередь требует разных системных библиотек. А эти системные библиотеки не всегда нужных версий. А если сюда добавить разные версии дистрибутивов и разные архитектуры процессоров, то начинается комбинаторный взрыв, и всё становится очень запутанным.

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

Всё это привело меня к единственно правильному решению - созданию собственного APT репозитория.

Требования

Изначально я создал APT репозиторий в S3 и закидывал туда пакеты без какой-либо структуры. Этот подход хорошо работает, если разрабатываешь платформенно-независимые приложения. r2cloud и r2cloud-ui - как раз такие. Первому нужна только Java, а второй написан на javascript и компилируется в набор статичных .js файлов. Когда же понадобилось нечто большее, я решил подойти к решению проблемы системно и начал с описания требований.

  • поддержка разных дистрибутивов. Мне нужно поддерживать Debian stretch, Debian buster, Ubuntu bionic и Ubuntu focal. Это две последние стабильные версии двух самых популярных дистрибутивов. Debian прежде всего нужен для RaspberryPI. Все RaspberryPi OS основаны на debian. А Ubuntu - это самый популярный дистрибутив как на сервере, так и среди Linux десктопов.
  • поддержка двух архитектур: armhf и amd64. В будущем планируется добавить arm64.
  • репозиторий должен быть самодостаточный. Это значит, что все зависимости должны устанавливаться либо из центрального репозитория, либо находится в r2cloud репозитории.

Поддержка разных дистрибутивов

Самый, наверное, главный вопрос: “а зачем вообще явно делать поддержку разных дистрибутивов”? Ubuntu сделан на основе Debian, поэтому достаточно было бы поддерживать Debian разных версий.

Но чем больше я пытался ответить на этот вопрос, тем больше убеждался, что явно разделять дистрибутивы и их версии - это правильное решение.

Зависимости

Самое очевидное преимущество разных версий дистрибутивов - возможность гибко управлять зависимостями. Например, есть такая библиотека check. Эта библиотека для создания юнит тестов на Си. Если ставить её через brew, то поставится версия 0.15.2:

И в версии 0.15.2 есть метод ck_assert_ptr_nonnull. Весьма удобный метод для того, чтобы проверять выделена ли память. Однако, при попытке собрать приложение в Ubuntu, будет возникать ошибка: метод ck_assert_ptr_nonnull не найден. А всё из-за того, что в Ubuntu bionic версия библиотеки 0.10.0. И там этого метода ещё нет.

Эту проблему можно решить несколькими способами:

  1. Не использовать метод ck_assert_ptr_nonnull и поддерживать минимальную версию check
  2. Создать отдельный бранч для конкретного дистрибутива, в котором использовать новый метод
  3. Использовать #if CHECK_VERSION >= 0.11 в исходном коде и превратить его в наслоение разных препроцессорных инструкций
  4. Запаковать check нужной версии в собственный репозиторий и поставлять его вместе с приложением

Понятное дело, для юнит-тестов 2, 3 и 4 - это перебор. И в моём случае я, скрепя сердцем, переписал юнит-тесты без использования ck_assert_ptr_nonnull. Но для других более важных библиотек, такой способ может и не подойти.

Например, библиотеку volk мне пришлось компилировать и загружать в свой репозиторий.

Компиляция

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

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

Дизайн APT репозитория

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

Для r2cloud APT репозитория я выбрал следующую схему:

  • хостинг - S3
  • каждый дистрибутив имеет кодовое имя, которое делается подпапкой
  • каждая поддерживаемая архитектура - это подпапка в дистрибутиве
  • компонент всегда один - “main”

Получилось нечто такое:

Тут важно заметить, что непосредственно бинарники могут быть переиспользованы между разными дистрибутивами. Они находятся в отдельной директории pool и их можно идентифицировать по имени, версии, компоненту и архитектуре. Вообще, это сделано специально, чтобы уменьшить размер директории pool. Ведь для того, чтобы перенести бинарник в более новые дистрибутивы, достаточно добавить его имя в файл Packages. Однако, это может быть проблемой, если нужно выпускать бинарник не совместимый с предыдущей версией ОС.

Чтобы решить эту проблему, Debian-сообщество рекомендует добавлять название дистрибутива в версию:

0.6.5-1~stretch

Но это может сработать не для всех пакетов, а для тех, которые собираются с source format=quilt. Например, rtl-sdr содержит source format=native, так что мне пришлось делать собственный форк.

Сборка пакетов

Из-за того, что появилось множество различных версий дистрибутивов, сборка сильно усложнилась. Единственным правильным способом собрать пакет под определённую версию ОС будет сборка именно в этой версии ОС.

Чтобы хоть как-то облегчить процесс, я накупил несколько флеш карт и установил на каждую из них свою версию дистрибутива:

Как только мне нужно собрать пакет под конкретную версию ОС, я вставляю нужную флеш карту в RaspberryPI и собираю пакет.

Для сборки под Ubuntu я решил пойти немного другим путём - использовать виртуалки. Они достаточно дешёвые и не нужно держать дома ещё одну железку. Достаточно создать виртуалку с поминутной оплатой и запустить там билд.

armhf vs arm64

Больше всего проблем я получил, собирая volk. Эта библиотека использует ассемблерный код и интристики для того, чтобы ускорять выполнение разных DSP преобразований. Я уже писал недавно о том, как мне удалось её ускорить. Так вот, сборка под RaspberryPI подарила мне много новых ощущений.

Во-первых, в Debian есть две разные архитектуры для 32битного ARM и 64битного ARM. Они называются соответственно armhf и arm64. Собранные пакеты для armhf находятся в разделе binary-armhf, для arm64 - в binary-arm64. Пока всё просто.

Во-вторых, в ARM есть такое расширение - NEON. Это расширение добавляет SIMD операции, с помощью которых можно в несколько раз ускорить приложение. И volk очень активно их использует. Но они не включены по умолчанию в RaspberryPI OS! Поэтому компиляция volk просто игнорирует NEON и производит не самый оптимальный код. Почему разработчики RaspberryPI по-умолчанию не включают NEON для меня загадка. Ведь они точно знают какой процессор стоит у них на плате. И все процессоры в RaspberryPI имеют это расширение.

В-третьих, RaspberryPI OS существует только для armhf. Именно поэтому, запуская компиляцию на RaspberryPI 4, где стоит процессор с arm64, я получаю пакет для armhf. Однако, volk, при компиляции игнорирует dpkg --print-architecture и определяет процессор как arm64! Он производит код для arm64, тот помещается в пакет для armhf, и RaspberryPI 3 не может его запустить. Бардак.

На поиск и решение всех этих особенностей у меня ушло несколько недель.

Подключение APT репозитория

Но зато подключение репозитория происходит в три строчки:

sudo apt-get install dirmngr lsb-release
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A5A70917
sudo bash -c "echo \"deb http://s3.amazonaws.com/r2cloud $(lsb_release --codename --short) main\" > /etc/apt/sources.list.d/r2cloud.list"

Самая важная часть - это $(lsb_release --codename --short). Я просто беру кодовое имя текущей операционной системы и подключаю соответствующую систему из r2cloud репозитория.

apt-html

Количество поддерживаемых дистрибутивов и пакетов стало таким большим, что мне пришлось написать специальную программу - apt-html. Это простая консольная утилита, которая генерирует красивую html страницу со списком пакетов в APT репозитории.

Запускается достаточно просто:

java -jar ./target/apt-html.jar --url http://s3.amazonaws.com/r2cloud --include-arch armhf,amd64 --include-component main --include-codename stretch,bionic,buster,focal --include-package sdr-server,libcpu-features-dev,libvolk2-bin,libvolk2.4-dbgsym,libvolk2.4,librtlsdr0,librtlsdr-dev,librtlsdr0-dbgsym,libiio,plutosdr,r2cloud-jdk,r2cloud-ui,rtl-sdr,wxtoimg,r2cloud --output-dir src/main/resources/

На вход подаётся URL APT репозитория, список архитектур для включения в отчёт, список пакетов, список дистрибутивов, а на выходе получается вот такая симпатичная страничка:

Выводы

Всё это упражнение в создании APT репозитория заняло у меня несколько недель. И это при том, что последние несколько лет я активно пишу всевозможные инструменты для управления apt репозиториями. Я не думаю, что разработчики Debian сознательно сделали систему такой сложной и запутанной. Скорее всего, со временем она стала обрастать множеством фич и опций, что превратило её в такую, какая она сейчас есть. Несмотря на это, я планирую и дальше развивать r2cloud APT репозиторий и добавлять туда разные полезные пакеты. Ведь это единственный простой способ для конечного пользователя взять и установить программу.

Пара идей на будущее:

  • добавить поддержку arm64
  • создать баг или pull request в RaspberryPI OS, чтобы включить опцию GCC “-mfpu=neon” по-умолчанию
  • возможно, создать rack из нескольких RaspberryPI и похожих плат на Intel, чтобы сделать небольшую билд ферму