По состоянию на 12 июня 2024 года нет хорошего туториала/документации по Compose Desktop ShadowJar.
Существующие руководства помогут настроить ComposeMultiplatform для распространения нативных таргетов.
Но что, если вы хотите использовать ShadowJar, чтобы каждый пользователь мог запустить ваше приложение через .jar
?
Дисклеймер: Это руководство предоставит информацию о некоторых частных случаях, но в зависимости от вашего проекта шаги могут отличаться.
Эта статья, вероятно, не предоставит полного охвата вашего частного случая. Более того, не каждый пользователь сможет запустить созданный .jar файл. Например, пользователь с Java 8 не сможет запустить .jar файл, который был построен с использованием более поздней версии Java. Но это не относится напрямую к статье.
Статья состоит из шагов:
Настройка
build.gradle.kts
с необходимыми плагинамиДобавление необходимых зависимостей
Настройка compose.desktop
Создание задачи shadowJar
Создание задачи ProGuard
Создание проекта
Если у вас нет проекта - скачайте его с https://terrakok.github.io/Compose-Multiplatform-Wizard/
Настройка зависимостей
Заполним наш libs.versions.toml
необходимыми зависимостями:
[versions]
# Project
packagename = "com.makeevrserg.composeshadow"
version-string = "1.0.0"
name = "ComposeShadow"
description = "Sample compose shadow"
[libraries]
proguard = { module = "com.guardsquare:proguard-gradle", version.strictly = "7.5.0" }
kotlin-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.strictly = "1.9.0-RC" }
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.strictly = "2.0.20-RC" }
kotlin-compose = { id = "org.jetbrains.compose", version.strictly = "1.7.0-alpha02" }
kotlin-compose-gradle = { id = "org.jetbrains.kotlin.plugin.compose", version.strictly = "2.0.20-RC" }
shadow = { id = "com.github.johnrengelman.shadow", version.strictly = "8.1.1" }
Настройка gradle.kts
В вашем root.gradle.kts
вы должны добавить proguard и shadow:
buildscript {
dependencies {
classpath(libs.proguard)
}
}
plugins {
alias(libs.plugins.some.other.dependencies).apply(false)
alias(libs.plugins.shadow).apply(false)
}
Теперь нам нужно настроить наш build.gradle.kts
для модуля composeDesktop
.
Основная проблема заключается в том, что мы не можем использовать kotlin("multiplatform")
Плагин ShadowJar
не работает с ним.
Таким образом, решение заключается в том, чтобы включить kotlin("jvm")
вместо него.
plugins {
kotlin("jvm")
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose")
alias("com.github.johnrengelman.shadow")
}
Теперь нам нужно добавить зависимости для compose. Здесь есть несколько сложностей:
Файлы JAR, собранные в Windows, будут запускаться только в Windows
Файлы JAR, собранные в Linux, будут запускаться в Windows и Linux
Файлы JAR, собранные в MacOS, будут запускаться в MacOS, Windows и Linux
Таким образом, мы должны собрать наш shadow в MacOS, чтобы поддерживать все таргетные платформы.
Но у меня нет MacOS! Нет проблем - GitHub предоставляет нам CI, который содержит различные образы, включая macos.
Вы можете использовать его для создания этого супер-универсального jar-файла
Тестируйте локально на вашей текущей ОС и используйте GitHub CI для сборки на всех ОС одновременно на раннерах MacOS.
Настройка зависимостей
Здесь мы включаем список всех таргетных платформ для compose desktop.
Нативные бинарные файлы будут встроены в результирующий .jar файл.
dependencies {
implementation(compose.desktop.macos_x64)
implementation(compose.desktop.macos_arm64)
implementation(compose.desktop.linux_x64)
implementation(compose.desktop.linux_arm64)
implementation(compose.desktop.windows_x64)
// Ваши остальные зависимости
}
Настройка плагина compose
Возможно, вы хотите использовать ShadowJar, но во время отладки удобно все запускать именно через compose-desktop:
compose.desktop {
application {
// Укажите главный класс, который содержит функцию main()
mainClass = "${libs.versions.packagename.get()}.MainKt"
// Добавьте параметры jvm по вашему вкусу
jvmArgs += listOf("--add-opens", "java.base/java.lang=ALL-UNNAMED")
// Настройте нативные дистрибутивы на всякий случай
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
licenseFile.set(rootProject.file("LICENSE.md"))
packageName = libs.versions.packagename.get()
description = libs.versions.description.get()
packageVersion = libs.versions.version.string.get()
// Здесь вы должны вставить модули, которые будут встроены
// Не очень удобно, если вы не знакомы с Java, но выведите в консоль это и посмотрите, какие jmods у вас есть
println("JMODS Folder: ${compose.desktop.application.javaHome}/jmods/java.base.jmod")
// Например, если вы используете ROOM Multiplatform, вам определённо нужно это
modules("java.sql")
// Или включите все модули сразу
includeAllModules = false
}
}
}
Наконец, первоначальный шаг завершён.
Теперь вы можете создавать jar и нативные дистрибутивы с помощью команд:
./gradlew :composeApp:packageDistributionForCurrentOS
./gradlew :composeApp:packageUberJarForCurrentOS
Но мы хотим использовать shadow для независимости от плагина composeDesktop!
Настройка ShadowJar
Стандартная таска ShadowJar, вероятно, не должна вызвать много проблем, но это может варьироваться от проекта к проекту.
Таска ниже охватывает многие частные случаи, поэтому, вероятно, должна работать в большинстве проектов.
val shadowJar by tasks.named<ShadowJar>("shadowJar") {
dependsOn(configurations)
// Различается для каждого проекта, но на всякий случай
minimize {
// Исключить swing-coroutines
exclude(dependency(libs.kotlin.coroutines.swing.get()))
// Исключить каждую таргет compose
exclude(dependency(dependencies.compose.desktop.macos_x64))
exclude(dependency(dependencies.compose.desktop.macos_arm64))
exclude(dependency(dependencies.compose.desktop.linux_x64))
exclude(dependency(dependencies.compose.desktop.linux_arm64))
exclude(dependency(dependencies.compose.desktop.windows_x64))
// Используете apache poi? Или какую-то другую древнюю зависимость Java?
// Не забудьте добавить исключение
exclude(dependency("org.apache.poi:poi-ooxml:.*"))
// Некоторые подпроекты исключились во время минимизации?
// Добавьте каждую зависимость вручную или всё сразу, как показано здесь
rootProject.subprojects.map(::dependency).forEach(::exclude)
}
// Некоторые настройки shadow по умолчанию
mergeServiceFiles()
isReproducibleFileOrder = true
archiveClassifier = null as String?
archiveVersion = "${libs.versions.version.string.get()}-desktop"
archiveBaseName = libs.versions.name.get()
// Переместите аутпут jar в другую папку
rootProject.file("jars").also { destination ->
if (!destination.exists()) destination.parentFile?.mkdirs()
destinationDirectory = destination
}
// Не забудьте указать главный файл, который содержит функцию main()
manifest {
attributes("Main-Class" to "${libs.versions.packagename.get()}.MainKt")
}
}
Теперь мы наконец можем создать наш .jar файл с помощью задачи ShadowJar ./gradlew :composeApp:shadowJar
И этот .jar файл содержит нативные бинарные файлы для всех платформ (если собран на macos)
![](https://habrastorage.org/getpro/habr/upload_files/8d4/632/e2e/8d4632e2ed8956205a70a4e28812ee63.png)
Хорошо, это круто, но разве я только что не распаковал .jar файл?
Я буквально могу видеть исходники что-то даже понимаю! Я не хочу, чтобы кто-то украл мой код!
В этом и пригодится ProGuard, мы наконец можем использовать таску Gradle ProGuard, давайте её настроим.
Настройка ProGuard
Задача proguard сложна из-за множества корнер кейсов.
Сомнительно, что вы успешно запустите задачу обфускации с первого раза.
Даже если это получится, вероятно, запущенная задача .jar скорее всего крашнется из-за незаресолвленного класса.
tasks.register<ProGuardTask>("obfuscate") {
dependsOn(shadowJar)
// Вставьте базовые jmods
libraryjars("${compose.desktop.application.javaHome}/jmods/java.base.jmod")
libraryjars("${compose.desktop.application.javaHome}/jmods/java.desktop.jmod")
// Если у вас есть что-то конкретное, добавьте эти jar здесь, как в примере
// Например, в случае использования apache-poi надо добавить это
libraryjars(project.file("lib").resolve("asm-3.1.jar"))
// Укажите, что нужно обфусцировать
injars(shadowJar.outputs.files)
// Установите аутпутный файл для обфусцированного jar
val obfuscated = rootProject.file("jars")
.resolve("${libs.versions.name.get()}-${libs.versions.version.string.get()}-desktop-obf.jar")
outjars(obfuscated)
// Не забудьте создать сиды, чтобы можно было восстановить обфусцированные исходники
printseeds("$buildDir/obfuscated/seeds.txt")
printmapping("$buildDir/obfuscated/mapping.txt")
// Добавьте ваши файлы proguard
// И не забудьте добавить proguard по умолчанию для compose desktop
configuration(files("proguard-rules.pro", "default-compose-desktop-rules.pro"))
verbose()
}
Мы наконец можем обфусцировать наш .jar файл ./gradlew :composeApp:obfuscate
Не используйте JetBrains runtime
Консоль говорит, что не может разрешить некоторые классы или что-то в этом роде!
Ещё один корнер кейс. Убедитесь, что вы не используете среду выполнения Java от JetBrains!
![](https://habrastorage.org/getpro/habr/upload_files/11e/b37/37c/11eb3737c0053961f7efe53975e3d9f1.png)
Во время задач обфускации ProGuard могут возникать другие различные приколы, которые не могут быть охвачены в этой статье.
Просто используйте ./gradlew :composeApp:obfuscate --info --stacktrace
, чтобы увидеть каждую ошибку, возникшую во время сборки, и добавляйте исключения самостоятельно.
Размер jar
В зависимости от вашей конфигурации ProGuard размер создаваемых jar-файлов будет различаться.
Но в любом случае, вы встраиваете 5 целевых платформ compose-desktop в один жарник.
Поэтому размер составляет минимум ~60 МБ.
Так что если вы готовы пожертвовать размером - shadowJar для вас!
![](https://habrastorage.org/getpro/habr/upload_files/211/d01/117/211d01117dc1818af8cb3aac01e15477.png)
Источники
Готовые работающие исходники находятся тут.