NDK交叉编译链
“NDK 编译失败”通常不是一个问题。一个 native 模块至少经过四个决定:目标 ABI 是什么、最低 API 是什么、头文件和 stub 库从哪里来、C++ runtime 是否随产物安装。命令行里只看到 ndk-build 或 CMake,源码却把这些决定分散在 Make 变量、toolchain 文件、sysroot 路径和安装规则中。
本文面向已经读过 lunch目标与编译变体、模块化编译 和 Android Emulator配置链 的读者。需要理解 ELF、共享库、Make/CMake 的基本概念;不展开 Clang 后端优化、LLVM IR、Gradle packaging task 或 linker 的全部实现。应用侧 NDK 负责 ndk-build/CMake,平台侧 build/soong 负责 NDK sysroot 和 stub 的生成;两者通过“sysroot、stub、target triple”这个产物接口相接,而不是同一个 Git 项目。
读完后,读者应能从 APP_ABI 或 ANDROID_ABI 追到 target triple、sysroot 搜索路径和输出目录,解释 APP_PLATFORM/minSdkVersion 为什么可能被 ABI 最低版本抬高,判断 c++_shared 是否会生成要打包的 libc++_shared.so,并能用 Soong 源码解释 sdk_version 为什么改变 AOSP 模块的头文件和库依赖。
1. 两套来源
NDK 发布仓库和 Android platform 不是“同一套 NDK 源码的两个目录”。NDK r29 的 build/ 是用户侧构建系统;Android 17 Soong 的 cc/ndk_sysroot.go 是平台侧生成器。前者消费已经安装好的 sysroot,后者把 bionic headers、API headers、stub shared libraries 和静态库整理到输出目录。
| 层 | 固定来源 | owner | 产物/消费者 |
|---|---|---|---|
| NDK 发布 | platform/ndk,ndk-r29 | ndk-build、CMake toolchain | native app 的 .o、.so、runtime |
| 平台生成 | platform/build/soong,android-17.0.0_r1 | NdkSingleton、ndk_library | AOSP out/soong/ndk sysroot/stub |
| API 合约 | prebuilts/ndk,android-17.0.0_r1 | release metadata | source.properties 与平台预置接口 |
| 最终加载 | Android dynamic linker/应用打包 | APK/进程 | lib/<abi>/*.so |
Android 17 固定 tag 下 prebuilts/ndk/current/source.properties 的 Pkg.Revision 是 25.0.8474149,而本文应用侧工具链固定为独立 ndk-r29。这不是冲突,而是版本边界:platform checkout 中的 prebuilt 由该平台 manifest 决定,外部 App 可以选择另一 NDK release。文章只在可验证的 sysroot/stub 概念处比较两者,不声称 Android 17 构建树内部正在使用 NDK r29。
图中 Soong 到 sysroot 的箭头不是应用构建时重新运行 Soong,而是平台构建先产生可被 NDK 工具使用的目录。若把 ndk-build 的 SYSROOT_API_LIB_DIR 当作 AOSP out/soong 的即时调用,就会误判依赖关系。
2. ABI入口
2.1 元数据
NDK r29 的 meta/abis.json 是 ABI 名称、架构和 triple 的单一入口。它没有把 ABI 简化成“arm 或 x86”;同一个 ABI 还携带 LLVM triple 和 bitness。以下代码块来自独立的 platform/ndk project:
源码文件:meta/abis.json(platform/ndk project)
{
"armeabi-v7a": {
"bitness": 32,
"default": true,
"deprecated": false,
"proc": "armv7-a",
"arch": "arm",
"triple": "arm-linux-androideabi",
"llvm_triple": "armv7-none-linux-androideabi"
},
"arm64-v8a": {
"bitness": 64,
"default": true,
"deprecated": false,
"proc": "aarch64",
"arch": "arm64",
"triple": "aarch64-linux-android",
"llvm_triple": "aarch64-none-linux-android"
},
"riscv64": {
"bitness": 64,
"default": false,
"deprecated": false,
"proc": "riscv64",
"arch": "riscv64",
"triple": "riscv64-linux-android",
"llvm_triple": "riscv64-none-linux-android"
},
"x86": {
"bitness": 32,
"default": true,
"deprecated": false,
"proc": "i686",
"arch": "x86",
"triple": "i686-linux-android",
"llvm_triple": "i686-none-linux-android"
},
"x86_64": {
"bitness": 64,
"default": true,
"deprecated": false,
"proc": "x86_64",
"arch": "x86_64",
"triple": "x86_64-linux-android",
"llvm_triple": "x86_64-none-linux-android"
}
}triple 是 NDK 目录和库路径使用的名字,例如 aarch64-linux-android;llvm_triple 是传给 Clang target 的名字,例如 aarch64-none-linux-android。二者相似但不相同,排查路径时不能凭字符串拼接猜测。
2.2 ndk-build变体
build/make/core/setup-abi.mk 在每个 ABI 变体中设置架构、平台级别和输出目录。注意它会把 ABI 的最低 OS 版本与用户的 APP_PLATFORM 比较,必要时提升 TARGET_PLATFORM_LEVEL。
源码文件:build/core/setup-abi.mk(platform/ndk project)
$(call ndk_log,Building application '$(NDK_APP_NAME)' for ABI '$(TARGET_ARCH_ABI)')
TARGET_ARCH := $(strip $(NDK_ABI.$(TARGET_ARCH_ABI).arch))
ifndef TARGET_ARCH
$(call __ndk_info,ERROR: The $(TARGET_ARCH_ABI) ABI has no associated architecture!)
$(call __ndk_error,Aborting...)
endif
TARGET_OUT := $(NDK_APP_OUT)/$(_app)/$(TARGET_ARCH_ABI)
TARGET_PLATFORM_LEVEL := $(APP_PLATFORM_LEVEL)
MIN_OS_FOR_TARGET := $(NDK_ABI_${TARGET_ARCH_ABI}_MIN_OS_VERSION)
ifneq ($(call lt,$(TARGET_PLATFORM_LEVEL),$(MIN_OS_FOR_TARGET)),)
TARGET_PLATFORM_LEVEL := $(MIN_OS_FOR_TARGET)
endif
TARGET_PLATFORM := android-$(TARGET_PLATFORM_LEVEL)
TARGET_ABI := $(TARGET_PLATFORM)-$(TARGET_ARCH_ABI)
ifeq ($(NDK_APP_OPTIM),debug)
TARGET_OBJS := $(TARGET_OUT)/objs-debug
else
TARGET_OBJS := $(TARGET_OUT)/objs
endif
include $(BUILD_SYSTEM)/setup-toolchain.mk这段代码有三个 owner:ABI 元数据拥有 NDK_ABI_*,Application.mk/命令行拥有 APP_PLATFORM_LEVEL,setup-abi.mk 拥有当前变体的最终 platform level 和对象目录。APP_PLATFORM=21 并不能保证所有 ABI 都以 21 编译;如果某 ABI 的最低 OS 更高,最终 TARGET_PLATFORM_LEVEL 会被抬高。
2.3 CMake入口
CMake 不读取 APP_ABI,它通过 ANDROID_ABI 传入同一个概念。NDK 自带的 android.toolchain.cmake 先防止重复包含,再决定是否走 legacy toolchain;非 legacy 分支随后读取 source.properties、解析 ABI 和 API。这里的关键不是变量数量,而是 CMake cache 中的 ABI 必须在第一次 configure 时确定;修改 ABI 后复用旧 build 目录,可能保留旧 compiler 和旧 sysroot。
cmake_minimum_required(VERSION 3.10.0)
if(ANDROID_NDK_TOOLCHAIN_INCLUDED)
return()
endif()
set(ANDROID_NDK_TOOLCHAIN_INCLUDED true)
if(DEFINED ANDROID_USE_LEGACY_TOOLCHAIN_FILE)
set(_USE_LEGACY_TOOLCHAIN_FILE ${ANDROID_USE_LEGACY_TOOLCHAIN_FILE})
else()
set(_USE_LEGACY_TOOLCHAIN_FILE true)
endif()
if(_USE_LEGACY_TOOLCHAIN_FILE)
include("${CMAKE_CURRENT_LIST_DIR}/android-legacy.toolchain.cmake")
return()
endif()
get_filename_component(ANDROID_NDK_EXPECTED_PATH
"${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
if(NOT ANDROID_NDK)
set(CMAKE_ANDROID_NDK "${ANDROID_NDK_EXPECTED_PATH}")
endif()以上代码来自独立 platform/ndk project 的 build/cmake/android.toolchain.cmake,省略的是 custom NDK path、revision 解析和后续 ABI/API 分支。它证明 toolchain 文件拥有“只执行一次”和 legacy 分流,不证明 CMake platform module 的完整 compiler 命令生成。
3. 工具链设置
3.1 Triple与flags
以 arm64 为例,aarch64-linux-android-clang/setup.mk 设置工具链名称、LLVM triple、库目录和 debug/release CFLAGS;riscv64 也有自己的 setup,并明确暂不配置 TSAN basename。
源码文件:build/core/toolchains/aarch64-linux-android-clang/setup.mk(platform/ndk project)
TOOLCHAIN_NAME := aarch64-linux-android
LLVM_TRIPLE := aarch64-none-linux-android
TARGET_TOOLCHAIN_ARCH_LIB_DIR := aarch64
TARGET_ASAN_BASENAME := libclang_rt.asan-aarch64-android.so
TARGET_TSAN_BASENAME := libclang_rt.tsan-aarch64-android.so
TARGET_UBSAN_BASENAME := libclang_rt.ubsan_standalone-aarch64-android.so
TARGET_CFLAGS := -fpic
TARGET_arm64_release_CFLAGS := \
-O2 \
-DNDEBUG
TARGET_arm64_debug_CFLAGS := \
-O0 \
-UNDEBUG \
-fno-limit-debug-infoTOOLCHAIN_NAME 会进入 sysroot 的目录名,LLVM_TRIPLE 用于 target,TARGET_CFLAGS 则是该 ABI 的默认编译约束。它们分属路径、编译器和优化三个维度,不能把“改变 triple”当作“改变优化级别”。
3.2 sysroot路径
setup-toolchain.mk 在选择具体 toolchain setup 后,建立 include、API library 和 unversioned library 三组路径:
源码文件:build/core/setup-toolchain.mk(platform/ndk project)
ifndef NDK_UNIFIED_SYSROOT_PATH
NDK_UNIFIED_SYSROOT_PATH := $(TOOLCHAIN_ROOT)/sysroot
endif
SYSROOT_INC := $(NDK_UNIFIED_SYSROOT_PATH)
SYSROOT_LIB_DIR := $(NDK_UNIFIED_SYSROOT_PATH)/usr/lib/$(TOOLCHAIN_NAME)
SYSROOT_API_LIB_DIR := $(SYSROOT_LIB_DIR)/$(TARGET_PLATFORM_LEVEL)
# API-specific libraries come first.
SYSROOT_LINK_ARG := -L $(SYSROOT_API_LIB_DIR) -L $(SYSROOT_LIB_DIR)
SYSROOT_ARCH_INC_ARG := \
-isystem $(SYSROOT_INC)/usr/include/$(TOOLCHAIN_NAME)
NDK_TOOLCHAIN_RESOURCE_DIR := $(shell $(TARGET_CXX) -print-resource-dir)
NDK_TOOLCHAIN_LIB_DIR := $(strip $(NDK_TOOLCHAIN_RESOURCE_DIR))/lib/linux这里的搜索顺序解释了一个常见链接现象:API 专属目录在前,未版本化目录在后,linker 会优先选择目标 API 目录中的 stub。头文件则包含公共 usr/include 和 triple 专属的 usr/include/$(TOOLCHAIN_NAME)。如果报错是“找不到头文件”,先查 include 路径;如果是“undefined reference”,再查 API 目录和符号引入版本。
4. 编译产物
4.1 对象路径
setup-abi.mk 让 debug 和 release 使用不同对象目录,避免切换优化模式时覆盖彼此的 .o。这个状态不是“构建类型标签”而已,它影响增量构建是否能复用旧对象。
源码文件:build/core/setup-abi.mk(platform/ndk project)
ifeq ($(NDK_APP_OPTIM),debug)
TARGET_OBJS := $(TARGET_OUT)/objs-debug
else
TARGET_OBJS := $(TARGET_OUT)/objs
endif
TARGET_OUT := $(NDK_APP_OUT)/$(_app)/$(TARGET_ARCH_ABI)
NDK_APP_DST_DIR := $(NDK_APP_LIBS_OUT)/$(TARGET_ARCH_ABI)当 APP_ABI := arm64-v8a x86_64 时,两个 ABI 会拥有不同的 TARGET_OUT 和安装目录。多 ABI 构建不是把一个 .so 重命名两次,而是分别编译、分别链接、分别安装。
4.2 共享库规则
build-shared-library.mk 本身很短,但它把模块标记为 SHARED_LIBRARY 并交给 build-module.mk。真正的源文件枚举、命令和依赖闭包由后续规则完成;不要从这个入口块推断完整 link command。
源码文件:build/core/build-shared-library.mk(platform/ndk project)
LOCAL_BUILD_SCRIPT := BUILD_SHARED_LIBRARY
LOCAL_MAKEFILE := $(local-makefile)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
LOCAL_MODULE_CLASS := SHARED_LIBRARY
include $(BUILD_SYSTEM)/build-module.mk源码中的 owner 转移是:Android.mk 声明模块,build-shared-library.mk 选择模块类别,build-module.mk 生成构建动作,安装规则把结果放到当前 ABI 的 NDK_APP_DST_DIR。出现“编译成功但 APK 没有 .so”时,应该继续看安装规则而不是只看 clang 输出。
4.3 STL状态
NDK r29 的 stl.mk 将 APP_STL 转成编译/链接 flag;只有 c++_shared 会额外添加一个安装目标。c++_static 不是“自动生成一个可共享 runtime”,而是把 libc++ 静态链接到模块。
源码文件:build/core/stl.mk(platform/ndk project)
ifeq ($(APP_STL),none)
LOCAL_CPPFLAGS += -nostdinc++
LOCAL_LDFLAGS += -nostdlib++
else ifeq ($(APP_STL),system)
LOCAL_LDFLAGS += -stdlib=libstdc++
ifneq (,$(call module-has-c++-features,$(LOCAL_MODULE),rtti exceptions))
LOCAL_LDLIBS += -lc++abi
endif
else ifeq ($(APP_STL),c++_static)
LOCAL_LDFLAGS += -static-libstdc++
endif
# c++_shared uses Clang's shared C++ runtime defaults.none 会关闭 C++ 头文件和标准库链接;system 是兼容分支,源码注释还指出 released NDK 实际仍使用 libc++ headers;c++_static 添加静态 runtime;c++_shared 不在这里添加 flag,而在安装规则中复制共享库。这里不能把四个值描述成“体积从小到大”的简单排序,因为模块数量和 ABI 数量会改变静态重复成本。
源码文件:build/core/install_stl.mk(platform/ndk project)
ifeq ($(APP_STL),c++_shared)
NDK_LIBCXX_TARGET := $(NDK_APP_DST_DIR)/libc++_shared.so
NDK_LIBCXX_LIB_PATH := $(SYSROOT_LIB_DIR)/libc++_shared.so
installed_modules: $(NDK_LIBCXX_TARGET)
$(NDK_LIBCXX_TARGET): PRIVATE_ABI := $(TARGET_ARCH_ABI)
$(NDK_LIBCXX_TARGET): PRIVATE_SRC := $(NDK_LIBCXX_LIB_PATH)
$(NDK_LIBCXX_TARGET): PRIVATE_DST := $(NDK_LIBCXX_TARGET)
$(NDK_LIBCXX_TARGET): clean-installed-binaries
$(hide) $(call host-install,$(PRIVATE_SRC),$(PRIVATE_DST))
endif所以 c++_shared 的可执行验证不是检查链接命令里有没有 -lc++_shared,而是检查每个 ABI 的 NDK_APP_DST_DIR 是否出现 libc++_shared.so,并继续确认 APK 打包层消费了这个目录。
5. 平台sysroot
5.1 Soong模块
Android 17 Soong 的 ndk_sysroot.go 说明平台侧 NDK 的四类输入:bionic headers、platform API headers、NDK stub shared libraries 和 bionic static libraries。ndk_headers 负责头文件,ndk_library 负责只在构建时使用的 stub,静态库则由平台构建后归档。
源码文件:build/soong/cc/ndk_sysroot.go
// The platform needs to provide the following artifacts for the NDK:
// 1. Bionic headers.
// 2. Platform API headers.
// 3. NDK stub shared libraries.
// 4. Bionic static libraries.
func RegisterNdkModuleTypes(ctx android.RegistrationContext) {
ctx.RegisterModuleType("ndk_headers", NdkHeadersFactory)
ctx.RegisterModuleType("ndk_library", NdkLibraryFactory)
ctx.RegisterModuleType("preprocessed_ndk_headers", preprocessedNdkHeadersFactory)
ctx.RegisterParallelSingletonType("ndk", NdkSingleton)
}
func getNdkSysrootBase(ctx android.PathContext) android.OutputPath {
return getNdkInstallBase(ctx).Join(ctx, "sysroot")
}这里的 stub 是链接时接口,不是设备运行时实现。把 NDK sysroot 里的 liblog.so、libc.so 当作可以从主机直接加载的完整库,会把“链接成功”和“运行时可用”混为一谈。
5.2 依赖收集
NdkSingleton.GenerateBuildActions 遍历所有 module proxy,收集 header install paths、stub install paths、静态库路径和 license,再用 timestamp 文件表达构建依赖。它还为头文件安排 C compatibility 检查。
源码文件:build/soong/cc/ndk_sysroot.go
相关函数/类型:ndkSingleton.GenerateBuildActions
func (n *ndkSingleton) GenerateBuildActions(ctx android.SingletonContext) {
var staticLibInstallPaths android.Paths
var headerSrcPaths android.Paths
var headerInstallPaths android.Paths
var headersToVerify []srcDestPair
ctx.VisitAllModuleProxies(func(module android.ModuleProxy) {
if !android.OtherModulePointerProviderOrDefault(
ctx, module, android.CommonModuleInfoProvider).Enabled {
return
}
if m, ok := android.OtherModuleProvider(ctx, module, NdkHeaderInfoProvider); ok {
headerSrcPaths = append(headerSrcPaths, m.SrcPaths...)
headerInstallPaths = append(headerInstallPaths, m.InstallPaths...)
if !m.SkipVerification {
for i, installPath := range m.InstallPaths {
headersToVerify = append(headersToVerify, srcDestPair{
src: m.SrcPaths[i], dest: installPath,
})
}
}
}
if ccInfo, ok := android.OtherModuleProvider(ctx, module, CcInfoProvider); ok {
if ccInfo.LinkerInfo != nil {
if library := ccInfo.LinkerInfo.LibraryDecoratorInfo; library != nil &&
library.NdkSysrootPath != nil {
staticLibInstallPaths = append(staticLibInstallPaths,
library.NdkSysrootPath)
}
}
}
})
// ...写入 NOTICE、timestamp、ABI header filter 和完整依赖...
}Enabled 过滤说明并非所有模块都进入 sysroot;NdkHeaderInfoProvider 和 CcInfoProvider 是 owner/consumer 交接点。timestamp 不是内容本身,而是 Ninja/Soong 增量图中的状态节点:头文件变化会触发 header timestamp,完整 sysroot 还会等待静态库路径。
5.3 C兼容验证
平台生成器用 Clang 对安装后的 header 做 -fsyntax-only 检查,但源码明确承认它只选择 aarch64、future API 和 generic 配置,不能证明所有 ABI/API 组合。
源码文件:build/soong/cc/ndk_sysroot.go
相关函数/类型:verifyNdkHeaderIsCCompatible
flags: fmt.Sprintf(
"-target aarch64-linux-android%d --sysroot %s",
android.FutureApiLevel.FinalOrFutureInt(),
getNdkSysrootBase(ctx).String(),
),这段代码的覆盖边界很窄:它能发现公共 header 在一个代表性 target 下无法作为 C 编译,不能推出 riscv64、低 API、fortify 变体都通过。文章将“有检查”与“全矩阵已证明”严格分开。
6. SDK边界
6.1 sdk_version解析
AOSP 的 cc 模块通过 sdk_version/min_sdk_version 生成 SDK variant。sdk_version 不是“把 NDK 路径写进编译命令”的开关,而是 Soong 选择 API surface、stub 和 variant 的输入。
源码文件:build/soong/android/sdk_version.go
相关函数/类型:SdkSpecFromWithConfig
func SdkSpecFromWithConfig(config Config, str string) SdkSpec {
switch str {
case "":
return SdkSpecPrivate
case "none":
return SdkSpecNone
case "core_platform":
return SdkSpecCorePlatform
default:
// syntax is [kind_]version
sep := strings.LastIndex(str, "_")
var kindString string
if sep == -1 {
kindString = ""
} else {
kindString = str[0:sep]
}
versionString := str[sep+1:]
// ...把 kindString 映射为 public/core/system 等,再解析 API level...
}
}none、core_platform 和空字符串的语义不同:前者表示没有受管理 SDK,后者进入 private。对 C/C++ 模块,后续 cc/sdk.go 会根据是否为 SDK variant 清理或保留 sdk_version,再影响 NDK stub 依赖;不能只看 Android.bp 的一行属性。
6.2 变体选择
cc/cc.go 的注释说明,min_sdk_version 对普通模块和 CRT object 的含义不同:普通模块用它约束最低运行版本,CRT object 则据此生成从该版本到 current 的多个 local variants。
源码文件:build/soong/cc/cc.go
相关函数/类型:moduleContextImpl.useSdk
func (ctx *moduleContextImpl) useSdk() bool {
return ctx.mod.UseSdk()
}
func (ctx *moduleContextImpl) isSdkVariant() bool {
return ctx.mod.IsSdkVariant()
}真正决定 UseSdk() 和 variant 分裂的是 cc/sdk.go 的 sdkTransitionMutator:设置 sdk_version 的模块通常分成 platform 和 sdk 两个 variation,SDK variant 面向嵌入 APK 的构建并被标记为 prevent-install。min_sdk_version 对 CRT object 还有特殊含义,会决定从哪个 API level 开始生成版本化 variant。这解释了“同一个 cc_library 为什么出现多个架构/SDK 变体”:Soong 同时展开 host/device、ABI、SDK 和 min SDK 维度。NDK 应用的 APP_ABI 是外部构建系统的循环;AOSP 模块的 variant 则由 Soong graph 管理,二者不能用相同的输出目录经验互相推导。
cc/sdk_test.go::TestSdkMutator 为这条结论提供对应测试。测试输入同时声明带 sdk_version: "current" 的 libsdk/sdkbinary 和纯 platform 的 libplatform;action 是运行 Soong 测试图并取得 android_arm64_armv8-a_sdk_shared、android_arm64_armv8-a_shared 等具体 variant;assert 检查 SDK variant 依赖 libsdkdep 的 SDK variant 和 ndk_libc++_shared,platform variant 则依赖 platform libc++。它证明依赖边选择,不证明这些库在真实 APK 中的安装顺序。
7. 失败分支
7.1 ABI错误
setup-toolchain.mk 在 ABI 没有 toolchain,或显式 NDK_TOOLCHAIN 不支持该 ABI 时终止构建:
源码文件:build/core/setup-toolchain.mk(platform/ndk project)
ifndef TARGET_TOOLCHAIN_LIST
$(call __ndk_info,There is no toolchain that supports the $(TARGET_ARCH_ABI) ABI.)
$(call __ndk_info,Please modify the APP_ABI definition in $(NDK_APP_APPLICATION_MK) to use)
$(call __ndk_info,a set of the following values: $(NDK_ALL_ABIS))
$(call __ndk_error,Aborting)
endif
TARGET_TOOLCHAIN := $(lastword $(TARGET_TOOLCHAIN_LIST))默认选择列表的最后一个 toolchain,当前 NDK 实际只有 Clang;显式指定不兼容 toolchain 则走另一条失败分支。恢复动作是修正 ABI/toolchain 输入并重新 configure/build,不能在旧 .o 上强行重命名。
7.2 STL错误
APP_STL=none 会禁用 C++ 标准头和标准库;c++_shared 则要求安装共享 runtime。可以用 NDK 自带测试作为对应测试:tests/build/cmake-libc++-shared/test_config.py 明确用 -DANDROID_STL=c++_shared,tests/build/cxx_only_from_deps 使用 APP_STL := c++_shared,而 tests/build/static_cxx_linkable 使用 c++_static。
rg -n 'APP_STL|ANDROID_STL' \
tests/build/cmake-libc++-shared \
tests/build/cxx_only_from_deps \
tests/build/static_cxx_linkable断言应是“测试输入选择了哪种 runtime”,而不是“所有 C++ 异常跨 .so 都已被测试”。源码能证明安装目标和 flag,具体 APK 中的打包与多库 ABI 交互仍需应用工程实验。
其中 cmake-libc++-shared 的 arrange 是一个 CMake 可执行目标和 -DANDROID_STL=c++_shared,被测源码调用只在 libandroid_support 中提供的函数;action 是配置并构建,assert 是 shared runtime 没被正确链接时构建必须失败。这个测试证明 CMake 选项进入了链接闭包,不证明 APK 最终携带该 .so。
7.3 API错误
NDK r29 的 meta/platforms.json 把支持范围写成 min: 21、max: 35,并给出 codename alias。使用高于设备运行版本的 API 头文件可能编译成功,但运行时符号仍受最低 API 和动态 linker 约束;因此“头文件找得到”不等于“设备可加载”。
7.4 中断清理
这条构建链没有事务式 cancel。ndk-build 和 CMake/Ninja 都把中间状态写进 ABI 对应的对象目录或 CMake build 目录;进程中断只停止后续 action,不会回滚已生成的 .o、dependency file、cache 或已复制 runtime。恢复时应清理发生问题的 ABI/build 变体并重新 configure,而不是删除源码或把另一个 ABI 的产物复制过来。setup-abi.mk 将 debug/release 和 ABI 分目录,正是局部清理能够成立的前提。
8. 验证方法
8.1 ndk-build复述
在 NDK r29 源码 checkout 中,使用固定文件做静态复述:
git show ndk-r29:meta/abis.json
git show ndk-r29:build/core/setup-abi.mk
git show ndk-r29:build/core/setup-toolchain.mk
git show ndk-r29:build/core/stl.mk
git show ndk-r29:build/core/install_stl.mk读者应能指出:ABI 进入 TARGET_ARCH_ABI,平台级别可能被 MIN_OS_FOR_TARGET 抬高,sysroot library path 包含版本目录,c++_shared 额外产生安装目标。这组命令证明源码关系,不证明在当前主机成功执行 NDK 构建。
8.2 Soong复述
在 Android 17 build/soong checkout 中:
git show android-17.0.0_r1:cc/ndk_sysroot.go
git show android-17.0.0_r1:cc/ndk_library.go
git show android-17.0.0_r1:android/sdk_version.go
git grep -n 'sdk_version\|min_sdk_version' android-17.0.0_r1 -- cc/cc_test.go cc/sdk_test.go关键断言是:ndk singleton 收集 header/stub/static library;ndk_library 是 build-time stub;sdk_version 被解析为 SdkSpec 并影响 SDK variant。它不能证明某个厂商模块的最终 linker 命令,除非该模块的 Android.bp、variant 和完整构建日志也被固定。
8.3 ELF验证
有真实构建产物时,使用与 NDK release 对应的工具检查 ABI、依赖和 page alignment:
llvm-readelf -h lib/arm64-v8a/libfoo.so
llvm-readelf -d lib/arm64-v8a/libfoo.so | grep NEEDED
llvm-readelf -l lib/arm64-v8a/libfoo.so | grep LOAD输入是具体 ABI 目录下的 .so,断言分别是 ELF machine、动态依赖和 LOAD segment 对齐。它们证明产物事实,不证明源代码使用了哪个 C++ API;API 边界仍需回到 sysroot/stub 和 sdk_version。
9. 边界总结
当问题是“头文件为什么找不到”,沿 setup-toolchain.mk → SYSROOT_INC 查;当问题是“某符号为什么链接失败”,沿 TARGET_PLATFORM_LEVEL → SYSROOT_API_LIB_DIR → stub library 查;当问题是“多个 ABI 只有一个能运行”,沿 meta/abis.json → setup-abi.mk → lib/<abi> 查;当问题是“C++ runtime 缺失”,沿 stl.mk → install_stl.mk → APK packaging 查;当问题发生在 AOSP 模块,沿 sdk_version → Soong SDK variant → ndk_library/ndk_headers 查。
不要把以下结论从本文外推:NDK sysroot stub 是设备运行库;所有 ABI/API 矩阵都被 Soong 的单一 C compatibility 检查覆盖;CMake 和 ndk-build 共享同一份增量状态;某个 .so 能被 llvm-readelf 识别就一定能在任意 Android 设备上加载。它们分别需要运行时 linker、完整 ABI 测试矩阵、构建目录实验和目标设备验证。
