Compose Multiplatform is a fantastic framework for building desktop applications, but its default Linux packaging capabilities, which rely on jpackage, have some known limitations. A common pain point for developers is the lack of fine-grained control over the generated .deb package, especially concerning the .desktop entry file.
This often leads to issues like:
- The application icon showing correctly in the app menu, but displaying as a generic icon in the dock or taskbar when running.
- The inability to place icons and other resources in standard system-wide directories like
/usr/share/icons.
This tutorial provides a robust, script-based solution to overcome these limitations. We will create a "post-processing" script that takes the basic .deb package generated by Gradle and enhances it into a professional, standards-compliant package. This solution was perfected during an intensive, all-night debugging session, and it's built to be both powerful and reliable.
- A working Compose Multiplatform project for Desktop.
- A Linux environment with standard tools (
dpkg-deb,ar,tar,sed,grep, etc.). - Your project version is managed in a
gradle/libs.versions.tomlfile.
First, we need to create a directory to hold our custom packaging assets.
- Inside your
composeAppmodule, create the directorypackaging/linux. - Place your application icon (e.g.,
exampleapp.png) inside this new directory. A size like 256x256 or 512x512 is recommended. - Create your custom
.desktopfile template in the same directory.
Your project structure should look like this:
.
├── gradle/
│ └── libs.versions.toml
└── composeApp/
├── build.gradle.kts
├── src/
└── packaging/
└── linux/
├── exampleapp.desktop <-- Your custom template
└── exampleapp.png <-- Your application icon
This is the core of our solution. We use a placeholder for the version and, most importantly, we can hardcode the correct StartupWMClass to fix the dock icon issue.
composeApp/packaging/linux/exampleapp.desktop:
[Desktop Entry]
Version=@version@
Type=Application
Name=ExampleApp
Comment=An example application built with Compose Multiplatform.
Exec=/opt/exampleapp/bin/ExampleApp
Icon=exampleapp
Terminal=false
Categories=Utility;Application;
StartupWMClass=com-example-MainKtVersion=@version@: A placeholder that our script will automatically replace.StartupWMClass=...: The crucial line to ensure the running application's window is correctly matched to its launcher. (You can find your app'sWM_CLASSby running it, then executingxprop WM_CLASSin a terminal and clicking on the app window. It is often derived from your main class, e.g.,com.example.MainKt).
This script automates the entire enhancement process. Place it in your project's root directory.
build-custom-deb.sh:
#!/bin/bash
# A script to build a custom .deb package by post-processing the one generated by Gradle.
set -e
# --- Configuration ---
APP_NAME="exampleapp"
DEB_ARCH="amd64"
TOML_FILE="gradle/libs.versions.toml"
CUSTOM_RESOURCES_DIR="composeApp/packaging/linux"
ICON_NAME="${APP_NAME}.png"
ICON_INSTALL_DIR="/usr/share/icons/hicolor/512x512/apps"
ORIGINAL_DEB_DIR="composeApp/build/compose/binaries/main-release/deb"
FINAL_DEB_DIR="composeApp/build/dist"
# --- Script Start ---
# 1. Read version from libs.versions.toml
echo "### 1. Reading version from '$TOML_FILE'..."
if [ ! -f "$TOML_FILE" ]; then
echo "ERROR: Version catalog file not found at '$TOML_FILE'"
exit 1
fi
VERSION=$(grep 'app-versionName' "$TOML_FILE" | cut -d '"' -f 2)
if [ -z "$VERSION" ]; then
echo "ERROR: Could not find 'app-versionName' in '$TOML_FILE'"
exit 1
fi
echo " - Found app-versionName: $VERSION"
# 2. Build the basic .deb package with Gradle
echo "### 2. Building the basic DEB package with Gradle..."
./gradlew packageReleaseDeb
# 3. Find the original .deb file
ORIGINAL_DEB_PATH=$(find "$ORIGINAL_DEB_DIR" -name "*.deb")
if [ -z "$ORIGINAL_DEB_PATH" ]; then
echo "ERROR: Original .deb file not found."
exit 1
fi
echo " - Found original DEB at: $ORIGINAL_DEB_PATH"
# 4. Unpack the original .deb file
WORK_DIR="composeApp/build/deb_work"
echo "### 3. Unpacking the original DEB file into '$WORK_DIR'..."
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
dpkg-deb -R "$ORIGINAL_DEB_PATH" "$WORK_DIR"
# 5. Inject custom files
echo "### 4. Injecting custom files..."
## Fixes by tavioribeiro
# A) REMOVE DUPLICATES: Delete any .desktop file generated by jpackage
# This prevents having two icons in the application menu.
find "$WORK_DIR" -name "*.desktop" -type f -delete
# B) FIX INSTALL SCRIPTS: Remove 'xdg-desktop-menu' commands
# Since we deleted the original .desktop file, the default post-install scripts
# will fail because they try to install a file that no longer exists.
if [ -f "$WORK_DIR/DEBIAN/postinst" ]; then
sed -i '/xdg-desktop-menu/d' "$WORK_DIR/DEBIAN/postinst"
echo " - Fixed 'postinst' script."
fi
if [ -f "$WORK_DIR/DEBIAN/prerm" ]; then
sed -i '/xdg-desktop-menu/d' "$WORK_DIR/DEBIAN/prerm"
echo " - Fixed 'prerm' script."
fi
# a. Inject .desktop file and replace placeholder
DESKTOP_TEMPLATE="$CUSTOM_RESOURCES_DIR/$APP_NAME.desktop"
DESTINATION_DESKTOP_DIR="$WORK_DIR/usr/share/applications"
mkdir -p "$DESTINATION_DESKTOP_DIR"
sed "s/@version@/$VERSION/g" "$DESKTOP_TEMPLATE" > "$DESTINATION_DESKTOP_DIR/$APP_NAME.desktop"
echo " - Injected .desktop file with version '$VERSION'"
# b. Inject icon file
ICON_SOURCE_PATH="$CUSTOM_RESOURCES_DIR/$ICON_NAME"
DESTINATION_ICON_DIR="$WORK_DIR/$ICON_INSTALL_DIR"
if [ ! -f "$ICON_SOURCE_PATH" ]; then
echo "WARNING: Icon file not found at '$ICON_SOURCE_PATH'. Skipping icon copy."
else
mkdir -p "$DESTINATION_ICON_DIR"
cp "$ICON_SOURCE_PATH" "$DESTINATION_ICON_DIR/"
echo " - Injected icon file to $ICON_INSTALL_DIR"
fi
# 6. Repack into the final .deb file with corrected ownership
FINAL_DEB_NAME="${APP_NAME}_${VERSION}_${DEB_ARCH}_custom.deb"
FINAL_DEB_PATH="$FINAL_DEB_DIR/$FINAL_DEB_NAME"
mkdir -p "$FINAL_DEB_DIR"
echo "### 5. Repacking into final DEB package..."
# The final, correct command to build a warning-free package
# Option first, then Command, then Arguments.
dpkg-deb --root-owner-group -b "$WORK_DIR" "$FINAL_DEB_PATH"
echo ""
echo "================================================="
echo "SUCCESS! Custom DEB package created at:"
echo "$FINAL_DEB_PATH"
echo "================================================="Using the script is straightforward.
- Make the script executable:
chmod +x build-custom-deb.sh
- Run the script from your project's root directory:
./build-custom-deb.sh
The script will handle everything automatically. Upon completion, you will find your professional, ready-to-distribute .deb file in the composeApp/build/dist/ directory.
- Read Version: The script starts by reading the
app-versionNamefrom yourgradle/libs.versions.tomlfile, ensuring your package version is always in sync with your project configuration. - Initial Build: It runs the standard
./gradlew packageReleaseDebtask to generate a basic.debpackage. - Unpack: It uses
dpkg-deb -Rto unpack the basic.debfile into a temporary working directory, exposing its complete filesystem structure. - Inject & Customize: This is the core logic. The script copies your custom icon to the standard
/usr/share/icons/...path and copies your.desktoptemplate to/usr/share/applications. During this process, it usessedto replace the@version@placeholder with the actual version string. - Repack: Finally, it uses
dpkg-deb --buildto re-assemble the modified directory into a new.debpackage. The--root-owner-groupflag is crucial here, as it ensures the final package has the correct file ownership (root:root), preventing any warnings during installation.
By following this guide, you can automate the creation of high-quality Debian packages for your Compose Multiplatform applications, providing a seamless and professional experience for your Linux users.
build-arch-package.sh: