本文是系列第3篇。前两篇分别从业务和系统原理的角度建立整体认知。本文进入实际工程领域——如何从OpenWrt源码出发,构建出包含自定义功能的固件。

一、为什么要从源码编译

OpenWrt官方提供了预编译固件,可以直接下载刷入设备。但在SD-WAN终端开发场景中,通常需要从源码编译,原因是:

  • 内核定制:需要修改内核驱动(如添加4G模块支持)、内核参数(如启用调试符号)
  • 默认配置:需要设置自定义的管理密码、默认IP地址、WiFi参数等
  • 软件包集成:需要将自定义的软件包(如Agent程序及运行时)直接编译进固件
  • 硬件适配:需要为新型号路由器添加设备支持

二、编译环境搭建

2.1 宿主机要求

OpenWrt编译需要在Linux环境下进行(推荐Ubuntu),且不要使用root用户。典型配置要求:

  • CPU:高主频多核(影响编译速度)
  • 内存:8GB以上
  • 硬盘:40GB以上可用空间
  • 网络:需要访问GitHub等外网资源(国内环境需配置代理或使用镜像源)

2.2 基础步骤

从零开始编译固件的完整流程如下:

# 1. 安装编译依赖
sudo apt-get install subversion g++ zlib1g-dev build-essential git \
    python rsync man-db libncurses5-dev gawk gettext unzip \
    file libssl-dev wget zip time curl

# 2. 获取源码
git clone https://github.com/openwrt/openwrt.git
cd openwrt

# 3. 选择版本分支
git tag                    # 查看可用版本标签
git checkout v19.07.8      # 切换到目标版本

# 4. 更新软件包源(feeds)
./scripts/feeds update -a  # 下载所有feed源的软件包索引
./scripts/feeds install -a # 创建符号链接到package目录

# 5. 配置编译选项
make menuconfig            # 弹出配置菜单

# 6. 下载源码包
make -j8 download V=s      # 预下载所有依赖源码

# 7. 开始编译
make -j1 V=s               # 首次编译建议单线程,方便定位错误

编译完成后,固件文件位于bin/targets/目录下。

三、编译目录结构

理解OpenWrt编译体系的关键是理解其目录结构。源码根目录下的主要目录及其作用:

openwrt/
├── config/          # 全局配置文件
├── dl/              # Download:从网络下载的源码压缩包
├── feeds/           # 外部软件包源的本地缓存
├── include/         # 编译系统使用的.mk文件
├── package/         # 软件包定义(Makefile和源码)
├── scripts/         # 编译辅助脚本
├── target/          # 平台相关代码(内核、设备树、分区表)
├── toolchain/       # 交叉编译工具链的构建定义
├── tools/           # 宿主机工具的构建定义
├── tmp/             # 编译临时文件
├── build_dir/       # 编译工作目录(解压、编译都在这里进行)
│   ├── host/        # 宿主机工具的编译
│   ├── toolchain-*/ # 交叉工具链的编译
│   └── target-*/    # 目标平台软件包的编译
├── staging_dir/     # 编译产物的安装目录
│   ├── host/        # 宿主机工具的安装位置
│   ├── toolchain-*/ # 交叉工具链的安装位置
│   └── target-*/    # 目标平台库文件的安装位置
└── bin/             # 最终输出目录
    └── targets/     # 按平台分类的固件和ipk包

3.1 几个关键目录的关系

理解编译过程,需要关注以下三个目录的协作:

dl(download):编译过程中的源码下载缓存。从网络下载的源码压缩包会存放在这里。如果需要使用自己修改过的源码(比如打了4G驱动补丁的内核),可以制作一个同名压缩包放入此目录,编译时会优先使用本地文件。

build_dir:实际的编译工作区。源码从dl解压到这里,configure、make、make install都在此目录中进行。需要注意的是,执行make clean会清空此目录,所以直接在build_dir中修改文件是不可持续的——必须通过打补丁的方式来持久化修改。

staging_dir:编译产物的”仓库”。编译好的库文件、头文件会安装到此目录,供后续编译其他依赖它的软件包时使用。交叉工具链也安装在此目录下。

3.2 feeds机制

OpenWrt的软件包来自多个”feed”(软件源),每个feed是一个独立的Git仓库。默认配置包含以下feed:

  • packages:基础库和工具(最常见的软件包来源)
  • luci:Web管理界面(LuCI)
  • routing:网络路由相关工具(如mwan3)
  • telephony:语音通信相关工具

feeds源的定义文件是源码根目录下的feeds.conf.default。可以将其修改为自维护的仓库地址:

# 官方源
src-git packages https://git.openwrt.org/feed/packages.git;openwrt-19.07

# 自维护源(如内部仓库)
src-git packages https://git.example.com/packages.git;openwrt-19.07

也可以添加本地源,将自定义软件包直接放在本地目录中:

src-link myfeed /path/to/myfeed

更新和安装feed的操作:

./scripts/feeds update -a           # 拉取所有feed的最新代码
./scripts/feeds install mypackage   # 只安装指定包
./scripts/feeds install -a          # 安装所有包

安装操作的本质是在package/feeds/目录下创建指向feeds/目录的符号链接。

四、单个软件包的编译过程

以编译一个名为lua的软件包为例,OpenWrt编译系统会将整个过程分为7个步骤:

步骤1:读取Makefile
    读取 package/utils/lua/Makefile,获取包的定义信息
    ↓
步骤2:获取源码
    如果是Git/SVN源,克隆源码到dl/目录,打包为 tar.gz
    如果是URL源,直接下载到dl/目录
    ↓
步骤3:解压源码
    将 dl/lua-x.x.x.tar.gz 解压到
    build_dir/target-mips_xxx/lua-x.x.x/
    ↓
步骤4:配置(Configure)
    在 build_dir 中执行 ./configure
    (通过环境变量传入交叉编译参数)
    ↓
步骤5:编译(Compile)
    在 build_dir 中执行 make
    ↓
步骤6:安装(Install)
    将编译产物安装到
    build_dir/target-mips_xxx/lua-x.x.x/ipkg-mipsel_24kc/
    ↓
步骤7:打包(Package)
    将 ipkg 目录打包为 lua-x.x.x-1_mipsel_24kc.ipk
    输出到 bin/mipsel_24kc/packages/base/

编译单个包的命令:

make package/lua/compile V=s     # 编译lua包
make package/lua/clean            # 清理lua包的编译产物
make package/lua/{clean,compile}  # 清理后重新编译

五、menuconfig配置详解

make menuconfig弹出的配置界面是整个编译流程的控制中心。主要配置项包括:

5.1 目标平台选择

Target System    → 处理器架构(如MediaTek Ralink MIPS)
Subtarget        → 具体子系列(如MT76x8)
Target Profile   → 设备型号(如具体的路由器型号)

这三项决定了编译出的固件适用于哪种硬件。选错目标平台,固件将无法在设备上运行。

5.2 软件包选择

每个软件包有三种状态:

  • <*>:编译并集成到固件中(默认选中,固件包含此包)
  • <M>:编译但不集成到固件(单独生成ipk文件,可以后续手动安装)
  • < >:不编译

在搜索包时可以使用/键搜索,然后按数字键跳转到对应位置。

5.3 固件镜像配置

Target Images
├── Kernel partition size (MB)     # 内核分区大小,默认较小
├── Root filesystem partition size # 根文件系统分区大小
└── ...                            # 其他镜像选项

如果编译时选择了大量内核模块,默认的内核分区大小(2MB)可能不够,需要适当增大,否则系统启动时会找不到内核。

六、编写OpenWrt软件包的Makefile

当需要将自己开发的软件打包到OpenWrt中时,需要编写符合OpenWrt规范的Makefile。

6.1 基本模板

include $(TOPDIR)/rules.mk

# 包的基本信息
PKG_NAME:=mypackage
PKG_VERSION:=1.0
PKG_RELEASE:=1

