Как работают snap, flatpak, appimage

Примерно вот так я впервые познакомился с snap Примерно вот так я впервые познакомился с snap

Распространение приложений в линуксе — это боль. Причем в наше время цикл обновлений приложений все уменьшается и эта боль чувствуется все сильнее. В связи с этим появляются технологии вроде snap, flatpak, которые декларируют решение этих проблем. Некоторые дистрибутивы (я смотрю на тебя, Ubuntu) даже начинают довольно агрессивную политику по их внедрению. Однако, несмотря на то, что про сами эти технологии много говорят (и ещё больше жалуются), про то, как они работают написано довольно мало. Попробуем исправить это.

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

Далее я буду использовать слово «приложение» как конечный продукт, который используют пользователи (например firefox), а слово «зависимость» как «динамическая библиотека», .so файл, который нужен приложению (например libsqlite.so)

В чем, собственно, проблема?

А проблема в том, как приложение получает свои зависимости.

В мире windows победила идеология, где каждое приложение тащит свои зависимости с собой (за исключением совсем системных). Нередко можно видеть, что приложение кладет в свою папку какой-нибудь gl.dll, sqlite.dll и подобное.

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

Такой подход имеет свои плюсы и минусы. Из плюсов:

  • если какой-нибудь QT используется сотней приложений, то он будет установлен не 100 раз, а только 1.

  • Если в каком-то пакете, например openssl, обнаружатся проблемы, то вы можете один раз обновить один пакет и вся система сразу будет пользоваться пропатченой версией, разработчикам конечных приложений не нужно делать вообще ничего.

  • Это хорошо сочетается с идеологией линукса «программа должна делать одну вещь и делать её хорошо».

Из минусов:

  • Многообразие дистрибутивов и следовательно форматов пакетов. Для поддержки всех версий нужно прилагать огромное количество усилий. Посмотрите только на такие плашки, сам факт того, что они существуют, несколько пугает.

  • А что делать, если двум приложениям требуется разные, несовместимые версии пакетов?

  • А что делать если приложению нужен пакет, который не предоставляется дистрибутивом?

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

  • Если дистрибутив обновляется (а вместе с ним все пакеты), нужно проверять работоспособность приложения. При этом хочется верить, что ментейнеры у проекта ещё есть. И да, вам будут присылать баг репорты на старые версии приложения в старых дистрибутивах, то есть нужно ещё поддерживать старые версии продукта, нельзя просто сказать «это починено в новой версии, обновитесь»

  • Возможность поправить баг и обновить пакет для всех — обоюдоострый меч. По причине того, что это иногда банально сложно — исправить что-то и сохранить все контракты (явные и неявные) вашей библиотеки. Вполне возможно своим «фиксом» автор пакета неявно создаст баг всем другим, кто от него зависит.

А ещё есть очень специфический тип приложений — игры. Их особенность в том, что они практически всегда closed source, после определенного времени вообще не обновляются, но при этом они активно используют железо, то есть им нужна серьезная поддержка со стороны ОС и драйверов. И как, спрашивается, нам можно запустить игру 10 летней давности на современном дистрибутиве, где все библиотеки уже уехали очень далеко?

В общем с распространением приложений в линуксе очень много проблем. Технологии приведенные в названии поста пытаются решить эти проблемы. Давайте посмотрим как они пытаются это делать.

6a9a9cdbfaf847036127f47ea7b5a857.webp

Appimage

Appimage в целом похожа на portable exe в винде. Основная идея довольно проста: мы берем все зависимости приложения (кроме совсем системных), упаковываем их в файл в формате squashfs и добавляем специальный хедер, чтобы этот файл стал ELF — исполняемым файлом. Внутри этого хедера находится программа, которая при запуске файла монтирует этот squashfs во временную папку и оттуда вызывает бинарник нашего приложения.

AppImage file structureAppImage file structure

Для сборки этого файла юзеру нужно создать специальную папку (AppDir) и положить в неё все, что нужно приложению для запуска: бинарник, .so файлы, иконки и т.п. AppDir должен содержать в корне запускаемый файл AppRun, который будет запускать непосредственно ваше приложение. В нем можно выставлять переменные окружения (в том числе для линкера, чтобы он смотрел не в корневые папки, а в наши) и входные параметры приложения. Для тривиальных вариантов AppRun может быть просто симлинком на сам бинарник. Для запаковки этой папки в AppImage файл есть официальный тул AppImageTool.

