把 20 个第三方库的交叉编译,从手工活做成框架
一、问题是什么入职某电视厂商智能电视操作系统团队的第二个月被分到一个新人任务把某流媒体平台设备认证框架NRDP 6.0所需的约20 个第三方开源库全部交叉编译到嵌入式 Linux 平台上去。这件事在团队里此前没人系统做过。原因很简单——它不属于业务功能属于基础设施。每个人遇到都是临时手工解决一次然后扔在自己工位上下一个人接手又从头来一遍。20 个库每个库都有自己的构建系统Autotools / CMake / Meson / 自定义 Makefile 四种都有依赖关系libpng 依赖 zlibcurl 依赖 openssl 和 c-ares配置参数host / target / sysroot / 工具链路径产物形态.so命名规则不统一如果按一个库一次手工的方式做做完 20 个相当于把同样的痛重复经历 20 次并且这份知识无法沉淀——下次工具链升级、库版本更替、新平台适配全部要再走一遍。我的判断是这件事必须做成一个框架而不是 20 段独立的脚本。二、Shell 不是零基础但缺关键特性此前在视频云平台做过一键部署Shell 写过不少。所以这次不是从if/then开始学而是发现要做的事情需要几个之前没碰过的特性关联数组declare -A要把库名 → 路径 产物模式的映射写清楚普通索引数组撑不住printf格式化 tput终端控制20 个库的编译输出需要分色、对齐否则失败定位是个噩梦xargs -I {}export -f选择性编译、并行编译需要这个组合IFS切分、read多模式把关联数组里path;pattern这种打包字符串拆开所以我做了一件略反直觉的事——先写两个纯练习脚本把这些特性吃透再去写编译框架本身。日期文件性质2021-11-29printf.sh特性练习关联数组、printf、tput、IFS、xargs、函数返回值、子 shell、read2021-12-08printf2.sh进阶练习export -f、xargs -I {}、brace expansion、for/until、字符串/文件测试2021-12-16build.sh编译框架成型从特性练习到框架产出17 天。回头看这种边学边产出的方式有个好处练习脚本里写过的每一个用法几乎都直接进了 build.sh。学习目标和产出目标重合没有学了不知道用在哪的浪费。三、框架的核心抽象最终的 build.sh 有几个关键设计。3.1 用关联数组把库元数据集中描述declare-Acmds cmds[zlib]sdk/3rd_lib/zlib-1.2.8;libz.so*cmds[c-ares]sdk/3rd_lib/c-ares-1.12.0;libcares.so*cmds[openssl]sdk/3rd_lib/openssl-1.0.2h;libssl.so*|libcrypto.so*# ... 共 20 个库每一行回答两个问题这个库在哪里编、它编出来的产物长什么样。这是整个框架的数据底座。后续所有动作编译、聚合、过滤都从这张表派生不再有第二处库元数据。3.2 每个库一个独立 build.sh根脚本只做编排20 个库的差异构建系统、特定参数、补丁需求封装在各自目录下的 build.sh 里根脚本不关心。根脚本只做四件事解析命令行参数编译哪些库按依赖顺序遍历关联数组进入每个库的目录调用./build.sh toolchain output把产物聚合到统一目录这是典型的接口与实现分离——根脚本是接口编排子 build.sh 是实现适配各自构建系统的差异。3.3 产物聚合copy_so2libs()20 个库散落在不同的源码目录里编出来但消费方设备认证框架只期望从一个目录拿.so。copy_so2libs()按照关联数组里登记的产物模式libz.so*、libssl.so*|libcrypto.so*等把所有.so收拢到3rd_libs/。这一步把源码组织方式和二进制消费方式解耦——源码可以按各自习惯组织二进制对外只有一个目录契约。3.4 选择性编译can_compile()20 个库的全量编译耗时不短日常开发只动其中 1~2 个。命令行参数支持指定库名白名单can_compile(lib_name)在遍历时做过滤。3.5 根路径定位get_parant N每个库的目录嵌套深度不一样。get_parant 5表示从当前位置向上 5 级定位 SDK 根目录让脚本可以从任意子目录调用而不出错——这是个小细节但在多人协作时省了非常多的我在错的目录跑了你的脚本事故。四、覆盖的 20 个库zlib, c-ares, Little-CMS, tremor, fdk-aac, libelf, libdwarf, icu, openssl, ogg, expat, netlink(libnl), libjpeg-turbo, libunwind, openh264, libpng, freetype, libwebp, curl, openjpeg按领域分类领域库音频编解码ogg, tremor, fdk-aac视频编解码openh264图像处理libpng, libjpeg-turbo, libwebp, Little-CMS, openjpeg网络curl, c-ares, openssl, netlink (libnl)文本/国际化icu, expat字体freetype压缩zlib调试libelf, libdwarf, libunwind五、异构构建系统的适配难点20 个库使用了四种不同的构建系统每种都要单独适配交叉编译Autotoolszlib / openssl / curl / libpng / freetype 等./configure --hosttriple --prefixsysroot CCcross-gccCMakelibjpeg-turbo / openh264 等通过CMAKE_TOOLCHAIN_FILE注入工具链定义Mesoncross file 描述目标平台手写 Makefiletremor / fdk-aac 等直接改 Makefile 里的CC/AR/LD等变量必要时打补丁虽然底层千差万别但对外暴露的接口被统一为同一个签名build.sh toolchain_path output_dir根脚本对所有库一视同仁地调这个签名。具体怎么把它桥接到 Autotools 还是 CMake是每个库 build.sh 自己的事。这个统一抽象是整个框架最重要的设计选择——它让加一个新库的成本变成在关联数组里加一行 在该库目录下写一个适配 build.sh不需要动根脚本。六、收获6.1 工程层面把20 次手工编译变成一次性的编译框架团队后续新人接手不再重复踩坑后续工具链升级、库版本更替时改动局限在单个库的 build.sh根脚本不动6.2 思维层面让我真正体会到几件事先确定抽象再写第一行实现代码。这件事如果一开始就上手写先把 zlib 编出来再说会一路被实现细节带走最后写出 20 个互不相干的脚本框架永远诞生不了。先写关联数组那一张表哪怕是空的就强制自己思考统一接口是什么。学习与产出可以同步发生。printf.sh和printf2.sh这两份练习脚本今天看是博客的学习曲线证据但当时它们是真实需要的——关联数组怎么遍历、xargs -I {}的引号转义怎么写必须在主战场之外的安全沙盒里先确认清楚否则在 build.sh 里调试会被构建错误的噪声淹没。新人任务里藏着支点。第三方库编译这种边角料做下来反而让我吃透了整套 SDK 的依赖关系全貌——后续深入设备认证框架对接层代码、做架构重构入口就是从这里来的。七、延伸同样思路在另一视频平台的复用之后接手另一视频平台的对接遇到类似问题该平台的依赖库与系统库高度耦合全量编译又慢又占地方。把依赖库与系统库解耦这条原则套上去做了一次拆分编译时间减少 30%存储占用减少 80%抽象一旦立住下一次类似场景不再需要从零思考。这才是把手工活做成框架的真正回报——不是省了一次工时而是省了所有未来类似场景的工时。造工具的工具把手工劳作变成可复用流程。