Uno WebAssembly Containerization

Adding Docker Support to Uno's Wasm head

Published on 22 June 2020

Intro

In this post I will show how to build and run Uno Platform WebAssembly projects within a Docker container. While I'm pretty familiar with containerization technologies, Uno's transformation of C#/XAML to WebAssembly via Mono was - and, to a certain extent, still is - a bit of a mystery. As such, this post will very much be an exploration of the technical underpinnings of these technologies.

Deployment

While most project heads in an Uno solution are native applications that get deployed to and run on a device (usually from an App store), the WebAssembly (Wasm) head is different. Artifacts from the compilation of the Wasm project need to be deployed to a server which is capable of serving them to a browser when requested.

This deployment is typically achieved by publishing the Wasm artifacts to IIS hosted on a [virtual] server, deploying an Azure App-Service replete with all artifacts or simply copying the artifacts to a file store capable of responding to HTTP requests.

However there is another modern deployment mechanism that has, in recent years, come to dominate the DevOps landscape: Containerization.

Add -> Docker Support...

Visual Studio has had first class support for creating and running Docker Containers for quite a while now and its integration into Visual Studio is very mature. In most instances, Containerizing a project has become as simple as right-clicking on the project, selecting "Add -> Docker Support..." and following the resulting dialogs. Unfortunately, despite being a web project which Visual Studio knows how to "Publish...", right clicking on an Uno Wasm project does not present the option to add Docker Support.

Given I was working on a solution with a containerized ReST API (along with NSwag-generated .NET Standard client project) I really wanted to be able to containerize the Wasm app too so it could form part of a composed deployment.

To simplify the process of working this out, I created a new Uno Platform project named ContaineredUnoWasm using version 2.4.0.0 of the Uno Platform templates and worked to containerize that. This project (completed with working containerized builds) can be found here.

Building in a container

Imitation is the sincerest form of flattery

If I was going to containerize the deployment of the Wasm project, I decided to go the whole hog and allow for multi-stage containerized builds, much like those created natively by Visual Studio. Without really thinking too much about it, my first - somewhat naïve - approach was simply to copy and customise a Dockerfile created by Visual Studio; in effect, something like this:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"]
RUN dotnet restore "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Shared/", "ContaineredUnoWasm/ContaineredUnoWasm.Shared/"]
COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/"]
RUN dotnet build "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/build 

FROM build AS publish
RUN dotnet publish "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/publish

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

We can then try building a container image from this Dockerfile by executing the following command (from the root of our repo):

$> docker build -f .\ContaineredUnoWasm\ContaineredUnoWasm.Wasm\Dockerfile .

Which fails with the somewhat confusing error:

Step 10/16 : RUN dotnet build "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/build
...
Build FAILED.
...
  Downloading mono-wasm-502dca36d36 to /tmp/mono-wasm-502dca36d36.zip
/root/.nuget/packages/uno.wasm.bootstrap/1.2.0/build/Uno.Wasm.Bootstrap.targets(124,5): error : System.ComponentModel.Win32Exception (2): No such file or directory [/src/ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj]

Oooh-kay. We know that file is exists as that's the file we started building!

Jaylee to the rescue!

Casting around for ideas as to what might be causing this issue, I came across this post from the one and only Jérôme Laban which shows how to build the Wasm project in Azure Devops using a container... which all sounded rather promising.

Reading this post shows that Jérôme is using a custom container image - nventive/wasm-build:1.0-bionic - within which the Wasm project is built. Looking this image up on Docker Hub and then navigating to the source repository allows us to see what bizarre wizardry Jérôme is using to build the Wasm project in a container. And here it is:

FROM mcr.microsoft.com/dotnet/core/sdk:2.2.105-bionic
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
RUN echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list
RUN apt-get update

# Install mono, msbuild and dependencies
RUN apt-get -y install sudo unzip python mono-devel msbuild libc6 ninja-build

# Setup for GitVersion 4.x 
RUN sudo apt-get install -y libgit2-dev libgit2-26 && \
ln -s /usr/lib/x86_64-linux-gnu/libgit2.so /lib/x86_64-linux-gnu/libgit2-15e1193.so

# Install node and puppeteer dependencies
RUN curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash - && \
	sudo apt install -y nodejs gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
	libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
	libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
	libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
	libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget

# Install and activate emscripten
RUN git clone https://github.com/juj/emsdk.git && \
	sudo chmod 777 /emsdk && \
    cd emsdk && \
    ./emsdk install sdk-1.38.28-64bit && \
    ./emsdk install sdk-1.38.30-64bit && \
    ./emsdk install sdk-1.38.31-64bit && \
    ./emsdk install sdk-1.38.34-64bit && \
    ./emsdk install latest && \
    ./emsdk activate sdk-1.38.31-64bit && \
    sudo chmod -R 777 /emsdk