Вообще желательно, чтобы AppDir содержал ещё некоторые служебные файлы (.desktop, иконки) для интеграции с хостовой системой, подробнее можно прочитать тут

Если вы хотите посмотреть внутренности AppDir конкретного AppImage, можно запустить его с ключиком --appimage-mount. Он примонтирует squashfs и выплюнет в stdout путь к примонтированной папке, в которую можно залесть и посмотреть что там делается

❯ ./appimagetool-x86_64.AppImage --appimage-mount
/tmp/.mount_appimaGnR7L6

В целом все довольно просто. Ну почти. Автору пакета нужно следить, чтобы приложение было «relocatable», то есть чтобы оно могло запускаться из любого места и в нем не было захардкоженных абсолютных путей к зависимостям. Это касается как динамических библиотек, так и ресурсов типа картинок. Вот с этим может быть проблема, так как поколения разработчиков на линукс опирались на конкретные захардкоженные пути и вычистить это все может быть не так просто.

Ещё один момент — приложение монтируется в read only файловую систему. Это значит, что приложение не может обновлять само себя, нужные внешние утилиты. Интеграция в desktop environment возможна при установке внешнего сервиса

Flatpak

Flatpak как технология ставит перед собой более амбициозные задачи. Если AppImage — это просто средство упаковки и запуска приложения, то flatpak реализует также sandboxing и распространение.

При разговоре о flatpak нужно помнить, что он задумывался как контейнер именно для GUI приложений. Это повлияло на некоторые решения в дизайне.

В Flatpak вы точно также собираете свои зависимости с собой (кроме тех, что есть в базовом рантайме, о нем дальше), но запускается это все в изолированном контейнере (да, это больше похоже на то, что делает Docker).

Основные технологии, на которой основана flatpak

  • bubblewrap — rootless контейнеры

  • OSTree — система дистрибуции файлов. Идеологически похожа на гит: есть репозиторий, ветки между которыми можно переключаться. Но оптимизированно все для работы с бинарными файлами (изначально это вообще делалось для разворачивания рабочих операционных систем). При скачивании приложения вы по-сути вычекиваете ветку из репозитория. Бонусом идет то, что если разные приложения (ветки) используют один и тот же файл, он не копируется только 1 раз.

Сборка пакета осуществляется тулами flatpak. Для этого нужно описать сборку в yml файле — пример такого файла для telegram-desktop Посмотрим на верхушку этого файла

id: org.telegram.desktop
runtime: org.freedesktop.Platform
runtime-version: '21.08'
sdk: org.freedesktop.Sdk
command: telegram-desktop
rename-icon: telegram
finish-args:
  - --share=ipc
  - --share=network
  - --socket=x11
  - --socket=wayland
  - --socket=pulseaudio
  - --device=all
  - --own-name=org.mpris.MediaPlayer2.tdesktop
  - --talk-name=org.freedesktop.Notifications
  - --talk-name=org.freedesktop.ScreenSaver
  - --talk-name=org.gnome.Mutter.IdleMonitor
  - --talk-name=org.kde.StatusNotifierWatcher
  - --talk-name=com.canonical.AppMenu.Registrar
  - --talk-name=com.canonical.Unity
  - --talk-name=com.canonical.indicator.application
  - --talk-name=org.ayatana.indicator.application
  - --filesystem=xdg-download
  - --filesystem=xdg-run/pipewire-0
  - --filesystem=host:ro
  - --nofilesystem=~/.TelegramDesktop
  - --env=QT_PLUGIN_PATH=/app/lib64/plugins:/app/lib/plugins
  - --env=PATH=/app/lib/webview/bin:/app/bin:/usr/bin

Из него видно, что помимо всяких id продукта, мы выбираем «runtime» и что мы хотим использовать из хостовой системы.

Runtime предоставляет базовые системные зависимости, там есть D-Bus, GLib, Gtk3, PulseAudio, X11, Wayland и прочее. Есть 3 стандартных рантайма на flathub: Freedesktop, GNOME и KDE. Также есть разные версии рантайма, для вашего контейнера подключится именно та, которую указали. Также это значит то, что скорее всего у вас на диске будет лежать несколько версий (а они не маленькие).

