/ aws

SkiaSharp on Linux (Containers)

Google's Skia is a cross platform library for 2D Graphics. SkiaSharp is a nuget package that wraps the Skia Library. Microsoft is a company that makes this harder.

So that's not really fair to Microsoft. They mean well, but their black magic around docker and Visual Studio integration makes this harder than it need be.

I'm using SkiaSharp in a web api. Most of the documentation for SkiaSharp is focused on Xamarin development for mobile devices so you need to interpolate a little if your relying on their documentation.

This is quite a long post detailing my discoveries and solution. If you just want the cheese in this sandwich, press END.

To get SkiaSharp working in a windows solution you just need to add the SkiaSharp nuget to your chosen project and away you go.

For Linux (with or without a container) it's a bit trickier.

Linux Setup

Firstly you need to install 1 pre-req on Linux, libfontconfig1. If your using a container, add this to the top of your docker file, just under the first (base) FROM:

RUN apt-get update;apt-get install libfontconfig1 -y

Otherwise, update your OS in whatever way you choose.

Visual Studio Docker Project

If your using a docker project in Visual Studio to develop & debug then you also need to copy the shared library, libSkiaSharp.so, to the docker image. This shared library is not included with the SkiaSharp nuget package. You need to get it from the git releases page.

You could do this copy in your deployment script, as part of the docker image build or add it as a content file to your Visual Studio project (which may break your windows testing). But it gets worse - or at least it did for me.

When building using the default docker compose project and docker build files, the application could not find the .so file, even when I manually copied it to the bin folder (bin/Release/netcore2.0). I got:

System.DllNotFoundException: Unable to load DLL 'libSkiaSharp'

Microsoft black magic

When you create a docker project for a web api project, Visual Studio creates a dockerfile for you that looks something like this:

FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 53486
EXPOSE 44389

FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY WebApplication4/WebApplication4.csproj WebApplication4/
RUN dotnet restore WebApplication4/WebApplication4.csproj
COPY . .
WORKDIR /src/WebApplication4
RUN dotnet build WebApplication4.csproj -c Release -o /app

FROM build AS publish
RUN dotnet publish WebApplication4.csproj -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "WebApplication4.dll"]

This is a multi-stage build. Note the various FROM lines. You might naturally, logically, rationally think any changes you make to this file will be reflected in your image when you press F5 to run the docker compose. Unfortunately, you would be incorrect.

Building the docker project in debug mode does not use all of your dockerfile.

Take note of this line during the build:

docker-compose  -f "c:\Dev\WebApplication4\docker-compose.yml" -f "c:\Dev\WebApplication4\docker-compose.override.yml" -f "c:\Dev\WebApplication4\obj\Docker\docker-compose.vs.debug.g.yml" -p dockercompose16087381738953648249 --no-ansi up -d --build --force-recreate --remove-orphans

Notice the docker-compose file in the obj\Docker folder? Visual Studio generates this temporary docker-compose file as part of the build and sets this during the build. It overrides your solutions docker-compose file. If you look at this generated file you will see a few interesting things:

version: '3.4'

services:
  webapplication4:
    image: webapplication4:dev
    build:
      target: base
    environment:
      - DOTNET_USE_POLLING_FILE_WATCHER=1
      - NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages
    volumes:
      - c:\Dev\WebApplication4\WebApplication4:/app
      - C:\Users\peter\vsdbg\vs2017u5:/remote_debugger:ro
      - C:\Users\peter\.nuget\packages\:/root/.nuget/packages:ro
      - C:\Program Files\dotnet\sdk\NuGetFallbackFolder:/root/.nuget/fallbackpackages:ro
    entrypoint: tail -f /dev/null
    labels:
      com.microsoft.visualstudio.debuggee.program: "dotnet"
      com.microsoft.visualstudio.debuggee.arguments: " --additionalProbingPath /root/.nuget/packages --additionalProbingPath /root/.nuget/fallbackpackages  bin/Debug/netcoreapp2.1/WebApplication4.dll"
      com.microsoft.visualstudio.debuggee.workingdirectory: "/app"
      com.microsoft.visualstudio.debuggee.killprogram: "/bin/bash -c \"if PID=$$(pidof -x dotnet); then kill $$PID; fi\""

Under build you will see the target is base. So, anything you have for other targets gets ignored during a debug build.

Next, the volumes are set to override the /app folder but since it's only using the base target, any customisations to you add to the final target in your dockerfile are not used in debug.

Lastly, note the volume set for the nuget pages that map to your user folder.

The voodoo that they did here makes the debug experience very nice. You get to see changes in the source files instantly in the running container which is great for web apps and web api's, you don't need to wait too long for the build and of course you get the debugger attached to the running instance.

Getting Debug Container Working

Back to SkiaSharp. The missing piece of the puzzle here is you need to copy libSkiaSharp.so to your local packages folder C:\Users\\[me]\\.nuget\packages\skiasharp\1.60.0\lib\netstandard1.3 - replacing the version and your user name of course. And don't forget to install libfontconfig1 of course.

Deployment and Release Builds

If you look at the release version of the docker-compose file that Visual Studio generates you will see that this is much less aggressive:

version: '3.4'

services:
  webapplication4:
    volumes:
      - C:\Users\peter\vsdbg\vs2017u5:/remote_debugger:ro
    entrypoint: tail -f /dev/null
    labels:
      com.microsoft.visualstudio.debuggee.program: "dotnet"
      #com.microsoft.visualstudio.debuggee.arguments: " $debuggee_arguments_probing_paths_webapplication4$ WebApplication4.dll"
      com.microsoft.visualstudio.debuggee.workingdirectory: "/app"
      com.microsoft.visualstudio.debuggee.killprogram: "/bin/bash -c \"if PID=$$(pidof -x dotnet); then kill $$PID; fi\""

Most importantly here, it does not override the target.

However, this is sort of moot as you are probably/hopefully not using your local builds for releases to testing and live. Right? YES ??

Heres the Cheese

If you use the standard dockerfile and managed to get libfontconfig1 and libSkiaSharp.so setup - your application will still not run. My solution was to customise the dockerfile thusly:

1: Use the dotnet dependencies-only base image and install libFontConfig1 to it:

FROM microsoft/dotnet:2.0-runtime-deps as base
RUN apt-get update;apt-get install libfontconfig1 -y
  1. In the publish target, create a self-contained deployment and copy the libSkiaSharp.so to it:
FROM build AS publish
RUN dotnet publish -c Release -r debian-x64 -o /app
WORKDIR /src
COPY ./linuxlibs/libSkiaSharp.so /app

Note above I built for debian-x64. There are many other RID's you can use. If you read the instructions from Microsoft about creating a self-contained deployment it mentions updating the project file with the RID, e.g. <RuntimeIdentifiers>win10-x64;debian-x64</RuntimeIdentifiers>. I found this wasn't necessary when using the CLI to publish.

3: Change the ENTRYPOINT for the SCD build:

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["./app output name (exe with no .exe :)"]

4: Optional. I create multiple dockerfiles for my app's - one for each config - so I have dockerfile, dockerfile.development and dockerfile.test.

Moral of this story

Only trust the black magic and voodoo you never actually see or experience. This is true magic. Everything else is smoke and mirrors.