18、AOSP中的Python脚本:Soong的Python插件、编译辅助脚本、构建系统工具链

说到AOSP的构建系统,大家第一反应肯定是Soong和Blueprint。但说实话,Python在AOSP里的地位远比很多人想象的要重要。我刚开始接触AOSP时,以为Python只是些零散的辅助工具,后来深入才发现——Python脚本几乎贯穿了整个构建流程的方方面面。

这一章,我们就来聊聊AOSP里的Python生态。我会从三个维度展开:Soong的Python插件机制、那些藏在角落里的编译辅助脚本、以及构建系统工具链中的Python身影。

18.1 Soong的Python插件:不只是“胶水”

Soong本身是用Go写的,但它的插件系统允许我们用Python来扩展构建逻辑。为什么会这样?因为有些场景下,Go的静态类型和编译特性反而成了掣肘——比如需要动态解析配置文件、或者跟外部工具交互时。

核心概念:Soong的Python插件本质上是一个“桥接层”。Soong在构建过程中会调用Python解释器,执行特定的插件脚本,并将结果回传给构建系统。

我记得第一次在项目中用Python插件,是为了处理一个自定义的镜像打包逻辑。当时需要根据设备型号动态生成分区表,用Go写的话要重新编译Soong,太麻烦了。用Python插件,改个脚本就行。

来看一个典型的插件结构:

# external/soong/plugins/my_plugin.py

def get_partition_table(device_variant):
    """根据设备变体返回分区表"""
    tables = {
        'phone': ['boot', 'system', 'vendor', 'userdata'],
        'tablet': ['boot', 'system', 'vendor', 'userdata', 'media'],
        'tv': ['boot', 'system', 'vendor', 'cache']
    }
    return tables.get(device_variant, tables['phone'])

def on_soong_load(ctx):
    """Soong加载时自动调用"""
    variant = ctx.Config('TARGET_DEVICE')
    partitions = get_partition_table(variant)
    ctx.SetGlobal('PARTITION_LIST', partitions)
    print(f"[Plugin] Loaded partitions for {variant}: {partitions}")

我的习惯:插件脚本里尽量少写复杂逻辑。Python插件只是“胶水”,真正的业务逻辑应该放在独立的Python模块里,插件只负责调用和传参。这样调试起来方便很多。

18.2 编译辅助脚本:那些“看不见”的功臣

AOSP里散落着大量Python脚本,它们不直接参与编译,但少了它们,整个构建流程会寸步难行。我粗略统计过,build/目录下就有超过200个Python脚本。

我把它们分成几类:

类别 典型脚本 作用
环境检测 checkenv.py 检查JDK、Python版本、磁盘空间等
配置生成 genconfig.py 根据BoardConfig生成编译配置
依赖分析 depcheck.py 检查模块间的依赖关系是否完整
日志处理 logcat_parser.py 解析编译日志,提取错误信息
镜像工具 img_from_target.py 从编译产物生成刷机镜像

这里我想重点聊聊checkenv.py。我曾经在一个新人的机器上遇到过编译到一半报错,原因是Python版本是3.11,但AOSP某个脚本只兼容到3.10。这种问题最坑了——编译跑了半小时才暴露出来。

避坑指南:我曾经因为没仔细看checkenv.py的输出,直接跳过了环境检查。结果编译到90%时崩溃,排查了整整一天才发现是OpenJDK版本不对。现在我的习惯是:每次编译前,先跑一遍source build/envsetup.sh,它会自动调用这些检测脚本。

18.3 构建系统工具链:Python的“隐藏技能”

除了Soong插件和辅助脚本,Python在AOSP构建工具链中还扮演着更底层的角色。比如:

  • Ninja文件生成器:Soong生成的Ninja文件里,有一部分构建规则是用Python脚本动态生成的
  • 符号处理工具symbols.py负责处理动态库的符号表,用于调试和性能分析
  • 增量编译优化incremental.py分析文件变更,决定哪些模块需要重新编译

嗯,这里要特别提一下增量编译。AOSP的增量编译一直是个老大难问题——全编译一次要两三个小时,谁受得了?Python脚本在这里起到了关键作用。

来看一个简化版的增量编译判断逻辑:

# build/soong/incremental_check.py

def should_rebuild(module_path, last_build_time):
    """判断模块是否需要重新编译"""
    import os
    from pathlib import Path
    
    # 检查模块目录下所有源文件的时间戳
    module_dir = Path(module_path)
    for file_path in module_dir.rglob('*'):
        if file_path.is_file():
            mtime = os.path.getmtime(file_path)
            if mtime > last_build_time:
                return True
    return False

def get_affected_modules(changed_files):
    """根据变更文件,找出受影响的模块"""
    affected = set()
    for f in changed_files:
        # 解析文件路径,找到所属模块
        module = resolve_module_from_path(f)
        if module:
            affected.add(module)
            # 还要考虑依赖传递
            affected.update(get_dependent_modules(module))
    return list(affected)

你想想看,如果没有这些Python脚本,每次编译都要全量扫描所有文件,那效率得多低?

18.4 实战:写一个自己的编译辅助脚本

光说不练假把式。我建议你动手写一个简单的脚本,用来统计编译产物的大小分布。这个需求我在项目中遇到过好几次——产品经理总问“为什么system分区又变大了?”

#!/usr/bin/env python3
# tools/analyze_out.py

import os
import sys
from collections import defaultdict

def analyze_out_dir(out_path):
    """分析out目录下的文件大小分布"""
    size_map = defaultdict(int)
    file_count = 0
    
    for root, dirs, files in os.walk(out_path):
        for f in files:
            file_path = os.path.join(root, f)
            try:
                size = os.path.getsize(file_path)
                # 按文件类型分类
                ext = os.path.splitext(f)[1] or 'no_ext'
                size_map[ext] += size
                file_count += 1
            except OSError:
                continue
    
    print(f"Total files: {file_count}")
    print(f"{'Extension':<15} {'Size (MB)':>15} {'Percentage':>15}")
    print("-" * 45)
    
    total_size = sum(size_map.values())
    for ext, size in sorted(size_map.items(), key=lambda x: -x[1]):
        mb = size / (1024 * 1024)
        pct = (size / total_size) * 100
        print(f"{ext:<15} {mb:>15.2f} {pct:>14.2f}%")

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: analyze_out.py <out_dir>")
        sys.exit(1)
    analyze_out_dir(sys.argv[1])

我的建议:把这个脚本放到build/tools/目录下,然后加到你的PATH里。以后每次编译完,跑一下就能直观看到哪些文件占用了空间。我曾经靠这个脚本发现了一个重复打包的so库,直接省了50MB的系统分区空间。

18.5 知识体系总览

说了这么多,我们来用一张图把整个知识体系串起来。这张图展示了Python脚本在AOSP构建系统中的位置和关系:

AOSP构建系统中的Python脚本体系 Soong构建系统 (Go) Soong Python插件 编译辅助脚本 构建工具链 动态配置生成 外部工具桥接 环境检测 配置生成 Ninja生成器 增量编译 实际应用场景 分区大小分析 | 依赖检查 | 镜像打包 | 编译日志解析 | 性能优化 Python脚本作为“胶水层”,连接Soong核心与具体构建任务 核心系统 插件层 辅助层 工具链 应用层

从这张图可以看得很清楚:Python脚本在AOSP构建系统中不是孤立存在的,它上接Soong核心,下连各种具体任务,中间还穿插着环境检测、配置生成等辅助功能。说白了,Python就是整个构建系统的“神经网络”——虽然不显眼,但少了它,整个系统就动不了。

我个人觉得,理解Python在AOSP中的角色,比死记硬背那些API更重要。因为当你真正理解了它的定位,遇到问题时自然知道该去哪里找答案——是写个插件?还是改个辅助脚本?还是直接动工具链?

嗯,这一章的内容就到这里。记住:Python脚本在AOSP里不是配角,而是真正的幕后英雄。


公众号:蓝海资料掘金营,微信deep3321