Одной из самых важнейших проблем для разработчика в мире Linux является создание правильных пакетов. Это обычно вызывает множество вопросов, поэтому в данной статье мы решили подробно их разобрать.
Введение
Большинство разработчиков считают, что правильным является создание одного RPM и одного DEB пакета, однако это в корне неверно. Как RPM, так и DEB являются лишь контейнерами с метаданными для того, что находится у них внутри.
В каждом дистрибутиве RPM/DEB пакеты имеют свои, зачастую ни с кем не совместимые, особенности. Также каждый дистрибутив имеет совершенно разные наборы и версии библиотек.
Например если мы соберём пакет в Fedora 29 и динамически привяжемся к Qt 5.11 или ffmpeg 4.0, то он в большинстве случаев не будет работать в более старой версии Fedora 26 с Qt 5.6 и ffmpeg 3.1. Это же касается и Debian/Ubuntu. Обратная совместимость API/ABI внутри библиотек работает обычно лишь в более новых версиях относительно старых, однако некоторые на неё вообще забивают.
Также RPM пакет, собранный для Fedora, скорее всего не будет работать в openSUSE или ALT Linux, поэтому если и собирать пакеты, то это нужно делать для каждого дистрибутива в отдельности и в пределах как минимум двух поддерживаемых версий.
Способы линковки
Существует два способа компоновки приложения с необходимыми ему библиотеками:
- динамическая — компоновщик линкует бинарник с уже существующими в системе динамическими библиотеками. При запуске они подгружаются в адресное пространство;
- статическая — все необходимые зависимости вшиваются внутрь бинарника статически.
Каждый способ имеет как достоинства, так и недостатки. Рассмотрим их более подробно.
Динамическая линковка
Преимущества:
- позволяет использовать разделяемую память библиотек, т.е. если какое-то запущенное приложение уже использует её, она не дублируется в памяти, что ускоряет запуск и снижает общее потребление памяти;
- позволяет оперативно исправлять уязвимости в сторонних библиотеках. Например если в libfoo обнаружена уязвимость, её мейнтейнеры в дистрибутиве выпустят обновление и после его установки будет достаточно лишь перезапустить наше приложение, чтобы оно подействовало.
Недостатки:
- мы привязываем свой бинарник к конкретной версии библиотеки, которая затем должна быть установлена в системе и иметь ту же, либо более новую версию (если имеется обратная совместимость по API/ABI);
- сложность распространения проприетарного ПО. В данном случае потребуется каким-то способом включать все используемые динамические библиотеки в дистрибутив (об этом далее).
Статическая линковка
Преимущества:
- всё, от чего зависит бинарник, вшивается непосредственно в него, поэтому он может быть запущен где угодно, на любой системе;
- нет проблем с зависимостями.
Недостатки:
- если в используемой библиотеке libfoo будет найдена уязвимость, потребуется полная пересборка и перелинковка всего проекта, а затем распространение обновлённой версии. Часто такое ПО остаётся уязвимым навсегда;
- нет возможности использования разделяемой памяти. Все библиотеки загружаются в адресное пространство процесса, даже если их копии уже присутствуют в памяти;
- огромный размер бинарника;
- проблемы с лицензированием для проприетарного ПО. Ряд лицензий, например GPL, запрещают статическую линковку с собственническим ПО. Лицензия LGPL (под ней распространяется например Qt) статическую линковку допускает, но при этом требует предоставления хотя бы объектных файлов всем покупателям для возможности перелинковки.
Альтернативы RPM/DEB пакетам
Чтобы не собирать пакеты под каждый из дистрибутивов по отдельности, были придуманы так называемые самодостаточные пакеты: Flatpak, AppImage, Snap.
Flatpak
Это наверное самый лучший из самодостаточных пакетов. Поддерживает динамическую линковку с большим количеством библиотек из рантаймов, что решает проблемы с лицензированием, их поддержкой в актуальном состоянии и исправлением в них ошибок, а также уязвимостей. Рантаймы оперативно обновляются мейнтейнерами в пределах версии. Например мы можем слинковаться с Qt версии 5.11 и openssl 1.1.0 и указать их в манифесте, тогда они будут автоматически загружены и установлены у пользователей. Ошибки и уязвимости в них будут исправляться автоматически.
Библиотеки, для которых нет рантаймов, могут быть упакованы внутрь флатпака и подгружаться по мере необходимости.
Flatpak позволяет установить разные версии приложений и библиотек одновременно. К тому же для установки не требуются права администратора.
Также Flatpak поддерживает изоляцию приложения внутри контейнера. В манифесте может быть указано к каким каталогам и ресурсам будет иметь доступ приложение. Всё остальное для него будет запрещено. Права очень гибко настраиваются.
Созданные флатпаки могут загружаться и использоваться на любом дистрибутиве GNU/Linux, поддерживающем их использование.
Репозиторий Flatpak пакетов создаётся достаточно легко и может хоститься на любом ресурсе, позволяющем размещение статических файлов по прямым ссылкам (даже на GitHub pages).
Документацию по Flatpak можно найти на официальном сайте вместе с готовыми примерами.
Нужно ли вообще собирать RPM/DEB пакеты?
Для разработчиков свободного ПО однозначно нет. Когда проект наберёт популярность, мейнтейнеры дистрибутивов опакетят его согласно их официальных гайдлайнов и добавят в основные репозитории. От разработчика здесь требуется лишь не чинить препятствий этому и внедрять исправления, которые могут предложить мейнтейнеры.
Для разработчиков собственнического ПО есть несколько вариантов:
- использовать динамическую линковку и собирать пакеты для каждого популярного дистрибутива в пределах поддерживаемых версий;
- использовать статическую линковку (если нет проблем с лицензиями) и создавать статические бинарники внутри generic RPM/DEB пакетов;
- использовать динамическую линковку и использовать RPATH при сборке бинарника (либо вместо RPATH использовать механизм LD_PRELOAD), а затем упаковывать все необходимые библиотеки внутрь generic RPM/DEB пакета вне стандартной library path, чтобы не вызывать проблем с зависимостями у общесистемных пакетов;
- использовать самодостаточные пакеты, например вышеупомянутый Flatpak.
Для всех разработчиков проприетарного ПО мы настоятельно рекомендуем внедрять Flatpak и по возможности максимально использовать его рантаймы.
Что насчет остальных самодостаточных пакетов (AppImage/Snap)? Стоит их рассматривать вообще?
AppImage — это особый контейнер, в котором размещён бинарник приложения, а также все его зависимости в виде shared библиотек. При запуске контейнер монтируется посредством FUSE и запускается содержащийся бинарник.
Snap — это контейнер, в котором размещён бинарник, слинкованный статически со всеми его зависимостями.
В обоих случаях если в одной из зависимых библиотек будет найдена уязвимость, потребуется полная пересборка пакета и доставка результата клиентам. К тому же абсолютно все зависимости дублируются как внутри пакетов, так и в памяти при работе.