No wonder our docker build was failing, look at all the additional dependencies we need to build the Wasm project.

You know, contrary to intuition, the more I learn about the how the C#->WebAssembly transformation works, the more it seems like magic.

Meanwhile, back in the Dockerfile

Right, so armed with Jérôme's magical mystical container image of power, we'll try rewriting our Dockerfile as follows:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80

FROM nventive/wasm-build:latest AS build
WORKDIR /src

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"]
RUN dotnet restore "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Shared/", "ContaineredUnoWasm/ContaineredUnoWasm.Shared/"]
COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/"]
RUN dotnet build "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/build 

FROM build AS publish
RUN dotnet publish "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/publish

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

Looks promising, let's give it a shot. Running this:

$> docker build -f .\ContaineredUnoWasm\ContaineredUnoWasm.Wasm\Dockerfile .

Produces this:

Step 8/14 : RUN dotnet build "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/build
 ---> Running in 1f7ff2159acc
Microsoft (R) Build Engine version 15.9.20+g88f5fadfbe for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restoring packages for /src/ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj...
  Installing Microsoft.Extensions.DependencyInjection.Abstractions 1.1.0.
  Installing System.Runtime.CompilerServices.Unsafe 4.3.0.
  Installing Microsoft.Extensions.Logging 1.1.1.
  Installing System.ValueTuple 4.4.0.
  Installing CommonServiceLocator 2.0.5.
  Installing System.Buffers 4.4.0.
  Installing System.Runtime.CompilerServices.Unsafe 4.5.2.
  Installing Microsoft.Extensions.Primitives 1.1.0.
  Installing Uno.SourceGenerationTasks 2.0.6.
  Installing Uno.Core 2.0.0.
  Installing System.Memory 4.5.2.
  Installing Uno.Core.Build 2.0.0.
  Installing System.Runtime.InteropServices.WindowsRuntime 4.3.0.
  Installing Microsoft.Extensions.Configuration.Abstractions 1.1.1.
  Installing Microsoft.Extensions.Logging.Abstractions 1.1.1.
  Installing Uno.UI 2.4.0.
  Installing Microsoft.Extensions.Logging.Console 1.1.1.
  Installing Microsoft.Extensions.Logging.Filter 1.1.1.
  Installing Uno.Wasm.Bootstrap 1.2.0.
  Installing Uno.Wasm.Bootstrap.DevServer 1.2.0.
  Generating MSBuild file /src/ContaineredUnoWasm/ContaineredUnoWasm.Wasm/obj/ContaineredUnoWasm.Wasm.csproj.nuget.g.props.
  Generating MSBuild file /src/ContaineredUnoWasm/ContaineredUnoWasm.Wasm/obj/ContaineredUnoWasm.Wasm.csproj.nuget.g.targets.
  Restore completed in 27.69 sec for /src/ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj.
It was not possible to find any compatible framework version
The specified framework 'Microsoft.NETCore.App', version '3.0.0' was not found.
  - Check application dependencies and target a framework version installed at:
      /usr/share/dotnet/
  - Installing .NET Core prerequisites might help resolve this problem:
      https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409
  - The .NET Core framework and SDK can be installed from:
      https://aka.ms/dotnet-download
  - The following versions are installed:
      2.2.3 at [/usr/share/dotnet/shared/Microsoft.NETCore.App]
/root/.nuget/packages/uno.sourcegenerationtasks/2.0.6/build/netstandard1.0/Uno.SourceGenerationTasks.targets(127,2): error : Generation failed, error code 150 [/src/ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj]

Build FAILED.

Oh, interesting stuff. An error we can work with and, if I'm not mistaken, I seem to remember the nventive/wasm-build Dockerfile starting with FROM mcr.microsoft.com/dotnet/core/sdk:2.2.105-bionic. Perhaps this Dockerfile is just a little out of date?

Updating Wasm-Build


- - - - EDIT - - - -

At this point I forked, cloned, updated and built a new version of the nventive/docker image. However, towards the end of authoring this blog post, a hunch caused me to search for "docker" in the "unoplatform" organisation on Github. On the last page of "Code" hits, I found a link to this Readme.md in which, about half way down, is a reference to another container image unoplatform/wasm-build.

This image was completely up to date and meant I no longer had to build a custom version so the rest of this (somewhat painful) section has been removed. Conversely, I decided to leave the slight misdirection in the section above as I thought it provided quite an insight into the complexities involved in building the Wasm output.

- - - - END EDIT - - - -


One step back, two steps forward