Далее указываются дополнительные привилегии: на ipc, сеть, файловую систему и т.п. По-умолчанию контейнер создается с минимальным количество прав. Он не может писать в файловую систему (кроме своей папки внутри $XDG_RUNTIME_DIR/app/$FLATPAK_ID), не может общаться с сетью, получать доступ к устройствам или другим процессам. Также у приложения усеченный список syscalls.

Для доступа к хосту можно или запросить привилегии, или использовать xdg-portals — специальное api, которое может запрашивать что-то в хостовом desktop environment. Например можно вызвать окошко выбора файла, код которого будет работать со стороны хоста. Также через порталы можно открывать ссылки, показывать нотификации, делать скриншоты и т.п. Поддержка порталов встроена в qt и gtk.

Далее в файле указываются шаги для сборки приложения и всех его зависимостей. Приложение собирается не на хосте, а внутри SDK (это то же самое что и runtime, но с дополнительными сборочными утилитами внутри). Также помимо runtime существуют «BaseApp», базовые приложение которые не нужно собирать и можно просто утянуть себе в контейнер. Обычно это большие фреймворки которые не хочется настраивать самому, наподобие Electron. Но это используется редко, сами авторы просят не создавать их без реальной необходимости.

Кстати говоря, при желании можно войти в контейнер и что-нибудь там поделать. Делается это командой

flatpak run --command=sh --devel 

(--devel позволяет запустить контейнер с расшириным рантаймом, в котором есть дебажные тулы типа gdb, strace и т.п.)

Подробнее про все это можно посмотреть в этом выступлении автора flatpak.

Snap

Snaps поставляются в виде .snap файлов (лежат в /var/lib/snapd/snaps), содержащие squashfs приложения. Чем-то похоже на appImage, но в отличии от него, все snap постоянно смонтированны в папку /snap (это на убунте, в других дистрибутивах папка может отличаться).

❯ mount | grep snap
/var/lib/snapd/snaps/bare_5.snap on /snap/bare/5 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/core18_2344.snap on /snap/core18/2344 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/gnome-3-34-1804_72.snap on /snap/gnome-3-34-1804/72 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/gnome-3-34-1804_77.snap on /snap/gnome-3-34-1804/77 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/gnome-3-38-2004_99.snap on /snap/gnome-3-38-2004/99 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/snap-store_547.snap on /snap/snap-store/547 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/snap-store_558.snap on /snap/snap-store/558 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/gtk-common-themes_1519.snap on /snap/gtk-common-themes/1519 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/snapd_15534.snap on /snap/snapd/15534 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/core20_1434.snap on /snap/core20/1434 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/snapcraft_7201.snap on /snap/snapcraft/7201 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/core18_2409.snap on /snap/core18/2409 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/gtk-common-themes_1534.snap on /snap/gtk-common-themes/1534 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/gopls_885.snap on /snap/gopls/885 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/snapd_15904.snap on /snap/snapd/15904 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/core20_1494.snap on /snap/core20/1494 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/core_13250.snap on /snap/core/13250 type squashfs (ro,nodev,relatime,x-gdu.hide)
/var/lib/snapd/snaps/hello-world_29.snap on /snap/hello-world/29 type squashfs (ro,nodev,relatime,x-gdu.hide)
tmpfs on /run/snapd/ns type tmpfs (rw,nosuid,nodev,noexec,relatime,size=802216k,mode=755,inode64)
nsfs on /run/snapd/ns/hello-world.mnt type nsfs (rw)
/var/lib/snapd/snaps/gnome-3-38-2004_106.snap on /snap/gnome-3-38-2004/106 type squashfs (ro,nodev,relatime,x-gdu.hide)

Ещё одно отличие от appImage — squashfs смонтированны через loop device, а не через fuse (поэтому lsblk выдается столько «мусора»).

Давайте посмотрим в такую папку. В данном случае у меня установлен «hello-world» пакет версии 29. При наличии несколько версий, будет несколько копий дерева файлов. На «текущую» версию указывает ссылка current.

❯ tree /snap/hello-world
/snap/hello-world
├── 29
│   ├── bin
│   │   ├── echo
│   │   ├── env
│   │   ├── evil
│   │   └── sh
│   └── meta
│       ├── gui
│       │   └── icon.png
│       └── snap.yaml
└── current -> 29

