Добавляем поддержку Flatpak в Compose Desktop

от автора

Те, кто делали мультиплатформенное приложение с помощью Compose Multiplatform, наверное уже сталкивались с тем, в как публиковать приложение. Для Linux на текущий момент доступны следующие форматы: Deb — «нативные» пакеты для Debian-подобных дистрибутивов; Rpm — такие же пакеты для Fedora, RHEL; AppImage — portable приложения(одним файлом). Недостаток первых двух — заточенность только под одну платформу(Debian и Fedora соответственно), второго — отсутствие пакетного менеджера в абсолютном большинстве дистрибутивов. Негодуя с этого, я решил внедрить compose-приложение в Flatpak — пакетный менеджер для sandboxed приложений. Sandboxed apps — приложения, которые по умолчанию не имеют доступа к файлам пользователя и другим настройкам. Flatpak дает уверенность, что та или иная функция/бинарник присутствуют в системе и могут быть использованы. Также с помощью Portals, которые встроены в Flatpak, приложение может безопасно и независимо осуществлять некоторые операции вроде доступа к камере, показа уведомлений и другого. Как вы могли видеть ранее, поддержки Flatpak в Compose Multiplatform нет.

Что нужно установить?

  • Непосредственно Flatpak. Как установить можно посмотреть здесь.

  • Flatpak-builder — для сборки flatpak-приложений. Обычно устанавливается так же, как и сам пакетный менеджер.

  • Установить org.freedesktop.Sdk и org.freedesktop.Platform версии 22.08 через flatpak.

Как мы собираемся реализовать поддержку Flatpak?

Так как у Compose Desktop из таргетов нет ничего универсальнее AppImage, то будем использовать его. Он собирается с помощью gradle task packageAppImage. Бинарник находится по пути build/compose/binaries/main/app/[appName]/, где [appName] — имя проекта/приложения.

В этой папке следующая структура:

MyApp     ├── bin     │   └── MyApp     └── lib         ├── app/         ├── libapplauncher.so         ├── runtime/         └── MyApp.png

В папке bin находится сам бинарник, который мы собираемся запускать. В папке lib находятся «кишки» приложения: app — jar-ники библиотек и ресурсы. libapplauncher.so — волшебный файл, соединяющий все внутренности. runtime — внутренние библиотеки. MyApp.png — иконка приложения(не используется).

Сам бинарник без папки lib/ не запустится!

Добавляем манифест и иконку

Для любого Flatpak-приложения нужен манифест. Аналог в Android-мире — AndroidManifest.xml. Он описывает разрешения и основную информацию о приложении. В Flatpak для этого используется формат описывания json или yaml.

Создадим файл src/desktopMain/resources/flatpak/manifest.yml:

