Skip to content

Linux app for PinePhone with pain and .NET Core

Posted in Experiences

If you follow me on Minds, you know that I received the PinePhone a few days ago. Since it is a mobile phone built for developers and that I am one, I decided to tinker with it. Tinker further than just swapping operating systems. This means code. After all, I’m the CodingNagger and need to live up to my name once in a while. Even though I took a liking to Go over the past few months, I want to touch a language that is dear to me: C#. Today I will experiment using .NET Core and I’ll even get the latest 5.0 version that Microsoft published recently. We will write a Linux app for the PinePhone.

As each time I want to build a .NET Core solution from scratch, I open that one post I wrote back in 2018 to create. Kudos to the .NET Core guys for putting together a tool for which commands stand the test of time and keep my posts relevant.

However, there is something that will need to change compared to that post. I need to create a UI project instead of a web one. After a good amount of digging, I chose Avalonia which seemingly works on Windows, Mac, and most importantly, Linux Systems. That way I can preview what I’m doing from one of my dev machines before running it on the phone.

The first thing I need to do here is installing the Avalonia templates using the .NET Core CLI:

dotnet new -i Avalonia.Templates

Now I can create my solution with the following commands which are grouped so you can copy/paste them with ease.

dotnet new sln -o DummyCounter # create new solution
cd DummyCounter
dotnet new -o DummyCounter # creates new Avalonia app project
dotnet sln add ./DummyCounter/DummyCounter.csproj # links project to solution

These commands will create the bare bones we need to get started as you can see here in my VSCode instance:

The XAML here looks a bit dark due to the extension being “axaml” and not “xaml”. Installing PimpMyAvalonia will sort that out and add some sort of Intellisense. If you do the same, you will see some colour too despite some people claiming that being wrong.

Life is better with colour!

Now let’s run our base and see what happens.

dotnet run --project .\DummyCounter\

Great, now that we have our dummy app, let’s turn it into a dummy counting app. But first, maybe we want a way to visualise our changes as we make them. Instead of constantly running the app. The same as we would in Visual Studio with traditional XAML.

Actually, there is an Avalonia extension that allows doing just that with Visual Studio. Also you get an AXAML previewer with the AvaloniaRider IDE. However, I like using a small set of tools so that I can have more time mastering them. That being said, if there are no other options then I will use what’s there. But before getting there I shall do some more exploring.

Eventually, I found a comment in a pull request on the Avalonia repository. It suggests that we can use a tool to enable a XAML previewer through a web browser with a command available through the tool. This is the example command:

dotnet exec --runtimeconfig ./Avalonia.BattleCity.runtimeconfig.json --depsfile ./Avalonia.BattleCity.deps.json ~/.nuget/packages/avalonia/0.8.999-cibuild0005045-beta/tools/netcoreapp2.0/designer/Avalonia.Designer.HostApp.dll --transport file:///home/kekekeks/Projects/Avalonia.BattleCity/MainWindow.xaml --method html --html-url ./Avalonia.BattleCity.dll

Since I never saw anything like that before it was tricky at best to understand. But with my experience I was able to gather that “Avalonia.BattleCity” must be the name of that guy’s app. It seems like it executes directly some code from the Avalonia Designer and exposes it as some html designer through localhost on port 6001.

First, I reproduced the command by using my project files and own version of the Avalonia package but removed the –runtimeconfig and –depsfile parameters to get more visibility as to what’s happening. After execution, I got that error:

A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in 'C:\Users\nagger\.nuget\packages\avalonia\0.10.0\tools\netcoreapp2.0\designer\'.
Failed to run as a self-contained app.
  - The application was run as a self-contained app because 'C:\Users\nagger\.nuget\packages\avalonia\0.10.0\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.runtimeconfig.json' was not found.
  - If this should be a framework-dependent app, add the 'C:\Users\nagger\.nuget\packages\avalonia\0.10.0\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.runtimeconfig.json' file and specify the appropriate framework.

My understanding from there is that I do actually need that runtime JSON config file. So I create an empty one to see what happens. This prompted that error among other things:

Invalid runtimeconfig.json

