Build ThorVG natives #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Builds ThorVG from source, once per RID, and stages each library at the path it will occupy | |
| # in this repository so the CD can merge the artifacts straight over the working tree. | |
| # | |
| # ThorVG publishes no binaries: every release carries a single source tarball. So unlike a | |
| # binding that downloads official libraries, this repository has to compile its own, and the | |
| # managed layer and the libraries must come from the same tag or the P/Invokes describe one | |
| # revision while the binary is another. | |
| # | |
| # Desktop and Android ship shared libraries under runtimes/<rid>/native/, which .NET probes by | |
| # RID. iOS has no dynamic loader for third-party code, so it ships a static archive that the | |
| # consumer's build links in — see buildTransitive/Evergine.Bindings.ThorVG.targets. | |
| name: Build ThorVG natives | |
| on: | |
| workflow_call: | |
| inputs: | |
| ref: | |
| description: 'ThorVG tag to build' | |
| required: false | |
| type: string | |
| default: 'v1.1.0' | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: 'ThorVG tag to build' | |
| required: false | |
| type: string | |
| default: 'v1.1.0' | |
| only-rid: | |
| description: 'Build a single RID (e.g. android-arm64). Empty builds all of them.' | |
| required: false | |
| type: string | |
| default: '' | |
| env: | |
| # Shared by every leg. -Dbindings=capi is the one that matters: it is OFF by default, and | |
| # without it the library builds perfectly and exports not one tvg_* symbol. | |
| # -Dstatic=true does NOT mean static linkage in ThorVG — it selects the bundled png/jpg/webp | |
| # decoders over system ones, which is what keeps these libraries free of external deps. | |
| # -Dextra drops openmp, which would otherwise pull a libomp/vcomp runtime the NuGet does not | |
| # ship. Linkage is chosen separately, with --default-library. | |
| # | |
| # No -Dtools here. Its default is already the empty array, and passing -Dtools='' through an | |
| # environment variable hands meson the two quote characters as part of the value, which it | |
| # rejects as not being one of the allowed choices. | |
| TVG_COMMON: >- | |
| --buildtype=release | |
| -Dbindings=capi | |
| -Dengines=cpu | |
| -Dloaders=all | |
| -Dsavers=all | |
| -Dstatic=true | |
| -Dextra=lottie_exp | |
| -Dlog=false | |
| -Dtests=false | |
| jobs: | |
| build: | |
| name: ${{ matrix.rid }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-24.04, rid: linux-x64, kind: shared } | |
| # A native ARM runner rather than a cross toolchain: meson cross-compilation needs a | |
| # cross file, and ThorVG ships none for linux-arm64. | |
| - { os: ubuntu-24.04-arm, rid: linux-arm64, kind: shared } | |
| - { os: windows-latest, rid: win-x64, kind: shared } | |
| - { os: windows-latest, rid: win-arm64, kind: shared, msvc-arch: amd64_arm64, cross: ThorvgGen/cross/win_arm64.txt } | |
| - { os: macos-latest, rid: osx-arm64, kind: shared } | |
| - { os: ubuntu-24.04, rid: android-arm64, kind: shared, ndk-cross: android_aarch64.txt } | |
| - { os: ubuntu-24.04, rid: android-x64, kind: shared, ndk-cross: android_x86_64.txt } | |
| # Static: iOS has no dynamic loader for third-party libraries, so the archive is | |
| # linked into the consumer's application instead of loaded at run time. | |
| - { os: macos-latest, rid: ios-arm64, kind: static, ios-cross: ios_arm64.txt } | |
| - { os: macos-latest, rid: iossimulator-arm64, kind: static, ios-cross: ios_simulator_arm64.txt } | |
| steps: | |
| - name: Checkout ThorVG | |
| if: inputs.only-rid == '' || inputs.only-rid == matrix.rid | |
| uses: actions/checkout@v5 | |
| with: | |
| repository: thorvg/thorvg | |
| ref: ${{ inputs.ref || 'v1.1.0' }} | |
| # The win-arm64 cross file lives in this repository, not upstream. | |
| - name: Checkout this repository's cross files | |
| if: (inputs.only-rid == '' || inputs.only-rid == matrix.rid) && matrix.cross != '' | |
| uses: actions/checkout@v5 | |
| with: | |
| path: .binding | |
| - name: Setup Python | |
| if: inputs.only-rid == '' || inputs.only-rid == matrix.rid | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install meson and ninja | |
| if: inputs.only-rid == '' || inputs.only-rid == matrix.rid | |
| run: pip install meson ninja | |
| # ThorVG's own Windows CI does this; meson needs cl.exe on PATH. For win-arm64 the | |
| # amd64_arm64 host/target pair cross-compiles from the x64 runner. | |
| - name: Setup MSVC | |
| if: (inputs.only-rid == '' || inputs.only-rid == matrix.rid) && startsWith(matrix.rid, 'win-') | |
| uses: ilammy/msvc-dev-cmd@v1 | |
| with: | |
| arch: ${{ matrix.msvc-arch || 'x64' }} | |
| - name: Setup Android NDK | |
| if: (inputs.only-rid == '' || inputs.only-rid == matrix.rid) && startsWith(matrix.rid, 'android-') | |
| id: ndk | |
| uses: nttld/setup-ndk@v1.5.0 | |
| with: | |
| ndk-version: r27c | |
| local-cache: true | |
| # ThorVG's Android cross files are templates: the literal tokens NDK, HOST_TAG and API | |
| # have to be substituted before meson can read them. Upstream's own CI does exactly this. | |
| - name: Prepare the Android cross file | |
| if: (inputs.only-rid == '' || inputs.only-rid == matrix.rid) && startsWith(matrix.rid, 'android-') | |
| shell: bash | |
| env: | |
| NDK: ${{ steps.ndk.outputs.ndk-path }} | |
| API: '24' | |
| run: | | |
| set -euo pipefail | |
| sed -e "s|NDK|$NDK|g" -e "s|HOST_TAG|linux-x86_64|g" -e "s|API|$API|g" \ | |
| "./cross/${{ matrix.ndk-cross }}" > /tmp/android_cross.txt | |
| cat /tmp/android_cross.txt | |
| - name: Configure and build (Unix) | |
| if: (inputs.only-rid == '' || inputs.only-rid == matrix.rid) && !startsWith(matrix.rid, 'win-') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cross="" | |
| case "${{ matrix.rid }}" in | |
| android-*) cross="--cross-file /tmp/android_cross.txt" ;; | |
| ios*) cross="--cross-file ./cross/${{ matrix.ios-cross }}" ;; | |
| esac | |
| # Android has no pthread — it is inside Bionic's libc — and upstream disables threads | |
| # for it rather than have meson look for a library that is not there. | |
| extra="" | |
| case "${{ matrix.rid }}" in | |
| android-*) extra="-Dthreads=false" ;; | |
| esac | |
| meson setup build $TVG_COMMON --default-library=${{ matrix.kind }} $cross $extra | |
| meson compile -C build | |
| - name: Configure and build (Windows) | |
| if: (inputs.only-rid == '' || inputs.only-rid == matrix.rid) && startsWith(matrix.rid, 'win-') | |
| shell: pwsh | |
| run: | | |
| $cross = if ('${{ matrix.cross }}') { "--cross-file", ".binding/${{ matrix.cross }}" } else { @() } | |
| meson setup build $env:TVG_COMMON.Split() --default-library=${{ matrix.kind }} @cross | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| meson compile -C build | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| # The library is named after the major version — thorvg-1.dll, libthorvg-1.so.1, | |
| # libthorvg-1.1.dylib — and on Unix the plain name is a symlink, which does not survive | |
| # NuGet packaging. Copy the real file under a stable name so DllImport("thorvg") keeps | |
| # resolving when ThorVG goes to 2.x. | |
| # | |
| # The iOS archives keep a libthorvg.a name on purpose: they resolve through __Internal | |
| # inside the executable, so the file name is irrelevant there. | |
| - name: Stage into the runtimes layout | |
| if: inputs.only-rid == '' || inputs.only-rid == matrix.rid | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| native="out/Evergine.Bindings.ThorVG/runtimes/${{ matrix.rid }}/native" | |
| mkdir -p "$native" | |
| case "${{ matrix.rid }}" in | |
| win-*) | |
| cp "$(find build -name 'thorvg*.dll' -print -quit)" "$native/thorvg.dll" ;; | |
| linux-*|android-*) | |
| cp "$(readlink -f "$(find build -name 'libthorvg*.so*' -print -quit)")" "$native/libthorvg.so" ;; | |
| osx-*) | |
| cp "$(readlink -f "$(find build -name 'libthorvg*.dylib' -print -quit)")" "$native/libthorvg.dylib" ;; | |
| ios*) | |
| cp "$(find build -name 'libthorvg*.a' -print -quit)" "$native/libthorvg.a" ;; | |
| esac | |
| find out -type f -exec ls -l {} \; | |
| # Three ways this build fails without failing: the wrong --default-library yields the | |
| # other kind of artifact, a missing -Dbindings=capi yields a library with no C API at all, | |
| # and a cross file with the wrong cpu_family silently builds for the host. All three would | |
| # upload happily and be committed as a working platform. | |
| - name: Verify the library is real and exports the C API | |
| if: inputs.only-rid == '' || inputs.only-rid == matrix.rid | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| lib=$(find out -type f | head -1) | |
| size=$(stat -c%s "$lib" 2>/dev/null || stat -f%z "$lib") | |
| echo "$lib: $size bytes" | |
| [ "$size" -gt 300000 ] || { echo "::error::$lib is only $size bytes"; exit 1; } | |
| # The symbol table is captured before it is searched rather than piped into grep -q: | |
| # under `set -o pipefail`, grep -q exits at the first match, nm dies of SIGPIPE, and | |
| # the pipeline reports failure precisely when the symbol WAS found. | |
| case "${{ matrix.rid }}" in | |
| linux-*|android-*) symbols=$(nm -D --defined-only "$lib" || true) ;; | |
| osx-*) symbols=$(nm -gU "$lib" || true) ;; | |
| # A static archive has no dynamic table; read the members' symbols instead. | |
| ios*) symbols=$(nm -g "$lib" 2>/dev/null || true) ;; | |
| # No dumpbin under bash and no strings on the Windows image; an export name is | |
| # plain ASCII in the export table, so reading the file as text finds it. | |
| win-*) symbols=$(grep -a -o 'tvg_engine_init' "$lib" || true) ;; | |
| esac | |
| case "$symbols" in | |
| *tvg_engine_init*) echo "tvg_engine_init is present." ;; | |
| *) echo "::error::tvg_engine_init is missing from $lib -- was -Dbindings=capi lost?"; exit 1 ;; | |
| esac | |
| - name: Upload | |
| if: inputs.only-rid == '' || inputs.only-rid == matrix.rid | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: natives-${{ matrix.rid }} | |
| path: out/ | |
| # Not `warn`: an empty artifact merges over the tree as no change at all, and the | |
| # publish would ship the previous libraries against a newer header. | |
| if-no-files-found: error |