Migrate Linux Docker builds to .NET official cross-compilation images#4062
Conversation
Replace Debian 11/13 and Alpine host containers with Azure Linux 3.0 base images using a host/sysroot-decoupled architecture. This eliminates ~100 Component Governance (CG) security alerts from stale OS packages while maintaining backward compatibility (glibc 2.31 for most targets). Key changes: - New azurelinux/glibc image: debootstrap creates Debian sysroots (Bullseye for glibc 2.31, Trixie for loongarch64's glibc 2.38) - New azurelinux/alpine image: apk.static with Alpine 3.21 (was 3.17 EOL) - New azurelinux/bionic image: Azure Linux host + NDK r26b (unchanged) - build.cake reads SYSROOT_PATH env var for cross-compilation paths - Trimmed Alpine packages: musl-dev+linux-headers+fontconfig-dev replaces build-base (eliminates busybox/binutils CVEs) - All images use --foreign debootstrap (no second-stage needed for sysroots) Architecture (same as .NET runtime): Azure Linux 3.0 host → clang-18 → --sysroot=/sysroot → libSkiaSharp.so Security impact: - Alpine 3.17 EOL alerts: ~94 → 0 (bumped to 3.21) - Debian host package alerts: 9 → 0 (Azure Linux replaces Debian) - build-base CVEs: ~50 → 0 (trimmed unnecessary packages) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
All images are Azure Linux now, so the extra nesting is redundant.
New paths: scripts/infra/native/linux/docker/{glibc,alpine,bionic}/
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add gawk and ca-certificates to all tdnf installs (needed by dotnet-install.sh and HTTPS downloads respectively) - Remove dpkg from glibc tdnf install (not available in Azure Linux; debootstrap brings binutils which provides ar) - Replace dpkg-deb extraction with ar+tar (works with binutils from Azure Linux) - Fix Alpine apk.static: write repositories file + use -p for package cache + --update-cache (apk needs persistent repo config) - Write APK_ARCH to /etc/skia-env so it persists across RUN layers All three images (alpine, glibc, bionic) now build successfully on Docker Desktop arm64. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📦 Try the packages from this PRWarning Do not run these scripts without first reviewing the code in this PR. Step 1 — Download the packages bash / macOS / Linux: curl -fsSL https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.sh | bash -s -- 4062PowerShell / Windows: iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4062"Step 2 — Add the local NuGet source dotnet nuget add source ~/.skiasharp/hives/pr-4062/packages --name skiasharp-pr-4062More options
Or download manually from Azure Pipelines — look for the Remove the source when you're done: dotnet nuget remove source skiasharp-pr-4062 |
|
📖 Documentation Preview The documentation for this PR has been deployed and is available at: 🔗 View Staging Site This preview will be updated automatically when you push new commits to this PR. This comment is automatically updated by the documentation staging workflow. |
…ements - glibc: Fix SYSROOT_PATH to differ per layout (debootstrap vs cross-packages) - glibc: Add libc6-dev cross package for riscv64/loongarch64 (required for linking) - glibc: Use -ports pool suffix for loongarch64 packages (gcc-14-cross-ports) - glibc: Add binutils (for readelf post-build check and ar for .deb extraction) - glibc: Fix c++ symlink to use SYSROOT_PATH-relative path - glibc: Relocate fontconfig files for cross-package arch layout - glibc: Switch to HTTPS for Debian package downloads - alpine: Revert to two-step apk.static invocation with -X flags (fixes 'Permission denied') - alpine: Add libstdc++-dev to sysroot (required for C++ compilation) - alpine: Add binutils to host (for readelf) - bionic: Add binutils to host (for readelf) - build.cake: Add -fuse-ld=lld for all variants (no cross-linker available) - build.cake: Add arch-specific include/lib paths for cross-compilation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
8f4c968 to
05cdaf8
Compare
…eign Root cause: debootstrap --foreign only extracts essential/base packages. The --include packages (libstdc++-dev, libgcc-dev, linux-libc-dev) are downloaded to var/cache/apt/archives/ but NOT extracted during first stage. The second stage (which we skip with --foreign) would normally unpack them. Fix: After debootstrap completes, manually extract the --include dev packages from the apt archives cache into the sysroot. Also adds the missing c++/current symlink to the Alpine Dockerfile and improves diagnostic output in the glibc symlink step so future failures are immediately visible in the Docker build log. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
05cdaf8 to
739ff33
Compare
Two issues fixed: 1. C++ headers must come before C headers in -I paths for #include_next to work. When <cmath> does #include_next <math.h>, clang searches paths AFTER the current file's location. If C headers are listed first, they're skipped by #include_next. 2. Replace grep -oP (Perl regex with \K) with portable grep -o + sed for cross-package URL parsing. Add diagnostic output to identify which packages are found/missing during extraction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Azure Linux's LLVM is built with only 'host;AMDGPU;BPF' targets, which means it cannot cross-compile to ARM, AArch64, RISC-V, or LoongArch. Replace tdnf clang/lld/llvm with the official LLVM 18.1.8 release from GitHub which includes all target backends. This fixes the 'No available targets are compatible with triple' errors for all non-x86_64 architectures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The official LLVM 18.1.8 release (built on Ubuntu 18.04) links against libtinfo.so.5, but Azure Linux 3.0 only ships libtinfo.so.6. The ABI is compatible between ncurses 5 and 6 for our usage (basic terminal info), so a symlink suffices. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'lld' is a generic driver that exits with code 1 when invoked directly (it requires being called as ld.lld, ld64.lld, lld-link, or wasm-ld). Use 'ld.lld --version' for the verification step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Debian's debootstrap creates multiarch dirs using 'i386-linux-gnu' but build.cake uses TOOLCHAIN_ARCH='i686-linux-gnu' for the clang target. Create a symlink bridge so include/lib paths resolve correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The linker was failing with 'cannot open crti.o' and 'cannot open crtbeginS.o' because: 1. No --sysroot was passed for glibc builds, so clang couldn't find CRT files in the debootstrap sysroot 2. No explicit -B/-L path to the GCC lib dir where crtbeginS.o lives Changes: - build.cake: Add SYSROOT_ROOT env var support; when set, passes --sysroot to clang. Add GCC_LIB_DIR env var support; when set, adds -B and -L flags for CRT object and libgcc discovery. - glibc Dockerfile: Export SYSROOT_ROOT=/sysroot and GCC_LIB_DIR for the debootstrap path. Export only GCC_LIB_DIR for the cross-package path (no --sysroot needed since explicit -I/-L covers it). - alpine Dockerfile: Export GCC_LIB_DIR for crtbeginS.o discovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Alpine:
- Add 'gcc' package to apk install (provides crtbeginS.o and libgcc.a
which are required for linking; libstdc++-dev only depends on the
runtime libgcc package, not the development files)
Cross-packages (riscv64/loongarch64):
- Add libc6-*-cross package to extraction list (provides runtime libs
like ld-linux-*.so.1 and libc.so.6 referenced by linker scripts)
- Add -B{sysroot}/lib/ to build.cake (CRT objects like crti.o are found
via -B paths, not -L paths; previously only -B for bin/ was set)
- Extend linker script sed to also strip bare '/lib/' prefix (handles
the dynamic linker path /lib/ld-linux-*.so.1 which lacks the
multiarch subdir)
- Relax .deb grep pattern to match any arch suffix (not just _all.deb)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
For debootstrap builds that pass --sysroot, lld automatically prepends the sysroot prefix to absolute paths in linker scripts. Converting them to relative paths would break resolution since the files aren't in the -L search paths. For cross-package builds (riscv64/loongarch64) that don't use --sysroot, the sed is still needed to make paths relative so lld can find them via -L flags. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add lib64/ to -B and -L paths in build.cake for loongarch64 support - Add lib64/ linker script paths to sed fix in glibc Dockerfile - Add MACHINE_ARCH build arg to glibc/alpine Dockerfiles for ARM Mac local testing - LLVM binary download now selects correct arch (x86_64 vs aarch64) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add libstdc++6 and libgcc-s1 runtime packages for cross-package builds (riscv64/loongarch64). Without these, lld falls back to static libstdc++.a which has incompatible .eh_frame sections with lld 18. - Change x86 TOOLCHAIN_ARCH from i686-linux-gnu to i386-linux-gnu to match Debian's multiarch directory naming. This eliminates the need for the i686→i386 symlink workaround entirely. - Use find-based GCC_LIB_DIR discovery (maxdepth 3) for debootstrap builds, since GCC uses i686-linux-gnu internally while multiarch uses i386-linux-gnu. - Remove dead multiarch symlink logic (no longer needed). - Add arch compat symlink for C++ headers when GCC's arch dir name differs from TOOLCHAIN_ARCH. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GCC 10's static libstdc++.a (Bullseye) has .eh_frame sections that lld 18 rejects with 'relocation refers to a symbol in a discarded section' errors. GCC 14 (Trixie) produces lld-compatible archives. Changes: - Move riscv64 from Bullseye to Trixie in CI matrix - Remove riscv64 from cross-package extraction path (now uses debootstrap) - Add unconditional merged-usr symlink pre-creation for debootstrap (required for Trixie, harmless for Bullseye since tar follows symlinks) - Simplify PORTS_SUFFIX (only loongarch64 uses cross-packages now) - Remove riscv64-specific fontconfig version override Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 28 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
scripts/infra/native/linux/docker/glibc-x86/Dockerfile:80
- There is a trailing space after the line-continuation backslash (
-DCOMPILER_RT_BUILD_SANITIZERS=OFF \). In shell this does not escape the newline, so the following-D...line becomes a separate command and the build will fail. Remove the trailing whitespace so the backslash is the last character on the line.
-DCOMPILER_RT_CXX_LIBRARY="libcxx" \
-DCOMPILER_RT_STATIC_CXX_LIBRARY=ON \
-DCOMPILER_RT_BUILD_SANITIZERS=OFF \
-DCOMPILER_RT_BUILD_MEMPROF=OFF \
# The libfuzzer build in LLVM doesn't correctly forward the required CMake properties to the "fuzzed libc++" build
- bionic/Dockerfile: wrap NDK glob with || true so the friendly FATAL check is reachable when no NDK is installed (was masked by set -e + pipefail) - glibc/Dockerfile, glibc-x86/Dockerfile: validate GCC_LIB_DIR discovery so missing crtbeginS.o fails with a clear error instead of producing bad GN paths downstream - build.cake: validate SYSROOT_PATH/SYSROOT_ROOT/GCC_LIB_DIR are set before generating GN args - building-linux.md: switch x64 example to arm64 (avoids IMAGE_ARCH mapping) and document the x64 caveat - debugging-methodology.md: point at all 4 Dockerfiles, not just glibc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use Docker multi-FROM stage aliases to handle the x64→amd64 image-tag
mapping internally:
FROM mcr.../cross-arm AS base-arm
FROM mcr.../cross-arm64 AS base-arm64
FROM mcr.../cross-amd64 AS base-x64
...
FROM base-${BUILD_ARCH}
BuildKit's DAG only pulls the stage actually referenced, so this has no
performance cost over the previous ARG-based approach. Removes the need
for callers (build-local.sh, CI YAML, docs) to know about the x64↔amd64
mapping at all — BUILD_ARCH is the only input.
Also reverts an unintentional skia submodule bump that crept into the
previous "Remove fallback logic" commit via git add -A.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The .deb `libfontconfig1-dev_<ver>_<arch>.deb` on Ubuntu 22.04+ (jammy,
noble) is a transitional empty package containing only copyright/changelog
— no headers. The actual headers moved to `libfontconfig-dev` (without
the "1").
This affected arm (jammy/22.04) and riscv64 (noble/24.04). The Dockerfile
silently succeeded at install but the compile failed later with:
fatal error: 'fontconfig/fontconfig.h' file not found
arm64/x64/x86 use the older bionic (18.04) packages where
`libfontconfig1-dev` still has the headers — those continue to work.
Also add a post-extract sanity check that fails fast if
`<sysroot>/usr/include/fontconfig/fontconfig.h` is missing, so any future
package-rename breakage is caught at image build time, not compile time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The .NET cross-image sysroots ship multiple GCC versions (verified: x86 and x64 both have GCC 6 and 7 installed). Using `find ... | head -1` picks whichever filesystem returns first — not deterministic across environments or future image bumps. Switch to `sort -V | tail -1` to always pick the highest GCC version, matching Ubuntu bionic's default toolchain (gcc-7). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erbatim The libcxx-builder stage in glibc-x86/Dockerfile now diffs from the upstream .NET amd64 Dockerfile by exactly 3 substitutions: - ARG ROOTFS_DIR=/crossrootfs/x64 -> x86 - build-rootfs.sh x64 bionic -> x86 bionic - TARGET_TRIPLE="x86_64-linux-gnu" -> i686-linux-gnu The COPY --from=builder block in stage 2 is also byte-for-byte identical to upstream. Renaming back from libcxx-builder makes the upstream sync diff cleaner for future updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7894122 to
9e60ec4
Compare
Opus findings (all verified):
- Update .agents/skills/security-audit/references/cg-alerts.md to reflect
the new Dockerfile layout — the deleted debian/11 and debian/13 paths
are gone, alpine uses ALPINE_VERSION (not DISTRO_VERSION), and the new
glibc-x86/ Dockerfile is now listed for CG fixes.
- Fix stale sysroot path in documentation/dev/debugging-methodology.md:
/usr/aarch64-linux-gnu/lib → ${SYSROOT_ROOT}/usr/lib/aarch64-linux-gnu
(matches the new .NET cross-image layout exposed via /etc/skia-env).
Codex findings:
- bionic/Dockerfile: NDK_HOME=$(ls ... | head -1) was non-deterministic
if multiple NDK versions are installed. Switch to find + sort -V | tail -1
so we always pick the highest version, matching the same fix already
applied for GCC_LIB_DIR.
Not actioned (verified non-issues):
- xz install (Opus IMP-3): xz IS pre-installed in the .NET cross base
image (verified: xz-5.4.4-3.azl3.x86_64). Don't add explicit dep.
- Classic Docker builder pulling all stages (Codex BLOCKING #1): CI is
using BuildKit (verified in build log #157963: '#5 [base-x64 1/1]'
is BuildKit syntax, classic builder uses 'Step N/N'). Only one base
image is pulled per build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6291b7e to
5476758
Compare
Validation of CI-built nupkgs revealed that linux-bionic-arm64 and
linux-bionic-x64 libSkiaSharp.so / libHarfBuzzSharp.so binaries had
`libc++_shared.so` as DT_NEEDED — a runtime dependency we don't bundle.
Root cause: This PR replaced `-static-libstdc++` with `-stdlib=libc++`
to switch the C++ standard library. For the .NET glibc/musl cross-images
this is fine because they ship libc++ as static-only
(`-DLIBCXX_ENABLE_SHARED=OFF`), so `-lc++` resolves to `libc++.a` and
gets embedded. But the Android NDK ships libc++ as both static and
shared; without `-static-libstdc++` clang's driver picks the shared
`libc++_shared.so`, leaving consumers needing to provide it themselves.
Fix: re-add `-static-libstdc++` to ldflags for the bionic variant only.
For glibc/musl this is a no-op (libc++.so doesn't exist in those sysroots);
for bionic it forces clang to use libc++_static.a.
Verified all other arches in the build:
- linux-{arm,arm64,x64,x86,riscv64,loongarch64}: glibc max at or below
per-arch verifyGlibcMax target
- linux-musl-{arm,arm64,x64,riscv64,loongarch64}: correct musl libc
names, zero GLIBC symbols
- android-{arm,arm64,x64,x86}: clean, libc++ statically linked (via
Skia's GN `ndk=` toolchain handling)
- All packages: only expected DT_NEEDED entries (libc, libm, libdl,
libpthread, librt, libstdc++, libfontconfig where appropriate)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Binary validation of build #4062.61 vs main showed every linux glibc/musl SkiaSharp.so and HarfBuzzSharp.so gained a new `libstdc++.so.6` DT_NEEDED entry compared to main. We do not want to add new runtime dependencies. Root cause: The .NET cross-image libc++.a is built with `-DLIBCXX_CXX_ABI=libstdc++`, so it delegates ABI symbols (operator delete, __cxa_throw, etc.) to libstdc++. The previous `-lstdc++` flag pulled the dynamic libstdc++.so.6 in to satisfy those references. Fix: switch to `-l:libstdc++.a` to pull the static archive instead. libstdc++.a is shipped in every .NET cross-image at GCC_LIB_DIR (we already add `-L$GCC_LIB_DIR` to the link search path), and on modern distros it's built as PIC so it embeds cleanly into our .so. The --whole-archive bracketing is still needed because GN places extra_ldflags BEFORE the object archives, so we must force libstdc++ symbols to be included before downstream references are processed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…me dep" This reverts commit ae71ce5.
…ibc++ build fix) (#4239) Support release-branch targeting in Skia upstream sync (#4239) The daily Skia upstream sync (`auto-skia-sync`) could only ever target `main`/`skiasharp`. Maintenance lines such as `release/4.148.x` still need ongoing bug-fix syncs from upstream Skia, and there was no way to drive one. This teaches the workflow to sync a `release/{major}.{milestone}.x` line and open PRs against it in both mono/SkiaSharp and mono/skia, while keeping the existing `main` behavior unchanged. ~~ Release-line targeting ~~ `skia-sync-detect.sh` becomes the single source of truth for milestone/branch resolution. It derives the target milestone from the trigger (cron current/next/latest, or an explicit `milestone` dispatch input) and only looks for a release line when the target is numerically older than main's milestone (the newest line is always served by `main`). A maintenance line shares the same branch name in both repos (`release/{major}.{milestone}.x`). It fails fast when the matching mono/skia release branch is missing — those branches are owned by the separate release process, not this sync — and hard-fails rather than guessing when a milestone matches multiple release branches. `skia-sync-align-submodule.sh` points `externals/skia` at the SHA the base branch records before the agent runs, since the agent checks out the workflow branch (usually `main`) rather than the target line. ~~ Extract detection into committed scripts ~~ The milestone/branch logic previously lived as two large, duplicated inline bash blocks in the workflow YAML. It is now extracted into committed, shellcheck-clean scripts, collapsing the `run:` blocks to ~1 and ~5 lines. The `pre_activation` gate runs the detector (`--gate`) and writes job outputs; the agent's align step re-runs the same detector without `--gate` and sources its output, because the agent job only `needs: activation` and cannot read `pre_activation`'s outputs in gh-aw. The extraction also folds in two review fixes: numeric `-lt` comparison (was `!=`) and a hard-fail on multi-match (was a silent `head -1`). ~~ Resolve mode from cron in the workflow ~~ The cron→mode mapping (7 AM → current, 5 PM → latest, else → next) was the only workflow-coupled knowledge left in the detector — it hardcoded the literal cron strings and read INPUT_MODE/INPUT_MILESTONE/SCHEDULE env vars. That mapping now lives in the workflow as a single GitHub Actions expression, next to where the crons are declared, and the detector takes real `--mode`/`--milestone` arguments. The script no longer knows anything about cron, so it is self-contained and unit-testable (`--mode latest` instead of faking `SCHEDULE='0 17 * * *'`). `mode` is staged into an env var rather than interpolated straight into `run:`, so the free-form `milestone` dispatch input cannot inject shell. ~~ Run the agent on Claude Opus 4.7 ~~ The engine is pinned to `claude-opus-4.7` (was the gh-aw default Sonnet). On this multi-repo, submodule-heavy task Opus produced materially cleaner runs — no failed commands, retries, or self-introduced build hacks. Opus 4.6 and 4.7 share the same credit multiplier, so this is a free upgrade to the latest snapshot. ~~ Fix the Linux native build (libc++) ~~ The sync's native build started failing because #4062 ("Migrate Linux Docker builds to .NET official cross-compilation images") moved the Linux build into the `mcr.microsoft.com/dotnet-buildtools/prereqs` cross images, which ship Clang + libc++. `native/linux/build.cake` now hardcodes `-stdlib=libc++` in its `extra_cflags`/`extra_ldflags`. The sync builds on a bare `ubuntu-latest` host — not inside that image — so libc++ was absent and `dotnet cake --target=externals-linux` failed to link. The agent could not self-remediate: there is no apt/sudo inside the AWF chroot and the firewall blocks OS package mirrors, so it got stuck trying to install it. The pre-agent "Install native build dependencies" step now installs `libc++-dev libc++abi-dev` on the host (visible to the chroot via the shared filesystem). The agent prompt is also hardened to forbid patching native build files or compiler/linker flags and attempting package installs; a genuinely missing host dependency must STOP and be reported for a human rather than worked around. Validated end-to-end against `release/4.148.x` with real upstream, native build, and tests: the skip, idempotent-rerun, and missing-upstream paths behave correctly, and a full sync produced a clean ~15 min native build with no build.cake hacks, 5544 tests passing (0 fail / 172 skip), and bug-fix-only PRs (cgmanifest hash + submodule pointer) on `release/4.148.x` in both repos. The detector arg interface and all three scripts were re-verified with stubs and shellcheck. The `release-branch` skill changes are intentionally out of scope and land separately. Co-authored-by: Matthew Leibowitz <mattleibow@live.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate Linux Docker builds to .NET official cross-compilation images
Motivation
Replace custom Docker infrastructure (debootstrap sysroots, manual LLVM downloads, toolchain construction) with thin layers on top of .NET's official cross-compilation images. This delegates toolchain and sysroot maintenance to the .NET infrastructure team, reduces Dockerfile complexity significantly, and ensures we stay current with upstream tooling.
Architecture
Docker Images (4 Dockerfiles)
glibc/cross-<arch>glibc-x86/cross-amd64+crossdeps-builder-amd64alpine/cross-<arch>-muslbionic/android-amd64Each takes only
BUILD_ARCHas input. All arch-specific config is derived internally via case statements.Sysroot libc Versions — Before vs After
The libc version in the sysroot determines the minimum distro that can run the built artifact.
verifyGlibcMaxin CI will fail the build if the base image bumps its sysroot unexpectedly.glibc targets:
musl/Alpine targets (no verifyGlibcMax — musl is ABI-stable across 1.x):
Android (bionic):
Key Design Decisions
glibc-x86/) that builds libc++ in a multi-stage build because the .NET team does not ship libc++ for x86.-stdlib=libc++replaces-static-libstdc++(base images ship libc++ in sysroot, not libstdc++)/etc/skia-envpattern for arch-dependent vars (Docker ENV cannot do case logic)IMAGE_ARCHbuild-arg for x64→amd64 mapping (.NET uses "amd64" in image tags)SYSROOT_ROOT/SYSROOT_PATHused by build.cake for all variants (removed hardcoded/alpine)#include_nextcorrectnessverifyGlibcMax— CI fails if the built .so exceeds the expected glibc versionFiles Changed
scripts/infra/native/linux/docker/glibc/Dockerfilescripts/infra/native/linux/docker/glibc-x86/Dockerfilescripts/infra/native/linux/docker/alpine/Dockerfilescripts/infra/native/linux/docker/bionic/Dockerfilescripts/infra/native/linux/docker/*/build-local.shscripts/infra/native/linux/docker/_clang-cross-common.shscripts/infra/native/linux/docker/debian/11/scripts/infra/native/linux/docker/debian/13/scripts/azure-templates-stages-native-linux.ymlnative/linux/build.cake-stdlib=libc++replaces-static-libstdc++native/linux-clang-cross/build.cake/alpineTesting
Runtime Dependencies
Switching from
-stdlib=libstdc++ -static-libstdc++to-stdlib=libc++(required for Skia's C++20 on the bionic-sysroot images that only ship GCC 6/7) means the libstdc++ ABI symbols that libc++ delegates to (operator delete,__cxa_throw, exception type info, etc.) are now resolved via a runtimelibstdc++.so.6reference instead of being statically embedded.This adds
libstdc++.so.6toDT_NEEDEDon every glibc and musl SkiaSharp/HarfBuzzSharp.so. Thelibc++.afrom the .NET cross-images is built with-DLIBCXX_CXX_ABI=libstdc++and cannot be linked alongsidelibstdc++.astatically (both archives definestd::logic_error,std::runtime_error, etc., causing duplicate-symbol link errors).This adds zero new requirements for end users —
libstdc++.so.6is already a hard dependency of the .NET Linux runtime itself, on every glibc and musl RID. Verified directly by inspectingMicrosoft.NETCore.App 9.0.0runtime payloads:libstdc++.so.6libcoreclr.so(the .NET runtime)libclrjit.so(JIT compiler)libclrgc.so(garbage collector)libhostfxr.so/libhostpolicy.so(host)libmscordaccore.so/libmscordbi.so(debugger)If
dotnetruns,libstdc++.so.6is already loaded. SkiaSharp requiring it imposes no additional baseline.libSkiaSharp.so(PR #4062)The
.NoDependenciespackage dropslibfontconfig.so.1; otherwise the dependency surface is identical.Eliminating the
libstdc++.so.6dep would require building libc++/libc++abi from source for every glibc/musl arch (extending theglibc-x86/Dockerfilemulti-stage pattern to all other Dockerfiles, ~250 LOC + ~15 min CI per arch) — explicitly chosen not to do this in this PR because it adds no real value over the .NET runtime baseline.