After some swift Qwant search, I found a page with a sample runtimeconfig.json. I copy it and change the library values to use my version of .NET Core. You can find your version of .NET Core by running dotnet --version . For me, it returned “5.0.102” so I used that:

    "runtimeOptions": {
      "tfm": "netcoreapp5.0",
      "framework": {
        "name": "Microsoft.AspNetCore.App",
        "version": "5.0.102"

I got yet another error but was closer.

It was not possible to find any compatible framework version
The framework 'Microsoft.AspNetCore.App', version '5.0.102' was not found.
  - The following frameworks were found:
      3.0.0 at [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
      3.1.4 at [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
      5.0.2 at [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]

You can resolve the problem by installing the specified framework and/or SDK.

The specified framework can be found at:

Basically, the version returned by the command I ran wasn’t quite the level of accuracy I needed then. Spotting the “5.0.2” version that most likely fits my needs I update the runtimeconfig.json file accordingly.

New error!

Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'Avalonia.Base, Version=, Culture=neutral, PublicKeyToken=c8d484a7012f9a8b'. The system cannot find the file specified.
File name: 'Avalonia.Base, Version=, Culture=neutral, PublicKeyToken=c8d484a7012f9a8b'
   at Avalonia.Designer.HostApp.Program.Main(String[] args)

This time, it seems to be a dependency issue. It can’t find an Avalonia base library. Since I recall removing a –deps parameter, I need to figure how to write one that would point at said library. This time I decide to google what a deps.json file is. As it the name suggests it’s a dependency manifest you can use when executing a dll directly. These are normally created my the compiling process but this is not a normal situation so we have to craft it manually.

As a start, I will create an almost empty one so that errors can help me fill the blanks once more. This is what we’ll start with.

    "runtimeTarget": {
      "name": ".NETCoreApp,Version=v5.0"
    "targets": {
      ".NETCoreApp,Version=v5.0": {
    "libraries": {

It gives me the same error which makes sense as we haven’t linked “Avalonia.Base” yet. I opened the file with all the DLLs and something hit me. There were deps.json and runtimeconfig.json files in there. I didn’t need to create new ones.

Upon that realisation I just updated the path to the depsfile and runtime parameters to use the ones generated my dotnet core when building. After updating the command it looks like all went well.

Now opening we get the result below when editing our MainWindow.axaml:

Now that we have some hot reload in place to check our views, I want to go a bit further. The command is not something I want to constantly paste from some text file and replacing random stuff in it to make it work. It’s time to create a Makefile.

dotnet exec --runtimeconfig DummyCounter/bin/Debug/net5.0/DummyCounter.runtimeconfig.json \
--depsfile DummyCounter/bin/Debug/net5.0/DummyCounter.deps.json  \
dotnet exec --runtimeconfig dotnet exec --runtimeconfig DummyCounter/bin/Debug/net5.0/DummyCounter.runtimeconfig.json \
 		--depsfile DummyCounter/bin/Debug/net5.0/DummyCounter.deps.json  \
 		C://Users/nagger/.nuget/packages/avalonia/0.10.0/tools/netcoreapp2.0/designer/Avalonia.Designer.HostApp.dll \
 		--transport file:///$(realpath DummyCounter/MainWindow.axaml) --method html --html-url \

For now since we have only one class to worry about, I first thought I’d leave it at that. However we can improve it a bit for potential other views. Since I’m using a Makefile, I can definitely write something a bit cleaner.

After a bit more time, I get to a fair-looking Makefile with more clarity as to how the command above is structured.

With that Makefile, running the following command will execute our web designer preview code:

 make view=MainWindow.axaml designer

It’ll run the file in DummyCounter/MainWindow.axaml. If I have another file in DummyCounter/Components/BigButton.axaml in the project, I can run the designer to it simply by running this:

 make view=Components/BigButton.axaml designer

Now let’s make it a the Dummy app a DummyCounter app!

The first thing I did there was cleaning up the project by moving the code into a “src” folder to avoid mixing classes with temporary binary files created by the build process. After a few more minutes fiddling around, I got to complete my DummyCounter. Since I wanted to avoid the pain of making my ViewModel properties observable, I used ReactiveUI to make that bit easier to write. I’ll put the whole thing on GitHub when I’m done.

After a few hours on educating myself on how this whole Avalonia thing works, I finally get something semi-satisfactory:

Web Preview

Now let’s take a look at the actual app by running our run command below:

make run

And voila!

The width changes as I set the web browser preview follows the designer settings from the AXAML file but the actual app dimensions will depend on the user will or his device. At least as far as I know and note that I know nothing of Avalonia even after this little exercise.

Now that we know the preview works and that the app built matches it. Let’s figure how to compile it for the PinePhone.

Since I’m on Windows, // write about the publish command targetting alpine-arm64 and try to copy the file onto the PinePhone and see if it works. The first thing to do is verifying we can reach it. On postmarketOS, the default hostname is “pine64-pinephone”.

(share pinephone photo here after fixing time)

ping pine64-pinephone

Next, we make sure we can connect to it through ssh.

If we can log onto the phone now we should be able to push our code there. We will use the scp command in order to do so. Since this post is getting quite long, I’ll speed through from here and skip through the experimenting towards uploading the right files in the archive and get them through the phone. Know that it involved scp and a few changes to the Makefile so that this time isn’t fully wasted. Eventually, here is the command I used to upload the archived files needed to run the app.

scp ./DummyCounter/bin/Debug/net5.0/alpine-arm64/publish/DummyCounter.tar pine@pine64-pinephone:~/DummyCounter.tar

After uploading the archive and extracting my files there I ran into more issues. Some dependencies needed by my libraries weren’t available on postmarketOS. I had to use “ldd” to check what they were.

While trying to figure what was wrong there, I found that it seems to be an issue with glibc. Since I don’t want to take the chance to mess up the phone for an experiment I decided to try another route. I recall that I found a couple of packaging systems meant to run Linux apps anywhere. Flatpak and Snap.

Snap felt more appealing but unfortunately the executable to install snap applications doesn’t exist in the Alpine package repository so that’s not an option for my postmarketOS phone. Only one option left: Flatpak.

The problem is it seems Flatpak requires you to install .NET Core on the phone which defeats the purpose of a self-contained application. Back to the square one. I dug further, trying to build and deploy a self-contained first, then a not self-contained app. Single file, then multiple files builds but still nothing.

I’ve been looking at this for way too many hours and will need to do something else if I am ever to be any kind of productive again. I don’t like sharing source code that doesn’t work as I intend it to so no new repo is coming out of this post despite past promises.

Hopefully, in a few months, we will see that the missing dependencies around libSkiaSharp get fixed for arm64 devices like the PinePhone. If you hear anything let me know. I’ll be waiting. And that’s where I intended to end my post. On a note of failure. However, after all my efforts and this writing, I threw out a hail Mary by asking people on Minds and on StackOverflow if they ever saw anything similar. Most people that encountered similar troubles found no answer so I had little hope.

Then I got a reply from StackOverflow. I needed to build my own libSkia so that I have a version compatible with Alpine Linux.

I forked SkiaSharp and followed instructions. It looked good until I got my first issue. As it turns out, there is no setting available for arm64 Alpine but I could make one from the amd64 config present. I didn’t even get to the build step and got an error.

This was the moment where I decided I didn’t have time for that. I had enough building components on Linux for days to see them not even function properly in end. Been there, done that, hate it, won’t recommend. This also means that it’s time to flash my PinePhone SD card with UBPorts which is built on top of the standard Ubuntu.

Some long minutes later, I had the build running on the phone but it wouldn’t complete booting. It seemed like I would not be able to write a Linux app for my PinePhone. At least not today.

But then, I thought of something. Plasma Mobile, the default OS shipped with my PinePhone, is built on top of a regular Linux distribution. This means the Avalonia built-app should work out of the box according to that SO reply from earlier. I pulled out the SD card, rebooted the phone.

I ran my local install make command after updating the ssh part to connect to my Plasma Mobile account. Then logged onto the phone. A few seconds later the app launched!

That was a painful but necessary journey. I may write a guide, later on, to write an app using .NET Core to write a Linux app for the PinePhone. Stay tuned and you’ll find out. I’d say the best way to interact with me and to get updates is on and the second-best way is by subscribing to me here. Thanks again for reading!


  1. Tobbe

    Nice article. I would like to read more articles about Avalonia or Uno Platform on Linux mobile. Thanks!

    February 1, 2022
  2. Tobias Johansson
    Tobias Johansson

    I would love to see a guide! I couldn’t get my test so to run on Plasma Mobile.

    February 1, 2022

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: