Building Your First .NET MAUI AppImage: A Complete Walkthrough

Building Your First .NET MAUI AppImage: A Complete Walkthrough

Learn how to package .NET MAUI Linux applications as portable AppImages using the OpenMaui.AppImage CLI tool, complete with custom icons, metadata, and one-click installation dialogs.

logikonline David H. Friedel Jr. · 2026-02-03 20:06:52 +00:00

Introduction to AppImage for .NET

If you've built a .NET MAUI application for Linux, you've likely faced the distribution challenge: how do you package your app so users can easily download and run it without wrestling with dependencies, package managers, or complex installation procedures?

Enter AppImage—a universal packaging format for Linux that bundles your application and its dependencies into a single executable file. Think of it as the Linux equivalent of a portable Windows .exe or macOS .app bundle. Users download the AppImage, mark it executable, and run it. No installation required.

Why AppImage for .NET MAUI?

The .NET MAUI framework brings cross-platform development to Linux, but distributing Linux apps has traditionally been fragmented across different package formats (.deb, .rpm, Flatpak, Snap). AppImage solves this by:

  • Working across distributions: One AppImage runs on Ubuntu, Fedora, Arch, and more
  • No root required: Users can run apps without system-level installation
  • Self-contained: All dependencies travel with your app
  • Portable: Download, run, and optionally install with desktop integration

The OpenMaui.AppImage tool streamlines the entire packaging process, automatically detecting your executable, extracting icons from your .csproj file, and creating a polished AppImage with an integrated installation dialog.

AppImages bring the 'download and run' simplicity of Windows executables to the Linux ecosystem, making your .NET MAUI apps truly portable across distributions.

What You'll Build

By the end of this walkthrough, you'll have:

  • A portable .AppImage file containing your MAUI app
  • Automatic executable and icon detection from your project
  • A zenity-based installation dialog that lets users install or run your app
  • Desktop integration with application menu entries and custom icons
  • Support for both self-contained and framework-dependent deployments

Installing the OpenMaui.AppImage Tool

The OpenMaui.AppImage tool is distributed as a .NET global tool, making installation straightforward on any system with the .NET SDK installed.

Prerequisites

Before installing the tool, ensure you have:

  1. .NET 9.0 SDK or later installed (download here)
  2. appimagetool installed on your Linux system
  3. zenity (optional, for the graphical installer dialog)

Installing appimagetool

The appimagetool utility is required to create AppImages. Install it using one of these methods:

# Download the official AppImage
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool

# Or install via package manager
# Ubuntu/Debian
sudo apt install appimagetool

# Fedora
sudo dnf install appimagetool

# Arch Linux
yay -S appimagetool-bin

Installing OpenMaui.AppImage

Once you have the prerequisites, install the tool as a .NET global tool:

dotnet tool install -g OpenMaui.AppImage

This makes the openmaui-appimage command available system-wide. Verify the installation:

openmaui-appimage --help

You should see the command-line options and usage information.

Building from Source (Optional)

If you prefer to build from source or contribute to the project:

git clone https://github.com/AuroraNetworks/openmaui-appimage.git
cd openmaui-appimage
dotnet build
dotnet pack
dotnet tool install -g --add-source ./src/OpenMaui.AppImage/bin/Release OpenMaui.AppImage

The tool is now ready to package your MAUI applications!

Publishing Your MAUI App for Linux

Before creating an AppImage, you need to publish your .NET MAUI application for Linux. The OpenMaui.AppImage tool works with both self-contained and framework-dependent deployments.

Understanding Deployment Models

Self-contained deployment bundles the .NET runtime with your app, creating a larger but completely standalone package:

dotnet publish -c Release -r linux-x64 --self-contained true

Framework-dependent deployment assumes the user has .NET installed, resulting in smaller packages:

dotnet publish -c Release -r linux-x64 --self-contained false

For AppImages, self-contained is often preferred since it guarantees your app runs regardless of the system's .NET installation.

The OpenMaui.AppImage tool automatically detects your executable, extracts icons from your project file, and creates a polished installation experience—all from a single command.

Publishing Your MAUI Project

Navigate to your MAUI project directory and publish for Linux:

cd YourMauiApp
dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=false

Important: Set PublishSingleFile=false for MAUI apps. While single-file publishing works for console apps, MAUI applications need their resources and native libraries accessible.

The published output will be in:

bin/Release/net9.0/linux-x64/publish/

Optimizing Your Icon for AppImage

The OpenMaui.AppImage tool automatically detects and extracts icons from your project's MauiIcon configuration. Here's how the tool parses your .csproj:

private string? TryGetIconFromCsproj(string inputDir, string execName)
{
    var doc = XDocument.Load(csprojPath);
    var mauiIcon = doc.Descendants("MauiIcon").FirstOrDefault();
    
    var includeAttr = mauiIcon.Attribute("Include")?.Value;
    var foregroundAttr = mauiIcon.Attribute("ForegroundFile")?.Value;
    var bgColorAttr = mauiIcon.Attribute("BackgroundColor")?.Value;
    
    // Composite background + foreground into final icon
    if (fgPath != null && File.Exists(fgPath))
    {
        return CompositeIcon(bgPath, fgPath, bgColorAttr, colorAttr);
    }
    
    return bgPath;
}

Your .csproj might look like this:

<ItemGroup>
    <MauiIcon Include="Resources\AppIcon\appicon.svg" 
              ForegroundFile="Resources\AppIcon\appiconfg.svg"
              Color="#FFFFFF"
              BackgroundColor="#512BD4" />
</ItemGroup>

The tool will automatically composite the foreground and background into a rounded-corner icon suitable for Linux desktop environments.

Verifying the Published Output

Before packaging, verify your publish directory contains:

  • Your main executable or .dll file
  • All required native libraries (.so files)
  • Resource files and assets
  • Icon files (if not auto-detected from .csproj)

List the contents:

ls -lh bin/Release/net9.0/linux-x64/publish/

You're now ready to create your AppImage!

Creating Your First AppImage

With your MAUI app published, creating an AppImage is a single command. The OpenMaui.AppImage tool handles all the complexity—detecting executables, extracting icons, creating the AppDir structure, and bundling everything into a portable AppImage.

Basic AppImage Creation

The minimal command requires three parameters:

openmaui-appimage \
  --input bin/Release/net9.0/linux-x64/publish \
  --output MyMauiApp.AppImage \
  --name "My MAUI App"

The tool will:

  1. Auto-detect your executable by scanning for ELF binaries or .dll files with matching .runtimeconfig.json
  2. Extract icons from your .csproj MauiIcon configuration
  3. Create an AppDir with the proper structure
  4. Generate an AppRun script that handles both self-contained and framework-dependent apps
  5. Create a .desktop file for desktop integration
  6. Bundle everything using appimagetool

Here's what the auto-detection looks like under the hood:

private string? AutoDetectExecutable(string inputDir, string appName)
{
    // Strategy 1: Look for native ELF executable (self-contained)
    var files = Directory.GetFiles(inputDir);
    foreach (var file in files)
    {
        var firstBytes = new byte[4];
        using (var fs = File.OpenRead(file))
            fs.Read(firstBytes, 0, 4);
        
        // ELF magic number: 0x7F 'E' 'L' 'F'
        if (firstBytes[0] == 0x7F && firstBytes[1] == 'E' && 
            firstBytes[2] == 'L' && firstBytes[3] == 'F')
        {
            return fileName;
        }
    }
    
    // Strategy 2: Look for .dll with .runtimeconfig.json
    var runtimeConfigs = Directory.GetFiles(inputDir, "*.runtimeconfig.json");
    foreach (var config in runtimeConfigs)
    {
        var baseName = Path.GetFileNameWithoutExtension(config)
            .Replace(".runtimeconfig", "");
        var dllPath = Path.Combine(inputDir, baseName + ".dll");
        if (File.Exists(dllPath) && 
            !baseName.StartsWith("Microsoft.") && 
            !baseName.StartsWith("System."))
        {
            return baseName;
        }
    }
}

Advanced Options

For more control over the AppImage creation, use additional options:

openmaui-appimage \
  --input bin/Release/net9.0/linux-x64/publish \
  --output MyMauiApp-1.2.3-x86_64.AppImage \
  --name "My MAUI App" \
  --executable MyMauiApp \
  --icon Resources/AppIcon/appicon.png \
  --category "Development" \
  --app-version "1.2.3" \
  --comment "A powerful MAUI application for Linux"

