По состоянию на 12 июня 2024 года нет хорошего туториала/документации по Compose Desktop ShadowJar.

Существующие руководства помогут настроить ComposeMultiplatform для распространения нативных таргетов.

Но что, если вы хотите использовать ShadowJar, чтобы каждый пользователь мог запустить ваше приложение через .jar?

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

Эта статья, вероятно, не предоставит полного охвата вашего частного случая. Более того, не каждый пользователь сможет запустить созданный .jar файл. Например, пользователь с Java 8 не сможет запустить .jar файл, который был построен с использованием более поздней версии Java. Но это не относится напрямую к статье.

Статья состоит из шагов:

  1. Настройка build.gradle.kts с необходимыми плагинами

  2. Добавление необходимых зависимостей

  3. Настройка compose.desktop

  4. Создание задачи shadowJar

  5. Создание задачи 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 для вас!

Источники

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

Комментарии (0)