A native link can succeed and still leave an Android app broken at startup. That happens when build-time symbol resolution, APK packaging, and runtime loading are treated as one step. In Android.mk, choose the dependency model first: platform library, source-built module, or prebuilt module. The variable follows from that decision.
The three dependency models
NDK system library: supplied by the Android platform/toolchain with public NDK APIs; link with an appropriate
-lflag.Source-built module: another
Android.mkmodule built in the same graph; reference its module name.Prebuilt shared library: one
.soper supported ABI, declared as a prebuilt module and packaged for runtime.Prebuilt static library: an ABI-specific
.aarchive whose object code is copied into the consuming link output.Header-only dependency: no linker input, but include paths and compile definitions still need a reproducible interface.
1. Link public NDK system libraries
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native_core
LOCAL_SRC_FILES := native_core.c
LOCAL_LDLIBS := -llog -lz
include $(BUILD_SHARED_LIBRARY)What LOCAL_LDLIBS actually contributes
LOCAL_PATHanchors relative paths to the directory containing this makefile.CLEAR_VARSresets per-module variables while preserving global values such asLOCAL_PATH.LOCAL_MODULEnames the module; ndk-build emits a library name derived from it.-llogresolves public Android logging APIs fromliblog;-lzresolves zlib APIs fromlibz.LOCAL_LDLIBScarries raw linker flags for this executable/shared-library link, not header search paths or APK copy instructions.
Include the matching public headers
#include <android/log.h>
#include <zlib.h>
int compress_bound_for_log(unsigned long input_size) {
const unsigned long bound = compressBound(input_size);
__android_log_print(ANDROID_LOG_INFO, "NativeCore",
"compressed bound=%lu", bound);
return bound > (unsigned long)INT_MAX ? -1 : (int)bound;
}Compilation and linking check different promises
Headers declare function signatures and constants to the compiler.
The link step resolves
compressBoundand__android_log_printthrough the libraries named above.A successful compile with unresolved link symbols usually means the declaration was visible but the defining library was absent or ordered incorrectly.
The narrowing conversion is guarded; production code should choose return types that preserve the actual range.
Only APIs exposed by the NDK for the selected minimum API level are supported application interfaces.
2. Link another module built from source
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := codec_support
LOCAL_SRC_FILES := codec/support.c
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/codec/include
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := native_core
LOCAL_SRC_FILES := native_core.c
LOCAL_STATIC_LIBRARIES := codec_support
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)Module names build a dependency graph
BUILD_STATIC_LIBRARYcreates an archive for the active ABI.The consumer references
codec_support, theLOCAL_MODULEvalue—not a guessed archive filename.LOCAL_EXPORT_C_INCLUDESmakes public headers available to modules that depend on this module.Private headers can use
LOCAL_C_INCLUDESwithout exporting them downstream.ndk-build uses the graph to order builds and link inputs more reliably than raw archive paths.
Static and shared linkage are not interchangeable
A static archive contributes needed object code to the final
.so; the archive itself is not loaded on the device.A shared dependency remains a distinct
.soand must be present for the device ABI at runtime.Static linking can duplicate code/runtime state across several shared libraries.
Shared linkage can reduce duplication but adds runtime dependency, symbol-visibility, loading, and packaging concerns.
License obligations, security updates, C++ runtime choice, binary size, process boundaries, and plugin architecture affect the decision.
3. Declare a prebuilt shared library per ABI
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := vendor_codec
LOCAL_SRC_FILES := libs/$(TARGET_ARCH_ABI)/libvendor_codec.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)The path selects one binary for the active ABI
A typical layout contains
libs/arm64-v8a/libvendor_codec.so,libs/armeabi-v7a/..., andlibs/x86_64/...only for ABIs the app supports.TARGET_ARCH_ABIis supplied by ndk-build for the current target.The prebuilt file must match the Android ABI, API requirements, ELF class/machine, and C/C++ binary interface expected by the consumer.
Exported includes describe the vendor library’s public compile-time interface.
A prebuilt module declaration lets the build graph reason about the dependency and package shared output appropriately through the Android Gradle Plugin integration.
Consume the prebuilt module
LOCAL_PATH := $(call my-dir)
include $(LOCAL_PATH)/vendor/Android.mk
include $(CLEAR_VARS)
LOCAL_MODULE := native_core
LOCAL_SRC_FILES := native_core.cpp
LOCAL_SHARED_LIBRARIES := vendor_codec
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)Include order and module uniqueness matter
Including the vendor makefile registers the
vendor_codecmodule.LOCAL_SHARED_LIBRARIESadds it as a build/link/runtime dependency.Each module name in the graph should be unambiguous.
Every module begins after
CLEAR_VARS; otherwise per-module state can leak.Do not mix a raw
-lvendor_codecflag with the declared module unless a specific documented linker-interface reason requires both.
Prebuilt static libraries use a parallel pattern
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := vendor_math
LOCAL_SRC_FILES := libs/$(TARGET_ARCH_ABI)/libvendor_math.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)A .a archive still belongs to one architecture
The archive must contain object files for the active target ABI, not host Linux/macOS objects.
Consume the module with
LOCAL_STATIC_LIBRARIES := vendor_math.Static archives may depend on other libraries; declare/link the full dependency interface.
C++ archives must agree on STL/runtime, exceptions, RTTI, compiler ABI, and exported symbol assumptions.
An archive built with a higher Android API can introduce unavailable symbols on older devices even if the final link succeeds.
Why -L/home/user/lib is the wrong default
It encodes one workstation’s directory, so teammates and CI cannot reproduce the build.
It does not choose a library by Android ABI.
It may accidentally select a host library with the same name.
It does not describe exported headers or transitive dependencies.
For shared libraries, it does not by itself guarantee the
.sois packaged into the APK.Library search order can silently pick a different binary after an environment change.
A declared prebuilt module keeps the path inside the project/dependency layout and visible to the build graph.
Wire ndk-build into Gradle
android {
defaultConfig {
minSdk = 24
ndk {
abiFilters += listOf("arm64-v8a", "x86_64")
}
externalNativeBuild {
ndkBuild {
arguments += "NDK_APPLICATION_MK:=src/main/cpp/Application.mk"
}
}
}
externalNativeBuild {
ndkBuild {
path = file("src/main/cpp/Android.mk")
}
}
}Gradle owns variants and packaging
The external native build points AGP at the root Android.mk.
ABI filters must match the prebuilt binaries and the device/emulator fleet you support.
minSdkaffects the native platform API surface exposed at build time.Pin a tested NDK revision elsewhere in the Android block for reproducible toolchains.
Do not copy the example ABI/minimum values blindly; choose them from product requirements, dependency availability, store rules, and testing capacity.
Application.mk controls shared native policy
APP_PLATFORM := android-24
APP_ABI := arm64-v8a x86_64
APP_STL := c++_shared
APP_CPPFLAGS := -std=c++20 -fexceptions -frttiKeep Gradle and ndk-build declarations aligned
APP_PLATFORMis the native minimum API level; align it with the application minimum unless a documented split is required.APP_ABIselects native targets; Gradle ABI filters can further control packaging.c++_sharedrequires the matching libc++ shared runtime to be packaged once for each ABI.Exceptions and RTTI increase capabilities and potential size; enable them because the code/dependencies require them.
Modernize legacy GCC, gnustl, STLport,
armeabi, MIPS, and standalone-toolchain declarations before using a current NDK.
Link order and transitive static dependencies
LOCAL_STATIC_LIBRARIES := registration_plugins core_utils
LOCAL_WHOLE_STATIC_LIBRARIES := self_registering_codeWhole-archive is a targeted tool, not a blanket fix
Static linkers normally extract archive members needed to resolve currently unresolved symbols.
Self-registering code referenced only through constructors/registries may be discarded.
LOCAL_WHOLE_STATIC_LIBRARIESforces all archive objects into the result.Forcing whole archives can inflate binaries, duplicate symbols, and include dead code.
Prefer explicit references or scoped whole-archive use after confirming the missing-registration mechanism.
Build with the committed Gradle Wrapper
./gradlew :app:assembleDebug --stacktraceBUILD SUCCESSFULA successful link is an intermediate checkpoint
The Wrapper selects the project’s declared Gradle distribution.
AGP invokes ndk-build for each configured variant/ABI.
--stacktraceretains the failing task/plugin context.Review native compiler/linker warnings rather than accepting success with hidden ABI/API problems.
Build release variants too; optimization, shrinking, symbols, and packaging can differ.
Inspect packaged native libraries
APK="app/build/outputs/apk/debug/app-debug.apk"
unzip -l "$APK" | rg "lib/[^/]+/.*\.so$"lib/arm64-v8a/libnative_core.so
lib/arm64-v8a/libvendor_codec.so
lib/arm64-v8a/libc++_shared.so
lib/x86_64/libnative_core.so
lib/x86_64/libvendor_codec.so
lib/x86_64/libc++_shared.soCheck the APK by ABI, not by filename alone
Replace the APK path with the actual variant output when project naming differs.
Every runtime shared dependency should exist for each packaged ABI unless supplied as a public system library.
Static archives should not appear because their selected object code is incorporated into a shared/executable output.
Unexpected duplicate C++ runtimes or missing vendor
.sofiles are packaging red flags.App Bundles split delivery by ABI; test generated APK sets or store-equivalent artifacts as well as a universal debug APK.
Inspect ELF dependencies and architecture
"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf" \
-h -d libnative_core.so | rg "Class:|Machine:|NEEDED|SONAME"Class: ELF64
Machine: AArch64
Shared library: [libvendor_codec.so]
Shared library: [liblog.so]
Shared library: [libc++_shared.so]ELF metadata predicts runtime requirements
Use the prebuilt host directory matching the build host; the example path is Linux x86_64.
The ELF class/machine must match the APK ABI directory.
NEEDEDentries list shared-library names the dynamic loader must resolve.SONAMEshould be consistent with the packaged dependency identity.Private/unexpected platform dependencies discovered here should be removed before release.
Debug unresolved symbols at link time
Confirm the symbol is declared by the header and defined by a linked library for this ABI.
For C APIs consumed by C++, verify the header uses the correct
extern "C"guards.Check spelling, namespace, overload, visibility, and whether a C++ method has an out-of-line definition.
Inspect archives/shared objects with NDK LLVM tools such as
llvm-nmorllvm-readelf.Declare all static transitive dependencies and review archive order/whole-archive needs.
Confirm debug and release use the same dependency graph where intended.
Do not solve a missing symbol by adding every library globally; identify the owning interface.
Debug runtime dlopen failures
adb logcat -c
adb shell am force-stop com.example.app
adb shell monkey -p com.example.app 1
adb logcat -d | rg "dlopen failed|UnsatisfiedLinkError|linker"java.lang.UnsatisfiedLinkError: dlopen failed: library "libvendor_codec.so" not foundRuntime diagnostics point to packaging or compatibility
Replace the package name with the application under test.
Clearing Logcat removes prior diagnostic history; do it only on a test device when acceptable.
“not found” can mean the direct library or one of its
NEEDEDdependencies is missing.“wrong ELF class” or architecture errors indicate an ABI/path mismatch.
“cannot locate symbol” may indicate API-level mismatch, incompatible vendor build, C++ ABI mismatch, or hidden/private symbol use.
Do not publish device serials, application secrets, or sensitive logs.
C++ runtime consistency
Use libc++ with current NDKs; GCC-era gnustl/STLport settings are obsolete.
All C++ libraries crossing ABI boundaries should agree on runtime, compiler ABI, exceptions, RTTI, and relevant compile definitions.
Passing STL containers or allocated memory across independently built library boundaries increases ABI and ownership risk.
Prefer a narrow C ABI for long-lived vendor boundaries when practical.
Do not package multiple conflicting
libc++_shared.socopies; select and test one compatible runtime per ABI.Rebuild third-party native dependencies with the selected NDK/toolchain when source and licensing permit.
System API availability is tied to minSdk
NDK sysroot libraries are API-level aware. A symbol introduced after the application’s minimum API can link under one configuration yet fail on older devices if guarded incorrectly or built against the wrong platform. Prefer APIs available at minSdk; otherwise use documented availability checks and runtime symbol loading patterns appropriate to the API.
Security and supply-chain review for prebuilts
Obtain binaries and headers from a trusted upstream with version, license, provenance, and checksums.
Scan dependencies and monitor upstream security advisories.
Keep debug symbols separately and know whether the vendor binary is stripped.
Inspect exported symbols and dynamic dependencies for unnecessary surface.
Test malformed inputs at the native boundary; memory-unsafe codecs/parsers are high-risk.
Record which source revision, NDK, flags, and patches produced each prebuilt when builds are under your control.
Plan update and rollback paths before shipping a binary-only dependency.
Common Android.mk mistakes
Use only `-L/path`: adds a search directory but does not name, model, or package a dependency.
Put headers in LOCAL_LDLIBS: header directories belong in include variables or exported module interfaces.
Name a prebuilt only with `-lfoo`: may link locally but loses ABI selection and packaging metadata.
Reuse one `.so` for every ABI: each ELF binary targets one architecture.
Link a host Linux library: Android uses a different target ABI/API/libc environment.
Declare a static archive as shared: choose the matching prebuilt module type.
Ignore transitive dependencies: direct link success/runtime loading may fail when required libraries are absent.
Hard-code obsolete CPU flags: use NDK/ABI configuration and profile-guided evidence rather than old
-marchrecipes.Assume BUILD SUCCESSFUL proves runtime: inspect the artifact and run it on every supported ABI/API class.
Completion checklist
Every dependency is classified as public NDK system, source-built, prebuilt shared, prebuilt static, or header-only.
LOCAL_LDLIBScontains intentional raw/system linker flags only.Source and prebuilt dependencies are declared as uniquely named ndk-build modules.
Headers and transitive dependency interfaces are exported deliberately.
Prebuilt binaries exist and are compatible for every supported ABI.
Gradle, Application.mk, minSdk, ABI filters, and pinned NDK agree.
Debug and release artifacts contain the expected
.soset and one compatible C++ runtime.ELF architecture, SONAME, NEEDED entries, and API dependencies are reviewed.
Device/emulator tests exercise JNI/native flows on every shipped ABI.
Licensing, provenance, symbols, security updates, and rollback are documented.
Official Android NDK references
Android.mk documents module variables including
LOCAL_LDLIBS, shared/static dependency variables, and exported flags.Use prebuilt libraries documents
PREBUILT_SHARED_LIBRARY,PREBUILT_STATIC_LIBRARY, ABI files, and consumer modules.Stable APIs lists public native platform libraries intended for NDK apps.
Android ABIs explains supported ABI names, architecture behavior, and packaging.
C++ support covers libc++, shared/static runtime choices, and one-STL guidance.
Other build systems explains the NDK Clang/sysroot model when integrating nonstandard builds.
Comments and corrections