How to Publish an Avalonia Application as AppImage

This comprehensive tutorial will guide you through creating an AppImage for any Avalonia (or .NET) application. AppImage is a universal Linux package format that works across all distributions without installation.

Table of Contents

  1. Prerequisites
  2. Understanding AppImage Structure
  3. Step-by-Step Guide
  4. Troubleshooting
  5. Distribution

Prerequisites

Required Tools

  • .NET SDK (version 8.0 or later)
  • appimagetool (will be downloaded automatically by the script)
  • Linux system (or WSL2 for Windows users)

Project Requirements

  • A working Avalonia application
  • Project configured for publishing (.csproj file)
  • Application icon (PNG format recommended)
  • Any additional assets (sounds, images, etc.)

Understanding AppImage Structure

An AppImage is essentially a compressed filesystem that contains:

YourApp.AppDir/
├── AppRun                    # Startup script
├── YourApp.desktop          # Desktop entry file
├── yourapp.png              # Application icon
├── .DirIcon                 # Symlink to icon
└── usr/
    ├── bin/
    │   └── YourApp          # Your executable
    ├── lib/
    │   └── *.so             # Shared libraries
    └── share/
        ├── applications/
        │   └── YourApp.desktop
        └── icons/
            └── hicolor/
                └── 256x256/
                    └── apps/
                        └── yourapp.png

Step-by-Step Guide

Step 1: Prepare Your Project

1.1 Configure .csproj for Native AOT (Optional but Recommended)

Add these properties to your YourApp.csproj:

<PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net9.0</TargetFramework>

    <!-- Version Information -->
    <Version>1.0.0</Version>
    <Company>YourCompany</Company>
    <Product>YourApp</Product>
    <Description>Your app description</Description>

    <!-- Native AOT Configuration (Optional) -->
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>false</InvariantGlobalization>
    <StripSymbols>true</StripSymbols>
</PropertyGroup>

1.2 Ensure Assets are Included

Make sure your assets are configured to be copied to output:

<ItemGroup>
    <AvaloniaResource Include="Assets/**" Exclude="Assets/yoursound.mp3"/>
    <Content Include="Assets/yoursound.mp3">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </Content>
</ItemGroup>

Step 2: Create AppDir Structure

2.1 Create the Base Directory

mkdir -p YourApp.AppDir/usr/bin
mkdir -p YourApp.AppDir/usr/lib
mkdir -p YourApp.AppDir/usr/share/applications
mkdir -p YourApp.AppDir/usr/share/icons/hicolor/256x256/apps

2.2 Create the Desktop Entry File

Create YourApp.AppDir/YourApp.desktop:

[Desktop Entry]
Name=YourApp
Exec=YourApp
Icon=yourapp
Type=Application
Categories=Utility;Office;
Comment=Your app description
Terminal=false
StartupWMClass=YourApp

Important Fields:

  • Name: Display name of your application
  • Exec: Must match your executable name
  • Icon: Icon name (without extension)
  • Categories: See freedesktop.org categories
  • StartupWMClass: Should match your app’s window class

2.3 Create the AppRun Script

Create YourApp.AppDir/AppRun:

#!/bin/bash

