Skip to content

Migrate Linux Docker builds to .NET official cross-compilation images#4062

Merged
mattleibow merged 49 commits into
mainfrom
mattleibow/animated-carnival
May 27, 2026
Merged

Migrate Linux Docker builds to .NET official cross-compilation images#4062
mattleibow merged 49 commits into
mainfrom
mattleibow/animated-carnival

Conversation

@mattleibow

@mattleibow mattleibow commented May 24, 2026

Copy link
Copy Markdown
Contributor

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

mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-net10.0-cross-<arch>
  └── Clang 20 + lld (built from source by .NET team)
  └── Complete sysroot at /crossrootfs/<arch>/ with libc++ (static)
  └── We add: .NET SDK (Cake), fontconfig-dev headers, /etc/skia-env toolchain vars

Docker Images (4 Dockerfiles)

Image Purpose Base Image
glibc/ Most glibc targets (arm, arm64, x64, riscv64, loongarch64) cross-<arch>
glibc-x86/ x86 only (self-contained — builds libc++ in stage 1) cross-amd64 + crossdeps-builder-amd64
alpine/ All musl targets (arm, arm64, x64, riscv64, loongarch64) cross-<arch>-musl
bionic/ Android (arm64, x64) android-amd64

Each takes only BUILD_ARCH as 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. verifyGlibcMax in CI will fail the build if the base image bumps its sysroot unexpectedly.

glibc targets:

Arch Before After Minimum supported distros
arm64 2.31 2.27 Ubuntu 18.04 (2.27) / Debian 10 buster (2.28) / RHEL 8 (2.28) / Fedora 28 (2.27)
x64 2.31 2.27 Ubuntu 18.04 (2.27) / Debian 10 buster (2.28) / RHEL 8 (2.28) / Fedora 28 (2.27)
x86 2.31 2.27 Ubuntu 18.04 (2.27) / Debian 10 buster (2.28) / RHEL 8 (2.28) / Fedora 28 (2.27)
arm 2.31 2.35 Ubuntu 22.04 (2.35) / Debian 12 bookworm (2.36) / RHEL 10 (2.39) / Fedora 36 (2.35)
riscv64 2.31 2.39 Ubuntu 24.04 (2.39) / Debian 13 trixie (2.41) / Fedora 40 (2.39)
loongarch64 2.38 2.42 Debian sid (2.42) / Loongnix 2025 (2.42)

↓ = lower minimum (broader compat), ↑ = higher minimum (narrower compat)

musl/Alpine targets (no verifyGlibcMax — musl is ABI-stable across 1.x):

Arch Sysroot Alpine musl Min Alpine
arm64 3.17 1.2.3 3.17+
arm 3.17 1.2.3 3.17+
x64 3.17 1.2.3 3.17+
riscv64 3.21 1.2.5 3.21+
loongarch64 3.24 alpha 1.2.6 3.24+ (edge)

Android (bionic):

Arch NDK API Level Min Android
arm64 27.2.12479018 29 Android 10+
x64 27.2.12479018 29 Android 10+

Key Design Decisions

  • Single Dockerfile per variant — loongarch64 consolidated into glibc (was a separate debian/13 image). x86 is the one exception: it has its own self-contained Dockerfile (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++)
  • Dynamic NDK discovery (bionic) — no hardcoded version; full paths for CC/CXX/AR/LD
  • SHA-256 pinned fontconfig .debs (glibc) — verified on download, fail-fast on mismatch
  • /etc/skia-env pattern for arch-dependent vars (Docker ENV cannot do case logic)
  • IMAGE_ARCH build-arg for x64→amd64 mapping (.NET uses "amd64" in image tags)
  • No symlinks, no workarounds — clean, minimal layers on top of official images
  • SYSROOT_ROOT / SYSROOT_PATH used by build.cake for all variants (removed hardcoded /alpine)
  • Include path ordering — C++ headers before C headers for #include_next correctness
  • Per-arch verifyGlibcMax — CI fails if the built .so exceeds the expected glibc version

Files Changed

