Skip to content

feat: Download skiko libraries at runtime to reduce the jar size #150

@kitadai31

Description

@kitadai31

Feature description

The reason the CLI jar size is currently large is because it includes native skiko (Skia for kotlin) libraries for all OSes.
By not including skiko in the jar and instead downloading it on the first launch of GUI, the size of the jar file can be significantly reduced.
(On-demand downloading, lazy downloading)

This has the following advantages:

  • Since only the skiko file for that OS will be downloaded, the total download size will be smaller.
  • The update frequency of the skiko version that the CLI depends on is lower than that of the CUI itself, so once downloaded, the skiko libraries can be used for a long period of time across multiple versions of the CLI. (The skiko version is only bumped when bumping the KMP dependency.)
  • The OS will absolutely never change to another OS after the first launch of Morphe GUI, so once skiko is downloaded, no further downloads of skiko will occur until bumping KMP.
  • Users who do not use a GUI and only use the CLI (e.g., Termux users) do not need to download unnecessary libraries.

This method has the following limitations, but Morphe CLI happens to satisfy all of them.

  • Requires internet on first launch. — ✅ Morphe GUI already requires an internet connection to function. The CLI one will continue to work offline.
  • Requires a place to save the Skiko library. — ✅ Morphe GUI has a user data directory in the system's AppData directory.

The following is my summary of the results I obtained by asking ChatGPT how to achieve this.

Answer by ChatGPT

The key points are the following three:

  1. Exclude the skiko dependency in build.gradle.kts.
  2. Detect the OS at startup.
  3. Download the corresponding skiko library and use System.load().

The implementation plan is outlined below.


1. Architecture

Basic structure

App.jar
↓
Startup
↓
OS detection
↓
Check if Skiko is present
↓
Download if not
↓
Save to AppData
↓
System.load()
↓
Start Compose

2. AppData storage location

fun appDataDir(): File {
    val home = System.getProperty("user.home")

    return when {
        isWindows -> File(System.getenv("APPDATA"), "MyApp")
        isMac -> File(home, "Library/Application Support/MyApp")
        else -> File(home, ".myapp")
    }
}

Example

Windows
C:\Users\user\AppData\Roaming\morphe-gui\skiko\0.148.0\skiko-windows-x64.dll

Mac
~/Library/Application Support/morphe-gui/skiko/0.148.0/libskiko-macos-arm64.dylib

3. Download skiko

You can get it from Skiko's GitHub releases.

Example: https://github.com/JetBrains/skiko/releases/download/0.7.90/

There you will find:

skiko-awt-runtime-windows-x64-0.148.0.jar
skiko-awt-runtime-windows-arm64-0.148.0.jar
skiko-awt-runtime-macos-x64-0.148.0.jar
skiko-awt-runtime-macos-arm64-0.148.0.jar
skiko-awt-runtime-linux-x64-0.148.0.jar
skiko-awt-runtime-linux-arm64-0.148.0.jar 

The dynamic libraries are included in the JAR file.

fun download(url: String, dest: File) {
    URL(url).openStream().use { input ->
        dest.outputStream().use { output ->
            input.copyTo(output)
        }
    }
    // Verify the expected file hash
}

Alternatively, you could host the dll/dylib/so yourself somewhere.

I (kitaidai31) noticed that skiko-awt-runtime-macos-arm64-0.148.0.jar contains two dylibs, one for x64 and one for arm64. Therefore, using this GitHub release will result in unnecessarily downloading the x64 dylib on Macs with Apple Silicon, so it might be better to do self hosting.


4. load native

fun loadSkiko() {
    val dir = appDataDir().apply { mkdirs() }

    val lib = when {
        isWindows -> File(dir, "skiko.dll")
        isMac -> File(dir, "libskiko.dylib")
        else -> File(dir, "libskiko.so")
    }

    if (!lib.exists()) {
        download(getUrlForOS(), lib)
    }

    System.load(lib.absolutePath)
}

5. Run before launching Compose

fun main() {
    loadSkiko()

    application {
        Window(onCloseRequest = ::exitApplication) {
            App()
        }
    }
}

6. Important Note

Skiko requires version matching.

Example:

compose 1.6.0
↓
skiko 0.7.90

You need to match the version of Skiko that Compose expects.

Additionally, for security reasons, hard-code the expected file hash into the Moprhe GUI code.
This ensures that the Morphe GUI is prevented from loading any libraries other than those it expects.

Note that in order to perform a download before launching Compose, you will likely need to implement a download progress dialog using AWT or Swing. This dialog box can be very simple. I think it will only have the message "Downloading necessary library..." and a cancel button.

Motivation

  • Reduce download size
  • Optimization for users who only use the CLI.

Acknowledgements

  • I have checked all open and closed feature requests and this is not a duplicate.
  • I have chosen an appropriate title.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions