Craft a .NET app

This how-to guide covers the steps, decisions, and implementation details that are unique when crafting a snap of an app built using .NET. We’ll work through the aspects unique to .NET-based apps by examining two existing project files: a .NET command-line tool and an Avalonia desktop app.

Command-line apps

The whatime example targets core22, where .NET parts use the original .NET plugin. For core24 and newer bases, see the Avalonia example below.

Example whatime project file

The following code comprises the project file of a .NET tool, whatime. This project is a CLI command for returning the current time in cities across the globe.

whatime project file
snapcraft.yaml
name: whatime
version: "1.0.0"
grade: devel
summary: Get the current time in various cities around the world
description: |
  Whatime is able to get you the current time in several different cities
  around the world.

base: core22
confinement: devmode

parts:
  whatime:
    plugin: dotnet
    dotnet-build-configuration: Release
    dotnet-self-contained-runtime-identifier: linux-x64
    source: .
    build-packages:
      - dotnet-sdk-6.0
    stage-packages:
      - libicu70

apps:
  whatime:
    command: Whatime

Add a part written for .NET

snapcraft.yaml
parts:
  whatime:
    plugin: dotnet
    dotnet-build-configuration: Release
    dotnet-self-contained-runtime-identifier: linux-x64
    source: .
    build-packages:
      - dotnet-sdk-6.0
    stage-packages:
      - libicu70

.NET parts are built with the .NET plugin.

To add a .NET part:

  1. Declare the general part keys, such as source, override-build, and so on.

  2. Set the plugin key to dotnet.

  3. By default, the plugin builds with the Release configuration, which is what you want for a published snap. If you’re iterating on a development snap and need the extra debugging symbols and unoptimized code of the Debug configuration, set the dotnet-build-configuration key to Debug.

  4. If you need to build the project as a single binary:

    1. In the .csproj file, add the following to the <PropertyGroup> tag:

      <PublishSingleFile>true</PublishSingleFile>
      
    2. Set dotnet-self-contained-runtime-identifier to the target architecture’s runtime identifier.

  5. For build-packages, list any required .NET SDK packages needed for build time.

Avalonia apps

Avalonia is a cross-platform UI framework for building .NET desktop apps. Unlike command-line tools, Avalonia apps need access to the desktop session and display server, and require extra rendering libraries at runtime.

On core24 and higher bases, the .NET plugin downloads the .NET SDK itself, and the .NET extensions connect the app to a shared .NET runtime snap. Together, they remove the need to declare the SDK as a build package or bundle the runtime inside the snap.

Example XamlPlayground project file

The following code comprises the project file of an Avalonia app, XamlPlayground. This project is a desktop app for experimenting with XAML markup and seeing the results in real time. It’s published on the Snap Store, and its snap is maintained in the community snap repository.

XamlPlayground project file
snapcraft.yaml
name: avalonia-xaml-playground
base: core24
adopt-info: xaml-playground
icon: snap/gui/avalonia-xaml-playground.png
summary: Avalonia XAML and C# playground
description: |
  The Avalonia XAML Playground application allows you to write
  XAML and code-behind pieces of code, with Avalonia controls,
  and seeing the results in real time.

grade: stable
confinement: strict

platforms:
  amd64:
  arm64:

parts:
  xaml-playground:
    plugin: dotnet
    source: https://github.com/AvaloniaUI/XamlPlayground
    source-type: git
    dotnet-version: "10.0"
    dotnet-configuration: Release
    dotnet-project: src/XamlPlayground.NetCore/XamlPlayground.NetCore.csproj
    dotnet-verbosity: minimal
    override-build: |
      version=$(git show -s --format=%h)
      craftctl set version="${version}"
      craftctl default
    organize:
      "*": opt/xamlplayground/

  prereqs:
    plugin: nil
    after:
      - xaml-playground
    stage-packages:
      - fontconfig
      - libfontconfig1
      - libice6
      - libsm6
      - libx11-6

apps:
  avalonia-xaml-playground:
    command: opt/xamlplayground/XamlPlayground.NetCore
    desktop: meta/gui/avalonia-xaml-playground.desktop
    extensions: [dotnet10]
    plugs:
      - desktop
      - desktop-legacy
      - home
      - opengl
      - unity7
      - wayland

lint:
  ignore:
    - library:
        - usr/lib/**/libICE*
        - usr/lib/**/libicu*
        - usr/lib/**/liblttng-ust-*
        - usr/lib/**/libSM*
        - usr/lib/**/libX11*
        - usr/lib/**/libXau*
        - usr/lib/**/libxcb*
        - usr/lib/**/libXdmcp*

