OpenWrt Image Generation Scripts and Firmware Layout: How the Build System Constructs Router Firmware
OpenWrt firmware images are constructed by a Make-based pipeline defined in include/image.mk and include/image-commands.mk, which orchestrates kernel compilation, rootfs creation, and device-specific binary assembly before appending JSON metadata.
Understanding the OpenWrt image generation scripts and firmware layout is essential for developers customizing builds or porting to new hardware. The openwrt/openwrt repository uses a sophisticated multi-stage build system that transforms kernel binaries and root filesystems into flashable router images through reusable macros and device-specific assembly logic.
Core Build System Architecture
The image generation system relies on two primary Makefiles that separate high-level orchestration from low-level implementation details.
High-Level Orchestration in include/image.mk
The file include/image.mk serves as the central conductor. It defines the BuildImage macro that drives the entire process and provides the Device/Build/* infrastructure used by every target. This Makefile handles variable initialization—such as constructing IMG_PREFIX (lines 44–52) from version, board, and subtarget strings—and coordinates the sequential stages from preparation through final artifact creation.
Low-Level Commands in include/image-commands.mk
The companion file include/image-commands.mk contains the actual building blocks. It implements a large collection of Build/* functions (e.g., Build/append-image, Build/uboot, Build/tplink-v1-image) that perform specific binary manipulation tasks. These functions are invoked by device definitions in target/linux/*/image/ directories to construct vendor-specific firmware formats.
The Five-Stage Image Generation Pipeline
The build system processes firmware creation through distinct phases orchestrated by include/image.mk.
1. Preparation and Staging
The process begins with Image/Prepare, which creates output directories ($(BIN_DIR), $(KDIR)/tmp) and sets up the rootfs staging area. This stage ensures the build environment is ready before any compilation begins.
2. Kernel Image Construction
The BuildKernel rules (found around lines 175–180 of include/image.mk) construct the raw kernel image (vmlinux) and optionally embed device tree blobs (DTBs). Functions like Image/BuildKernel/MkuImage and Image/BuildKernel/Initramfs handle compression and header formatting according to target requirements.
3. Root Filesystem Generation
Depending on the selected rootfs type (squashfs, jffs2, ext4, ubifs, erofs, or targz), the build system invokes one of the Image/mkfs/* macros. For example, Image/mkfs/squashfs mounts the staged root directory (via $(call mkfs_target_dir,$(1))) and executes mksquashfs4, while Image/mkfs/ext4 calls make_ext4fs. Each macro packages the root filesystem into the appropriate binary format.
4. Device-Specific Image Assembly
After kernel and rootfs binaries are ready, the pipeline enters the composition phase. The Device/Build/image macro creates a temporary combined image at $(KDIR)/tmp/$(call DEVICE_IMG_NAME,…) and then delegates to family-specific builders. Depending on the device definition, this might call Build/tplink-v1-image, Build/fit, Build/append-ubi, or custom vendor functions defined in target/linux/*/image/*.mk.
5. Metadata Appending and Signing
The final stage injects verification data. Build/append-metadata (lines 92–99 of include/image-commands.mk) calculates SHA-256 checksums and embeds a JSON metadata blob generated by scripts/json_add_image_info.py. For signed images, the system uses usign/ucert to cryptographically verify firmware authenticity before the binaries are copied to $(BIN_DIR).
Firmware Layout Structure
The resulting firmware image is a linear binary containing several logical sections:
+-------------------+-------------------+-------------------+-------------------+
| Header (optional) | Kernel (uImage) | RootFS (squashfs) | Metadata (JSON) |
+-------------------+-------------------+-------------------+-------------------+
- Header: Vendor-specific data added by functions like
Build/tplink-v1-headerorBuild/dlink-ai-recovery-header. - Kernel: May be a raw uImage, FIT (Flattened Image Tree) container via
Build/fit-image, or uImage with appended DTB (Build/append-dtb). - RootFS: The filesystem image generated by the selected
mkfstool. - Metadata: JSON block containing version strings, compatible device lists, and checksums added by
Build/append-metadata.
Essential Build Variables
Several key variables control how images are named and structured.
IMG_PREFIX and Naming Conventions
The IMG_PREFIX variable (defined in include/image.mk, lines 44–52) forms the base filename for all artifacts. It typically follows the pattern openwrt-$(VERSION)-$(BOARD)-$(SUBTARGET), resulting in names like openwrt-22.03.5-ramips-mt7620.
Device Metadata and Image Types
Device-specific variables control image generation:
DEVICE_VENDOR,DEVICE_MODEL,DEVICE_NAME: Populated byDevice/Initand exported viaDevice/Exportfor metadata generation.IMAGES: List of image types to build (e.g.,sysupgrade factory).ARTIFACTS: Additional files to generate separately from primary images (such as standalone DTB files).IMAGE_METADATA: Custom JSON fields appended to the standard metadata block.
Implementing Custom Devices
Adding support for new hardware requires creating device entries in target/linux/*/image/*.mk files.
Basic Device Definition
Here is a minimal device definition that builds both sysupgrade and factory images:
# target/linux/ramips/image/myrouter.mk
DEVICE_VENDOR := MyVendor
DEVICE_MODEL := MyRouter
DEVICE_NAME := myrouter
DEVICE_PACKAGES := kmod-mt7603 kmod-usb2
IMAGES := sysupgrade factory
KERNEL := Image/BuildKernel/MkuImage,gzip,$(KERNEL_LOADADDR),$(KERNEL_ENTRY),$(KDIR)/vmlinux
$(eval $(call Device,$(DEVICE_NAME)))
Running make generates:
bin/targets/ramips/mt7620/openwrt-22.03.5-ramips-mt7620-myrouter-sysupgrade.binbin/targets/ramips/mt7620/openwrt-22.03.5-ramips-mt7620-myrouter-factory.bin
Using Vendor-Specific Builders
For devices requiring proprietary headers, use specialized builders defined in include/image-commands.mk:
# TP-Link 2022 style device
TPLINK_BOARD_ID := 0x12345678
TPLINK_SUPPORT_STRING := "OpenWrt 22.03.5"
IMAGES := sysupgrade
$(eval $(call Device/Build,image,$(1),sysupgrade,$(DEVICE_NAME)))
This expands to Build/tplink-image-2022, which executes scripts/tplink-mkimage-2022.py to embed support strings.
Injecting Custom Metadata
Append custom fields to the firmware metadata using the IMAGE_METADATA variable:
IMAGE_METADATA := "\"custom\":\"value\""
The Build/append-metadata function automatically incorporates this into the final JSON block:
{
"metadata_version": "1.1",
"compat_version": "1.0",
"custom": "value",
"version": {
"dist": "OpenWrt",
"version": "22.03.5",
"revision": "r12345",
"target": "ramips/mt7620",
"board": "myrouter"
}
}
Summary
- OpenWrt image generation is orchestrated by
include/image.mkandinclude/image-commands.mk, which provide theBuildImagemacro andBuild/*command functions. - The pipeline follows five stages: preparation, kernel compilation, rootfs creation (
Image/mkfs/*), device-specific assembly (Device/Build/image), and metadata signing (Build/append-metadata). - Firmware binaries follow a linear layout: optional vendor header, kernel (uImage/FIT), root filesystem (squashfs/ext4/etc.), and JSON metadata.
- Device definitions in
target/linux/*/image/*.mkuse variables likeIMAGES,DEVICE_VENDOR, andIMG_PREFIXto control output filenames and formats. - Custom builders can reuse existing functions (e.g.,
Build/fit,Build/append-ubi) or implement new ones ininclude/image-commands.mk.
Frequently Asked Questions
What is the difference between include/image.mk and include/image-commands.mk?
include/image.mk handles high-level orchestration, defining the BuildImage macro and coordinating the overall pipeline flow. include/image-commands.mk implements the low-level building blocks—such as Build/append-image, Build/tplink-v1-image, and Build/fit-image—that perform actual binary manipulation and header generation.
How does OpenWrt handle different root filesystem types?
The build system selects the appropriate Image/mkfs/* macro based on CONFIG_TARGET_ROOTFS_* options. For squashfs, it calls mksquashfs4; for ext4, it uses make_ext4fs; for UBI volumes, it invokes scripts/ubinize-image.sh. Each macro mounts the staged root directory and produces the corresponding binary format before the image assembly stage.
Where are device-specific image configurations stored?
Device definitions reside in target-specific Makefiles located at target/linux/<arch>/image/*.mk. These files invoke Device/Init to set metadata variables, define IMAGES and ARTIFACTS lists, and specify which Build/* functions from include/image-commands.mk should assemble the final binaries.
How is firmware metadata embedded into the final image?
The Build/append-metadata function (defined in include/image-commands.mk) calls scripts/json_add_image_info.py to generate a JSON description of the firmware. This includes version information, compatible devices, and a SHA-256 checksum. The metadata is appended to the binary after the kernel and rootfs sections, enabling sysupgrade to verify compatibility before flashing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →