Skip to content

APEX构建边界

追踪 APEX 依赖入图、变体选择、VNDK image 变体、文件收集、apexer 打包与 signapk 签名,并定位可更新约束的失败边界。

基于android-17.0.0_r1
AndroidSoongAPEXVNDKMainline

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 依赖变体选择。

对象所有者何时生效消费者
ApexInfoandroid.ApexInfoProvidertransition mutate依赖模块的 variant 和检查器
apex_availableAPEX-aware 模块variant mutate / payload 检查apexInfoMutator、可用性检查
apexFileapexBundleGenerateAndroidBuildActionsstaging copy、apexer
未签名 APEXbuildApex ruleaction 执行后signapk、验证 rule
已签名 .apexapexBundle.outputFilesign 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

go
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

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

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

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

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

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

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

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

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

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

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

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

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,并在需要时设置 outputApexFile

7. 更新约束 ​

7.1 最低API ​

可更新 APEX 必须设置 min_sdk_version,不能使用 current,也不能使用 platform APIs。源码中的原因不是“Mainline 约定”,而是构建系统需要一个稳定的 API 下界,并能沿同一 APEX 的依赖边检查每个模块是否支持该下界。

源码文件:build/soong/apex/apex.go

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 的访问结果。

可执行的源码验证如下:

bash
# 固定 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 分发已经发生。