拓冰建站拓冰建站
首页 / 资讯中心 / 正文

BuildRoot构建imx6ull系统实践:从rootfs到第三方库集成与避坑指南

三年前第一次写BuildRoot时我还在用最朴素的方式给imx6ull做系统——交叉编译工具链、busybox、各种依赖库全是手工下载、手工编、手工往rootfs里塞。那篇帖子今天回看只能算一份使用笔记真正在项目里反复踩过的坑其实基本没写。这次说“重制版”就是把这些年反复用、反复踩的东西补上重点放在两件事一是把BuildRoot构建imx6ull系统的运作机制讲透二是把“往BuildRoot构建出的镜像里添加一个第三方so库”这个过程完整走一遍。后者也是后台私信里被问到最多的场景几乎所有做imx6ull产品的朋友迟早都会遇到。如果你刚接触BuildRoot可以把这篇当作一份实践地图如果你已经用它做了一段时间建议直接跳到第3章和第4章那里有我在真实项目里花过不少时间才排查清楚的问题。1. 为什么imx6ull项目我会坚持用BuildRoot1.1 从手工rootfs到构建系统的真实转折最早做imx6ull样机时我的流程是这样的先用gcc交叉编译工具链编出busybox然后手工下载zlib、openssl、sqlite这些依赖库一个个指定--prefix、--host编完再往rootfs目录里拷。听上去挺直接但真正跑起来全是问题。最典型的一次为了给业务程序加一个WebSocket库结果它又依赖了三个小库三个小库又各自有不同的编译选项要求折腾了两天最后编译过了运行又因为库版本对不上报version CXXABI_1.3.8 not found。这种状态下的rootfs是“一次性的”没有任何记录说明某个库当时为什么这样编、打了什么补丁、装到哪个目录。换一台电脑基本就要重来。转用BuildRoot后想法变了系统的最终状态不再是一堆手工操作的结果而是一个defconfig文件加一个make命令就能从零恢复的东西。imx6ull的uboot、内核、rootfs、工具链全在同一个构建体系里管起来配置文件本身就成了项目文档。团队里面任何人拉下来跑一次构建得到的镜像是一致的。1.2 BuildRoot、Yocto与手工方案实际对比下来的取舍很多人在选方案时会在BuildRoot和Yocto之间犹豫。我在imx6ull项目里两个都用过也见过同行在这上面反复折腾最终结论是如果产品形态比较固定、团队规模不大BuildRoot是更务实的选择。维度BuildRootYocto手工拼rootfs学习成本中等Kconfig思路和Linux内核配置很像高bitbake、layer、recipe的体系比较大初看低但维护成本非常隐蔽从零到出镜像通常几十分钟到半天首次准备可能需要一两天依赖多少决定时间不可控可重复构建高defconfig加hash校验能锁定版本高layer机制很强低极易受主机环境影响uboot/内核集成支持能在一个构建里统一下来支持功能很强全手工容易漏NXP imx6ull适配官方和社区都有现成板级配置NXP官方BSP基于Yocto但比较重资料零散适合场景产品明确、BSP相对固定、人少多产品线、复杂定制、大团队验证板子、临时实验Yocto不是不好而是对几个人的小团队来说太“重”。它的layer机制在管理多个产品变体时确实强但代价是概念多、构建耗时久、前期投入大。BuildRoot则更像一台带有菜单的自动化产线Kconfig选好需要的组件产线自动完成下载、编译、安装、打包。两者不冲突但imx6ull这种单板、单一产品形态BuildRoot的性价比明显更高。2. 理清BuildRoot构建imx6ull的全过程才知道卡在哪2.1 一份可用的imx6ull defconfig长什么样很多教程会让新手直接跑make freescale_imx6ull_evk_defconfig然后make这当然能跑通但如果你不知道这个配置里到底选了些什么出了问题基本无从下手。我建议至少把下面这类关键项过一遍BR2_army BR2_cortex_a7y BR2_ARM_FPU_VFPV4y BR2_ARM_EABIHFy BR2_TOOLCHAIN_BUILDROOT_GLIBCy BR2_LINUX_KERNELy BR2_LINUX_KERNEL_CUSTOM_VERSIONy BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE5.15.32 BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILEboard/mycompany/linux-imx6ull.config BR2_PACKAGE_BUSYBOXy BR2_TARGET_ROOTFS_EXT2y BR2_TARGET_ROOTFS_EXT2_4y BR2_ROOTFS_OVERLAYboard/mycompany/rootfs-overlay这里几个项很重要。BR2_cortex_a7代表CPU核型号imx6ull的Cortex-A7和Cortex-A9的构建参数不一样BR2_ARM_FPU_VFPV4和BR2_ARM_EABIHF决定了浮点ABI是硬浮点还是软浮点这一点在接第三方so库时特别容易出问题后面第4章详细说。工具链方面我选了glibc因为很多闭源二进制库对glibc的兼容性最好musl虽然更精简但遇上vendor只拿glibc编出来的库会很头疼。内核这块我习惯用BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE指定一份保存好的内核配置文件而不是完全依赖内核自带的imx_v7_defconfig。原因很简单官方defconfig会在内核版本升级时变化你自己保存的config才能保证项目可控。2.2 从下载到生成镜像五个阶段如何推进BuildRoot的执行过程可以粗略分成五个阶段这一点比具体命令更重要因为排查问题本质上就是在判断“现在卡在哪一个阶段”。下载根据.mk文件里的SITE地址把源码包和补丁下载到dl/目录并校验hash。日志里会出现 foo 1.0 Downloading。这个阶段常见问题就是网络不通或hash不匹配。解压与打补丁源码从dl/解压到build/然后打入.mk里定义的补丁。日志是 foo 1.0 Extracting和 foo 1.0 Patching。编译执行BUILD_CMDS里定义的动作。日志是 foo 1.0 Building。如果编译出错问题基本在源码本身或编译选项。安装把产物安装到staging目录和target目录。日志是 foo 1.0 Installing to target directory。这一阶段最容易出现“编译成功但rootfs里没有库”的问题。最终打包所有包安装完毕BuildRoot会执行target目录的收尾处理清空无用文件、处理符号链接、执行rootfs overlay和post-build脚本最后生成images/下的uboot、内核、rootfs镜像。之所以强调这个阶段划分是因为很多人一看到日志里ERROR就直接怀疑编译实际往往是下载或者安装阶段出问题。区分阶段后排查速度快很多。2.3 output目录怎么配合排查问题构建完成后最常打交道的几个目录在output/下output/target最终rootfs的根目录。程序运行缺库、缺配置都先来这里查。它就是一个解开的rootfs你在开发板上看到的文件系统结构基本和它一致。output/build每个包的源码目录比如output/build/busybox-1.36.1/。想看某个包到底编了什么、临时文件在哪都来这。output/host主机侧的工具链和工具。BuildRoot会把交叉编译工具链、fakeroot、genimage等工具都装在这里所以获取工具链路径一般是output/host/bin/下的arm-buildroot-linux-gnueabihf-gcc。output/images最终产物imx6ull项目里一般是u-boot.bin、zImage、imx6ull-xxx.dtb、rootfs.ext4这些。有个小技巧output/build下每个包目录里都有一堆.stamp_开头的文件它们是BuildRoot增量构建的依据。比如你想强制重编某个包不需要删整个目录执行make 包名-dirclean再make就可以。这个比直接手动删目录更规范因为BuildRoot会正确处理stamp状态。3. 给imx6ull文件系统添加第三方so库从“能用”到“好用”3.1 一个真实场景接入vendor提供的音频算法库假设你们产品是一个带语音功能的工业设备imx6ull上跑Linux业务程序需要调用vendor提供的回声消除库vendor给的是一个二进制包里面有libfoo_aec.so.1.2.3、头文件foo_aec.h以及一个LICENSE文件。这个库要进rootfs被/usr/bin下的业务程序加载。乍一看很简单拷到rootfs里不就行了但问题在于怎么拷、拷到哪个stage、如何让后续构建还能复现、如何让依赖关系可维护。这就有了两条路线一是走BuildRoot的package机制二是用overlay或post-build脚本。两者适用场景不同我分别说。3.2 用package方式管理第三方库推荐BuildRoot的package机制本质上是把“一个软件组件该从哪里下载、怎么编译、怎么安装”写成一套描述文件。对于第三方二进制库即使没有源码也完全可以套用这个机制。在BuildRoot源码目录下新建package/libfoo_aec/包含Config.in和libfoo_aec.mkpackage/libfoo_aec/ ├── Config.in ├── libfoo_aec.mk └── libfoo_aec.hashConfig.in内容config BR2_PACKAGE_LIBFOO_AEC bool libfoo_aec help Third-party AEC audio library for imx6ull. Binary distribution from vendor.同时在package/Config.in里找一个合适的位置加一行source package/libfoo_aec/Config.inlibfoo_aec.mk是我最常用的一种写法LIBFOO_AEC_VERSION 1.2.3 LIBFOO_AEC_SITE /home/me/vendor/libfoo_aec LIBFOO_AEC_SITE_METHOD local LIBFOO_AEC_LICENSE PROPRIETARY LIBFOO_AEC_INSTALL_STAGING YES define LIBFOO_AEC_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0644 $(D)/include/foo_aec.h $(STAGING_DIR)/usr/include/foo_aec.h $(INSTALL) -D -m 0755 $(D)/libfoo_aec.so.1.2.3 $(TARGET_DIR)/usr/lib/libfoo_aec.so.1.2.3 ln -sf libfoo_aec.so.1.2.3 $(TARGET_DIR)/usr/lib/libfoo_aec.so.1 ln -sf libfoo_aec.so.1 $(TARGET_DIR)/usr/lib/libfoo_aec.so $(INSTALL) -D -m 0644 $(D)/LICENSE $(TARGET_DIR)/usr/share/licenses/libfoo_aec/LICENSE endef $(eval $(generic-package))这里有几个点特别值得说明。LIBFOO_AEC_SITE_METHOD local表示直接把本机目录作为源码来源BuildRoot不会下载也不会做hash校验。这种方式最适合开发期调试但正式交给团队或CI时建议把二进制包打成tar放到内部服务器然后用URL方式加LIBFOO_AEC_SOURCE和hash文件保证可复现。INSTALL_STAGING YES决定了头文件和库是否装进staging目录。staging是给其他包“交叉编译时可见”的根目录。如果后续还有别的软件要链接这个库就必须开这个选项如果只是运行时加载只需要target目录就够了。命令最后用了ln -sf建了两个软链接这是很多新手会漏掉的。Linux动态加载器通常按libfoo_aec.so.1这样的名字去找库如果你只放了libfoo_aec.so链接器不一定认。vendor给的so文件如果带soname尤其要注意按soname建链接。配置好之后编译就非常顺make O../build-imx6ull freescale_imx6ull_evk_defconfig make O../build-imx6ull menuconfig # 在Target packages菜单里勾选libfoo_aec make O../build-imx6ull调试时还可以用make libfoo_aec-rebuild重编这个包或者make libfoo_aec-dirclean彻底清理后重编完全不影响其他包。单是这一点就已经比手工拷库高明太多。3.3 二进制很稳定时用overlay和post-build脚本更省事如果你的二进制库几乎不变只是需要原样放进rootfs用overlay更直接。BuildRoot支持一个目录作为rootfs的覆盖层构建target时会把这个目录里的内容整体拷到rootfs对应位置。先建目录board/mycompany/rootfs-overlay/ └── usr/ └── lib/ ├── libfoo_aec.so.1.2.3 ├── libfoo_aec.so.1 - libfoo_aec.so.1.2.3 └── libfoo_aec.so - libfoo_aec.so.1然后在defconfig里加BR2_ROOTFS_OVERLAYboard/mycompany/rootfs-overlayBuildRoot生成rootfs时会把rootfs-overlay里面的文件树完整拷过去符号链接也会保留。这个方式的优点是零学习成本、零配置缺点是overlay内容不参与任何依赖管理库换了版本需要手动同步目录容易忘记。适合那种“只发布一次之后基本不动”的纯二进制组件。post-build脚本则更灵活一些。在defconfig里指定BR2_ROOTFS_POST_BUILD_SCRIPTboard/mycompany/post-build.sh脚本会在rootfs收尾阶段执行比如按不同产品型号把不同目录下的库拷进去#!/bin/sh set -e TARGET_DIR$1 if [ -f $TARGET_DIR/etc/board-type ]; then cp -a board/mycompany/extra-libs/type-a/* $TARGET_DIR/usr/lib/ else cp -a board/mycompany/extra-libs/type-b/* $TARGET_DIR/usr/lib/ fi实际选择时我的习惯是需要被其他包链接、有版本迭代、需要参与构建依赖的组件必须走package单纯的二进制分发、不参与依赖关系的优先overlay需要按构建参数做条件化拷贝时才上post-build脚本。这个决策顺序能帮你避开大部分维护上的坑。3.4 构建完成后的验证清单库加进去了不代表就能加载。我每次构建完会按这个清单核一遍可以少跑很多次目标板测试在宿主机上先看rootfs里到底有没有库find /path/to/output/target/usr/lib/ -name libfoo*用目标工具链的readelf查看业务程序的动态依赖arm-buildroot-linux-gnueabihf-readelf -d /path/to/output/target/usr/bin/your_app | grep NEEDED如果NEEDED列出的是libfoo_aec.so.1那rootfs里就必须存在一个实际能被加载的libfoo_aec.so.1文件或软链接。只放了libfoo_aec.so不带版本号运行时一定会报cannot open shared object file。上目标板后同样查一次系统里的库以及依赖readelf -d /usr/bin/your_app | grep NEEDED ls -l /usr/lib/libfoo*如果业务程序还用了别的库比如libssl.so.3也要顺手检查一下rootfs里有没有版本匹配的库。我曾遇到过NEEDED里少了依赖、业务程序启动时半天定位不到原因的情况后来发现是宿主机上链接时自动带进了某个主机库路径目标板根本没有所以链接阶段就要绷紧这根弦。4. 重制版想重点提醒的几个坑4.1 浮点ABI不匹配库装进去也会崩溃这是我在imx6ull上遇到的最隐蔽的问题之一。当时接一个第三方音视频库编译链接全部顺利业务程序一跑就Segmentation fault连日志都没有。排查了很久最后发现是库的浮点ABI和BuildRoot工具链对不上。imx6ull的Cortex-A7默认支持硬浮点BuildRoot默认配置下BR2_ARM_EABIHFy工具链生成的程序使用VFP寄存器传浮点参数。而vendor给的旧库是用软浮点ABI编的两边约定不一致函数传参方式不同调用时栈和寄存器行为混乱直接崩溃。排查办法是用工具链的readelf查看库的属性arm-buildroot-linux-gnueabihf-readelf -A libfoo_aec.so | grep Tag_ABI_VFP_args如果输出包含Tag_ABI_VFP_args: VFP registers说明是硬浮点如果这一行不存在或显示integer那就是软浮点。应用程序也一样可以查arm-buildroot-linux-gnueabihf-readelf -A /path/to/your_app | grep Tag_ABI_VFP_args解决方案一般只能找vendor要一个与工具链ABI匹配的版本或者统一把整个工具链配置改成软浮点重新构建系统。但imx6ull用软浮点会损失一些浮点性能不太值得。最好的做法是在立项阶段就把工具链版本、ABI信息写进技术对接文档要求第三方库按这个规范提供。4.2 编译通过不等于rootfs里的库完整另一个高频坑是程序确实链接了libfoo_aec.so编译也通过了但生成的rootfs里就是没有这个库。原因往往出在安装阶段比如只设置了INSTALL_STAGING YES而INSTALL_TARGET_CMDS里没有把库放进$(TARGET_DIR)或者链接时用-L指定的是宿主机某个目录链接器直接用了主机上的同名库而不是staging目录下的库。遇到这种情况先看BuildRoot的包日志里安装阶段是否执行了 libfoo_aec 1.2.3 Installing to target directory然后直接去output/target/usr/lib/确认文件是否存在。如果存在但开发板上报找不到大概率是软链接不对。readelf -d显示的NEEDED是什么名字rootfs里就必须有能被加载器找到的对应文件名这个逻辑之前说过值得反复强调。还有一次比较特殊库是放进去了但库本身依赖了另一个系统库我没注意到。检查方法是对这个so本身也做一次readelfarm-buildroot-linux-gnueabihf-readelf -d output/target/usr/lib/libfoo_aec.so.1.2.3 | grep NEEDED如果有额外的libbar.so.2再确认rootfs里是否包含对应的库和符号链接。这个“依赖链”的验证往往比验证主程序更关键。4.3 BuildRoot大版本升级带来的隐性变化BuildRoot迭代速度不算慢从2021.02跳到2023.02这种跨大版本升级绝不能简单认为“defconfig不变就一切不变”。我升级过一次发现原配置里不少Kconfig选项在menuconfig里消失了但不是真的消失了而是改名、换依赖或者被合并到了别的选项里。比如rootfs文件系统相关的若干选项、某些工具链选项在不同版本里写法都有调整。更隐蔽的是默认行为变化。同一个busybox版本切换、内核配置变化可能导致原本正常的某个命令行工具出现行为差异而这些变化不一定会在构建日志里明显体现。所以我的建议是升级前一定用savedefconfig保存一份当前配置并用git留痕。命令很简单make O../build-imx6ull savedefconfig它会把当前完整菜单配置精简成一个最小化的defconfig方便你diff。升级后先不急着版本大跳小而快地升对比每次构建日志里各包的版本变化再用第3章的验证清单检查rootfs。这样即使有问题定位范围也会小很多。5. 再次谈BuildRoot我对它的一些新理解5.1 核心不是脚本而是带状态的包管理模型第一次用BuildRoot时我内心把它理解成一个“超级脚本库”——反正最后生成了rootfs能跑就行。后来反复改包、清包、排查依赖才意识到它真正了不起的地方不是“自动化”而是“带状态管理的构建模型”。output/build下每个包都有很多.stamp_文件它们记录了这个包当前执行到了解压、补丁、编译还是安装阶段。当你改了一句源码只需要make 包名-rebuildBuildRoot会根据stamp状态只重编这个包而不是把全系统重来一遍。这就是为什么它能支撑几千个软件包的构建还能保持效率。理解了这一点你就不再会动不动make clean全部重来而是精准地清理和重编目标包。这种模型也改变了我的构建哲学构建系统不是“一把梭跑完拉倒”而是一个可以被你随时打开外壳检修的机器。哪个环节不对看stamp、看日志、看对应的output/build目录就能定位到很具体的层级。5.2 用BR2_EXTERNAL把自定义内容从主源码里分离这里再说一个工程化建议。前面第3章的示例是在BuildRoot源码目录里直接新增package这样开发期很方便但长期维护有个隐患一旦你升级BuildRoot版本目录覆盖或冲突很难避免。更干净的方式是使用BR2_EXTERNAL把自定义的包、板级配置、overlay全部放到BuildRoot源码之外的独立目录。目录结构类似br2-external/ ├── Config.in ├── external.desc ├── external.mk └── package/ └── libfoo_aec/ ├── Config.in └── libfoo_aec.mk构建时指定外部treemake BR2_EXTERNAL/path/to/br2-external O../build-imx6ull freescale_imx6ull_evk_defconfig这样可以做到BuildRoot主源码保持“干净”你的定制内容被独立管理两个部分都可以分别升级和备份。imx6ull项目的uboot、内核配置、启动脚本、产品应用软件都适合放进这个外部tree里。5.3 对imx6ull长期维护的一点建议如果让我给刚把BuildRoot捡起来的同行一句建议那就是永远把它当成一台能打开外壳检修的机器不要当成黑盒。引入任何新组件沿着dl/、output/build、output/target这条线去验证任何一次rootfs异常先问自己它发生在下载、编译、安装还是最终打包阶段再动手改。我现在的imx6ull项目已经把defconfig、board目录、BR2_EXTERNAL、补丁全部收进git任何一个新人按照README执行一遍构建就能拿到和线上产品一模一样的镜像。这个过程在手工rootfs时代是几乎不可能做到的。希望这篇重制版能帮你少走一些我当年走过的弯路。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门