app-id: com.company.myapp runtime: org.freedesktop.Platform runtime-version: '22.08' sdk: org.freedesktop.Sdk command: /app/bin/MyApp finish-args:     - --share=network     - --socket=x11     - --socket=fallback-x11     - --device=dri modules:     - name: myapp       buildsystem: simple       build-commands:         - cp -r bin/ /app/bin/         - cp -r lib/ /app/lib/         - mkdir -p /app/share/applications         - install -D com.company.myapp.desktop /app/share/applications/com.company.myapp.desktop         - mkdir -p /app/share/icons/hicolor/scalable/apps/         - cp -r logo_round_preview.svg /app/share/icons/hicolor/scalable/apps/com.company.myapp.svg       sources:         - type: file           path: logo_round_preview.svg         - type: dir           path: "bin/"           dest: "bin/"         - type: dir           path: "lib/"           dest: "lib/"         - type: file           path: com.company.myapp.desktop
  • (1) Id приложения(как applicationId в Android). Обязательный пункт

  • (2) Какой runtime нам нужен. Выбираем org.freedesktop.Platform так как это стандартный runtime. Обязательный пункт

  • (3) Версия runtime. Очень желательно использовать наиболее свежую версию. Обязательный пункт

  • (4) Sdk для сборки приложения. Обязательный пункт

  • (5) Путь к бинарнику приложения. Обязательный пункт

  • (6-10) Разрешения для приложения:

    1. --share=network — доступ к интернету.

    2. --socket=x11 — доступ к оконному менеджеру X11. На данный момент Compose Desktop не поддерживает Wayland. Обязательный пункт

    3. --socket=fallback-x11 — чтобы нормально запускаться под чистым Wayland(не напрямую). Очень желательно

    4. --device=dri — аппаратное ускорение с помощью GPU. Очень желательно

  • (11-31) Модули для установки(список).

  • (12) Название модуля.

  • (13) Система сборки.

  • (14-20) Команды во время сборки:

    • (15-16) Копирование папки bin и lib в соответствующие папки в внутреннее хранилище приложения.

    • (17) Создание папки, где будет храниться файл конфигурации иконки.

    • (18) Копирование файла конфигурации в внутреннее хранилище. Файл обязательно назвать так [appId].desktop, где [appId] — id приложения.

    • (19) Создание папки, где будет храниться сама иконка.

    • (20) Копирование иконки в внутреннее хранилище. Обязательно svg. Если хотите загружать в других форматах, смотрите здесь

  • (21-31) Ресурсы, которые нужны модулю:

    • (22) Тип ресурса. Самые используемые — file и dir

    • (23) Путь к файлу. Также, вместо path можно вставлять url и брать файлы c интернета.

Далее создадим файл конфигурации иконки src/desktopMain/resources/flatpak/icon.desktop. Он нужен, чтобы понимать системе, как показывать и запускать приложение(надо указывать всё):

[Desktop Entry] Encoding=UTF-8 Version=1.0 Type=Application Terminal=false Exec=/app/bin/MyApp Name=MyApp Icon=com.company.myapp 
  1. Указание, что это файл конфигурации иконки.

  2. Кодировка(UTF-8, стандартная).

  3. Версия спецификации файла конфигурации.

  4. Тип приложения. В нашем случае — Application.

  5. Запускать ли приложение в терминале. Если указать true, то при запуске приложения запустится и терминал.

  6. Путь к бинарнику, который мы указывали в манифесте.

  7. Имя, которое будет видно в лаунчере.

  8. Иконка приложения в виде id приложения(именно поэтому мы указывали app id, когда копировали иконку)

Дальше, если хотим svg, надо будет добавить иконку в src/desktopMain/resources/. Если хотим другие форматы, смотрим здесь.

Конфигурируем Gradle

В первую очередь, надо добавить поддержку AppImage в наш проект. Делается это в build.gradle.kts:

compose.desktop {   //...   application {     //...     nativeDistributions {       //...       targetFormats(         TargetFormat.AppImage,         // Другие форматы       )     }   } }

Также, добавим новый task в тот же файл:

val appId = "com.company.myapp" tasks.register("packageFlatpak") {     dependsOn("packageAppImage")     doLast {         delete {             delete("$buildDir/flatpak/bin/")             delete("$buildDir/flatpak/lib/")         }         copy {             from("$buildDir/compose/binaries/main/app/MyApp/")             into("$buildDir/flatpak/")             exclude("$buildDir/compose/binaries/main/app/MyApp/lib/runtime/legal")         }         copy {             from("$rootDir/src/desktopMain/resources/logo_round_preview.svg")             into("$buildDir/flatpak/")         }         copy {             from("$rootDir/src/desktopMain/resources/logo_round.svg")             into("$buildDir/flatpak/")         }         copy {             from("$rootDir/src/desktopMain/resources/flatpak/manifest.yml")             into("$buildDir/flatpak/")             rename {                 "$appId.yml"             }         }         copy {             from("$rootDir/src/desktopMain/resources/flatpak/icon.desktop")             into("$buildDir/flatpak/")             rename {                 "$appId.desktop"             }         }         exec {             workingDir("$buildDir/flatpak")             commandLine("flatpak-builder --install --user --force-clean --state-dir=build/flatpak-builder --repo=build/flatpak-repo build/flatpak-target $appId.yml".split(" "))         }     } }

Этот task будет копировать все файлы в build/flatpak, собирать и устанавливать приложение.

Если хотим запускать приложение прямо из ide, то создаем еще один task:

tasks.register("runFlatpak") {     dependsOn("packageFlatpak")     doLast {         exec {             commandLine("flatpak run $appId".split(" "))         }     } }

После синхронизации градла все эти программы будут должны появиться во вкладке Gradle Tasks -> other:

Если дважды кликнем по нему, то автоматически запустится этот task.

Интеграция с системой

На текущий момент метод isSystemInDarkTheme всегда возвращает false, если запускать приложение в Flatpak. Здесь на помощь приходят Portals. С помощью них мы можем получить информацию о текущей теме устройства. К сожалению, это api появилось совсем недавно, поэтому оно поддерживается только новыми средами рабочего стола. Это делается с помощью этой команды:

gdbus call --session --dest=org.freedesktop.portal.Desktop --object-path=/org/freedesktop/portal/desktop --method=org.freedesktop.portal.Settings.Read org.freedesktop.appearance color-scheme

Команда возвращает темную тему в таком формате:

(<<uint32 1>>,)

Где 1 означает, что темная тема включена. Если она отключена, то вместо 1 будет 0.

Давайте напишем функцию, которая будет читать вывод с терминала:

suspend fun runInShell(command: String): Process {     return ProcessBuilder(*command.split(" ").toTypedArray())         .redirectError(ProcessBuilder.Redirect.INHERIT)         .start().apply {             waitFor(60, TimeUnit.MINUTES)         } }  suspend fun Process.readString(): String {     var o = ""     val b = BufferedReader(InputStreamReader(inputStream))     var line = ""     while (b?.readLine()?.also { line = it } != null) o += line     return o }

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

В модуле commonMain создадим expect Composable-функцию, которая будет возвращать тему в системе:

@Composable expect fun platformIsSystemInDarkTheme(): Boolean

В модуле desktopMain создадим имлементацию этого метода, где будет раз в 0.1 секунду проверять на текущую тему:

private const val command =     "gdbus call --session --dest=org.freedesktop.portal.Desktop --object-path=/org/freedesktop/portal/desktop --method=org.freedesktop.portal.Settings.Read org.freedesktop.appearance color-scheme"  @Composable actual fun platformIsSystemInDarkTheme(): Boolean {     var darkTheme by remember { mutableStateOf(false) }     LaunchedEffect(true) {         while (true) {             kotlin.runCatching {                 val str = runInShell(command).readString()                 darkTheme = str[10] == '1'             }             delay(100)         }     }     return darkTheme }

В фунции, где вы прописываете тему приложения, вызываем метод(один раз!):

@Composable fun AppTheme(     darkTheme: Boolean = platformIsSystemInDarkTheme(),     dynamicColor: Boolean = true,     content: @Composable () -> Unit ) {   //... }

Теперь ваше приложение автоматически подстраивается под системную тему.

Небольшие хитрости

Если у вас не запускается приложение, то попробуйте обновить compose до крайней версии. Зачастую это помогает(особенно в alpha-версиях). Если не поможет, то уберите параметры undecorated и transparent в Composable-окне приложения:

fun main() =    application {     Window(       onCloseRequest = {         exitApplication()       },        title = "MyApp",       undecorated = true, // закомментировать, если не запускается       transparent = true // закомментировать, если не запускается       ) {         // content       }   }

Заключение

Если хотите подробнее углубиться в тему Flatpak, то читайте документацию.

Исходники вы можете увидеть здесь.

Автор чукча, поэтому если увидели ошибки — пишите.


ссылка на оригинал статьи https://habr.com/ru/post/701078/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *