По состоянию на 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)

Хорошо, это круто, но разве я только что не распаковал .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!

Во время задач обфускации ProGuard могут возникать другие различные приколы, которые не могут быть охвачены в этой статье.
Просто используйте ./gradlew :composeApp:obfuscate --info --stacktrace
, чтобы увидеть каждую ошибку, возникшую во время сборки, и добавляйте исключения самостоятельно.
Размер jar
В зависимости от вашей конфигурации ProGuard размер создаваемых jar-файлов будет различаться.
Но в любом случае, вы встраиваете 5 целевых платформ compose-desktop в один жарник.
Поэтому размер составляет минимум ~60 МБ.
Так что если вы готовы пожертвовать размером - shadowJar для вас!

Источники
Готовые работающие исходники находятся тут.
Автор: makeevrserg