PKG_BUILD_DIR:= $(BUILD_DIR)/$(PKG_NAME)

include $(INCLUDE_DIR)/package.mk

# 包的元数据(在menuconfig中显示)
define Package/$(PKG_NAME)
    SECTION:=utils
    CATEGORY:=Utilities
    TITLE:=My custom package
    DEPENDS:=+libc +libpthread    # 依赖包(+号表示自动选中)
    PKGARCH:=all                   # 目标架构(all表示不依赖具体平台)
    MAINTAINER:=author@example.com
endef

define Package/$(PKG_NAME)/description
    A detailed description of my custom package.
endef

# 编译前的准备工作(留空表示无需准备)
define Build/Prepare
endef

# 编译步骤(留空表示无需编译)
define Build/Compile
endef

# 安装到固件中(将文件复制到目标根目录)
define Package/$(PKG_NAME)/install
    $(INSTALL_DIR) $(1)/usr/bin
    $(INSTALL_BIN) ./files/myprogram $(1)/usr/bin/
    $(INSTALL_DIR) $(1)/etc/config
    $(INSTALL_CONF) ./files/myconfig $(1)/etc/config/
endef

# 编译入口
$(eval $(call BuildPackage,$(PKG_NAME)))

6.2 关键变量说明

  • $(1):代表目标系统的根目录(即固件中的/
  • $(INSTALL_DIR):创建目录(权限0755)
  • $(INSTALL_BIN):复制可执行文件(权限0755)
  • $(INSTALL_DATA):复制数据文件(权限0644)
  • $(INSTALL_CONF):复制配置文件(权限0600)
  • $(CP):复制文件或目录
  • $(RM):删除文件或目录

6.3 安装前后脚本

Makefile支持定义包安装和卸载时自动执行的脚本:

# 安装前执行
define Package/$(PKG_NAME)/preinst
    #!/bin/sh
    echo "Before installing mypackage..."
endef

# 安装后执行(常用于创建软链接、设置权限)
define Package/$(PKG_NAME)/postinst
    #!/bin/sh
    if [ -z "$${IPKG_INSTROOT}" ]; then
        echo "Enabling mypackage service..."
        /etc/init.d/mypackage enable
    fi
endef

# 卸载前执行
define Package/$(PKG_NAME)/prerm
    #!/bin/sh
    if [ -z "$${IPKG_INSTROOT}" ]; then
        /etc/init.d/mypackage disable
    fi
endef

注意$${IPKG_INSTROOT}变量的用途:当系统在固件编译过程中安装包时(不是在真实设备上安装),这个变量有值,脚本会跳过服务操作。只有在真实设备上通过opkg安装时,才会执行服务启用/禁用。

6.4 实际示例:打包预编译的运行时环境

在SD-WAN终端中,Agent的运行时环境以预编译包的形式集成。以下是实际使用的Makefile简化版:

PKG_NAME:=sdwan-runtime-env
PKG_VERSION:=1
PKG_RELEASE:=2

define Package/$(PKG_NAME)
    SECTION:=lang
    CATEGORY:=Languages
    SUBMENU:=Scripting
    PKGARCH:=all    # 预编译包不依赖特定平台
    DEPENDS:=+libpthread +libbz2 +libopenssl +libffi +libsqlite3 +libuci +zlib
    TITLE:=Runtime environment for SD-WAN agent
endef

# 无需编译,直接安装
define Build/Compile
endef

# 将预编译文件复制到目标位置
define Package/$(PKG_NAME)/install
    $(CP) ./files/* $(1)/
    # 创建软链接
    $(LN) runtime $(1)/usr/bin/runtime
    $(LN) libruntime.so.1.0 $(1)/usr/lib/libruntime.so
endef

七、SDK与Toolchain:应用层开发

如果不需要修改内核,只需要开发应用程序,可以使用SDK(Software Development Kit)而不是完整的源码工程。

7.1 SDK vs 源码工程

对比项 完整源码工程 SDK
内核编译 支持 不支持
应用开发 支持 支持
包含的软件包 全部 仅平台相关的基础包
feeds 包含 不包含
体积 大(数GB) 较小
适用场景 固件定制、内核开发 应用层ipk包开发

SDK的获取方式有两种:

  1. 从OpenWrt官网下载对应平台和版本的预编译SDK
  2. 在源码工程的make menuconfig中选择编译SDK,输出文件在bin/目录下

7.2 Toolchain(交叉编译工具链)

Toolchain是交叉编译的核心工具集。它包含了在x86主机上编译MIPS/ARM目标程序所需的所有工具——编译器(gcc)、链接器(ld)、汇编器(as)等。

使用Toolchain之前,需要配置环境变量:

# 将Toolchain的bin目录加入PATH
export PATH=/opt/OpenWrt-Toolchain-ramips-mt7628_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-i686/toolchain-mipsel_24kec+dsp_gcc-4.8-linaro_uClibc-0.9.33.2/bin:$PATH

# 设置staging目录(编译时查找头文件和库文件的路径)
export STAGING_DIR=/path/to/openwrt/staging_dir

# 使变量生效
source /etc/bash.bashrc

验证安装:

mipsel-openwrt-linux-gcc -v    # 应输出交叉编译器版本信息

7.3 C运行库的选择

OpenWrt使用的C标准库会影响交叉编译的兼容性:

C库 特点 适用场景
glibc 功能最全,体积最大 桌面/服务器系统
uClibc 专为嵌入式设计,体积小 不支持MMU的微处理器
musl libc 轻量级,标准兼容性好 OpenWrt默认选择

OpenWrt从较新版本开始默认使用musl libc。它的设计目标是作为glibc的轻量替代品,同时保持良好的POSIX标准兼容性。在交叉编译时,需要确保编译器工具链和目标设备上的C库版本一致。

八、手动打包ipk

在某些场景下,可能需要跳过编译系统,直接将准备好的文件打包为ipk格式(比如将交叉编译好的运行时库打包):

# 1. 创建临时目录结构
mkdir -p /tmp/mypackage/CONTROL
mkdir -p /tmp/mypackage/usr/bin
mkdir -p /tmp/mypackage/usr/lib

# 2. 放入程序文件
cp myprogram /tmp/mypackage/usr/bin/

# 3. 编写CONTROL控制文件
cat > /tmp/mypackage/CONTROL/control << EOF
Package: mypackage
Version: 1.0.0-1
Depends: libc, libpthread
Architecture: mipsel_24kc
Installed-Size: 102400
Section: utils
Description: My custom package for OpenWrt
EOF

# 4. 打包
scripts/ipkg-build -o root -g root /tmp/mypackage /tmp/

注意事项:

  • Architecture字段需要与目标平台匹配,可通过opkg print-architecture查看
  • 在Ubuntu环境下打包时,ipkg-build脚本中调用的date命令格式可能不兼容,需要在脚本头部添加alias date="date \"+%Y-%m-%d %H:%M:%S\""来修复

九、常见编译问题

9.1 编译清理命令

make clean      # 清理bin和build_dir(保留配置和toolchain)
make dirclean   # 额外清理staging_dir和toolchain
make distclean  # 彻底清理(包括配置和下载的源码,慎用)

9.2 内核补丁的持久化

在build_dir中修改的内核文件,执行make clean后会丢失。正确的做法是:

  1. 保存修改前的原始文件
  2. 修改文件
  3. 使用diff -ruN生成补丁文件
  4. 将补丁文件放入target/linux/<board>/patches/目录

这样每次编译时,补丁会自动应用到内核源码上。

9.3 查找依赖关系

编译时如果提示缺少某个库文件,可以通过以下方式定位依赖来源:

cd staging_dir/target-*/pkginfo
grep "libxxx.so" *.provides    # 查找哪个包提供了该库

下一篇将进入定制化开发领域——如何修改OpenWrt的默认配置、网络初始化流程和MAC地址设置,让固件一开机就符合SD-WAN终端的要求。