File Change
scripts/infra/native/linux/docker/glibc/Dockerfile New — .NET cross base + SHA-pinned fontconfig (138 lines)
scripts/infra/native/linux/docker/glibc-x86/Dockerfile New — self-contained x86 Dockerfile, builds libc++ via multi-stage (mirrors upstream .NET amd64 Dockerfile with 3 substitutions)
scripts/infra/native/linux/docker/alpine/Dockerfile Rewritten — .NET musl cross base + apk fontconfig (93 lines)
scripts/infra/native/linux/docker/bionic/Dockerfile Rewritten — .NET android base + dynamic NDK (67 lines)
scripts/infra/native/linux/docker/*/build-local.sh Simplified — pass BUILD_ARCH, map x64→amd64 for IMAGE_ARCH
scripts/infra/native/linux/docker/_clang-cross-common.sh Deleted — logic moved into Dockerfiles
scripts/infra/native/linux/docker/debian/11/ Deleted — replaced by glibc/
scripts/infra/native/linux/docker/debian/13/ Deleted — loongarch64 absorbed into glibc/
scripts/azure-templates-stages-native-linux.yml Updated matrix, IMAGE_ARCH for x64, per-arch verifyGlibcMax
native/linux/build.cake -stdlib=libc++ replaces -static-libstdc++
native/linux-clang-cross/build.cake SYSROOT_ROOT/SYSROOT_PATH/GCC_LIB_DIR, include path ordering, removed hardcoded /alpine

Testing

  • glibc: arm64, x64, x86, arm, riscv64, loongarch64 — all pass locally
  • alpine: arm64, arm — pass locally
  • bionic: arm64 — passes locally
  • CI green (ADO pipeline)

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 runtime libstdc++.so.6 reference instead of being statically embedded.

This adds libstdc++.so.6 to DT_NEEDED on every glibc and musl SkiaSharp/HarfBuzzSharp .so. The libc++.a from the .NET cross-images is built with -DLIBCXX_CXX_ABI=libstdc++ and cannot be linked alongside libstdc++.a statically (both archives define std::logic_error, std::runtime_error, etc., causing duplicate-symbol link errors).

This adds zero new requirements for end userslibstdc++.so.6 is already a hard dependency of the .NET Linux runtime itself, on every glibc and musl RID. Verified directly by inspecting Microsoft.NETCore.App 9.0.0 runtime payloads:

.NET native library NEEDED libstdc++.so.6
libcoreclr.so (the .NET runtime)
libclrjit.so (JIT compiler)
libclrgc.so (garbage collector)
libhostfxr.so / libhostpolicy.so (host)
libmscordaccore.so / libmscordbi.so (debugger)

If dotnet runs, libstdc++.so.6 is already loaded. SkiaSharp requiring it imposes no additional baseline.

RID DT_NEEDED for libSkiaSharp.so (PR #4062) GLIBC max
linux-arm ld-linux-armhf, libc, libfontconfig, libm, libstdc++ 2.35
linux-arm64 libc, libdl, libfontconfig, libm, libpthread, librt, libstdc++ 2.27
linux-x64 ld-linux-x86-64, libc, libdl, libfontconfig, libm, libpthread, librt, libstdc++ 2.27
linux-x86 ld-linux.so.2, libc, libdl, libfontconfig, libm, libpthread, librt, libstdc++ 2.27
linux-riscv64 ld-linux-riscv64, libc, libfontconfig, libm, libstdc++ 2.38
linux-loongarch64 ld-linux-loongarch, libc, libfontconfig, libm, libstdc++ 2.38
linux-musl-arm libc.musl-armv7, libfontconfig, libstdc++ — (musl)
linux-musl-arm64 libc.musl-aarch64, libfontconfig, libstdc++ — (musl)
linux-musl-x64 libc.musl-x86_64, libfontconfig, libstdc++ — (musl)
linux-musl-riscv64 libc.musl-riscv64, libfontconfig, libstdc++ — (musl)
linux-musl-loongarch64 libc.musl-loongarch64, libfontconfig, libstdc++ — (musl)
linux-bionic-arm64 libc, libdl, libm — (Android NDK static libc++)
linux-bionic-x64 libc, libdl, libm — (Android NDK static libc++)

The .NoDependencies package drops libfontconfig.so.1; otherwise the dependency surface is identical.

Eliminating the libstdc++.so.6 dep would require building libc++/libc++abi from source for every glibc/musl arch (extending the glibc-x86/Dockerfile multi-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.

mattleibow and others added 3 commits May 25, 2026 00:05
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>
@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

📦 Try the packages from this PR

Warning

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 -- 4062

PowerShell / 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-4062
More options
Option Description
--successful-only / -SuccessfulOnly Only use successful builds
--force / -Force Overwrite previously downloaded packages
--list / -List List available artifacts without downloading
--build-id ID / -BuildId ID Download from a specific build

Or download manually from Azure Pipelines — look for the nuget artifact on the build for this PR.

Remove the source when you're done:

dotnet nuget remove source skiasharp-pr-4062

@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Site
🔗 View Staging Docs
🔗 View Staging Gallery (Blazor)
🔗 View Staging Gallery (Uno Platform)
🔗 View Staging SkiaFiddle

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>
@mattleibow
mattleibow force-pushed the mattleibow/animated-carnival branch 2 times, most recently from 8f4c968 to 05cdaf8 Compare May 25, 2026 05:01
…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>
@mattleibow
mattleibow force-pushed the mattleibow/animated-carnival branch from 05cdaf8 to 739ff33 Compare May 25, 2026 05:03
mattleibow and others added 5 commits May 25, 2026 07:21
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>
mattleibow and others added 2 commits May 25, 2026 10:02
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread scripts/infra/native/linux/docker/glibc-x86/Dockerfile
Comment thread scripts/infra/native/linux/docker/bionic/Dockerfile Outdated
Comment thread documentation/dev/building-linux.md
Comment thread documentation/dev/debugging-methodology.md Outdated
Comment thread native/linux-clang-cross/build.cake
Comment thread scripts/infra/native/linux/docker/glibc/Dockerfile Outdated
Comment thread scripts/infra/native/linux/docker/glibc-x86/Dockerfile Outdated
mattleibow and others added 2 commits May 26, 2026 18:14
- 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 27 changed files in this pull request and generated 3 comments.

Comment thread scripts/infra/native/linux/docker/glibc-x86/Dockerfile
Comment thread scripts/infra/native/linux/docker/glibc/Dockerfile
Comment thread scripts/infra/native/linux/docker/glibc-x86/Dockerfile
mattleibow and others added 4 commits May 26, 2026 19:52
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>
mattleibow and others added 2 commits May 26, 2026 23:54
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>
@mattleibow
mattleibow force-pushed the mattleibow/animated-carnival branch from 6291b7e to 5476758 Compare May 26, 2026 23:24
mattleibow and others added 3 commits May 27, 2026 05:29
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>
@mattleibow
mattleibow merged commit 0967714 into main May 27, 2026
6 checks passed
@mattleibow
mattleibow deleted the mattleibow/animated-carnival branch May 27, 2026 23:59
@mattleibow mattleibow added this to the 4.148.0-rc.1 milestone Jun 15, 2026
mattleibow added a commit that referenced this pull request Jun 25, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants