APEX构建边界
本文面向已经读过 Android.bp属性链、Soong构建链 和 Soong模块类型 的读者。你需要知道 Soong 会先加载模块、运行 mutator、再生成 build rule;不需要预先记住所有 VNDK 库名。
本文只回答一个构建侧问题:一个 apex 模块声明了 native、Java 或测试依赖后,Soong 如何决定依赖属于哪个 APEX 变体,如何把它们映射到 payload 路径,最后如何生成并签名 .apex 文件。VNDK 只讨论它与 image 变体和 VNDK APEX 的交点;Mainline 只讨论 AOSP 能从 updatable、min_sdk_version 和稳定接口检查中证明的约束,不把构建源码外推成 Play 分发或设备激活流程。
读完后,你应能从一个 APEX 名称反查 ApexInfo,解释为什么同一个 cc_library 会出现 platform、vendor 或 APEX variant,定位 apex_available / min_sdk_version 失败,并从 Ninja 规则判断哪个步骤负责 payload、哪个步骤负责外层签名。
1. 边界模型
APEX 构建有三个容易混淆的边界。apexBundle 是声明和产物的 owner;依赖模块是被选择、编译并提供文件的 producer;apexer、signapk 是构建 action 的 consumer。apexd 在设备上消费最终文件,但不参与本文的 Soong 依赖变体选择。
| 对象 | 所有者 | 何时生效 | 消费者 |
|---|---|---|---|
ApexInfo | android.ApexInfoProvider | transition mutate | 依赖模块的 variant 和检查器 |
apex_available | APEX-aware 模块 | variant mutate / payload 检查 | apexInfoMutator、可用性检查 |
apexFile | apexBundle | GenerateAndroidBuildActions | staging copy、apexer |
| 未签名 APEX | buildApex rule | action 执行后 | signapk、验证 rule |
已签名 .apex | apexBundle.outputFile | sign rule 完成 | 安装镜像或后续发布流程 |
2. 依赖入图
2.1 Bundle入口
apexBundle.DepsMutator 是声明进入图的入口。它读取 native_shared_libs、rust_dyn_libs、binaries、java_libs 等属性,并按目标 ABI 合并 multilib 与架构属性。这里还没有复制文件,也没有创建 APEX variant;它只建立 Blueprint 的直接依赖边。
源码文件:build/soong/apex/apex.go
相关函数/类型:908-956, android-17.0.0_r1
func (a *apexBundle) DepsMutator(ctx android.BottomUpMutatorContext) {
targets := ctx.MultiTargets()
imageVariation := a.getImageVariation()
for i, target := range targets {
var deps ResolvedApexNativeDependencies
deps.Merge(ctx, a.properties.Multilib.Both)
deps.Merge(ctx, ApexNativeDependencies{
Native_shared_libs: a.properties.Native_shared_libs,
Rust_dyn_libs: a.properties.Rust_dyn_libs,
Tests: a.properties.Tests,
Jni_libs: a.properties.Jni_libs,
Kernel_modules: a.properties.Kernel_modules,
})
if i == 0 {
deps.Merge(ctx, a.properties.Multilib.First)
deps.Merge(ctx, ApexNativeDependencies{Binaries: a.properties.Binaries})
}
// ... 合并 lib32/lib64 与具体 arch 属性
a.addDependenciesForNativeModules(ctx, deps, imageVariation, target)
}
}targets 决定要为哪些 ABI 加边;imageVariation 把 APEX 的安装镜像要求传给 native 依赖。比如一个只声明在 multilib.first 的 binary 不会因为 native_shared_libs 的默认行为被复制到两个 ABI。这个阶段的消费者是后续 mutator,而不是 apexer。
2.2 依赖标签
APEX 依赖边不是普通的 addDependency。Soong 用 dependencyTag 记录这条边是否真的进入 payload、是否必须使用 source module,以及是否需要沿同一 APEX 传播。
源码文件:build/soong/apex/apex.go
type dependencyTag struct {
blueprint.BaseDependencyTag
name string
payload bool
sourceOnly bool
memberType android.SdkMemberType
installable bool
// ...
}payload=false 的边可以指向签名 key 等构建辅助模块,不应被当成 APEX 内文件。反过来,payload=true 只表示候选文件进入 payload,最终仍要经过 apex_available、重复文件和平台可用性检查。这个区分解释了为什么“依赖图里出现了模块”不等于“模块一定出现在 apex_payload.img”。
2.3 成员传播
依赖边建立后,generateApexInfo 为普通 source APEX 生成一份上下文。VNDK APEX 是刻意的例外:它不从 APEX 反向列出每个 VNDK 库,而是由库自身的 vndk.enabled 属性识别成员。
源码文件:build/soong/apex/apex.go
func (a *apexBundle) generateApexInfo(ctx generateApexInfoContext) android.ApexInfo {
if a.vndkApex {
return android.ApexInfo{}
}
minSdkVersion := a.minSdkVersion(ctx)
if minSdkVersion.IsNone() {
minSdkVersion = android.FutureApiLevel
}
apexVariationName := ctx.ModuleName()
if a.GetOverriddenBy() != "" {
apexVariationName = a.GetOverriddenBy()
}
return android.ApexInfo{
ApexVariationName: apexVariationName,
MinSdkVersion: minSdkVersion,
Updatable: a.Updatable(),
UsePlatformApis: a.UsePlatformApis(),
BaseApexName: ctx.ModuleName(),
ApexAvailableName: proptools.String(a.properties.Apex_available_name),
}
}ApexInfo 的 owner 是 APEX transition provider。它携带 APEX 名称、最低 API、是否可更新和是否允许 platform API;这些字段不是描述性标签,而是后续 variant 合并和 updatable 检查的输入。
3. 变体选择
3.1 信息传播
android.ApexModuleBase 为普通 APEX-aware 模块提供 transition 默认实现。根 APEX outgoing 时保留信息;依赖 incoming 时先判断 dependency tag 是否属于同一 APEX,再决定保留、清空或最小化信息。
源码文件:build/soong/android/apex.go
func (m *ApexModuleBase) ApexTransitionMutatorOutgoing(
ctx OutgoingTransitionContext, info ApexInfo) ApexInfo {
if !ctx.Module().(ApexModule).GetDepInSameApexChecker().
OutgoingDepIsInSameApex(ctx.DepTag()) {
return ApexInfo{}
}
return info
}
func (m *ApexModuleBase) ApexTransitionMutatorIncoming(
ctx IncomingTransitionContext, info ApexInfo) ApexInfo {
module := ctx.Module().(ApexModule)
if !module.CanHaveApexVariants() ||
!module.GetDepInSameApexChecker().IncomingDepIsInSameApex(ctx.DepTag()) {
return ApexInfo{}
}
if info.ApexVariationName == "" || ctx.DepTag() == RequiredDepTag {
return ApexInfo{}
}
if !module.UniqueApexVariations() &&
!m.ApexProperties.UniqueApexVariationsForDeps && !info.ForPrebuiltApex {
return info.Minimize()
}
return info
}这里有三个关键时机:Split 产生候选信息,incoming/outgoing 变换依赖边上的信息,Mutate 把最终信息写入 ApexInfoProvider。info.Minimize() 会把两个具有相同 min_sdk_version 和 API 使用约束的 APEX 变体合并为类似 apex30 的共享 variant;UniqueApexVariations 或 prebuilt APEX 则禁止这种去重。
3.2 平台保留
平台 variant 用空的 ApexVariationName 表示,而不是另造一个“platform”名称。ApexModuleBase.ApexTransitionMutatorMutate 在非平台 variant 上检查 apex_available,并写入 ApexAvailableInfoProvider;平台 variant 还可能因不可用而被标记为不可安装。
源码文件:build/soong/android/apex.go
func (m *ApexModuleBase) ApexTransitionMutatorMutate(
ctx BottomUpMutatorContext, info ApexInfo) {
SetProvider(ctx, ApexInfoProvider, info)
module := ctx.Module().(ApexModule)
platformVariation := info.ApexVariationName == ""
if !platformVariation {
m.checkApexAvailableProperty(ctx)
SetProvider(ctx, ApexAvailableInfoProvider,
ApexAvailableInfo{ApexAvailableFor: module.ApexAvailableFor()})
}
platformAvailabilityInfo, _ := ModuleProvider(ctx, PlatformAvailabilityInfoProvider)
if platformVariation && !ctx.Host() &&
!module.AvailableFor(AvailableToPlatform) &&
platformAvailabilityInfo.NotAvailableToPlatform {
module.MakeUninstallable()
}
}因此,依赖同一个库的 platform consumer 和 APEX consumer 可能看到两个 module variant。platform 是否保留,不由 APEX 文件收集阶段补救,而在 transition 和可用性阶段先决定。
3.3 可用性拒绝
apex_available 的空值默认等价于只对 platform 可用;//apex_available:anyapex 匹配任意 APEX,但不匹配 platform。精确名和受限通配符都在 CheckAvailableForApex 中判断。
源码文件:build/soong/android/apex.go
func CheckAvailableForApex(what string, values []string) bool {
if len(values) == 0 {
return what == AvailableToPlatform
}
// ... 特定兼容模块的临时例外
for _, name := range values {
if name == what {
return true
}
if name == AvailableToAnyApex && what != AvailableToPlatform {
return true
}
if strings.ContainsAny(name, "*?") && MatchApex(name, what) {
return true
}
}
return false
}真正的拒绝发生在 payload 依赖遍历中:Soong 会报告依赖路径、缺少的 APEX 名称和建议的 apex_available 值。它不会把模块静默复制进 payload,也不会因为 platform variant 存在就绕过限制。源码还保留少数兼容性例外,所以判断具体产品时应同时检查当前 tag 的特殊分支。
4. Native边界
4.1 Image变体
VNDK 与 APEX transition 是两套正交机制。APEX transition 说明“这个模块属于哪个 APEX”;image transition 说明 native 模块面向 system、vendor、product、ramdisk 还是 recovery。cc.Module 在 SetImageVariation 中把 vendor 版本设置为 ImageVariation=vendor,并从 vendor.<version> 变体名提取 VndkVersion。
源码文件:build/soong/cc/image.go
func (c *Module) SetImageVariation(ctx android.ImageInterfaceContext, variant string) {
// ... ramdisk、vendor_ramdisk 与 recovery 分支
if strings.HasPrefix(variant, android.VendorVariation) {
c.Properties.ImageVariation = android.VendorVariation
if strings.HasPrefix(variant, VendorVariationPrefix) {
c.Properties.VndkVersion = strings.TrimPrefix(variant, VendorVariationPrefix)
}
squashVendorSrcs(c)
} else if strings.HasPrefix(variant, android.ProductVariation) {
c.Properties.ImageVariation = android.ProductVariation
if strings.HasPrefix(variant, ProductVariationPrefix) {
c.Properties.VndkVersion = strings.TrimPrefix(variant, ProductVariationPrefix)
}
squashProductSrcs(c)
}
// ... vendor public library 标记
}这段代码的消费者是 C/C++ 编译属性和链接检查器。它不负责把库放进 APEX;它只保证 vendor/product 变体使用正确的源码选择和 VNDK 版本上下文。
4.2 VNDK集合
Android 17 的 VNDK 集合由生成的 *.libraries.<version>.txt 文件和模块属性共同定义。Soong 的 VndkLibrariesTxtModules 只负责把逻辑文件名插入具体版本,不应被解读为固定成员清单。
源码文件:build/soong/cc/vndk.go
func VndkLibrariesTxtModules(vndkVersion string, ctx android.BaseModuleContext) []string {
return []string{
insertVndkVersion("vndkcore.libraries.txt", vndkVersion),
insertVndkVersion("vndksp.libraries.txt", vndkVersion),
insertVndkVersion("vndkprivate.libraries.txt", vndkVersion),
insertVndkVersion("vndkproduct.libraries.txt", vndkVersion),
insertVndkVersion("llndk.libraries.txt", vndkVersion),
}
}VndkProperties 进一步区分 enabled、support_system_process 和 private。其中 support_system_process 为 false 时是 VNDK-core;为 true 时是 VNDK-SP 子集,并有更窄的依赖规则。结论的边界是“构建器按属性和版本清单选择集合”,不能仅凭库名推断某个模块在所有 Android 版本都属于同一集合。
4.3 版本变体
cc.image 的变体生成逻辑会为 snapshot prebuilt 追加 vendor.<snapshotVersion>,对 vendor_available / product_available 模块分别产生 image variant。VNDK APEX 本身却在 generateApexInfo 中返回空信息,因为它的成员不是通过普通依赖边收集。这是两条不同的 membership 规则:
5. 文件收集
5.1 访问依赖
进入 GenerateAndroidBuildActions 后,apexBundle 才开始把依赖模块转换成 apexFile。depVisitor 根据 dependency tag 和 provider 信息判断文件类别、安装目录、symlink、ABI 与 transitive 属性。
源码文件:build/soong/apex/apex.go
func (a *apexBundle) GenerateAndroidBuildActions(ctx android.ModuleContext) {
if !a.commonBuildActions(ctx) {
return
}
vctx := visitorContext{
handleSpecialLibs: !android.Bool(a.properties.Ignore_system_library_special_case),
checkDuplicate: a.shouldCheckDuplicate(ctx),
unwantedTransitiveDeps: a.properties.Unwanted_transitive_deps,
}
ctx.WalkDepsProxy(func(child, parent android.ModuleProxy) bool {
return a.depVisitor(&vctx, ctx, child, parent)
})
vctx.normalizeFileInfo(ctx)
a.filesInfo = vctx.filesInfo
a.duplicateTransitiveFilesInfo = vctx.duplicateTransitiveFilesInfo
// ... 生成 manifest、依赖信息并调用 buildApex
}apexFile 保存的不是单纯路径。它还携带 module provider、partition、安装目录、symlink、是否 transitive 等信息。后续 buildApex 会依据这些字段选择复制、创建链接或运行 assemble_vintf。
5.2 路径归属
apexFile.path() 将 installDir 与文件 stem 拼成 APEX 根目录下的相对路径;因此一个 native 库进入 lib64/ 的事实来自 module 的 multilib 与 file class,而不是 manifest 文本中的“库列表”。availableToPlatform() 则读取 CommonModuleInfoProvider,决定是否可以启用 system 库链接优化。
源码文件:build/soong/apex/apex.go
func (af *apexFile) path() string {
return af.apexRelativePath(af.stem())
}
func (af *apexFile) availableToPlatform() bool {
if af.providers == nil || af.providers.commonInfo == nil {
return false
}
if af.providers.commonInfo.IsApexModule {
return android.CheckAvailableForApex(
android.AvailableToPlatform,
af.providers.commonInfo.ApexAvailableFor)
}
return false
}5.3 复制决策
buildApex 的第一步把 filesInfo 转成 staging 目录命令。若 transitive 文件同时可用于 platform 且启用了 linkToSystemLib,它会建立指向 /system/... 的 symlink;否则复制真实 built file。这个优化改变的是 payload 中的文件形式,不改变依赖变体的选择。
源码文件:build/soong/apex/builder.go
for _, fi := range a.filesInfo {
destPath := imageDir.Join(ctx, fi.path()).String()
copyCommands = append(copyCommands, mkdirCmd+" -p "+filepath.Dir(destPath))
if a.linkToSystemLib && fi.transitiveDep && fi.availableToPlatform() {
pathOnDevice := filepath.Join("/", fi.partition, fi.path())
copyCommands = append(copyCommands,
lnCmd+" -sfn "+pathOnDevice+" "+destPath)
} else {
// ... VINTF、extra zip、install 与 symlink 分支
copyCommands = append(copyCommands,
cpCmd+" -f "+fi.builtFile.String()+" "+destPath)
implicitInputs = append(implicitInputs, fi.builtFile)
}
// ... 测试数据和 install map
}6. 容器生成
6.1 apexer输入
buildApex 先生成 canned_fs_config、file contexts、manifest、NOTICE 和 payload 文件树,然后把它们连同 public key 与 --min_sdk_version 传给 apexer。因此 apexer 是容器生成器,不是依赖解析器。
源码文件:build/soong/apex/builder.go
cannedFsConfig := a.buildCannedFsConfig(ctx)
fileContexts := a.buildFileContexts(ctx)
implicitInputs = append(implicitInputs, fileContexts, a.privateKeyFile, a.publicKeyFile)
optFlags = append(optFlags, "--pubkey "+a.publicKeyFile.String())
moduleMinSdkVersion := a.minSdkVersion(ctx)
minSdkVersion := moduleMinSdkVersion.String()
if moduleMinSdkVersion.IsCurrent() || moduleMinSdkVersion.IsNone() {
minSdkVersion = ctx.Config().DefaultAppTargetSdk(ctx).String()
// ... 可选 API fingerprint 输入
}
// ... 计算 targetSdkVersion
optFlags = append(optFlags, "--target_sdk_version "+targetSdkVersion)
optFlags = append(optFlags, "--min_sdk_version "+minSdkVersion)当 min_sdk_version 是 current 或未设置时,构建 action 会将其转成当前分支可用的 API 表达式;这不是把 APEX 自动变成可更新模块。可更新性检查在 Soong 之前的 checkUpdatable 中完成。
6.2 两层签名
apexer 产出 *.unsigned,随后 signapk 以 APEX certificate/private key 生成最终 .apex。--pubkey 和 payload 相关 key 属于 APEX/AVB 层;signapk 的 PEM 与 key 属于外层 APK 签名层,二者不能混为“同一个签名”。
6.3 构建验证
签名 rule 还挂接 runApexLinkerconfigValidation、sepolicy tests、unwanted transitive dependency 检查和 host_apex_verifier。这些 validation 是签名输出的前置约束;验证失败时,Ninja 不应把签名结果视为可安装产物。
源码文件:build/soong/apex/builder.go
// ... 组装 signapk rule、输出路径和隐式输入
validations = append(validations,
runApexLinkerconfigValidation(ctx, signedOutputFile, imageZipOut))
if !a.skipValidation(hostApexVerifier) &&
android.InList(a.payloadFsType, []fsType{ext4, erofs}) {
validations = append(validations, runApexHostVerifier(ctx, a,
signedOutputFile))
}
ctx.Build(pctx, android.BuildParams{
Rule: rule,
Output: signedOutputFile,
Input: unsignedOutputFile,
Implicits: implicits,
Validations: validations,
})
// ... 保存 signedOutputFile,并在需要时设置 outputApexFile7. 更新约束
7.1 最低API
可更新 APEX 必须设置 min_sdk_version,不能使用 current,也不能使用 platform APIs。源码中的原因不是“Mainline 约定”,而是构建系统需要一个稳定的 API 下界,并能沿同一 APEX 的依赖边检查每个模块是否支持该下界。
源码文件:build/soong/apex/apex.go
func (a *apexBundle) checkUpdatable(ctx android.ModuleContext) {
if a.Updatable() {
if a.minSdkVersionValue(ctx) == "" {
ctx.PropertyErrorf("updatable", "updatable APEXes should set min_sdk_version as well")
}
if a.minSdkVersion(ctx).IsCurrent() {
ctx.PropertyErrorf("updatable", "updatable APEXes should not set min_sdk_version to current")
}
if a.UsePlatformApis() {
ctx.PropertyErrorf("updatable", "updatable APEXes can't use platform APIs")
}
// ... future_updatable、Java SDK 与 classpath 检查
}
}CheckMinSdkVersion 沿 payload 依赖遍历;跨出 APEX 边界的 external dependency 不再继续检查,因为它代表 APEX 对外的稳定接口。这个停止条件决定了“所有依赖都必须带同一 min SDK”并不是准确说法。
7.2 稳定接口
对 updatable APEX,Soong 还检查 Java stable SDK、classpath fragment 和 app dependency 的 updatable 属性。它们证明的是构建时的接口约束,不证明设备端已经下载、激活或回滚某个模块。
7.3 Mainline范围
在 AOSP 构建源码中,Mainline 的可观察事实是:一部分 APEX 被标记为 updatable,因此触发最低 API、platform API、classpath 和依赖稳定性检查;apexer 生成可签名容器,最终产物可被后续系统组件消费。Play 服务端的分发策略、设备选择、回滚窗口和实际激活时机不由这些 Soong 函数证明,不能写成本文的结论。
8. 失败分流
apex_available 失败通常发生在 payload 依赖检查,而不是 C/C++ 链接时;min_sdk_version 失败通常带有依赖路径;key 缺失则在 GenerateAndroidBuildActions 设置错误 rule 或直接 property error。把这些现象统一叫“APEX 签名失败”会丢失真正的 owner 和修复入口。
9. 构建测试
Soong 的 apex/apex_test.go 提供两类可反向核验的输入。
第一类是 manifest/min SDK 规则。TestApexManifestMinSdkVersion 建立 myapex_30、myapex_current 和未声明版本的三个 APEX,打开 API fingerprint 环境,然后读取 apexRule 的 opt_flags,断言 30 保留为 --min_sdk_version 30,其余被转换为带 fingerprint 的当前 API 表达式。它证明的是 build rule 参数转换,不证明设备上的 apexd 版本选择。
第二类是依赖拒绝。TestApexAvailable_DirectDep 让 myapex 直接依赖只对 otherapex 开放的 libfoo,断言错误包含 requires "libfoo" that doesn't list the APEX under 'apex_available';TestApexAvailable_IndirectDep 再构造 libfoo → libbar → libbaz,断言错误同时打印三段 dependency tag 路径。它们证明直接和传递 payload 依赖都会被检查,也揭示了测试中的 product-specific 临时豁免;它们不证明运行时 linker namespace 的访问结果。
可执行的源码验证如下:
# 固定 tag 的关键入口
rg -n "DepsMutator|generateApexInfo|ApexTransitionMutatorIncoming" \
build/soong/apex/apex.go build/soong/android/apex.go
# 检查一个产物由哪些 rule 生成
grep -n "apexer\|signapk" out/soong/build.ninja | head
# 构建后只读检查 APEX 外层与 payload 元数据
deapexer info out/target/product/<product>/system/apex/<name>.apex
deapexer list out/target/product/<product>/system/apex/<name>.apex复述主线时应能回答:哪个属性把依赖加入图?哪个 provider 让依赖带上 APEX 名称?哪个 transition 保留 platform variant?哪个结构保存安装路径?哪条 rule 生成 unsigned,哪条 rule 完成 outer signature?若答案跳过 dependencyTag 或把 VNDK APEX 当普通依赖图,说明还没有区分两种 membership 规则。
10. 边界收束
APEX 构建不是“把 Android.bp 列表压成 zip”。Soong 先以 dependency tag 建图,再用 ApexInfo 在依赖边上传播 APEX 身份和最低 API;image mutator 同时为 native 模块选择 system/vendor/product 与 VNDK 版本;GenerateAndroidBuildActions 把最终 variant 转成带 owner/provider 信息的 apexFile;apexer 负责 payload 容器,signapk 负责外层签名,验证 rule 再决定签名结果能否进入构建输出。
这条链能解释三个常见误判:库出现在依赖图里不等于进入 payload;VNDK 版本变体不等于普通 APEX variant;updatable: true 证明的是构建侧稳定性约束,不是 Play 分发已经发生。
