27K Star 的 OpenWrt,不是路由器固件,是 125 个平台的交叉编译编排器

发布于 2026年07月11日 18:26 #DevOps#Github 解读 原文链接

27K Star 的 OpenWrt,不是路由器固件,是 125 个平台的交叉编译编排器 封面图
  • OpenWrt 是构建固件的系统,而非固件本身,提供从源码交叉编译定制固件的工具链
  • 125 个 target 平台通过声明式 Makefile 定义架构、特性、内核版本,构建系统自动处理
  • 自编译工具链不信任系统 GCC,实现可复现构建,确保二进制一致性
  • feeds 系统从外部仓库拉取软件包,实现分布式包管理和可插拔源
  • 可复现交叉编译范式由自编译工具链、声明式 target 和可插拔包源三层架构支撑

上周有个朋友买了个小米路由器,想刷 OpenWrt。他跑来问我,OpenWrt 到底是个什么,为什么一个路由器系统能有 27,509 个 Star,12,615 个 Fork?

我问他,你觉得 OpenWrt 是什么?他说,就是个路由器固件吧,像 Windows 之于电脑。

这个类比差得有点远。OpenWrt 不是固件,它是一个构建固件的系统。你拿到的不只是一个镜像文件,而是一整套从源码交叉编译出针对你硬件的定制固件的工具链。这才是它 20 年不被淘汰的核心。

一句话定位,不是固件是构建器

大多数路由器固件,DD-WRT、Tomato、华硕官改,给你的是一个编译好的镜像文件,你刷进去就用。能改的有限,不能装的软件也有限。

OpenWrt 给你的是一套构建系统。你在这套系统里选目标硬件、选内核版本、选要装的软件包,然后它帮你从源码开始,交叉编译出一个完全定制的固件镜像。make menuconfig 这个操作,用过 Linux 内核编译的人会很熟悉,OpenWrt 把同样的体验搬到了路由器固件上。

这个差异决定了 OpenWrt 的生态形态。它不是一个产品,是一个平台。别人在它上面构建自己的固件,比如 immortalwrt、immortalwrt-23.05 各种社区分支,底层全是 OpenWrt 的构建系统。

stamp 依赖链,构建流水线的骨架

翻开 OpenWrt 的顶层 Makefile,最核心的是这一段依赖定义,

$(toolchain/stamp-compile): $(tools/stamp-compile) $(if $(CONFIG_BUILDBOT),toolchain_rebuild_check)
$(target/stamp-compile): $(toolchain/stamp-compile) $(tools/stamp-compile) $(BUILD_DIR)/.prepared
$(package/stamp-compile): $(target/stamp-compile) $(package/stamp-cleanup)
$(package/stamp-install): $(package/stamp-compile)
$(target/stamp-install): $(package/stamp-compile) $(package/stamp-install)

五条依赖定义,画出了一条完整的构建流水线。tools 先编译(宿主工具),toolchain 跟上(交叉编译工具链),然后 target(内核和平台代码),接着 package(用户态软件包),最后生成镜像。每一步用 stamp 文件标记完成状态,不重复编译。

你想想看这条链路意味着什么。你要给一台联发科 MT7986 的路由器编译固件,OpenWrt 会先在你 x86 机器上编译一套针对 ARM 的 GCC 工具链(toolchain/gcc/),用这个工具链交叉编译 Linux 内核(target/linux/mediatek/),再交叉编译你选的每个软件包(package/),最后打包成可刷写的镜像(include/image.mk)。

全链路从 C 源码开始,不依赖任何预编译二进制。这就是为什么 OpenWrt 能支持 125 个 target 平台,每个平台的内核版本、CPU 架构、驱动补丁可以完全独立配置。

125 个 target,每个都是独立的 Linux 移植

我拉了完整的文件树数了一下,target/linux/ 下有 125 个 Makefile,对应 125 个 target 平台。从常见的 mediatek(联发科)、qualcommax(高通)、ramips(雷凌)、ath79(高通 Atheros),到冷门的 pistachio(MIPS 创意芯片)、malta(QEMU MIPS 仿真)、sifiveu(RISC-V)。

每个 target 的 Makefile 长这样,以 target/linux/mediatek/Makefile 为例,

ARCH:=arm
BOARD:=mediatek
BOARDNAME:=MediaTek ARM
SUBTARGETS:=filogic mt7622 mt7623 mt7629
FEATURES:=dt-overlay emmc fpu gpio nand pci pcie rootfs-part separate_ramdisk squashfs usb
KERNEL_PATCHVER:=6.18

六个字段定义一个平台。ARCH 决定 CPU 架构,SUBTARGETS 把一个芯片系列拆成更细的型号,FEATURES 声明这个平台支持哪些硬件能力(能不能接 eMMC、有没有 PCIe、支不支持硬件浮点),KERNEL_PATCHVER 指定用哪个 Linux 内核版本。

这些字段不是文档注释,它们是构建系统的输入参数。include/kernel.mk 会读 KERNEL_PATCHVER 决定下载哪个版本的内核源码,include/image.mk 会读 FEATURES 决定镜像格式(squashfs 还是 jffs2,要不要 separate ramdisk)。整个构建系统是声明式的,你声明平台能力,系统按声明生成构建步骤。

每个 target 下面还有 base-files/ 目录,存放该平台特有的网络配置(etc/board.d/02_network)、LED 配置(01_leds)、升级脚本(lib/upgrade/platform.sh)和设备树源文件(dts/)。这些文件在构建时会被打包进镜像的 rootfs,构成该平台的「出厂配置」。

toolchain 自编译,不信任任何预编译产物

OpenWrt 对工具链的态度很偏执,不信任你系统上的 GCC,自己编译一套。

toolchain/ 目录下有完整的工具链构建定义,包括 binutils(链接器)、gcc(编译器)、fortify-headers(安全强化头文件)、glibc/musl(C 库)。每个组件都有版本选择(Config.version)和补丁目录(patches/)。

你想想看这个工作量。binutils 有 2.44、2.45、2.46 三个版本的补丁,每个版本有 MIPS 动态链接符号修复、默认模拟模式修改等平台特定补丁。GCC 的补丁更多,从 ABI 兼容到软浮点模拟,每个 target 架构的特殊处理都要在这里解决。

这套自编译工具链的好处是可复现性。在 include/image.mk 里有这么一行,

IMG_PART_SIGNATURE:=$(shell echo $(SOURCE_DATE_EPOCH)$(LINUX_VERMAGIC) | $(MKHASH) md5 | cut -b1-8)

镜像的分区签名是 SOURCE_DATE_EPOCH(构建时间戳)和 LINUX_VERMAGIC(内核版本魔法数)的 md5。只要源码版本和构建环境一致,任何人编译出的镜像二进制完全相同。这不是碰巧,是 OpenWrt 刻意设计的可复现构建(reproducible build),让社区可以验证官方发布的镜像确实来自公开源码。

feeds 系统,多仓库的包聚合层

OpenWrt 的软件包不在主仓库里。主仓库只有构建系统和核心包,其余的通过 feeds 机制从外部仓库拉取。

feeds.conf.default 定义了默认的 feed 源,

src-git packages https://git.openwrt.org/feed/packages.git
src-git luci https://git.openwrt.org/project/luci.git
src-git routing https://git.openwrt.org/feed/routing.git
src-git telephony https://git.openwrt.org/feed/telephony.git
src-git video https://github.com/openwrt/video.git

五个外部仓库,涵盖通用软件包(packages)、Web 界面(LuCI)、路由协议(routing)、电话(telephony)和视频(video)。用户也可以加自己的 feed(src-link custom /usr/src/openwrt/custom-feed)。

include/feeds.mk 负责把 feeds 的包安装到 package/feeds/ 下作为符号链接。构建时,这些外部包和主仓库的包平等参与编译。scripts/feeds 脚本做的是 update(拉最新定义)和 install(创建符号链接)两件事。

这个设计让 OpenWrt 的包生态可以分布式管理。核心构建系统保持精简(13,459 个文件已经够大了),社区包各自独立演进,不互相阻塞。你甚至可以完全不碰官方 feeds,只用自己的私有 feed,构建一个完全封闭的定制固件。

ipk 和 apk,双包格式并存

include/feeds.mk 里有一个细节让我注意到了。包文件查找同时支持两种格式,

opkg_package_files = $(wildcard \
	$(foreach dir,$(PACKAGE_SUBDIRS), \
	  $(foreach pkg,$(1), $(dir)/$(pkg)_*.ipk)))

apk_package_files = $(wildcard \
	$(foreach dir,$(PACKAGE_SUBDIRS), \
	  $(foreach pkg,$(1), $(dir)/$(pkg)*.apk)))

OpenWrt 正在从传统的 ipk(基于 opkg 包管理器)迁移到 apk(Alpine 的 apk-tools)。这是一个重大架构变更。ipk 是 OpenWrt 自己设计的包格式,简陋但够用,没有依赖解析能力。apk 带来了真正的依赖管理、签名验证和事务性更新。

include/package-pack.mkIPKG_STATE_DIR:=$(TARGET_DIR)/usr/lib/opkg 这个路径定义还在,说明迁移还没完成,两套系统并存。这种迁移在嵌入式 Linux 圈子是大事,因为无数社区脚本和教程都假设 opkg 命令可用。OpenWrt 选择渐进式迁移而不是一刀切,是务实的工程决策。

LLM 审查 PR,开源项目的前沿实践

.github/ 目录的时候我发现了两个有意思的文件,.github/llm-review-rules.md.github/workflows/llm-review.yml

