OpenWrt 编译踩坑记录

写在前面:全网有成千上万个编译 OpenWrt 的文章,竟无一能搜到详细的定制与踩坑记录。 当你发现同芯片产品销量加起来几百万,刷机玩家帖子阅览量好几万,但所有定制的 repo 只有…… 1个(略有夸张,事实上是uboot两个代际各一个,rom 两个代际各一个,总共3个主要维护者)的时候,你也会同时产生符合刻板印象但又匪夷所思的双重情绪

入门必知

挑选硬件

我的方法是直接搜路由器的爆款,然后人气排行顺序里找一个网上玩家很多的设备。 我这台小米 AX3000T 快递到货的第一个小时就变成了 OpenWrt 系统,就因为我是瞄准了系统去选的设备。便宜,质量不错,社区庞大。

基础概念

名词解释通常约等于常识,真正的基础概念 此处不解释,只列一些要花一点时间才逐渐理解的事实:

  1. OpenWrt 有不同版本的 fork,有个国区特供版叫 ImmortalWrt,基本修改方向是增加一些国人常用的软件包和特性,以及更多美化(符合国区玩家的思路)

    • 应该首选这个 fork 的原因不是因为「通常它更实用」,而是「国内仅有的开发者往往都在这个 fork 上工作」
    • 要选国内社区的成果的原因也不是因为「更实用」,而是因为这些设备的主销地区都在国内,国外很少玩它
  2. 现在(以及未来)像 OS 这种级别的项目都是超级工程,因此开发者总是倾向于在一些大 mono repo 上维护/实现成千上万的硬件适配。但反过来,不管你接触到什么样的设备,基本架构都会高度一致。

    • 提这点的原因是我发现之前开发嵌入式设备接触 uboot 的经验完全可以平移到任何使用 bootloader + 内核 + 文件系统 架构的设备上,它们用到的 bootloader 大概率都是同一个 uboot 项目。理解这一点有助于分辨危险操作的边界 —— 这意味着
      • bootloader 总是能执行,几乎不用担心所有救砖机制都失效了要拆芯片的可能性
      • 你几乎总是能通过串口 + lrzsz 刷回任何东西 —— 通过串口。
  3. 源码树结构

    • OpenWrt 的源码树结构是一个庞杂又设计简单的超级 Makefile 系统,没有采用任何像 CMake + ninja (ESP-IDF)、 bitbake (yocto) 这样的现代构建系统。这就导致源码项目几乎都以「文件夹」的形态出现,换言之 —— 「解压、替换一个src.zip」反而是符合该项目flavor的方式。这也就解释了为什么国内Fork版的 ImmortalWrt 代码hygiene堪忧,里面夹杂了很多拼音项目、不知从哪复制粘贴来的移植、不知依赖为什么这么写的申必 Makefile,以及各种奇怪的 patch
    • 也正由于这种组织特性,源码往往以「厂商」、「Category」为单位组织 —— 驱动、系统app、luci app会出现在同一个「MTK」文件夹下,尽管理论上它们属于完全不同的构建阶段、依赖也各不互通。

Docker Build

docker 的好处自然是能隔离干净openwrt的一堆自带工具链,减少主系统包的冗余。但openwrt强制使用非root用户build,而docker环境默认root。你又几乎必定需要一个root用户来修改容器里的工具以便随时修改build流程/写定制文件。一个不用来回切换用户(保持root用户build)的简单方式是:

  • export FORCE_UNSAFE_CONFIGURE=1
  • FAKE fakeroot
    cat <<EOF > staging_dir/host/bin/fakeroot  
    #!/bin/bash  
    # 绕过所有伪装逻辑,直接运行  
    "\$@"  
    EOF
    fakeroot原本用于fake需要root权限的系统调用,通过 LD_PRELOAD 劫持并记录原本的意图调用,先向 caller 返回成功来跳过 build 过程的特权读写,最后把记下来的特权读写通过 daemon 还原到目标文件系统中。但我们有一个真正的root,所以直接 pass through 所有参数,让调用真正成功即可。

实用经验

UBOOT

FINAL FailSafe

  • 当网络访问 uboot web 页面彻底失效的时候,还有主板上的 UART 作为最后手段。
    本来想展开讲讲的,但想起发过的这条评论,作为guide足够了

ROM