Но предполагается, что пользователь в эти места не лезет, а запускает приложения из другого места — /snap/bin/, который обычно добавлен в $PATH. При этом видно, что бинарники в этой папке указывают просто на /usr/bin/snap.

~
❯ hello-world
Hello World!

~
❯ where hello-world
/snap/bin/hello-world

~
❯ ls -l /snap/bin/hello-world
lrwxrwxrwx 1 root root 13 мая 29 09:53 /snap/bin/hello-world -> /usr/bin/snap

Чтобы понять разницу, запустим hello-world.evil двумя способами

/snap/hello-world
❯ hello-world.evil
Hello Evil World!
This example demonstrates the app confinement
You should see a permission denied error next
/snap/hello-world/29/bin/evil: 9: /snap/hello-world/29/bin/evil: cannot create /var/tmp/myevil.txt: Permission denied

/snap/hello-world
❯ ./current/bin/evil
Hello Evil World!
This example demonstrates the app confinement
You should see a permission denied error next
If you see this line the confinement is not working correctly, please file a bug

/snap/hello-world
❯ cat /var/tmp/myevil.txt
Haha

И глянем в вывод dmesk

❯ dmesg
...

[348350.198906] audit: type=1400 audit(1653807204.078:105): apparmor="DENIED" operation="capable" profile="/snap/snapd/15904/usr/lib/snapd/snap-confine" pid=627336 comm="snap-confine" capability=4  capname="fsetid"
[348354.281614] audit: type=1400 audit(1653807208.162:106): apparmor="DENIED" operation="mknod" profile="snap.hello-world.evil" name="/var/tmp/myevil.txt" pid=627395 comm="evil" requested_mask="c" denied_mask="c" fsuid=1000 ouid=1000

Это говорит нам о том, что при запуске snap понимает какое приложение хотели
запустить через него, и запускает его, но только через профайл apparmor (его можно посмотреть в /var/lib/snapd/apparmor/profiles/snap.hello-world.evil). То есть если в flatpak sandboxing нашего приложение делалось через namespaces (контейнеризацию), то тут оно реализовано через правила apparmor. Посмотреть какие права есть у приложение можно через его манифест. В hello-world никаких дополнительных прав нет

❯ cat /snap/hello-world/current/meta/snap.yaml
name: hello-world
version: 6.4
architectures: [ all ]
summary: The 'hello-world' of snaps
description: |
    This is a simple snap example that includes a few interesting binaries
    to demonstrate snaps and their confinement.
    * hello-world.env  - dump the env of commands run inside app sandbox
    * hello-world.evil - show how snappy sandboxes binaries
    * hello-world.sh   - enter interactive shell that runs in app sandbox
    * hello-world      - simply output text
apps:
 env:
   command: bin/env
 evil:
   command: bin/evil
 sh:
   command: bin/sh
 hello-world:
   command: bin/echo

Для сравнения можно посмотреть snap.yaml от firefox. В терминологии snap приложение, которому нужны дополнительные возможности, должны поставить плагины (plugs)

/snap/firefox/current/meta/snap.yaml

❯ cat /snap/firefox/current/meta/snap.yaml
name: firefox
version: 101.0.1-1
summary: Mozilla Firefox web browser
description: Firefox is a powerful, extensible web browser with support for modern
  web application technologies.
apps:
  firefox:
    command: firefox.launcher
    environment:
      GTK_USE_PORTAL: 1
      HOME: $SNAP_USER_COMMON
      PIPEWIRE_CONFIG_NAME: $SNAP/usr/share/pipewire/pipewire.conf
      PIPEWIRE_MODULE_DIR: $SNAP/usr/lib/x86_64-linux-gnu/pipewire-0.3
      SPA_PLUGIN_DIR: $SNAP/usr/lib/x86_64-linux-gnu/spa-0.2
    slots:
    - dbus-daemon
    - mpris
    plugs:
    - desktop
    - desktop-legacy
    - gsettings
    - opengl
    - wayland
    - x11
    - audio-playback
    - audio-record
    - avahi-observe
    - browser-sandbox
    - camera
    - cups-control
    - hardware-observe
    - home
    - joystick
    - network
    - network-observe
    - removable-media
    - screen-inhibit-control
    - system-packages-doc
    - u2f-devices
    - unity7
    - upower-observe
    command-chain:
    - snap/command-chain/snapcraft-runner
    - snap/command-chain/desktop-launch
  geckodriver:
    command: usr/lib/firefox/geckodriver
    plugs:
    - desktop
    - desktop-legacy
    - gsettings
    - opengl
    - wayland
    - x11
    - network-bind
    - audio-playback
    - audio-record
    - avahi-observe
    - browser-sandbox
    - camera
    - cups-control
    - hardware-observe
    - home
    - joystick
    - network
    - network-observe
    - removable-media
    - screen-inhibit-control
    - system-packages-doc
    - u2f-devices
    - unity7
    - upower-observe
    slots:
    - dbus-daemon
    - mpris
    command-chain:
    - snap/command-chain/snapcraft-runner
    - snap/command-chain/desktop-launch
