Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ae4ae24
Add containerized test legs via the bootstrapper docker feature
Copilot Jul 22, 2026
37ced31
Merge remote-tracking branch 'origin/main' into mattleibow-nano-serve…
Copilot Jul 22, 2026
7166a4d
Describe containerized testing in final-state terms
Copilot Jul 22, 2026
b2a97b9
Make Alpine container tests resolve emoji fonts
Copilot Jul 22, 2026
cd05f7b
Skip system-font tests on fontless container legs via a trait filter
Copilot Jul 22, 2026
75c7dcb
Merge remote-tracking branch 'origin/main' into mattleibow-nano-serve…
Copilot Jul 22, 2026
3feb06a
Self-skip font tests on fontless platforms instead of a trait filter
Copilot Jul 22, 2026
99d8a19
Generate a cmd.cmd script for the Windows Docker run, mirroring Linux
Copilot Jul 22, 2026
1b0c977
Drop redundant comments from the container test legs
Copilot Jul 22, 2026
ce6df00
Make the container test legs gating
Copilot Jul 22, 2026
a85c23c
Drop redundant --platform flag from the container image build
Copilot Jul 22, 2026
6ecc146
Update containerized-testing doc to match the gating legs
Copilot Jul 22, 2026
fe9eebe
Skip sn.exe strong-name re-sign in the Nano Server container build
Copilot Jul 22, 2026
c997405
Skip mdoc.exe doc generation in the Nano Server container build
Copilot Jul 22, 2026
0a37c08
Retrigger CI (tvOS agent dropout skipped the container legs)
Copilot Jul 23, 2026
488ae05
Green the six Nano Server test failures
Copilot Jul 23, 2026
f496c52
Detect Nano Server from the OS instead of probing XPS
Copilot Jul 23, 2026
01f18ae
Move SupportsXps to BaseTest and drop VisualPlatform.Tag
Copilot Jul 23, 2026
c891e54
Simplify VisualPlatform.Tags to a single yield method
Copilot Jul 23, 2026
ef379fc
Harden Nano detection and native-swap arch selection from review
Copilot Jul 23, 2026
caafc9d
Restructure container test legs: Azure Linux, bare NoDeps images, cle…
Copilot Jul 23, 2026
7e350b2
Drop the linux no-dependencies TODO now that the NoDeps legs exist
Copilot Jul 23, 2026
04c143c
Skip the heavy bitmap-threading stress variant on musl (#4515)
Copilot Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion binding/IncludeNativeAssets.HarfBuzzSharp.targets
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@
<BuildArch>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant)</BuildArch>
</PropertyGroup>

<ItemGroup Condition="!$(TargetFramework.Contains('-')) or $(TargetFramework.Contains('-desktop'))">
<!--
Explicit native-platform override (used by the containerized test legs). When
HarfBuzzSharpNativePlatform is set (e.g. 'nanoserver', 'alpine', 'linux'), copy the native library
from output/native/<HarfBuzzSharpNativePlatform>/<arch> ONLY, instead of the OS-derived globs below.
Used by the containerized test legs — see documentation/dev/containerized-testing.md.
-->
<ItemGroup Condition="'$(HarfBuzzSharpNativePlatform)' != '' and (!$(TargetFramework.Contains('-')) or $(TargetFramework.Contains('-desktop')))">
<Content Include="$(MSBuildThisFileDirectory)..\output\native\$(HarfBuzzSharpNativePlatform)\$(BuildArch)\libHarfBuzzSharp.*" Visible="False" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Comment thread
Copilot marked this conversation as resolved.

<ItemGroup Condition="'$(HarfBuzzSharpNativePlatform)' == '' and (!$(TargetFramework.Contains('-')) or $(TargetFramework.Contains('-desktop')))">
<Content Include="$(MSBuildThisFileDirectory)..\output\native\windows\$(BuildArch)\libHarfBuzzSharp.*" Condition="'$(Platform)' == '' or '$(Platform)' == 'AnyCPU' or '$(Platform)' == 'Any CPU'" Visible="False" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(MSBuildThisFileDirectory)..\output\native\windows\$(Platform)\libHarfBuzzSharp.*" Condition="'$(Platform)' != '' and '$(Platform)' != 'AnyCPU' and '$(Platform)' != 'Any CPU'" Visible="False" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(MSBuildThisFileDirectory)..\output\native\osx\libHarfBuzzSharp.*" Visible="False" CopyToOutputDirectory="PreserveNewest" />
Expand Down
12 changes: 11 additions & 1 deletion binding/IncludeNativeAssets.SkiaSharp.targets
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@
<BuildArch>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant)</BuildArch>
</PropertyGroup>

