The console test suite (SkiaSharp.Tests.Console) can be run inside a Docker container against a
chosen native build. This covers container runtimes that the agent-based test legs don't — Linux
(Azure Linux, glibc), Linux (Alpine, musl), their No Dependencies variants, and Windows
Nano Server.
Pieces:
- Cake target
tests-container— runs the console suite against a prebuilt native library. Builds no externals. - Env images
scripts/infra/tests/docker/—azurelinux/,azurelinux-nodeps/,alpine/,alpine-nodeps/,nanoserver/. - CI legs
tests_container_linux,tests_container_linux_nodeps,tests_container_alpine_linux,tests_container_alpine_nodeps,tests_container_nanoserver_windowsinscripts/azure-templates-stages-test.yml.
The desktop Linux leg already runs on an Ubuntu agent, so the glibc container leg uses Azure
Linux instead — a different, minimal glibc distro rather than a second Ubuntu run.
Each leg uses the bootstrapper docker: feature (azure-templates-jobs-bootstrapper.yml), which:
- builds the env image from
<docker>/Dockerfilewith theDocker@2task, then - runs
dotnet cake --target=tests-containerinside that image against the mounted repo:docker run --volume <repo>:<work> skiasharp … dotnet cake --target=tests-container ….
The feature runs on both Linux and Windows agents: Linux mounts /work and runs via /bin/sh (POSIX,
so the bare No Dependencies images need no shell package); Windows mounts C:\work and runs via cmd.
The env images provide only what each scenario needs. The two fontconfig images add fontconfig
- fonts (see Fonts); the No Dependencies and Nano Server images install nothing
beyond the base .NET SDK — that is the point of those legs. There is no
COPYin these Dockerfiles; the repo is mounted at run time.
- builds
tests/SkiaSharp.Tests.Consolewith-p:TargetFrameworks=net10.0, collapsing the multi-targeted binding projects to a single TFM so no Android/iOS workloads are needed; - runs the suite via
dotnet test, writingoutput/logs/testlogs/**/TestResults.trx.
It builds no externals — the prebuilt libSkiaSharp comes from output/native/<platform>/<arch>/
in the mounted repo (in CI, the merged native artifact; locally, externals-download).
--nativePlatform=<platform> sets the SkiaSharpNativePlatform / HarfBuzzSharpNativePlatform
MSBuild properties. In binding/IncludeNativeAssets.SkiaSharp.targets (and the HarfBuzz twin), those
properties make the desktop Content include copy the native library from
output/native/<platform>/<arch>/ only. The <arch> is derived from the host (OSArchitecture).
This is how a container runs against a platform-specific build the OS-derived defaults don't map —
nanoserver (output/native/nanoserver/x64/libSkiaSharp.dll) on Windows, and alpine
(output/native/alpine/x64/libSkiaSharp.so) on musl.
Build the env image, then run tests-container inside it against the mounted repo:
# Linux Azure Linux (glibc). --nativePlatform picks the build; <arch> is auto from the host.
dotnet cake --target=externals-download
docker build -t skiasharp-tests-env scripts/infra/tests/docker/azurelinux
docker run --rm --volume "$(pwd):/work" -w /work skiasharp-tests-env \
/bin/sh -c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=linux"# Linux Alpine (musl).
docker build -t skiasharp-tests-env-alpine scripts/infra/tests/docker/alpine
docker run --rm --volume "$(pwd):/work" -w /work skiasharp-tests-env-alpine \
/bin/sh -c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=alpine"# No Dependencies (bare image, no fontconfig). Swap in alpine-nodeps / --nativePlatform=alpinenodeps
# for the musl variant.
docker build -t skiasharp-tests-env-nodeps scripts/infra/tests/docker/azurelinux-nodeps
docker run --rm --volume "$(pwd):/work" -w /work skiasharp-tests-env-nodeps \
/bin/sh -c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=linuxnodeps"# Nano Server (Windows container host only).
dotnet cake --target=externals-nanoserver
docker build -t skiasharp-tests-env scripts/infra/tests/docker/nanoserver
docker run --rm --volume "${pwd}:C:\work" -w C:\work skiasharp-tests-env `
cmd /c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=nanoserver"Each leg is a bootstrapper job in the tests stage that:
- declares
requiredArtifacts: - name: native, sooutput/native/…is the merged native artifact; - sets
docker:to the env image andtarget: tests-containerwithadditionalArgs: --nativePlatform=<…>; - publishes the TRX via
PublishTestResults@2(Azure DevOps Tests tab) and thetestlogs_container_<platform>artifact.
The legs are gating: a genuine test failure fails the build. Environment differences that are not
SkiaSharp regressions — most notably the fontless nodeps and Nano Server builds — are handled by
the runtime self-skip helpers (see Fonts below), so those tests skip rather than fail.
These images deliberately carry no GPU stack — no X server, no Mesa, no ICD — since their purpose is
to prove SkiaSharp works without those dependencies. GPU backends are required by default
(see gpu-test-policy.md), so each container leg declares the backends its image
cannot provide through the bootstrapper's env: parameter. Only the shared console suite runs here —
tests-container builds SkiaSharp.Tests.Console alone — so the Vulkan and Direct3D satellites
never load and need no declaration.
The Nano Server leg requires a Windows agent with container support (matching the ltsc2022 image
base) and pulls the Nano Server .NET SDK image.
Whether the suite can resolve system fonts depends on how the native library was compiled, not just on the image:
- fontconfig builds (
linux,alpine) enumerate whatever fonts are installed in the image. Base .NET SDK images ship no fonts, so the two fontconfig env images install them:fontconfig+ DejaVu on both, plus an emoji font for the Unicode tests. Alpine installsfont-noto-emoji(packaged); Azure Linux has no emoji font in its repos, so its Dockerfile downloads Noto Emoji (SIL OFL) pinned by URL commit and verified by SHA-256. These provide the families the test config expects (DefaultFontFamily,UnicodeFontFamilies). - non-fontconfig / bare — the No Dependencies variants (
linuxnodeps,alpinenodeps, built withskia_use_fontconfig=false) run in bare images with nothing installed, and Nano Server — all enumerate no system fonts regardless of what the image contains. Their font manager (SkFontMgr_New_Custom_Empty, a FreeType scanner) can only use fonts loaded explicitly (SKTypeface.FromFile/FromStream/FromData). APIs that resolve a system family or the default typeface (SKTypeface.FromFamilyName,SKFontManager.Default, a defaultSKFont) have nothing to bind to.
The Linux test config chooses UnicodeFontFamilies per libc — Symbola (Ubuntu desktop) or
Noto Emoji (Azure Linux container) on glibc, and Noto Color Emoji on musl — keyed off
PlatformConfiguration.IsGlibc.
Tests that need system fonts detect the environment at runtime and self-skip, so the same suite runs unmodified everywhere:
SkipWhenNoSystemFontManager()— for tests that enumerate or match families (SKFontManager/SKFontStyleSet, orMatchCharacter). Skips where there is no usable font manager: WASM, the NoDependencies builds, and Nano Server.SkipWhenNoDefaultFont()— for tests that measure or draw with the default typeface. Skips where the default typeface is empty: the NoDependencies builds and Nano Server. WASM keeps a single embedded default font, so these still run there.