From 3220577f0a7d2b19c76010b2061342590491ef8c Mon Sep 17 00:00:00 2001 From: jcant0n Date: Sun, 9 Aug 2026 15:39:20 +0200 Subject: [PATCH] feat: verify both android libraries reach the APK The README has said it plainly since this morning: android-arm and android-arm64 ship and nothing verifies them. This is the leg that does, ported from Cesium.NET's, where it is green against the real .nupkg. One APK, both ABIs, which is where this differs from Cesium's single-ABI equivalent: the smoke project multi-targets android-arm and android-arm64, so a single build has to put lib/armeabi-v7a/libJoltC.so and lib/arm64-v8a/libJoltC.so in the APK, each over a megabyte, with no other ABI alongside. The build succeeding proves none of that -- a package can carry runtimes/android-*/native and lose them on the way in -- so the leg opens the APK and looks. The activity is not run. No runner can execute an APK without an emulator, and standing one up per release was decided against in Cesium and holds here. The calls in MainActivity exist so the binding assembly is genuinely referenced rather than trimmed, and the README row says the libraries themselves are not executed. Verified locally before pushing, against a package packed from this branch: the APK carries both libraries, 27.0 MB and 23.1 MB uncompressed, and exactly the two expected ABIs. --- .github/workflows/package-smoke.yml | 115 ++++++++++++++++++++++- AndroidSmokeTest/AndroidSmokeTest.csproj | 34 +++++++ AndroidSmokeTest/MainActivity.cs | 48 ++++++++++ AndroidSmokeTest/NuGet.config | 8 ++ README.md | 4 +- 5 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 AndroidSmokeTest/AndroidSmokeTest.csproj create mode 100644 AndroidSmokeTest/MainActivity.cs create mode 100644 AndroidSmokeTest/NuGet.config diff --git a/.github/workflows/package-smoke.yml b/.github/workflows/package-smoke.yml index 25f7eaf..ad83648 100644 --- a/.github/workflows/package-smoke.yml +++ b/.github/workflows/package-smoke.yml @@ -4,9 +4,11 @@ # produced, drops a sphere onto a floor through the native library, and fails the job if the # sphere does not fall, does not settle, or falls through. # -# Six of the ten identifiers, and the gap is honest rather than an oversight: android-arm, -# android-arm64, ios-arm64 and iossimulator-arm64 ship in the package and no GitHub runner can -# execute a console application on them. They are built and never run. +# Eight of the ten identifiers. ios-arm64 and iossimulator-arm64 cannot be *run* on a runner, +# but the simulator is linked and its executable inspected; android cannot be run either, but an +# APK is built against the package and opened, and both libJoltC.so have to be inside it. What +# remains unexecuted anywhere is ios-arm64 (a device build needs a signing identity) and the +# android libraries themselves, which is stated in the README rather than glossed. # # browser-wasm is the sixth and it needs its own job rather than a row in the matrix, because # nothing about it resembles the others: the archive is linked into the application when it @@ -346,3 +348,110 @@ jobs: fi echo " ok JoltC_Init is defined (T) in the executable" echo "PASS" + + # Builds an APK against the package and then opens it. `dotnet build` succeeding proves the + # managed side compiled and says nothing about whether the native libraries travelled with it, + # which is the only question this leg exists to answer. + # + # One APK, both ABIs. Unlike Cesium.NET's equivalent, this package ships android-arm and + # android-arm64, so the project multi-targets both runtime identifiers and a single build + # verifies both libraries reached the APK. + android: + name: android + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.x' + + - name: Install the android workload + run: dotnet workload install android + + - name: Fetch the package from this run + if: inputs.cd-run-id == '' + uses: actions/download-artifact@v4 + with: + name: nuget-packages + path: AndroidSmokeTest/local-packages + + - name: Fetch the package from an earlier run + if: inputs.cd-run-id != '' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir -p AndroidSmokeTest/local-packages + gh run download "${{ inputs.cd-run-id }}" --repo "$GITHUB_REPOSITORY" -n nuget-packages -D AndroidSmokeTest/local-packages + + - name: Work out which version is under test + shell: bash + run: | + set -euo pipefail + ls -la AndroidSmokeTest/local-packages/ + + pkg=$(ls AndroidSmokeTest/local-packages/*.nupkg | head -1) + version=$(basename "$pkg" .nupkg | sed 's/^Evergine\.Bindings\.JoltPhysics\.//') + if [ -z "$version" ] || [ "$version" = "$(basename "$pkg" .nupkg)" ]; then + echo "::error::could not read a version out of '$(basename "$pkg")'" + exit 1 + fi + echo "JOLT_VERSION=$version" >> "$GITHUB_ENV" + echo "testing $version on android" + + - name: Build an APK against the package + shell: bash + working-directory: AndroidSmokeTest + run: | + set -euo pipefail + dotnet build -c Release -p:JoltPhysicsVersion="$JOLT_VERSION" + + # The assertion. A package can carry runtimes/android-*/native and still lose them on the + # way into the APK, and the build would be green either way. + - name: Check both native libraries are inside the APK + shell: bash + working-directory: AndroidSmokeTest + run: | + set -euo pipefail + apk=$(find bin -name '*-Signed.apk' | head -1) + [ -n "$apk" ] || { echo "::error::no APK was produced"; exit 1; } + + echo "APK: $apk" + python3 - "$apk" <<'CHECK' + import sys, zipfile, os + + apk = sys.argv[1] + z = zipfile.ZipFile(apk) + wanted = ["lib/armeabi-v7a/libJoltC.so", "lib/arm64-v8a/libJoltC.so"] + + libs = sorted(n for n in z.namelist() if n.startswith("lib/")) + print(" APK is %.1f MB and carries %d native libraries" % (os.path.getsize(apk) / 1e6, len(libs))) + + bad = 0 + for w in wanted: + if w not in libs: + print("::error::%s is not in the APK. The package carried it and the build dropped it." % w) + bad += 1 + continue + size = z.getinfo(w).file_size + # A library that is present and empty would satisfy presence and nothing else. + if size < 1_000_000: + print("::error::%s is only %d bytes; that is not JoltC." % (w, size)) + bad += 1 + continue + print(" ok %s (%.1f MB uncompressed)" % (w, size / 1e6)) + + # Every ABI here is one the package had to supply. Anything else means the project + # asked for a runtime identifier JoltPhysicsC does not build. + abis = {n.split("/")[1] for n in libs} + if abis != {"armeabi-v7a", "arm64-v8a"}: + print("::error::unexpected ABIs in the APK: %s" % ", ".join(sorted(abis))) + bad += 1 + + if bad: + sys.exit(1) + print("PASS") + CHECK diff --git a/AndroidSmokeTest/AndroidSmokeTest.csproj b/AndroidSmokeTest/AndroidSmokeTest.csproj new file mode 100644 index 0000000..d8d7e15 --- /dev/null +++ b/AndroidSmokeTest/AndroidSmokeTest.csproj @@ -0,0 +1,34 @@ + + + + net10.0-android + 21 + Exe + disable + true + com.evergine.joltsmoketest + 1 + 1.0 + + + android-arm;android-arm64 + + + + + + + + diff --git a/AndroidSmokeTest/MainActivity.cs b/AndroidSmokeTest/MainActivity.cs new file mode 100644 index 0000000..2bcfb66 --- /dev/null +++ b/AndroidSmokeTest/MainActivity.cs @@ -0,0 +1,48 @@ +// Exists so the package has to be consumed by something that really builds for Android. +// +// It is not run. No GitHub runner can execute an APK without an emulator, and standing one up +// per release was decided against. What the CI leg asserts instead is that libJoltC.so ends up +// inside the APK under lib/armeabi-v7a/ and lib/arm64-v8a/ -- which is the class of mistake +// this package actually shipped for months on wasm and iOS: a native library present in the +// package and absent from where the platform looks for it. A build that merely compiles would +// not notice either. +// +// The calls below are here so the binding assembly is genuinely referenced rather than trimmed +// away as unused. They run on a device and never in CI. + +using Android.App; +using Android.OS; +using Android.Widget; +using Evergine.Bindings.JoltPhysics; + +namespace AndroidSmokeTest; + +[Activity(Label = "Jolt smoke", MainLauncher = true)] +public class MainActivity : Activity +{ + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + + // The same first calls the desktop leg makes. Reaching them at all means the native + // library was found and loaded out of the APK. + JoltPhysics.RegisterDefaultAllocator(); + JoltPhysics.Init(); + JoltPhysics.CreateFactory(); + JoltPhysics.RegisterTypes(); + + System.IntPtr tempAllocator = JoltPhysics.TempAllocator_Create(4 * 1024 * 1024); + System.IntPtr jobSystem = JoltPhysics.JobSystemThreadPool_Create(1024, 8, 1); + + bool ok = tempAllocator != System.IntPtr.Zero && jobSystem != System.IntPtr.Zero; + + var text = new TextView(this) + { + Text = ok + ? $"JoltC loaded: allocator=0x{tempAllocator:x} jobs=0x{jobSystem:x}" + : "JoltC did not load", + }; + + SetContentView(text); + } +} diff --git a/AndroidSmokeTest/NuGet.config b/AndroidSmokeTest/NuGet.config new file mode 100644 index 0000000..9f7da04 --- /dev/null +++ b/AndroidSmokeTest/NuGet.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/README.md b/README.md index 331bbb5..c9e42b1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This repository contains low-level bindings for [JoltPhysics](https://github.com - [x] iOS Simulator ARM64 - [x] Browser WASM -Ten runtime identifiers ship. Seven of them are checked against the real `.nupkg` before a +Ten runtime identifiers ship. Nine of them are checked against the real `.nupkg` before a release is published, and a failure stops the publish. What that check is worth differs, and the difference is worth knowing rather than glossing: @@ -41,7 +41,7 @@ difference is worth knowing rather than glossing: | `browser-wasm` | the same, under node, against the archive linked into the application at publish time | | `iossimulator-arm64` | an application is linked against the package and its executable has to **define** the entry points, not merely reference them | | `ios-arm64` | **not verified directly.** A device build needs a signing identity CI does not have. It links the same archive through the same targets file as the simulator, so the evidence is indirect | -| `android-arm`, `android-arm64` | **not verified.** They are ordinary shared libraries loaded the way the desktop ones are, but nothing installs the package on Android today | +| `android-arm`, `android-arm64` | an APK is built against the package and opened, and both `libJoltC.so` have to be inside it, over a megabyte each, with no other ABI alongside. The libraries themselves are not executed | iOS works differently from every other identifier and you do not have to do anything about it: Apple only lets an application load dynamic libraries that ship inside its own bundle, so the