<ItemGroup Condition="!$(TargetFramework.Contains('-')) or $(TargetFramework.Contains('-desktop'))">
<!--
Explicit native-platform override (used by the containerized test legs). When
SkiaSharpNativePlatform is set (e.g. 'nanoserver', 'alpine', 'linux'), copy the native library
from output/native/<SkiaSharpNativePlatform>/<arch> ONLY, instead of the OS-derived globs below.
Used by the containerized test legs — see documentation/dev/containerized-testing.md.
-->
<ItemGroup Condition="'$(SkiaSharpNativePlatform)' != '' and (!$(TargetFramework.Contains('-')) or $(TargetFramework.Contains('-desktop')))">
<Content Include="$(MSBuildThisFileDirectory)..\output\native\$(SkiaSharpNativePlatform)\$(BuildArch)\libSkiaSharp.*" Visible="False" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Comment thread
Copilot marked this conversation as resolved.

<ItemGroup Condition="'$(SkiaSharpNativePlatform)' == '' and (!$(TargetFramework.Contains('-')) or $(TargetFramework.Contains('-desktop')))">
<Content Include="$(MSBuildThisFileDirectory)..\output\native\windows\$(BuildArch)\libSkiaSharp.*" Condition="'$(Platform)' == '' or '$(Platform)' == 'AnyCPU' or '$(Platform)' == 'Any CPU'" Visible="False" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(MSBuildThisFileDirectory)..\output\native\windows\$(Platform)\libSkiaSharp.*" Condition="'$(Platform)' != '' and '$(Platform)' != 'AnyCPU' and '$(Platform)' != 'Any CPU'" Visible="False" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(MSBuildThisFileDirectory)..\output\native\osx\libSkiaSharp.*" Visible="False" CopyToOutputDirectory="PreserveNewest" />
Expand Down
4 changes: 4 additions & 0 deletions build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ Task ("tests-netcore")
.IsDependentOn ("externals")
.Does (() => RunCake ("./scripts/infra/tests/tests-netcore.cake", "Default"));

Task ("tests-container")
.Description ("Run the console test suite against prebuilt natives (used by the containerized test legs).")
.Does (() => RunCake ("./scripts/infra/tests/tests-container.cake", "Default"));

Task ("tests-android")
.Description ("Run all Android tests.")
.IsDependentOn ("externals")
Expand Down
5 changes: 5 additions & 0 deletions documentation/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ C# Wrapper (binding/SkiaSharp/) → P/Invoke → C API (externals/skia/src/c
| [building.md](building.md) | Build on Windows & macOS |
| [building-linux.md](building-linux.md) | Build native libraries for Linux |

### Testing
| Document | Description |
|----------|-------------|
| [containerized-testing.md](containerized-testing.md) | Run the console test suite inside a Docker container (Linux glibc/Alpine + Nano Server) via the bootstrapper `docker:` feature + the `tests-container` cake target |

### Releasing
| Document | Description |
|----------|-------------|
Expand Down
125 changes: 125 additions & 0 deletions documentation/dev/containerized-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Containerized testing (Docker)

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
glibc**, **Linux Alpine (musl)**, and **Windows Nano Server**.

Pieces:

- **Cake target** [`tests-container`](../../scripts/infra/tests/tests-container.cake) — runs the
console suite against a prebuilt native library. Builds no externals.
- **Env images** [`scripts/infra/tests/docker/`](../../scripts/infra/tests/docker) —
`glibc/`, `alpine/`, `nanoserver/`.
- **CI legs** `tests_container_linux`, `tests_container_alpine_linux`,
`tests_container_nanoserver_windows` in
[`scripts/azure-templates-stages-test.yml`](../../scripts/azure-templates-stages-test.yml).

## How a leg runs

Each leg uses the bootstrapper `docker:` feature (`azure-templates-jobs-bootstrapper.yml`), which:

1. builds the **env image** from `<docker>/Dockerfile` with the `Docker@2` task, then
2. runs `dotnet cake --target=tests-container` **inside** 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/bash`;
Windows mounts `C:\work` and runs via `cmd`.

The **env images** provide only the runtime and fonts — the SDK, and for the Linux images the
`fontconfig` + DejaVu fonts. The Nano Server image is the Nano Server **.NET SDK** image, which has
no system fonts (see [Fonts on Nano Server](#fonts-on-nano-server)). There is no `COPY` in these
Dockerfiles; the repo is mounted at run time.

## What `tests-container` does

1. builds `tests/SkiaSharp.Tests.Console` with `-p:TargetFrameworks=net10.0`, collapsing the
multi-targeted binding projects to a single TFM so no Android/iOS workloads are needed;
2. runs the suite via `dotnet test`, writing `output/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`).

## Selecting the native build

`--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.

## Running it locally

Build the env image, then run `tests-container` inside it against the mounted repo:

```bash
# 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/glibc
docker run --rm --volume "$(pwd):/work" -w /work skiasharp-tests-env \
/bin/bash -c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=linux"
```

```bash
# 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/bash -c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=alpine"
```

```powershell
# 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"
```

## CI wiring

Each leg is a bootstrapper job in the `tests` stage that:

- declares `requiredArtifacts: - name: native`, so `output/native/…` is the merged native artifact;
- sets `docker:` to the env image and `target: tests-container` with
`additionalArgs: --nativePlatform=<…>`;
- publishes the TRX via `PublishTestResults@2` (Azure DevOps Tests tab) and the
`testlogs_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](#fonts) below), so those tests skip rather than fail.

The Nano Server leg requires a Windows agent with container support (matching the `ltsc2022` image
base) and pulls the Nano Server .NET SDK image.

## Fonts

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 env images install them: `fontconfig` + DejaVu on both,
plus `font-noto-emoji` on Alpine for emoji coverage. These provide the families the test config
expects (`DefaultFontFamily`, `UnicodeFontFamilies`).
- **non-fontconfig builds** — the NoDependencies variants (`linuxnodeps`, `alpinenodeps`, built with
`skia_use_fontconfig=false`) and **Nano Server** — 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 default `SKFont`) have nothing to bind to.

The Linux test config chooses `UnicodeFontFamilies` per libc — `Symbola` on glibc (from
`ttf-ancient-fonts`) and `Noto Color Emoji` on musl (from `font-noto-emoji`), 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`, or `MatchCharacter`). 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.
43 changes: 29 additions & 14 deletions scripts/azure-templates-jobs-bootstrapper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ jobs:
dockerfile: ${{ parameters.docker }}/Dockerfile
context: ${{ parameters.docker }}
image: skiasharp:skiasharp
buildArguments: --platform linux/amd64 --tag skiasharp ${{ parameters.dockerArgs }}
buildArguments: --tag skiasharp ${{ parameters.dockerArgs }}
enableNetwork: true
- ${{ if ne(parameters.use1ESPipelineTemplates, 'true') }}:
- task: Docker@2
Expand All @@ -387,19 +387,34 @@ jobs:
command: build
buildContext: ${{ parameters.docker }}
dockerfile: ${{ parameters.docker }}/Dockerfile
arguments: --platform linux/amd64 --tag skiasharp ${{ parameters.dockerArgs }}
- bash: |
echo dotnet tool restore > cmd.sh
echo dotnet cake --target=${{ parameters.target }} --verbosity=${{ parameters.verbosity }} --configuration=${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} >> cmd.sh
sed -i 's/--gnArgs=\" \"//' cmd.sh
cat cmd.sh
displayName: Generate the script for the Docker image
condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
- bash: |
docker run --rm --name skiasharp --volume $(pwd):/work skiasharp /bin/bash /work/cmd.sh
displayName: Run the bootstrapper for ${{ parameters.target }} using the Docker image
retryCountOnTaskFailure: ${{ parameters.retryCount }}
condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
arguments: --tag skiasharp ${{ parameters.dockerArgs }}
# Run the target inside the image against the mounted repo. Linux and Windows containers
# differ in shell and mount path, so the run step is branched by agent OS.
- ${{ if ne(parameters.buildAgent.pool.os, 'windows') }}:
- bash: |
echo dotnet tool restore > cmd.sh
echo dotnet cake --target=${{ parameters.target }} --verbosity=${{ parameters.verbosity }} --configuration=${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} >> cmd.sh
sed -i 's/--gnArgs=\" \"//' cmd.sh
cat cmd.sh
displayName: Generate the script for the Docker image
condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
- bash: |
docker run --rm --name skiasharp --volume $(pwd):/work skiasharp /bin/bash /work/cmd.sh
displayName: Run the bootstrapper for ${{ parameters.target }} using the Docker image
retryCountOnTaskFailure: ${{ parameters.retryCount }}
condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
- ${{ if eq(parameters.buildAgent.pool.os, 'windows') }}:
- pwsh: |
'dotnet tool restore' | Set-Content cmd.cmd -Encoding ascii
'dotnet cake --target=${{ parameters.target }} --verbosity=${{ parameters.verbosity }} --configuration=${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }}' | Add-Content cmd.cmd -Encoding ascii
Get-Content cmd.cmd
displayName: Generate the script for the Docker image
condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
- pwsh: |
docker run --rm --name skiasharp --volume "$(Build.SourcesDirectory):C:\work" -w C:\work skiasharp cmd /c C:\work\cmd.cmd
displayName: Run the bootstrapper for ${{ parameters.target }} using the Docker image
retryCountOnTaskFailure: ${{ parameters.retryCount }}
condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))

- pwsh: .\scripts\infra\native\shared\get-free-space.ps1
displayName: Get the volume information after the build
Expand Down
Loading
Loading