architectures:
- amd64
assumes:
- command-chain
- snapd2.43
base: core20
confinement: strict
environment:
  SNAP_DESKTOP_RUNTIME: $SNAP/gnome-platform
  GTK_USE_PORTAL: '1'
grade: stable
hooks:
  configure:
    command-chain:
    - snap/command-chain/hooks-configure-desktop
    plugs:
    - desktop
layout:
  /usr/share/libdrm:
    bind: $SNAP/gnome-platform/usr/share/libdrm
  /usr/lib/x86_64-linux-gnu/webkit2gtk-4.0:
    bind: $SNAP/gnome-platform/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0
  /usr/share/xml/iso-codes:
    bind: $SNAP/gnome-platform/usr/share/xml/iso-codes
plugs:
  browser-sandbox:
    interface: browser-support
    allow-sandbox: true
  desktop:
    mount-host-font-cache: false
  dot-mozilla-firefox:
    interface: personal-files
    read:
    - $HOME/.mozilla/firefox
  etc-firefox-policies:
    interface: system-files
    read:
    - /etc/firefox/policies
  gnome-3-38-2004:
    interface: content
    target: $SNAP/gnome-platform
    default-provider: gnome-3-38-2004
  gtk-3-themes:
    interface: content
    target: $SNAP/data-dir/themes
    default-provider: gtk-common-themes
  icon-themes:
    interface: content
    target: $SNAP/data-dir/icons
    default-provider: gtk-common-themes
  sound-themes:
    interface: content
    target: $SNAP/data-dir/sounds
    default-provider: gtk-common-themes
slots:
  dbus-daemon:
    interface: dbus
    bus: session
    name: org.mozilla.firefox

Для некоторых приложений (например для VS Code) используется --classic ограничения (вместо дефолтного --strict), тогда он будет запущен без sandboxing’а и ему будет доступны все функции хоста.

Layouts

Хоть snap не использует полноценные контейнеры, он использует mount namespace для решения проблемы захардкоженных путей типа /etc/, /usr/share/ и т.п. Я упоминал об этой проблеме в разделе про AppImage, там она решается только переписыванием приложения или патчем бинарника (абсолютные пути заменяются на относительные).

В snap есть механизм layout, в котором можно отобразить файлы и папки из доступных snap’у путей в глобальную иерархию. Это настраивается в том же snapcraft.yaml, например

layout:
  /var/lib/foo:
    bind: $SNAP_DATA/var/lib/foo
  /usr/share/foo:
    bind: $SNAP/usr/share/foo
  /etc/foo.conf:
    bind-file: $SNAP_DATA/etc/foo.conf

Почему холодный старт долгий?

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

Внутренности snap заархивированы, соответственно при запуске их нужно разархивировать. Судя по всему команда snap выбрала неудачных метод сжатия (в отличие от AppImage), который лучше сжимает, но работает долго. Вот тут они пишут, как попытались это исправить.

Также при старте нужно раскрутить apparmor и проставить все указанные layout, то есть сделать правильные mount namespace. Если layout очень развесистый, это действительно может тормозить запуск, о чем они сами пишут в документации.

По поводу snap нужно также понимать, что изначально эта технология была сделана для IoT, а потом её натянули на десктопы. Насколько была удачна идея, покажет история. Вот тут пользователем popeydc составлен довольно неплохой список почему люди не очень любят snap.

Заключительные мысли

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

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

© Habrahabr.ru