把 20 个第三方库的交叉编译,从手工活做成框架

一、问题是什么

入职某电视厂商智能电视操作系统团队的第二个月,被分到一个新人任务:把某流媒体平台设备认证框架(NRDP 6.0)所需的约 20 个第三方开源库,全部交叉编译到嵌入式 Linux 平台上去。

这件事在团队里此前没人系统做过。原因很简单——它不属于业务功能,属于"基础设施"。每个人遇到都是临时手工解决一次,然后扔在自己工位上,下一个人接手又从头来一遍。

20 个库,每个库都有自己的:

  • 构建系统(Autotools / CMake / Meson / 自定义 Makefile 四种都有)
  • 依赖关系(libpng 依赖 zlib,curl 依赖 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-29 printf.sh 特性练习:关联数组、printftputIFSxargs、函数返回值、子 shell、read
2021-12-08 printf2.sh 进阶练习:export -fxargs -I {}、brace expansion、for/until、字符串/文件测试
2021-12-16 build.sh 编译框架成型

从特性练习到框架产出,17 天。回头看,这种"边学边产出"的方式有个好处:练习脚本里写过的每一个用法,几乎都直接进了 build.sh。学习目标和产出目标重合,没有"学了不知道用在哪"的浪费。

三、框架的核心抽象

最终的 build.sh 有几个关键设计。

3.1 用关联数组把"库元数据"集中描述

declare -A cmds
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 里,根脚本不关心。

根脚本只做四件事:

  1. 解析命令行参数(编译哪些库)
  2. 按依赖顺序遍历关联数组
  3. 进入每个库的目录,调用 ./build.sh <toolchain> <output>
  4. 把产物聚合到统一目录

这是典型的"接口与实现分离"——根脚本是接口(编排),子 build.sh 是实现(适配各自构建系统的差异)。

3.3 产物聚合 copy_so2libs()

20 个库散落在不同的源码目录里编出来,但消费方(设备认证框架)只期望从一个目录拿 .socopy_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 个库使用了四种不同的构建系统,每种都要单独适配交叉编译:

  • Autotools(zlib / openssl / curl / libpng / freetype 等):./configure --host=<triple> --prefix=<sysroot> CC=<cross-gcc>
  • CMake(libjpeg-turbo / openh264 等):通过 CMAKE_TOOLCHAIN_FILE 注入工具链定义
  • Meson:cross file 描述目标平台
  • 手写 Makefile(tremor / 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.shprintf2.sh 这两份练习脚本,今天看是博客的"学习曲线证据",但当时它们是真实需要的——关联数组怎么遍历、xargs -I {} 的引号转义怎么写,必须在主战场之外的安全沙盒里先确认清楚,否则在 build.sh 里调试会被构建错误的噪声淹没。

新人任务里藏着支点。第三方库编译这种"边角料",做下来反而让我吃透了整套 SDK 的依赖关系全貌——后续深入设备认证框架对接层代码、做架构重构,入口就是从这里来的。

七、延伸:同样思路在另一视频平台的复用

之后接手另一视频平台的对接,遇到类似问题:该平台的依赖库与系统库高度耦合,全量编译又慢又占地方。

把"依赖库与系统库解耦"这条原则套上去,做了一次拆分:

  • 编译时间减少 30%
  • 存储占用减少 80%

抽象一旦立住,下一次类似场景不再需要从零思考。这才是把手工活做成框架的真正回报——不是省了一次工时,而是省了所有未来类似场景的工时


造工具的工具,把手工劳作变成可复用流程。

posted @ 2026-06-23 15:15  荣--  阅读(3)  评论(0)    收藏  举报