Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/check-16k-page-alignment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Check 16 KB page alignment

# Google Play requires every 64-bit native library in an upload to be linked
# for 16 KB memory pages: API 35+ uploads since 2025-11-01, and Wear OS uploads
# containing native code from 2026-09-15.
# See https://developer.android.com/guide/practices/page-sizes.
#
# Alignment is decided at link time, so nothing downstream can repair it -- not
# AGP's ZIP alignment, not bundletool, not the developer. A Codename One
# artifact that gets it wrong ships a broken app the developer cannot fix, and
# the breakage is invisible until Play review or a real 16 KB device: the app
# builds and runs on every 4 KB device in the office.
#
# Deliberately its own workflow rather than a step in pr.yml, whose paths filter
# would let a PR that only adds a native artifact skip the check. The scan is a
# ZIP listing plus an ELF header read, so running it on every PR costs nothing.

permissions:
contents: read

on:
workflow_dispatch:
pull_request:
branches:
- master
push:
branches:
- master

jobs:
check-16k-page-alignment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Unit-test the gate's classification boundaries
# Every case in here is one a review caught: something unreadable
# passing as compliant, or a valid artifact rejected by a rule that
# does not apply to it. Run before the scan, so a broken gate is
# reported as a broken gate rather than as a clean tree.
shell: bash
run: python3 scripts/test_check_16k_page_alignment.py

- name: Scan tracked native artifacts for 16 KB page alignment
shell: bash
run: scripts/check-16k-page-alignment.py --verbose
44 changes: 44 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,50 @@ Platform-specific native code locations:
- **Android**: Within Android port module (Java/Kotlin)
- **JavaSE**: Within JavaSE port (Java)

#### Prebuilt Android `.so` files must be 16 KB page aligned

Google Play requires every **64-bit** shared library in an upload to be linked
for 16 KB memory pages -- API 35+ uploads since 2025-11-01, Wear OS uploads
containing native code from 2026-09-15. Each `PT_LOAD` segment needs `p_align`
of `0x4000` or more (<https://developer.android.com/guide/practices/page-sizes>).

Alignment is settled at **link** time. AGP aligns the ZIP entries and bundletool
reports the outcome, but nothing downstream can move a segment inside a prebuilt
`.so`, so a Codename One artifact that gets it wrong ships a broken app the
developer cannot fix from their own sources. And it is silent: the app builds
and runs on every 4 KB device, and only Play review or a real 16 KB device says
otherwise.

Build any Android native artifact with **NDK r28 or newer**. r28 is the first
release that links this way by default, and the first whose prebuilt
`libc++_shared.so` and `libomp.so` are aligned -- those are copied out of the
NDK, so `-Wl,-z,max-page-size=16384` cannot fix them on r26/r27.

Separately, the NDK's own floor has been API 21 since r26, so any AAR built
here needs `minSdk 21` and an entry in `PlatformFeatureCatalog` with
`.androidMinimumSdk(21)` to lift the application's default floor of 19 past the
manifest merger. A lower `minSdk` in the library is a claim the `.so` files do
not honour -- check `.note.android.ident`, which records the API level the
slice was really compiled against.

`scripts/check-16k-page-alignment.py` gates this over every tracked archive and
loose `.so` in the tree, in its own workflow
(`.github/workflows/check-16k-page-alignment.yml`) rather than inside `pr.yml`,
whose `paths` filter would let a PR that only adds a native artifact skip it.
Containers and libraries are found by magic bytes, not by extension, so a
`.cn1lib` or any future container is covered.

The rule reaches only 64-bit libraries under Android packaging. 32-bit slices
are exempt because 16 KB pages are a 64-bit feature, and a desktop `.so` --
which a cn1lib can ship through `nativelinux`/`nativese` -- is correct at
0x1000. Integrity is not scoped that way: a `.so` that is not an ELF, or is
truncated, fails wherever it sits.

```bash
scripts/check-16k-page-alignment.py # every tracked artifact
scripts/check-16k-page-alignment.py --verbose # list everything scanned
```

#### ParparVM native names are checked, and getting one wrong is silent

ParparVM encodes the **whole Java signature in the C function name**. For
Expand Down
60 changes: 60 additions & 0 deletions maven/cn1-ai-whisper/android-aar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,63 @@ WHISPER_INCLUDE_DIR=/path/to/whisper.cpp/include \
`arm64-v8a/libwhisper.so`. The build script copies the resulting AAR to
`../android/src/main/resources/cn1-ai-whisper-android.aar`, where the cn1lib
packager and Android builder already know how to pick it up.

## whisper.cpp version

The checked-in AAR is built from whisper.cpp `v1.9.1-77-g0874de3e`:

```sh
git clone https://github.com/ggml-org/whisper.cpp.git
git -C whisper.cpp checkout 0874de3e
```

Record the new commit here whenever the AAR is rebuilt against a different one.
Nothing in the tree derives it, and the only reason it was recoverable the last
time was that the version and the abbreviated hash happened to survive as
strings inside `libcn1aiwhisper.so`.

## 16 KB memory pages

Google Play requires every **64-bit** shared library in an upload to be linked
for 16 KB memory pages -- for API 35+ uploads since 2025-11-01, and for Wear OS
uploads containing native code from 2026-09-15. In ELF terms every `PT_LOAD`
segment needs `p_align` of `0x4000` or more. See
<https://developer.android.com/guide/practices/page-sizes>.

This is decided at **link** time. AGP aligns the ZIP entries and bundletool
reports the outcome, but neither can move a segment inside a prebuilt `.so`, so
an AAR that gets this wrong cannot be repaired by the application that consumes
it -- and the failure is silent until Play review or a real 16 KB device,
because the app builds and runs on every 4 KB device.

Two things keep it right, and both are needed:

- `build.gradle` pins **NDK r28** (`28.2.13676358`). r28 is the first release
that links this way by default, and, more to the point, the first whose
prebuilt `libc++_shared.so` and `libomp.so` are aligned. Those two are copied
into the AAR from the NDK itself, so no flag set in this project can fix them
on r26/r27.
- `src/main/cpp/CMakeLists.txt` passes `-Wl,-z,max-page-size=16384` and
`-Wl,-z,common-page-size=16384`, which covers `libcn1aiwhisper.so` if this
project is ever driven by an older NDK or a standalone CMake invocation.

The library also declares `minSdk 21` now. That is not a consequence of the NDK
bump: the NDK's own floor has been API 21 since r26, so every slice this project
has ever shipped carries API 21 in its `.note.android.ident` while the AAR
manifest claimed 19. The old value was a claim the native code never honoured,
and an API 19 install would have failed at `System.loadLibrary`. Codename One's
default `android.min_sdk_version` is 19, so `PlatformFeatureCatalog` raises an
application's floor to 21 when it references `com.codename1.ai.whisper`;
without that the manifest merger rejects the build.

Verify a rebuild before committing it:

```sh
scripts/check-16k-page-alignment.py --verbose
```

It runs on every PR as `.github/workflows/check-16k-page-alignment.yml` and
covers every native artifact in the tree, not just this one. It finds
containers and libraries by magic bytes rather than by extension, and applies
the alignment rule only to 64-bit libraries under Android packaging -- a
desktop `.so` shipped through `nativelinux`/`nativese` is correct at 0x1000.
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,27 @@ def whisperIncludeDir = project.findProperty('whisperIncludeDir') ?: System.gete
android {
namespace 'com.codename1.ai.whisper.android'
compileSdk 33
ndkVersion '26.3.11579264'
// NDK r28 is the first release that links shared libraries with 16 KB
// aligned PT_LOAD segments by default, which Google Play requires of every
// 64-bit native library (API 35+ uploads since 2025-11-01, Wear OS from
// 2026-09-15). The requirement covers libc++_shared.so and libomp.so too,
// and those are NDK *prebuilts* -- no linker flag applied to this project
// can realign them, so moving the NDK forward is the only fix rather than
// one option among several. scripts/check-16k-page-alignment.py gates the
// staged AAR.
ndkVersion '28.2.13676358'

defaultConfig {
minSdk 19
// 21 is what the native code has always actually required: the NDK's
// own floor has been API 21 since r26 (meta/platforms.json "min": 21),
// so the previous `minSdk 19` was a manifest claim the .so files never
// honoured -- every shipped slice, 32-bit included, carries API 21 in
// its .note.android.ident. Declaring it makes the AAR honest instead
// of letting an API 19 install fail at System.loadLibrary.
// PlatformFeatureCatalog raises a consuming app's
// android.min_sdk_version to match, so the manifest merge still
// succeeds for a default (19) project.
minSdk 21
externalNativeBuild {
cmake {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
cmake_minimum_required(VERSION 3.22.1)
project(cn1_ai_whisper_android LANGUAGES C CXX)

# Google Play requires every 64-bit shared library in an upload to have
# PT_LOAD segments aligned to 16 KB (API 35+ uploads since 2025-11-01, Wear OS
# from 2026-09-15) -- see https://developer.android.com/guide/practices/page-sizes.
# NDK r28, which build.gradle pins, already does this by default; these flags
# are what keeps the result correct if this project is ever driven by an older
# NDK or a standalone CMake invocation, where the default is still 4 KB and the
# breakage is silent (the library loads fine on every 4 KB device).
#
# Set before any target is declared so it also reaches the whisper.cpp
# subproject added below.
add_link_options(-Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384)

add_library(cn1aiwhisper SHARED native_whisper_jni.cpp)
target_compile_features(cn1aiwhisper PRIVATE cxx_std_17)

Expand Down
Binary file not shown.
Loading
Loading