diff --git a/.github/workflows/nuget-static-graphics.yml b/.github/workflows/nuget-static-graphics.yml index c2d1e33..1609538 100644 --- a/.github/workflows/nuget-static-graphics.yml +++ b/.github/workflows/nuget-static-graphics.yml @@ -20,6 +20,7 @@ on: env: SKIASHARP_VERSION: "3.119.2" ANGLE_BRANCH: "7151" + DOTNET_ALPINE_IMAGE: mcr.microsoft.com/dotnet/sdk:10.0-alpine jobs: build-linux: @@ -33,22 +34,59 @@ jobs: - rid: linux-arm64 target_cpu: arm64 runner: ubuntu-24.04-arm + - rid: linux-musl-x64 + target_cpu: x64 + runner: ubuntu-latest + - rid: linux-musl-arm64 + target_cpu: arm64 + runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - name: Install native build dependencies + if: ${{ !startsWith(matrix.rid, 'linux-musl-') }} run: | sudo apt-get update sudo apt-get install -y clang lld llvm g++ ninja-build pkg-config libfontconfig1-dev libglib2.0-dev libgl1-mesa-dev libpci-dev libx11-dev libxcb1-dev libxext-dev - name: Build Linux static graphics libraries + if: ${{ !startsWith(matrix.rid, 'linux-musl-') }} env: RID: ${{ matrix.rid }} TARGET_CPU: ${{ matrix.target_cpu }} LLVM_AR: llvm-ar run: scripts/build-linux-static-graphics.sh all + - name: Build Linux musl static graphics libraries + if: ${{ startsWith(matrix.rid, 'linux-musl-') }} + run: | + docker run --rm \ + -v "$PWD:/workspace" \ + -w /workspace \ + -e RID="${{ matrix.rid }}" \ + -e TARGET_CPU="${{ matrix.target_cpu }}" \ + -e OUTPUT_DIR="/workspace/External/NativeStatic/${{ matrix.rid }}/native" \ + -e SKIASHARP_VERSION="$SKIASHARP_VERSION" \ + -e ANGLE_BRANCH="$ANGLE_BRANCH" \ + -e LLVM_AR=llvm-ar \ + -e DEPOT_TOOLS_BOOTSTRAP_PYTHON3=0 \ + -e VPYTHON_BYPASS="manually managed python not supported by chrome operations" \ + "$DOTNET_ALPINE_IMAGE" \ + sh -lc ' + set -e + apk add --no-cache bash ca-certificates git python3 py3-httplib2 py3-pysocks py3-six clang compiler-rt lld llvm g++ ninja pkgconf fontconfig-dev glib-dev mesa-dev libx11-dev libxcb-dev libxext-dev pciutils-dev linux-headers perl curl tar xz patch gcompat libc6-compat + python3 - <<'"'"'PY'"'"' + import pathlib + import httplib2 + + path = pathlib.Path(httplib2.__file__).with_name("socks.py") + if not path.exists(): + path.write_text("from socks import *\n") + PY + bash scripts/build-linux-static-graphics.sh all + ' + - name: Upload Linux static archives uses: actions/upload-artifact@v4 with: @@ -127,7 +165,7 @@ jobs: runner: macos-14 - rid: osx-arm64 target_cpu: arm64 - runner: macos-latest + runner: macos-15 runs-on: ${{ matrix.runner }} env: RID: ${{ matrix.rid }} @@ -140,6 +178,19 @@ jobs: - name: Install native build dependencies run: brew install ninja + - name: Install Xcode Metal toolchain + run: | + set -euo pipefail + cat > /tmp/staticlink-metal-check.metal <<'EOF' + #include + using namespace metal; + kernel void staticlink_metal_check(device float* values [[buffer(0)]], uint id [[thread_position_in_grid]]) { values[id] = 0; } + EOF + if ! xcrun metal -c /tmp/staticlink-metal-check.metal -o /tmp/staticlink-metal-check.air >/dev/null 2>&1; then + sudo xcodebuild -downloadComponent MetalToolchain + fi + xcrun metal -c /tmp/staticlink-metal-check.metal -o /tmp/staticlink-metal-check.air >/dev/null + - name: Build macOS static graphics libraries run: bash ./scripts/build-macos-static-graphics.sh all @@ -162,13 +213,13 @@ jobs: runner: macos-14 avalonia_version: 11.3.14 - rid: osx-arm64 - runner: macos-latest + runner: macos-15 avalonia_version: 11.3.14 - rid: osx-x64 runner: macos-14 avalonia_version: 12.0.3 - rid: osx-arm64 - runner: macos-latest + runner: macos-15 avalonia_version: 12.0.3 runs-on: ${{ matrix.runner }} env: @@ -234,7 +285,7 @@ jobs: - name: Move static archives into RID directories run: | - for rid in win-x64 win-x86 win-arm64 linux-x64 linux-arm64 osx-x64 osx-arm64; do + for rid in win-x64 win-x86 win-arm64 linux-x64 linux-arm64 linux-musl-x64 linux-musl-arm64 osx-x64 osx-arm64; do mkdir -p "External/NativeStatic/$rid" mv "External/NativeStatic/native-static-$rid"/* "External/NativeStatic/$rid/" rmdir "External/NativeStatic/native-static-$rid" @@ -246,7 +297,7 @@ jobs: - name: Verify static archives run: | - for rid in linux-x64 linux-arm64 osx-x64 osx-arm64; do + for rid in linux-x64 linux-arm64 linux-musl-x64 linux-musl-arm64 osx-x64 osx-arm64; do test -f External/NativeStatic/$rid/native/libskia.a test -f External/NativeStatic/$rid/native/libSkiaSharp.a test -f External/NativeStatic/$rid/native/libHarfBuzzSharp.a @@ -261,6 +312,7 @@ jobs: test -f External/NativeStatic/$rid/native/libANGLE_static.lib test -f External/NativeStatic/$rid/native/libGLESv2_static.lib done + test -f External/NativeStatic/win-x86/native/skia_x86_stdcall_thunks.lib - name: Pack NuGet package run: | @@ -321,12 +373,16 @@ jobs: runner: ubuntu-latest - rid: linux-arm64 runner: ubuntu-24.04-arm + - rid: linux-musl-x64 + runner: ubuntu-latest + - rid: linux-musl-arm64 + runner: ubuntu-24.04-arm - rid: osx-x64 runner: macos-14 avalonia_version: 11.3.14 native_version: 11.3.14.1 - rid: osx-arm64 - runner: macos-latest + runner: macos-15 avalonia_version: 11.3.14 native_version: 11.3.14.1 - rid: osx-x64 @@ -334,7 +390,7 @@ jobs: avalonia_version: 12.0.3 native_version: 12.0.3.1 - rid: osx-arm64 - runner: macos-latest + runner: macos-15 avalonia_version: 12.0.3 native_version: 12.0.3.1 runs-on: ${{ matrix.runner }} @@ -348,7 +404,7 @@ jobs: arch: ${{ matrix.msvc_arch }} - name: Install Linux publish dependencies - if: ${{ startsWith(matrix.rid, 'linux-') }} + if: ${{ startsWith(matrix.rid, 'linux-') && !startsWith(matrix.rid, 'linux-musl-') }} run: | sudo apt-get update sudo apt-get install -y clang lld zlib1g-dev libfontconfig1-dev @@ -365,6 +421,7 @@ jobs: path: artifacts/nuget - name: Publish Avalonia single-file smoke app + if: ${{ !startsWith(matrix.rid, 'linux-musl-') }} shell: pwsh run: | $ErrorActionPreference = "Stop" @@ -391,6 +448,23 @@ jobs: /p:StripSymbols=true ` /p:PublishDir="$PWD/artifacts/smoke-${{ matrix.rid }}/" + - name: Publish Avalonia musl smoke app + if: ${{ startsWith(matrix.rid, 'linux-musl-') }} + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $package = Get-ChildItem -Path "artifacts/nuget" -Filter "StaticLink.Avalonia.*.nupkg" | Where-Object { $_.Name -notlike "StaticLink.Avalonia.Native.*" } | Select-Object -First 1 + if (-not $package) { throw "StaticLink NuGet package was not downloaded." } + if ($package.BaseName -notmatch '^StaticLink\.Avalonia\.(.+)$') { throw "Unexpected package name: $($package.Name)" } + $version = $Matches[1] + $projectDir = Join-Path $PWD "artifacts/avalonia-smoke-src" + pwsh ./scripts/create-avalonia-smoke.ps1 -ProjectDir $projectDir -NuGetSource "/workspace/artifacts/nuget" -StaticLinkVersion $version -AvaloniaVersion "11.3.14" + docker run --rm ` + -v "${{ github.workspace }}:/workspace" ` + -w /workspace ` + "${{ env.DOTNET_ALPINE_IMAGE }}" ` + sh -c "apk add --no-cache clang lld build-base zlib-dev fontconfig-dev && dotnet publish /workspace/artifacts/avalonia-smoke-src/AvaloniaStaticLinkSmoke.csproj -c Release -r ${{ matrix.rid }} --configfile /workspace/artifacts/avalonia-smoke-src/NuGet.config /p:PublishAot=true /p:SelfContained=true /p:PublishSingleFile=true /p:StripSymbols=true /p:PublishDir=/workspace/artifacts/smoke-${{ matrix.rid }}/" + - name: Verify no dynamic graphics libraries remain shell: pwsh run: | @@ -404,6 +478,28 @@ jobs: } Get-ChildItem -Path $publishDir -Recurse -File | ForEach-Object { $_.FullName } + - name: Verify musl ELF dependencies + if: ${{ startsWith(matrix.rid, 'linux-musl-') }} + shell: bash + run: | + set -euo pipefail + docker run --rm \ + -v "${{ github.workspace }}:/workspace" \ + -w /workspace \ + alpine:latest \ + sh -c 'apk add --no-cache binutils pax-utils && app="artifacts/smoke-${{ matrix.rid }}/AvaloniaStaticLinkSmoke" && readelf -l "$app" | grep "Requesting program interpreter: /lib/ld-musl-" && scanelf -n "$app" && ! scanelf -n "$app" | grep -E "ld-linux|ld64.so"' + + - name: Run Linux musl Avalonia smoke app + if: ${{ startsWith(matrix.rid, 'linux-musl-') }} + shell: bash + run: | + set -euo pipefail + docker run --rm \ + -v "${{ github.workspace }}:/workspace" \ + -w /workspace/artifacts/smoke-${{ matrix.rid }} \ + alpine:latest \ + sh -c 'apk add --no-cache xvfb-run libstdc++ libgcc fontconfig freetype ttf-dejavu libx11 libxcb libxext libxrender libxi libxrandr libxcursor libxfixes libxinerama libice libsm mesa-gl mesa-egl && fc-cache -f && fc-match sans && chmod +x ./AvaloniaStaticLinkSmoke && timeout 15s xvfb-run -a ./AvaloniaStaticLinkSmoke || code=$?; if [ "${code:-0}" = 124 ] || [ "${code:-0}" = 143 ]; then echo "Avalonia musl smoke app stayed alive for 15 seconds."; exit 0; fi; exit "${code:-0}"' + - name: Run macOS Avalonia smoke app if: ${{ startsWith(matrix.rid, 'osx-') }} shell: bash diff --git a/NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj b/NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj index 06aef8c..21eb745 100644 --- a/NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj +++ b/NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj @@ -1,6 +1,6 @@ - net10.0 + net8.0 true true false @@ -20,7 +20,7 @@ - + diff --git a/NuGet/AvaloniaNative/lib/net10.0/_._ b/NuGet/AvaloniaNative/lib/net8.0/_._ similarity index 100% rename from NuGet/AvaloniaNative/lib/net10.0/_._ rename to NuGet/AvaloniaNative/lib/net8.0/_._ diff --git a/NuGet/StaticGraphics/StaticLink.Avalonia.csproj b/NuGet/StaticGraphics/StaticLink.Avalonia.csproj index bfc2dd4..86394a3 100644 --- a/NuGet/StaticGraphics/StaticLink.Avalonia.csproj +++ b/NuGet/StaticGraphics/StaticLink.Avalonia.csproj @@ -1,6 +1,6 @@ - net10.0 + net8.0 true true false @@ -24,7 +24,7 @@ - + @@ -32,6 +32,8 @@ + + diff --git a/NuGet/StaticGraphics/buildTransitive/StaticLink.Avalonia.targets b/NuGet/StaticGraphics/buildTransitive/StaticLink.Avalonia.targets index ed03adc..59cefce 100644 --- a/NuGet/StaticGraphics/buildTransitive/StaticLink.Avalonia.targets +++ b/NuGet/StaticGraphics/buildTransitive/StaticLink.Avalonia.targets @@ -5,10 +5,12 @@ <_StaticLinkAvaloniaRid Condition="'$(RuntimeIdentifier)' == 'win-arm64'">win-arm64 <_StaticLinkAvaloniaRid Condition="'$(RuntimeIdentifier)' == 'linux-x64'">linux-x64 <_StaticLinkAvaloniaRid Condition="'$(RuntimeIdentifier)' == 'linux-arm64'">linux-arm64 + <_StaticLinkAvaloniaRid Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64'">linux-musl-x64 + <_StaticLinkAvaloniaRid Condition="'$(RuntimeIdentifier)' == 'linux-musl-arm64'">linux-musl-arm64 <_StaticLinkAvaloniaRid Condition="'$(RuntimeIdentifier)' == 'osx-x64'">osx-x64 <_StaticLinkAvaloniaRid Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">osx-arm64 <_StaticLinkAvaloniaNativeRoot Condition="'$(_StaticLinkAvaloniaRid)' != ''">$(StaticLinkAvaloniaRoot)$(_StaticLinkAvaloniaRid)/native - lld + lld @@ -17,6 +19,7 @@ + @@ -31,7 +34,51 @@ - + + + <_StaticLinkAvaloniaDirectPInvokeList>$(NativeIntermediateOutputPath)staticlink-avalonia.directpinvokes.txt + + + <_StaticLinkAvaloniaDirectPInvoke Include="libHarfBuzzSharp" Condition="Exists('$(_StaticLinkAvaloniaNativeRoot)/libHarfBuzzSharp.lib')" /> + <_StaticLinkAvaloniaDirectPInvoke Include="libHarfBuzzSharp.dll" Condition="Exists('$(_StaticLinkAvaloniaNativeRoot)/libHarfBuzzSharp.lib')" /> + <_StaticLinkAvaloniaDirectPInvoke Include="libSkiaSharp" Condition="Exists('$(_StaticLinkAvaloniaNativeRoot)/SkiaSharp.lib')" /> + <_StaticLinkAvaloniaDirectPInvoke Include="libSkiaSharp.dll" Condition="Exists('$(_StaticLinkAvaloniaNativeRoot)/SkiaSharp.lib')" /> + <_StaticLinkAvaloniaDirectPInvoke Include="libGLESv2" Condition="Exists('$(_StaticLinkAvaloniaNativeRoot)/libGLESv2_static.lib')" /> + <_StaticLinkAvaloniaDirectPInvoke Include="libGLESv2.dll" Condition="Exists('$(_StaticLinkAvaloniaNativeRoot)/libGLESv2_static.lib')" /> + <_StaticLinkAvaloniaDirectPInvoke Include="av_libglesv2" Condition="Exists('$(_StaticLinkAvaloniaNativeRoot)/libGLESv2_static.lib')" /> + <_StaticLinkAvaloniaDirectPInvoke Include="av_libglesv2.dll" Condition="Exists('$(_StaticLinkAvaloniaNativeRoot)/libGLESv2_static.lib')" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -44,7 +91,7 @@ - + diff --git a/NuGet/StaticGraphics/lib/net10.0/_._ b/NuGet/StaticGraphics/lib/net8.0/_._ similarity index 100% rename from NuGet/StaticGraphics/lib/net10.0/_._ rename to NuGet/StaticGraphics/lib/net8.0/_._ diff --git a/scripts/build-linux-static-graphics.sh b/scripts/build-linux-static-graphics.sh index d05e333..9a621ca 100755 --- a/scripts/build-linux-static-graphics.sh +++ b/scripts/build-linux-static-graphics.sh @@ -37,6 +37,10 @@ require_cmd() { fi } +is_musl_rid() { + [[ "$RID" == linux-musl-* ]] +} + ensure_tools() { require_cmd git require_cmd python3 @@ -50,12 +54,84 @@ ensure_tools() { ensure_depot_tools() { local depot_dir="$WORK_DIR/depot_tools" + local python_bin_dir + python_bin_dir="$(dirname "$(command -v python3)")" if [[ ! -d "$depot_dir/.git" ]]; then git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git "$depot_dir" else git -C "$depot_dir" pull --ff-only fi - export PATH="$depot_dir:$PATH" + if is_musl_rid; then + initialize_depot_tools_system_python "$depot_dir" + patch_depot_tools_python_deps "$depot_dir" + fi + export PATH="$python_bin_dir:$depot_dir:$PATH" +} + +initialize_depot_tools_system_python() { + local depot_dir="$1" + local python_bin_dir + python_bin_dir="$(dirname "$(command -v python3)")" + + if [[ -d "$depot_dir" ]]; then + python3 - "$depot_dir" "$python_bin_dir" <<'PY' +import os +import pathlib +import sys + +depot_dir = pathlib.Path(sys.argv[1]).resolve() +python_bin_dir = pathlib.Path(sys.argv[2]).resolve() +marker = depot_dir / "python3_bin_reldir.txt" +marker.write_text(os.path.relpath(python_bin_dir, depot_dir) + "\n") +PY + fi +} + +patch_depot_tools_python_deps() { + local depot_dir="$1" + local gsutil_dir="$depot_dir/external_bin/gsutil/gsutil_4.68/gsutil" + local gsutil_third_party="$gsutil_dir/third_party" + + if [[ -d "$gsutil_dir" && ! -f "$gsutil_dir/six.py" ]]; then + python3 - "$gsutil_dir/six.py" <<'PY' +import pathlib +import shutil +import six +import sys + +src = pathlib.Path(six.__file__) +dest = pathlib.Path(sys.argv[1]) +shutil.copyfile(src, dest) +PY + fi + + if [[ -d "$gsutil_third_party" && ! -f "$gsutil_third_party/six.py" ]]; then + python3 - "$gsutil_third_party/six.py" <<'PY' +import pathlib +import shutil +import six +import sys + +src = pathlib.Path(six.__file__) +dest = pathlib.Path(sys.argv[1]) +shutil.copyfile(src, dest) +PY + fi +} + +prepend_python_module_path() { + local module="$1" + local module_dir + module_dir="$(python3 - "$module" <<'PY' +import importlib +import pathlib +import sys + +module = importlib.import_module(sys.argv[1]) +print(pathlib.Path(module.__file__).parent) +PY +)" + export PYTHONPATH="$module_dir:${PYTHONPATH:-}" } sync_skiasharp() { @@ -186,16 +262,28 @@ build_angle() { cd "$src" apply_angle_patches "$src" python3 scripts/bootstrap.py + if is_musl_rid; then + initialize_depot_tools_system_python "$src/third_party/depot_tools" + patch_depot_tools_python_deps "$src/third_party/depot_tools" + prepend_python_module_path six + prepare_angle_gcs_artifacts "$src" + fi + prepare_musl_clang_runtime + prepare_musl_libstdcxx_headers gclient sync -f -D -R local out_dir="$src/out/linux-static-$TARGET_CPU" + local angle_is_clang="false" + if is_musl_rid; then + angle_is_clang="true" + fi mkdir -p "$out_dir" cat >"$out_dir/args.gn" <>"$args_file" <<'EOF_ARGS' +clang_base_path = "/usr" +clang_use_chrome_plugins = false +EOF_ARGS + fi +} + +prepare_musl_clang_runtime() { + if ! is_musl_rid; then + return 0 + fi + + local arch + local gnu_triple + case "$TARGET_CPU" in + x64) + arch="x86_64" + gnu_triple="x86_64-unknown-linux-gnu" + ;; + arm64) + arch="aarch64" + gnu_triple="aarch64-unknown-linux-gnu" + ;; + *) + echo "Unsupported musl ANGLE target_cpu for clang runtime: $TARGET_CPU" >&2 + return 1 + ;; + esac + + local src + src="$(find /usr/lib/llvm* /usr/lib/clang -path "*/${arch}-alpine-linux-musl/libclang_rt.builtins-${arch}.a" -print -quit 2>/dev/null || true)" + if [[ -z "$src" ]]; then + echo "Could not find Alpine compiler-rt builtins for $arch" >&2 + return 1 + fi + + local clang_version + clang_version="$(clang -print-resource-dir | awk -F/ '{print $NF}')" + local dest_dir="/usr/lib/clang/$clang_version/lib/$gnu_triple" + mkdir -p "$dest_dir" + ln -sf "$src" "$dest_dir/libclang_rt.builtins.a" +} + +prepare_musl_libstdcxx_headers() { + if ! is_musl_rid; then + return 0 + fi + + local arch + local musl_triple + case "$TARGET_CPU" in + x64) + arch="x86_64" + musl_triple="x86_64-linux-musl" + ;; + arm64) + arch="aarch64" + musl_triple="aarch64-linux-musl" + ;; + *) + echo "Unsupported musl ANGLE target_cpu for libstdc++ headers: $TARGET_CPU" >&2 + return 1 + ;; + esac + + local cxx_root + cxx_root="$(find /usr/include/c++ -mindepth 1 -maxdepth 1 -type d -print -quit 2>/dev/null || true)" + if [[ -z "$cxx_root" ]]; then + echo "Could not find libstdc++ include root" >&2 + return 1 + fi + + local src_dir="$cxx_root/${arch}-alpine-linux-musl" + local dest_dir="$cxx_root/$musl_triple" + if [[ ! -d "$src_dir" ]]; then + echo "Could not find Alpine libstdc++ target headers: $src_dir" >&2 + return 1 + fi + + ln -sfn "$src_dir" "$dest_dir" +} + copy_first_existing() { local dest="$1" shift diff --git a/scripts/build-windows-static-graphics.ps1 b/scripts/build-windows-static-graphics.ps1 index 120e86d..5d90d2a 100644 --- a/scripts/build-windows-static-graphics.ps1 +++ b/scripts/build-windows-static-graphics.ps1 @@ -72,6 +72,275 @@ function Copy-FirstExisting($Destination, [string[]]$Candidates) { throw "None of the expected files exist for ${Destination}: $($Candidates -join ', ')" } +function Split-ParameterList($Parameters) { + if ([string]::IsNullOrWhiteSpace($Parameters)) { + return @() + } + + $items = [System.Collections.Generic.List[string]]::new() + $start = 0 + $depth = 0 + for ($i = 0; $i -lt $Parameters.Length; $i++) { + $ch = $Parameters[$i] + if ($ch -eq '[' -or $ch -eq '(' -or $ch -eq '<') { + $depth++ + } elseif ($ch -eq ']' -or $ch -eq ')' -or $ch -eq '>') { + if ($depth -gt 0) { $depth-- } + } elseif ($ch -eq ',' -and $depth -eq 0) { + $items.Add($Parameters.Substring($start, $i - $start).Trim()) + $start = $i + 1 + } + } + $items.Add($Parameters.Substring($start).Trim()) + return $items | Where-Object { $_ } +} + +function Get-X86NativeParameterSize($Parameter) { + $parameter = [regex]::Replace($Parameter, '/\*.*?\*/', '').Trim() + $parameter = [regex]::Replace($parameter, '\[[^\]]+\]\s*', '').Trim() + $parameter = [regex]::Replace($parameter, '\b(ref|out|in)\b\s*', '').Trim() + if (-not $parameter) { return 0 } + + $parts = $parameter -split '\s+' + if ($parts.Length -gt 1) { + $type = ($parts[0..($parts.Length - 2)] -join ' ') + } else { + $type = $parts[0] + } + $type = $type.Trim() + + if ($script:X86GeneratedStructSizes -and $script:X86GeneratedStructSizes.ContainsKey($type)) { + return $script:X86GeneratedStructSizes[$type] + } + + if ($type.Contains('*') -or $type.EndsWith('[]') -or $type -eq 'String' -or $type -eq 'string' -or $type.EndsWith('Delegate')) { + return 4 + } + + switch -Regex ($type) { + '^(Int64|UInt64|long|ulong|Double|double)$' { return 8 } + default { return 4 } + } +} + +function Align-X86Size($Size, $Alignment) { + if ($Alignment -le 1) { return $Size } + return [int]([Math]::Ceiling($Size / [double]$Alignment) * $Alignment) +} + +function Get-X86ManagedTypeLayout($Type, $KnownSizes) { + if ($Type.Contains('*') -or $Type.Contains('delegate*') -or $Type.EndsWith('[]') -or $Type.EndsWith('Delegate')) { + return @{ Size = 4; Alignment = 4 } + } + + switch -Regex ($Type) { + '^(Byte|SByte|bool)$' { return @{ Size = 1; Alignment = 1 } } + '^(Int16|UInt16|short|ushort|Char)$' { return @{ Size = 2; Alignment = 2 } } + '^(Int64|UInt64|long|ulong|Double|double)$' { return @{ Size = 8; Alignment = 4 } } + default { + if ($KnownSizes.ContainsKey($Type)) { + return @{ Size = $KnownSizes[$Type]; Alignment = 4 } + } + return @{ Size = 4; Alignment = 4 } + } + } +} + +function Get-X86GeneratedStructSizes($BindingFiles) { + $structFields = @{} + + foreach ($bindingFile in $BindingFiles) { + $lines = Get-Content -Path $bindingFile + $current = $null + $depth = 0 + $includeStack = @($true) + foreach ($line in $lines) { + if ($line -match '^\s*#if\s+USE_LIBRARY_IMPORT\b') { + $includeStack += $includeStack[-1] + continue + } + if ($line -match '^\s*#else\b') { + if ($includeStack.Count -gt 1) { + $parentActive = $includeStack[-2] + $includeStack[-1] = $parentActive -and (-not $includeStack[-1]) + } + continue + } + if ($line -match '^\s*#endif\b') { + if ($includeStack.Count -gt 1) { + $includeStack = @($includeStack[0..($includeStack.Count - 2)]) + } + continue + } + if (-not $includeStack[-1]) { + continue + } + + if (-not $current -and $line -match '\bstruct\s+([A-Za-z0-9_]+)\b') { + $current = $Matches[1] + $structFields[$current] = [System.Collections.Generic.List[string]]::new() + $depth = ([regex]::Matches($line, '\{')).Count - ([regex]::Matches($line, '\}')).Count + continue + } + + if ($current) { + if ($depth -eq 1 -and $line -match '^\s*public\s+(.+?)\s+[A-Za-z0-9_]+;\s*$') { + $fieldType = ([regex]::Replace($Matches[1].Trim(), '#if.*$', '')).Trim() + $structFields[$current].Add($fieldType) + } + + $depth += ([regex]::Matches($line, '\{')).Count + $depth -= ([regex]::Matches($line, '\}')).Count + if ($depth -le 0) { + $current = $null + } + } + } + } + + $sizes = @{} + $pending = @($structFields.Keys) + while ($pending.Count -gt 0) { + $next = @() + $progress = $false + foreach ($name in $pending) { + $offset = 0 + $maxAlign = 1 + $resolved = $true + foreach ($fieldType in $structFields[$name]) { + if ($structFields.ContainsKey($fieldType) -and -not $sizes.ContainsKey($fieldType)) { + $resolved = $false + break + } + $layout = Get-X86ManagedTypeLayout $fieldType $sizes + $maxAlign = [Math]::Max($maxAlign, [Math]::Min($layout.Alignment, 4)) + $offset = Align-X86Size $offset ([Math]::Min($layout.Alignment, 4)) + $offset += $layout.Size + } + if ($resolved) { + $sizes[$name] = Align-X86Size $offset $maxAlign + $progress = $true + } else { + $next += $name + } + } + if (-not $progress) { break } + $pending = $next + } + + return $sizes +} + +function Get-X86PInvokeThunks($BindingFiles, $DefinedSymbols) { + $thunks = @{} + $signaturePattern = 'internal static (?:extern|partial)\s+.+?\s+((?:sk|gr|hb)_[A-Za-z0-9_]+)\s*\((.*?)\);' + $script:X86GeneratedStructSizes = Get-X86GeneratedStructSizes $BindingFiles + + foreach ($bindingFile in $BindingFiles) { + if (-not (Test-Path $bindingFile)) { + throw "Missing binding file for win-x86 thunk generation: $bindingFile" + } + + $content = Get-Content -Raw -Path $bindingFile + foreach ($match in [regex]::Matches($content, $signaturePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)) { + $symbol = "_$($match.Groups[1].Value)" + if (-not $DefinedSymbols.ContainsKey($symbol)) { + continue + } + + $bytes = 0 + foreach ($parameter in Split-ParameterList $match.Groups[2].Value) { + $bytes += Get-X86NativeParameterSize $parameter + } + + $decorated = "${symbol}@${bytes}" + $thunks[$decorated] = [pscustomobject]@{ + Symbol = $symbol + Bytes = $bytes + Decorated = $decorated + } + } + } + + return $thunks.Values | Sort-Object Symbol, Bytes +} + +function New-WinX86SkiaStdcallThunks([string[]]$InputLibraries, [string[]]$BindingFiles, $Destination) { + if ($TargetCpu -ne "x86") { + return + } + + $llvmNm = Get-Command llvm-nm.exe -ErrorAction SilentlyContinue + if (-not $llvmNm) { + $llvmNm = Get-Command llvm-nm -ErrorAction SilentlyContinue + } + if (-not $llvmNm) { + throw "Missing required command: llvm-nm" + } + Require-Command ml.exe + Require-Command lib.exe + + $thunkDir = Join-Path $WorkDir "win-x86-skia-stdcall-thunks" + New-Item -ItemType Directory -Path $thunkDir -Force | Out-Null + $asmPath = Join-Path $thunkDir "skia_x86_stdcall_thunks.asm" + $objPath = Join-Path $thunkDir "skia_x86_stdcall_thunks.obj" + + $definedSymbols = @{} + foreach ($library in $InputLibraries) { + & $llvmNm.Source --defined-only $library | + ForEach-Object { + if ($_ -match '\sT\s(_(?:sk|gr|hb)_[A-Za-z0-9_]+)$') { + $definedSymbols[$Matches[1]] = $true + } + } + } + + if (-not $definedSymbols.Count) { + throw "No SkiaSharp/HarfBuzzSharp C API symbols found in $($InputLibraries -join ', ')" + } + + $thunks = @(Get-X86PInvokeThunks $BindingFiles $definedSymbols) + if (-not $thunks) { + throw "No win-x86 stdcall thunks generated from $($BindingFiles -join ', ')" + } + + $lines = [System.Collections.Generic.List[string]]::new() + $lines.Add("OPTION CASEMAP:NONE") + $lines.Add(".386") + $lines.Add(".model flat") + foreach ($symbol in ($thunks | Select-Object -ExpandProperty Symbol -Unique)) { + $lines.Add("EXTERN ${symbol}:PROC") + } + $lines.Add("_TEXT SEGMENT") + foreach ($thunk in $thunks) { + $lines.Add("PUBLIC $($thunk.Decorated)") + $lines.Add("$($thunk.Decorated) PROC") + for ($offset = 0; $offset -lt $thunk.Bytes; $offset += 4) { + $lines.Add(" push DWORD PTR [esp+$($thunk.Bytes)]") + } + $lines.Add(" call $($thunk.Symbol)") + if ($thunk.Bytes -gt 0) { + $lines.Add(" add esp, $($thunk.Bytes)") + } + $lines.Add(" ret $($thunk.Bytes)") + $lines.Add("$($thunk.Decorated) ENDP") + } + $lines.Add("_TEXT ENDS") + $lines.Add("END") + Set-Content -Path $asmPath -Value $lines -Encoding ASCII + + & ml.exe /nologo /c /coff /safeseh /Fo$objPath $asmPath + if ($LASTEXITCODE -ne 0) { + throw "Failed to assemble win-x86 Skia stdcall thunks." + } + + & lib.exe /nologo /out:$Destination $objPath + if ($LASTEXITCODE -ne 0) { + throw "Failed to archive win-x86 Skia stdcall thunks." + } + Write-Host "Wrote $Destination" +} + function Sync-SkiaSharp { $src = Join-Path $WorkDir "SkiaSharp-$SkiaSharpVersion" if (-not (Test-Path (Join-Path $src ".git"))) { @@ -146,7 +415,7 @@ skia_use_system_libpng = false skia_use_system_libwebp = false skia_use_system_zlib = false skia_use_vulkan = false -skia_use_xps = false +skia_use_xps = true extra_cflags = [ "-DSKIA_C_DLL" ] extra_cflags_cc = [ "/GR" ] "@ | Set-Content -Path (Join-Path $outDir "args.gn") -Encoding ASCII @@ -159,9 +428,12 @@ extra_cflags_cc = [ "/GR" ] Pop-Location } - Copy-FirstExisting (Join-Path $OutputDir "skia.lib") @((Join-Path $outDir "skia.lib"), (Join-Path $outDir "obj\skia.lib")) - Copy-FirstExisting (Join-Path $OutputDir "SkiaSharp.lib") @((Join-Path $outDir "SkiaSharp.lib"), (Join-Path $outDir "obj\SkiaSharp.lib")) - Copy-FirstExisting (Join-Path $OutputDir "libHarfBuzzSharp.lib") @( + $skiaLib = Join-Path $OutputDir "skia.lib" + $skiaSharpLib = Join-Path $OutputDir "SkiaSharp.lib" + $harfbuzzLib = Join-Path $OutputDir "libHarfBuzzSharp.lib" + Copy-FirstExisting $skiaLib @((Join-Path $outDir "skia.lib"), (Join-Path $outDir "obj\skia.lib")) + Copy-FirstExisting $skiaSharpLib @((Join-Path $outDir "SkiaSharp.lib"), (Join-Path $outDir "obj\SkiaSharp.lib")) + Copy-FirstExisting $harfbuzzLib @( (Join-Path $outDir "libHarfBuzzSharp.lib"), (Join-Path $outDir "HarfBuzzSharp.lib"), (Join-Path $outDir "obj\libHarfBuzzSharp.lib"), @@ -169,6 +441,10 @@ extra_cflags_cc = [ "/GR" ] (Join-Path $outDir "obj\HarfBuzzSharp\libHarfBuzzSharp.lib"), (Join-Path $outDir "obj\HarfBuzzSharp\HarfBuzzSharp.lib") ) + New-WinX86SkiaStdcallThunks ` + -InputLibraries @($skiaLib, $skiaSharpLib, $harfbuzzLib) ` + -BindingFiles @((Join-Path $src "binding\SkiaSharp\SkiaApi.generated.cs"), (Join-Path $src "binding\HarfBuzzSharp\HarfBuzzApi.generated.cs")) ` + -Destination (Join-Path $OutputDir "skia_x86_stdcall_thunks.lib") } function Sync-Angle {