diff --git a/.github/workflows/nuget-avalonia-native.yml b/.github/workflows/nuget-avalonia-native.yml new file mode 100644 index 0000000..12d71d5 --- /dev/null +++ b/.github/workflows/nuget-avalonia-native.yml @@ -0,0 +1,278 @@ +name: staticlink-avalonia-native + +on: + schedule: + - cron: "17 3 * * *" + workflow_dispatch: + inputs: + avalonia_version: + description: Avalonia version to build; empty uses the latest stable NuGet version + required: false + type: string + force_build: + description: Build even when this StaticLink.Avalonia.Native version already exists + required: true + default: false + type: boolean + publish_to_nuget: + description: Publish with NuGet Trusted Publishing after smoke tests + required: true + default: true + type: boolean + +concurrency: + group: staticlink-avalonia-native + cancel-in-progress: false + +jobs: + detect: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + avalonia_version: ${{ steps.version.outputs.avalonia_version }} + package_version: ${{ steps.version.outputs.package_version }} + should_build: ${{ steps.version.outputs.should_build }} + steps: + - name: Detect unpublished Avalonia version + id: version + shell: bash + env: + REQUESTED_VERSION: ${{ inputs.avalonia_version }} + FORCE_BUILD: ${{ inputs.force_build }} + run: | + set -euo pipefail + + avalonia_version="$REQUESTED_VERSION" + if [ -z "$avalonia_version" ]; then + avalonia_version="$(curl --fail --silent --show-error --location \ + https://api.nuget.org/v3-flatcontainer/avalonia/index.json \ + | jq -r '[.versions[] | select(contains("-") | not)] | last')" + fi + + if [ -z "$avalonia_version" ] || [ "$avalonia_version" = "null" ]; then + echo "Could not determine the latest stable Avalonia version." >&2 + exit 1 + fi + + package_version="$avalonia_version.1" + package_url="https://api.nuget.org/v3-flatcontainer/staticlink.avalonia.native/$package_version/staticlink.avalonia.native.$package_version.nupkg" + should_build=true + if [ "$FORCE_BUILD" != "true" ] && curl --fail --silent --location --output /dev/null "$package_url"; then + should_build=false + fi + + echo "Avalonia version: $avalonia_version" + echo "StaticLink.Avalonia.Native version: $package_version" + echo "Build required: $should_build" + echo "avalonia_version=$avalonia_version" >> "$GITHUB_OUTPUT" + echo "package_version=$package_version" >> "$GITHUB_OUTPUT" + echo "should_build=$should_build" >> "$GITHUB_OUTPUT" + + build: + needs: detect + if: ${{ needs.detect.outputs.should_build == 'true' }} + strategy: + fail-fast: false + matrix: + include: + - rid: osx-x64 + runner: macos-14 + - rid: osx-arm64 + runner: macos-15 + runs-on: ${{ matrix.runner }} + permissions: + contents: read + env: + RID: ${{ matrix.rid }} + AVALONIA_VERSION: ${{ needs.detect.outputs.avalonia_version }} + WORK_DIR: ${{ github.workspace }}/External/AvaloniaNative/.work + OUTPUT_DIR: ${{ github.workspace }}/External/AvaloniaNative/${{ matrix.rid }}/native + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET for Avalonia header generation + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - name: Build static AvaloniaNative + run: bash scripts/build-macos-avalonia-native-static.sh + + - name: Build macOS font preinit object + run: bash scripts/build-macos-font-preinit.sh + + - name: Upload static AvaloniaNative + uses: actions/upload-artifact@v4 + with: + name: native-avalonia-${{ matrix.rid }} + path: | + External/AvaloniaNative/${{ matrix.rid }}/native/libAvaloniaNative.a + External/AvaloniaNative/${{ matrix.rid }}/native/macos_font_preinit.o + if-no-files-found: error + + pack: + needs: + - detect + - build + if: ${{ needs.detect.outputs.should_build == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Download static AvaloniaNative archives + uses: actions/download-artifact@v4 + with: + pattern: native-avalonia-* + path: artifacts/native + merge-multiple: false + + - name: Prepare package inputs + shell: bash + run: | + set -euo pipefail + for rid in osx-x64 osx-arm64; do + mkdir -p "External/AvaloniaNative/$rid/native" + cp "artifacts/native/native-avalonia-$rid/libAvaloniaNative.a" "External/AvaloniaNative/$rid/native/" + cp "artifacts/native/native-avalonia-$rid/macos_font_preinit.o" "External/AvaloniaNative/$rid/native/" + done + + - name: Pack StaticLink.Avalonia.Native + run: | + dotnet pack NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj \ + -c Release \ + -o artifacts/nuget \ + /p:PackageVersion=${{ needs.detect.outputs.package_version }} \ + /p:Version=${{ needs.detect.outputs.package_version }} + + - name: Upload NuGet package + uses: actions/upload-artifact@v4 + with: + name: staticlink-avalonia-native-${{ needs.detect.outputs.package_version }} + path: artifacts/nuget/*.nupkg + if-no-files-found: error + + smoke: + needs: + - detect + - pack + if: ${{ needs.detect.outputs.should_build == 'true' }} + strategy: + fail-fast: false + matrix: + include: + - rid: osx-x64 + runner: macos-14 + - rid: osx-arm64 + runner: macos-15 + runs-on: ${{ matrix.runner }} + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Download NuGet package + uses: actions/download-artifact@v4 + with: + name: staticlink-avalonia-native-${{ needs.detect.outputs.package_version }} + path: artifacts/nuget + + - name: Publish Avalonia NativeAOT smoke app + shell: pwsh + run: | + $projectDir = Join-Path $PWD "artifacts/avalonia-native-smoke-src" + pwsh ./scripts/create-avalonia-smoke.ps1 ` + -ProjectDir $projectDir ` + -NuGetSource (Resolve-Path "artifacts/nuget") ` + -AvaloniaVersion "${{ needs.detect.outputs.avalonia_version }}" ` + -StaticLinkNativeVersion "${{ needs.detect.outputs.package_version }}" + dotnet publish (Join-Path $projectDir "AvaloniaStaticLinkSmoke.csproj") ` + -c Release ` + -r "${{ matrix.rid }}" ` + --configfile (Join-Path $projectDir "NuGet.config") ` + /p:PublishDir="$PWD/artifacts/smoke-${{ matrix.rid }}/" + + - name: Verify AvaloniaNative is statically linked + shell: bash + run: | + set -euo pipefail + test ! -f "artifacts/smoke-${{ matrix.rid }}/libAvaloniaNative.dylib" + test -x "artifacts/smoke-${{ matrix.rid }}/AvaloniaStaticLinkSmoke" + + - name: Run Avalonia smoke app + shell: bash + run: | + set -euo pipefail + cd "artifacts/smoke-${{ matrix.rid }}" + python3 - <<'PY' + import subprocess + import sys + + proc = subprocess.Popen( + ["./AvaloniaStaticLinkSmoke"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + try: + output, _ = proc.communicate(timeout=15) + except subprocess.TimeoutExpired: + proc.kill() + output, _ = proc.communicate() + print(output or "") + print("Avalonia smoke app stayed alive for 15 seconds.") + sys.exit(0) + + print(output or "") + if proc.returncode == 0: + sys.exit(0) + sys.exit(proc.returncode if proc.returncode > 0 else 1) + PY + + publish: + needs: + - detect + - smoke + if: ${{ needs.detect.outputs.should_build == 'true' && (github.event_name == 'schedule' || inputs.publish_to_nuget == true) }} + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Download NuGet package + uses: actions/download-artifact@v4 + with: + name: staticlink-avalonia-native-${{ needs.detect.outputs.package_version }} + path: artifacts/nuget + + - name: Validate Trusted Publishing configuration + env: + NUGET_USER: ${{ vars.NUGET_USER }} + run: test -n "$NUGET_USER" + + - name: NuGet login + id: login + uses: NuGet/login@v1 + with: + user: ${{ vars.NUGET_USER }} + + - name: Publish to nuget.org + run: | + dotnet nuget push artifacts/nuget/*.nupkg \ + --source https://www.nuget.org/api/v2/package \ + --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \ + --skip-duplicate diff --git a/.github/workflows/nuget-static-graphics.yml b/.github/workflows/nuget-static-graphics.yml index 1609538..663265b 100644 --- a/.github/workflows/nuget-static-graphics.yml +++ b/.github/workflows/nuget-static-graphics.yml @@ -194,9 +194,6 @@ jobs: - name: Build macOS static graphics libraries run: bash ./scripts/build-macos-static-graphics.sh all - - name: Build macOS font preinit object - run: bash scripts/build-macos-font-preinit.sh - - name: Upload macOS static archives uses: actions/upload-artifact@v4 with: @@ -204,61 +201,12 @@ jobs: path: External/NativeStatic/${{ matrix.rid }}/* if-no-files-found: error - build-macos-avalonia-native: - strategy: - fail-fast: false - matrix: - include: - - rid: osx-x64 - runner: macos-14 - avalonia_version: 11.3.14 - - rid: osx-arm64 - runner: macos-15 - avalonia_version: 11.3.14 - - rid: osx-x64 - runner: macos-14 - avalonia_version: 12.0.3 - - rid: osx-arm64 - runner: macos-15 - avalonia_version: 12.0.3 - runs-on: ${{ matrix.runner }} - env: - RID: ${{ matrix.rid }} - WORK_DIR: ${{ github.workspace }}/External/NativeStatic/.work - OUTPUT_DIR: ${{ github.workspace }}/External/NativeStatic/${{ matrix.rid }}/native - AVALONIA_VERSION: ${{ matrix.avalonia_version }} - steps: - - uses: actions/checkout@v4 - - - name: Setup .NET for Avalonia header generation - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - 10.0.x - - - name: Build static AvaloniaNative - run: bash ./scripts/build-macos-avalonia-native-static.sh - - - name: Build macOS font preinit object - run: bash scripts/build-macos-font-preinit.sh - - - name: Upload static AvaloniaNative - uses: actions/upload-artifact@v4 - with: - name: native-avalonia-${{ matrix.avalonia_version }}-${{ matrix.rid }} - path: | - External/NativeStatic/${{ matrix.rid }}/native/libAvaloniaNative.a - External/NativeStatic/${{ matrix.rid }}/native/macos_font_preinit.o - if-no-files-found: error - pack: runs-on: ubuntu-latest needs: - build-linux - build-windows - build-macos - - build-macos-avalonia-native permissions: contents: read steps: @@ -276,13 +224,6 @@ jobs: path: External/NativeStatic merge-multiple: false - - name: Download static AvaloniaNative archives - uses: actions/download-artifact@v4 - with: - pattern: native-avalonia-* - path: External/AvaloniaNative - merge-multiple: false - - name: Move static archives into RID directories run: | for rid in win-x64 win-x86 win-arm64 linux-x64 linux-arm64 linux-musl-x64 linux-musl-arm64 osx-x64 osx-arm64; do @@ -315,18 +256,7 @@ jobs: test -f External/NativeStatic/win-x86/native/skia_x86_stdcall_thunks.lib - name: Pack NuGet package - run: | - dotnet pack NuGet/StaticGraphics/StaticLink.Avalonia.csproj -c Release -o artifacts/nuget - for version in 11.3.14 12.0.3; do - mkdir -p External/NativeStatic/osx-x64/native External/NativeStatic/osx-arm64/native - cp External/AvaloniaNative/native-avalonia-$version-osx-x64/* External/NativeStatic/osx-x64/native/ - cp External/AvaloniaNative/native-avalonia-$version-osx-arm64/* External/NativeStatic/osx-arm64/native/ - dotnet pack NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj \ - -c Release \ - -o artifacts/nuget \ - /p:PackageVersion=$version.1 \ - /p:Version=$version.1 - done + run: dotnet pack NuGet/StaticGraphics/StaticLink.Avalonia.csproj -c Release -o artifacts/nuget - name: Upload NuGet artifact uses: actions/upload-artifact@v4 @@ -380,19 +310,9 @@ jobs: - rid: osx-x64 runner: macos-14 avalonia_version: 11.3.14 - native_version: 11.3.14.1 - rid: osx-arm64 runner: macos-15 avalonia_version: 11.3.14 - native_version: 11.3.14.1 - - rid: osx-x64 - runner: macos-14 - avalonia_version: 12.0.3 - native_version: 12.0.3.1 - - rid: osx-arm64 - runner: macos-15 - avalonia_version: 12.0.3 - native_version: 12.0.3.1 runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 @@ -429,15 +349,10 @@ jobs: 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] - $nativeVersion = "${{ matrix.native_version }}" $avaloniaVersion = "${{ matrix.avalonia_version }}" if (-not $avaloniaVersion) { $avaloniaVersion = "11.3.14" } - if ($nativeVersion) { - $nativePackage = Get-ChildItem -Path "artifacts/nuget" -Filter "StaticLink.Avalonia.Native.$nativeVersion.nupkg" | Select-Object -First 1 - if (-not $nativePackage) { throw "StaticLink AvaloniaNative NuGet package $nativeVersion was not downloaded." } - } $projectDir = Join-Path $PWD "artifacts/avalonia-smoke-src" - pwsh ./scripts/create-avalonia-smoke.ps1 -ProjectDir $projectDir -NuGetSource (Resolve-Path "artifacts/nuget") -StaticLinkVersion $version -AvaloniaVersion $avaloniaVersion -StaticLinkNativeVersion $nativeVersion + pwsh ./scripts/create-avalonia-smoke.ps1 -ProjectDir $projectDir -NuGetSource (Resolve-Path "artifacts/nuget") -StaticLinkVersion $version -AvaloniaVersion $avaloniaVersion dotnet publish (Join-Path $projectDir "AvaloniaStaticLinkSmoke.csproj") ` -c Release ` -r "${{ matrix.rid }}" ` @@ -470,7 +385,7 @@ jobs: run: | $publishDir = "artifacts/smoke-${{ matrix.rid }}" $blocked = Get-ChildItem -Path $publishDir -Recurse -File | Where-Object { - $_.Name -in @("libSkiaSharp.dll", "libHarfBuzzSharp.dll", "av_libglesv2.dll", "libSkiaSharp.so", "libHarfBuzzSharp.so", "libAvaloniaNative.dylib", "libSkiaSharp.dylib", "libHarfBuzzSharp.dylib") + $_.Name -in @("libSkiaSharp.dll", "libHarfBuzzSharp.dll", "av_libglesv2.dll", "libSkiaSharp.so", "libHarfBuzzSharp.so", "libSkiaSharp.dylib", "libHarfBuzzSharp.dylib") } if ($blocked) { $blocked | ForEach-Object { Write-Error "Unexpected dynamic graphics library: $($_.FullName)" } diff --git a/.gitignore b/.gitignore index b6f0af6..bae7f23 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ artifacts/ *.nupkg *.snupkg External/NativeStatic/.work/ +External/AvaloniaNative/.work/ diff --git a/NuGet/AvaloniaNative/README.md b/NuGet/AvaloniaNative/README.md new file mode 100644 index 0000000..aacdb38 --- /dev/null +++ b/NuGet/AvaloniaNative/README.md @@ -0,0 +1,12 @@ +# StaticLink.Avalonia.Native + +Static `AvaloniaNative` libraries for macOS Avalonia NativeAOT publishing. + +The package version matches the Avalonia version with a `.1` suffix. For example, use `StaticLink.Avalonia.Native` version `12.1.0.1` with Avalonia `12.1.0`. + +```xml + + +``` + +The package supports `osx-x64` and `osx-arm64` NativeAOT publishing. diff --git a/NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj b/NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj index 21eb745..639f7f2 100644 --- a/NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj +++ b/NuGet/AvaloniaNative/StaticLink.Avalonia.Native.csproj @@ -6,7 +6,7 @@ false false StaticLink.Avalonia.Native - 11.3.14.1 + 0.0.0 greepar greepar Static AvaloniaNative library for macOS Avalonia NativeAOT publishing. @@ -19,14 +19,14 @@ - + - - - - + + + + diff --git a/README.md b/README.md index 58930ba..b8cb49f 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,9 @@ dotnet publish -c Release -r win-x64 \ Use the RID you need, such as `win-x86`, `linux-x64`, `linux-arm64`, `osx-arm64`, or `osx-x64`. For macOS, avoid the Metal renderer for fully static output. Use OpenGL or Software. + +## Native Package Automation + +`.github/workflows/nuget-avalonia-native.yml` runs daily and checks the latest stable `Avalonia` version on NuGet.org. If `StaticLink.Avalonia.Native..1` does not exist, it builds both macOS architectures from the matching Avalonia source tag, runs NativeAOT smoke tests, and publishes the package with NuGet Trusted Publishing. + +Configure NuGet.org Trusted Publishing for this repository and the `nuget-avalonia-native.yml` workflow, then add the NuGet.org account name as the repository variable `NUGET_USER`. No long-lived NuGet API key is used by this workflow. diff --git a/scripts/build-macos-avalonia-native-static.sh b/scripts/build-macos-avalonia-native-static.sh index d32833d..288d1c0 100644 --- a/scripts/build-macos-avalonia-native-static.sh +++ b/scripts/build-macos-avalonia-native-static.sh @@ -3,8 +3,8 @@ set -euo pipefail RID="${RID:-osx-arm64}" AVALONIA_VERSION="${AVALONIA_VERSION:-11.3.14}" -WORK_DIR="${WORK_DIR:-$PWD/External/NativeStatic/.work}" -OUTPUT_DIR="${OUTPUT_DIR:-$PWD/External/NativeStatic/$RID/native}" +WORK_DIR="${WORK_DIR:-$PWD/External/AvaloniaNative/.work}" +OUTPUT_DIR="${OUTPUT_DIR:-$PWD/External/AvaloniaNative/$RID/native}" BUILD_JOBS="${BUILD_JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 4)}" case "$RID" in diff --git a/scripts/create-avalonia-smoke.ps1 b/scripts/create-avalonia-smoke.ps1 index e9eb5f8..cfc75c1 100644 --- a/scripts/create-avalonia-smoke.ps1 +++ b/scripts/create-avalonia-smoke.ps1 @@ -5,8 +5,7 @@ param( [Parameter(Mandatory = $true)] [string]$NuGetSource, - [Parameter(Mandatory = $true)] - [string]$StaticLinkVersion, + [string]$StaticLinkVersion = "", [string]$AvaloniaVersion = "11.3.14", @@ -22,6 +21,11 @@ if ($StaticLinkNativeVersion) { $nativeReference = "`n " } +$staticLinkReference = "" +if ($StaticLinkVersion) { + $staticLinkReference = "`n " +} + @" @@ -52,8 +56,7 @@ if ($StaticLinkNativeVersion) { - - $nativeReference + $staticLinkReference$nativeReference "@ | Set-Content -Path (Join-Path $ProjectDir "AvaloniaStaticLinkSmoke.csproj") -Encoding UTF8