Option Reference:

  • --input, -i: Path to published app directory (required)
  • --output, -o: Output AppImage file path (required)
  • --name, -n: Application display name (required)
  • --executable, -e: Executable name (auto-detected if omitted)
  • --icon: Custom icon path (auto-detected from .csproj if omitted)
  • --category, -c: Desktop category (default: "Utility")
  • --app-version: Version string (default: "1.0.0")
  • --comment: Application description

What Gets Created

The tool creates a complete AppDir structure:

MyMauiApp.AppDir/
├── AppRun                          # Launch script
├── MyMauiApp.desktop               # Desktop entry
├── MyMauiApp.svg                   # Application icon
└── usr/
    ├── bin/
    │   ├── MyMauiApp               # Your app files
    │   ├── MyMauiApp.dll
    │   ├── libSkiaSharp.so
    │   └── ...                     # All dependencies
    └── share/
        ├── applications/
        │   └── MyMauiApp.desktop
        └── icons/
            └── hicolor/
                ├── scalable/apps/
                │   └── MyMauiApp.svg
                └── 256x256/apps/
                    └── MyMauiApp.png

The AppRun script is the heart of the AppImage, handling installation, runtime detection, and app launching:

#!/bin/bash
SELF=$(readlink -f "$0")
HERE=${SELF%/*}

export APPIMAGE_NAME="My MAUI App"
export APPIMAGE_VERSION="1.2.3"
export PATH="$HERE/usr/bin:$PATH"
export LD_LIBRARY_PATH="$HERE/usr/bin:$LD_LIBRARY_PATH"

# Check if self-contained or framework-dependent
if [ -x "$HERE/usr/bin/$EXEC_NAME" ]; then
    exec "$HERE/usr/bin/$EXEC_NAME" "$@"
else
    exec dotnet "$EXEC_NAME.dll" "$@"
fi

Build Output

When the build completes successfully, you'll see:

Building AppImage for My MAUI App...
  Input: bin/Release/net9.0/linux-x64/publish
  Output: MyMauiApp.AppImage
  Auto-detected executable: MyMauiApp
  Auto-detected icon: Resources/AppIcon/appicon.svg
  Creating AppDir at: /tmp/appimage-abc123/MyMauiApp.AppDir
  Copying application files...
  Creating AppRun script...
  Creating .desktop file...
  Setting up icon...
  Creating AppImage...

AppImage created successfully: MyMauiApp.AppImage

To run:
  chmod +x MyMauiApp.AppImage
  ./MyMauiApp.AppImage

Your AppImage is now ready to distribute!

Testing and Running the AppImage

Once your AppImage is built, testing it is straightforward. The AppImage format includes a built-in installation dialog powered by zenity, giving users a polished first-run experience.

Making the AppImage Executable

First, ensure the AppImage has execute permissions:

chmod +x MyMauiApp.AppImage

First Run: The Installation Dialog

When a user runs your AppImage for the first time, they're greeted with a zenity dialog offering installation options:

./MyMauiApp.AppImage

The dialog displays:

  • Application name and version
  • Custom icon (auto-detected from your project)
  • Description (from the --comment option)
  • Two buttons: "Install" or "Run Only"

This is powered by the AppRun script's zenity integration:

if command -v zenity &> /dev/null; then
    zenity --question --title="$APPIMAGE_NAME" \
        --text="<b>$APPIMAGE_NAME</b>\nVersion $APPIMAGE_VERSION\n\n$APPIMAGE_COMMENT\n\nWould you like to install this application?" \
        --ok-label="Install" --cancel-label="Run Only" \
        --width=350 $ICON_OPT 2>/dev/null
    ZENITY_EXIT=$?
    
    if [ $ZENITY_EXIT -eq 0 ]; then
        do_install
        zenity --info --title="Installation Complete" \
            --text="$APPIMAGE_NAME has been installed.\n\nYou can find it in your application menu." \
            --width=300 $ICON_OPT 2>/dev/null
    fi
fi

What Happens When Users Click "Install"

The installation process:

  1. Copies the AppImage to ~/.local/bin/
  2. Creates a .desktop file in ~/.local/share/applications/
  3. Installs the icon to ~/.local/share/icons/hicolor/
  4. Updates the desktop database for menu integration
  5. Creates a marker file in ~/.local/share/openmaui-installed/

Here's the installation function from the AppRun script:

do_install() {
    APPIMAGE_BASENAME=$(basename "$APPIMAGE")
    SANITIZED=$(echo "$APPIMAGE_NAME" | tr ' ' '_')
    
    mkdir -p "$BIN_DIR" "$APPS_DIR" "$ICONS_DIR_SCALABLE" "$ICONS_DIR_256" "$INSTALLED_MARKER"
    
    # Copy AppImage
    cp "$APPIMAGE" "$BIN_DIR/$APPIMAGE_BASENAME"
    chmod +x "$BIN_DIR/$APPIMAGE_BASENAME"
    
    # Copy icon
    if [ -f "$HERE/${SANITIZED}.svg" ]; then
        cp "$HERE/${SANITIZED}.svg" "$ICONS_DIR_SCALABLE/${SANITIZED}.svg"
    fi
    
    # Create .desktop file with proper WM_CLASS
    WM_CLASS=$(echo "$APPIMAGE_NAME" | tr -d ' _')
    cat > "$APPS_DIR/${SANITIZED}.desktop" << DESKTOP
[Desktop Entry]
Type=Application
Name=$APPIMAGE_NAME
Comment=$APPIMAGE_COMMENT
Exec=$BIN_DIR/$APPIMAGE_BASENAME
Icon=$SANITIZED
Categories=$APPIMAGE_CATEGORY;
Terminal=false
StartupWMClass=$WM_CLASS
X-AppImage-Version=$APPIMAGE_VERSION
DESKTOP
    
    # Update desktop database
    command -v update-desktop-database &> /dev/null && update-desktop-database "$APPS_DIR" 2>/dev/null
}

After installation, the app appears in the system application menu and can be launched like any native application.

Running Without Installation

If users click "Run Only", the app launches immediately without system integration. This is perfect for:

  • Testing before committing to installation
  • Portable use from USB drives or network shares
  • Temporary access without cluttering the system

Command-Line Options

Your AppImage supports several command-line flags:

# Force show installation dialog
./MyMauiApp.AppImage --install

# Uninstall the application
./MyMauiApp.AppImage --uninstall

# Show help
./MyMauiApp.AppImage --help

The --uninstall option removes:

  • The AppImage from ~/.local/bin/
  • The .desktop file
  • All installed icons
  • The installation marker

And updates the desktop database to remove menu entries.

Testing Desktop Integration

After installation, verify desktop integration:

# Check the .desktop file
cat ~/.local/share/applications/My_MAUI_App.desktop

# Verify icon installation
ls ~/.local/share/icons/hicolor/scalable/apps/My_MAUI_App.*

# Launch from the command line
gtk-launch My_MAUI_App.desktop

You should also see your app in:

  • GNOME Activities / Application Grid
  • KDE Application Launcher
  • Xfce Application Finder
  • Any other desktop environment's application menu

Testing on Different Distributions

The beauty of AppImage is cross-distribution compatibility. Test your AppImage on:

  • Ubuntu/Debian-based: Pop!_OS, Linux Mint, elementary OS
  • Fedora/RHEL-based: Fedora Workstation, CentOS Stream
  • Arch-based: Manjaro, EndeavourOS
  • Others: openSUSE, Solus, Void Linux

The same AppImage should work across all of them without modification!

Troubleshooting Common Issues

Even with automated tooling, you may encounter issues when creating or running AppImages. Here are solutions to the most common problems.

Issue: "Could not find executable"

Error message:

Error: Could not find executable 'MyApp' in bin/Release/net9.0/linux-x64/publish
Available DLLs: MyApp.dll, Microsoft.Maui.dll, ...
Use --executable to specify the correct name.

Cause: The tool couldn't auto-detect your main executable.

Solution 1: Explicitly specify the executable name:

openmaui-appimage \
  --input bin/Release/net9.0/linux-x64/publish \
  --output MyApp.AppImage \
  --name "My App" \
  --executable MyApp

Solution 2: If your app name contains spaces, try the version without spaces:

--executable MyMauiApp  # Instead of "My Maui App"

Issue: "appimagetool not found"

Error message:

Error: appimagetool not found.

Please install appimagetool:
  wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
  chmod +x appimagetool-x86_64.AppImage
  sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool

Cause: The appimagetool utility isn't installed or isn't in your PATH.

Solution: Follow the installation instructions in the error message, or install via your package manager:

# Ubuntu/Debian
sudo apt install appimagetool

# Fedora
sudo dnf install appimagetool

# Arch
yay -S appimagetool-bin

Issue: Icon Not Displaying

Symptom: The AppImage works but shows a generic icon in the application menu.

Cause: Icon wasn't found or properly embedded.

Solution 1: Explicitly provide an icon:

openmaui-appimage \
  --icon Resources/AppIcon/appicon.png \
  # ... other options

Solution 2: Ensure your .csproj has a MauiIcon entry:

<ItemGroup>
    <MauiIcon Include="Resources\AppIcon\appicon.svg" />
</ItemGroup>

Solution 3: Use SVG format for best results:

# Convert PNG to SVG if needed
convert appicon.png -background none -gravity center -extent 256x256 appicon.svg

Issue: "dotnet: command not found" When Running AppImage

Symptom: Framework-dependent AppImage fails with "dotnet: command not found".

Cause: The user doesn't have .NET runtime installed, and your AppImage isn't self-contained.

Solution 1: Publish as self-contained:

dotnet publish -c Release -r linux-x64 --self-contained true

Solution 2: Document the .NET requirement in your app description:

openmaui-appimage \
  --comment "Requires .NET 9.0 runtime. Install from https://dotnet.microsoft.com" \
  # ... other options

Solution 3: The AppRun script attempts to find dotnet in common locations:

DOTNET_CMD="dotnet"
if ! command -v dotnet &> /dev/null; then
    for d in /usr/share/dotnet /usr/lib/dotnet /opt/dotnet "$HOME/.dotnet"; do
        [ -x "$d/dotnet" ] && DOTNET_CMD="$d/dotnet" && break
    done
fi

But self-contained is the most reliable approach.

Issue: AppImage Crashes on Wayland

Symptom: App works on X11 but crashes on Wayland.

Cause: Some MAUI/GTK components may have Wayland compatibility issues.

Solution: Force X11 mode via environment variable:

GDK_BACKEND=x11 ./MyMauiApp.AppImage

Or modify the AppRun script to default to X11:

# Add before exec command
export GDK_BACKEND=x11

Issue: Permission Denied

Symptom: ./MyApp.AppImage: Permission denied

Cause: AppImage doesn't have execute permissions.

Solution:

chmod +x MyApp.AppImage
./MyApp.AppImage

Issue: "FUSE not available" Error

Error message:

Cannot mount AppImage, please check your FUSE setup.

Cause: Modern AppImages use FUSE to mount their filesystem, but FUSE isn't configured.

Solution 1: Install FUSE:

# Ubuntu/Debian
sudo apt install fuse libfuse2

# Fedora
sudo dnf install fuse fuse-libs

# Arch
sudo pacman -S fuse2

Solution 2: Extract and run manually:

./MyApp.AppImage --appimage-extract
./squashfs-root/AppRun

Issue: Missing Native Libraries

Symptom: App crashes with "cannot open shared object file" errors.

Cause: Your app depends on system libraries not bundled in the AppImage.

Solution 1: Ensure publish includes all native dependencies:

dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false

Solution 2: Check which libraries are missing:

ldd bin/Release/net9.0/linux-x64/publish/MyApp

Copy any missing .so files to your publish directory before creating the AppImage.

Debugging AppRun Scripts

To debug the AppRun script, extract the AppImage and run with bash tracing:

./MyApp.AppImage --appimage-extract
bash -x squashfs-root/AppRun

This shows each command as it executes, helping identify where failures occur.

46

Comments

No comments yet. Be the first to share your thoughts!

Leave a Comment

Enter your name and email to verify before commenting.

Enter the 6-digit code sent to your email.