Те, кто делали мультиплатформенное приложение с помощью 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) Разрешения для приложения:
-
--share=network— доступ к интернету. -
--socket=x11— доступ к оконному менеджеру X11. На данный момент Compose Desktop не поддерживает Wayland. Обязательный пункт -
--socket=fallback-x11— чтобы нормально запускаться под чистым Wayland(не напрямую). Очень желательно -
--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
-
Указание, что это файл конфигурации иконки.
-
Кодировка(UTF-8, стандартная).
-
Версия спецификации файла конфигурации.
-
Тип приложения. В нашем случае — Application.
-
Запускать ли приложение в терминале. Если указать
true, то при запуске приложения запустится и терминал. -
Путь к бинарнику, который мы указывали в манифесте.
-
Имя, которое будет видно в лаунчере.
-
Иконка приложения в виде 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/
Добавить комментарий