Complete Avalonia UI Linux Publishing Guide with Native AOT

Publishing and deploying Avalonia UI applications on Linux requires careful consideration of deployment methods, optimization strategies, and system integration. This comprehensive guide covers everything from Native AOT compilation to desktop integration.

Table of Contents

  • Publishing Options Overview
  • Native AOT Deployment (Recommended)
  • Traditional Publishing Options
  • Handling Trimming Issues
  • System Installation
  • Desktop Integration
  • Complete Installation Script
  • Troubleshooting

Publishing Options Overview

Recommended Order of Preference:

you can also check the documentation

  1. Native AOT – Best performance and size
  2. Self-Contained with Trimming – Good balance
  3. Framework-Dependent – Smallest but requires .NET

Native AOT Deployment (Recommended)

Native AOT provides the best combination of performance, startup time, and distribution size for Avalonia applications.

Benefits

  • Faster startup – No JIT compilation needed
  • Reduced memory footprint – More efficient than standard .NET
  • Self-contained – No .NET runtime installation required
  • Better security – Reduced attack surface
  • Optimal size – When combined with proper trimming

Project Configuration

Add these properties to your .csproj file:

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <!-- Native AOT specific settings -->
  <BuiltInComInteropSupport>false</BuiltInComInteropSupport>
  <TrimMode>link</TrimMode>
  <!-- Optional optimizations -->
  <InvariantGlobalization>true</InvariantGlobalization>
  <DebuggerSupport>false</DebuggerSupport>
  <EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
  <EventSourceSupport>false</EventSourceSupport>
</PropertyGroup>

<ItemGroup>
  <!-- Preserve essential Avalonia assemblies -->
  <TrimmerRootAssembly Include="Avalonia.Themes.Fluent" />
  <TrimmerRootAssembly Include="Avalonia.Themes.Default" />
  <TrimmerRootAssembly Include="Avalonia.Controls" />
</ItemGroup>

XAML Preparation for AOT

Ensure your XAML is AOT-ready:

<!-- Enable compiled bindings in your UserControls and Windows -->
<UserControl x:Class="YourApp.Views.MainView"
             x:CompileBindings="True"
             xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
    <!-- Use static resources instead of dynamic where possible -->
    <UserControl.Resources>
        <SolidColorBrush x:Key="MyBrush" Color="Blue" />
    </UserControl.Resources>
    
</UserControl>

Publishing Command

dotnet publish -r linux-x64 -c Release

Expected Results

  • Size: ~15-30MB (much smaller than standard self-contained)
  • Startup: Significantly faster
  • Memory: Lower baseline usage
  • Dependencies: Single executable with minimal native libraries

Traditional Publishing Options

Self-Contained Deployment (Legacy Approach)

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

Pros:

  • Works on any Linux system
  • No .NET installation required

Cons:

  • Larger size (~95MB+ vs ~20MB with AOT)
  • Higher memory usage (~145MB+ RAM)
  • Slower startup

Framework-Dependent Deployment

dotnet publish -r linux-x64 -c Release

Pros:

  • Very small size (~5-15MB)
  • Lower memory usage
  • Fastest for development/testing

Cons:

  • Users must have .NET runtime installed
  • Version compatibility issues

Handling Trimming Issues

JSON Serialization with AOT

Problem: JSON serialization often breaks with trimming and AOT.

Solution 1: Use Source Generators (Recommended for AOT)

using System.Text.Json.Serialization;

[JsonSerializable(typeof(List<TodoTask>))]
[JsonSerializable(typeof(TodoTask))]
[JsonSerializable(typeof(AppSettings))]
public partial class AppJsonContext : JsonSerializerContext
{
}

// Usage in your code:
public class DataService
{
    public void SaveTasks(List<TodoTask> tasks)
    {
        var json = JsonSerializer.Serialize(tasks, AppJsonContext.Default.ListTodoTask);
        File.WriteAllText(GetDataPath(), json);
    }

    public List<TodoTask> LoadTasks()
    {
        var json = File.ReadAllText(GetDataPath());
        return JsonSerializer.Deserialize(json, AppJsonContext.Default.ListTodoTask) ?? new();
    }
}

Solution 2: Preserve Types in Trimmer

Create TrimmerRoots.xml:

<linker>
  <assembly fullname="YourAppName">
    <type fullname="YourAppName.Models.TodoTask" preserve="all" />
    <type fullname="YourAppName.Models.AppSettings" preserve="all" />
    <type fullname="YourAppName.ViewModels*" preserve="all" />
  </assembly>
</linker>

Add to .csproj:

<ItemGroup>
  <TrimmerRootDescriptor Include="TrimmerRoots.xml" />
</ItemGroup>

ViewModels and Reflection Issues

For MVVM applications using dependency injection:

<ItemGroup>
  <!-- Preserve your ViewModels -->
  <TrimmerRootAssembly Include="YourApp.ViewModels" />
</ItemGroup>

Or in TrimmerRoots.xml:

<linker>
  <assembly fullname="YourApp">
    <namespace fullname="YourApp.ViewModels" preserve="all" />
    <namespace fullname="YourApp.Services" preserve="all" />
  </assembly>
</linker>

File Path Handling

Problem: Assembly.GetExecutingAssembly().Location returns empty string with AOT.

Solution: Use proper cross-platform paths:

public static class AppPaths
{
    private static readonly string AppName = "YourAppName";
    
    public static string GetAppDataDirectory()
    {
        var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        var appDataDir = Path.Combine(homeDir, $".{AppName}");
        Directory.CreateDirectory(appDataDir);
        return appDataDir;
    }
    
    public static string GetConfigFilePath()
    {
        return Path.Combine(GetAppDataDirectory(), "config.json");
    }
    
    public static string GetDataFilePath()
    {
        return Path.Combine(GetAppDataDirectory(), "data.json");
    }
}

System Installation

Understanding AOT Output

After AOT publishing, you’ll typically have:

  • Your main executable (self-contained, no .exe extension)
  • Minimal native libraries (if any)
  • No .NET runtime files (they’re compiled in)

The output is much cleaner than traditional .NET deployments.

Installation Methods

Method 1: System-Wide Installation

# Navigate to publish directory
cd ./bin/Release/net8.0/linux-x64/publish/

# Create installation directory
sudo mkdir -p /opt/YourAppName

# Copy all files (usually just the executable)
sudo cp -r ./* /opt/YourAppName/

# Make executable
sudo chmod +x /opt/YourAppName/YourAppName

# Create symlink for command line access
sudo ln -sf /opt/YourAppName/YourAppName /usr/local/bin/YourAppName

Method 2: User-Only Installation

# Create local installation directory
mkdir -p ~/.local/opt/YourAppName
mkdir -p ~/.local/bin

# Copy files
cp -r ./bin/Release/net8.0/linux-x64/publish/* ~/.local/opt/YourAppName/

# Make executable
chmod +x ~/.local/opt/YourAppName/YourAppName

# Create symlink
ln -sf ~/.local/opt/YourAppName/YourAppName ~/.local/bin/YourAppName

# Ensure ~/.local/bin is in PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Desktop Integration

System-Wide Desktop Entry

sudo tee /usr/share/applications/YourAppName.desktop > /dev/null << EOF
[Desktop Entry]
Name=Your App Display Name
Comment=Brief description of your app
Exec=/opt/YourAppName/YourAppName
Icon=utilities-text-editor
Type=Application
Categories=Utility;Office;
Keywords=productivity;task;management;
StartupNotify=true
Terminal=false
EOF

# Update desktop database
sudo update-desktop-database /usr/share/applications/

User-Only Desktop Entry

mkdir -p ~/.local/share/applications/

cat > ~/.local/share/applications/YourAppName.desktop << EOF
[Desktop Entry]
Name=Your App Display Name
Comment=Brief description of your app
Exec=$HOME/.local/opt/YourAppName/YourAppName
Icon=utilities-text-editor
Type=Application
Categories=Utility;Office;
Keywords=productivity;task;management;
StartupNotify=true
Terminal=false
EOF

update-desktop-database ~/.local/share/applications/

Custom Application Icon

To use a custom icon:

  1. Prepare your icon: Create a 256×256 PNG file
  2. Install the icon: # System-widesudo cp your-icon.png /usr/share/pixmaps/yourapp.png# User-onlymkdir -p ~/.local/share/icons/hicolor/256x256/apps/cp your-icon.png ~/.local/share/icons/hicolor/256x256/apps/yourapp.png
  3. Update desktop entry: Icon=yourapp# or full path: Icon=/usr/share/pixmaps/yourapp.png

Complete Installation Script

Here’s a comprehensive script that handles Native AOT building and installation:

#!/bin/bash
set -e

# Configuration
APP_NAME="YourAppName"
DISPLAY_NAME="Your App Display Name"
DESCRIPTION="Brief description of your app"
VERSION="1.0.0"
INSTALL_TYPE="${1:-system}"  # system or user

# Paths
if [ "$INSTALL_TYPE" = "user" ]; then
    INSTALL_DIR="$HOME/.local/opt/$APP_NAME"
    BIN_LINK="$HOME/.local/bin/$APP_NAME"
    DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
    ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
    UPDATE_CMD="update-desktop-database ~/.local/share/applications/"
else
    INSTALL_DIR="/opt/$APP_NAME"
    BIN_LINK="/usr/local/bin/$APP_NAME"
    DESKTOP_FILE="/usr/share/applications/$APP_NAME.desktop"
    ICON_DIR="/usr/share/pixmaps"
    UPDATE_CMD="sudo update-desktop-database /usr/share/applications/"
    SUDO="sudo"
fi

echo "Building $DISPLAY_NAME with Native AOT..."
dotnet publish -r linux-x64 -c Release

# Verify build
PUBLISH_DIR="./bin/Release/net8.0/linux-x64/publish"
if [ ! -f "$PUBLISH_DIR/$APP_NAME" ]; then
    echo "Error: Build failed or executable not found"
    exit 1
fi

echo "Installing $DISPLAY_NAME ($INSTALL_TYPE installation)..."

# Create directories
if [ "$INSTALL_TYPE" = "user" ]; then
    mkdir -p "$INSTALL_DIR" "$HOME/.local/bin" "$HOME/.local/share/applications" "$ICON_DIR"
else
    $SUDO mkdir -p "$INSTALL_DIR"
fi

# Copy files
if [ "$INSTALL_TYPE" = "user" ]; then
    cp -r $PUBLISH_DIR/* "$INSTALL_DIR/"
    chmod +x "$INSTALL_DIR/$APP_NAME"
else
    $SUDO cp -r $PUBLISH_DIR/* "$INSTALL_DIR/"
    $SUDO chmod +x "$INSTALL_DIR/$APP_NAME"
fi

# Create symlink
if [ "$INSTALL_TYPE" = "user" ]; then
    ln -sf "$INSTALL_DIR/$APP_NAME" "$BIN_LINK"
else
    $SUDO ln -sf "$INSTALL_DIR/$APP_NAME" "$BIN_LINK"
fi

# Install icon if exists
if [ -f "icon.png" ]; then
    if [ "$INSTALL_TYPE" = "user" ]; then
        cp icon.png "$ICON_DIR/$APP_NAME.png"
        ICON_NAME="$APP_NAME"
    else
        $SUDO cp icon.png "$ICON_DIR/$APP_NAME.png"
        ICON_NAME="$APP_NAME"
    fi
else
    ICON_NAME="utilities-text-editor"
fi

# Create desktop entry
DESKTOP_CONTENT="[Desktop Entry]
Name=$DISPLAY_NAME
Comment=$DESCRIPTION
Exec=$INSTALL_DIR/$APP_NAME
Icon=$ICON_NAME
Type=Application
Categories=Utility;Office;
Keywords=productivity;task;management;
StartupNotify=true
Terminal=false
Version=$VERSION"

if [ "$INSTALL_TYPE" = "user" ]; then
    echo "$DESKTOP_CONTENT" > "$DESKTOP_FILE"
else
    echo "$DESKTOP_CONTENT" | $SUDO tee "$DESKTOP_FILE" > /dev/null
fi

# Update desktop database
eval $UPDATE_CMD 2>/dev/null || true

# Add to PATH if needed (user installation)
if [ "$INSTALL_TYPE" = "user" ] && ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
    echo ""
    echo "Adding ~/.local/bin to PATH..."
    echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
    echo "   Please run: source ~/.bashrc"
fi

echo ""
echo "$DISPLAY_NAME installed successfully!"
echo ""
echo "Installation details:"
echo "  Installed to: $INSTALL_DIR"
echo "  Command: $APP_NAME"
echo "  Desktop entry: Available in applications menu"
echo ""
echo "You can now:"
echo "  • Run from terminal: $APP_NAME"
echo "  • Find it in your applications menu"
echo "  • Launch with: $INSTALL_DIR/$APP_NAME"

Using the Installation Script

  1. Save as install.sh in your project root
  2. Update configuration variables at the top
  3. Make executable and run: chmod +x install.sh# System-wide installation (requires sudo)./install.sh system# User-only installation./install.sh user

Troubleshooting

Native AOT Specific Issues

Trimming Breaks Functionality

  • Test without AOT first: Remove <PublishAot>true</PublishAot>
  • Add specific assemblies to <TrimmerRootAssembly>
  • Use source generators for JSON serialization
  • Check TrimmerRoots.xml for reflection-heavy code

Missing Avalonia Controls

<ItemGroup>
  <TrimmerRootAssembly Include="Avalonia.Controls" />
  <TrimmerRootAssembly Include="Avalonia.Controls.DataGrid" />
  <!-- Add other control libraries you use -->
</ItemGroup>

Performance Issues

  • Ensure you’re using Release configuration
  • Check for unnecessary reflection usage
  • Profile memory usage to identify issues

General Issues

Large File Size

  • Use Native AOT instead of self-contained
  • Enable InvariantGlobalization if you don’t need localization
  • Remove debug symbols: <DebugSymbols>false</DebugSymbols>

Desktop Entry Not Appearing

  • Check file permissions: ls -la ~/.local/share/applications/
  • Update desktop database: update-desktop-database ~/.local/share/applications/
  • Verify categories match your desktop environment
  • Log out/in or restart desktop session

Command Not Found

  • Verify PATH includes installation directory
  • Check symlink: ls -la /usr/local/bin/YourApp or ls -la ~/.local/bin/YourApp
  • Try absolute path: /opt/YourApp/YourApp

Runtime Errors

  • Check missing dependencies: ldd YourApp
  • Verify all required files are in installation directory
  • Check file permissions: ls -la /opt/YourApp/

Best Practices

For Native AOT Success

  1. Use compiled bindings – Add x:CompileBindings="True" to XAML
  2. Minimize reflection – Use source generators where possible
  3. Test thoroughly – AOT can break functionality silently
  4. Configure trimming properly – Be explicit about what to preserve

For Deployment

  1. Choose the right method – Native AOT for most users, framework-dependent for developers
  2. Proper file organization – Keep related files together
  3. Desktop integration – Makes your app feel native
  4. Icon consistency – Use proper icon sizes and locations

For Maintenance

  1. Version your releases – Include version in desktop entries
  2. Provide uninstall scripts – Make removal clean
  3. Document dependencies – Even AOT apps may have some
  4. Test on target systems – Different distros may behave differently

This guide provides a complete workflow for publishing professional Avalonia UI applications on Linux with optimal performance and proper system integration.

Mohammed Chami
Mohammed Chami
Articles: 45

Leave a Reply

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