Skip to content

fix: support 16 KB page sizes on 64 bit Android targets - #117

Open
freeKC wants to merge 1 commit into
GregoryConrad:mainfrom
freeKC:android-16kb-pages
Open

freeKC wants to merge 1 commit into
GregoryConrad:mainfrom
freeKC:android-16kb-pages

Conversation

@freeKC

@freeKC freeKC commented Oct 22, 2026

Copy link
Copy Markdown

Google Play requires the 64 bit native libraries of apps targeting Android 15 or later to support 16 KB page sizes, and from 1 February 2027 it will no longer accept updates that do not (Support 16 KB page sizes). NDK r28 and later align on 16 KB by default, but older NDKs align on 4 KB, and the Flutter tool hands the build hook whichever NDK it finds first (for example ANDROID_NDK_HOME, which is r27d on GitHub's Ubuntu runners).

What this changes:

  • For aarch64-linux-android and x86_64-linux-android, CARGO_TARGET_<TRIPLE>_LINKER now points to a small wrapper script in the hook's output directory. It runs the NDK's clang with -Wl,-z,max-page-size=16384 before rustc's own arguments. That is the flag NDK r28's clang already passes on its own (checked with clang -###), so with NDK r28c the built library is byte for byte the same as before.
  • Nothing goes through rustflags: a target.<triple>.rustflags would make Cargo ignore the user's build.rustflags, and RUSTFLAGS replaces everything. Since the flag comes first, a max-page-size set by the user still wins (LLD keeps the last one).
  • armv7-linux-androideabi and the other platforms are unchanged, as Play's rule only covers 64 bit libraries.
  • On Windows the wrapper is a .cmd that hands over to the NDK's own .cmd.
  • A short README section, and a step in the Android CI job that checks every LOAD segment of the example's library in the APK is aligned to at least 0x4000.

About older devices: a library aligned on 16 KB also loads on devices with 4 KB pages, and it is what NDK r28 already produces by default.

I did not add an option to turn it off, since a user's own -C link-arg=-Wl,-z,max-page-size=... takes precedence, but I can add one if you prefer.

How I checked it, on Linux x86_64 with NDK r27d and r28c and the examples' Rust 1.90.0:

  • flutter test test (7 new tests: the flags per target, the wrapper scripts, and running the wrapper), the examples' tests, flutter analyze --fatal-infos --fatal-warnings and dart format pass.
  • The dart_only example built through the hook (with testCodeBuildHook) for the three Android targets, LOAD alignment from readelf -lW:
arm64 x86_64 armv7
main, NDK r27d 0x1000 0x1000 0x1000
this branch, NDK r27d 0x4000 0x4000 0x1000
main and this branch, NDK r28c 0x4000, same file 0x4000, same file 0x1000, same file
  • With [build] rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=65536"] in a Cargo config, the result is 0x10000, so the user's flags are kept and win. With RUSTFLAGS="-C link-arg=-Wl,-soname,from_rustflags.so", the soname is set and the alignment is still 0x4000.
  • flutter build apk --debug of the Flutter example with ANDROID_NDK_HOME set to r27d: libflutter_example.so goes from 0x1000 to 0x4000 in arm64-v8a and x86_64. The new CI step fails on the APK from main and passes on this branch.
  • Windows: I could not build on a Windows host. I ran the generated .cmd against the real aarch64-linux-android35-clang.cmd of NDK r27d with a stub clang.exe, spawned with Rust's std::process::Command like rustc does: the arguments, including paths with spaces, arrive unchanged after the flag, and a failing link still exits with 1.
readelf -lW, dart_only example, aarch64, NDK r27d
main:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x0125d8 0x0125d8 R   0x1000
  LOAD           0x0125d8 0x00000000000135d8 0x00000000000135d8 0x037ed8 0x037ed8 R E 0x1000
  LOAD           0x04a4b0 0x000000000004c4b0 0x000000000004c4b0 0x002400 0x002b50 RW  0x1000
  LOAD           0x04c8b0 0x000000000004f8b0 0x000000000004f8b0 0x000998 0x001270 RW  0x1000
this branch:
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x0125d8 0x0125d8 R   0x4000
  LOAD           0x0125d8 0x00000000000165d8 0x00000000000165d8 0x037ed8 0x037ed8 R E 0x4000
  LOAD           0x04a4b0 0x00000000000524b0 0x00000000000524b0 0x002400 0x002b50 RW  0x4000
  LOAD           0x04c8b0 0x00000000000588b0 0x00000000000588b0 0x000998 0x001270 RW  0x4000

Fixes #116

NDKs older than r28 align the LOAD segments of shared libraries on 4 KB,
while Google Play requires the 64 bit libraries of apps targeting
Android 15 or later to support 16 KB page sizes. The Flutter tool can
hand such an NDK to the build hook (for example via ANDROID_NDK_HOME).

aarch64-linux-android and x86_64-linux-android are now linked through a
small wrapper around the NDK's clang that adds
-Wl,-z,max-page-size=16384, the flag NDK r28's clang passes on its own.
Nothing goes through rustflags, so the user's RUSTFLAGS and Cargo config
are left as they are, and a max-page-size set by the user still takes
precedence. With NDK r28 and later the output is unchanged, and
armv7-linux-androideabi is not affected.

The Android integration test job now also checks the alignment of the
example's library.

Fixes GregoryConrad#116
Comment thread README.md
Comment on lines +92 to +103

## Android 16 KB page sizes
On the 64 bit Android targets (`aarch64-linux-android` and `x86_64-linux-android`),
your library is linked with `-Wl,-z,max-page-size=16384`
so that it supports 16 KB page sizes, which
[Google Play requires](https://developer.android.com/guide/practices/page-sizes)
for apps targeting Android 15 or later.
NDK r28 and newer already do this by default, but older NDKs align on 4 KB.
The flag is added by a small wrapper around the NDK's `clang`,
so your own `rustflags` are left untouched;
if you need a different page size, pass it yourself
(e.g., `-C link-arg=-Wl,-z,max-page-size=4096`) and yours will take precedence.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can probably just leave this out for simplicity's sake

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally would prefer to keep this in the README. This is going to be a hot topic in the new year when they are forced to switch.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understandable--I normally just like to leave impl details out of public-facing docs like this. a nod to the 16kb page sizes would be fine tho

/// The contents of a script that invokes [linker] with [flags]
/// followed by all of the arguments the script itself was given.
@visibleForTesting
static String linkerWrapperScript({

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to do this without a script? i.e., can we just inject an env variable to cargo by default (on the appropriate targets) if the user doesn't already set it (or a conflicting variable) themselves? making a wrapper script for this hurts my soul a bit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android libraries are 4 KB aligned when the build hook gets an NDK older than r28 (Google Play 16 KB rule)

3 participants