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.