Ok, having found a new wasm-build image, lets try integrating it into the Dockerfile for building ContaineredUnoWasm as follows:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80

FROM unoplatform/wasm-build:latest AS build
WORKDIR /src

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"]
RUN dotnet restore "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Shared/", "ContaineredUnoWasm/ContaineredUnoWasm.Shared/"]
COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/"]
RUN dotnet build "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/build 

FROM build AS publish
RUN dotnet publish "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/publish

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

Which is built using the command:

$> docker build -f .\ContaineredUnoWasm\ContaineredUnoWasm.Wasm\Dockerfile . -t ibebbs/containeredunowasm:latest

And results in:

Successfully built c2c4c48cf33a
Successfully tagged ibebbs/containeredunowasm:latest

Huzzah, it worked!

BRILLIANT!!!!

Running from a container

Now, unless I'm very much mistaken, running our containerized Wasm app should be as simple as starting the container and navigating to the exposed port in a browser. As such, let's run the command:

$> docker run -p 5000:80 ibebbs/containeredunowasm:latest

Results in:

It was not possible to find any installed .NET Core SDKs
  Did you mean to run .NET Core SDK commands? Install a .NET Core SDK from:
      https://aka.ms/dotnet-download

Errr... no I didn't, I wanted to run my app service. Let's see what's going on here by opening an interactive shell within the container and listing the contents:

docker run -it --entrypoint /bin/bash ibebbs/containeredunowasm:latest
root@52acb17752f2:/app# dir
AppManifest.js                                             Uno.UI.css
Assets                                                     Uno.UI.dll
CommonServiceLocator.dll                                   Uno.UI.js
Fonts.css                                                  Uno.Xaml.dll
Microsoft.Extensions.Configuration.Abstractions.dll        Uno.dll
Microsoft.Extensions.DependencyInjection.Abstractions.dll  _compressed_br
Microsoft.Extensions.Logging.Abstractions.dll              _compressed_gz
Microsoft.Extensions.Logging.Console.dll                   corebindings.o
Microsoft.Extensions.Logging.Filter.dll                    dotnet.js
Microsoft.Extensions.Logging.dll                           dotnet.wasm
Microsoft.Extensions.Primitives.dll                        driver.o
Properties                                                 index.html
System.Buffers.dll                                         jquery-pep.js
System.Collections.Immutable.dll                           managed-4aa732b23652301ed854d2dd646ce71b0b0b5e3f
System.ComponentModel.dll                                  mono-config.js
System.Linq.dll                                            normalize.css
System.Memory.dll                                          refs
System.Numerics.Vectors.dll                                require.js
System.Reflection.Emit.ILGeneration.dll                    runtime.js
System.Reflection.Emit.Lightweight.dll                     server.py
System.Runtime.CompilerServices.Unsafe.dll                 service-worker.js
System.Runtime.InteropServices.WindowsRuntime.dll          setImmediate.js
System.Threading.dll                                       uno-bootstrap.css
Uno.Core.dll                                               uno-bootstrap.js
Uno.Foundation.dll                                         uno-config.js
Uno.UI.Toolkit.dll                                         web.config
Uno.UI.Wasm.dll                                            zlib-helper.o

What? No ContaineredUnoWasm.Wasm.dll but an "index.html"? This looks suspiciously like a...

Mind Blown

Yup, very much mistaken. The result of compiling the Wasm project isn't a hosted service but is ... of course ... just content which can be hosted by another service. As such, there's really nothing for the container to run and, as such, we're going to need a web server to serve this content.

Kestrel vs Nginx

Now, this being a .net solution and me being a .net fanboi, I really wanted serve this content from a .net webserver. I was thinking it should be possible to find an off-the-[docker hub]-shelf image of a kestrel webserver which was/could be configured to serve static content. But no, despite a significant search, it appeared that, if I wanted to serve the content from Kestrel I'd have to add, build and containerize a bespoke web project.

So instead I decided to use nginx which is configured to serve static content out-of-the-box.

To use nginx, I amended the Dockerfile for ContaineredUnoWasm to this:

FROM unoplatform/wasm-build:latest AS build
WORKDIR /src

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"]
RUN dotnet restore "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Shared/", "ContaineredUnoWasm/ContaineredUnoWasm.Shared/"]
COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/"]
RUN dotnet build "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/build 

FROM build AS publish
RUN dotnet publish "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/publish

FROM nginx:alpine
EXPOSE 80
COPY --from=publish /app/publish /usr/share/nginx/html

Then rebuilt and started the image:

