whoami

Lucas Jenß

cat /etc/motd

The Coding Journal ツ — Notes taken on an epic coding journey. Technical solutions, debugging notes, and practical guides from the trenches of software development.

Close-up of white dominoes with black dots standing on green felt surface, shallow depth of field, focused mood

ls -la ~/languages/

total 8
drwxr-xr-x
▶ PHP
▶ Ruby
▶ Scala
▶ C#
▶ JavaScript
▶ Objective-C
▶ Shell Scripting

ls -la ~/toolchain/

total 7
drwxr-xr-x
▶ Typo3
▶ Akka
▶ Capistrano
▶ Git
▶ MAMP
▶ Adobe Illustrator
▶ NSTrackingArea (Cocoa)

uname -a

platforms
drwxr-xr-x
▶ Mac OS X
▶ Unix

Building Android NDK shared libraries with CMake instead of ndk-build

For years, Android developers writing C and C++ code have relied on ndk-build to compile their native modules. While the tool still ships with the NDK, its role has steadily shrunk. Google now recommends CMake for new projects, and the Android Studio tooling has followed suit. In practice, this shift has rippled through Australian dev shops from Melbourne to Brisbane, where studios maintain mixed codebases and want a build system that survives the next NDK release.

CMake brings a unified syntax, broader platform support and better integration with modern IDEs. It also lets you reuse the same build configuration when targeting Linux servers, Windows desktops or iOS, which matters for teams that share code between mobile and embedded products. The transition is not entirely painless, but the long-term payoff usually justifies the upfront effort.

Why CMake is taking over from ndk-build

Google's official guidance has been clear for several releases: ndk-build is in maintenance mode, while CMake is the recommended path for native Android code. The Android NDK documentation itself now points new users toward CMake, and the Gradle plugin has steadily reduced its ndk-build integration in favour of externalNativeBuild blocks.

The second reason is tooling. Android Studio's C++ support, including syntax highlighting, indexing and debugging, works best with CMake. Clangd-based language services slot in cleanly, and the IDE can navigate symbols across Java, Kotlin and C++ files without extra configuration. For a developer in Sydney juggling a JNI bridge, a Flutter plugin and a custom renderer, that single workflow saves a lot of friction.

A third factor is the broader ecosystem. Many open-source C++ libraries already ship with CMakeLists.txt files. Reusing them through add_subdirectory or FetchContent is often simpler than translating their build scripts into Android.mk.

Feature CMake ndk-build
Status in NDK Recommended, actively developed Maintenance mode
Build configuration CMakeLists.txt files Android.mk + Application.mk
IDE integration First-class in Android Studio Limited and shrinking
Cross-project reuse Works on iOS, Linux, Windows Android only
Dependency management FetchContent, find_package, vcpkg Manual setup
Build speed (large projects) Fast with Ninja generator Slower on parallel builds

Setting up your project for CMake-based NDK builds

The Gradle side of the configuration is usually the first hurdle. In your module-level build.gradle, replace the old ndkBuild block with an externalNativeBuild block that points at your CMakeLists.txt. The NDK version and the list of ABIs go under the defaultConfig ndk section, just as before.

Set the path explicitly. A typical setup includes externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.22.1" } }. Pinning the CMake version avoids surprises when a colleague upgrades Android Studio and the bundled toolchain changes underneath you.

Local development on Australian NBN connections can feel slow when the Gradle daemon decides to redownload toolchain components. Add the NDK and CMake paths to your local.properties or set ANDROID_NDK_HOME in your shell to keep everything reproducible across the team. For studios spread between Brisbane, Melbourne and Perth, a shared toolchain config also makes handovers far less painful.

Writing a working CMakeLists for shared libraries

The heart of the migration is the CMakeLists.txt itself. A minimal but production-ready version looks like this:

cmake_minimum_required(VERSION 3.22.1) project("mynative" LANGUAGES C CXX) add_library(mynative SHARED src/main/cpp/native.cpp) target_link_libraries(mynative PRIVATE log) target_compile_options(mynative PRIVATE -Wall -Wextra)

The SHARED keyword tells CMake to produce a .so file, which is what Android expects for System.loadLibrary calls. If you forget the PRIVATE, PUBLIC or INTERFACE keywords on target_link_libraries, you may end up with missing symbols when another module consumes your library.