OpenWrt 在用 LLM 自动审查 PR。llm-review.yml 定义了一个定时任务,每天 UTC 3 点和 15 点跑一次,每次最多审查 24 个 PR。审查规则写在 llm-review-rules.md 里,包括设备树语法现代化(LED label 要用 color + function 属性而不是旧式 label)、MAC 地址从 MTD 读取要改用 nvmem-cells、补丁要用 make target/linux/refresh 而不是 git format-patch 刷新等。

你想想看这个场景。OpenWrt 每天收大量「新增设备支持」的 PR,很多是业余开发者提交的,代码风格不符合内核规范,设备树写法过时。人工 review 这些 PR 非常耗时。用 LLM 做第一轮筛选,把明显的风格问题和过时写法标出来,人工 review 只关注实质性问题。这是一个非常实际的 LLM 应用场景,不是为了炫技,是为了解决维护者带宽不足的真问题。

镜像是镜像,上游是上游

有一个容易被忽略的事实。OpenWrt 的 GitHub 仓库不是主仓库。

README 写得很清楚,「This repository is a mirror of https://git.openwrt.org/openwrt/openwrt.git It is for reference only and is not active for check-ins. We will continue to accept Pull Requests here.」

GitHub 上 27,509 个 Star,12,615 个 Fork,但代码的权威来源是 git.openwrt.org。GitHub 只是镜像和 PR 接收窗口。PR 在这里提交后,会通过 staging tree 流转到 git.openwrt.org 合并。

这种模式在大型开源项目里不算罕见(Linux 内核也是类似,GitHub 上只有 torvalds/linux 镜像),但在路由器固件领域,OpenWrt 是唯一一个坚持自托管 Git 的。好处是 CI/CD 不依赖 GitHub Actions(虽然 GitHub 这边也有 workflow),坏处是贡献者需要适应两套系统。

issue 区确实是活跃的。高赞 issue 集中在硬件支持(#21083 MediaTek BPi-R4 Pro 支持,318 条评论)、网络回归(#10224 PPPoE flow offload 不工作,320 条评论,开了 3 年多没关;#11650 DSA 破坏 WLAN 漫游,202 条评论)和驱动问题(#14541 ath10k 5GHz 射频检测不到,287 条评论)。这些 issue 的特点是,每个都绑定特定的硬件平台和内核版本,复现成本高,修复需要内核驱动知识,不是随便改改配置能解决的。这也是 OpenWrt 维护成本高的结构性原因,125 个平台 × N 个内核版本 = 测试矩阵爆炸。

可复现的交叉编译范式

拆完 OpenWrt 的构建系统,我想提炼一个东西出来。

OpenWrt 做的事情,抽象到最高层,就是「在一个架构上,从源码编译出另一个架构的可运行系统」。这件事的难点不是编译本身,而是让整个过程可复现、可扩展、可定制

可复现靠的是自编译工具链 + SOURCE_DATE_EPOCH 时间戳 + LINUX_VERMAGIC 版本签名。同样的源码在任何机器上编译出二进制一致的镜像,这让供应链安全审计成为可能。

可扩展靠的是 target 声明式定义。加一个新硬件平台,只需要在 target/linux/<new_platform>/Makefile 里写 6 行声明,指定架构、子平台、特性、内核版本,构建系统自动处理剩下的交叉编译、补丁应用、镜像打包。125 个平台就是这么积累起来的。

可定制靠的是 menuconfig + feeds。用户不需要改 OpenWrt 的源码,只需要在配置界面里勾选,就能控制固件里包含哪些包、内核开哪些选项、镜像用什么文件系统。feeds 机制让包来源完全可插拔。

这个「自编译工具链 + 声明式 target + 可插拔包源」的三层架构,我称之为可复现交叉编译范式。它不只适用于路由器。你想想看,Yocto Project 做的也是类似的事,用 BitBake 从源码交叉编译嵌入式 Linux。Buildroot 也是。区别在于 OpenWrt 更聚焦网络设备场景,Yocto 更通用,Buildroot 更轻量。

什么场景该用 OpenWrt?你要给网络设备(路由器、网关、AP)构建定制固件,需要完整的包管理和 Web 界面,OpenWrt 是事实标准。什么场景不该用?如果你的设备不是网络设备,或者你不需要包管理(只需要一个精简的 rootfs),Buildroot 更合适,构建更快,复杂度更低。如果你做的是工业级嵌入式产品,需要长期支持和完整的 SDK 生态,Yocto 虽然学习曲线陡峭,但更适合商业化场景。

OpenWrt 20 年的生命力,不是因为它做了什么别人做不了的事,而是因为它把交叉编译这件事做到了「普通开发者也能上手」的程度。125 个平台的支持不是一天建成的,是 20 年里一个平台一个平台攒出来的。这种积累,才是开源项目真正的护城河。

评论互动

© 2026 王若风的技术博客 · Powered by Astro