屎山开始堆积……

  • 开局起手式:
    1. 装编译toolchain的toolchain
    2. ./scripts/feeds update -a
    3. ./scripts/feeds install -a
      这会更新 feeds/** 以及 feeds/packages/** 的内容,包含通用软件源中的各种源码。

Source Tree

  • package/ 中的子目录存放了 feeds 以外的 custom packages

    • 通过 $(eval $(call BuildPackage,SELF)) 这行 magic 指令把SELF加入到构建目标列表中。
    • 与之配套,每个package的 makefile 中会有一段 magic define,形如
       define KernelPackage/mt_wifi  
         CATEGORY:=MTK Properties  
         TITLE:=MTK wifi AP driver  
         DEPENDS:=+wifi-dats  
         DEPENDS+=+kmod-conninfra  
         DEPENDS+=+kmod-mediatek_hnat  
         FILES:=$(PKG_BUILD_DIR)/mt_wifi_ap/mt_wifi.ko \  
            $(PKG_BUILD_DIR)/mt_wifi/embedded/plug_in/warp_proxy/mtk_warp_proxy.ko  
         DEPENDS+=+kmod-warp  
         AUTOLOAD:=$(call AutoProbe,mt_wifi mtk_warp_proxy)  
         SUBMENU:=Drivers  
         MENU:=1  
      endef

    eval 会执行define段中的赋值指令,从而规定该package的发布文件、构建文件、依赖等信息

  • files/etc/banner => SSH 登录显示的 banner

  • package/base-files/files/bin/config_generate => ImmortalWRT 把开机初始化配置。luci 的 banner (机器的 hostname) 和默认 serve 地址都是通过配置文件固定的,因此修改默认配置文件需要直接来改默认配置的生成器。

initramfs VS squashfs

initramfs 是一套纯内存虚拟文件系统,把整个文件系统与内核封装到一起;一般用于重刷主系统(不需要文件落盘)的recovery系统。与之对应,squashfs 是把准备写入flash的系统文件树压缩到一起,最大化空间利用率的一种文件系统。但在实践中几乎完全不需要 initramfs ,原因有几点:

  1. 还有uboot作为刷入新系统的保底,尤其是在刷入了支持网络访问的uboot的场合
  2. 内核裁剪非常困难和费神 —— 因为在选用设备的默认 kconfig 时已经预选中了大量主系统必须模块和特性,受限于源码树的维护质量,还可能有大量不合理的硬依赖。这会导致initramfs的镜像/内核并不能比主系统小多少,双倍浪费空间
  3. 最坑的一点:构建系统天生不具备双profile能力,生成 initramfs 还是 squashfs 是同一个配置文件里的不同开关控制的,在频繁调整自定义改动的情况下根本不可能维护一致性良好的精简 initramfs+全功能 squashfs.

package-y VS package-m

menuconfig 中,一个必选包用 _*_ 表示,可选的 builtin 包用 <*>(子模块)或 [*](主功能)表示。这些 * 标记的包会在构建目标列表中被表示为 package-y 格式,封装到最终产物的文件系统或镜像(内核)中。

而一个 modulerized 包会用 [M]<M> 表示,构建产物会是一个可另行安装的软件包(.ipk)。当构建目标是内核特性时,这项特性不会构建为内核镜像的一部分,而是以 .ko (kmod)的形式存在,通过 insmod 来加载。由于kmod与内核有版本一致性约束,内核仅能加载与自己同版本的kmod,所以如果自己构建ROM时没选好内核模块,事后是无法通过公开软件源安装的。这些 modulerized 的包在构建目标列表中被表示为 package-m格式,构建完成后它们会 populate 到输出目录,要 刷好系统后自行上传 并安装。

所以如何选定 package-y 包与 package-m 包就有十分微妙的讲究。标y太多,最终镜像太大将完全无法完成刷入,标m太多可能导致 bootstrap 阶段缺失重要功能,或者安装时要上传大量文件(luci WebUI是不支持批量上传安装的,要靠 opkg 命令行)增加麻烦。


简易的判断标准:

  • 网络和wifi相关的kmod必须 -y ,否则可能开机即失联,这种情况还得接上串口打断系统启动流程回到uboot才能重刷系统。
  • 相互冲突的 feature 可以全都设为 -m 方便做 AB 对照
  • 控制 -y-m 的比例,使得最终镜像在 RAM 的 60% 以下,否则镜像会无法完成上传。
  • 如果一些包的依赖死活要带上你不想要或不理解为什么要的东西,不要在 -y-m 上纠结,那一定是这个包 Makefile 里写的 DEPENDS 的屎。直接去擦干净。

mtwifi

最后提一嘴 mtwifi - 苦痛之源。

其实这个型号的设备在 OpenWrt 官方社区就有稳定维护版本。然而开源社区使用的 MTK WiFi 驱动 MT76xx 存在严重的断流问题,内网UDP吞吐率不到20M/s,这导致诸如 moonlight 等串流工具几乎完全无法使用。

mtwifi 是一个 闭源 leak 的版本,它「SDK的部分」,性能是远远优于开源驱动的,能够根本上解决适配不佳导致的网络栈问题。然而这个 leak 的源头无法追溯,也没有官方指引,导致其维护质量实在一言难尽,你甚至不知道维护者自己知不知道如何维护.这个PR连猜带蒙最后 work 了一部分,又在几个版本后被完全引入的新文件覆盖了。整个流程没有什么讨论交流,也没有review,也没有评估衍生版本的影响,几乎可以说是我提交出去被合并的最莫名其妙的一个PR。