# AppRun script for YourApp
SELF=$(readlink -f "$0")
HERE=${SELF%/*}

# Export library paths
export LD_LIBRARY_PATH="${HERE}/usr/lib:${HERE}/usr/bin:${LD_LIBRARY_PATH}"
export PATH="${HERE}/usr/bin:${PATH}"

# Set data directory to user's home to avoid read-only filesystem issues
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"

# Change to user's home directory (not the AppImage mount point)
cd "$HOME"

# Run the application
exec "${HERE}/usr/bin/YourApp" "$@"

Make it executable:

chmod +x YourApp.AppDir/AppRun

Key Points:

  • LD_LIBRARY_PATH: Ensures shared libraries are found
  • XDG_DATA_HOME & XDG_CONFIG_HOME: Ensures app writes to user’s home, not the read-only AppImage mount
  • cd "$HOME": Prevents issues with read-only filesystem
  • exec: Replaces the shell process with your app

2.4 Add the Icon

Copy your icon:

cp YourApp/Assets/youricon.png YourApp.AppDir/yourapp.png
cp YourApp/Assets/youricon.png YourApp.AppDir/usr/share/icons/hicolor/256x256/apps/yourapp.png

Create a symlink for .DirIcon:

cd YourApp.AppDir
ln -s yourapp.png .DirIcon
cd ..

Step 3: Publish Your Application

3.1 Standard .NET Publish

dotnet publish YourApp/YourApp.csproj \
  -c Release \
  -r linux-x64 \
  --self-contained \
  -o ./publish/linux-x64

3.2 With Native AOT (Smaller, Faster)

dotnet publish YourApp/YourApp.csproj \
  -c Release \
  -r linux-x64 \
  --self-contained \
  -p:PublishAot=true \
  -p:PublishSingleFile=true \
  -p:PublishTrimmed=true \
  -o ./publish/linux-x64

Publish Options Explained:

  • -c Release: Release configuration (optimized)
  • -r linux-x64: Target Linux 64-bit
  • --self-contained: Include .NET runtime
  • -p:PublishAot=true: Enable Native AOT compilation
  • -p:PublishSingleFile=true: Create single executable
  • -p:PublishTrimmed=true: Remove unused code

Step 4: Copy Files to AppDir

4.1 Copy the Executable

cp ./publish/linux-x64/YourApp YourApp.AppDir/usr/bin/

4.2 Copy Shared Libraries

Avalonia apps typically need these libraries:

# Copy all .so files
for lib in ./publish/linux-x64/*.so; do
    if [ -f "$lib" ]; then
        cp "$lib" YourApp.AppDir/usr/lib/
    fi
done

Common libraries:

  • libSkiaSharp.so – Graphics rendering
  • libHarfBuzzSharp.so – Text shaping
  • libAvaloniaNative.so – Native platform support

4.3 Copy Additional Assets

# Copy any additional files (sounds, data files, etc.)
cp ./publish/linux-x64/yoursound.mp3 YourApp.AppDir/usr/bin/

4.4 Copy Desktop File

cp YourApp.AppDir/YourApp.desktop YourApp.AppDir/usr/share/applications/

Step 5: Build the AppImage

5.1 Download appimagetool (if not installed)

wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage

5.2 Build the AppImage

ARCH=x86_64 ./appimagetool-x86_64.AppImage YourApp.AppDir YourApp-1.0.0-x86_64.AppImage

Or if appimagetool is installed system-wide:

ARCH=x86_64 appimagetool YourApp.AppDir YourApp-1.0.0-x86_64.AppImage

Step 6: Automate with a Build Script

Create build-appimage.sh:

#!/bin/bash

set -e  # Exit on error

VERSION="1.0.0"
ARCH="x86_64"
APP_NAME="YourApp"
APPDIR="${APP_NAME}.AppDir"

echo "Building ${APP_NAME} AppImage v${VERSION}..."

# Check for appimagetool
if ! command -v appimagetool &> /dev/null; then
    echo " appimagetool not found. Downloading..."
    wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage -O appimagetool
    chmod +x appimagetool
    APPIMAGETOOL="./appimagetool"
else
    APPIMAGETOOL="appimagetool"
fi

# Clean previous builds
echo " Cleaning previous builds..."
rm -rf ./publish/linux-x64
rm -rf ./${APPDIR}/usr

# Publish the application
echo " Publishing application..."
dotnet publish ${APP_NAME}/${APP_NAME}.csproj \
  -c Release \
  -r linux-x64 \
  --self-contained \
  -o ./publish/linux-x64

# Create AppDir structure
echo " Creating AppImage directory structure..."
mkdir -p ${APPDIR}/usr/bin
mkdir -p ${APPDIR}/usr/lib
mkdir -p ${APPDIR}/usr/share/applications
mkdir -p ${APPDIR}/usr/share/icons/hicolor/256x256/apps

# Copy application files
echo " Copying application files..."
cp ./publish/linux-x64/${APP_NAME} ${APPDIR}/usr/bin/

# Copy shared libraries
for lib in ./publish/linux-x64/*.so; do
    if [ -f "$lib" ]; then
        cp "$lib" ${APPDIR}/usr/lib/
        echo " Copied $(basename $lib)"
    fi
done

# Copy assets (customize based on your needs)
if [ -f "./publish/linux-x64/yourasset.mp3" ]; then
    cp ./publish/linux-x64/yourasset.mp3 ${APPDIR}/usr/bin/
fi

# Copy icon
if [ -f "${APP_NAME}/Assets/youricon.png" ]; then
    cp ${APP_NAME}/Assets/youricon.png ${APPDIR}/yourapp.png
    cp ${APP_NAME}/Assets/youricon.png ${APPDIR}/usr/share/icons/hicolor/256x256/apps/yourapp.png
fi

# Copy desktop file
cp ${APPDIR}/${APP_NAME}.desktop ${APPDIR}/usr/share/applications/

# Make AppRun executable
chmod +x ${APPDIR}/AppRun

# Build AppImage
echo "Building AppImage..."
ARCH=${ARCH} ${APPIMAGETOOL} ${APPDIR} ${APP_NAME}-${VERSION}-${ARCH}.AppImage

echo " AppImage created: ${APP_NAME}-${VERSION}-${ARCH}.AppImage"

Make it executable and run:

chmod +x build-appimage.sh
./build-appimage.sh

Troubleshooting

Common Issues and Solutions

1. Application Won’t Start

Symptom: AppImage runs but app doesn’t appear

Solutions:

  • Check AppRun script has correct executable path
  • Verify LD_LIBRARY_PATH includes both usr/lib and usr/bin
  • Ensure all .so files are copied to usr/lib
# Debug by running AppRun directly
./YourApp.AppDir/AppRun

2. Missing Shared Libraries

Symptom: Error about missing .so files

Solutions:

  • Check publish output for all .so files
  • Copy them to AppDir/usr/lib
  • For Avalonia apps, ensure you have:
  • libSkiaSharp.so
  • libHarfBuzzSharp.so
  • libAvaloniaNative.so
# List all .so files in publish directory
find ./publish/linux-x64 -name "*.so"

3. Icon Not Showing

Symptom: AppImage shows default icon

Solutions:

  • Ensure icon is named correctly in .desktop file
  • Icon should be in both root and usr/share/icons/hicolor/256x256/apps/
  • Create .DirIcon symlink: ln -s yourapp.png .DirIcon

4. Data Not Persisting

Symptom: App settings/data lost after restart

Solutions:

  • Ensure AppRun sets XDG_DATA_HOME and XDG_CONFIG_HOME
  • App should write to ~/.local/share/YourApp not to AppImage mount
  • Use cd "$HOME" in AppRun before launching app

5. Assets Not Found

Symptom: Sounds, images, or other assets missing

Solutions:

  • Copy assets to usr/bin directory
  • Ensure .csproj has CopyToPublishDirectory set
  • Check asset paths in your code (use relative paths)
<Content Include="Assets/sound.mp3">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>

6. AppImage Too Large

Symptom: AppImage file is very large (>100MB)

Solutions:

  • Enable Native AOT: -p:PublishAot=true
  • Enable trimming: -p:PublishTrimmed=true
  • Use single file publish: -p:PublishSingleFile=true
  • Remove debug symbols: -p:StripSymbols=true

Typical sizes:

  • Without AOT: 80-120 MB
  • With AOT: 30-50 MB

Distribution

Testing Your AppImage

# Make executable
chmod +x YourApp-1.0.0-x86_64.AppImage

# Run it
./YourApp-1.0.0-x86_64.AppImage

# Test on different distributions (recommended)
# - Ubuntu/Debian
# - Fedora/RHEL
# - Arch Linux
# - openSUSE

Publishing to GitHub Releases

  1. Create a Release on GitHub:
   git tag v1.0.0
   git push origin v1.0.0
  1. Upload AppImage:
  • Go to GitHub → Releases → Create new release
  • Upload YourApp-1.0.0-x86_64.AppImage
  • Add release notes
  1. Installation Instructions for Users:
   # Download
   wget https://github.com/yourusername/yourapp/releases/download/v1.0.0/YourApp-1.0.0-x86_64.AppImage

   # Make executable
   chmod +x YourApp-1.0.0-x86_64.AppImage

   # Run
   ./YourApp-1.0.0-x86_64.AppImage

Optional: AppImageHub

Submit your AppImage to AppImageHub for wider distribution.


Advanced Topics

Code Signing

Sign your AppImage for added security:

# Generate key
gpg --gen-key

# Sign AppImage
gpg --armor --detach-sign YourApp-1.0.0-x86_64.AppImage

# Verify
gpg --verify YourApp-1.0.0-x86_64.AppImage.asc YourApp-1.0.0-x86_64.AppImage

Desktop Integration

AppImages can integrate with the desktop environment:

# Install AppImageLauncher (recommended for users)
# It automatically integrates AppImages into the system

Update Mechanism

Implement AppImage updates using AppImageUpdate:

Add to your .desktop file:

X-AppImage-Update-Information=gh-releases-zsync|username|repository|latest|YourApp-*-x86_64.AppImage.zsync

Complete Example: ProductivityCake

Here’s the complete structure used for ProductivityCake:

ProductivityCake/
├── ProductivityCake/              # Source code
│   ├── ProductivityCake.csproj
│   ├── Assets/
│   │   ├── icons8-cake-96.png
│   │   └── alarm.mp3
│   └── ...
├── ProductivityCake.AppDir/       # AppImage structure
│   ├── AppRun                     # Startup script
│   ├── ProductivityCake.desktop   # Desktop entry
│   ├── productivitycake.png       # Icon
│   ├── .DirIcon -> productivitycake.png
│   └── usr/
│       ├── bin/
│       │   ├── ProductivityCake   # Executable
│       │   └── alarm.mp3          # Asset
│       ├── lib/
│       │   ├── libSkiaSharp.so
│       │   └── libHarfBuzzSharp.so
│       └── share/
│           ├── applications/
│           │   └── ProductivityCake.desktop
│           └── icons/
│               └── hicolor/
│                   └── 256x256/
│                       └── apps/
│                           └── productivitycake.png
└── build-appimage.sh              # Build script

Checklist

Before building your AppImage, ensure:

  • [ ] .csproj is configured for publishing
  • [ ] AppDir structure is created
  • [ ] AppRun script is created and executable
  • [ ] .desktop file is created with correct information
  • [ ] Icon is in place (both root and usr/share/icons)
  • [ ] .DirIcon symlink is created
  • [ ] Application publishes successfully
  • [ ] All .so files are copied to usr/lib
  • [ ] All assets are copied to appropriate locations
  • [ ] Desktop file is copied to usr/share/applications
  • [ ] appimagetool is available
  • [ ] AppImage builds without errors
  • [ ] AppImage runs on your system
  • [ ] AppImage tested on different distributions (if possible)

Resources


Conclusion

Creating an AppImage for your Avalonia application provides:

  • Universal compatibility across Linux distributions
  • No installation required – just download and run
  • Self-contained – includes all dependencies
  • Easy distribution – single file to share
  • User-friendly – works like native apps

This tutorial can be adapted for any .NET application, not just Avalonia. The key is understanding the AppDir structure and ensuring all dependencies are included.

Happy packaging!

Mohammed Chami
Mohammed Chami
Articles: 44

Leave a Reply

Your email address will not be published. Required fields are marked *