$> docker build -f .\ContaineredUnoWasm\ContaineredUnoWasm.Wasm\Dockerfile . -t ibebbs/containeredunowasm:latest
...
Successfully built 4c1fc5cb9efd
Successfully tagged ibebbs/containeredunowasm:latest
$> docker run -p 5000:80 ibebbs/containeredunowasm:latest
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Configuration complete; ready for start up

Looks promising. Let's hit http://localhost:5000 with a browser:

Uno Logo

Well, that's better. Lets see if we can resolve the "Incorrect response MIME type" issue by telling nginx about the mime types to serve. This is done by adding a mime.types file to our project containing:

types {
  text/html                             html htm shtml;
  text/css                              css;
  text/javascript						            js;
  application/wasm                      wasm;
  application/octet-stream              dll clr;
  application/json                      json;
  application/font-woff                 woff woff2;
}

And adding it to our nginx container by amending the Dockerfile for ContaineredUnoWasm as shown here:

FROM unoplatform/wasm-build:latest AS build
WORKDIR /src

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"]
RUN dotnet restore "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Shared/", "ContaineredUnoWasm/ContaineredUnoWasm.Shared/"]
COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/"]
RUN dotnet build "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/build 

FROM build AS publish
RUN dotnet publish "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/publish

FROM nginx:alpine
EXPOSE 80
COPY --from=publish /app/publish /usr/share/nginx/html
COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/mime.types", "/etc/nginx/mime.types" ]

Now, rebuilding the image, running the container and hitting the endpoint in a browser gives us:

Hello World!

Wahoo!

A little house keeping

Of course, needing to add a "mime.types" file with this exact content to any Wasm project we want to containerize is a pain so instead lets make a container image that already includes this file. In a fork of the unoplatform/docker repository, I'll add a new folder called wasm-serve within which I'll add the mime.types file from above and the following Dockerfile:

FROM nginx:alpine
COPY ["mime.types", "/etc/nginx/mime.types" ]

Building this image with the command docker build . -t wasm-server:latest allows me to modify the Dockerfile for ContaineredUnoWasm as follows:

FROM unoplatform/wasm-build:latest AS build
WORKDIR /src

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"]
RUN dotnet restore "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj"

COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Shared/", "ContaineredUnoWasm/ContaineredUnoWasm.Shared/"]
COPY ["ContaineredUnoWasm/ContaineredUnoWasm.Wasm/", "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/"]
RUN dotnet build "ContaineredUnoWasm/ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/build 

FROM build AS publish
RUN dotnet publish "ContaineredUnoWasm.Wasm/ContaineredUnoWasm.Wasm.csproj" -c Release -o /app/publish

FROM ibebbs/wasm-serve:latest
EXPOSE 80
COPY --from=publish /app/publish /usr/share/nginx/html

Which, when rebuilt and run, still results in the running app.

Done and done.

Future improvements

At the moment the docker build needs to download the "mono-wasm SDK" on each build. It would be much better download the mono-wasm SDK in an earlier step of the Dockerfile so that it is cached and doesn't need to be downloaded each time. I'm looking into doing this and will provide an update as an when I've worked out how to do it.

This article suggests downloading the SDK from here but the build output from Visual Studio suggests the SDK is coming from a blob owned by Uno (https://unowasmbootstrap.blob.core.windows.net/runtime/mono-wasm-###########.zip). Either way, once it's downloaded, setting the WASM_SDK to the path of the unzipped sdk should ensure the sdk doesn't need to be downloaded during build.

I'm also still having problems building/running more advanced projects within a container. Hitting a weird error (Object doesn't support property or method '_coreDispatcherCallback') when running the resulting WASM in a browser. Still banging my head against this one so any advice gratefully received.

- - - - EDIT - - - -

And as if by magic, Jérôme nails the answer in one fell swoop:


- - - - END EDIT - - - -

Conclusion

It really was a bit of a roller-coaster of a journey to get a containerized build / image of an Uno Wasm project running.

One thing it really highlighted to me is the amazing impact of open-source code. Any time an issue was encountered, I was able to find the exact code/file causing the problem and thereby find a solution. For someone who cut his teeth programming before the internet made resolving every programming issue as simple as a quick jaunt to StackOverflow, and when source-code was something you had to pay through the nose for, working in and with OSS for the past several years has been an real eye-opener (if you'll excuse the somewhat strained facial idioms).

However, we also discovered that this transparency can also be a double-edged sword. The .NET ecosystem - and especially Uno - is moving forward at an incredible cadence with new releases causing older versions to be deprecated and documentation to become dated. As we can see above, this can throw serious curve-balls to anyone endeavouring to stray from the beaten path or get a better understanding of how things work.

Anyway, should you have any suggestions for, or questions about, anything above please feel free to drop me a line using any of the links below or from my about page.