Declare the supported platforms

snapcraft.yaml
platforms:
  amd64:
  arm64:

The dotnet plugin builds with a .NET SDK that’s only published for the amd64 and arm64 architectures, so an Avalonia snap can only target those architectures.

To declare the supported platforms, list amd64 and arm64 in the top-level platforms key. See Select platforms for the available ways to declare platforms.

Add the .NET part

snapcraft.yaml
parts:
  xaml-playground:
    plugin: dotnet
    source: https://github.com/AvaloniaUI/XamlPlayground
    source-type: git
    dotnet-version: "10.0"
    dotnet-configuration: Release
    dotnet-project: src/XamlPlayground.NetCore/XamlPlayground.NetCore.csproj
    dotnet-verbosity: minimal
    override-build: |
      version=$(git show -s --format=%h)
      craftctl set version="${version}"
      craftctl default
    organize:
      "*": opt/xamlplayground/

On core24 and higher bases, .NET parts are built with the .NET plugin (v2).

To add the .NET part:

  1. Declare the general part keys, such as source, override-build, and so on.

  2. Set the plugin key to dotnet.

  3. Set the dotnet-version key to the version of the .NET SDK to download and build with. It must match the framework version the project targets, as declared by the <TargetFramework> tag in the .csproj file. XamlPlayground targets net10.0, so the version is "10.0".

  4. If the repository contains multiple projects, set dotnet-project to the path of the project file that builds the app.

  5. By default, the plugin builds with the Release configuration, which is what you want for a published snap. If you’re iterating on a development snap and need the extra debugging symbols and unoptimized code of the Debug configuration, set the dotnet-configuration key to Debug.

  6. The plugin publishes the app directly into the part’s install directory. To keep the published files separate from the system directories of the snap, use the organize key to move them into a dedicated directory, such as opt/xamlplayground/.

Stage the rendering dependencies

snapcraft.yaml
  prereqs:
    plugin: nil
    after:
      - xaml-playground
    stage-packages:
      - fontconfig
      - libfontconfig1
      - libice6
      - libsm6
      - libx11-6

Avalonia apps need font and X11 client libraries at runtime that the .NET runtime content snap doesn’t provide. Stage them with a separate part that runs after the .NET part:

  1. Declare a new part and set its plugin key to nil.

  2. List the .NET part with the after key to ensure the runtime libraries aren’t overwritten.

  3. Declare the following font and X11 client libraries with the stage-packages key:

    • fontconfig

    • libfontconfig1

    • libice6

    • libsm6

    • libx11-6

Declare the Avalonia app

snapcraft.yaml
apps:
  avalonia-xaml-playground:
    command: opt/xamlplayground/XamlPlayground.NetCore
    desktop: meta/gui/avalonia-xaml-playground.desktop
    extensions: [dotnet10]
    plugs:
      - desktop
      - desktop-legacy
      - home
      - opengl
      - unity7
      - wayland

Avalonia apps must use the .NET extension that matches their target framework. Apps targeting the net10.0 framework, for example, must use the dotnet10 extension. The extension configures the app’s runtime environment and connects it to the shared .NET runtime snap. The .NET extensions reference provides details on how the extensions modify snap project files.

To add the Avalonia app:

  1. Declare the general app keys, such as command. The command is the path of the published binary inside the snap, including the directory set with the part’s organize key.

  2. Set the desktop key to the path of the app’s .desktop file so the app appears in the desktop environment’s app grid.

  3. List the .NET extension that matches the app’s target framework with the extensions key.

  4. List the interfaces the app needs to access the desktop session and render its windows with the plugs key:

    • desktop and desktop-legacy for integration with the desktop session.

    • wayland and opengl for display output and graphics acceleration.

    • home for access to the user’s files.

    • unity7 for desktop integration on X11 sessions.

If the app needs network access, such as to fetch online content, add the network interface as well.

Silence the library linter warnings

snapcraft.yaml
lint:
  ignore:
    - library:
        - usr/lib/**/libICE*
        - usr/lib/**/libicu*
        - usr/lib/**/liblttng-ust-*
        - usr/lib/**/libSM*
        - usr/lib/**/libX11*
        - usr/lib/**/libXau*
        - usr/lib/**/libxcb*
        - usr/lib/**/libXdmcp*

Avalonia apps ship some libraries, such as the .NET runtime’s own copies of libicu, that the library linter reports as unused or conflicting. These warnings are expected, and you can silence them with the top-level lint key. The Linters references contains more details on the linters Snapcraft runs.