For more complex setups, split your code into multiple libraries and use target_link_libraries to express the dependency graph. This works much like Gradle's implementation and api configuration, and it scales better than a single monolithic Android.mk. The same pattern is useful when you maintain other JVM-adjacent stacks; the debugging-a-php-memory-limit-error-in-a-legacy-typo3-extension walkthrough covers a similar tracing approach through layered code, and the habits transfer well.

Common pitfalls and how to avoid them

Several issues trip up teams making the jump. Header search paths configured with include_directories end up global, which can clash with transitive dependencies. Prefer target_include_directories with the PRIVATE or PUBLIC scope, and you will get the same hygiene you expect from a well-graded Java module.

Symbol stripping is another frequent source of confusion. ndk-build applies a default strip step, but CMake does not unless you add it. For release builds, append $<$CONFIG:Release:-Wl,--strip-all> to your linker flags, otherwise your APK grows by several megabytes for every ABI.

ABI-specific code paths also deserve attention. If you maintain per-ABI source files, use ANDROID_ABI in if() blocks inside your CMakeLists rather than separate Android.mk files. That keeps related logic in one place and makes diff reviews easier.

Configuration drift between debug and release builds can mask problems until they reach testers. Run a release build locally with the same flags your CI uses, and add a small set of assertions at JNI entry points. If you have spent time chasing understanding-and-fixing-scala-akka-dead-letters-warnings, the discipline of instrumenting message boundaries will feel familiar.

Practical recommendations for a smooth migration

Migrating an existing project does not have to be a big-bang rewrite. The following steps have served Australian teams well and keep risk contained while the build system evolves.

  • Convert one module at a time, keeping the old ndk-build configuration as a fallback during the transition.
  • Pin your CMake version in build.gradle and document it in a CONTRIBUTING note, so contributors do not pull a newer toolchain accidentally.
  • Use the Ninja generator (-G Ninja) for faster local builds, especially on laptops with limited thermal headroom during the Australian summer.
  • Keep your JNI bindings thin and put business logic in regular C++ classes that are unit-testable on the host.
  • Run ABI matrix tests through Firebase Test Lab or a similar service before declaring the migration done.
  • Set up ABI filters in defaultConfig.ndk so you only build the ABIs your users actually need, trimming APK size significantly.
  • Document your native build in a short README inside src/main/cpp so the next maintainer can get up to speed quickly.

For ongoing experiments and side projects, some developers publish notes on personal sites such as the narmadi.net blog, where smaller CMake recipes and post-mortems often appear before they make it into a formal blog post or conference talk.

Take the first step by switching your next new module to CMake from day one. The Gradle template in Android Studio already wires it up, so you will inherit a build configuration that lines up with the rest of the C++ ecosystem. Run a full release build on a real device, verify that the .so files load without UnsatisfiedLinkError, and commit the CMakeLists alongside the Java code so the two evolve together.

Once the build is green, layer in ccache or sccache support. Repeated clean builds drop from minutes to seconds, which makes native iteration feel closer to the JVM feedback loop Australian engineers expect. That single improvement often decides whether the team embraces native code or quietly routes around it.


cat ~/interests.json

KeyValue
editorTerminal-first workflow
osMac OS X / Unix
vcsGit, distributed version control
deployCapistrano, cron automation
graphicsSVG, Adobe Illustrator troubleshooting
networkingIP validation, SSH, VPN

git log --oneline --reverse

2013-10-30

Solving SVG import issues in Adobe Illustrator CS6 and CC

When importing an SVG into Illustrator, the operation fails with an unknown error [CANT]. A workaround for this Adobe-side bug.

2013

Solving NDK build issues on OS X

Troubleshooting native development kit compilation problems on Mac OS X.

2013

Programmatically adding PHP generated TypoScript to the backend configuration

Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.

2013

ArgumentError: Could not parse PKey: no start line

Debugging an SSH key parsing error encountered during deployment.

2011-08-04

Validating IP-Addresses in PHP

Using PHP filter functions with flags like FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6, and understanding how filter_var handles reserved IP addresses.

2011-07-09

Cocoa: Using NSTrackingArea

A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.


cat ~/contact.txt

github: github.com/x3ro
stackoverflow: x3ro
coderwall: coderwall.com/x3ro
twitter: @x3rames