I struggled recently to work out how to build an APK out of a native Android app without using Gradle.

Android is pretty good these days at letting you build your app without needing to write any Java, thanks to NativeActivity and Native App Glue, but even in the official native activity example Gradle is still used as the build tool in order to package up an APK.

In a C++-based codebase, such as in my case a multiplatform game engine, we really don’t want to introduce additional build tools and configurations just for one platform. It would be great if we could instead put together our APK using just the command line and the tools provided in the NDK. Turns out we can!

I’m going to assume you’re already set up with the NDK and so on. I have my Android SDK root location in the environment variable ANDROID_SDK.

Building

For the purposes of this tutorial I’m going to use the native activity example from the ndk-samples repository. If you’ve already built your Android application into a shared native library then feel free to skip this section.

Within the ndk-samples repo, navigate to native-activity/app/src/main/cpp. I’m building the shared library binary using CMake’s built-in Android support - if you’re using the old toolchain or similar feel free to modify the command to suit your build setup.

I create a build directory for an out-of tree build, navigate into it, and run the following incantation:

cmake -DCMAKE_SYSTEM_NAME="Android" \
  -DCMAKE_ANDROID_NDK="${ANDROID_SDK}/ndk/25.1.8937393" \ # NDK location for CMake
  -DANDROID_NDK="${ANDROID_SDK}/ndk/25.1.8937393" \ # NDK location needed by the project's CMakeLists.txt
  ..

And build it:

make

You should now have a libnative-activity.so in your build directory.

Packaging

We need to set up the directory structure that will represent the inside of our APK. We just need to define where our shared library object will live. This is dependent on which ABI you’re building against - I’m using armeabi-v7a here.

Make a directory structure like this. The top-level name apk here can be whatever you like, it doesn’t get bundled.

apk/
    lib/
        armeabi-v7a/

You can create it like so:

mkdir -p apk/lib/armeabi-v7a

For the sake of speed I’m going to strip the library and put it into our directory structure. If you want to keep your debug symbols in, you can just copy it. Note that the path to your toolchain binaries will vary depending on which platform you’re working on (I’m on macOS, hence darwin-x86_64).

"${ANDROID_SDK}/ndk/25.1.8937393/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-strip" \
  libnative-activity.so \
  -o ./apk/lib/armeabi-v7a/libnative-activity.so

Now it’s time to create the actual APK:

"${ANDROID_SDK}/build-tools/30.0.3/aapt" package \
  -f \ # Overwrite the APK if it exists
  -M ../../AndroidManifest.xml \ # The manifest
  -I ~/Code/android-sdk/platforms/android-33/android.jar \ # The Android library
  -S ../../res \ # Resources (icons, values, etc.)
  -F apk-unaligned.apk \ # Output file
  apk # Our directory structure we created previously

Align and sign

Before the APK can be installed on a device, it needs to be zipaligned and signed.

zipalign is a tool which aligns files within an archive (like our APK) relative to the start of the file. This means your app can read stuff out of the APK like it would read from memory directly, rather than needing to copy stuff into RAM first.

The incantation is like this:

"${ANDROID_SDK}/build-tools/30.0.3/zipalign" -f 4 apk-unaligned.apk apk-unsigned.apk

You’re probably already familiar with signing - it validates that the APK is created by us and not a malicious third party. Assuming you’ve already set up a debug or release keychain, you can sign like this:

"${ANDROID_SDK}/build-tools/30.0.3/apksigner sign \
  --ks ~/.android/debug.keystore \
  --ks-key-alias=androiddebugkey \
  --out apk-signed.apk \
  apk-unsigned.apk

Enter your password (it’s android for the default debug keystore) and you’ll have a signed APK ready to run on a device!

Deploying

You can install your new APK onto a real Android device or emulator using adb:

"${ANDROID_SDK}/platform-tools/bin/adb" install -r apk-signed.apk

Here’s the native example running on my relatively ancient HTC One M8. It’s on Android 5! Android’s backwards-compatibility is impressive.

Be aware that this APK will only support devices with the ABI you built against. You can either build your library for each ABI (a so-called ‘fat APK’) or you can create one of the newer Android App Bundles, which I’ll cover in a separate blog post once I work out how the hell they work.

So far I’ve been pretty impressed by Android’s tooling and rather less impressed by its documentation. Let’s see how things progress.